PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.3
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.3
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.5.3, at app/Http/Requests/ProductUpdateRequest.php

569 lines 25.8 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 $data = $this->all();
188 $hasDetail = isset($data['detail']) && is_array($data['detail']);
189 $hasVariants = isset($data['variants']) && is_array($data['variants']);
190
191 if (!$hasDetail && !$hasVariants) {
192 $rules = [];
193 if (isset($data['post_title'])) {
194 $rules['post_title'] = 'sanitizeText|maxLength:200';
195 }
196 if (isset($data['post_excerpt'])) {
197 $rules['post_excerpt'] = ['nullable', 'string'];
198 }
199 if (isset($data['post_status'])) {
200 $rules['post_status'] = ['string', function ($attribute, $value) {
201 if (!in_array($value, ['publish', 'draft', 'future', 'private'], true)) {
202 return __('Invalid post status provided.', 'fluent-cart');
203 }
204 return null;
205 }];
206 }
207 if (isset($data['post_status']) && $data['post_status'] === 'future') {
208 $rules['post_date'] = function ($attribute, $value) {
209 return $this->validatePostDate($attribute, $value);
210 };
211 }
212 return $rules;
213 }
214
215 $variationType = Arr::get($data, 'detail.variation_type', 'simple');
216 $rules = [
217 'post_title' => 'required|sanitizeText|maxLength:200',
218 'post_excerpt' => [
219 'nullable',
220 'string',
221 'validate_excerpt' => function ($attr, $value) {
222 $words = explode(' ', $value);
223 $excerpt_length = (int)apply_filters('excerpt_length', 55);
224 if (count($words) > $excerpt_length) {
225 return sprintf(
226 /* translators: %d: The maximum number of words allowed */
227 __("The excerpt field cannot contain more than %d words.", 'fluent-cart'),
228 $excerpt_length
229 );
230 }
231 return null;
232 }
233 ],
234 'post_status' => ['required', 'string'],
235 'post_date' => function ($attribute, $value) {
236 return $this->validatePostDate($attribute, $value);
237 },
238 'comment_status' => 'nullable|sanitizeText|maxLength:100',
239 'detail.fulfillment_type' => 'required|sanitizeText|maxLength:100',
240 'detail.variation_type' => 'required|sanitizeText|maxLength:100',
241 'detail.min_price' => 'nullable|numeric',
242 'detail.max_price' => 'nullable|numeric',
243 'detail.stock_availability' => 'nullable|sanitizeText',
244 'detail.manage_stock' => 'nullable|numeric',
245 'detail.manage_downloadable' => 'nullable|numeric',
246 'detail.other_info' => 'nullable|array',
247 'detail.other_info.group_pricing_by' => 'nullable|sanitizeText|in:payment_type,repeat_interval,none',
248 'detail.other_info.sold_individually' => 'nullable|sanitizeText|in:yes,no',
249 'detail.other_info.use_pricing_table' => 'nullable|sanitizeText',
250 'detail.other_info.shipping_class' => ['nullable', function ($attribute, $value) {
251 return $this->validateShippingClassId($attribute, $value);
252 }],
253 'detail.other_info.tax_class' => ['nullable', function ($attribute, $value) {
254 return $this->validateTaxClassId($attribute, $value);
255 }],
256 'detail.other_info.tax_exempt' => 'nullable|sanitizeText|in:yes,no',
257 'detail.other_info.active_editor' => 'nullable|sanitizeText',
258 'product_terms' => 'nullable|array',
259 'product_terms.*' => 'nullable|array',
260 'product_terms.*.*' => 'nullable|numeric',
261
262 // 'variants' => 'required_if:post_status,publish,future',
263 'variants.*.variation_title' => 'required|sanitizeText|maxLength:200',
264 'variants.*.fulfillment_type' => ['sanitizeText', function ($attribute, $value) {
265 if (!in_array($value, ['physical', 'digital'])) {
266 return __('Invalid fulfillment type.', 'fluent-cart');
267 }
268 return null;
269 }],
270 'variants.*.post_id' => 'required|numeric',
271 'variants.*.item_price' => 'nullable|numeric|min:0',
272 'variants.*.compare_price' => [
273 'nullable',
274 'numeric',
275 function ($attribute, $value) {
276 $index = explode('.', $attribute)[1];
277 $itemPrice = $this->get("variants.$index.item_price");
278 if (empty($itemPrice)) {
279 $itemPrice = 0;
280 }
281 if ($value !== null && $value < $itemPrice) {
282 return sprintf(__("Compare price must be greater than or equal to item price.", 'fluent-cart'));
283 }
284 return null;
285 },
286 ],
287 'variants.*.manage_cost' => 'nullable|sanitizeText|maxLength:10',
288 // Variant tax classes are stored as slugs, not numeric IDs like the
289 // older product-detail tax field.
290 'variants.*.other_info.tax_class' => ['nullable', function ($attribute, $value) {
291 return $this->validateTaxClassSlug($attribute, $value);
292 }],
293 'variants.*.other_info.tax_exempt' => 'nullable|sanitizeText|in:yes,no',
294 // 'variants.*.shipping_class' => ['nullable', 'numeric', function ($attribute, $value) {
295 // return $this->validateShippingClassId($attribute, $value);
296 // }],
297 // 'variants.*.item_cost' => 'required_if:variants.*.manage_cost,true',
298 'variants.*.serial_index' => 'nullable|numeric',
299 // 'variants.*.downloadable' => 'nullable|sanitizeText|maxLength:10',
300 ];
301
302 if ($variationType === 'simple') {
303 $variantsOtherInfoRules = [
304
305 'variants.*.other_info' => 'required|array',
306 'variants.*.other_info.description' => 'nullable|sanitizeTextArea|maxLength:255',
307 'variants.*.other_info.payment_type' => 'required|sanitizeText|in:onetime,subscription',
308 'variants.*.other_info.times' => [
309 function ($attribute, $value) {
310 $index = explode('.', $attribute)[1];
311 if ($this->get("variants.$index.other_info.payment_type") !== 'subscription') {
312 return null;
313 }
314 if (!empty($value) && !is_numeric($value)) {
315 return __('Times must be a number.', 'fluent-cart');
316 }
317 return null;
318 },
319 ],
320 'variants.*.other_info.trial_days' => [
321 function ($attribute, $value) {
322 $index = explode('.', $attribute)[1];
323 if ($this->get("variants.$index.other_info.payment_type") !== 'subscription') {
324 return null;
325 }
326 if (!empty($value) && !is_numeric($value)) {
327 return __('Trial days must be a number.', 'fluent-cart');
328 }
329 if (!empty($value) && $value > 365) {
330 return __('Trial period cannot exceed 365 days.', 'fluent-cart');
331 }
332 return null;
333 },
334 ],
335 'variants.*.other_info.repeat_interval' => 'required_if:variants.*.other_info.payment_type,subscription|sanitizeText|maxLength:100',
336 'variants.*.other_info.billing_summary' => 'nullable|sanitizeTextArea|maxLength:255',
337 'variants.*.other_info.manage_setup_fee' => 'required_if:variants.*.other_info.payment_type,subscription|sanitizeText|maxLength:100',
338 'variants.*.other_info.signup_fee' => 'required_if:variants.*.other_info.manage_setup_fee,yes',
339 'variants.*.other_info.signup_fee_name' => 'required_if:variants.*.other_info.manage_setup_fee,yes|sanitizeText|maxLength:100',
340 ];
341 $rules = array_merge($rules, $variantsOtherInfoRules);
342
343 }
344
345
346 // $stockValidations = [
347 // 'variants.*.manage_stock' => 'nullable|numeric',
348 // 'variants.*.stock_status' => 'required_if:detail.manage_stock,1|sanitizeText|maxLength:50',
349 // 'variants.*.total_stock' => 'required|numeric',
350 // 'variants.*.available' => 'required|numeric',
351 // 'variants.*.committed' => 'required|numeric',
352 // 'variants.*.on_hold' => 'required|numeric',
353 // ];
354 //
355 // if($this->get('detail.manage_stock') == 1) {
356 // $rules = array_merge($rules, $stockValidations);
357 // }
358 return $rules;
359 }
360
361 public function afterValidation($validator): array
362 {
363 $data = $this->get();
364 foreach (Arr::get($data, 'variants', []) as $index => &$variant) {
365 if (empty($variant['item_price'])) {
366 $variant['item_price'] = 0;
367 }
368 }
369 return $data;
370 }
371
372
373 /**
374 * @return array
375 */
376 public function messages(): array
377 {
378 $messages = [
379 'post_title.required' => esc_html__('Title is required.', 'fluent-cart'),
380 'post_title.max' => esc_html__('Title may not be greater than 200 characters.', 'fluent-cart'),
381 'detail.fulfillment_type.required' => esc_html__('Fulfilment Type is required.', 'fluent-cart'),
382 'detail.variation_type.required' => esc_html__('Variation Type is required.', 'fluent-cart'),
383 'variants.required_if' => esc_html__('Pricing is required when status is publish or scheduled.', 'fluent-cart'),
384 'variants.*.variation_title.required' => esc_html__('Title is required.', 'fluent-cart'),
385 'variants.*.variation_title.max' => esc_html__('Title may not be greater than 200 characters.', 'fluent-cart'),
386 'variants.*.item_price.required' => esc_html__('Price is required.', 'fluent-cart'),
387 'variants.*.item_price.numeric' => esc_html__('Price must be a number.', 'fluent-cart'),
388 'variants.*.item_price.min' => esc_html__('Price must be a positive number greater than 0.', 'fluent-cart'),
389
390 ];
391
392 $variationType = Arr::get($this->all(), 'detail.variation_type', 'simple');
393 if ($variationType === 'simple') {
394 $otherInfoMessages = [
395 'variants.*.stock_status.required_if' => esc_html__('Stock status is required.', 'fluent-cart'),
396 'variants.*.item_cost.required_if' => esc_html__('Item cost is required.', 'fluent-cart'),
397 'variants.*.other_info.description.max' => esc_html__('Description may not be greater than 255 characters.', 'fluent-cart'),
398 'variants.*.other_info.payment_type.required' => esc_html__('Payment Type is required.', 'fluent-cart'),
399 'variants.*.other_info.times.required_if' => esc_html__('Times is required.', 'fluent-cart'),
400 'variants.*.other_info.repeat_interval.required_if' => esc_html__('Interval is required.', 'fluent-cart'),
401 'variants.*.other_info.signup_fee.required_if' => esc_html__('Setup Fee Amount is required.', 'fluent-cart'),
402 'variants.*.other_info.signup_fee_name.required_if' => esc_html__('Setup Fee Name is required.', 'fluent-cart'),
403 ];
404
405 $messages = array_merge($messages, $otherInfoMessages);
406 }
407
408 return $messages;
409 }
410
411
412 /**
413 * @return array
414 */
415 public function sanitize(): array
416 {
417 return $this->getSanitizersForExistingFields();
418 }
419
420 /**
421 * Only return sanitizers for fields that actually exist in the request
422 *
423 * @return array
424 */
425 private function getSanitizersForExistingFields(): array
426 {
427 $data = $this->all();
428 $sanitizers = [];
429
430 // Basic fields
431 $basicFieldMap = [
432 'ID' => 'intval',
433 'post_title' => 'sanitize_text_field',
434 'post_name' => 'sanitize_text_field',
435 'post_status' => 'sanitize_text_field',
436 'comment_status' => 'sanitize_text_field',
437 'post_date' => 'sanitize_text_field',
438 'post_excerpt' => 'wp_strip_all_tags',
439 'post_content' => 'wp_kses_post',
440 'metaValue' => function ($value) { return $value; },
441 ];
442
443 foreach ($basicFieldMap as $field => $sanitizer) {
444 if (array_key_exists($field, $data)) {
445 $sanitizers[$field] = $sanitizer;
446 }
447 }
448
449 // Detail fields
450 if (isset($data['detail']) && is_array($data['detail'])) {
451 $detailFieldMap = [
452 'detail.id' => 'intval',
453 'detail.post_id' => 'intval',
454 'detail.fulfillment_type' => 'sanitize_text_field',
455 'detail.variation_type' => 'sanitize_key',
456 'detail.stock_availability' => 'sanitize_key',
457 'detail.manage_stock' => 'intval',
458 'detail.manage_downloadable' => 'intval',
459 'detail.default_variation_id' => 'intval',
460 'detail.other_info.group_pricing_by' => 'sanitize_text_field',
461 'detail.other_info.sold_individually' => 'sanitize_text_field',
462 'detail.other_info.use_pricing_table' => 'sanitize_text_field',
463 'detail.other_info.shipping_class' => 'intval',
464 'detail.other_info.tax_class' => 'intval',
465 'detail.other_info.tax_exempt' => 'sanitize_text_field',
466 'detail.other_info.active_editor' => 'sanitize_text_field',
467 ];
468
469 foreach ($detailFieldMap as $field => $sanitizer) {
470 if (Arr::has($data, $field)) {
471 $sanitizers[$field] = $sanitizer;
472 }
473 }
474 }
475
476 // Product terms
477 if (isset($data['product_terms']) && is_array($data['product_terms'])) {
478 $sanitizers['product_terms.*.product-categories'] = 'sanitize_text_field';
479 $sanitizers['product_terms.*.product-brands'] = 'sanitize_text_field';
480 }
481
482 // Gallery
483 if (isset($data['gallery']) && is_array($data['gallery'])) {
484 $sanitizers['gallery.*.id'] = 'intval';
485 $sanitizers['gallery.*.url'] = function ($value) {
486 return empty($value) ? '' : sanitize_url($value);
487 };
488 $sanitizers['gallery.*.title'] = 'sanitize_text_field';
489 }
490
491 // Variants
492 if (isset($data['variants']) && is_array($data['variants'])) {
493 foreach ($data['variants'] as $index => $variant) {
494 $variantFieldMap = [
495 "variants.$index.id" => 'intval',
496 "variants.$index.rowId" => 'intval',
497 "variants.$index.post_id" => 'intval',
498 "variants.$index.variation_title" => 'sanitize_text_field',
499 "variants.$index.item_price" => 'floatval',
500 "variants.$index.compare_price" => 'floatval',
501 "variants.$index.item_cost" => 'floatval',
502 "variants.$index.manage_cost" => 'sanitize_text_field',
503 "variants.$index.total_stock" => 'intval',
504 "variants.$index.available" => 'intval',
505 "variants.$index.shipping_class" => function ($value) {
506 return $value ? intval($value) : null;
507 },
508 "variants.$index.committed" => 'intval',
509 "variants.$index.on_hold" => 'intval',
510 "variants.$index.manage_stock" => 'intval',
511 "variants.$index.stock_status" => 'sanitize_key',
512 "variants.$index.serial_index" => 'intval',
513 "variants.$index.downloadable" => 'sanitize_text_field',
514 "variants.$index.fulfillment_type" => 'sanitize_text_field',
515 "variants.$index.sku" => function ($value) {
516 return empty($value) ? null : sanitize_text_field($value);
517 },
518 ];
519
520 foreach ($variantFieldMap as $field => $sanitizer) {
521 if (Arr::has($data, $field)) {
522 $sanitizers[$field] = $sanitizer;
523 }
524 }
525
526 // Variant media
527 if (isset($variant['media']) && is_array($variant['media'])) {
528 $sanitizers["variants.$index.media.*.id"] = 'intval';
529 $sanitizers["variants.$index.media.*.url"] = function ($value) {
530 return empty($value) ? '' : sanitize_url($value);
531 };
532 $sanitizers["variants.$index.media.*.title"] = 'sanitize_text_field';
533 }
534
535 // Variant other_info
536 if (isset($variant['other_info'])) {
537 $sanitizers["variants.$index.other_info"] = function ($value) {
538 return is_array($value) ? $value : [];
539 };
540
541 if (is_array($variant['other_info'])) {
542 $otherInfoFieldMap = [
543 "variants.$index.other_info.description" => function ($value) { return $value; },
544 "variants.$index.other_info.payment_type" => 'sanitize_text_field',
545 "variants.$index.other_info.times" => 'sanitize_text_field',
546 "variants.$index.other_info.repeat_interval" => 'sanitize_text_field',
547 "variants.$index.other_info.billing_summary" => 'sanitize_text_field',
548 "variants.$index.other_info.manage_setup_fee" => 'sanitize_text_field',
549 "variants.$index.other_info.signup_fee" => 'floatval',
550 "variants.$index.other_info.signup_fee_name" => 'sanitize_text_field',
551 "variants.$index.other_info.installment" => 'sanitize_text_field',
552 "variants.$index.other_info.tax_class" => 'sanitize_text_field',
553 "variants.$index.other_info.tax_exempt" => 'sanitize_text_field',
554 ];
555
556 foreach ($otherInfoFieldMap as $field => $sanitizer) {
557 if (Arr::has($data, $field)) {
558 $sanitizers[$field] = $sanitizer;
559 }
560 }
561 }
562 }
563 }
564 }
565
566 return $sanitizers;
567 }
568 }
569