PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / trunk
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler vtrunk
1.6.6 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 All 49 releases
fluent-cart / app / Services / Payments / PaymentHelper.php

PaymentHelper.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler trunk, at app/Services/Payments/PaymentHelper.php

301 lines 12.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\Services\Payments;
4
5 use FluentCart\Api\StoreSettings;
6 use FluentCart\App\Helpers\Helper;
7 use FluentCart\App\Helpers\Status;
8 use FluentCart\App\Models\OrderTransaction;
9 use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager;
10 use FluentCart\App\Services\URL;
11 use FluentCart\Framework\Support\Arr;
12
13 class PaymentHelper
14 {
15 public string $slug = '';
16
17 public function __construct($slug)
18 {
19 $this->slug = $slug;
20 }
21
22 public function listenerUrl($args = [])
23 {
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)];
29
30 return apply_filters('fluent_cart/ipn_url_' . $this->slug, $data);
31 }
32
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)
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
50 $queryArgs = array_merge(
51 array(
52 'method' => $this->slug,
53 'trx_hash' => $uuid,
54 'fct_redirect' => 'yes'
55 ),
56 is_array($args) ? $args : []
57 );
58
59 $receiptUrl = (new StoreSettings())->getReceiptPage();
60
61 if (empty($receiptUrl)) {
62 $receiptUrl = site_url();
63 }
64
65 $context = [
66 'transaction_hash' => $uuid,
67 'args' => $args,
68 'payment_method' => $this->slug ?? '',
69 'transaction' => $transactionModel,
70 'order' => $transactionModel ? $transactionModel->order : null
71 ];
72 return apply_filters('fluent_cart/payment/success_url', add_query_arg($queryArgs, $receiptUrl), $context);
73 }
74
75 public static function getCustomPaymentLink($orderHash): string
76 {
77 return wp_sanitize_redirect(
78 URL::appendQueryParams(home_url('/?fluent-cart=custom_checkout'), [
79 'order_hash' => $orderHash,
80 ]));
81 }
82
83 /*
84 *This will be hooked from basePaymentMethod
85 * validate payment method is active for checkout items, before order creation
86 */
87 public static function validateAndGetPayMethod($cartCheckoutHelper, $orderData, $extraCharge = 0)
88 {
89 $paymentMethod = Arr::get($orderData, 'others._fct_pay_method');
90 $isZeroPayment = $cartCheckoutHelper->getItemsAmountTotal(false, false) + $extraCharge <= 0;
91 $zeroMethodForced = false;
92 if ($isZeroPayment && $cartCheckoutHelper->getCart()->getEstimatedRecurringTotal() <= 0) {
93 $paymentMethod = apply_filters('fluent_cart/default_payment_method_for_zero_payment', 'offline_payment', []);
94 $zeroMethodForced = true;
95 }
96
97 if (!GatewayManager::has($paymentMethod)) {
98 wp_send_json([
99 'status' => 'failed',
100 'message' => __('No valid payment method found!', 'fluent-cart'),
101 'data' => []
102 ], 423
103 );
104 };
105
106 $gateway = GatewayManager::getInstance($paymentMethod);
107 $status = $gateway->validatePaymentMethod([
108 'isValid' => false,
109 'reason' => __('No payment method found!', 'fluent-cart'),
110 'isZeroPayment' => $isZeroPayment
111 ]);
112
113 if (!Arr::get($status, 'isValid')) {
114 wp_send_json(
115 [
116 'status' => 'failed',
117 'message' => Arr::get($status, 'reason'),
118 'data' => []
119 ], 423
120 );
121 }
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
148 return $paymentMethod;
149 }
150
151 public static function updateTransactionRefundedTotal($parentTransaction, $refundedAmount)
152 {
153 // update the transaction. only update refunded_total, in meta, if exist add
154 $meta = $parentTransaction->meta;
155 $alreadyRefunded = Arr::get($meta, 'refunded_total', 0);
156 $meta['refunded_total'] = $alreadyRefunded + $refundedAmount;
157 $parentTransaction->meta = $meta;
158 $parentTransaction->save();
159 }
160
161 // public static function updateOrderRefundedTotal($order, $refundedAmount, &$type): void
162 // {
163 // // update order data
164 // $netOrderPaidAmount = $order->total_paid - $order->total_refunded;
165 // $isFullRefund = $refundedAmount == $netOrderPaidAmount;
166 //
167 // if ($isFullRefund) {
168 // $order->payment_status = Status::PAYMENT_REFUNDED;
169 // $type = 'full';
170 // } else {
171 // $order->payment_status = Status::PAYMENT_PARTIALLY_REFUNDED;
172 // $type = 'partial';
173 // }
174 // $order->total_refund += $refundedAmount;
175 // $order->save();
176 // }
177
178 /**
179 * @param string $paymentMethod
180 * @param array $paymentMethodDetails
181 * @param array $additionalData
182 * @return array
183 * parse payment method details and return a common format
184 * filter hook: 'fluent_cart/payments/parse_payment_method_details'
185 */
186 public static function parsePaymentMethodDetails($paymentGateway, $paymentMethodDetails, $additionalData = []): array
187 {
188 $billingInfo = [
189 'method' => $paymentGateway,
190 'type' => null,
191 'details' => [],
192 'billing_details' => [
193 'name' => null,
194 'email' => null,
195 'phone' => null,
196 'address' => [
197 'country' => null,
198 'postal_code' => null,
199 'line1' => null,
200 'line2' => null,
201 'city' => null,
202 'state' => null,
203 ]
204 ]
205 ];
206
207 if ($paymentGateway === 'stripe') {
208 $type = Arr::get($paymentMethodDetails, 'type', 'card');
209 $billingInfo['type'] = $type;
210
211 if ($type === 'card') {
212 $billingInfo['details'] = [
213 'type' => 'card',
214 'brand' => sanitize_text_field(Arr::get($paymentMethodDetails, 'card.brand')),
215 'last_4' => sanitize_text_field(Arr::get($paymentMethodDetails, 'card.last4')),
216 'exp_month' => sanitize_text_field(Arr::get($paymentMethodDetails, 'card.exp_month')),
217 'exp_year' => sanitize_text_field(Arr::get($paymentMethodDetails, 'card.exp_year')),
218 'fingerprint' => sanitize_text_field(Arr::get($paymentMethodDetails, 'card.fingerprint')),
219 'payment_method_id' => sanitize_text_field(Arr::get($paymentMethodDetails, 'id'))
220 ];
221 } else {
222 // For other Stripe payment methods (ACH, SEPA, etc.)
223 $billingInfo['details'] = [
224 'payment_method_id' => sanitize_text_field(Arr::get($paymentMethodDetails, 'id')),
225 'type' => Arr::get($paymentMethodDetails, $type, [])
226 ];
227 }
228
229 // Common billing details for Stripe
230 $billingInfo['billing_details'] = [
231 'name' => sanitize_text_field(Arr::get($paymentMethodDetails, 'billing_details.name')),
232 'email' => sanitize_email(Arr::get($paymentMethodDetails, 'billing_details.email', '')),
233 'phone' => sanitize_text_field(Arr::get($paymentMethodDetails, 'billing_details.phone')),
234 'address' => [
235 'country' => sanitize_text_field(Arr::get($paymentMethodDetails, 'billing_details.address.country')),
236 'postal_code' => sanitize_text_field(Arr::get($paymentMethodDetails, 'billing_details.address.postal_code')),
237 'line1' => sanitize_text_field(Arr::get($paymentMethodDetails, 'billing_details.address.line1')),
238 'line2' => sanitize_text_field(Arr::get($paymentMethodDetails, 'billing_details.address.line2')),
239 'city' => sanitize_text_field(Arr::get($paymentMethodDetails, 'billing_details.address.city')),
240 'state' => sanitize_text_field(Arr::get($paymentMethodDetails, 'billing_details.address.state')),
241 ]
242 ];
243
244 } elseif ($paymentGateway === 'paypal') {
245 $billingInfo['type'] = 'standard';
246 $billingInfo['details'] = [
247 'email' => sanitize_email(Arr::get($paymentMethodDetails, 'email', '')),
248 'payer_id' => sanitize_text_field(Arr::get($paymentMethodDetails, 'payer_id')),
249 ];
250
251 // PayPal billing details
252 $billingInfo['billing_details'] = [
253 'name' => sanitize_text_field(Arr::get($paymentMethodDetails, 'name')),
254 'email' => sanitize_email(Arr::get($paymentMethodDetails, 'email', '')),
255 'phone' => sanitize_text_field(Arr::get($paymentMethodDetails, 'phone')),
256 'address' => [
257 'country' => sanitize_text_field(Arr::get($paymentMethodDetails, 'address.country_code')),
258 'postal_code' => sanitize_text_field(Arr::get($paymentMethodDetails, 'address.postal_code')),
259 'line1' => sanitize_text_field(Arr::get($paymentMethodDetails, 'address.address_line_1')),
260 'line2' => sanitize_text_field(Arr::get($paymentMethodDetails, 'address.address_line_2')),
261 ]
262 ];
263 }
264
265 return $billingInfo;
266 }
267
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 */
275 public static function getIntervalDays($interval = ''): int
276 {
277 if ($interval === 'yearly') {
278 $days = 365;
279 } elseif ($interval === 'monthly') {
280 $days = (int) gmdate('t'); // exact days in current month (handles leap year) to avoid false remaining days calculation
281 } elseif ($interval === 'weekly') {
282 $days = 7;
283 } elseif ($interval === 'quarterly') {
284 $days = 90;
285 } elseif ($interval === 'half_yearly') {
286 $days = 182;
287 } elseif ($interval === 'daily') {
288 $days = 1;
289 } else {
290 $days = 0;
291 }
292
293 $days = (int) apply_filters('fluent_cart/subscription_interval_in_days', $days, [
294 'interval' => $interval
295 ]);
296
297 return $days > 0 ? $days : 0;
298 }
299
300 }
301