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.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 trunk 1.2.0 All 47 releases
fluent-cart / app / Http / Requests / ProductVariationRequest.php

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

370 lines 16.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\Http\Requests;
4
5 use FluentCart\App\Helpers\Helper;
6 use FluentCart\App\Http\Rules\RequiredWhenRule;
7 use FluentCart\Framework\Foundation\RequestGuard;
8 use FluentCart\Framework\Support\Arr;
9
10 class ProductVariationRequest extends RequestGuard
11 {
12 /**
13 * Normalize variant data before validation.
14 *
15 * This method is primarily used to ensure that the `variants` array contains
16 * the required structure, especially during data migration or when
17 * optional fields like `other_info` are not provided.
18 *
19 * Key operations:
20 * - Sets a default `fulfillment_type` (fallback to 'physical') if missing.
21 * - Sets a default `payment_type` (fallback to 'onetime') if missing.
22 * - Ensures `other_info` exists and assigns default billing/setup fee-related values if it's empty.
23 *
24 * This helps avoid issues during validation or processing by guaranteeing a consistent data structure.
25 *
26 * @return array The normalized data ready for validation.
27 */
28 public function beforeValidation()
29 {
30 $data = $this->all();
31 $fulfilmentType = Arr::get(
32 $data,
33 'variants.fulfillment_type',
34 Arr::get($data, 'variants.fulfillment_type', 'physical')
35 );
36 $paymentType = Arr::get($data, 'variants.payment_type', 'onetime');
37 $manageCost = Arr::get($data, 'variants.manage_cost');
38 if (empty($manageCost)) {
39 $manageCost = 'false';
40 }
41 $data['variants']['fulfillment_type'] = $fulfilmentType;
42 $data['variants']['manage_cost'] = $manageCost;
43
44 $variantOtherInfo = Arr::wrap(Arr::get($data, 'variants.other_info'));
45
46 // Ensure other_info is an array
47 if (empty($variantOtherInfo)) {
48 $variantOtherInfo = [
49 'payment_type' => $paymentType,
50 'times' => '',
51 'trial_days' => '',
52 'repeat_interval' => 'yearly',
53 'billing_summary' => '',
54 'manage_setup_fee' => 'no',
55 'signup_fee_name' => '',
56 'signup_fee' => '',
57 'setup_fee_per_item' => 'no',
58 //'purchasable' => 'yes',
59
60 ];
61 }
62 $data['variants']['other_info'] = $variantOtherInfo;
63
64 if (Arr::get($variantOtherInfo, 'payment_type') === 'onetime') {
65 $subscriptionFields = ['trial_days', 'times', 'repeat_interval', 'billing_summary', 'manage_setup_fee', 'signup_fee', 'signup_fee_name', 'setup_fee_per_item'];
66 foreach ($subscriptionFields as $field) {
67 unset($data['variants']['other_info'][$field]);
68 }
69 }
70
71 return $data;
72 }
73
74 /**
75 * @return array
76 */
77 public function rules()
78 {
79 // total_stock / available / committed / on_hold are `INT(11) NULL DEFAULT 0`
80 // (ProductVariationMigrator), so an empty counter is a legitimate stored state for
81 // a variation that never tracked stock. ProductEditModel::createOrUpdatePricing()
82 // posts the variation back exactly as the drawer loaded it, NULLs included, so
83 // demanding a number unconditionally rejected a plain price edit on such a row.
84 //
85 // This cannot be expressed as `nullable|numeric`: Validator::filterExcludeables()
86 // drops EVERY rule for a falsy value the moment `nullable` is present, which
87 // would disarm the conditional requirement too. It cannot sit beside a
88 // `required_if` string either — filterRequiredIf() discarded this closure along
89 // with every other rule whenever tracking was off, leaving the guard inert. The
90 // requirement is a RequiredWhenRule closure below for that reason.
91 $numericWhenProvided = function ($attribute, $value) {
92 if ($value === null || $value === '') {
93 return null;
94 }
95
96 if (!is_numeric($value)) {
97 return esc_html__('Stock quantity must be a number.', 'fluent-cart');
98 }
99
100 return null;
101 };
102
103 return [
104 'variants.variation_title' => 'required|sanitizeText|maxLength:200',
105 'variants.sku' => 'nullable|sanitizeText|maxLength:30|unique:fct_product_variations,sku' . ($this->get('variants.id') ? ',' . $this->get('variants.id') : ''),
106 'variants.item_price' => 'nullable|numeric|min:0',
107 'variants.compare_price' => [
108 'nullable',
109 'numeric',
110 function ($attribute, $value) {
111 $itemPrice = $this->get("variants.item_price");
112 if (empty($itemPrice)) {
113 $itemPrice = 0;
114 }
115 if ($value !== null && $value < $itemPrice) {
116 return sprintf(__("Compare price must be greater than or equal to item price.", 'fluent-cart'));
117 }
118 return null;
119 },
120 ],
121 'variants.manage_cost' => 'nullable|sanitizeText|maxLength:10',
122 'variants.item_cost' => [
123 RequiredWhenRule::make(
124 'variants.manage_cost',
125 'true',
126 esc_html__('Item cost is required.', 'fluent-cart')
127 ),
128 ],
129 'variants.fulfillment_type' => 'required|sanitizeText|maxLength:100',
130 'variants.shipping_class' => function ($attr, $value) {
131 if ($value && !(\FluentCart\App\Models\ShippingClass::find(intval($value)))) {
132 return __('The selected shipping class does not exist.', 'fluent-cart');
133 }
134 return null;
135 },
136
137 'variants.manage_stock' => 'nullable|numeric',
138 'variants.stock_status' => [
139 RequiredWhenRule::make(
140 'variants.manage_stock',
141 '1',
142 esc_html__('Stock status is required.', 'fluent-cart')
143 ),
144 'sanitizeText',
145 'maxLength:50',
146 ],
147 // Quantities are only demanded once tracking is actually on, mirroring
148 // stock_status directly above.
149 'variants.total_stock' => [
150 RequiredWhenRule::make(
151 'variants.manage_stock',
152 '1',
153 esc_html__('Stock quantity is required when inventory tracking is on.', 'fluent-cart')
154 ),
155 $numericWhenProvided,
156 ],
157 'variants.available' => [
158 RequiredWhenRule::make(
159 'variants.manage_stock',
160 '1',
161 esc_html__('Available quantity is required when inventory tracking is on.', 'fluent-cart')
162 ),
163 $numericWhenProvided,
164 ],
165 // 'variants.available' => [
166 // 'required',
167 // 'numeric',
168 // function ($attribute, $value, $fail) {
169 // if ($this->variants['stock_status'] == 'in-stock' && $value <= 0) {
170 // return __("The available stock must be greater than 0 when stock is set to in stock", 'fluent-cart');
171 // }
172 // return null;
173 // },
174 // ],
175 // Ledger columns the merchant never edits — they are maintained by the stock
176 // listeners, so they only have to be a number when the payload carries one.
177 'variants.committed' => [$numericWhenProvided],
178 'variants.on_hold' => [$numericWhenProvided],
179
180 'variants.serial_index' => 'nullable|numeric',
181
182 'variants.other_info' => 'required|array',
183 'variants.other_info.description' => 'nullable|sanitizeTextArea|maxLength:255',
184 'variants.other_info.payment_type' => 'required|sanitizeText|in:onetime,subscription',
185 'variants.other_info.times' => [
186 function ($attribute, $value) {
187 if ($this->get('variants.other_info.payment_type') !== 'subscription') {
188 return null;
189 }
190 if (!empty($value) && !is_numeric($value)) {
191 return __('Times must be a number.', 'fluent-cart');
192 }
193 return Helper::installmentTimesError($this->get('variants.other_info'));
194 },
195 ],
196 'variants.other_info.trial_days' => [
197 function ($attribute, $value) {
198 if ($this->get('variants.other_info.payment_type') !== 'subscription') {
199 return null;
200 }
201 if (!empty($value) && !is_numeric($value)) {
202 return __('Trial days must be a number.', 'fluent-cart');
203 }
204 if (!empty($value) && $value > 365) {
205 return __('Trial period cannot exceed 365 days.', 'fluent-cart');
206 }
207 return null;
208 },
209 ],
210 'variants.other_info.repeat_interval' => [
211 RequiredWhenRule::make(
212 'variants.other_info.payment_type',
213 'subscription',
214 esc_html__('Interval is required.', 'fluent-cart')
215 ),
216 'sanitizeText',
217 'maxLength:100',
218 ],
219 'variants.other_info.billing_summary' => 'nullable|sanitizeTextArea|maxLength:255',
220 'variants.other_info.manage_setup_fee' => [
221 RequiredWhenRule::make(
222 'variants.other_info.payment_type',
223 'subscription',
224 esc_html__('Setup Fee option is required.', 'fluent-cart')
225 ),
226 'sanitizeText',
227 'maxLength:100',
228 ],
229 'variants.other_info.signup_fee' => [
230 RequiredWhenRule::make(
231 'variants.other_info.manage_setup_fee',
232 'yes',
233 esc_html__('Setup Fee Amount is required.', 'fluent-cart')
234 ),
235 ],
236 'variants.other_info.signup_fee_name' => [
237 RequiredWhenRule::make(
238 'variants.other_info.manage_setup_fee',
239 'yes',
240 esc_html__('Setup Fee Name is required.', 'fluent-cart')
241 ),
242 'sanitizeText',
243 'maxLength:100',
244 ],
245 'variants.other_info.package_slug' => 'nullable|sanitizeText|maxLength:100',
246 'variants.other_info.weight' => 'nullable|numeric',
247 'variants.other_info.weight_unit' => 'nullable|sanitizeText|maxLength:10',
248 'variants.other_info.length' => 'nullable|numeric',
249 'variants.other_info.width' => 'nullable|numeric',
250 'variants.other_info.height' => 'nullable|numeric',
251 'variants.other_info.tax_class' => ['nullable', function ($attribute, $value) {
252 if (empty($value)) {
253 return null;
254 }
255
256 return empty(\FluentCart\App\Models\TaxClass::query()->where('slug', sanitize_text_field($value))->first())
257 ? __('Invalid Tax Class.', 'fluent-cart')
258 : null;
259 }],
260 'variants.other_info.tax_exempt' => 'nullable|sanitizeText|in:yes,no',
261
262 'variants.downloadable' => 'nullable|sanitizeText|maxLength:10',
263 ];
264 }
265
266
267 public function afterValidation($validator): array
268 {
269
270 $data = $this->get();
271
272 $price = $data['variants']['item_price'];
273
274 if (empty($price)) {
275 $data['variants']['item_price'] = 0;
276 }
277
278 return $data;
279 }
280
281
282 /**
283 * @return array
284 */
285 public function messages()
286 {
287 return [
288 'variants.variation_title.required' => esc_html__('Title is required.', 'fluent-cart'),
289 'variants.variation_title.max' => esc_html__('Title may not be greater than 200 characters.', 'fluent-cart'),
290 'variants.sku.max' => esc_html__('SKU may not be greater than 30 characters.', 'fluent-cart'),
291 'variants.sku.unique' => esc_html__('The SKU must be unique.', 'fluent-cart'),
292 'variants.item_price.required' => esc_html__('Price is required.', 'fluent-cart'),
293 'variants.item_price.numeric' => esc_html__('Price must be a number.', 'fluent-cart'),
294 'variants.item_price.min' => esc_html__('Price must be a positive number greater than 0.', 'fluent-cart'),
295 'variants.fulfillment_type.required' => esc_html__('Fulfilment Type is required.', 'fluent-cart'),
296
297 'variants.other_info.description.max' => esc_html__('Description may not be greater than 255 characters.', 'fluent-cart'),
298 'variants.other_info.payment_type.required' => esc_html__('Payment Type is required.', 'fluent-cart'),
299 'variants.other_info.times.required_if' => esc_html__('Times is required.', 'fluent-cart'),
300 'variants.other_info.trial_days.numeric' => esc_html__('Trial days must be a number.', 'fluent-cart'),
301 'variants.other_info.trial_days.max' => esc_html__('Trial period cannot exceed 365 days.', 'fluent-cart'),
302 ];
303 }
304
305
306 /**
307 * @return array
308 */
309 public function sanitize()
310 {
311
312 return [
313 'variants.id' => 'intval',
314 'variants.rowId' => 'intval',
315 'variants.post_id' => 'intval',
316 'variants.variation_title' => 'sanitize_text_field',
317 'variants.sku' => 'sanitize_text_field',
318 'variants.item_price' => 'floatval',
319 'variants.compare_price' => 'floatval',
320 'variants.manage_cost' => 'sanitize_text_field',
321 'variants.fulfillment_type' => 'sanitize_text_field',
322 'variants.item_cost' => 'floatval',
323 'variants.total_stock' => 'intval',
324 'variants.available' => 'intval',
325 'variants.committed' => 'intval',
326 'variants.on_hold' => 'intval',
327 'variants.manage_stock' => 'intval',
328 'variants.shipping_class' => function ($value) {
329 return $value ? intval($value) : null;
330 },
331 'variants.stock_status' => 'sanitize_key',
332 'variants.serial_index' => 'intval',
333 'variants.media.*.id' => 'intval',
334 'variants.media.*.url' => function ($value) {
335 if (empty($value)) {
336 return '';
337 }
338
339 return sanitize_url($value);
340 },
341 'variants.media.*.title' => 'sanitize_text_field',
342
343 'variants.downloadable' => 'sanitize_text_field',
344
345 'variants.other_info' => function ($value) {
346 return is_array($value) ? $value : [];
347 },
348 'variants.other_info.description' => 'sanitize_text_field',
349 'variants.other_info.payment_type' => 'sanitize_text_field',
350 'variants.other_info.times' => 'sanitize_text_field',
351 'variants.other_info.trial_days' => 'sanitize_text_field',
352 'variants.other_info.repeat_interval' => 'sanitize_text_field',
353 'variants.other_info.billing_summary' => 'sanitize_text_field',
354 'variants.other_info.manage_setup_fee' => 'sanitize_text_field',
355 'variants.other_info.signup_fee' => 'floatval',
356 'variants.other_info.signup_fee_name' => 'sanitize_text_field',
357 'variants.other_info.package_slug' => 'sanitize_text_field',
358 'variants.other_info.weight' => 'floatval',
359 'variants.other_info.weight_unit' => 'sanitize_text_field',
360 'variants.other_info.length' => 'floatval',
361 'variants.other_info.width' => 'floatval',
362 'variants.other_info.height' => 'floatval',
363 'variants.other_info.tax_class' => 'sanitize_text_field',
364 'variants.other_info.tax_exempt' => 'sanitize_text_field',
365 //'variants.other_info.purchasable' => 'sanitize_text_field',
366 ];
367
368 }
369 }
370