PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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
← All changes | app/Http/Requests/ProductUpdateRequest.php +91 -24 1.5.3 → 1.6.5 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,9 @@
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.fluent_player_video' => 'nullable|array',
258 263 'product_terms' => 'nullable|array',
259 264 'product_terms.*' => 'nullable|array',
260 265 'product_terms.*.*' => 'nullable|numeric',
261 266
@@ -272,16 +277,34 @@
272 277 'variants.*.compare_price' => [
273 278 'nullable',
274 279 'numeric',
275 280 function ($attribute, $value) {
281 + // Zero or empty means no compare-at price, which the storefront
282 + // also treats as "none" — nothing to compare.
283 + if ($value === null || $value === '' || (float) $value <= 0) {
284 + return null;
285 + }
286 +
276 287 $index = explode('.', $attribute)[1];
277 288 $itemPrice = $this->get("variants.$index.item_price");
278 - if (empty($itemPrice)) {
279 - $itemPrice = 0;
289 +
290 + // The editor posts only what changed, so a compare-price-only edit
291 + // carries no item_price. Falling back to 0 made the comparison
292 + // vacuous and the rule passed for any value; read the stored price
293 + // for that variation instead, which is what the row is really
294 + // being compared against.
295 + if ($itemPrice === null || $itemPrice === '') {
296 + $variantId = $this->get("variants.$index.id");
297 +
298 + $itemPrice = $variantId
299 + ? ProductVariation::query()->where('id', $variantId)->value('item_price')
300 + : 0;
280 301 }
281 - if ($value !== null && $value < $itemPrice) {
302 +
303 + if ((float) $value < (float) $itemPrice) {
282 304 return sprintf(__("Compare price must be greater than or equal to item price.", 'fluent-cart'));
283 305 }
306 +
284 307 return null;
285 308 },
286 309 ],
287 310 'variants.*.manage_cost' => 'nullable|sanitizeText|maxLength:10',
@@ -296,8 +319,17 @@
296 319 // }],
297 320 // 'variants.*.item_cost' => 'required_if:variants.*.manage_cost,true',
298 321 'variants.*.serial_index' => 'nullable|numeric',
299 322 // 'variants.*.downloadable' => 'nullable|sanitizeText|maxLength:10',
323 + // Outside the variation_type gate below: the other_info rules there only
324 + // register for 'simple', but a simple_variations save posts variants too.
325 + 'variants.*.other_info.times' => [
326 + function ($attribute, $value) {
327 + $index = explode('.', $attribute)[1];
328 +
329 + return Helper::installmentTimesError($this->get("variants.$index.other_info"));
330 + },
331 + ],
300 332 ];
301 333
302 334 if ($variationType === 'simple') {
303 335 $variantsOtherInfoRules = [
@@ -313,9 +345,9 @@
313 345 }
314 346 if (!empty($value) && !is_numeric($value)) {
315 347 return __('Times must be a number.', 'fluent-cart');
316 348 }
317 - return null;
349 + return Helper::installmentTimesError($this->get("variants.$index.other_info"));
318 350 },
319 351 ],
320 352 'variants.*.other_info.trial_days' => [
321 353 function ($attribute, $value) {
@@ -331,13 +363,43 @@
331 363 }
332 364 return null;
333 365 },
334 366 ],
335 - 'variants.*.other_info.repeat_interval' => 'required_if:variants.*.other_info.payment_type,subscription|sanitizeText|maxLength:100',
367 + 'variants.*.other_info.repeat_interval' => [
368 + RequiredWhenRule::make(
369 + 'variants.*.other_info.payment_type',
370 + 'subscription',
371 + esc_html__('Interval is required.', 'fluent-cart')
372 + ),
373 + 'sanitizeText',
374 + 'maxLength:100',
375 + ],
336 376 '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',
377 + 'variants.*.other_info.manage_setup_fee' => [
378 + RequiredWhenRule::make(
379 + 'variants.*.other_info.payment_type',
380 + 'subscription',
381 + esc_html__('Setup Fee option is required.', 'fluent-cart')
382 + ),
383 + 'sanitizeText',
384 + 'maxLength:100',
385 + ],
386 + 'variants.*.other_info.signup_fee' => [
387 + RequiredWhenRule::make(
388 + 'variants.*.other_info.manage_setup_fee',
389 + 'yes',
390 + esc_html__('Setup Fee Amount is required.', 'fluent-cart')
391 + ),
392 + ],
393 + 'variants.*.other_info.signup_fee_name' => [
394 + RequiredWhenRule::make(
395 + 'variants.*.other_info.manage_setup_fee',
396 + 'yes',
397 + esc_html__('Setup Fee Name is required.', 'fluent-cart')
398 + ),
399 + 'sanitizeText',
400 + 'maxLength:100',
401 + ],
340 402 ];
341 403 $rules = array_merge($rules, $variantsOtherInfoRules);
342 404
343 405 }
@@ -396,11 +458,8 @@
396 458 'variants.*.item_cost.required_if' => esc_html__('Item cost is required.', 'fluent-cart'),
397 459 'variants.*.other_info.description.max' => esc_html__('Description may not be greater than 255 characters.', 'fluent-cart'),
398 460 'variants.*.other_info.payment_type.required' => esc_html__('Payment Type is required.', 'fluent-cart'),
399 461 '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 462 ];
404 463
405 464 $messages = array_merge($messages, $otherInfoMessages);
406 465 }
@@ -463,8 +522,11 @@
463 522 'detail.other_info.shipping_class' => 'intval',
464 523 'detail.other_info.tax_class' => 'intval',
465 524 'detail.other_info.tax_exempt' => 'sanitize_text_field',
466 525 'detail.other_info.active_editor' => 'sanitize_text_field',
526 + 'detail.other_info.fluent_player_video' => function ($value) {
527 + return ProductVideoSettings::sanitize($value);
528 + },
467 529 ];
468 530
469 531 foreach ($detailFieldMap as $field => $sanitizer) {
470 532 if (Arr::has($data, $field)) {
@@ -512,9 +574,14 @@
512 574 "variants.$index.serial_index" => 'intval',
513 575 "variants.$index.downloadable" => 'sanitize_text_field',
514 576 "variants.$index.fulfillment_type" => 'sanitize_text_field',
515 577 "variants.$index.sku" => function ($value) {
516 - return empty($value) ? null : sanitize_text_field($value);
578 + // empty() treats the string "0" as empty too, which would silently
579 + // convert a legitimate sku of "0" to NULL — sku is a textual
580 + // identifier (VARCHAR), not a numeric flag. Match the explicit
581 + // '' / null check ProductResource::update() already uses for the
582 + // same reason when it clears a sku.
583 + return ($value === '' || $value === null) ? null : sanitize_text_field($value);
517 584 },
518 585 ];
519 586
520 587 foreach ($variantFieldMap as $field => $sanitizer) {