'currency_code' => get_woocommerce_currency(), ], 'setup_fee_failure_action' => 'CANCEL', 'payment_failure_threshold' => 1, ], 'taxes' => [ 'percentage' => '0', 'inclusive' => wc_prices_include_tax(), ], ]; if ( $order && $order->get_total_tax() ) { $percentage = static::get_percentage( $order->get_subtotal(), $order->get_total_tax() ); $plan_data['taxes'] = [ 'percentage' => $percentage, //paypal only support percentage 'inclusive' => true, ]; } // finally create subscription plan $processor = SubscriptionProcessor::init(); $created_plan = $processor->create_plan( $plan_data ); if ( is_wp_error( $created_plan ) ) { static::log_paypal_error( $product->get_id(), $created_plan, 'subscription_plan_create' ); return $created_plan; } //store plan id for later use if ( $order ) { $order->update_meta_data( '_dokan_paypal_marketplace_subscription_plan_id', $created_plan['id'] ); $order->save(); } return $created_plan['id']; } /** * Get paypal formatted billing cycles from product id * * @param int $product_id Product ID * @param \WC_Order|int $order Order ID * @param int|null $vendor_id Vendor ID * * @see https://developer.paypal.com/docs/api/subscriptions/v1/#definition-billing_cycle * * @since 3.3.7 * * @return WP_Error|array */ protected static function get_billing_cycles( $product_id, $order, $vendor_id = null ) { // try getting order object if ( ! is_a( $order, 'WC_Abstract_Order' ) ) { $order = wc_get_order( $order ); } if ( ! $order ) { return new WP_Error( 'invalid-order', __( 'Can not create subscription plan under PayPal due to invalid order.', 'dokan' ) ); } // try getting vendor id if ( null === $vendor_id && $order instanceof \WC_Abstract_Order ) { $vendor_id = $order->get_customer_id(); } // get SubscriptionPack object $subscription = new SubscriptionPack( $product_id, $vendor_id ); $vendor_used_trial = false; // if vendor already has used a trial pack, create a new plan without trial period if ( ! empty( $vendor_id ) && SubscriptionHelper::has_used_trial_pack( $vendor_id ) ) { $vendor_used_trial = true; } if ( ! $subscription->is_recurring() ) { return []; } $billing_cycles = []; $billing_sequence = 1; //maybe add trial billing details if ( $subscription->is_trial() && ! $vendor_used_trial ) { $trial_interval_unit = $subscription->get_trial_period_types(); //day, week, month, year $trial_interval_count = absint( $subscription->get_trial_range() ); //int $trial_interval_count = static::validate_subscription_frequency( $trial_interval_unit, $trial_interval_count ); $billing_cycles[] = static::format_subscription_billing_cycle_data( [ 'interval_unit' => $trial_interval_unit, 'interval_count' => $trial_interval_count, 'sequence' => $billing_sequence++, 'tenure_type' => 'TRIAL', 'price' => 0, 'total_cycles' => 1, ] ); } $product_pack = $subscription->get_product(); $subscription_total_cycles = $subscription->get_period_length(); // Billing cycle stop, 0 (zero) for never stops $subscription_interval_unit = $subscription->get_period_type(); // interval_unit: day, week, month, year $subscription_interval_count = $subscription->get_recurring_interval(); // interval_count: int $subscription_interval_count = static::validate_subscription_frequency( $subscription_interval_unit, $subscription_interval_count ); //discount added? if ( $order instanceof \WC_Abstract_Order && $order->get_discount_total() ) { // seller used discount, discounted amount will apply as trial into trial $billing_cycles[] = static::format_subscription_billing_cycle_data( [ 'interval_unit' => $subscription_interval_unit, 'interval_count' => $subscription_interval_count, 'sequence' => $billing_sequence++, 'tenure_type' => 'TRIAL', 'price' => $order->get_total(), 'total_cycles' => 1, ] ); } // for regular billing cycle $order_items = $order->get_items( 'line_item' ); /** * @var \WC_Order_Item_Product $order_item */ $order_item = current( $order_items ); if ( ! $order_item instanceof \WC_Order_Item_Product ) { return new WP_Error( 'invalid-order-item', __( 'Invalid Order Item Found.', 'dokan' ) ); } // calculate price from order item, this is because discount will not apply on recurring payment $price = (float) $order_item->get_subtotal( 'edit' ) + (float) $order_item->get_subtotal_tax( 'edit' ); $billing_cycles[] = static::format_subscription_billing_cycle_data( [ 'interval_unit' => $subscription_interval_unit, 'interval_count' => $subscription_interval_count, 'sequence' => $billing_sequence++, 'tenure_type' => 'REGULAR', 'price' => $price, 'total_cycles' => $subscription_total_cycles, ] ); return $billing_cycles; } /** * Format and make a complete billing cycle data * * @see https://developer.paypal.com/docs/api/subscriptions/v1/#definition-billing_cycle * * @param array $data holds all the necessary param to make the billing cycle data * * @return array */ public static function format_subscription_billing_cycle_data( $data ) { $billing_cycle_data = [ 'frequency' => [ 'interval_unit' => isset( $data['interval_unit'] ) ? strtoupper( $data['interval_unit'] ) : 'WEEK', 'interval_count' => isset( $data['interval_count'] ) ? $data['interval_count'] : 1, ], 'tenure_type' => isset( $data['tenure_type'] ) ? strtoupper( $data['tenure_type'] ) : 'REGULAR', 'sequence' => isset( $data['sequence'] ) ? $data['sequence'] : 1, 'total_cycles' => isset( $data['total_cycles'] ) ? $data['total_cycles'] : 0, ]; if ( isset( $data['price'] ) ) { $billing_cycle_data['pricing_scheme'] = [ 'fixed_price' => [ 'value' => wc_format_decimal( $data['price'], 2 ), 'currency_code' => isset( $data['currency'] ) ? $data['currency'] : get_woocommerce_currency(), ], ]; } return $billing_cycle_data; } /** * Frequency validation * * @see https://developer.paypal.com/docs/api/subscriptions/v1/#definition-frequency * * @param string $trial_interval_unit * @param int $trial_interval_count * * @since 3.3.7 * * @return int */ public static function validate_subscription_frequency( $trial_interval_unit, $trial_interval_count ) { switch ( strtoupper( $trial_interval_unit ) ) { case 'DAY': if ( $trial_interval_count > 365 ) { $trial_interval_count = 365; } break; case 'WEEK': if ( $trial_interval_count > 52 ) { $trial_interval_count = 52; } break; case 'MONTH': if ( $trial_interval_count > 12 ) { $trial_interval_count = 12; } break; case 'YEAR': if ( $trial_interval_count > 1 ) { $trial_interval_count = 1; } break; } return $trial_interval_count; } /** * Set Order status to Cancelled with an order note * * @param \WC_Order $order * @param string $status * @param string|null $note * * @since 3.3.7 * * @return void */ public static function update_order_status( $order, $status = 'cancelled', $note = null ) { if ( ! is_a( $order, 'WC_Abstract_Order' ) ) { return; } $note = ! empty( $note ) ? $note : __( 'Subscription Cancelled.', 'dokan' ); $order->add_order_note( $note ); $order->set_status( $status ); $order->save(); } /** * Validates cart contents to ensure they're allowed to be paid through Stripe Express. * * @since 3.11.4 * * @return bool */ public static function validate_cart_items(): bool { $is_valid = true; /* * This payment method can't be used if a Vendor is not connected to Stripe express. So we need to traverse all the cart items to check if any vendor is not connected. * * @since 3.11.4 * * @param boolean $is_valid Default is true */ if ( apply_filters( 'dokan_paypal_marketplace_needs_cart_validation', true ) && ! empty( WC()->cart->cart_contents ) ) { foreach ( WC()->cart->get_cart_contents() as $item ) { $product_id = $item['data']->get_id(); /* * If it contains vendor subscription product, * we don't need to check whether the vendor is connected or not. * Because in this case, the vendors themselves are customers. */ if ( class_exists(SubscriptionHelper::class) && SubscriptionHelper::is_subscription_product( $product_id ) ) { $is_valid = true; break; } // Get vendor id from product id $vendor_id = dokan_get_vendor_by_product( $product_id, true ); if ( ! $vendor_id ) { $is_valid = false; break; } /* * If any vendor is not registered for a Dokan PayPal Marketplace account, * and/or not enabled for payouts, the gateway is not available for checkout. */ if ( ! self::is_seller_enable_for_receive_payment( $vendor_id ) ) { $is_valid = false; break; } } } /** * Filter to validate cart items. * It can be used in case of any other validation logic. * * @since 3.11.4 * * @param boolean $is_valid Default is true */ return apply_filters( 'dokan_paypal_marketplace_validate_cart_items', $is_valid ); } }