PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.1
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 trunk All 48 releases
fluent-cart / app / Http / Requests / ProductUpdateRequest.php

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

542 lines 24.7 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\Models\ShippingClass;
6 use FluentCart\App\Services\DateTime\DateTime;
7 use FluentCart\Framework\Foundation\RequestGuard;
8 use FluentCart\Framework\Support\Arr;
9
10 class ProductUpdateRequest extends RequestGuard
11 {
12 /**
13 * Prepare and normalize the incoming data before validation.
14 *
15 * This method ensures that each variant in the request payload has the necessary structure
16 * expected for processing, particularly focusing on the `other_info` attribute.
17 *
18 * If `other_info` is missing from a variant (common during data migration scenarios),
19 * this method sets default values to prevent validation or processing errors.
20 *
21 * It also ensures that:
22 * - `fulfillment_type` is consistently applied across all variants, defaulting to 'physical'.
23 * - `payment_type` is set (defaulting to 'onetime') and injected into `other_info`.
24 * - `other_info` is populated with a consistent structure containing default billing and setup fee options.
25 *
26 * @return array The normalized request data ready for validation.
27 */
28 public function beforeValidation()
29 {
30 $data = $this->all();
31
32 $subscriptionFields = ['trial_days', 'times', 'repeat_interval', 'billing_summary', 'manage_setup_fee', 'signup_fee', 'signup_fee_name', 'setup_fee_per_item'];
33 foreach (Arr::get($data, 'variants', []) as $index => $variant) {
34 if (Arr::get($variant, 'other_info.payment_type') === 'onetime') {
35 foreach ($subscriptionFields as $field) {
36 unset($data['variants'][$index]['other_info'][$field]);
37 }
38 }
39 }
40
41 return $data;
42 //
43 // $fulfilmentType = Arr::get(
44 // $data,
45 // 'detail.fulfillment_type',
46 // Arr::get($data, 'detail.fulfillment_type', 'physical')
47 // );
48 //
49 // $variants = Arr::wrap(
50 // Arr::get($data, 'variants', [])
51 // );
52 //
53 // foreach ($variants as $index => &$variant) {
54 // $variant['fulfillment_type'] = $variant['fulfillment_type'] ?? $fulfilmentType;
55 //
56 // $variant['other_info'] = Arr::wrap(Arr::get($variant, 'other_info')) ?? [];
57 //
58 // // get payment_type from $variant
59 // $paymentType = Arr::get($variant, 'other_info.payment_type', 'onetime');
60 //
61 // $variant['shipping_class'] = Arr::get($variant, 'shipping_class', null);
62 //
63 // $variant['other_info'] = [
64 // 'payment_type' => $paymentType,
65 // 'times' => Arr::get($variant, 'other_info.times', ''),
66 // 'trial_days' => Arr::get($variant, 'other_info.trial_days', ''),
67 // //'purchasable' => Arr::get($variant, 'other_info.purchasable', ''),
68 // 'repeat_interval' => Arr::get($variant, 'other_info.repeat_interval', 'yearly'),
69 // 'billing_summary' => Arr::get($variant, 'other_info.billing_summary', ''),
70 // 'manage_setup_fee' => Arr::get($variant, 'other_info.manage_setup_fee', 'no'),
71 // 'signup_fee_name' => Arr::get($variant, 'other_info.signup_fee_name', ''),
72 // 'signup_fee' => Arr::get($variant, 'other_info.signup_fee', ''),
73 // 'setup_fee_per_item' => Arr::get($variant, 'other_info.setup_fee_per_item', 'no'),
74 // 'installment' => Arr::get($variant, 'other_info.installment', 'no'),
75 // ];
76 //
77 // $data['variants'][$index] = $variant;
78 // }
79 //
80 // return $data;
81 }
82
83 function validateShippingClassId($attribute, $value): ?string
84 {
85 static $checked = [];
86
87 if (empty($value)) {
88 return null;
89 }
90
91 // Return cached result if we've already checked this value
92 if (isset($checked[$value])) {
93 return $checked[$value];
94 }
95
96 if (!is_numeric($value)) {
97 $checked[$value] = __("Invalid Shipping Class.", 'fluent-cart');
98 return $checked[$value];
99 }
100
101 if (empty(ShippingClass::query()->find($value))) {
102 $checked[$value] = __("Invalid Shipping Class.", 'fluent-cart');
103 return $checked[$value];
104 }
105
106 return null;
107
108 }
109
110 function validateTaxClassId($attribute, $value): ?string
111 {
112 static $checked = [];
113
114 if (empty($value)) {
115 return null;
116 }
117
118 // Return cached result if we've already checked this value
119 if (isset($checked[$value])) {
120 return $checked[$value];
121 }
122
123 if (!is_numeric($value)) {
124 $checked[$value] = __("Invalid Tax Class.", 'fluent-cart');
125 return $checked[$value];
126 }
127
128 if (empty(\FluentCart\App\Models\TaxClass::query()->find($value))) {
129 $checked[$value] = __("Invalid Tax Class.", 'fluent-cart');
130 return $checked[$value];
131 }
132
133 return null;
134
135 }
136
137 function validateTaxClassSlug($attribute, $value): ?string
138 {
139 static $checked = [];
140
141 if (empty($value)) {
142 return null;
143 }
144
145 $value = sanitize_text_field($value);
146
147 if (isset($checked[$value])) {
148 return $checked[$value];
149 }
150
151 if (empty(\FluentCart\App\Models\TaxClass::query()->where('slug', $value)->first())) {
152 $checked[$value] = __("Invalid Tax Class.", 'fluent-cart');
153 return $checked[$value];
154 }
155
156 return null;
157 }
158
159 public function validatePostDate($attribute, $value): ?string
160 {
161 if ($this->get('post_status') !== 'future') {
162 return null;
163 }
164
165 if (empty($value)) {
166 return __("The post date is required when status is scheduled.", 'fluent-cart');
167 }
168 $currentTime = DateTime::gmtNow();
169 try {
170 $postDate = DateTime::anyTimeToGmt($value);
171 } catch (\Exception $exception) {
172 return __("The post date is invalid.", 'fluent-cart');
173 }
174
175 if ($postDate < $currentTime) {
176 return sprintf(__("The post date must be in the future.", 'fluent-cart'));
177 }
178 return null;
179 }
180
181
182 /**
183 * @return array
184 */
185 public function rules(): array
186 {
187
188 $variationType = Arr::get($this->all(), 'detail.variation_type', 'simple');
189 $rules = [
190 'post_title' => 'required|sanitizeText|maxLength:200',
191 'post_excerpt' => [
192 'nullable',
193 'string',
194 'validate_excerpt' => function ($attr, $value) {
195 $words = explode(' ', $value);
196 $excerpt_length = (int)apply_filters('excerpt_length', 55);
197 if (count($words) > $excerpt_length) {
198 return sprintf(
199 /* translators: %d: The maximum number of words allowed */
200 __("The excerpt field cannot contain more than %d words.", 'fluent-cart'),
201 $excerpt_length
202 );
203 }
204 return null;
205 }
206 ],
207 'post_status' => ['required', 'string'],
208 'post_date' => function ($attribute, $value) {
209 return $this->validatePostDate($attribute, $value);
210 },
211 'comment_status' => 'nullable|sanitizeText|maxLength:100',
212 'detail.fulfillment_type' => 'required|sanitizeText|maxLength:100',
213 'detail.variation_type' => 'required|sanitizeText|maxLength:100',
214 'detail.min_price' => 'nullable|numeric',
215 'detail.max_price' => 'nullable|numeric',
216 'detail.stock_availability' => 'nullable|sanitizeText',
217 'detail.manage_stock' => 'nullable|numeric',
218 'detail.manage_downloadable' => 'nullable|numeric',
219 'detail.other_info' => 'nullable|array',
220 'detail.other_info.group_pricing_by' => 'nullable|sanitizeText|in:payment_type,repeat_interval,none',
221 'detail.other_info.sold_individually' => 'nullable|sanitizeText|in:yes,no',
222 'detail.other_info.use_pricing_table' => 'nullable|sanitizeText',
223 'detail.other_info.shipping_class' => ['nullable', function ($attribute, $value) {
224 return $this->validateShippingClassId($attribute, $value);
225 }],
226 'detail.other_info.tax_class' => ['nullable', function ($attribute, $value) {
227 return $this->validateTaxClassId($attribute, $value);
228 }],
229 'detail.other_info.tax_exempt' => 'nullable|sanitizeText|in:yes,no',
230 'detail.other_info.active_editor' => 'nullable|sanitizeText',
231 'product_terms' => 'nullable|array',
232 'product_terms.*' => 'nullable|array',
233 'product_terms.*.*' => 'nullable|numeric',
234
235 // 'variants' => 'required_if:post_status,publish,future',
236 'variants.*.variation_title' => 'required|sanitizeText|maxLength:200',
237 'variants.*.fulfillment_type' => ['sanitizeText', function ($attribute, $value) {
238 if (!in_array($value, ['physical', 'digital'])) {
239 return __('Invalid fulfillment type.', 'fluent-cart');
240 }
241 return null;
242 }],
243 'variants.*.post_id' => 'required|numeric',
244 'variants.*.item_price' => 'nullable|numeric|min:0',
245 'variants.*.compare_price' => [
246 'nullable',
247 'numeric',
248 function ($attribute, $value) {
249 $index = explode('.', $attribute)[1];
250 $itemPrice = $this->get("variants.$index.item_price");
251 if (empty($itemPrice)) {
252 $itemPrice = 0;
253 }
254 if ($value !== null && $value < $itemPrice) {
255 return sprintf(__("Compare price must be greater than or equal to item price.", 'fluent-cart'));
256 }
257 return null;
258 },
259 ],
260 'variants.*.manage_cost' => 'nullable|sanitizeText|maxLength:10',
261 // Variant tax classes are stored as slugs, not numeric IDs like the
262 // older product-detail tax field.
263 'variants.*.other_info.tax_class' => ['nullable', function ($attribute, $value) {
264 return $this->validateTaxClassSlug($attribute, $value);
265 }],
266 'variants.*.other_info.tax_exempt' => 'nullable|sanitizeText|in:yes,no',
267 // 'variants.*.shipping_class' => ['nullable', 'numeric', function ($attribute, $value) {
268 // return $this->validateShippingClassId($attribute, $value);
269 // }],
270 // 'variants.*.item_cost' => 'required_if:variants.*.manage_cost,true',
271 'variants.*.serial_index' => 'nullable|numeric',
272 // 'variants.*.downloadable' => 'nullable|sanitizeText|maxLength:10',
273 ];
274
275 if ($variationType === 'simple') {
276 $variantsOtherInfoRules = [
277
278 'variants.*.other_info' => 'required|array',
279 'variants.*.other_info.description' => 'nullable|sanitizeTextArea|maxLength:255',
280 'variants.*.other_info.payment_type' => 'required|sanitizeText|in:onetime,subscription',
281 'variants.*.other_info.times' => [
282 function ($attribute, $value) {
283 $index = explode('.', $attribute)[1];
284 if ($this->get("variants.$index.other_info.payment_type") !== 'subscription') {
285 return null;
286 }
287 if (!empty($value) && !is_numeric($value)) {
288 return __('Times must be a number.', 'fluent-cart');
289 }
290 return null;
291 },
292 ],
293 'variants.*.other_info.trial_days' => [
294 function ($attribute, $value) {
295 $index = explode('.', $attribute)[1];
296 if ($this->get("variants.$index.other_info.payment_type") !== 'subscription') {
297 return null;
298 }
299 if (!empty($value) && !is_numeric($value)) {
300 return __('Trial days must be a number.', 'fluent-cart');
301 }
302 if (!empty($value) && $value > 365) {
303 return __('Trial period cannot exceed 365 days.', 'fluent-cart');
304 }
305 return null;
306 },
307 ],
308 'variants.*.other_info.repeat_interval' => 'required_if:variants.*.other_info.payment_type,subscription|sanitizeText|maxLength:100',
309 'variants.*.other_info.billing_summary' => 'nullable|sanitizeTextArea|maxLength:255',
310 'variants.*.other_info.manage_setup_fee' => 'required_if:variants.*.other_info.payment_type,subscription|sanitizeText|maxLength:100',
311 'variants.*.other_info.signup_fee' => 'required_if:variants.*.other_info.manage_setup_fee,yes',
312 'variants.*.other_info.signup_fee_name' => 'required_if:variants.*.other_info.manage_setup_fee,yes|sanitizeText|maxLength:100',
313 ];
314 $rules = array_merge($rules, $variantsOtherInfoRules);
315
316 }
317
318
319 // $stockValidations = [
320 // 'variants.*.manage_stock' => 'nullable|numeric',
321 // 'variants.*.stock_status' => 'required_if:detail.manage_stock,1|sanitizeText|maxLength:50',
322 // 'variants.*.total_stock' => 'required|numeric',
323 // 'variants.*.available' => 'required|numeric',
324 // 'variants.*.committed' => 'required|numeric',
325 // 'variants.*.on_hold' => 'required|numeric',
326 // ];
327 //
328 // if($this->get('detail.manage_stock') == 1) {
329 // $rules = array_merge($rules, $stockValidations);
330 // }
331 return $rules;
332 }
333
334 public function afterValidation($validator): array
335 {
336 $data = $this->get();
337 foreach (Arr::get($data, 'variants', []) as $index => &$variant) {
338 if (empty($variant['item_price'])) {
339 $variant['item_price'] = 0;
340 }
341 }
342 return $data;
343 }
344
345
346 /**
347 * @return array
348 */
349 public function messages(): array
350 {
351 $messages = [
352 'post_title.required' => esc_html__('Title is required.', 'fluent-cart'),
353 'post_title.max' => esc_html__('Title may not be greater than 200 characters.', 'fluent-cart'),
354 'detail.fulfillment_type.required' => esc_html__('Fulfilment Type is required.', 'fluent-cart'),
355 'detail.variation_type.required' => esc_html__('Variation Type is required.', 'fluent-cart'),
356 'variants.required_if' => esc_html__('Pricing is required when status is publish or scheduled.', 'fluent-cart'),
357 'variants.*.variation_title.required' => esc_html__('Title is required.', 'fluent-cart'),
358 'variants.*.variation_title.max' => esc_html__('Title may not be greater than 200 characters.', 'fluent-cart'),
359 'variants.*.item_price.required' => esc_html__('Price is required.', 'fluent-cart'),
360 'variants.*.item_price.numeric' => esc_html__('Price must be a number.', 'fluent-cart'),
361 'variants.*.item_price.min' => esc_html__('Price must be a positive number greater than 0.', 'fluent-cart'),
362
363 ];
364
365 $variationType = Arr::get($this->all(), 'detail.variation_type', 'simple');
366 if ($variationType === 'simple') {
367 $otherInfoMessages = [
368 'variants.*.stock_status.required_if' => esc_html__('Stock status is required.', 'fluent-cart'),
369 'variants.*.item_cost.required_if' => esc_html__('Item cost is required.', 'fluent-cart'),
370 'variants.*.other_info.description.max' => esc_html__('Description may not be greater than 255 characters.', 'fluent-cart'),
371 'variants.*.other_info.payment_type.required' => esc_html__('Payment Type is required.', 'fluent-cart'),
372 'variants.*.other_info.times.required_if' => esc_html__('Times is required.', 'fluent-cart'),
373 'variants.*.other_info.repeat_interval.required_if' => esc_html__('Interval is required.', 'fluent-cart'),
374 'variants.*.other_info.signup_fee.required_if' => esc_html__('Setup Fee Amount is required.', 'fluent-cart'),
375 'variants.*.other_info.signup_fee_name.required_if' => esc_html__('Setup Fee Name is required.', 'fluent-cart'),
376 ];
377
378 $messages = array_merge($messages, $otherInfoMessages);
379 }
380
381 return $messages;
382 }
383
384
385 /**
386 * @return array
387 */
388 public function sanitize(): array
389 {
390 return $this->getSanitizersForExistingFields();
391 }
392
393 /**
394 * Only return sanitizers for fields that actually exist in the request
395 *
396 * @return array
397 */
398 private function getSanitizersForExistingFields(): array
399 {
400 $data = $this->all();
401 $sanitizers = [];
402
403 // Basic fields
404 $basicFieldMap = [
405 'ID' => 'intval',
406 'post_title' => 'sanitize_text_field',
407 'post_name' => 'sanitize_text_field',
408 'post_status' => 'sanitize_text_field',
409 'comment_status' => 'sanitize_text_field',
410 'post_date' => 'sanitize_text_field',
411 'post_excerpt' => 'wp_strip_all_tags',
412 'post_content' => 'wp_kses_post',
413 'metaValue' => function ($value) { return $value; },
414 ];
415
416 foreach ($basicFieldMap as $field => $sanitizer) {
417 if (array_key_exists($field, $data)) {
418 $sanitizers[$field] = $sanitizer;
419 }
420 }
421
422 // Detail fields
423 if (isset($data['detail']) && is_array($data['detail'])) {
424 $detailFieldMap = [
425 'detail.id' => 'intval',
426 'detail.post_id' => 'intval',
427 'detail.fulfillment_type' => 'sanitize_text_field',
428 'detail.variation_type' => 'sanitize_key',
429 'detail.stock_availability' => 'sanitize_key',
430 'detail.manage_stock' => 'intval',
431 'detail.manage_downloadable' => 'intval',
432 'detail.default_variation_id' => 'intval',
433 'detail.other_info.group_pricing_by' => 'sanitize_text_field',
434 'detail.other_info.sold_individually' => 'sanitize_text_field',
435 'detail.other_info.use_pricing_table' => 'sanitize_text_field',
436 'detail.other_info.shipping_class' => 'intval',
437 'detail.other_info.tax_class' => 'intval',
438 'detail.other_info.tax_exempt' => 'sanitize_text_field',
439 'detail.other_info.active_editor' => 'sanitize_text_field',
440 ];
441
442 foreach ($detailFieldMap as $field => $sanitizer) {
443 if (Arr::has($data, $field)) {
444 $sanitizers[$field] = $sanitizer;
445 }
446 }
447 }
448
449 // Product terms
450 if (isset($data['product_terms']) && is_array($data['product_terms'])) {
451 $sanitizers['product_terms.*.product-categories'] = 'sanitize_text_field';
452 $sanitizers['product_terms.*.product-brands'] = 'sanitize_text_field';
453 }
454
455 // Gallery
456 if (isset($data['gallery']) && is_array($data['gallery'])) {
457 $sanitizers['gallery.*.id'] = 'intval';
458 $sanitizers['gallery.*.url'] = function ($value) {
459 return empty($value) ? '' : sanitize_url($value);
460 };
461 $sanitizers['gallery.*.title'] = 'sanitize_text_field';
462 }
463
464 // Variants
465 if (isset($data['variants']) && is_array($data['variants'])) {
466 foreach ($data['variants'] as $index => $variant) {
467 $variantFieldMap = [
468 "variants.$index.id" => 'intval',
469 "variants.$index.rowId" => 'intval',
470 "variants.$index.post_id" => 'intval',
471 "variants.$index.variation_title" => 'sanitize_text_field',
472 "variants.$index.item_price" => 'floatval',
473 "variants.$index.compare_price" => 'floatval',
474 "variants.$index.item_cost" => 'floatval',
475 "variants.$index.manage_cost" => 'sanitize_text_field',
476 "variants.$index.total_stock" => 'intval',
477 "variants.$index.available" => 'intval',
478 "variants.$index.shipping_class" => function ($value) {
479 return $value ? intval($value) : null;
480 },
481 "variants.$index.committed" => 'intval',
482 "variants.$index.on_hold" => 'intval',
483 "variants.$index.manage_stock" => 'intval',
484 "variants.$index.stock_status" => 'sanitize_key',
485 "variants.$index.serial_index" => 'intval',
486 "variants.$index.downloadable" => 'sanitize_text_field',
487 "variants.$index.fulfillment_type" => 'sanitize_text_field',
488 "variants.$index.sku" => function ($value) {
489 return empty($value) ? null : sanitize_text_field($value);
490 },
491 ];
492
493 foreach ($variantFieldMap as $field => $sanitizer) {
494 if (Arr::has($data, $field)) {
495 $sanitizers[$field] = $sanitizer;
496 }
497 }
498
499 // Variant media
500 if (isset($variant['media']) && is_array($variant['media'])) {
501 $sanitizers["variants.$index.media.*.id"] = 'intval';
502 $sanitizers["variants.$index.media.*.url"] = function ($value) {
503 return empty($value) ? '' : sanitize_url($value);
504 };
505 $sanitizers["variants.$index.media.*.title"] = 'sanitize_text_field';
506 }
507
508 // Variant other_info
509 if (isset($variant['other_info'])) {
510 $sanitizers["variants.$index.other_info"] = function ($value) {
511 return is_array($value) ? $value : [];
512 };
513
514 if (is_array($variant['other_info'])) {
515 $otherInfoFieldMap = [
516 "variants.$index.other_info.description" => function ($value) { return $value; },
517 "variants.$index.other_info.payment_type" => 'sanitize_text_field',
518 "variants.$index.other_info.times" => 'sanitize_text_field',
519 "variants.$index.other_info.repeat_interval" => 'sanitize_text_field',
520 "variants.$index.other_info.billing_summary" => 'sanitize_text_field',
521 "variants.$index.other_info.manage_setup_fee" => 'sanitize_text_field',
522 "variants.$index.other_info.signup_fee" => 'floatval',
523 "variants.$index.other_info.signup_fee_name" => 'sanitize_text_field',
524 "variants.$index.other_info.installment" => 'sanitize_text_field',
525 "variants.$index.other_info.tax_class" => 'sanitize_text_field',
526 "variants.$index.other_info.tax_exempt" => 'sanitize_text_field',
527 ];
528
529 foreach ($otherInfoFieldMap as $field => $sanitizer) {
530 if (Arr::has($data, $field)) {
531 $sanitizers[$field] = $sanitizer;
532 }
533 }
534 }
535 }
536 }
537 }
538
539 return $sanitizers;
540 }
541 }
542