PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.0 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 All 50 releases
← All changes | app/Http/Requests/ProductUpdateRequest.php +93 -24 1.5.3 → 1.7.0 View file →
@@ -1,10 +1,14 @@
1 1 <?php
2 2
3 3 namespace FluentCart\App\Http\Requests;
4 4
5 +use FluentCart\App\Helpers\Helper;
6 +use FluentCart\App\Models\ProductVariation;
5 7 use FluentCart\App\Models\ShippingClass;
8 +use FluentCart\App\Modules\FluentPlayer\ProductVideoSettings;
6 9 use FluentCart\App\Services\DateTime\DateTime;
10 +use FluentCart\App\Http\Rules\RequiredWhenRule;
7 11 use FluentCart\Framework\Foundation\RequestGuard;
8 12 use FluentCart\Framework\Support\Arr;
9 13
10 14 class ProductUpdateRequest extends RequestGuard
@@ -9,22 +13,22 @@
9 13
10 14 class ProductUpdateRequest extends RequestGuard
11 15 {
12 16 /**
13 - * Prepare and normalize the incoming data before validation.
17 + * Drop the subscription fields from any variant saved as one-time, so a
18 + * product switched back to a single payment does not keep billing settings
19 + * the merchant can no longer see.
14 20 *
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.
21 + * This does NOT fill missing keys. The block below it — which rebuilt
22 + * `other_info` and `fulfillment_type` from defaults, as ProductRequest still
23 + * does on create — stays commented out deliberately: on an update the payload
24 + * is a partial row, so rebuilding the column from defaults would overwrite a
25 + * stored subscription's interval and setup fee on an unrelated edit. Callers
26 + * therefore have to send a complete variant row; the editor builds one in
27 + * Models/Product/productUpdatePayload.js by merging each change record with
28 + * the variation it came from.
17 29 *
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.
30 + * @return array The request data, with dead subscription fields removed.
27 31 */
28 32 public function beforeValidation()
29 33 {
30 34 $data = $this->all();
@@ -254,8 +258,10 @@
254 258 return $this->validateTaxClassId($attribute, $value);
255 259 }],
256 260 'detail.other_info.tax_exempt' => 'nullable|sanitizeText|in:yes,no',
257 261 'detail.other_info.active_editor' => 'nullable|sanitizeText',
262 + 'detail.other_info.reviews_enabled' => 'nullable|sanitizeText|in:yes,no',
263 + 'detail.other_info.fluent_player_video' => 'nullable|array',
258 264 'product_terms' => 'nullable|array',
259 265 'product_terms.*' => 'nullable|array',
260 266 'product_terms.*.*' => 'nullable|numeric',
261 267
@@ -272,16 +278,34 @@
272 278 'variants.*.compare_price' => [
273 279 'nullable',
274 280 'numeric',
275 281 function ($attribute, $value) {
282 + // Zero or empty means no compare-at price, which the storefront
283 + // also treats as "none" — nothing to compare.
284 + if ($value === null || $value === '' || (float) $value <= 0) {
285 + return null;
286 + }
287 +
276 288 $index = explode('.', $attribute)[1];
277 289 $itemPrice = $this->get("variants.$index.item_price");
278 - if (empty($itemPrice)) {
279 - $itemPrice = 0;
290 +
291 + // The editor posts only what changed, so a compare-price-only edit
292 + // carries no item_price. Falling back to 0 made the comparison
293 + // vacuous and the rule passed for any value; read the stored price
294 + // for that variation instead, which is what the row is really
295 + // being compared against.
296 + if ($itemPrice === null || $itemPrice === '') {
297 + $variantId = $this->get("variants.$index.id");
298 +
299 + $itemPrice = $variantId
300 + ? ProductVariation::query()->where('id', $variantId)->value('item_price')
301 + : 0;
280 302 }
281 - if ($value !== null && $value < $itemPrice) {
303 +
304 + if ((float) $value < (float) $itemPrice) {
282 305 return sprintf(__("Compare price must be greater than or equal to item price.", 'fluent-cart'));
283 306 }
307 +
284 308 return null;
285 309 },
286 310 ],
287 311 'variants.*.manage_cost' => 'nullable|sanitizeText|maxLength:10',
@@ -296,8 +320,17 @@
296 320 // }],
297 321 // 'variants.*.item_cost' => 'required_if:variants.*.manage_cost,true',
298 322 'variants.*.serial_index' => 'nullable|numeric',
299 323 // 'variants.*.downloadable' => 'nullable|sanitizeText|maxLength:10',
324 + // Outside the variation_type gate below: the other_info rules there only
325 + // register for 'simple', but a simple_variations save posts variants too.
326 + 'variants.*.other_info.times' => [
327 + function ($attribute, $value) {
328 + $index = explode('.', $attribute)[1];
329 +
330 + return Helper::installmentTimesError($this->get("variants.$index.other_info"));
331 + },
332 + ],
300 333 ];
301 334
302 335 if ($variationType === 'simple') {
303 336 $variantsOtherInfoRules = [
@@ -313,9 +346,9 @@
313 346 }
314 347 if (!empty($value) && !is_numeric($value)) {
315 348 return __('Times must be a number.', 'fluent-cart');
316 349 }
317 - return null;
350 + return Helper::installmentTimesError($this->get("variants.$index.other_info"));
318 351 },
319 352 ],
320 353 'variants.*.other_info.trial_days' => [
321 354 function ($attribute, $value) {
@@ -331,13 +364,43 @@
331 364 }
332 365 return null;
333 366 },
334 367 ],
335 - 'variants.*.other_info.repeat_interval' => 'required_if:variants.*.other_info.payment_type,subscription|sanitizeText|maxLength:100',
368 + 'variants.*.other_info.repeat_interval' => [
369 + RequiredWhenRule::make(
370 + 'variants.*.other_info.payment_type',
371 + 'subscription',
372 + esc_html__('Interval is required.', 'fluent-cart')
373 + ),
374 + 'sanitizeText',
375 + 'maxLength:100',
376 + ],
336 377 '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',
378 + 'variants.*.other_info.manage_setup_fee' => [
379 + RequiredWhenRule::make(
380 + 'variants.*.other_info.payment_type',
381 + 'subscription',
382 + esc_html__('Setup Fee option is required.', 'fluent-cart')
383 + ),
384 + 'sanitizeText',
385 + 'maxLength:100',
386 + ],
387 + 'variants.*.other_info.signup_fee' => [
388 + RequiredWhenRule::make(
389 + 'variants.*.other_info.manage_setup_fee',
390 + 'yes',
391 + esc_html__('Setup Fee Amount is required.', 'fluent-cart')
392 + ),
393 + ],
394 + 'variants.*.other_info.signup_fee_name' => [
395 + RequiredWhenRule::make(
396 + 'variants.*.other_info.manage_setup_fee',
397 + 'yes',
398 + esc_html__('Setup Fee Name is required.', 'fluent-cart')
399 + ),
400 + 'sanitizeText',
401 + 'maxLength:100',
402 + ],
340 403 ];
341 404 $rules = array_merge($rules, $variantsOtherInfoRules);
342 405
343 406 }
@@ -396,11 +459,8 @@
396 459 'variants.*.item_cost.required_if' => esc_html__('Item cost is required.', 'fluent-cart'),
397 460 'variants.*.other_info.description.max' => esc_html__('Description may not be greater than 255 characters.', 'fluent-cart'),
398 461 'variants.*.other_info.payment_type.required' => esc_html__('Payment Type is required.', 'fluent-cart'),
399 462 '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 463 ];
404 464
405 465 $messages = array_merge($messages, $otherInfoMessages);
406 466 }
@@ -463,8 +523,12 @@
463 523 'detail.other_info.shipping_class' => 'intval',
464 524 'detail.other_info.tax_class' => 'intval',
465 525 'detail.other_info.tax_exempt' => 'sanitize_text_field',
466 526 'detail.other_info.active_editor' => 'sanitize_text_field',
527 + 'detail.other_info.reviews_enabled' => 'sanitize_text_field',
528 + 'detail.other_info.fluent_player_video' => function ($value) {
529 + return ProductVideoSettings::sanitize($value);
530 + },
467 531 ];
468 532
469 533 foreach ($detailFieldMap as $field => $sanitizer) {
470 534 if (Arr::has($data, $field)) {
@@ -512,9 +576,14 @@
512 576 "variants.$index.serial_index" => 'intval',
513 577 "variants.$index.downloadable" => 'sanitize_text_field',
514 578 "variants.$index.fulfillment_type" => 'sanitize_text_field',
515 579 "variants.$index.sku" => function ($value) {
516 - return empty($value) ? null : sanitize_text_field($value);
580 + // empty() treats the string "0" as empty too, which would silently
581 + // convert a legitimate sku of "0" to NULL — sku is a textual
582 + // identifier (VARCHAR), not a numeric flag. Match the explicit
583 + // '' / null check ProductResource::update() already uses for the
584 + // same reason when it clears a sku.
585 + return ($value === '' || $value === null) ? null : sanitize_text_field($value);
517 586 },
518 587 ];
519 588
520 589 foreach ($variantFieldMap as $field => $sanitizer) {