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 / PayPalGateway / PayPalSubscriptions.php

PayPalSubscriptions.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.2, at app/Modules/PaymentMethods/PayPalGateway/PayPalSubscriptions.php

286 lines 11.9 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\PayPalGateway;
4
5 use FluentCart\App\Helpers\Helper;
6 use FluentCart\App\Helpers\Status;
7 use FluentCart\App\Models\OrderTransaction;
8 use FluentCart\App\Models\Subscription;
9 use FluentCart\App\Modules\PaymentMethods\Core\AbstractSubscriptionModule;
10 use FluentCart\App\Modules\PaymentMethods\PayPalGateway\API\API;
11 use FluentCart\App\Modules\Subscriptions\Services\SubscriptionService;
12 use FluentCart\App\Services\DateTime\DateTime;
13 use FluentCart\Framework\Support\Arr;
14
15 class PayPalSubscriptions extends AbstractSubscriptionModule
16 {
17 /**
18 * Read-only lookup used by the admin "Edit Vendor IDs" verify action.
19 *
20 * PayPal's status vocabulary is its own (ACTIVE / SUSPENDED / CANCELLED), so it is
21 * mapped through SubscriptionManager the same way the resync path does.
22 */
23 public function verifyVendorSubscription(array $args, $mode = 'current')
24 {
25 $vendorSubscriptionId = Arr::get($args, 'vendor_subscription_id');
26
27 if (!$vendorSubscriptionId) {
28 return new \WP_Error('invalid_subscription', __('A Vendor Subscription ID is required to look up a PayPal subscription.', 'fluent-cart'));
29 }
30
31 $subscription = (new API())->verifySubscription($vendorSubscriptionId, $mode);
32
33 if (is_wp_error($subscription)) {
34 return $subscription;
35 }
36
37 $nextBilling = Arr::get($subscription, 'billing_info.next_billing_time');
38
39 return [
40 'id' => Arr::get($subscription, 'id'),
41 'status' => (new SubscriptionManager)->getCorrectSubscriptionStatus(Arr::get($subscription, 'status')),
42 'customer_id' => Arr::get($subscription, 'subscriber.payer_id'),
43 'amount' => Arr::get($subscription, 'billing_info.last_payment.amount.value', ''),
44 'currency' => strtoupper((string) Arr::get($subscription, 'billing_info.last_payment.amount.currency_code')),
45 'next_billing_date' => $nextBilling ? gmdate('Y-m-d H:i:s', strtotime($nextBilling)) : '',
46 ];
47 }
48
49 public function reSyncSubscriptionFromRemote(Subscription $subscriptionModel)
50 {
51 $order = $subscriptionModel->order;
52
53 $paypalSubscription = (new API())->verifySubscription($subscriptionModel->vendor_subscription_id, $order->mode);
54
55 if (is_wp_error($paypalSubscription)) {
56 return $paypalSubscription;
57 }
58
59 $newPayment = false;
60
61 $subscriptionStatus = (new SubscriptionManager)->getCorrectSubscriptionStatus(Arr::get($paypalSubscription, 'status'));
62 $nextBillingDate = Arr::get($paypalSubscription, 'billing_info.next_billing_time') ?? null;
63
64 $payer = Arr::get($paypalSubscription, 'subscriber', []);
65
66 if ($nextBillingDate) {
67 $nextBillingDate = gmdate('Y-m-d H:i:s', strtotime($nextBillingDate));
68 }
69
70 $subscriptionUpdateData = array_filter([
71 'current_payment_method' => 'paypal',
72 'status' => $subscriptionStatus
73 ]);
74
75 if ($nextBillingDate) {
76 $subscriptionUpdateData['next_billing_date'] = $nextBillingDate;
77 }
78
79 if (Arr::get($paypalSubscription, 'status') === 'CANCELLED') {
80 $statusUpdateTime = Arr::get($paypalSubscription, 'status_update_time');
81 if ($statusUpdateTime) {
82 $subscriptionUpdateData['canceled_at'] = gmdate('Y-m-d H:i:s', strtotime($statusUpdateTime));
83 }
84 }
85
86 $startTime = Arr::get($paypalSubscription, 'start_time');
87 $endTime = DateTime::gmtNow()->format('Y-m-d\TH:i:s.v\Z');
88
89 $response = (new API())->getResource('billing/subscriptions/' . $subscriptionModel->vendor_subscription_id . '/transactions', [
90 'start_time' => $startTime,
91 'end_time' => $endTime
92 ], $order->mode);
93
94 if (is_wp_error($response)) {
95 return $response;
96 }
97
98 // reverse the array to get the latest transaction last
99 $paypalTransactions = array_reverse(Arr::get($response, 'transactions', []));
100
101
102 if (!empty($paypalTransactions)) {
103 foreach ($paypalTransactions as $paypalTransaction) {
104
105 $amount = Helper::toCent(Arr::get($paypalTransaction, 'amount_with_breakdown.gross_amount.value', 0));
106 $chargeId = Arr::get($paypalTransaction, 'id');
107
108 $status = strtolower(Arr::get($paypalTransaction, 'status'));
109
110 if ($status == 'completed') {
111 // PayPal reports when the remote charge completed; that is the
112 // settlement moment, not this resync's run time.
113 $settledAt = DateTime::anyTimeToGmt(Arr::get($paypalTransaction, 'time'))->format('Y-m-d H:i:s');
114
115 // status drives the branch below and meta is merged on update,
116 // so both must be selected — omitting them would read null and
117 // wipe unrelated meta keys.
118 $transaction = OrderTransaction::query()
119 ->select(['id', 'order_id', 'status', 'meta'])
120 ->where('vendor_charge_id', $chargeId)
121 ->first();
122
123 if (!$transaction) {
124 // check if any transaction related to this subscription exists without vendor_charge_id, mainly for first cycle payment
125 $transaction = OrderTransaction::query()
126 ->select(['id', 'order_id', 'status', 'meta'])
127 ->where('subscription_id', $subscriptionModel->id)
128 ->where('vendor_charge_id', '')
129 ->where('total', $amount)
130 ->where('transaction_type', Status::TRANSACTION_TYPE_CHARGE)
131 ->first();
132
133 if ($transaction) {
134 $meta = array_merge($transaction->meta, ['payer' => $payer]);
135 if (empty($meta['settled_at'])) {
136 $meta['settled_at'] = $settledAt;
137 }
138 $transaction->update([
139 'vendor_charge_id' => $chargeId,
140 'status' => Status::TRANSACTION_SUCCEEDED,
141 'payment_method_type' => 'PayPal',
142 'meta' => $meta
143 ]);
144 continue;
145 }
146
147 $transactionData = [
148 'subscription_id' => $subscriptionModel->id,
149 'payment_method' => 'paypal',
150 'vendor_charge_id' => $chargeId,
151 'payment_method_type' => 'PayPal',
152 'total' => $amount,
153 'meta' => [
154 'payer' => $payer,
155 'settled_at' => $settledAt
156 ],
157 'created_at' => DateTime::anyTimeToGmt(Arr::get($paypalTransaction, 'time'))->format('Y-m-d H:i:s'),
158 ];
159
160 $newPayment = true;
161 SubscriptionService::recordRenewalPayment($transactionData, $subscriptionModel, $subscriptionUpdateData);
162 } else if ($transaction->status !== Status::TRANSACTION_SUCCEEDED) {
163 $meta = array_merge($transaction->meta, ['payer' => $payer]);
164 if (empty($meta['settled_at'])) {
165 $meta['settled_at'] = $settledAt;
166 }
167 $transaction->update([
168 'vendor_charge_id' => $chargeId,
169 'status' => Status::TRANSACTION_SUCCEEDED,
170 'meta' => $meta
171 ]);
172 }
173 }
174 }
175 }
176
177 if (!$newPayment) {
178 $subscriptionModel = SubscriptionService::syncSubscriptionStates($subscriptionModel, $subscriptionUpdateData);
179 } else {
180 $subscriptionModel = Subscription::query()->find($subscriptionModel->id);
181 }
182
183 return $subscriptionModel;
184 }
185
186 public function cancel($vendorSubscriptionId, $args = [])
187 {
188 if (!$vendorSubscriptionId) {
189 return new \WP_Error('invalid_subscription', __('Invalid vendor subscription ID.', 'fluent-cart'));
190 }
191
192 // first check , before canceling the subscription
193 $paypalSubscription = (new API())->verifySubscription($vendorSubscriptionId, Arr::get($args, 'mode', ''));
194
195 if (is_wp_error($paypalSubscription)) {
196 return $paypalSubscription;
197 }
198
199 $subscriptionStatus = (new SubscriptionManager)->getCorrectSubscriptionStatus(Arr::get($paypalSubscription, 'status'));
200
201 // CANCELLED and EXPIRED are terminal at PayPal — the cancel API rejects them with SUBSCRIPTION_STATUS_INVALID
202 if (in_array($subscriptionStatus, [Status::SUBSCRIPTION_CANCELED, Status::SUBSCRIPTION_EXPIRED])) {
203 $result = [
204 'status' => $subscriptionStatus
205 ];
206
207 if ($subscriptionStatus === Status::SUBSCRIPTION_CANCELED) {
208 $statusUpdateTime = Arr::get($paypalSubscription, 'status_update_time');
209 $result['canceled_at'] = $statusUpdateTime ? gmdate('Y-m-d H:i:s', strtotime($statusUpdateTime)) : NULL;
210 }
211
212 return $result;
213 }
214
215 $response = API::createResource('billing/subscriptions/' . $vendorSubscriptionId . '/cancel', [
216 'reason' => Arr::get($args, 'reason', __('Subscription canceled.', 'fluent-cart')),
217 ], Arr::get($args, 'mode', ''));
218
219 if (is_wp_error($response)) {
220 return $response;
221 }
222
223 return [
224 'status' => Status::SUBSCRIPTION_CANCELED
225 ];
226 }
227
228 /**
229 * Resume (activate) a suspended PayPal subscription.
230 *
231 * Called by SubscriptionService::resumeSubscription for automatic subscriptions.
232 * Remote first: activates the subscription at PayPal, and only on success flips
233 * the local status and fires the SubscriptionResumed event.
234 *
235 * @param Subscription $subscription
236 * @param string $reason
237 * @return true|\WP_Error
238 */
239 public function resume(Subscription $subscription, $reason = '')
240 {
241 $vendorSubscriptionId = $subscription->vendor_subscription_id;
242
243 if (!$vendorSubscriptionId) {
244 return new \WP_Error('invalid_subscription', __('Invalid vendor subscription ID.', 'fluent-cart'));
245 }
246
247 $order = $subscription->order;
248
249 $response = API::createResource('billing/subscriptions/' . $vendorSubscriptionId . '/activate', [
250 'reason' => $reason ?: __('Subscription resumed.', 'fluent-cart'),
251 ], $order ? $order->mode : '');
252
253 if (is_wp_error($response)) {
254 return $response;
255 }
256
257 $oldStatus = $subscription->status;
258 $subscription->status = Status::SUBSCRIPTION_ACTIVE;
259 $subscription->save();
260
261 $subscription->addLog(
262 'Subscription resumed',
263 $reason ?: __('Subscription resumed via PayPal', 'fluent-cart'),
264 'info'
265 );
266
267 SubscriptionService::dispatchStatusEvent($subscription, 'resumed', [
268 'old_status' => $oldStatus,
269 'reason' => $reason,
270 ]);
271
272 return true;
273 }
274
275 public function getOrCreateNewPlan($subscriptionId, $reason)
276 {
277 (new SubscriptionManager())->getOrCreateNewPlan($subscriptionId, $reason);
278 }
279
280 public function confirmSubscriptionSwitch($data, $subscriptionId)
281 {
282 (new SubscriptionManager())->confirmSubscriptionSwitch($data, $subscriptionId);
283 }
284
285 }
286