PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 All 51 releases
← All changes | app/Modules/PaymentMethods/PayPalGateway/Processor.php +91 -35 1.6.2 → 1.7.1 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)
@@ -243,8 +244,21 @@
243 244 ],
244 245 ];
245 246 }
246 247
248 + $brandName = (new PayPalSettingsBase())->getBrandName([
249 + 'order' => $order,
250 + 'subscription' => $paymentInstance->subscription,
251 + ]);
252 +
253 + if ($brandName !== '') {
254 + if (isset($extraBody['payment_source'])) {
255 + $extraBody['payment_source']['paypal']['experience_context']['brand_name'] = $brandName;
256 + } else {
257 + $extraBody['application_context'] = ['shipping_preference' => 'NO_SHIPPING', 'brand_name' => $brandName];
258 + }
259 + }
260 +
247 261 $paypalOrder = API::createOrder($purchaseUnits, $extraBody);
248 262
249 263 // Vaulting is a convenience; the purchase is the point. A merchant account
250 264 // not approved for vaulting can reject the order outright because of the
@@ -312,18 +326,29 @@
312 326 {
313 327 $order = $paymentInstance->order;
314 328 $transaction = $paymentInstance->transaction;
315 329
330 + $experienceContext = [
331 + 'return_url' => PaymentHelper::getCustomPaymentLink($order->uuid),
332 + 'cancel_url' => \FluentCart\App\Modules\PaymentMethods\Core\AbstractPaymentGateway::getCancelUrl(),
333 + 'shipping_preference' => 'NO_SHIPPING',
334 + ];
335 +
336 + $brandName = (new PayPalSettingsBase())->getBrandName([
337 + 'order' => $order,
338 + 'subscription' => $paymentInstance->subscription,
339 + ]);
340 +
341 + if ($brandName !== '') {
342 + $experienceContext['brand_name'] = $brandName;
343 + }
344 +
316 345 $setupToken = API::makeRequest('vault/setup-tokens', 'v3', 'POST', [
317 346 'payment_source' => [
318 347 'paypal' => [
319 348 'usage_type' => 'MERCHANT',
320 349 'customer_type' => 'CONSUMER',
321 - 'experience_context' => [
322 - 'return_url' => PaymentHelper::getCustomPaymentLink($order->uuid),
323 - 'cancel_url' => \FluentCart\App\Modules\PaymentMethods\Core\AbstractPaymentGateway::getCancelUrl(),
324 - 'shipping_preference' => 'NO_SHIPPING',
325 - ],
350 + 'experience_context' => $experienceContext,
326 351 ],
327 352 ],
328 353 ]);
329 354
@@ -559,9 +584,13 @@
559 584 'uuid' => $subscription->uuid,
560 585 ]
561 586 ],
562 587 'response' => [
563 - 'planId' => Arr::get($paypalPlan, 'id')
588 + 'planId' => Arr::get($paypalPlan, 'id'),
589 + 'brandName' => (new PayPalSettingsBase())->getBrandName([
590 + 'order' => $paymentInstance->order,
591 + 'subscription' => $subscription,
592 + ]),
564 593 ]
565 594 ];
566 595 }
567 596
@@ -609,8 +638,24 @@
609 638 }
610 639
611 640 $transactionUpdateData['meta'] = array_merge($transaction->meta ?? [], Arr::get($transactionArgs, 'meta', []));
612 641
642 + // A zero-decimal total is stored x100 but charged rounded, so PayPal reports back a
643 + // figure up to half a unit away from the stored one. The wire comparison upstream has
644 + // already proved this is the same payment. Keep the stored number: it is what the
645 + // order's line items sum to, so adopting the rounded one would either strand the order
646 + // partially_paid (rounded down) or fake an overpayment (rounded up). Record what
647 + // actually moved in meta instead. activateSubscription() already leaves total alone.
648 + $reportedTotal = (int)Arr::get($transactionUpdateData, 'total', 0);
649 + if ($reportedTotal
650 + && $reportedTotal !== (int)$transaction->total
651 + && PayPalHelper::currencyDecimals($transaction->currency) === 0
652 + && $reportedTotal === PayPalHelper::wireCents($transaction->total, $transaction->currency)
653 + ) {
654 + unset($transactionUpdateData['total']);
655 + $transactionUpdateData['meta']['wire_total'] = $reportedTotal;
656 + }
657 +
613 658 $transaction->fill($transactionUpdateData);
614 659 $transaction->save();
615 660
616 661 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 +741,12 @@
696 741 // never deliver a paid product for free.
697 742 $currencyMatches = !$lastPaymentCurrency || !$transaction->currency
698 743 || strtoupper($transaction->currency) === $lastPaymentCurrency;
699 744
745 + $expectedAmount = PayPalHelper::wireCents($transaction->total, $transaction->currency);
746 +
700 747 $initialPaymentVerified = $lastPaymentAmount
701 - && $transaction->total == $lastPaymentAmount
748 + && $expectedAmount == $lastPaymentAmount
702 749 && $currencyMatches;
703 750
704 751 if ($initialPaymentVerified || $transaction->total == 0) {
705 752 $transactionUpdateData = array_filter([
@@ -717,9 +764,9 @@
717 764 __('PayPal Subscription Payment Mismatch', 'fluent-cart'),
718 765 sprintf(
719 766 /* translators: %1$s: expected amount, %2$s: expected currency, %3$s: received amount, %4$s: received currency */
720 767 __('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),
768 + Helper::toDecimal($expectedAmount),
722 769 $transaction->currency,
723 770 Helper::toDecimal($lastPaymentAmount),
724 771 $lastPaymentCurrency
725 772 ),
@@ -760,8 +807,10 @@
760 807 if ($subscriptionModel->trial_days > 0) {
761 808 $subscriptionUpdateData['status'] = Status::SUBSCRIPTION_TRIALING;
762 809 }
763 810
811 + $subscriptionUpdateData['bill_count'] = $subscriptionModel->calculateBillCount();
812 +
764 813 // Atomic conditional update: only the caller that actually flips status out of a
765 814 // pre-active state wins the transition, so concurrent AJAX-return + webhook calls
766 815 // can't both dispatch SubscriptionActivated.
767 816 $activatedNow = (bool) Subscription::query()
@@ -803,11 +852,16 @@
803 852 return $subscriptionModel;
804 853 }
805 854
806 855
807 - private function toDecimal($cents)
856 + /**
857 + * Cents to a decimal amount rounded to the precision PayPal accepts for
858 + * the currency (HUF/JPY/TWD take no decimals). Rounding before the
859 + * breakdown arithmetic keeps the parts summing to the total.
860 + */
861 + private function toDecimal($cents, $currency)
808 862 {
809 - return Helper::toDecimalWithoutComma($cents);
863 + return PayPalHelper::toDecimalAmount($cents, $currency);
810 864 }
811 865
812 866 /**
813 867 * Persist the vaulted PayPal payment token from a captured order onto the
@@ -925,9 +979,9 @@
925 979 'reference_id' => $transaction->uuid,
926 980 'custom_id' => $transaction->uuid,
927 981 'amount' => [
928 982 'currency_code' => strtoupper($transaction->currency),
929 - 'value' => number_format($this->toDecimal((int) $transaction->total), 2, '.', ''),
983 + 'value' => PayPalHelper::formatAmount((int) $transaction->total, $transaction->currency),
930 984 ],
931 985 ];
932 986
933 987 $paypalOrder = API::createOrder($purchaseUnit, [
@@ -1037,15 +1091,17 @@
1037 1091 return new \WP_Error('currency_mismatch', __('The PayPal payment currency does not match this transaction. Please verify the payment at PayPal.', 'fluent-cart'));
1038 1092 }
1039 1093
1040 1094 $captureAmount = Helper::toCent(Arr::get($capture, 'amount.value', 0));
1041 - if ($captureAmount !== (int) $transaction->total) {
1095 + $expectedAmount = PayPalHelper::wireCents($transaction->total, $transaction->currency);
1096 +
1097 + if ($captureAmount !== $expectedAmount) {
1042 1098 fluent_cart_warning_log(
1043 1099 __('PayPal Amount Mismatch On Sync', 'fluent-cart'),
1044 1100 sprintf(
1045 1101 /* translators: %1$s: expected amount, %2$s: received amount */
1046 1102 __('Capture amount mismatch detected during transaction sync. Expected: %1$s, Received: %2$s. Transaction was not confirmed.', 'fluent-cart'),
1047 - Helper::toDecimal($transaction->total),
1103 + Helper::toDecimal($expectedAmount),
1048 1104 Helper::toDecimal($captureAmount)
1049 1105 ),
1050 1106 [
1051 1107 'module_name' => 'order',