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
fluent-cart / app / Services / Payments / PaymentInstance.php

PaymentInstance.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.5, at app/Services/Payments/PaymentInstance.php

188 lines 6.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Payments;
4
5 use FluentCart\App\Helpers\Status;
6 use FluentCart\App\Models\Order;
7 use FluentCart\App\Models\OrderTransaction;
8 use FluentCart\App\Models\Subscription;
9 use FluentCart\App\Services\DateTime\DateTime;
10 use FluentCart\Framework\Support\Arr;
11
12 class PaymentInstance
13 {
14
15 /**
16 * @var Order
17 */
18 public $order;
19
20 public $transaction;
21
22 public $subscription;
23
24 public $paymentType;
25
26 public function __construct(Order $order)
27 {
28 $this->order = $order;
29 $this->setupData();
30 }
31
32 public function setupData()
33 {
34 $this->transaction = OrderTransaction::query()
35 ->where('transaction_type', Status::TRANSACTION_TYPE_CHARGE)
36 ->where('order_id', $this->order->id)
37 ->latest()
38 ->first();
39
40 $this->subscription = null;
41
42 if ($this->order->type === 'subscription') {
43 $this->subscription = Subscription::query()
44 ->where('parent_order_id', $this->order->id)
45 ->first();
46 } else if ($this->order->type === Status::ORDER_TYPE_RENEWAL) {
47 $this->subscription = Subscription::query()
48 ->where('parent_order_id', $this->order->parent_id)
49 ->first();
50 }
51
52 }
53
54 public function setTransaction(OrderTransaction $transaction)
55 {
56 $this->transaction = $transaction;
57 return $this;
58 }
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
84 public function getExtraAddonAmount()
85 {
86 if (empty($this->subscription)) {
87 return 0;
88 }
89
90 $taxBehavior = (int) $this->order->tax_behavior;
91
92 $extraItems = $this->order->order_items->filter(function ($item) {
93 return $item->payment_type !== 'subscription' && $item->payment_type !== 'signup_fee' && $item->payment_type !== 'fee';
94 });
95
96 $extraAmount = 0;
97 foreach ($extraItems as $item) {
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 }
108 }
109
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 }
132 }
133
134 return $extraAmount;
135 }
136
137
138 public function getSubscriptionCancelAtTimeStamp()
139 {
140 if (!$this->subscription) {
141 return null;
142 }
143
144 $billTimes = $this->subscription->getRequiredBillTimes();
145
146 if ($billTimes <= 0) {
147 return null;
148 }
149
150 $interval = $this->subscription->billing_interval;
151
152
153 if ($interval == 'daily') {
154 $interval = 'day';
155 }
156
157 $interValMaps = [
158 'day' => 'days',
159 'weekly' => 'weeks',
160 'monthly' => 'months',
161 'quarterly' => 'months',
162 'half_yearly' => 'months',
163 'yearly' => 'years'
164 ];
165
166 if (isset($interValMaps[$interval]) && $billTimes > 0) {
167 $interval = $interValMaps[$interval];
168
169 // Adjust billTimes for quarterly and half_yearly
170 if ($this->subscription->billing_interval === 'quarterly') {
171 $billTimes = $billTimes * 3; // Convert to months
172 } elseif ($this->subscription->billing_interval === 'half_yearly') {
173 $billTimes = $billTimes * 6; // Convert to months
174 }
175 }
176
177
178 $timestamp = strtotime('+ ' . $billTimes . ' ' . $interval);
179
180 if (!$this->subscription->bill_count && $this->subscription->trial_days) {
181 $timestamp = $timestamp + $this->subscription->trial_days * 24 * 60 * 60; // Add trial days in seconds
182 }
183
184 return $timestamp;
185 }
186 }
187
188