PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.26
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.26
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
fluent-cart / app / Modules / PaymentMethods / StripeGateway / SubscriptionsManager.php

SubscriptionsManager.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.26, at app/Modules/PaymentMethods/StripeGateway/SubscriptionsManager.php

242 lines 8.6 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\Modules\PaymentMethods\StripeGateway;
4
5 use FluentCart\App\Events\Subscription\SubscriptionActivated;
6 use FluentCart\App\Helpers\Status;
7 use FluentCart\App\Models\OrderTransaction;
8 use FluentCart\App\Models\Subscription;
9 use FluentCart\App\Models\SubscriptionMeta;
10 use FluentCart\App\Modules\PaymentMethods\StripeGateway\API\API;
11 use FluentCart\App\Services\DateTime\DateTime;
12 use FluentCart\Framework\Support\Arr;
13
14 class SubscriptionsManager
15 {
16 public function updateSubscriptionStatus($subscriptionId, $status)
17 {
18 $subscription = Subscription::query()->where('id', $subscriptionId)->first();
19 if ($subscription) {
20 $subscription->status = $status;
21 $subscription->save();
22 }
23 }
24
25 public function validate($vendorChargeId, $data)
26 {
27 if ($vendorChargeId !== Arr::get($data, 'vendor_charge_id')) {
28 return new \WP_Error('invalid_vendor_charge_id', __('Invalid vendor charge ID.', 'fluent-cart'));
29 }
30 return true;
31 }
32
33
34 /**
35 * verify payment method via SetupIntent for future off-session payments, fraud prevention, and SCA compliance.
36 * @return true | wp_send_json_success | wp_send_json_error
37 * @throws \Exception
38 */
39 public static function verifyPaymentMethod($paymentMethodId, $customerId, $offSession = true)
40 {
41 $rateLimitCheck = static::checkRateLimit($customerId);
42 if (is_wp_error($rateLimitCheck)) {
43 static::sendError(__('Too many verification attempts. You can try again tomorrow.', 'fluent-cart'), 429);
44 }
45
46 // Verify via SetupIntent (for SCA compliance)
47 $setupIntent = (new API())->createStripeObject('setup_intents', [
48 'payment_method' => $paymentMethodId,
49 'customer' => $customerId,
50 'payment_method_types' => ['card'],
51 'confirm' => 'true',
52 'usage' => 'off_session'
53 ]);
54
55 if (is_wp_error($setupIntent)) {
56 static::sendError($setupIntent->get_error_message());
57 }
58
59 $status = Arr::get($setupIntent, 'status');
60
61 if ('requires_action' === $status) {
62 wp_send_json([
63 'status' => 'requires_action',
64 'message' => __('Payment method updated successfully', 'fluent-cart'),
65 'client_secret' => Arr::get($setupIntent, 'client_secret'),
66 'customer_id' => $customerId,
67 ], 200);
68 }
69
70 if ('succeeded' !== $status) {
71 wp_send_json([
72 'status' => 'failed',
73 'message' => __('Card verification failed', 'fluent-cart')
74 ], 423);
75 }
76 return true;
77 }
78
79 /**
80 * Check rate limit for SetupIntent creation to prevent card testing fraud.
81 *
82 * Rate limit: 3 attempts per day per customer (for subscription card updates)
83 *
84 * @param string $customerId Stripe customer ID
85 * @return bool|\WP_Error Returns true if allowed, WP_Error if rate limited
86 */
87 protected static function checkRateLimit($customerId)
88 {
89 $customerDailyLimit = apply_filters('fluent_cart/stripe/setup_intent_rate_limit_customer_daily', 3, $customerId);
90
91 $customerDailyKey = 'fct_stripe_setup_intent_rate_daily_' . md5($customerId);
92 $customerDailyAttempts = get_transient($customerDailyKey) ?: 0;
93
94 if ($customerDailyAttempts >= $customerDailyLimit) {
95 return new \WP_Error('rate_limit_exceeded', __('Daily verification limit reached. You can try again tomorrow.', 'fluent-cart'));
96 }
97
98 set_transient($customerDailyKey, $customerDailyAttempts + 1, DAY_IN_SECONDS);
99
100 return true;
101 }
102
103 public function getRemainingRateLimit($customerId)
104 {
105 $customerDailyLimit = apply_filters('fluent_cart/stripe/setup_intent_rate_limit_customer_daily', 3, $customerId);
106 $customerDailyKey = 'fct_stripe_setup_intent_rate_daily_' . md5($customerId);
107 $customerDailyAttempts = get_transient($customerDailyKey) ?: 0;
108 return $customerDailyLimit - $customerDailyAttempts;
109 }
110
111 public function getOrCreateStripeCustomer($pm)
112 {
113 $api = new API();
114 // check if customer exist with the email, currently not using
115 $email = Arr::get($pm, 'billing_details.email');
116 $customers = $api->getStripeObject('customers', [
117 'email' => $email, 'limit' => 1
118 ]);
119
120 if ($customers && !is_wp_error($customers) && !empty($customers['data'][0])) {
121 return Arr::get($customers, 'data.0.id');
122 }
123
124 $response = $api->createStripeObject('customers', [
125 'name' => Arr::get($pm, 'billing_details.name'),
126 'email' => Arr::get($pm, 'billing_details.email'),
127 'address' => Arr::get($pm, 'billing_details.address'),
128 ]);
129
130 if (is_wp_error($response)) {
131 static::sendError($response->get_error_message());
132 }
133
134 return Arr::get($response, 'id');
135 }
136
137 public static function addOldSubscriptionMeta($subscriptionId, $oldSubData)
138 {
139 $defaults = [
140 'payment_method' => '',
141 'vendor_subscription_id' => '',
142 'vendor_customer_id' => '',
143 'vendor_plan_id' => '',
144 'payment_source' => '',
145 'canceled_at' => null,
146 'reason' => '',
147 'expire_at' => null,
148 ];
149
150 $oldSubscription = array_merge($defaults, $oldSubData);
151
152 // get if exists
153 $existingMeta = SubscriptionMeta::query()
154 ->where('subscription_id', '=', $subscriptionId)
155 ->where('meta_key', '=', 'old_subscriptions')
156 ->first();
157
158 $oldSubscriptions = [];
159
160 if ($existingMeta && $existingMeta->meta_value) {
161 if (is_string($existingMeta->meta_value)) {
162 $decoded = json_decode($existingMeta->meta_value, true);
163 $oldSubscriptions = is_array($decoded) ? $decoded : [];
164 } else {
165 $oldSubscriptions = (array)$existingMeta->meta_value;
166 }
167 }
168
169 // Add new subscription data
170 $oldSubscriptions[] = $oldSubscription;
171
172 // Update or create the meta with JSON encoded value
173 SubscriptionMeta::updateOrCreate(
174 [
175 'subscription_id' => $subscriptionId,
176 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
177 'meta_key' => 'old_subscriptions'
178 ],
179 [
180 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
181 'meta_value' => $oldSubscriptions
182 ]
183 );
184 }
185
186 public static function sendError($message, $code = 423)
187 {
188 wp_send_json([
189 'status' => 'failed',
190 'message' => $message
191 ], $code);
192 }
193
194 // Used by IPN and Charge Success to confirm subscription after charge succeeded
195 public function confirmSubscriptionAfterChargeSucceeded(Subscription $subscription, $billingInfo = [])
196 {
197 $order = $subscription->order;
198
199 if (!$order) {
200 return;
201 }
202
203 $api = new API();
204 $response = $api->getStripeObject('subscriptions/' . $subscription->vendor_subscription_id, [], $order->mode);
205
206 if (is_wp_error($response)) {
207 return;
208 }
209
210 $nextBillingDate = Arr::get($response, 'current_period_end') ?? null;
211
212 if ($nextBillingDate) {
213 $nextBillingDate = gmdate('Y-m-d H:i:s', (int) $nextBillingDate);
214 }
215
216 $status = StripeHelper::transformSubscriptionStatus($response, $subscription);
217 $billCount = OrderTransaction::query()->where('subscription_id', $subscription->id)->count();
218
219 $oldStatus = $subscription->status;
220
221 if (Arr::get($response, 'id')) {
222 $subscription->next_billing_date = $nextBillingDate;
223 $subscription->status = $status;
224 $subscription->current_payment_method = 'stripe';
225 $subscription->vendor_subscription_id = Arr::get($response, 'id');
226 $subscription->bill_count = $billCount;
227 $subscription->save();
228 }
229
230 if ($billingInfo) {
231 $subscription->updateMeta('active_payment_method', $billingInfo);
232 }
233
234 if ($oldStatus != $subscription->status && (Status::SUBSCRIPTION_ACTIVE === $subscription->status || Status::SUBSCRIPTION_TRIALING === $subscription->status)) {
235 (new SubscriptionActivated($subscription, $order, $order->customer))->dispatch();
236 }
237
238 return $subscription;
239 }
240
241 }
242