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

241 lines 8.5 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: 3 attempts per day per customer (for subscription card updates)
82 *
83 * @param string $customerId Stripe customer ID
84 * @return bool|\WP_Error Returns true if allowed, WP_Error if rate limited
85 */
86 protected static function checkRateLimit($customerId)
87 {
88 $customerDailyLimit = apply_filters('fluent_cart/stripe/setup_intent_rate_limit_customer_daily', 3, $customerId);
89
90 $customerDailyKey = 'fct_stripe_setup_intent_rate_daily_' . md5($customerId);
91 $customerDailyAttempts = get_transient($customerDailyKey) ?: 0;
92
93 if ($customerDailyAttempts >= $customerDailyLimit) {
94 return new \WP_Error('rate_limit_exceeded', __('Daily verification limit reached. You can try again tomorrow.', 'fluent-cart'));
95 }
96
97 set_transient($customerDailyKey, $customerDailyAttempts + 1, DAY_IN_SECONDS);
98
99 return true;
100 }
101
102 public function getRemainingRateLimit($customerId)
103 {
104 $customerDailyLimit = apply_filters('fluent_cart/stripe/setup_intent_rate_limit_customer_daily', 3, $customerId);
105 $customerDailyKey = 'fct_stripe_setup_intent_rate_daily_' . md5($customerId);
106 $customerDailyAttempts = get_transient($customerDailyKey) ?: 0;
107 return $customerDailyLimit - $customerDailyAttempts;
108 }
109
110 public function getOrCreateStripeCustomer($pm)
111 {
112 $api = new API();
113 // check if customer exist with the email, currently not using
114 $email = Arr::get($pm, 'billing_details.email');
115 $customers = $api->getStripeObject('customers', [
116 'email' => $email, 'limit' => 1
117 ]);
118
119 if ($customers && !is_wp_error($customers) && !empty($customers['data'][0])) {
120 return Arr::get($customers, 'data.0.id');
121 }
122
123 $response = $api->createStripeObject('customers', [
124 'name' => Arr::get($pm, 'billing_details.name'),
125 'email' => Arr::get($pm, 'billing_details.email'),
126 'address' => Arr::get($pm, 'billing_details.address'),
127 ]);
128
129 if (is_wp_error($response)) {
130 static::sendError($response->get_error_message());
131 }
132
133 return Arr::get($response, 'id');
134 }
135
136 public static function addOldSubscriptionMeta($subscriptionId, $oldSubData)
137 {
138 $defaults = [
139 'payment_method' => '',
140 'vendor_subscription_id' => '',
141 'vendor_customer_id' => '',
142 'vendor_plan_id' => '',
143 'payment_source' => '',
144 'canceled_at' => null,
145 'reason' => '',
146 'expire_at' => null,
147 ];
148
149 $oldSubscription = array_merge($defaults, $oldSubData);
150
151 // get if exists
152 $existingMeta = SubscriptionMeta::query()
153 ->where('subscription_id', '=', $subscriptionId)
154 ->where('meta_key', '=', 'old_subscriptions')
155 ->first();
156
157 $oldSubscriptions = [];
158
159 if ($existingMeta && $existingMeta->meta_value) {
160 if (is_string($existingMeta->meta_value)) {
161 $decoded = json_decode($existingMeta->meta_value, true);
162 $oldSubscriptions = is_array($decoded) ? $decoded : [];
163 } else {
164 $oldSubscriptions = (array)$existingMeta->meta_value;
165 }
166 }
167
168 // Add new subscription data
169 $oldSubscriptions[] = $oldSubscription;
170
171 // Update or create the meta with JSON encoded value
172 SubscriptionMeta::updateOrCreate(
173 [
174 'subscription_id' => $subscriptionId,
175 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
176 'meta_key' => 'old_subscriptions'
177 ],
178 [
179 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
180 'meta_value' => $oldSubscriptions
181 ]
182 );
183 }
184
185 public static function sendError($message, $code = 423)
186 {
187 wp_send_json([
188 'status' => 'failed',
189 'message' => $message
190 ], $code);
191 }
192
193 // Used by IPN and Charge Success to confirm subscription after charge succeeded
194 public function confirmSubscriptionAfterChargeSucceeded(Subscription $subscription, $billingInfo = [])
195 {
196 $order = $subscription->order;
197
198 if (!$order) {
199 return;
200 }
201
202 $api = new API();
203 $response = $api->getStripeObject('subscriptions/' . $subscription->vendor_subscription_id, [], $order->mode);
204
205 if (is_wp_error($response)) {
206 return;
207 }
208
209 $nextBillingDate = Arr::get($response, 'current_period_end') ?? null;
210
211 if ($nextBillingDate) {
212 $nextBillingDate = gmdate('Y-m-d H:i:s', (int) $nextBillingDate);
213 }
214
215 $status = StripeHelper::transformSubscriptionStatus($response, $subscription);
216 $billCount = $subscription->calculateBillCount();
217
218 $oldStatus = $subscription->status;
219
220 if (Arr::get($response, 'id')) {
221 $subscription->next_billing_date = $nextBillingDate;
222 $subscription->status = $status;
223 $subscription->current_payment_method = 'stripe';
224 $subscription->vendor_subscription_id = Arr::get($response, 'id');
225 $subscription->bill_count = $billCount;
226 $subscription->save();
227 }
228
229 if ($billingInfo) {
230 $subscription->updateMeta('active_payment_method', $billingInfo);
231 }
232
233 if ($oldStatus != $subscription->status && (Status::SUBSCRIPTION_ACTIVE === $subscription->status || Status::SUBSCRIPTION_TRIALING === $subscription->status)) {
234 (new SubscriptionActivated($subscription, $order, $order->customer))->dispatch();
235 }
236
237 return $subscription;
238 }
239
240 }
241