| @@ -4,8 +4,9 @@ | ||
| 4 | 4 | |
| 5 | 5 | use FluentCart\Api\StoreSettings; |
| 6 | 6 | use FluentCart\App\Helpers\Helper; |
| 7 | 7 | use FluentCart\App\Helpers\Status; |
| 8 | +use FluentCart\App\Models\OrderTransaction; | |
| 8 | 9 | use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager; |
| 9 | 10 | use FluentCart\App\Services\URL; |
| 10 | 11 | use FluentCart\Framework\Support\Arr; |
| 11 | 12 | |
| @@ -19,18 +20,34 @@ | ||
| 19 | 20 | } |
| 20 | 21 | |
| 21 | 22 | public function listenerUrl($args = []) |
| 22 | 23 | { |
| 23 | - $listener = '?fct_payment_listener=1&method=' . $this->slug; | |
| 24 | - $data = apply_filters_deprecated('fluent_cart_ipn_url_' . $this->slug, [ | |
| 25 | - ['listener_url' => site_url($listener)] | |
| 26 | - ], '1.3.16', 'fluent_cart/ipn_url_' . $this->slug, 'Use fluent_cart/ipn_url_' . $this->slug . ' instead of fluent_cart_ipn_url_' . $this->slug . '.'); | |
| 24 | + // Must match the WebRoutes dispatch contract: it reads $_REQUEST['fluent-cart'] | |
| 25 | + // as the routed page and fires do_action('fluent_cart_action_' . $page), and | |
| 26 | + // GlobalPaymentHandler listens on 'fluent_cart_action_fct_payment_listener_ipn'. | |
| 27 | + $listener = '/?fluent-cart=fct_payment_listener_ipn&method=' . $this->slug; | |
| 28 | + $data = ['listener_url' => site_url($listener)]; | |
| 27 | 29 | |
| 28 | 30 | return apply_filters('fluent_cart/ipn_url_' . $this->slug, $data); |
| 29 | 31 | } |
| 30 | 32 | |
| 31 | - public function successUrl($uuid, $args = null) | |
| 33 | + /** | |
| 34 | + * Build the filterable post-payment success URL. | |
| 35 | + * | |
| 36 | + * @param OrderTransaction|string $transaction Transaction model or its uuid | |
| 37 | + * @param array|null $args Extra query args merged into the URL | |
| 38 | + * @return string | |
| 39 | + */ | |
| 40 | + public function successUrl($transaction, $args = null) | |
| 32 | 41 | { |
| 42 | + if ($transaction instanceof OrderTransaction) { | |
| 43 | + $transactionModel = $transaction; | |
| 44 | + $uuid = $transaction->uuid; | |
| 45 | + } else { | |
| 46 | + $uuid = (string)$transaction; | |
| 47 | + $transactionModel = OrderTransaction::query()->where('uuid', $uuid)->first(); | |
| 48 | + } | |
| 49 | + | |
| 33 | 50 | $queryArgs = array_merge( |
| 34 | 51 | array( |
| 35 | 52 | 'method' => $this->slug, |
| 36 | 53 | 'trx_hash' => $uuid, |
| @@ -47,13 +64,13 @@ | ||
| 47 | 64 | |
| 48 | 65 | $context = [ |
| 49 | 66 | 'transaction_hash' => $uuid, |
| 50 | 67 | 'args' => $args, |
| 51 | - 'payment_method' => $this->slug ?? '' | |
| 68 | + 'payment_method' => $this->slug ?? '', | |
| 69 | + 'transaction' => $transactionModel, | |
| 70 | + 'order' => $transactionModel ? $transactionModel->order : null | |
| 52 | 71 | ]; |
| 53 | - $url = apply_filters_deprecated('fluentcart/payment/success_url', [add_query_arg($queryArgs, $receiptUrl), $context], '1.3.16', 'fluent_cart/payment/success_url', 'Use fluent_cart/payment/success_url instead of fluentcart/payment/success_url.'); | |
| 54 | - | |
| 55 | - return apply_filters('fluent_cart/payment/success_url', $url, $context); | |
| 72 | + return apply_filters('fluent_cart/payment/success_url', add_query_arg($queryArgs, $receiptUrl), $context); | |
| 56 | 73 | } |
| 57 | 74 | |
| 58 | 75 | public static function getCustomPaymentLink($orderHash): string |
| 59 | 76 | { |
| @@ -70,10 +87,12 @@ | ||
| 70 | 87 | public static function validateAndGetPayMethod($cartCheckoutHelper, $orderData, $extraCharge = 0) |
| 71 | 88 | { |
| 72 | 89 | $paymentMethod = Arr::get($orderData, 'others._fct_pay_method'); |
| 73 | 90 | $isZeroPayment = $cartCheckoutHelper->getItemsAmountTotal(false, false) + $extraCharge <= 0; |
| 91 | + $zeroMethodForced = false; | |
| 74 | 92 | if ($isZeroPayment && $cartCheckoutHelper->getCart()->getEstimatedRecurringTotal() <= 0) { |
| 75 | 93 | $paymentMethod = apply_filters('fluent_cart/default_payment_method_for_zero_payment', 'offline_payment', []); |
| 94 | + $zeroMethodForced = true; | |
| 76 | 95 | } |
| 77 | 96 | |
| 78 | 97 | if (!GatewayManager::has($paymentMethod)) { |
| 79 | 98 | wp_send_json([ |
| @@ -100,8 +119,33 @@ | ||
| 100 | 119 | ], 423 |
| 101 | 120 | ); |
| 102 | 121 | } |
| 103 | 122 | |
| 123 | + // The checkout gateway-visibility filters (manual-subscription admission, | |
| 124 | + // store-managed capability gate, renewal pre-due-date block, reactivation | |
| 125 | + // restriction) only hide gateways in the rendered UI. The POSTed method must | |
| 126 | + // pass the same gate, or a crafted request can start/convert a subscription | |
| 127 | + // through a gateway the store never offered. Only the forced-offline zero | |
| 128 | + // checkout (no recurring — the UI renders no method picker and the method is | |
| 129 | + // overridden above) is exempt; a zero-payable SUBSCRIPTION cart (free trial) | |
| 130 | + // keeps the customer-picked gateway and must pass the gate like any other. | |
| 131 | + $cart = $cartCheckoutHelper->getCart(); | |
| 132 | + if ($cart && !$zeroMethodForced) { | |
| 133 | + $allowedGateways = apply_filters('fluent_cart/checkout_active_payment_methods', [$gateway], [ | |
| 134 | + 'cart' => $cart | |
| 135 | + ]); | |
| 136 | + | |
| 137 | + if (!in_array($gateway, (array) $allowedGateways, true)) { | |
| 138 | + wp_send_json( | |
| 139 | + [ | |
| 140 | + 'status' => 'failed', | |
| 141 | + 'message' => __('This payment method is not available for this order. Please choose another payment method!', 'fluent-cart'), | |
| 142 | + 'data' => [] | |
| 143 | + ], 423 | |
| 144 | + ); | |
| 145 | + } | |
| 146 | + } | |
| 147 | + | |
| 104 | 148 | return $paymentMethod; |
| 105 | 149 | } |
| 106 | 150 | |
| 107 | 151 | public static function updateTransactionRefundedTotal($parentTransaction, $refundedAmount) |
| @@ -221,14 +265,20 @@ | ||
| 221 | 265 | return $billingInfo; |
| 222 | 266 | } |
| 223 | 267 | |
| 224 | 268 | |
| 269 | + /** | |
| 270 | + * Days in one billing cycle. Returns 0 when the interval cannot be resolved — | |
| 271 | + * neither a core interval nor one a fluent_cart/subscription_interval_in_days | |
| 272 | + * callback resolved to a positive day count. Callers must treat < 1 as | |
| 273 | + * unresolvable, never as a one-day cycle. | |
| 274 | + */ | |
| 225 | 275 | public static function getIntervalDays($interval = ''): int |
| 226 | 276 | { |
| 227 | 277 | if ($interval === 'yearly') { |
| 228 | 278 | $days = 365; |
| 229 | 279 | } elseif ($interval === 'monthly') { |
| 230 | - $days = 30; | |
| 280 | + $days = (int) gmdate('t'); // exact days in current month (handles leap year) to avoid false remaining days calculation | |
| 231 | 281 | } elseif ($interval === 'weekly') { |
| 232 | 282 | $days = 7; |
| 233 | 283 | } elseif ($interval === 'quarterly') { |
| 234 | 284 | $days = 90; |
| @@ -233,14 +283,18 @@ | ||
| 233 | 283 | } elseif ($interval === 'quarterly') { |
| 234 | 284 | $days = 90; |
| 235 | 285 | } elseif ($interval === 'half_yearly') { |
| 236 | 286 | $days = 182; |
| 287 | + } elseif ($interval === 'daily') { | |
| 288 | + $days = 1; | |
| 237 | 289 | } else { |
| 238 | - $days = 1; | |
| 290 | + $days = 0; | |
| 239 | 291 | } |
| 240 | 292 | |
| 241 | - return apply_filters('fluent_cart/subscription_interval_in_days', $days, [ | |
| 293 | + $days = (int) apply_filters('fluent_cart/subscription_interval_in_days', $days, [ | |
| 242 | 294 | 'interval' => $interval |
| 243 | 295 | ]); |
| 296 | + | |
| 297 | + return $days > 0 ? $days : 0; | |
| 244 | 298 | } |
| 245 | 299 | |
| 246 | 300 | } |