PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 / Hooks / Handlers / CustomCheckout / CustomCheckout.php

CustomCheckout.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Hooks/Handlers/CustomCheckout/CustomCheckout.php

252 lines 10.3 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\Hooks\Handlers\CustomCheckout;
4
5 use FluentCart\Api\StoreSettings;
6 use FluentCart\App\Helpers\CartHelper;
7 use FluentCart\App\Helpers\Status;
8 use FluentCart\App\Models\Cart;
9 use FluentCart\App\Models\Order;
10 use FluentCart\App\Services\ProductItemService;
11 use FluentCart\Framework\Support\Arr;
12
13 class CustomCheckout
14 {
15 public function register()
16 {
17 add_action('fluent_cart_action_custom_checkout', [$this, 'handleCustomCheckoutRedirect'], 10, 1);
18 }
19
20 /*
21 * 1. validation
22 * 2. create cart
23 * - subscription
24 * discount, signup etc.
25 * - onetime
26 * 3. redirect to the checkout with cart
27 */
28 public function handleCustomCheckoutRedirect($data)
29 {
30 $orderHash = sanitize_text_field(Arr::get($data, 'order_hash', ''));
31 $order = Order::query()->where('uuid', $orderHash)->first();
32 if (!$order) {
33 die('Invalid order!');
34 }
35
36 if ($order->payment_status === Status::PAYMENT_PAID || $order->status === Status::ORDER_COMPLETED) {
37 die('Order already completed!');
38 }
39
40 $totalDue = $order->total_amount - $order->paid;
41
42 if ($totalDue <= 0) {
43 die('No due amount found!');
44 }
45
46 // Post-tax credits (plan upgrade) live inside manual_discount_total on the order
47 // but must NOT be redistributed as item-level manual discounts on re-pay — that
48 // would shrink the tax base. They are re-applied post-tax via checkout_data below.
49 $prorateCredit = (int) Arr::get($order->config, 'prorate_credit', 0);
50 $upgradeDiscount = (int) Arr::get($order->config, 'upgrade_discount', 0);
51 $itemManualDiscountTotal = max(0, (int) $order->manual_discount_total - $prorateCredit - $upgradeDiscount);
52
53 if ($order->type === Status::ORDER_TYPE_SUBSCRIPTION) {
54 $orderItem = $order->order_items->filter(function ($item) {
55 return !in_array($item->payment_type, ['signup_fee', 'fee']);
56 })->first();
57
58 if (!$orderItem) {
59 die('Subscription order item not found!');
60 }
61
62 $subscriptionItemData = ProductItemService::getItem([
63 'order_id' => $orderItem->order_id,
64 'product_id' => $orderItem->post_id,
65 'variation_id' => $orderItem->object_id,
66 ]);
67
68 if (!$subscriptionItemData || !$subscriptionItemData->variation) {
69 die('Failed to load product data for custom checkout!');
70 }
71
72 $newItem = $subscriptionItemData->variation->toArray();
73 $isCustom = $subscriptionItemData->is_custom;
74
75 Arr::set($newItem, 'item_price', $orderItem->unit_price);
76
77 // Keep the variation's other_info as-is (trial_days, signup_fee, repeat_interval, times).
78 // The variation's other_info has the clean product-level config that CheckoutProcessor
79 // expects when processing the cart.
80
81 if ($isCustom) {
82 Arr::set($newItem, 'post_title', $subscriptionItemData->variation->post_title);
83 Arr::set($newItem, 'variation_type', $subscriptionItemData->variation->variation_type);
84 } else {
85 Arr::set($newItem, 'post_title', $subscriptionItemData->variation->product->post_title);
86 Arr::set($newItem, 'variation_type', $subscriptionItemData->variation->product_detail->variation_type);
87 }
88
89 if ($order->coupon_discount_total > 0 || $itemManualDiscountTotal > 0) {
90 Arr::set($newItem, 'coupon_discount', (int) $order->coupon_discount_total);
91 Arr::set($newItem, 'manual_discount', $itemManualDiscountTotal);
92 }
93
94 $instantCart = CartHelper::generateCartFromCustomVariation($newItem, $orderItem->quantity);
95
96 } else {
97 $instantCart = new Cart();
98 $instantCart->cart_data = static::buildOneTimeCartItems($order, $itemManualDiscountTotal);
99 }
100
101 $primaryBillingAddress = [];
102 if ($order->billing_address) {
103 $primaryBillingAddress = $order->billing_address
104 ->getFormattedDataForCheckout('billing_');
105 }
106 $instantCart->cart_group = 'instant';
107 $instantCart->first_name = $order->customer->first_name;
108 $instantCart->last_name = $order->customer->last_name;
109 $instantCart->email = $order->customer->email;
110 $instantCart->customer_id = $order->customer->customer_id;
111 $instantCart->user_id = $order->customer->user_id;
112 $instantCart->order_id = $order->id;
113 $instantCart->cart_hash = md5('custom_payment_cart_' . wp_generate_uuid4() . time());
114
115 // Preserve original order fees so the custom payment checkout
116 // shows exactly what was charged, not whatever the current filter returns
117 $originalFees = [];
118 $feeOrderItems = $order->order_items->filter(function ($item) {
119 return $item->payment_type === 'fee';
120 });
121
122 foreach ($feeOrderItems as $feeItem) {
123 $otherInfo = $feeItem->other_info ?? [];
124 $originalFees[] = [
125 'key' => Arr::get($otherInfo, 'fee_key', ''),
126 'label' => $feeItem->title,
127 'amount' => (int) $feeItem->unit_price,
128 'source' => Arr::get($otherInfo, 'source', 'custom'),
129 'taxable' => !empty(Arr::get($otherInfo, 'taxable')),
130 'meta' => Arr::get($otherInfo, 'meta', []),
131 ];
132 }
133
134 $checkoutData = [
135 'is_locked' => 'yes',
136 'disable_coupons' => 'yes',
137 'custom_checkout' => 'yes',
138 'form_data' => $primaryBillingAddress,
139 'fees' => $originalFees,
140 'custom_checkout_data' => [
141 'coupon_discount_total' => $order->coupon_discount_total,
142 'manual_discount_total' => $itemManualDiscountTotal,
143 'discount_total' => $order->coupon_discount_total + $itemManualDiscountTotal,
144 'shipping_total' => $order->shipping_total,
145 ],
146 '__cart_notices' => [
147 [
148 'id' => 'custom_payment_notice',
149 'type' => 'info',
150 'content' => sprintf(
151 /* translators: %1$s: the order UUID. */
152 __('You are making payment for your order (#%1$s).', 'fluent-cart'),
153 $order->uuid
154 ),
155 ]
156 ]
157 ];
158
159 if ($order->type === Status::ORDER_TYPE_RENEWAL) {
160 $checkoutData['renewal_order'] = [
161 'due_date' => $order->getMeta('due_date'),
162 ];
163 }
164
165 if ($prorateCredit > 0) {
166 $checkoutData['prorate_credit'] = [
167 'amount' => $prorateCredit,
168 'title' => __('Prorate Credit', 'fluent-cart'),
169 ];
170 }
171
172 if ($upgradeDiscount > 0) {
173 $checkoutData['upgrade_discount'] = [
174 'amount' => $upgradeDiscount,
175 'title' => __('Upgrade Discount', 'fluent-cart'),
176 ];
177 }
178
179 $instantCart->checkout_data = $checkoutData;
180
181 $instantCart->save();
182
183 $cartHash = $instantCart->cart_hash;
184
185 $checkoutUrl = add_query_arg(
186 [
187 'fct_cart_hash' => $cartHash,
188 ],
189 (new StoreSettings())->getCheckoutPage()
190 );
191
192 wp_redirect($checkoutUrl);
193 exit();
194 }
195
196 /**
197 * Build cart items for a one-time (non-subscription) order re-pay, priced at the
198 * order's own unit_price rather than the current catalogue price.
199 */
200 protected static function buildOneTimeCartItems(Order $order, $itemManualDiscountTotal)
201 {
202 $items = [];
203 $productItems = $order->order_items->filter(function ($orderItem) {
204 return !in_array($orderItem->payment_type, ['fee', 'signup_fee']);
205 });
206
207 foreach ($productItems as $orderItem) {
208 $itemData = ProductItemService::getItem([
209 'order_id' => $orderItem->order_id,
210 'product_id' => $orderItem->post_id,
211 'variation_id' => $orderItem->object_id,
212 ]);
213 if (!$itemData || !$itemData->variation) {
214 die('Failed to load product data for custom checkout!');
215 }
216 $item = $itemData->variation->toArray();
217
218 Arr::set($item, 'coupon_discount', (string)($orderItem->discount_total)); // item discount total is always coupon discount + manual discount
219 Arr::set($item, 'tax_amount', $orderItem->tax_amount);
220 Arr::set($item, 'post_title', $orderItem->post_title);
221 Arr::set($item, 'variation_type', $itemData->variation->product_detail->variation_type);
222
223 // Use the order item's unit_price, not the current catalogue price — the order
224 // may carry a price agreed at purchase time (adjusted unit_price, since expired
225 // catalogue price) that differs from what the product sells for today.
226 Arr::set($item, 'item_price', $orderItem->unit_price);
227
228 // For renewal invoices, also strip trial_days so CheckoutProcessor doesn't
229 // apply trial (0) pricing on a renewal.
230 if ($order->type === Status::ORDER_TYPE_RENEWAL) {
231 $otherInfo = Arr::get($item, 'other_info', []);
232 $otherInfo['trial_days'] = 0;
233 Arr::set($item, 'other_info', $otherInfo);
234 }
235
236 if ($itemManualDiscountTotal) {
237 // Use the order item's own stored split rather than proportionally
238 // redistributing the order-level manual_discount_total — a manual
239 // discount concentrated on one item must stay on that item, or this
240 // reconstruction both keeps it there AND leaves a duplicate slice
241 // behind as "coupon_discount" on it.
242 $manualDiscount = max(0, (int) $orderItem->discount_total - (int) $orderItem->coupon_discount);
243 Arr::set($item, 'manual_discount', $manualDiscount);
244 Arr::set($item, 'coupon_discount', (int) $orderItem->coupon_discount);
245 }
246
247 $items[] = CartHelper::generateCartItemCustomItem($item, $orderItem->quantity);
248 }
249
250 return $items;
251 }
252 }