product; $variation = $item->variation; if (!$variation || !$product) { return new \WP_Error('invalid_product', esc_html__('Invalid product or variation.', 'fluent-cart')); } $sitePrefix = Helper::getSitePrefix(); $planId = 'fct_pp_plan_' . $data['currency'] . '_' . $data['variation_id'] . '_' . $data['recurring_amount'] . '_' . $data['billing_interval'] . '_' . $data['interval_count'] . '_' . $data['trial_days'] . '_' . $data['signup_fee'] . '_' . $data['bill_times']; $planId = apply_filters('fluent_cart/paypal_plan_id', $planId, [ 'plan_data' => $data, 'variation' => $variation, 'product' => $product ]); $paypalPlan = null; if ($product && $product instanceof Product) { $paypalPlanId = $product->getProductMeta($planId); if ($paypalPlanId) { $paypalPlan = API::getResource('billing/plans/' . $paypalPlanId); if (!is_wp_error($paypalPlan)) { return $paypalPlan; } } } $paypalProduct = self::getRemoteProductByData([ 'id' => 'prod_' . $data['product_id'] . '_' . $sitePrefix, 'name' => sanitize_text_field($product->post_title), 'description' => sanitize_text_field($product->post_excerpt) ]); if (is_wp_error($paypalProduct)) { return $paypalProduct; } $planData = wp_parse_args($data, [ 'plan_name' => $product->post_title . ' - ' . $variation->variation_title, 'plan_description' => $product->post_title, 'paypal_product_id' => $paypalProduct['id'], ]); $paypalPlanArgs = self::getPayPalPlanArgs($planData); $paypalPlan = API::createResource('billing/plans', $paypalPlanArgs); if (is_wp_error($paypalPlan)) { return $paypalPlan; } if ($product && $product instanceof Product) { $product->updateProductMeta($planId, $paypalPlan['id']); } return $paypalPlan; } public static function processRemoteRefund($transaction, $amount, $args) { $chargeId = $transaction->vendor_charge_id; if (!$chargeId) { return new \WP_Error('invalid_charge_id', __('Please provide a valid charge Id!', 'fluent-cart')); } $refundData = [ 'custom_id' => $transaction->uuid, 'amount' => array( 'value' => self::formatAmount($amount, $transaction->currency), 'currency_code' => $transaction->currency ), ]; $reason = Arr::get($args, 'reason', ''); if ($reason) { $refundData['note_to_payer'] = substr($reason, 0, 255); } $vendorRefund = (new API())->makeRequest('payments/captures/' . $chargeId . '/refund', 'v2', 'POST', $refundData); if (is_wp_error($vendorRefund)) { return $vendorRefund; } if (Arr::get($vendorRefund, 'status') !== 'COMPLETED') { return new \WP_Error('refund_not_completed', __('Refund not completed in paypal. Please refund from PayPal Manually.', 'fluent-cart')); } return Arr::get($vendorRefund, 'id'); } private static function getRemoteProductByData($productData = []) { if (isset($productData['id']) && strlen($productData['id']) > 50) { $productData['id'] = substr($productData['id'], 0, 48); // PayPal product ID should be less than 50 characters } if (isset($productData['id'])) { $existingProduct = API::getResource('catalogs/products/' . $productData['id']); if (!is_wp_error($existingProduct)) { return $existingProduct; } } $formattedData = [ 'id' => Arr::get($productData, 'id'), 'name' => Arr::get($productData, 'name'), 'description' => Arr::get($productData, 'description'), 'type' => Arr::get($productData, 'type'), ]; $validTypes = ['PHYSICAL', 'DIGITAL', 'SERVICE']; if (!in_array($formattedData['type'], $validTypes)) { $formattedData['type'] = 'DIGITAL'; // Default to PHYSICAL if type is not valid } // name should be under 127 characters, add '...' if it is more than 127 characters if ($formattedData['name'] && strlen($formattedData['name']) > 127) { $formattedData['name'] = substr($formattedData['name'], 0, 120) . '...'; } // description should be under 256 characters, add '...' if it is more than 256 characters if ($formattedData['description'] && strlen($formattedData['description']) > 256) { $formattedData['description'] = substr($formattedData['description'], 0, 250) . '...'; } $formattedData = array_filter($formattedData); $createdProduct = API::createResource('catalogs/products', $formattedData); return $createdProduct; } private static function getPayPalPlanArgs($data = []) { // Default values $defaults = [ 'paypal_product_id' => '', 'trial_days' => 0, 'billing_interval' => 'monthly', 'currency' => 'USD', 'interval_count' => 1, 'recurring_amount' => 0, 'signup_fee' => 0, 'bill_times' => 0, 'plan_name' => __('Subscription', 'fluent-cart'), 'plan_description' => __('Subscription Plan', 'fluent-cart') ]; // Merge input data with defaults $data = wp_parse_args($data, $defaults); $data['product_id'] = $data['paypal_product_id']; $data['recurring_total'] = $data['recurring_amount']; // Convert amounts from cents to the currency's decimal precision $recurringCents = (int) $data['recurring_total']; $signupFeeCents = (int) $data['signup_fee']; $recurringAmount = self::toDecimalAmount($recurringCents, $data['currency']); $initialAmount = $signupFeeCents > 0 ? self::toDecimalAmount($signupFeeCents, $data['currency']) : 0; $hasSignupFee = $signupFeeCents > 0; // Map billing interval to PayPal API interval unit $interval_map = [ Status::BILLING_MONTHLY => 'MONTH', Status::BILLING_QUARTERLY => 'MONTH', Status::BILLING_HALF_YEARLY => 'MONTH', Status::BILLING_YEARLY => 'YEAR', Status::BILLING_WEEKLY => 'WEEK', Status::BILLING_DAILY => 'DAY' ]; $interval_unit = isset($interval_map[$data['billing_interval']]) ? $interval_map[$data['billing_interval']] : 'MONTH'; // Determine interval_count based on billing_interval $intervalCount = 1; if ($data['billing_interval'] === Status::BILLING_QUARTERLY) { $intervalCount = 3; } elseif ($data['billing_interval'] === Status::BILLING_HALF_YEARLY) { $intervalCount = 6; } $intervalCount = 1; if ($data['billing_interval'] === Status::BILLING_QUARTERLY) { $intervalCount = 3; } elseif ($data['billing_interval'] === Status::BILLING_HALF_YEARLY) { $intervalCount = 6; } $billingPeriod = [ 'interval_unit' => $interval_unit, 'interval_frequency' => $intervalCount, ]; $billingPeriod = apply_filters('fluent_cart/subscription_billing_period', $billingPeriod, [ 'subscription_interval' => $data['billing_interval'], 'payment_method' => 'paypal', ]); // Base plan structure $plan_name = $data['plan_name']; $plan_description = $data['plan_description']; $paypal_plan = [ 'product_id' => $data['product_id'], 'name' => $plan_name, 'description' => $plan_description, 'status' => 'ACTIVE', 'payment_preferences' => [ 'auto_bill_outstanding' => true, 'payment_failure_threshold' => 3 ] ]; $normalCycle = [ 'frequency' => [ 'interval_unit' => Arr::get($billingPeriod, 'interval_unit'), 'interval_count' => Arr::get($billingPeriod, 'interval_frequency'), ], 'tenure_type' => 'REGULAR', 'sequence' => 1, 'total_cycles' => $data['bill_times'], 'pricing_scheme' => [ 'fixed_price' => [ 'value' => self::formatDecimalAmount($recurringAmount, $data['currency']), 'currency_code' => $data['currency'] ] ] ]; $trialCycle = []; if ($data['trial_days'] > 0) { $trialCycle = [ 'tenure_type' => 'TRIAL', 'frequency' => [ 'interval_unit' => 'DAY', 'interval_count' => $data['trial_days'] ], 'sequence' => 1, 'pricing_scheme' => [ 'fixed_price' => [ 'value' => self::formatDecimalAmount(0, $data['currency']), 'currency_code' => $data['currency'] ] ], 'total_cycles' => 1 ]; $normalCycle['sequence'] = 2; // If there's a trial, the regular cycle sequence should be 2 } if ($hasSignupFee) { if ($trialCycle) { $trialCycle['pricing_scheme']['fixed_price']['value'] = self::formatDecimalAmount($initialAmount, $data['currency']); } else { $trialCycle = [ 'tenure_type' => 'TRIAL', 'frequency' => [ 'interval_unit' => $interval_unit, 'interval_count' => 1 ], 'sequence' => 1, 'pricing_scheme' => [ 'fixed_price' => [ // Sum the cents, then round once. Rounding the fee and the // recurring price separately overcharges by up to one minor // unit on a zero-decimal currency, and drops a fee that is // smaller than one. 'value' => self::formatDecimalAmount( self::toDecimalAmount($recurringCents + $signupFeeCents, $data['currency']), $data['currency'] ), 'currency_code' => $data['currency'] ] ], 'total_cycles' => 1 ]; $normalCycle['total_cycles'] = $data['bill_times'] > 1 ? $data['bill_times'] - 1 : $data['bill_times']; // this trial cycle is without trial days, it's an adjusted paid trial cycle to take signupFee with one transaction $normalCycle['sequence'] = 2; // If there's a trial, the regular cycle sequence should be 2 } } $cycles = []; if ($trialCycle) { $cycles[] = $trialCycle; } $cycles[] = $normalCycle; $paypal_plan['billing_cycles'] = $cycles; return $paypal_plan; } }