| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCart\App\Modules\PaymentMethods\PayPalGateway; |
| 4 |
|
| 5 |
use FluentCart\App\App; |
| 6 |
use FluentCart\App\Helpers\Status; |
| 7 |
use FluentCart\App\Models\Order; |
| 8 |
use FluentCart\App\Models\OrderTransaction; |
| 9 |
use FluentCart\App\Models\ProductVariation; |
| 10 |
use FluentCart\App\Models\Subscription; |
| 11 |
use FluentCart\App\Models\SubscriptionMeta; |
| 12 |
use FluentCart\App\Modules\PaymentMethods\PayPalGateway\API\API; |
| 13 |
use FluentCart\App\Services\DateTime\DateTime; |
| 14 |
use FluentCart\App\Services\Payments\PaymentHelper; |
| 15 |
use FluentCart\App\Services\Payments\SubscriptionHelper; |
| 16 |
use FluentCart\Framework\Support\Arr; |
| 17 |
|
| 18 |
class SubscriptionManager |
| 19 |
{ |
| 20 |
|
| 21 |
/** |
| 22 |
* @throws \Exception |
| 23 |
*/ |
| 24 |
public function pauseSubscription($data, $order, $subscription) |
| 25 |
{ |
| 26 |
if (!current_user_can('manage_options')) { |
| 27 |
throw new \Exception(esc_html__('Sorry, You do not have permission to pause subscription!', 'fluent-cart')); |
| 28 |
} |
| 29 |
|
| 30 |
$vendorSubscriptionId = Arr::get($data, 'vendor_subscription_id'); |
| 31 |
$reason = Arr::get($data, 'reason'); |
| 32 |
|
| 33 |
if (!$vendorSubscriptionId || !$subscription) { |
| 34 |
throw new \Exception(esc_html__('Sorry, Subscription not found!', 'fluent-cart')); |
| 35 |
} |
| 36 |
|
| 37 |
try { |
| 38 |
(new API())->makeRequest('billing/subscriptions/' . $vendorSubscriptionId . '/suspend', 'v1', 'POST', [ |
| 39 |
'reason' => $reason |
| 40 |
]); |
| 41 |
} catch (\Exception $e) { |
| 42 |
throw new \Exception(esc_html($e->getMessage())); |
| 43 |
} |
| 44 |
|
| 45 |
$subscription = Subscription::query()->where('id', $subscription->id)->first(); |
| 46 |
if ($subscription) { |
| 47 |
$subscription->status = Status::SUBSCRIPTION_PAUSED; |
| 48 |
$subscription->save(); |
| 49 |
} |
| 50 |
|
| 51 |
wp_send_json(array( |
| 52 |
'message' => __('Subscription has been paused successfully', 'fluent-cart') |
| 53 |
), 200); |
| 54 |
} |
| 55 |
|
| 56 |
/** |
| 57 |
* @throws \Exception |
| 58 |
*/ |
| 59 |
public function resumeSubscription($data, $order, $subscription) |
| 60 |
{ |
| 61 |
if (!current_user_can('manage_options')) { |
| 62 |
throw new \Exception(esc_html__('Sorry, You do not have permission to resume subscription!', 'fluent-cart')); |
| 63 |
} |
| 64 |
|
| 65 |
$vendorSubscriptionId = Arr::get($data, 'vendor_subscription_id'); |
| 66 |
$reason = Arr::get($data, 'reason'); |
| 67 |
|
| 68 |
if (!$vendorSubscriptionId || !$subscription) { |
| 69 |
throw new \Exception(esc_html__('Sorry, Subscription not found!', 'fluent-cart')); |
| 70 |
} |
| 71 |
|
| 72 |
$response = (new API())->makeRequest('billing/subscriptions/' . $vendorSubscriptionId . '/activate', 'v1', 'POST', [ |
| 73 |
'reason' => $reason ?? 'Customer requested' |
| 74 |
]); |
| 75 |
|
| 76 |
if (is_wp_error($response)) { |
| 77 |
throw new \Exception(esc_html($response->get_error_message())); |
| 78 |
} |
| 79 |
|
| 80 |
$subscription = Subscription::query()->where('id', $subscription->id)->first(); |
| 81 |
if ($subscription) { |
| 82 |
$subscription->status = Status::SUBSCRIPTION_ACTIVE; |
| 83 |
$subscription->save(); |
| 84 |
} |
| 85 |
|
| 86 |
wp_send_json(array( |
| 87 |
'message' => __('Subscription has been resumed successfully', 'fluent-cart') |
| 88 |
), 200); |
| 89 |
|
| 90 |
} |
| 91 |
|
| 92 |
/** |
| 93 |
* @throws \Exception |
| 94 |
*/ |
| 95 |
public function getOrCreateNewPlan($subscriptionId, $reason) |
| 96 |
{ |
| 97 |
if (!$subscriptionId) { |
| 98 |
wp_send_json([ |
| 99 |
'message' => __('Sorry, Subscription ID is not available!', 'fluent-cart'), |
| 100 |
], 423); |
| 101 |
} |
| 102 |
|
| 103 |
$subscriptionModel = Subscription::query()->where('id', $subscriptionId)->first(); |
| 104 |
$order = Order::query()->where('id', $subscriptionModel->parent_order_id)->with('order_items')->first(); |
| 105 |
if (!$subscriptionModel || !$order) { |
| 106 |
wp_send_json([ |
| 107 |
'message' => __('Sorry, Subscription or Order not found!', 'fluent-cart'), |
| 108 |
], 423); |
| 109 |
} |
| 110 |
|
| 111 |
// get the variation from variation id |
| 112 |
$variation = ProductVariation::query()->findOrFail($subscriptionModel->variation_id); |
| 113 |
|
| 114 |
$processedSubscriptionItem = $this->getSubscriptionItemForUpdate($subscriptionModel, $variation); |
| 115 |
|
| 116 |
$data = wp_parse_args($processedSubscriptionItem, [ |
| 117 |
'order_id' => $subscriptionModel->parent_order_id, |
| 118 |
'product_id' => $subscriptionModel->product_id, |
| 119 |
'variation_id' => $subscriptionModel->variation_id, |
| 120 |
'currency' => $subscriptionModel->order->currency, |
| 121 |
'interval_count' => 1, |
| 122 |
'signup_fee' => 0, // default setup fee in cents ($0.00) |
| 123 |
]); |
| 124 |
|
| 125 |
$plan = PayPalHelper::getPayPalPlan($data); |
| 126 |
|
| 127 |
if (is_wp_error($plan)) { |
| 128 |
wp_send_json([ |
| 129 |
'status' => 'error', |
| 130 |
'message' => $plan->get_error_message(), |
| 131 |
], 422); |
| 132 |
} |
| 133 |
|
| 134 |
wp_send_json([ |
| 135 |
'status' => 'success', |
| 136 |
'message' => 'Plan created successfully', |
| 137 |
'plan' => $plan, |
| 138 |
], 200); |
| 139 |
} |
| 140 |
|
| 141 |
public function confirmSubscriptionSwitch($data, $subscriptionId) |
| 142 |
{ |
| 143 |
$newVendorSubscriptionId = Arr::get($data, 'newVendorSubscriptionId'); |
| 144 |
$vendorOrderId = Arr::get($data, 'vendorOrderId'); |
| 145 |
|
| 146 |
$subscription = Subscription::query()->where('id', $subscriptionId)->first(); |
| 147 |
$order = Order::query()->where('id', $subscription->parent_order_id)->first(); |
| 148 |
|
| 149 |
if (!$newVendorSubscriptionId || !$vendorOrderId) { |
| 150 |
wp_send_json([ |
| 151 |
'status' => 'error', |
| 152 |
'message' => __('Sorry, New Subscription ID or Vendor Order ID is not available!', 'fluent-cart'), |
| 153 |
], 422); |
| 154 |
} |
| 155 |
|
| 156 |
// get the subscription from paypal |
| 157 |
$paypalSubscription = (new API())->verifySubscription($newVendorSubscriptionId); |
| 158 |
|
| 159 |
if (is_wp_error($paypalSubscription)) { |
| 160 |
// log that subscription was created but not found in paypal or connecting issue |
| 161 |
fluent_cart_error_log( |
| 162 |
__('Subscription was created but not found in paypal or connecting issue', 'fluent-cart'), |
| 163 |
$paypalSubscription->get_error_message(), |
| 164 |
[ |
| 165 |
'module_name' => 'Subscription', |
| 166 |
'module_id' => $subscription->id, |
| 167 |
'log_type' => 'api' |
| 168 |
] |
| 169 |
); |
| 170 |
wp_send_json([ |
| 171 |
'status' => 'error', |
| 172 |
'message' => $paypalSubscription->get_error_message(), |
| 173 |
], 422); |
| 174 |
} |
| 175 |
|
| 176 |
$oldPaymentMethod = $subscription->current_payment_method; |
| 177 |
$oldVendorSubscriptionId = $subscription->vendor_subscription_id; |
| 178 |
$oldVendorCustomerId = $subscription->vendor_customer_id; |
| 179 |
$oldVendorPlanId = $subscription->vendor_plan_id; |
| 180 |
|
| 181 |
$vendorSubscriptionId = Arr::get($paypalSubscription, 'id'); |
| 182 |
$vendorPlanId = Arr::get($paypalSubscription, 'plan_id'); |
| 183 |
$vendorCustomerId = Arr::get($paypalSubscription, 'subscriber.payer_id'); |
| 184 |
|
| 185 |
$nextBillingDate = Arr::get($paypalSubscription, 'billing_info.next_billing_time') ?? null; |
| 186 |
if (!empty($nextBillingDate)) { |
| 187 |
$nextBillingDate = gmdate('Y-m-d H:i:s', strtotime($nextBillingDate)); |
| 188 |
} |
| 189 |
|
| 190 |
$config = $subscription->config ?: []; |
| 191 |
|
| 192 |
// update subscription in table |
| 193 |
$data = array_filter([ |
| 194 |
'vendor_subscription_id' => $vendorSubscriptionId, |
| 195 |
'vendor_plan_id' => $vendorPlanId, |
| 196 |
'current_payment_method' => 'paypal', |
| 197 |
'vendor_customer_id' => $vendorCustomerId, |
| 198 |
'next_billing_date' => $nextBillingDate, |
| 199 |
'status' => $this->getCorrectSubscriptionStatus(Arr::get($paypalSubscription, 'status')), |
| 200 |
'config' => array_merge($config, ['is_trial_days_simulated' => 'yes']) |
| 201 |
]); |
| 202 |
|
| 203 |
Subscription::query()->where('id', $subscriptionId)->update($data); |
| 204 |
|
| 205 |
$billingInfo = PaymentHelper::parsePaymentMethodDetails('paypal', [ |
| 206 |
'email' => Arr::get($paypalSubscription, 'subscriber.email_address'), |
| 207 |
'payer_id' => Arr::get($paypalSubscription, 'subscriber.payer_id'), |
| 208 |
'name' => Arr::get($paypalSubscription, 'subscriber.name.given_name') . ' ' . Arr::get($paypalSubscription, 'subscriber.name.surname'), |
| 209 |
'address' => Arr::get($paypalSubscription, 'subscriber.shipping_address.address') |
| 210 |
]); |
| 211 |
// update subscription meta for active payment method |
| 212 |
|
| 213 |
$subscription->updateMeta('active_payment_method', $billingInfo); |
| 214 |
|
| 215 |
$gateway = App::gateway($oldPaymentMethod); |
| 216 |
if ($gateway && $gateway->subscriptions) { |
| 217 |
$gateway->subscriptions->cancel( |
| 218 |
$oldVendorSubscriptionId, |
| 219 |
[ |
| 220 |
'reason' => __('Subscription switched to PayPal', 'fluent-cart'), |
| 221 |
'mode' => $order->mode |
| 222 |
] |
| 223 |
); |
| 224 |
} |
| 225 |
|
| 226 |
// add or update subscription meta of old subscriptions |
| 227 |
$paymentSource = SubscriptionMeta::query() |
| 228 |
->where('subscription_id', $subscription->id) |
| 229 |
->where('meta_key', 'active_payment_method') |
| 230 |
->first(); |
| 231 |
|
| 232 |
if ($paymentSource) { |
| 233 |
$paymentSource = $paymentSource->meta_value; |
| 234 |
} |
| 235 |
|
| 236 |
$oldSubData = [ |
| 237 |
'payment_method' => $oldPaymentMethod, |
| 238 |
'vendor_subscription_id' => $oldVendorSubscriptionId, |
| 239 |
'vendor_customer_id' => $oldVendorCustomerId, |
| 240 |
'vendor_plan_id' => $oldVendorPlanId, |
| 241 |
'reason' => 'switch_payment_method', |
| 242 |
'payment_source' => $paymentSource ?? '', |
| 243 |
'canceled_at' => DateTime::gmtNow(), |
| 244 |
]; |
| 245 |
|
| 246 |
self::addOldSubscriptionMeta($subscription, $oldSubData); |
| 247 |
|
| 248 |
wp_send_json([ |
| 249 |
'status' => 'success', |
| 250 |
'message' => __('Subscription updated successfully', 'fluent-cart'), |
| 251 |
'data' => $vendorSubscriptionId |
| 252 |
], 200); |
| 253 |
|
| 254 |
} |
| 255 |
|
| 256 |
/** |
| 257 |
* Confirm subscription reactivation |
| 258 |
* |
| 259 |
* @param array $data | newVendorSubscriptionId (string) required |
| 260 |
* @param int $subscriptionId |
| 261 |
* @return void |
| 262 |
*/ |
| 263 |
public static function addOldSubscriptionMeta($subscription, $oldSubData) |
| 264 |
{ |
| 265 |
$defaults = [ |
| 266 |
'payment_method' => '', |
| 267 |
'vendor_subscription_id' => '', |
| 268 |
'vendor_customer_id' => '', |
| 269 |
'vendor_plan_id' => '', |
| 270 |
'bill_count' => 0, |
| 271 |
'payment_source' => '', |
| 272 |
'canceled_at' => null, |
| 273 |
'reason' => '', |
| 274 |
'expire_at' => null, |
| 275 |
]; |
| 276 |
$oldSubscription = array_merge($defaults, $oldSubData); |
| 277 |
|
| 278 |
// get if exists |
| 279 |
$oldSubscriptions = SubscriptionMeta::query() |
| 280 |
->where('subscription_id', '=', $subscription->id) |
| 281 |
->where('meta_key', '=', 'old_subscriptions') |
| 282 |
->first(); |
| 283 |
|
| 284 |
if ($oldSubscriptions && $oldSubscriptions->meta_value) { |
| 285 |
if (is_string($oldSubscriptions->meta_value)) { |
| 286 |
$oldSubscriptions = $oldSubscriptions->meta_value; |
| 287 |
} |
| 288 |
$oldSubscriptions = (array)$oldSubscriptions ?: []; |
| 289 |
$oldSubscriptions[] = $oldSubscription; |
| 290 |
} else { |
| 291 |
$oldSubscriptions = [$oldSubscription]; |
| 292 |
} |
| 293 |
|
| 294 |
SubscriptionMeta::updateOrCreate([ |
| 295 |
'subscription_id' => $subscription->id, |
| 296 |
//phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key |
| 297 |
'meta_key' => 'old_subscriptions' |
| 298 |
], [ |
| 299 |
//phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value |
| 300 |
'meta_value' => $oldSubscriptions |
| 301 |
]); |
| 302 |
} |
| 303 |
|
| 304 |
public function getSubscriptionItemForUpdate($subscriptionModel, $variation) |
| 305 |
{ |
| 306 |
$trialDays = 0; |
| 307 |
|
| 308 |
// trial days is the difference between the next billing date and the current date in days |
| 309 |
$nextBillingDate = $subscriptionModel->next_billing_date; |
| 310 |
$nextBillingTimestamp = strtotime($nextBillingDate); |
| 311 |
|
| 312 |
if ($nextBillingTimestamp && $nextBillingTimestamp > time()) { |
| 313 |
$trialDays = ceil(($nextBillingTimestamp - time()) / 86400); |
| 314 |
} |
| 315 |
|
| 316 |
$billCount = OrderTransaction::query()->where('subscription_id', $subscriptionModel->id) |
| 317 |
->where('transaction_type', Status::TRANSACTION_TYPE_CHARGE) |
| 318 |
->where('status', Status::TRANSACTION_SUCCEEDED) |
| 319 |
->count(); |
| 320 |
|
| 321 |
|
| 322 |
$trialDays = SubscriptionHelper::checkTrailDaysLoopHole($subscriptionModel, $trialDays); |
| 323 |
|
| 324 |
// max trial days is 365 |
| 325 |
if ($trialDays > 365) { |
| 326 |
$trialDays = 365; |
| 327 |
} |
| 328 |
|
| 329 |
$billTimes = Arr::get($subscriptionModel, 'bill_times', 0); |
| 330 |
if ($billTimes && $billCount) { |
| 331 |
$billTimes = $billTimes - $billCount; |
| 332 |
} else { |
| 333 |
$billTimes = 0; |
| 334 |
} |
| 335 |
|
| 336 |
$expireAt = null; |
| 337 |
// expire at is the end date of the subscription, if bill times is 0 then it is null |
| 338 |
if ($billTimes) { |
| 339 |
$expireAt = SubscriptionHelper::getSubscriptionCancelAtTimeStamp($trialDays, $billTimes, $subscriptionModel->billing_interval); |
| 340 |
} |
| 341 |
|
| 342 |
$processedSubscriptionItem = [ |
| 343 |
'billing_interval' => Arr::get($subscriptionModel, 'billing_interval'), |
| 344 |
'recurring_amount' => Arr::get($subscriptionModel, 'recurring_total'), |
| 345 |
'line_total' => intval(Arr::get($subscriptionModel, 'recurring_total')), |
| 346 |
'id' => Arr::get($subscriptionModel, 'variation_id'), |
| 347 |
'trial_days' => $trialDays, |
| 348 |
'product_id' => Arr::get($subscriptionModel, 'product_id'), |
| 349 |
'parent_order_id' => Arr::get($subscriptionModel, 'parent_order_id'), |
| 350 |
'item_name' => Arr::get($subscriptionModel, 'item_name'), |
| 351 |
'expire_at' => $expireAt, |
| 352 |
'bill_times' => $billTimes, |
| 353 |
]; |
| 354 |
|
| 355 |
if ($trialDays > 0) { |
| 356 |
$processedSubscriptionItem['trial_end'] = $nextBillingTimestamp; |
| 357 |
} |
| 358 |
|
| 359 |
return $processedSubscriptionItem; |
| 360 |
|
| 361 |
} |
| 362 |
|
| 363 |
public function getCorrectSubscriptionStatus($status): string |
| 364 |
{ |
| 365 |
$status = strtolower($status); |
| 366 |
if ('active' == $status) { |
| 367 |
$status = Status::SUBSCRIPTION_ACTIVE; |
| 368 |
} else if ('trialing' == $status) { |
| 369 |
$status = Status::SUBSCRIPTION_TRIALING; |
| 370 |
} else if ('cancelled' == $status || 'canceled' == $status) { |
| 371 |
$status = Status::SUBSCRIPTION_CANCELED; |
| 372 |
} else if ('expired' == $status) { |
| 373 |
$status = Status::SUBSCRIPTION_EXPIRED; |
| 374 |
} else if ('paused' == $status) { |
| 375 |
$status = Status::SUBSCRIPTION_PAUSED; |
| 376 |
} else if ('expiring' == $status) { |
| 377 |
$status = Status::SUBSCRIPTION_EXPIRING; |
| 378 |
} else if ('suspended' == $status) { |
| 379 |
$status = Status::SUBSCRIPTION_PAUSED; |
| 380 |
} |
| 381 |
return $status; |
| 382 |
} |
| 383 |
|
| 384 |
public function sendError($message, $code = 422): void |
| 385 |
{ |
| 386 |
wp_send_json([ |
| 387 |
'status' => 'failed', |
| 388 |
'message' => $message |
| 389 |
], $code); |
| 390 |
} |
| 391 |
|
| 392 |
} |
| 393 |
|