PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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 1.3.20 1.3.19 All 49 releases
← All changes | app/Services/Payments/PaymentInstance.php +58 -5 1.3.21 → 1.6.5 View file →
@@ -56,8 +56,32 @@
56 56 $this->transaction = $transaction;
57 57 return $this;
58 58 }
59 59
60 + /**
61 + * Gateway-agnostic idempotency seed for the current charge attempt. The ONLY
62 + * seed source for gateway idempotency keys — full contract in
63 + * .claude/skills/coding-rules/payment-idempotency.md.
64 + *
65 + * Stable across duplicate submissions (transaction uuid survives draft-order
66 + * re-submission -> gateway dedupes), fresh after an observed failure
67 + * (payment_attempt is bumped when a FAILED transaction is re-submitted ->
68 + * retries are never blocked or answered with a cached gateway error).
69 + *
70 + * @return string Empty string when no charge transaction exists — callers
71 + * must skip idempotency rather than share a static key.
72 + */
73 + public function getIdempotencySeed()
74 + {
75 + if (!$this->transaction || !$this->transaction->uuid) {
76 + return '';
77 + }
78 +
79 + $attempt = (int) Arr::get($this->transaction->meta ?: [], 'payment_attempt', 0);
80 +
81 + return $this->transaction->uuid . (int) $this->transaction->total . ($attempt ? '_r' . $attempt : '');
82 + }
83 +
60 84 public function getExtraAddonAmount()
61 85 {
62 86 if (empty($this->subscription)) {
63 87 return 0;
@@ -62,8 +86,10 @@
62 86 if (empty($this->subscription)) {
63 87 return 0;
64 88 }
65 89
90 + $taxBehavior = (int) $this->order->tax_behavior;
91 +
66 92 $extraItems = $this->order->order_items->filter(function ($item) {
67 93 return $item->payment_type !== 'subscription' && $item->payment_type !== 'signup_fee' && $item->payment_type !== 'fee';
68 94 });
69 95
@@ -68,15 +94,42 @@
68 94 });
69 95
70 96 $extraAmount = 0;
71 97 foreach ($extraItems as $item) {
72 - $extraAmount += $item->line_total;
98 + $extraAmount += (int) $item->line_total;
99 +
100 + if ($taxBehavior === 1) {
101 + $extraAmount += (int) $item->tax_amount;
102 + } elseif ($taxBehavior === 3) {
103 + $inclusive = (bool) Arr::get($item->line_meta, 'tax_config.inclusive', false);
104 + if (!$inclusive) {
105 + $extraAmount += (int) $item->tax_amount;
106 + }
107 + }
73 108 }
74 109
75 - // shipping charge, in case addons are physical products
76 - $shippingCharge = $this->order->shipping_total;
77 - if ($shippingCharge) {
78 - $extraAmount += $shippingCharge;
110 + // shipping for physical addons (digital sub + one-time physical items)
111 + // skip when subscription itself is physical — shipping already in recurring_total
112 + // if subscription is digital, but has physical addons, we need to add shipping for those addons, because shipping is not included in recurring_total for digital subscriptions
113 + $subscriptionItem = $this->order->order_items->filter(function ($item) {
114 + return $item->payment_type === 'subscription';
115 + })->first();
116 + $subscriptionIsPhysical = $subscriptionItem && $subscriptionItem->fulfillment_type === 'physical';
117 +
118 + if (!$subscriptionIsPhysical) {
119 + $shippingCharge = (int) $this->order->shipping_total;
120 + if ($shippingCharge) {
121 + $extraAmount += $shippingCharge;
122 +
123 + if ($taxBehavior === 1) {
124 + $extraAmount += (int) $this->order->shipping_tax;
125 + } elseif ($taxBehavior === 3) {
126 + $storeTaxBehavior = (int) $this->order->getMeta('store_tax_behavior', 1);
127 + if ($storeTaxBehavior === 1) {
128 + $extraAmount += (int) $this->order->shipping_tax;
129 + }
130 + }
131 + }
79 132 }
80 133
81 134 return $extraAmount;
82 135 }