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 / ProductRequest.php

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

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