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

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