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

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