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

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