PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 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 All 51 releases
fluent-cart / app / Http / Requests / OrderRequest.php

OrderRequest.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.1, at app/Http/Requests/OrderRequest.php

270 lines 14.0 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\Http\Requests;
4
5 use FluentCart\App\Helpers\CartHelper;
6 use FluentCart\App\Helpers\Helper;
7 use FluentCart\Framework\Foundation\RequestGuard;
8 use FluentCart\Framework\Support\Arr;
9
10 class OrderRequest extends RequestGuard
11 {
12
13 /**
14 * @return array
15 */
16 public function rules(): array
17 {
18 return [
19 'status' => 'nullable|sanitizeText|maxLength:50',
20 'invoice_no' => 'nullable|sanitizeText|maxLength:100',
21 'fulfillment_type' => 'nullable|sanitizeText|maxLength:50',
22 'type' => 'nullable|sanitizeText|maxLength:50',
23 'payment_method' => 'nullable|sanitizeText|maxLength:50',
24 'payment_method_title' => 'nullable|sanitizeText|maxLength:50',
25 'payment_status' => 'nullable|sanitizeText|maxLength:50',
26 'currency' => 'nullable|sanitizeText|maxLength:10',
27 'subtotal' => 'numeric',
28 'discount_tax' => 'numeric',
29 'manual_discount_total' => 'numeric',
30 'coupon_discount_total' => 'numeric',
31 'shipping_tax' => 'numeric',
32 // min/max close the silent-corruption window: 1e19 passes `numeric`
33 // but wraps to a negative BIGINT through a float-to-int cast, and a
34 // negative shipping charge has no meaning. The bound matches the
35 // Helper::roundCent() guard (float's exact-integer range).
36 'shipping_total' => 'numeric|min:0|max:9000000000000000',
37 'tax_total' => 'numeric',
38 'total_amount' => 'numeric',
39 'rate' => 'numeric',
40 'note' => 'nullable|sanitizeTextArea|maxLength:5000',
41 'uuid' => 'nullable|sanitizeText|maxLength:100',
42 'ip_address' => 'nullable|sanitizeText|maxLength:100',
43 'completed_at' => 'nullable|sanitizeText|maxLength:100',
44 'refunded_at' => 'nullable|sanitizeText|maxLength:100',
45 'customer_id' => 'required|numeric',
46 'user_tz' => 'nullable|sanitizeText|maxLength:50',
47
48 'order_items' => 'required|array',
49 "order_items.*.id" => 'numeric|min:1',
50 "order_items.*.order_id" => 'numeric|min:1',
51 "order_items.*.post_id" => 'numeric|min:1',
52 "order_items.*.variation_id" => 'numeric|min:1',
53 "order_items.*.object_id" => 'numeric|min:1',
54 "order_items.*.fulfillment_type" => 'nullable|sanitizeText',
55 "order_items.*.payment_type" => 'nullable|sanitizeText|maxLength:100',
56 "order_items.*.quantity" => 'numeric|min:1|max:' . CartHelper::maxQuantity(),
57 "order_items.*.post_title" => 'nullable|sanitizeText|maxLength:255',
58 "order_items.*.title" => 'nullable|sanitizeText|maxLength:255',
59 "order_items.*.price" => 'numeric',
60 "order_items.*.unit_price" => 'numeric',
61 "order_items.*.shipping_charge" => 'nullable|numeric',
62 "order_items.*.item_cost" => 'numeric',
63 "order_items.*.item_total" => 'numeric',
64 "order_items.*.tax_amount" => 'numeric',
65 "order_items.*.discount_total" => 'numeric',
66 "order_items.*.total" => 'numeric',
67 "order_items.*.line_total" => 'numeric',
68 "order_items.*.cart_index" => 'nullable|numeric',
69 "order_items.*.rate" => 'nullable|numeric',
70 "order_items.*.line_meta" => 'nullable|array',
71 "order_items.*.other_info" => 'nullable|array',
72
73 "discount.type" => 'nullable|sanitizeText|maxLength:100',
74 "discount.value" => 'nullable|numeric',
75 "discount.label" => 'nullable|sanitizeText|maxLength:100',
76 "discount.reason" => 'nullable|sanitizeText|maxLength:100',
77 "discount.action" => 'nullable|sanitizeText|maxLength:100',
78
79 'shipping' => 'nullable|array',
80 "shipping.*.type" => 'nullable|sanitizeText|maxLength:100',
81 "shipping.*.rate_name" => 'nullable|sanitizeText|maxLength:100',
82 "shipping.*.custom_price" => 'nullable|numeric',
83
84 'deletedItems' => 'nullable|array',
85 'tax_behavior' => 'nullable|numeric|min:0',
86 'tax_lines' => 'nullable|array',
87 'tax_lines.*.rate_id' => 'nullable|numeric|min:0',
88 'tax_lines.*.tax_amount' => 'nullable|numeric|min:0',
89 'tax_lines.*.label' => 'nullable|sanitizeText',
90 'tax_lines.*.is_compound'=> 'nullable',
91
92 // `applied_coupon` is the admin order screen handing back, untouched, what
93 // POST coupons/apply returned: a map KEYED BY COUPON CODE whose rows are
94 // CouponServiceAdmin discount data (see ensureCouponExistInDiscountData()),
95 // NOT fct_applied_coupons rows. AdminOrderProcessor::insertAppliedCoupons()
96 // reads the code keys plus `id` and `discount` and builds its insert rows
97 // from the Coupon model, so those two are the whole load-bearing contract;
98 // everything else in the map is display metadata.
99 //
100 // The previous rules described fct_applied_coupons columns (coupon_id, code,
101 // discounted_amount, stackable) that no caller has ever sent. They were inert
102 // while the validator skipped absent wildcard children, and became a hard
103 // 422 on every coupon order once it started materializing them.
104 //
105 // The per-row closure is the backstop, not decoration: whether the wildcard
106 // rules below can fire at all depends on the validator materializing absent
107 // children, so on its own `applied_coupon.*.id => required` is silently
108 // unenforced on older framework builds. insertAppliedCoupons() subscripts
109 // ['id'] unguarded, so an entry without one writes a null coupon_id.
110 'applied_coupon' => ['nullable', 'array', function ($attribute, $value) {
111 if (!is_array($value)) {
112 return null; // the `array` rule already reports this
113 }
114
115 foreach ($value as $code => $row) {
116 $couponId = is_array($row) ? Arr::get($row, 'id') : null;
117
118 if (!is_numeric($couponId) || (int) $couponId < 1) {
119 return sprintf(
120 /* translators: %1$s: the coupon code the admin applied to the order. */
121 __('The applied coupon "%1$s" is missing its coupon id.', 'fluent-cart'),
122 sanitize_text_field((string) $code)
123 );
124 }
125 }
126
127 return null;
128 }],
129 "applied_coupon.*.id" => 'required|numeric|min:1',
130 // Bounded for the same reason as shipping_total above: sanitize() routes this
131 // through Helper::roundCent(), which throws outside float's exact-integer
132 // range, and a negative coupon discount has no meaning.
133 "applied_coupon.*.discount" => 'required|numeric|min:0|max:9000000000000000',
134 "applied_coupon.*.title" => 'nullable|sanitizeText|maxLength:192',
135 "applied_coupon.*.type" => 'nullable|sanitizeText|maxLength:100',
136 "applied_coupon.*.amount" => 'nullable|numeric',
137 "applied_coupon.*.actual_amount" => 'nullable|numeric',
138 "applied_coupon.*.unit_amount" => 'nullable|numeric',
139 "applied_coupon.*.actual_quantity" => 'nullable|numeric',
140 'trigger' => 'nullable|string',
141 ];
142 }
143
144
145 /**
146 * @return array
147 */
148 public function messages(): array
149 {
150 return [
151 'customer_id.required' => esc_html__('Customer selection is required', 'fluent-cart'),
152 'order_items.required' => esc_html__('Item selection is required', 'fluent-cart'),
153 ];
154 }
155
156
157 /**
158 * @return array
159 */
160 public function sanitize()
161 {
162 return [
163 'id' => 'intval',
164 'status' => 'sanitize_text_field',
165 'invoice_no' => 'sanitize_text_field',
166 'fulfillment_type' => 'sanitize_text_field',
167 'type' => 'sanitize_text_field',
168 'customer_id' => 'intval',
169 'payment_method' => 'sanitize_text_field',
170 'payment_method_title' => 'sanitize_text_field',
171 'payment_status' => 'sanitize_text_field',
172 'currency' => 'sanitize_text_field',
173 'subtotal' => 'floatval',
174 'discount_tax' => 'floatval',
175 'manual_discount_total' => 'floatval',
176 'coupon_discount_total' => 'floatval',
177 'shipping_tax' => 'floatval',
178 // Cents column: normalize at the boundary so every consumer of this request
179 // receives a whole-cent int. floatval alone let a client-computed 19.99 * 100
180 // arrive as 1998.9999999999998, which any later int cast would truncate.
181 //
182 // Wrapped in a closure, NOT passed as [Helper::class, 'roundCent']: an array
183 // value in this map is a LIST of callbacks, iterated one by one
184 // (vendor/wpfluent/framework/src/WPFluent/Support/Sanitizer.php:456-464), so the
185 // array-callable form would try to call Helper() as a function.
186 'shipping_total' => function ($value) {
187 return Helper::roundCent($value);
188 },
189 'tax_total' => 'floatval',
190 'tax_behavior' => 'intval',
191 'total_amount' => 'floatval',
192 'rate' => 'sanitize_text_field',
193 'note' => 'sanitize_textarea_field',
194 'uuid' => 'sanitize_text_field',
195 'ip_address' => 'sanitize_text_field',
196 'billing_address_id' => 'intval',
197 'shipping_address_id' => 'intval',
198 'completed_at' => 'sanitize_text_field',
199 'refunded_at' => 'sanitize_text_field',
200 'user_tz' => 'sanitize_text_field',
201
202 "order_items.*.id" => 'intval',
203 "order_items.*.order_id" => 'intval',
204 "order_items.*.post_id" => 'intval',
205 "order_items.*.object_id" => 'intval',
206 "order_items.*.payment_type" => 'sanitize_text_field',
207 "order_items.*.quantity" => 'intval',
208 "order_items.*.post_title" => 'sanitize_text_field',
209 "order_items.*.title" => 'sanitize_text_field',
210 "order_items.*.shipping_charge" => 'intval',
211 "order_items.*.price" => 'floatval',
212 "order_items.*.unit_price" => 'floatval',
213 "order_items.*.item_cost" => 'floatval',
214 "order_items.*.item_total" => 'floatval',
215 "order_items.*.tax_amount" => 'floatval',
216 "order_items.*.discount_total" => 'floatval',
217 "order_items.*.total" => 'floatval',
218 "order_items.*.line_total" => 'floatval',
219 "order_items.*.cart_index" => 'intval',
220 "order_items.*.rate" => 'floatval',
221 "order_items.*.line_meta" => function ($value) {
222 return is_array($value) ? $value : [];
223 },
224 "order_items.*.other_info" => function ($value) {
225 return is_array($value) ? $value : [];
226 },
227
228 "discount.type" => 'sanitize_text_field',
229 "discount.value" => 'floatval',
230 "discount.label" => 'sanitize_text_field',
231 "discount.reason" => 'sanitize_text_field',
232 "discount.action" => 'sanitize_text_field',
233
234 "shipping.*.type" => 'sanitize_text_field',
235 "shipping.*.rate_name" => 'sanitize_text_field',
236 "shipping.*.custom_price" => 'floatval',
237
238 "deletedItems" => function ($value) {
239 return is_array($value) ? $value : [];
240 },
241 "tax_lines" => function ($value) {
242 return is_array($value) ? $value : [];
243 },
244 "tax_lines.*.rate_id" => 'intval',
245 "tax_lines.*.tax_amount" => 'intval',
246 "tax_lines.*.label" => 'sanitize_text_field',
247 "tax_lines.*.is_compound"=> function ($value) {
248 return (bool) $value;
249 },
250
251 // Mirrors rules(): the coupons/apply discount-data shape, keyed by coupon code.
252 "applied_coupon.*.id" => 'intval',
253 // Already cents (CouponServiceAdmin rounds the distributed discount to two
254 // decimals in cents) — normalize the float artifact without scaling. A bare
255 // intval() here truncates, so a 9.99 discount would persist a cent short.
256 "applied_coupon.*.discount" => function ($value) {
257 return Helper::roundCent($value);
258 },
259 "applied_coupon.*.title" => 'sanitize_text_field',
260 "applied_coupon.*.type" => 'sanitize_text_field',
261 "applied_coupon.*.amount" => 'intval',
262 "applied_coupon.*.actual_amount" => 'floatval',
263 "applied_coupon.*.unit_amount" => 'intval',
264 "applied_coupon.*.actual_quantity" => 'intval',
265 'trigger' => 'sanitize_text_field',
266 ];
267
268 }
269 }
270