← All changes
|
app/Modules/PaymentMethods/PayPalGateway/Processor.php
+164
-37
1.6.0
→
1.6.6
View file →
| @@ -18,19 +18,76 @@ | ||
| 18 | 18 | use FluentCart\Framework\Support\Arr; |
| 19 | 19 | |
| 20 | 20 | class Processor |
| 21 | 21 | { |
| 22 | + /** | |
| 23 | + * Does this order-create error actually implicate the vault attributes? | |
| 24 | + * | |
| 25 | + * PayPal reports the offending field path in details[].field, which is the | |
| 26 | + * structural signal — it points straight at attributes/vault when the vault | |
| 27 | + * block is the problem, and elsewhere when it is not. details[].issue is | |
| 28 | + * matched too, against a deliberately small list: guessing broadly here would | |
| 29 | + * recreate the bug this method exists to prevent, so anything unrecognised is | |
| 30 | + * treated as unrelated and the error is returned untouched. | |
| 31 | + * | |
| 32 | + * The issue list is filterable because PayPal can introduce codes faster than | |
| 33 | + * a core release can follow, and a missing code should be correctable without | |
| 34 | + * one. | |
| 35 | + * | |
| 36 | + * @param mixed $error | |
| 37 | + * @return bool | |
| 38 | + */ | |
| 39 | + public static function isVaultRejection($error): bool | |
| 40 | + { | |
| 41 | + if (!is_wp_error($error)) { | |
| 42 | + return false; | |
| 43 | + } | |
| 44 | + | |
| 45 | + $body = $error->get_error_data(); | |
| 46 | + if (!is_array($body)) { | |
| 47 | + return false; | |
| 48 | + } | |
| 49 | + | |
| 50 | + $vaultIssues = apply_filters('fluent_cart/payments/paypal_vault_rejection_issues', [ | |
| 51 | + 'PAYMENT_SOURCE_CANNOT_BE_USED', | |
| 52 | + 'PAYMENT_SOURCE_NOT_VAULTABLE', | |
| 53 | + 'VAULTING_NOT_ENABLED', | |
| 54 | + 'MERCHANT_NOT_ENABLED_FOR_VAULTING', | |
| 55 | + 'VAULT_ID_NOT_SUPPORTED', | |
| 56 | + ]); | |
| 57 | + | |
| 58 | + foreach ((array) Arr::get($body, 'details', []) as $detail) { | |
| 59 | + if (!is_array($detail)) { | |
| 60 | + continue; | |
| 61 | + } | |
| 62 | + | |
| 63 | + // Structural: PayPal names the field it rejected. | |
| 64 | + $field = strtolower((string) Arr::get($detail, 'field', '')); | |
| 65 | + if ($field !== '' && strpos($field, 'vault') !== false) { | |
| 66 | + return true; | |
| 67 | + } | |
| 68 | + | |
| 69 | + $issue = strtoupper((string) Arr::get($detail, 'issue', '')); | |
| 70 | + if ($issue !== '' && in_array($issue, $vaultIssues, true)) { | |
| 71 | + return true; | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 75 | + return false; | |
| 76 | + } | |
| 77 | + | |
| 22 | 78 | public function handleSinglePayment(PaymentInstance $paymentInstance, $args = []) |
| 23 | 79 | { |
| 24 | 80 | $transaction = $paymentInstance->transaction; |
| 25 | 81 | $order = $paymentInstance->order; |
| 26 | 82 | |
| 83 | + $currency = $transaction->currency; | |
| 27 | 84 | $itemsSubTotal = 0; |
| 28 | 85 | $formattedItems = []; |
| 29 | 86 | |
| 30 | 87 | foreach ($order->order_items as $item) { |
| 31 | 88 | $quantity = $item->quantity ?? 1; |
| 32 | - $perQuantity = $this->toDecimal($item->line_total / $quantity); | |
| 89 | + $perQuantity = $this->toDecimal($item->line_total / $quantity, $currency); | |
| 33 | 90 | $title = $item->post_title . ' ' . $item->title; |
| 34 | 91 | |
| 35 | 92 | $formattedItems[] = [ |
| 36 | 93 | 'name' => strlen($title) > 127 ? substr($title, 0, 120) . '...' : $title, |
| @@ -35,10 +92,10 @@ | ||
| 35 | 92 | $formattedItems[] = [ |
| 36 | 93 | 'name' => strlen($title) > 127 ? substr($title, 0, 120) . '...' : $title, |
| 37 | 94 | 'description' => strlen($title) > 4000 ? substr($title, 0, 3997) . '...' : $title, |
| 38 | 95 | 'unit_amount' => [ |
| 39 | - 'currency_code' => $transaction->currency, | |
| 40 | - 'value' => number_format($perQuantity, 2, '.', ''), | |
| 96 | + 'currency_code' => $currency, | |
| 97 | + 'value' => PayPalHelper::formatDecimalAmount($perQuantity, $currency), | |
| 41 | 98 | ], |
| 42 | 99 | 'quantity' => $quantity, |
| 43 | 100 | ]; |
| 44 | 101 | |
| @@ -44,9 +101,9 @@ | ||
| 44 | 101 | |
| 45 | 102 | $itemsSubTotal += $perQuantity * $quantity; |
| 46 | 103 | } |
| 47 | 104 | |
| 48 | - $chargingAmount = $this->toDecimal($transaction->total); | |
| 105 | + $chargingAmount = $this->toDecimal($transaction->total, $currency); | |
| 49 | 106 | $pushedTotal = $itemsSubTotal; |
| 50 | 107 | |
| 51 | 108 | |
| 52 | 109 | // Learn more at: https://developer.paypal.com/docs/api/orders/v2/#definition-purchase_unit |
| @@ -52,14 +109,14 @@ | ||
| 52 | 109 | // Learn more at: https://developer.paypal.com/docs/api/orders/v2/#definition-purchase_unit |
| 53 | 110 | $purchaseUnits = [ |
| 54 | 111 | 'reference_id' => $transaction->uuid, // This is the order UUID |
| 55 | 112 | 'amount' => [ // https://developer.paypal.com/docs/api/orders/v2/#definition-amount_breakdown |
| 56 | - 'currency_code' => $transaction->currency, | |
| 57 | - 'value' => number_format($chargingAmount, 2, '.', ''), | |
| 113 | + 'currency_code' => $currency, | |
| 114 | + 'value' => PayPalHelper::formatDecimalAmount($chargingAmount, $currency), | |
| 58 | 115 | 'breakdown' => [ |
| 59 | 116 | 'item_total' => [ |
| 60 | - 'currency_code' => $transaction->currency, | |
| 61 | - 'value' => number_format($itemsSubTotal, 2, '.', ''), | |
| 117 | + 'currency_code' => $currency, | |
| 118 | + 'value' => PayPalHelper::formatDecimalAmount($itemsSubTotal, $currency), | |
| 62 | 119 | ] |
| 63 | 120 | ] |
| 64 | 121 | ], |
| 65 | 122 | 'items' => $formattedItems |
| @@ -76,12 +133,12 @@ | ||
| 76 | 133 | } |
| 77 | 134 | } |
| 78 | 135 | |
| 79 | 136 | if ($order->shipping_total > 0) { |
| 80 | - $shippingAmount = $this->toDecimal($order->shipping_total); | |
| 137 | + $shippingAmount = $this->toDecimal($order->shipping_total, $currency); | |
| 81 | 138 | $purchaseUnits['amount']['breakdown']['shipping'] = [ |
| 82 | - 'currency_code' => $transaction->currency, | |
| 83 | - 'value' => number_format($shippingAmount, 2, '.', ''), | |
| 139 | + 'currency_code' => $currency, | |
| 140 | + 'value' => PayPalHelper::formatDecimalAmount($shippingAmount, $currency), | |
| 84 | 141 | ]; |
| 85 | 142 | $pushedTotal += $shippingAmount; |
| 86 | 143 | } |
| 87 | 144 | |
| @@ -99,16 +156,16 @@ | ||
| 99 | 156 | |
| 100 | 157 | if ($taxBehavior === 1) { |
| 101 | 158 | // Pure exclusive: all tax is additive on top of item prices. |
| 102 | 159 | // tax_total includes product + fee tax (both exclusive). |
| 103 | - $taxTotal = $this->toDecimal($order->tax_total) + $this->toDecimal($order->shipping_tax); | |
| 160 | + $taxTotal = $this->toDecimal($order->tax_total, $currency) + $this->toDecimal($order->shipping_tax, $currency); | |
| 104 | 161 | } elseif ($taxBehavior === 3) { |
| 105 | 162 | // Mixed: only exclusive product + fee tax is additive; shipping conditional. |
| 106 | - $taxTotal = $this->toDecimal($exclusiveTaxTotal); | |
| 163 | + $taxTotal = $this->toDecimal($exclusiveTaxTotal, $currency); | |
| 107 | 164 | if ($storeTaxBehavior === 1) { |
| 108 | 165 | // Store is exclusive: fees and shipping are also exclusive. |
| 109 | - $taxTotal += $this->toDecimal($order->shipping_tax); | |
| 110 | - $taxTotal += $this->toDecimal($feeTax); | |
| 166 | + $taxTotal += $this->toDecimal($order->shipping_tax, $currency); | |
| 167 | + $taxTotal += $this->toDecimal($feeTax, $currency); | |
| 111 | 168 | } |
| 112 | 169 | } else { |
| 113 | 170 | $taxTotal = 0; |
| 114 | 171 | } |
| @@ -114,10 +171,10 @@ | ||
| 114 | 171 | } |
| 115 | 172 | |
| 116 | 173 | if ($taxTotal > 0) { |
| 117 | 174 | $purchaseUnits['amount']['breakdown']['tax_total'] = [ |
| 118 | - 'currency_code' => $transaction->currency, | |
| 119 | - 'value' => number_format($taxTotal, 2, '.', ''), | |
| 175 | + 'currency_code' => $currency, | |
| 176 | + 'value' => PayPalHelper::formatDecimalAmount($taxTotal, $currency), | |
| 120 | 177 | ]; |
| 121 | 178 | $pushedTotal += $taxTotal; |
| 122 | 179 | } |
| 123 | 180 | |
| @@ -123,10 +180,10 @@ | ||
| 123 | 180 | |
| 124 | 181 | if ($chargingAmount < $pushedTotal) { |
| 125 | 182 | $discount = $pushedTotal - $chargingAmount; |
| 126 | 183 | $purchaseUnits['amount']['breakdown']['discount'] = [ |
| 127 | - 'currency_code' => $transaction->currency, | |
| 128 | - 'value' => number_format($discount, 2, '.', ''), | |
| 184 | + 'currency_code' => $currency, | |
| 185 | + 'value' => PayPalHelper::formatDecimalAmount($discount, $currency), | |
| 129 | 186 | ]; |
| 130 | 187 | } else if ($chargingAmount > $pushedTotal) { |
| 131 | 188 | $extraChargeNeedToBeAdded = $chargingAmount - $pushedTotal; |
| 132 | 189 | $formattedItems[] = [ |
| @@ -131,10 +188,10 @@ | ||
| 131 | 188 | $extraChargeNeedToBeAdded = $chargingAmount - $pushedTotal; |
| 132 | 189 | $formattedItems[] = [ |
| 133 | 190 | 'name' => __('Adjustment Amount', 'fluent-cart'), |
| 134 | 191 | 'unit_amount' => [ |
| 135 | - 'currency_code' => $transaction->currency, | |
| 136 | - 'value' => number_format($extraChargeNeedToBeAdded, 2, '.', ''), | |
| 192 | + 'currency_code' => $currency, | |
| 193 | + 'value' => PayPalHelper::formatDecimalAmount($extraChargeNeedToBeAdded, $currency), | |
| 137 | 194 | ], |
| 138 | 195 | 'quantity' => 1, |
| 139 | 196 | ]; |
| 140 | 197 | |
| @@ -141,9 +198,9 @@ | ||
| 141 | 198 | $purchaseUnits['items'] = $formattedItems; |
| 142 | 199 | |
| 143 | 200 | //now the total amount need to be adjusted with item total value |
| 144 | 201 | $adjustedItemTotal = $itemsSubTotal + $extraChargeNeedToBeAdded; |
| 145 | - $purchaseUnits['amount']['breakdown']['item_total']['value'] = number_format($adjustedItemTotal, 2, '.', ''); | |
| 202 | + $purchaseUnits['amount']['breakdown']['item_total']['value'] = PayPalHelper::formatDecimalAmount($adjustedItemTotal, $currency); | |
| 146 | 203 | } |
| 147 | 204 | |
| 148 | 205 | // System (auto-charged, store-billed) subscription checkout: vault the |
| 149 | 206 | // buyer's PayPal account during this purchase (Vault v3 save-on-success) |
| @@ -148,10 +205,26 @@ | ||
| 148 | 205 | // System (auto-charged, store-billed) subscription checkout: vault the |
| 149 | 206 | // buyer's PayPal account during this purchase (Vault v3 save-on-success) |
| 150 | 207 | // so future renewal invoices can be charged merchant-initiated. The buyer |
| 151 | 208 | // sees and approves the save agreement inside PayPal's own approval UI. |
| 209 | + // Vaulting on a plain one-time order cannot be requested from outside core: | |
| 210 | + // the vault_attributes filter below fires only once this branch is already | |
| 211 | + // taken, so it can shape a vault but never ask for one. This filter is the | |
| 212 | + // PayPal counterpart of fluent_cart/payments/stripe_onetime_intent_args, and | |
| 213 | + // it is what lets the saved-payment-methods module vault on buyer consent. | |
| 214 | + // Defaults to the existing value, so with no listener behaviour is unchanged. | |
| 215 | + $vaultOnSuccess = apply_filters( | |
| 216 | + 'fluent_cart/payments/paypal_vault_one_time', | |
| 217 | + !empty($args['vault_on_success']), | |
| 218 | + [ | |
| 219 | + 'order' => $order, | |
| 220 | + 'transaction' => $transaction, | |
| 221 | + 'subscription' => $paymentInstance->subscription, | |
| 222 | + ] | |
| 223 | + ); | |
| 224 | + | |
| 152 | 225 | $extraBody = []; |
| 153 | - if (!empty($args['vault_on_success'])) { | |
| 226 | + if ($vaultOnSuccess) { | |
| 154 | 227 | $vaultAttributes = apply_filters('fluent_cart/paypal/vault_attributes', [ |
| 155 | 228 | 'store_in_vault' => 'ON_SUCCESS', |
| 156 | 229 | 'usage_type' => 'MERCHANT', |
| 157 | 230 | 'customer_type' => 'CONSUMER', |
| @@ -173,8 +246,32 @@ | ||
| 173 | 246 | } |
| 174 | 247 | |
| 175 | 248 | $paypalOrder = API::createOrder($purchaseUnits, $extraBody); |
| 176 | 249 | |
| 250 | + // Vaulting is a convenience; the purchase is the point. A merchant account | |
| 251 | + // not approved for vaulting can reject the order outright because of the | |
| 252 | + // vault attributes, and failing the sale over a save the buyer merely | |
| 253 | + // opted into would be the wrong trade. Retry once without them and let | |
| 254 | + // listeners record that this account cannot vault, so the saving UI can | |
| 255 | + // stop being offered instead of failing silently on every order. | |
| 256 | + // | |
| 257 | + // ONLY for an error that actually implicates the vault attributes. An auth | |
| 258 | + // failure, rate limit, malformed amount or transport error is not evidence | |
| 259 | + // that this account cannot vault: retrying would not fix it, and telling a | |
| 260 | + // listener otherwise would switch saving off for a perfectly capable | |
| 261 | + // account on the strength of an unrelated outage. | |
| 262 | + if (is_wp_error($paypalOrder) && $vaultOnSuccess && self::isVaultRejection($paypalOrder)) { | |
| 263 | + do_action('fluent_cart/payments/paypal_vault_rejected', [ | |
| 264 | + 'order' => $order, | |
| 265 | + 'transaction' => $transaction, | |
| 266 | + 'error' => $paypalOrder, | |
| 267 | + ]); | |
| 268 | + | |
| 269 | + unset($extraBody['payment_source']['paypal']['attributes']); | |
| 270 | + | |
| 271 | + $paypalOrder = API::createOrder($purchaseUnits, $extraBody); | |
| 272 | + } | |
| 273 | + | |
| 177 | 274 | if (is_wp_error($paypalOrder)) { |
| 178 | 275 | return $paypalOrder; |
| 179 | 276 | } |
| 180 | 277 | |
| @@ -440,15 +537,14 @@ | ||
| 440 | 537 | 'vendor_plan_id' => Arr::get($paypalPlan, 'id'), |
| 441 | 538 | 'vendor_response' => json_encode($paypalPlan, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) |
| 442 | 539 | ]; |
| 443 | 540 | |
| 541 | + $subscription->update($subscriptionUpdateFields); | |
| 542 | + | |
| 444 | 543 | if ($orderType == 'renewal' && !empty($data['trial_days'])) { |
| 445 | - $config = $subscription->config ?: []; | |
| 446 | - $subscriptionUpdateFields['config'] = array_merge($config, ['is_trial_days_simulated' => 'yes']); | |
| 544 | + $subscription->mergeConfig(['is_trial_days_simulated' => 'yes']); | |
| 447 | 545 | } |
| 448 | 546 | |
| 449 | - $subscription->update($subscriptionUpdateFields); | |
| 450 | - | |
| 451 | 547 | return [ |
| 452 | 548 | 'status' => 'success', |
| 453 | 549 | 'nextAction' => 'paypal', |
| 454 | 550 | 'actionName' => 'custom', |
| @@ -514,8 +610,24 @@ | ||
| 514 | 610 | } |
| 515 | 611 | |
| 516 | 612 | $transactionUpdateData['meta'] = array_merge($transaction->meta ?? [], Arr::get($transactionArgs, 'meta', [])); |
| 517 | 613 | |
| 614 | + // A zero-decimal total is stored x100 but charged rounded, so PayPal reports back a | |
| 615 | + // figure up to half a unit away from the stored one. The wire comparison upstream has | |
| 616 | + // already proved this is the same payment. Keep the stored number: it is what the | |
| 617 | + // order's line items sum to, so adopting the rounded one would either strand the order | |
| 618 | + // partially_paid (rounded down) or fake an overpayment (rounded up). Record what | |
| 619 | + // actually moved in meta instead. activateSubscription() already leaves total alone. | |
| 620 | + $reportedTotal = (int)Arr::get($transactionUpdateData, 'total', 0); | |
| 621 | + if ($reportedTotal | |
| 622 | + && $reportedTotal !== (int)$transaction->total | |
| 623 | + && PayPalHelper::currencyDecimals($transaction->currency) === 0 | |
| 624 | + && $reportedTotal === PayPalHelper::wireCents($transaction->total, $transaction->currency) | |
| 625 | + ) { | |
| 626 | + unset($transactionUpdateData['total']); | |
| 627 | + $transactionUpdateData['meta']['wire_total'] = $reportedTotal; | |
| 628 | + } | |
| 629 | + | |
| 518 | 630 | $transaction->fill($transactionUpdateData); |
| 519 | 631 | $transaction->save(); |
| 520 | 632 | |
| 521 | 633 | fluent_cart_add_log(__('PayPal Payment Confirmation', 'fluent-cart'), __('Payment confirmation received from PayPal. Transaction ID: ', 'fluent-cart') . Arr::get($transactionArgs, 'vendor_charge_id', ''), 'info', [ |
| @@ -601,10 +713,12 @@ | ||
| 601 | 713 | // never deliver a paid product for free. |
| 602 | 714 | $currencyMatches = !$lastPaymentCurrency || !$transaction->currency |
| 603 | 715 | || strtoupper($transaction->currency) === $lastPaymentCurrency; |
| 604 | 716 | |
| 717 | + $expectedAmount = PayPalHelper::wireCents($transaction->total, $transaction->currency); | |
| 718 | + | |
| 605 | 719 | $initialPaymentVerified = $lastPaymentAmount |
| 606 | - && $transaction->total == $lastPaymentAmount | |
| 720 | + && $expectedAmount == $lastPaymentAmount | |
| 607 | 721 | && $currencyMatches; |
| 608 | 722 | |
| 609 | 723 | if ($initialPaymentVerified || $transaction->total == 0) { |
| 610 | 724 | $transactionUpdateData = array_filter([ |
| @@ -622,9 +736,9 @@ | ||
| 622 | 736 | __('PayPal Subscription Payment Mismatch', 'fluent-cart'), |
| 623 | 737 | sprintf( |
| 624 | 738 | /* translators: %1$s: expected amount, %2$s: expected currency, %3$s: received amount, %4$s: received currency */ |
| 625 | 739 | __('Subscription initial payment mismatch. Expected: %1$s %2$s, Received: %3$s %4$s. Order not marked paid; awaiting webhook.', 'fluent-cart'), |
| 626 | - Helper::toDecimal($transaction->total), | |
| 740 | + Helper::toDecimal($expectedAmount), | |
| 627 | 741 | $transaction->currency, |
| 628 | 742 | Helper::toDecimal($lastPaymentAmount), |
| 629 | 743 | $lastPaymentCurrency |
| 630 | 744 | ), |
| @@ -708,11 +822,16 @@ | ||
| 708 | 822 | return $subscriptionModel; |
| 709 | 823 | } |
| 710 | 824 | |
| 711 | 825 | |
| 712 | - private function toDecimal($cents) | |
| 826 | + /** | |
| 827 | + * Cents to a decimal amount rounded to the precision PayPal accepts for | |
| 828 | + * the currency (HUF/JPY/TWD take no decimals). Rounding before the | |
| 829 | + * breakdown arithmetic keeps the parts summing to the total. | |
| 830 | + */ | |
| 831 | + private function toDecimal($cents, $currency) | |
| 713 | 832 | { |
| 714 | - return Helper::toDecimalWithoutComma($cents); | |
| 833 | + return PayPalHelper::toDecimalAmount($cents, $currency); | |
| 715 | 834 | } |
| 716 | 835 | |
| 717 | 836 | /** |
| 718 | 837 | * Persist the vaulted PayPal payment token from a captured order onto the |
| @@ -830,9 +949,9 @@ | ||
| 830 | 949 | 'reference_id' => $transaction->uuid, |
| 831 | 950 | 'custom_id' => $transaction->uuid, |
| 832 | 951 | 'amount' => [ |
| 833 | 952 | 'currency_code' => strtoupper($transaction->currency), |
| 834 | - 'value' => number_format($this->toDecimal((int) $transaction->total), 2, '.', ''), | |
| 953 | + 'value' => PayPalHelper::formatAmount((int) $transaction->total, $transaction->currency), | |
| 835 | 954 | ], |
| 836 | 955 | ]; |
| 837 | 956 | |
| 838 | 957 | $paypalOrder = API::createOrder($purchaseUnit, [ |
| @@ -839,9 +958,9 @@ | ||
| 839 | 958 | 'payment_source' => ['paypal' => ['vault_id' => $token]], |
| 840 | 959 | ], [ |
| 841 | 960 | // One vendor charge per (order, attempt) — a scheduler double-fire |
| 842 | 961 | // replays the original response instead of charging twice. |
| 843 | - 'PayPal-Request-Id' => 'fct_system_charge_' . $order->id . '_' . $attempt, | |
| 962 | + 'PayPal-Request-Id' => 'fct_system_charge_' . $order->uuid . '_' . $attempt, | |
| 844 | 963 | ]); |
| 845 | 964 | |
| 846 | 965 | if (is_wp_error($paypalOrder)) { |
| 847 | 966 | return $paypalOrder; |
| @@ -942,15 +1061,17 @@ | ||
| 942 | 1061 | return new \WP_Error('currency_mismatch', __('The PayPal payment currency does not match this transaction. Please verify the payment at PayPal.', 'fluent-cart')); |
| 943 | 1062 | } |
| 944 | 1063 | |
| 945 | 1064 | $captureAmount = Helper::toCent(Arr::get($capture, 'amount.value', 0)); |
| 946 | - if ($captureAmount !== (int) $transaction->total) { | |
| 1065 | + $expectedAmount = PayPalHelper::wireCents($transaction->total, $transaction->currency); | |
| 1066 | + | |
| 1067 | + if ($captureAmount !== $expectedAmount) { | |
| 947 | 1068 | fluent_cart_warning_log( |
| 948 | 1069 | __('PayPal Amount Mismatch On Sync', 'fluent-cart'), |
| 949 | 1070 | sprintf( |
| 950 | 1071 | /* translators: %1$s: expected amount, %2$s: received amount */ |
| 951 | 1072 | __('Capture amount mismatch detected during transaction sync. Expected: %1$s, Received: %2$s. Transaction was not confirmed.', 'fluent-cart'), |
| 952 | - Helper::toDecimal($transaction->total), | |
| 1073 | + Helper::toDecimal($expectedAmount), | |
| 953 | 1074 | Helper::toDecimal($captureAmount) |
| 954 | 1075 | ), |
| 955 | 1076 | [ |
| 956 | 1077 | 'module_name' => 'order', |
| @@ -992,11 +1113,17 @@ | ||
| 992 | 1113 | * ids for reconciliation, confirm completed captures through the normal |
| 993 | 1114 | * capture path, report settling captures as 'processing', everything else as |
| 994 | 1115 | * a definitive failure with PayPal's reason. |
| 995 | 1116 | * |
| 996 | - * @return true|string|\WP_Error | |
| 1117 | + * Public so an extension charging a vaulted token outside the renewal engine | |
| 1118 | + * (saved payment methods) settles through this exact contract rather than | |
| 1119 | + * reimplementing it. The PENDING branch in particular is money-critical: a | |
| 1120 | + * settling eCheck is neither paid nor failed, and a duplicate of this logic | |
| 1121 | + * would eventually drift and mis-report one. | |
| 1122 | + * | |
| 1123 | + * @return true|string|\WP_Error true = captured, 'processing' = settling | |
| 997 | 1124 | */ |
| 998 | - private function settleVaultChargeResponse(OrderTransaction $transaction, $paypalOrder) | |
| 1125 | + public function settleVaultChargeResponse(OrderTransaction $transaction, $paypalOrder) | |
| 999 | 1126 | { |
| 1000 | 1127 | $orderStatus = strtoupper((string) Arr::get($paypalOrder, 'status', '')); |
| 1001 | 1128 | $capture = Arr::get($paypalOrder, 'purchase_units.0.payments.captures.0', []); |
| 1002 | 1129 | $captureId = Arr::get($capture, 'id', ''); |