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

245 lines 9.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\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' => 'You are making payment for your order (#' . $order->uuid . ').',
151 ]
152 ]
153 ];
154
155 if ($order->type === Status::ORDER_TYPE_RENEWAL) {
156 $checkoutData['renewal_order'] = [
157 'due_date' => $order->getMeta('due_date'),
158 ];
159 }
160
161 if ($prorateCredit > 0) {
162 $checkoutData['prorate_credit'] = [
163 'amount' => $prorateCredit,
164 'title' => __('Prorate Credit', 'fluent-cart'),
165 ];
166 }
167
168 if ($upgradeDiscount > 0) {
169 $checkoutData['upgrade_discount'] = [
170 'amount' => $upgradeDiscount,
171 'title' => __('Upgrade Discount', 'fluent-cart'),
172 ];
173 }
174
175 $instantCart->checkout_data = $checkoutData;
176
177 $instantCart->save();
178
179 $cartHash = $instantCart->cart_hash;
180
181 $checkoutUrl = add_query_arg(
182 [
183 'fct_cart_hash' => $cartHash,
184 ],
185 (new StoreSettings())->getCheckoutPage()
186 );
187
188 wp_redirect($checkoutUrl);
189 exit();
190 }
191
192 /**
193 * Build cart items for a one-time (non-subscription) order re-pay, priced at the
194 * order's own unit_price rather than the current catalogue price.
195 */
196 protected static function buildOneTimeCartItems(Order $order, $itemManualDiscountTotal)
197 {
198 $items = [];
199 $productItems = $order->order_items->filter(function ($orderItem) {
200 return !in_array($orderItem->payment_type, ['fee', 'signup_fee']);
201 });
202
203 foreach ($productItems as $orderItem) {
204 $itemData = ProductItemService::getItem([
205 'order_id' => $orderItem->order_id,
206 'product_id' => $orderItem->post_id,
207 'variation_id' => $orderItem->object_id,
208 ]);
209 if (!$itemData || !$itemData->variation) {
210 die('Failed to load product data for custom checkout!');
211 }
212 $item = $itemData->variation->toArray();
213
214 Arr::set($item, 'coupon_discount', (string)($orderItem->discount_total)); // item discount total is always coupon discount + manual discount
215 Arr::set($item, 'tax_amount', $orderItem->tax_amount);
216 Arr::set($item, 'post_title', $orderItem->post_title);
217 Arr::set($item, 'variation_type', $itemData->variation->product_detail->variation_type);
218
219 // Use the order item's unit_price, not the current catalogue price — the order
220 // may carry a price agreed at purchase time (adjusted unit_price, since expired
221 // catalogue price) that differs from what the product sells for today.
222 Arr::set($item, 'item_price', $orderItem->unit_price);
223
224 // For renewal invoices, also strip trial_days so CheckoutProcessor doesn't
225 // apply trial (0) pricing on a renewal.
226 if ($order->type === Status::ORDER_TYPE_RENEWAL) {
227 $otherInfo = Arr::get($item, 'other_info', []);
228 $otherInfo['trial_days'] = 0;
229 Arr::set($item, 'other_info', $otherInfo);
230 }
231
232 if ($itemManualDiscountTotal) {
233 $subtotal = Arr::get($orderItem, 'subtotal');
234 $manualDiscount = ($subtotal * $itemManualDiscountTotal) / $order->subtotal;
235 $couponDiscount = max(0, $orderItem->discount_total - $manualDiscount);
236 Arr::set($item, 'manual_discount', $manualDiscount);
237 Arr::set($item, 'coupon_discount', $couponDiscount);
238 }
239
240 $items[] = CartHelper::generateCartItemCustomItem($item, $orderItem->quantity);
241 }
242
243 return $items;
244 }
245 }