← All changes
|
app/Modules/PaymentMethods/PayPalGateway/Processor.php
+56
-30
1.6.1
→
1.6.5
View file →
| @@ -79,14 +79,15 @@ | ||
| 79 | 79 | { |
| 80 | 80 | $transaction = $paymentInstance->transaction; |
| 81 | 81 | $order = $paymentInstance->order; |
| 82 | 82 | |
| 83 | + $currency = $transaction->currency; | |
| 83 | 84 | $itemsSubTotal = 0; |
| 84 | 85 | $formattedItems = []; |
| 85 | 86 | |
| 86 | 87 | foreach ($order->order_items as $item) { |
| 87 | 88 | $quantity = $item->quantity ?? 1; |
| 88 | - $perQuantity = $this->toDecimal($item->line_total / $quantity); | |
| 89 | + $perQuantity = $this->toDecimal($item->line_total / $quantity, $currency); | |
| 89 | 90 | $title = $item->post_title . ' ' . $item->title; |
| 90 | 91 | |
| 91 | 92 | $formattedItems[] = [ |
| 92 | 93 | 'name' => strlen($title) > 127 ? substr($title, 0, 120) . '...' : $title, |
| @@ -91,10 +92,10 @@ | ||
| 91 | 92 | $formattedItems[] = [ |
| 92 | 93 | 'name' => strlen($title) > 127 ? substr($title, 0, 120) . '...' : $title, |
| 93 | 94 | 'description' => strlen($title) > 4000 ? substr($title, 0, 3997) . '...' : $title, |
| 94 | 95 | 'unit_amount' => [ |
| 95 | - 'currency_code' => $transaction->currency, | |
| 96 | - 'value' => number_format($perQuantity, 2, '.', ''), | |
| 96 | + 'currency_code' => $currency, | |
| 97 | + 'value' => PayPalHelper::formatDecimalAmount($perQuantity, $currency), | |
| 97 | 98 | ], |
| 98 | 99 | 'quantity' => $quantity, |
| 99 | 100 | ]; |
| 100 | 101 | |
| @@ -100,9 +101,9 @@ | ||
| 100 | 101 | |
| 101 | 102 | $itemsSubTotal += $perQuantity * $quantity; |
| 102 | 103 | } |
| 103 | 104 | |
| 104 | - $chargingAmount = $this->toDecimal($transaction->total); | |
| 105 | + $chargingAmount = $this->toDecimal($transaction->total, $currency); | |
| 105 | 106 | $pushedTotal = $itemsSubTotal; |
| 106 | 107 | |
| 107 | 108 | |
| 108 | 109 | // Learn more at: https://developer.paypal.com/docs/api/orders/v2/#definition-purchase_unit |
| @@ -108,14 +109,14 @@ | ||
| 108 | 109 | // Learn more at: https://developer.paypal.com/docs/api/orders/v2/#definition-purchase_unit |
| 109 | 110 | $purchaseUnits = [ |
| 110 | 111 | 'reference_id' => $transaction->uuid, // This is the order UUID |
| 111 | 112 | 'amount' => [ // https://developer.paypal.com/docs/api/orders/v2/#definition-amount_breakdown |
| 112 | - 'currency_code' => $transaction->currency, | |
| 113 | - 'value' => number_format($chargingAmount, 2, '.', ''), | |
| 113 | + 'currency_code' => $currency, | |
| 114 | + 'value' => PayPalHelper::formatDecimalAmount($chargingAmount, $currency), | |
| 114 | 115 | 'breakdown' => [ |
| 115 | 116 | 'item_total' => [ |
| 116 | - 'currency_code' => $transaction->currency, | |
| 117 | - 'value' => number_format($itemsSubTotal, 2, '.', ''), | |
| 117 | + 'currency_code' => $currency, | |
| 118 | + 'value' => PayPalHelper::formatDecimalAmount($itemsSubTotal, $currency), | |
| 118 | 119 | ] |
| 119 | 120 | ] |
| 120 | 121 | ], |
| 121 | 122 | 'items' => $formattedItems |
| @@ -132,12 +133,12 @@ | ||
| 132 | 133 | } |
| 133 | 134 | } |
| 134 | 135 | |
| 135 | 136 | if ($order->shipping_total > 0) { |
| 136 | - $shippingAmount = $this->toDecimal($order->shipping_total); | |
| 137 | + $shippingAmount = $this->toDecimal($order->shipping_total, $currency); | |
| 137 | 138 | $purchaseUnits['amount']['breakdown']['shipping'] = [ |
| 138 | - 'currency_code' => $transaction->currency, | |
| 139 | - 'value' => number_format($shippingAmount, 2, '.', ''), | |
| 139 | + 'currency_code' => $currency, | |
| 140 | + 'value' => PayPalHelper::formatDecimalAmount($shippingAmount, $currency), | |
| 140 | 141 | ]; |
| 141 | 142 | $pushedTotal += $shippingAmount; |
| 142 | 143 | } |
| 143 | 144 | |
| @@ -155,16 +156,16 @@ | ||
| 155 | 156 | |
| 156 | 157 | if ($taxBehavior === 1) { |
| 157 | 158 | // Pure exclusive: all tax is additive on top of item prices. |
| 158 | 159 | // tax_total includes product + fee tax (both exclusive). |
| 159 | - $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); | |
| 160 | 161 | } elseif ($taxBehavior === 3) { |
| 161 | 162 | // Mixed: only exclusive product + fee tax is additive; shipping conditional. |
| 162 | - $taxTotal = $this->toDecimal($exclusiveTaxTotal); | |
| 163 | + $taxTotal = $this->toDecimal($exclusiveTaxTotal, $currency); | |
| 163 | 164 | if ($storeTaxBehavior === 1) { |
| 164 | 165 | // Store is exclusive: fees and shipping are also exclusive. |
| 165 | - $taxTotal += $this->toDecimal($order->shipping_tax); | |
| 166 | - $taxTotal += $this->toDecimal($feeTax); | |
| 166 | + $taxTotal += $this->toDecimal($order->shipping_tax, $currency); | |
| 167 | + $taxTotal += $this->toDecimal($feeTax, $currency); | |
| 167 | 168 | } |
| 168 | 169 | } else { |
| 169 | 170 | $taxTotal = 0; |
| 170 | 171 | } |
| @@ -170,10 +171,10 @@ | ||
| 170 | 171 | } |
| 171 | 172 | |
| 172 | 173 | if ($taxTotal > 0) { |
| 173 | 174 | $purchaseUnits['amount']['breakdown']['tax_total'] = [ |
| 174 | - 'currency_code' => $transaction->currency, | |
| 175 | - 'value' => number_format($taxTotal, 2, '.', ''), | |
| 175 | + 'currency_code' => $currency, | |
| 176 | + 'value' => PayPalHelper::formatDecimalAmount($taxTotal, $currency), | |
| 176 | 177 | ]; |
| 177 | 178 | $pushedTotal += $taxTotal; |
| 178 | 179 | } |
| 179 | 180 | |
| @@ -179,10 +180,10 @@ | ||
| 179 | 180 | |
| 180 | 181 | if ($chargingAmount < $pushedTotal) { |
| 181 | 182 | $discount = $pushedTotal - $chargingAmount; |
| 182 | 183 | $purchaseUnits['amount']['breakdown']['discount'] = [ |
| 183 | - 'currency_code' => $transaction->currency, | |
| 184 | - 'value' => number_format($discount, 2, '.', ''), | |
| 184 | + 'currency_code' => $currency, | |
| 185 | + 'value' => PayPalHelper::formatDecimalAmount($discount, $currency), | |
| 185 | 186 | ]; |
| 186 | 187 | } else if ($chargingAmount > $pushedTotal) { |
| 187 | 188 | $extraChargeNeedToBeAdded = $chargingAmount - $pushedTotal; |
| 188 | 189 | $formattedItems[] = [ |
| @@ -187,10 +188,10 @@ | ||
| 187 | 188 | $extraChargeNeedToBeAdded = $chargingAmount - $pushedTotal; |
| 188 | 189 | $formattedItems[] = [ |
| 189 | 190 | 'name' => __('Adjustment Amount', 'fluent-cart'), |
| 190 | 191 | 'unit_amount' => [ |
| 191 | - 'currency_code' => $transaction->currency, | |
| 192 | - 'value' => number_format($extraChargeNeedToBeAdded, 2, '.', ''), | |
| 192 | + 'currency_code' => $currency, | |
| 193 | + 'value' => PayPalHelper::formatDecimalAmount($extraChargeNeedToBeAdded, $currency), | |
| 193 | 194 | ], |
| 194 | 195 | 'quantity' => 1, |
| 195 | 196 | ]; |
| 196 | 197 | |
| @@ -197,9 +198,9 @@ | ||
| 197 | 198 | $purchaseUnits['items'] = $formattedItems; |
| 198 | 199 | |
| 199 | 200 | //now the total amount need to be adjusted with item total value |
| 200 | 201 | $adjustedItemTotal = $itemsSubTotal + $extraChargeNeedToBeAdded; |
| 201 | - $purchaseUnits['amount']['breakdown']['item_total']['value'] = number_format($adjustedItemTotal, 2, '.', ''); | |
| 202 | + $purchaseUnits['amount']['breakdown']['item_total']['value'] = PayPalHelper::formatDecimalAmount($adjustedItemTotal, $currency); | |
| 202 | 203 | } |
| 203 | 204 | |
| 204 | 205 | // System (auto-charged, store-billed) subscription checkout: vault the |
| 205 | 206 | // buyer's PayPal account during this purchase (Vault v3 save-on-success) |
| @@ -609,8 +610,24 @@ | ||
| 609 | 610 | } |
| 610 | 611 | |
| 611 | 612 | $transactionUpdateData['meta'] = array_merge($transaction->meta ?? [], Arr::get($transactionArgs, 'meta', [])); |
| 612 | 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 | + | |
| 613 | 630 | $transaction->fill($transactionUpdateData); |
| 614 | 631 | $transaction->save(); |
| 615 | 632 | |
| 616 | 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', [ |
| @@ -696,10 +713,12 @@ | ||
| 696 | 713 | // never deliver a paid product for free. |
| 697 | 714 | $currencyMatches = !$lastPaymentCurrency || !$transaction->currency |
| 698 | 715 | || strtoupper($transaction->currency) === $lastPaymentCurrency; |
| 699 | 716 | |
| 717 | + $expectedAmount = PayPalHelper::wireCents($transaction->total, $transaction->currency); | |
| 718 | + | |
| 700 | 719 | $initialPaymentVerified = $lastPaymentAmount |
| 701 | - && $transaction->total == $lastPaymentAmount | |
| 720 | + && $expectedAmount == $lastPaymentAmount | |
| 702 | 721 | && $currencyMatches; |
| 703 | 722 | |
| 704 | 723 | if ($initialPaymentVerified || $transaction->total == 0) { |
| 705 | 724 | $transactionUpdateData = array_filter([ |
| @@ -717,9 +736,9 @@ | ||
| 717 | 736 | __('PayPal Subscription Payment Mismatch', 'fluent-cart'), |
| 718 | 737 | sprintf( |
| 719 | 738 | /* translators: %1$s: expected amount, %2$s: expected currency, %3$s: received amount, %4$s: received currency */ |
| 720 | 739 | __('Subscription initial payment mismatch. Expected: %1$s %2$s, Received: %3$s %4$s. Order not marked paid; awaiting webhook.', 'fluent-cart'), |
| 721 | - Helper::toDecimal($transaction->total), | |
| 740 | + Helper::toDecimal($expectedAmount), | |
| 722 | 741 | $transaction->currency, |
| 723 | 742 | Helper::toDecimal($lastPaymentAmount), |
| 724 | 743 | $lastPaymentCurrency |
| 725 | 744 | ), |
| @@ -803,11 +822,16 @@ | ||
| 803 | 822 | return $subscriptionModel; |
| 804 | 823 | } |
| 805 | 824 | |
| 806 | 825 | |
| 807 | - 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) | |
| 808 | 832 | { |
| 809 | - return Helper::toDecimalWithoutComma($cents); | |
| 833 | + return PayPalHelper::toDecimalAmount($cents, $currency); | |
| 810 | 834 | } |
| 811 | 835 | |
| 812 | 836 | /** |
| 813 | 837 | * Persist the vaulted PayPal payment token from a captured order onto the |
| @@ -925,9 +949,9 @@ | ||
| 925 | 949 | 'reference_id' => $transaction->uuid, |
| 926 | 950 | 'custom_id' => $transaction->uuid, |
| 927 | 951 | 'amount' => [ |
| 928 | 952 | 'currency_code' => strtoupper($transaction->currency), |
| 929 | - 'value' => number_format($this->toDecimal((int) $transaction->total), 2, '.', ''), | |
| 953 | + 'value' => PayPalHelper::formatAmount((int) $transaction->total, $transaction->currency), | |
| 930 | 954 | ], |
| 931 | 955 | ]; |
| 932 | 956 | |
| 933 | 957 | $paypalOrder = API::createOrder($purchaseUnit, [ |
| @@ -934,9 +958,9 @@ | ||
| 934 | 958 | 'payment_source' => ['paypal' => ['vault_id' => $token]], |
| 935 | 959 | ], [ |
| 936 | 960 | // One vendor charge per (order, attempt) — a scheduler double-fire |
| 937 | 961 | // replays the original response instead of charging twice. |
| 938 | - 'PayPal-Request-Id' => 'fct_system_charge_' . $order->id . '_' . $attempt, | |
| 962 | + 'PayPal-Request-Id' => 'fct_system_charge_' . $order->uuid . '_' . $attempt, | |
| 939 | 963 | ]); |
| 940 | 964 | |
| 941 | 965 | if (is_wp_error($paypalOrder)) { |
| 942 | 966 | return $paypalOrder; |
| @@ -1037,15 +1061,17 @@ | ||
| 1037 | 1061 | return new \WP_Error('currency_mismatch', __('The PayPal payment currency does not match this transaction. Please verify the payment at PayPal.', 'fluent-cart')); |
| 1038 | 1062 | } |
| 1039 | 1063 | |
| 1040 | 1064 | $captureAmount = Helper::toCent(Arr::get($capture, 'amount.value', 0)); |
| 1041 | - if ($captureAmount !== (int) $transaction->total) { | |
| 1065 | + $expectedAmount = PayPalHelper::wireCents($transaction->total, $transaction->currency); | |
| 1066 | + | |
| 1067 | + if ($captureAmount !== $expectedAmount) { | |
| 1042 | 1068 | fluent_cart_warning_log( |
| 1043 | 1069 | __('PayPal Amount Mismatch On Sync', 'fluent-cart'), |
| 1044 | 1070 | sprintf( |
| 1045 | 1071 | /* translators: %1$s: expected amount, %2$s: received amount */ |
| 1046 | 1072 | __('Capture amount mismatch detected during transaction sync. Expected: %1$s, Received: %2$s. Transaction was not confirmed.', 'fluent-cart'), |
| 1047 | - Helper::toDecimal($transaction->total), | |
| 1073 | + Helper::toDecimal($expectedAmount), | |
| 1048 | 1074 | Helper::toDecimal($captureAmount) |
| 1049 | 1075 | ), |
| 1050 | 1076 | [ |
| 1051 | 1077 | 'module_name' => 'order', |