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.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 trunk All 48 releases
← All changes | app/Services/Payments/SubscriptionHelper.php +59 -1 1.6.2 → 1.6.5 View file →
@@ -1,12 +1,14 @@
1 1 <?php
2 2
3 3 namespace FluentCart\App\Services\Payments;
4 4
5 +use FluentCart\Api\StoreSettings;
5 6 use FluentCart\App\App;
6 7 use FluentCart\App\Helpers\Helper;
7 8 use FluentCart\App\Helpers\Status;
8 9 use FluentCart\App\Models\Order;
10 +use FluentCart\App\Models\OrderTransaction;
9 11 use FluentCart\App\Models\Subscription;
10 12 use FluentCart\App\Services\DateTime\DateTime;
11 13 use FluentCart\Framework\Support\Arr;
12 14
@@ -11,8 +13,42 @@
11 13 use FluentCart\Framework\Support\Arr;
12 14
13 15 class SubscriptionHelper
14 16 {
17 + /**
18 + * When money actually moved for an order, not when the row was created.
19 + * A pending/COD/bank-transfer order can sit for months before it is paid,
20 + * so `created_at` is not a safe billing anchor. Prefers the latest succeeded
21 + * charge's meta.settled_at (the gateway's own settlement time), falls back
22 + * to that transaction's created_at, then the order's completed_at, and only
23 + * falls back to order created_at when nothing else exists (e.g. a $0 order).
24 + */
25 + public static function resolvePaidAnchor(Order $order)
26 + {
27 + $lastCharge = OrderTransaction::query()
28 + ->where('order_id', $order->id)
29 + ->where('transaction_type', Status::TRANSACTION_TYPE_CHARGE)
30 + ->where('status', Status::TRANSACTION_SUCCEEDED)
31 + ->orderBy('id', 'DESC')
32 + ->first();
33 +
34 + if ($lastCharge) {
35 + $settledAt = Arr::get($lastCharge->meta, 'settled_at');
36 + if (!empty($settledAt)) {
37 + return $settledAt;
38 + }
39 + if (!empty($lastCharge->created_at)) {
40 + return $lastCharge->created_at;
41 + }
42 + }
43 +
44 + if (!empty($order->completed_at)) {
45 + return $order->completed_at;
46 + }
47 +
48 + return $order->created_at;
49 + }
50 +
15 51 /*
16 52 * @param $subscriptionModel
17 53 * @return string|null
18 54 *
@@ -42,9 +78,10 @@
42 78 }
43 79
44 80
45 81 if ($subscriptionModel->bill_count == 0) {
46 - $baseDate = $subscriptionModel->created_at;
82 + $parentOrder = $subscriptionModel->order;
83 + $baseDate = $parentOrder ? self::resolvePaidAnchor($parentOrder) : $subscriptionModel->created_at;
47 84
48 85 } elseif (!empty($subscriptionModel->next_billing_date) && strtotime($subscriptionModel->next_billing_date) < time()) {
49 86 $baseDate = $subscriptionModel->next_billing_date;
50 87 } else {
@@ -152,8 +189,29 @@
152 189 if (!$ts || $ts <= 0) {
153 190 return null;
154 191 }
155 192 return gmdate('Y-m-d H:i:s', $ts);
193 + }
194 +
195 + /**
196 + * Whether renewal work is restricted to the store's current mode. On by
197 + * default; a live store deliberately flipped to test mode can turn it off
198 + * so live subscriptions keep billing. Fail-closed: only an explicit 'no'
199 + * disables — a malformed value written past the request sanitizer must
200 + * not silently drop staging protection.
201 + */
202 + public static function isModeGuardEnabled(): bool
203 + {
204 + return (new StoreSettings())->get('subscription_mode_guard', 'yes') !== 'no';
205 + }
206 +
207 + /**
208 + * Whether renewal work (invoice creation, automatic charging) may run for
209 + * an order of the given mode under the current store mode + guard setting.
210 + */
211 + public static function canProcessInMode(string $orderMode): bool
212 + {
213 + return !self::isModeGuardEnabled() || $orderMode === (new StoreSettings())->get('order_mode');
156 214 }
157 215
158 216 public static function getSubscriptionsGracePeriodDays()
159 217 {