PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.2
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.2
1.6.6 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 All 49 releases
fluent-cart / app / Http / Requests / ProductRequest.php

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

458 lines 20.3 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 ProductRequest 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
31
32 $data = $this->all();
33
34 $fulfilmentType = Arr::get(
35 $data,
36 'detail.fulfillment_type',
37 Arr::get($data, 'detail.fulfillment_type', 'physical')
38 );
39
40 $variants = Arr::wrap(
41 Arr::get($data, 'variants', [])
42 );
43
44 foreach ($variants as $index => &$variant) {
45 $variant['fulfillment_type'] = $variant['fulfillment_type'] ?? $fulfilmentType;
46
47 $originalOtherInfo = Arr::wrap(Arr::get($variant, 'other_info')) ?? [];
48 $variant['other_info'] = $originalOtherInfo;
49
50 // get payment_type from $variant
51 $paymentType = Arr::get($variant, 'other_info.payment_type', 'onetime');
52
53 $variant['shipping_class'] = Arr::get($variant, 'shipping_class', null);
54
55 // Preserve tax fields during normalization so they survive the same
56 // save pipeline as the rest of variant other_info.
57 $variant['other_info'] = [
58 'payment_type' => $paymentType,
59 'times' => Arr::get($variant, 'other_info.times', ''),
60 'trial_days' => Arr::get($variant, 'other_info.trial_days', ''),
61 //'purchasable' => Arr::get($variant, 'other_info.purchasable', ''),
62 'repeat_interval' => Arr::get($variant, 'other_info.repeat_interval', 'yearly'),
63 'billing_summary' => Arr::get($variant, 'other_info.billing_summary', ''),
64 'manage_setup_fee' => Arr::get($variant, 'other_info.manage_setup_fee', 'no'),
65 'signup_fee_name' => Arr::get($variant, 'other_info.signup_fee_name', ''),
66 'signup_fee' => Arr::get($variant, 'other_info.signup_fee', ''),
67 'setup_fee_per_item' => Arr::get($variant, 'other_info.setup_fee_per_item', 'no'),
68 'installment' => Arr::get($variant, 'other_info.installment', 'no'),
69 ];
70
71 if (Arr::has($originalOtherInfo, 'tax_class')) {
72 $variant['other_info']['tax_class'] = Arr::get($originalOtherInfo, 'tax_class');
73 }
74
75 if (Arr::has($originalOtherInfo, 'tax_exempt')) {
76 $variant['other_info']['tax_exempt'] = Arr::get($originalOtherInfo, 'tax_exempt');
77 }
78
79 $data['variants'][$index] = $variant;
80 }
81
82 return $data;
83 }
84
85 function validateShippingClassId($attribute, $value): ?string
86 {
87 static $checked = [];
88
89 if (empty($value)) {
90 return null;
91 }
92
93 // Return cached result if we've already checked this value
94 if (isset($checked[$value])) {
95 return $checked[$value];
96 }
97
98 if (!is_numeric($value)) {
99 $checked[$value] = __("Invalid Shipping Class.", 'fluent-cart');
100 return $checked[$value];
101 }
102
103 if (empty(ShippingClass::query()->find($value))) {
104 $checked[$value] = __("Invalid Shipping Class.", 'fluent-cart');
105 return $checked[$value];
106 }
107
108 return null;
109
110 }
111
112 function validateTaxClassId($attribute, $value): ?string
113 {
114 static $checked = [];
115
116 if (empty($value)) {
117 return null;
118 }
119
120 // Return cached result if we've already checked this value
121 if (isset($checked[$value])) {
122 return $checked[$value];
123 }
124
125 if (!is_numeric($value)) {
126 $checked[$value] = __("Invalid Tax Class.", 'fluent-cart');
127 return $checked[$value];
128 }
129
130 if (empty(\FluentCart\App\Models\TaxClass::query()->find($value))) {
131 $checked[$value] = __("Invalid Tax Class.", 'fluent-cart');
132 return $checked[$value];
133 }
134
135 return null;
136
137 }
138
139 function validateTaxClassSlug($attribute, $value): ?string
140 {
141 static $checked = [];
142
143 if (empty($value)) {
144 return null;
145 }
146
147 $value = sanitize_text_field($value);
148
149 if (isset($checked[$value])) {
150 return $checked[$value];
151 }
152
153 if (empty(\FluentCart\App\Models\TaxClass::query()->where('slug', $value)->first())) {
154 $checked[$value] = __("Invalid Tax Class.", 'fluent-cart');
155 return $checked[$value];
156 }
157
158 return null;
159 }
160
161 public function validatePostDate($attribute, $value): ?string
162 {
163 if ($this->get('post_status') !== 'future') {
164 return null;
165 }
166
167 if (empty($value)) {
168 return __("The post date is required when status is scheduled.", 'fluent-cart');
169 }
170 $currentTime = DateTime::gmtNow();
171 try {
172 $postDate = DateTime::anyTimeToGmt($value);
173 } catch (\Exception $exception) {
174 return __("The post date is invalid.", 'fluent-cart');
175 }
176
177 if ($postDate < $currentTime) {
178 return sprintf(__("The post date must be in the future.", 'fluent-cart'));
179 }
180 return null;
181 }
182
183
184 /**
185 * @return array
186 */
187 public function rules(): array
188 {
189
190 $variationType = Arr::get($this->all(), 'detail.variation_type', 'simple');
191 $rules = [
192 'post_title' => 'required|sanitizeText|maxLength:200',
193 'post_excerpt' => [
194 'nullable',
195 'string',
196 'validate_excerpt' => function ($attr, $value) {
197 $words = explode(' ', $value);
198 $excerpt_length = (int)apply_filters('excerpt_length', 55);
199 if (count($words) > $excerpt_length) {
200 return sprintf(
201 /* translators: %d: The maximum number of words allowed */
202 __("The excerpt field cannot contain more than %d words.", 'fluent-cart'),
203 $excerpt_length
204 );
205 }
206 return null;
207 }
208 ],
209 'post_status' => ['required', 'string'],
210 'post_date' => function ($attribute, $value) {
211 return $this->validatePostDate($attribute, $value);
212 },
213 'comment_status' => 'nullable|sanitizeText|maxLength:100',
214 'detail.fulfillment_type' => 'required|sanitizeText|maxLength:100',
215 'detail.variation_type' => 'required|sanitizeText|maxLength:100',
216 'detail.min_price' => 'nullable|numeric',
217 'detail.max_price' => 'nullable|numeric',
218 'detail.stock_availability' => 'nullable|sanitizeText',
219 'detail.manage_stock' => 'nullable|numeric',
220 'detail.manage_downloadable' => 'nullable|numeric',
221 'detail.other_info' => 'nullable|array',
222 'detail.other_info.group_pricing_by' => 'nullable|sanitizeText|in:payment_type,repeat_interval,none',
223 'detail.other_info.sold_individually' => 'nullable|sanitizeText|in:yes,no',
224 'detail.other_info.use_pricing_table' => 'nullable|sanitizeText',
225 'detail.other_info.shipping_class' => ['nullable', function ($attribute, $value) {
226 return $this->validateShippingClassId($attribute, $value);
227 }],
228 'detail.other_info.tax_class' => ['nullable', function ($attribute, $value) {
229 return $this->validateTaxClassId($attribute, $value);
230 }],
231 'detail.other_info.tax_exempt' => 'nullable|sanitizeText|in:yes,no',
232 'detail.other_info.active_editor' => 'nullable|sanitizeText',
233 'product_terms' => 'nullable|array',
234 'product_terms.*' => 'nullable|array',
235 'product_terms.*.*' => 'nullable|numeric',
236
237 // 'variants' => 'required_if:post_status,publish,future',
238 'variants.*.variation_title' => 'required|sanitizeText|maxLength:200',
239 'variants.*.post_id' => 'required|numeric',
240 'variants.*.item_price' => 'nullable|numeric|min:0',
241 'variants.*.compare_price' => [
242 'nullable',
243 'numeric',
244 function ($attribute, $value) {
245 $index = explode('.', $attribute)[1];
246 $itemPrice = $this->get("variants.$index.item_price");
247 if (empty($itemPrice)) {
248 $itemPrice = 0;
249 }
250 if ($value !== null && $value < $itemPrice) {
251 return sprintf(__("Compare price must be greater than or equal to item price.", 'fluent-cart'));
252 }
253 return null;
254 },
255 ],
256 'variants.*.manage_cost' => 'nullable|sanitizeText|maxLength:10',
257 // Variant tax classes are stored as slugs, not numeric IDs like the
258 // older product-detail tax field.
259 'variants.*.other_info.tax_class' => ['nullable', function ($attribute, $value) {
260 return $this->validateTaxClassSlug($attribute, $value);
261 }],
262 'variants.*.other_info.tax_exempt' => 'nullable|sanitizeText|in:yes,no',
263 // 'variants.*.shipping_class' => ['nullable', 'numeric', function ($attribute, $value) {
264 // return $this->validateShippingClassId($attribute, $value);
265 // }],
266 // 'variants.*.item_cost' => 'required_if:variants.*.manage_cost,true',
267 'variants.*.serial_index' => 'nullable|numeric',
268 // 'variants.*.downloadable' => 'nullable|sanitizeText|maxLength:10',
269 ];
270
271 if ($variationType === 'simple') {
272 $variantsOtherInfoRules = [
273
274 'variants.*.other_info' => 'required|array',
275 'variants.*.other_info.description' => 'nullable|sanitizeTextArea|maxLength:255',
276 'variants.*.other_info.payment_type' => 'required|sanitizeText|in:onetime,subscription',
277 'variants.*.other_info.times' => 'nullable|sanitizeText|maxLength:50',
278 'variants.*.other_info.trial_days' => 'nullable|sanitizeText|maxLength:365',
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
362 $sanitizer = [
363 'ID' => 'intval',
364 'post_title' => 'sanitize_text_field',
365 'post_name' => 'sanitize_text_field',
366 'post_status' => 'sanitize_text_field',
367 'comment_status' => 'sanitize_text_field',
368 'post_date' => 'sanitize_text_field',
369 'post_excerpt' => 'wp_strip_all_tags',
370 'post_content' => 'wp_kses_post',
371
372 'detail.id' => 'intval',
373 'detail.post_id' => 'intval',
374 'detail.fulfillment_type' => 'sanitize_text_field',
375 'detail.variation_type' => 'sanitize_key',
376 'detail.stock_availability' => 'sanitize_key',
377 'detail.manage_stock' => 'intval',
378 'detail.manage_downloadable' => 'intval',
379 'detail.default_variation_id' => 'intval',
380 'detail.other_info.group_pricing_by' => 'sanitize_text_field',
381 'detail.other_info.sold_individually' => 'sanitize_text_field',
382 'detail.other_info.use_pricing_table' => 'sanitize_text_field',
383 'detail.other_info.shipping_class' => 'intval',
384 'detail.other_info.tax_class' => 'intval',
385 'detail.other_info.tax_exempt' => 'sanitize_text_field',
386 'detail.other_info.active_editor' => 'sanitize_text_field',
387 'variants.*.id' => 'intval',
388 'variants.*.rowId' => 'intval',
389 'variants.*.post_id' => 'intval',
390 'variants.*.variation_title' => 'sanitize_text_field',
391 'variants.*.item_price' => 'floatval',
392 'variants.*.compare_price' => 'floatval',
393 'variants.*.item_cost' => 'floatval',
394
395 'product_terms.*.product-categories' => 'sanitize_text_field',
396 'product_terms.*.product-brands' => 'sanitize_text_field',
397
398 'gallery.*.id' => 'intval',
399 'gallery.*.url' => function ($value) {
400 if (empty($value)) {
401 return '';
402 }
403
404 return sanitize_url($value);
405 },
406 'gallery.*.title' => 'sanitize_text_field',
407 'metaValue' => function ($value) {
408 return $value;
409 }
410 ];
411
412 $otherInfoSanitizer = [
413 'variants.*.manage_cost' => 'sanitize_text_field',
414 'variants.*.total_stock' => 'intval',
415 'variants.*.available' => 'intval',
416 'variants.*.shipping_class' => 'intval',
417 'variants.*.committed' => 'intval',
418 'variants.*.on_hold' => 'intval',
419 'variants.*.manage_stock' => 'intval',
420 'variants.*.stock_status' => 'sanitize_key',
421 'variants.*.serial_index' => 'intval',
422 'variants.*.media.*.id' => 'intval',
423 'variants.*.media.*.url' => function ($value) {
424 if (empty($value)) {
425 return '';
426 }
427
428 return sanitize_url($value);
429 },
430 'variants.*.media.*.title' => 'sanitize_text_field',
431
432 'variants.*.downloadable' => 'sanitize_text_field',
433
434 'variants.*.other_info' => function ($value) {
435 return is_array($value) ? $value : [];
436 },
437 'variants.*.other_info.description' => function ($value) {
438 return $value;
439 },
440 'variants.*.other_info.payment_type' => 'sanitize_text_field',
441 'variants.*.other_info.times' => 'sanitize_text_field',
442 'variants.*.other_info.repeat_interval' => 'sanitize_text_field',
443 'variants.*.other_info.billing_summary' => 'sanitize_text_field',
444 'variants.*.other_info.manage_setup_fee' => 'sanitize_text_field',
445 'variants.*.other_info.signup_fee' => 'floatval',
446 'variants.*.other_info.signup_fee_name' => 'sanitize_text_field',
447 'variants.*.other_info.installment' => 'sanitize_text_field',
448 'variants.*.other_info.tax_class' => 'sanitize_text_field',
449 'variants.*.other_info.tax_exempt' => 'sanitize_text_field',
450 ];
451
452
453 $sanitizer = array_merge($sanitizer, $otherInfoSanitizer);
454
455 return $sanitizer;
456 }
457 }
458