| @@ -1,8 +1,10 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace FluentCart\App\Http\Requests; |
| 4 | 4 | |
| 5 | +use FluentCart\App\Helpers\Helper; | |
| 6 | +use FluentCart\App\Http\Rules\RequiredWhenRule; | |
| 5 | 7 | use FluentCart\Framework\Foundation\RequestGuard; |
| 6 | 8 | use FluentCart\Framework\Support\Arr; |
| 7 | 9 | |
| 8 | 10 | class ProductVariationRequest extends RequestGuard |
| @@ -73,8 +75,32 @@ | ||
| 73 | 75 | * @return array |
| 74 | 76 | */ |
| 75 | 77 | public function rules() |
| 76 | 78 | { |
| 79 | + // total_stock / available / committed / on_hold are `INT(11) NULL DEFAULT 0` | |
| 80 | + // (ProductVariationMigrator), so an empty counter is a legitimate stored state for | |
| 81 | + // a variation that never tracked stock. ProductEditModel::createOrUpdatePricing() | |
| 82 | + // posts the variation back exactly as the drawer loaded it, NULLs included, so | |
| 83 | + // demanding a number unconditionally rejected a plain price edit on such a row. | |
| 84 | + // | |
| 85 | + // This cannot be expressed as `nullable|numeric`: Validator::filterExcludeables() | |
| 86 | + // drops EVERY rule for a falsy value the moment `nullable` is present, which | |
| 87 | + // would disarm the conditional requirement too. It cannot sit beside a | |
| 88 | + // `required_if` string either — filterRequiredIf() discarded this closure along | |
| 89 | + // with every other rule whenever tracking was off, leaving the guard inert. The | |
| 90 | + // requirement is a RequiredWhenRule closure below for that reason. | |
| 91 | + $numericWhenProvided = function ($attribute, $value) { | |
| 92 | + if ($value === null || $value === '') { | |
| 93 | + return null; | |
| 94 | + } | |
| 95 | + | |
| 96 | + if (!is_numeric($value)) { | |
| 97 | + return esc_html__('Stock quantity must be a number.', 'fluent-cart'); | |
| 98 | + } | |
| 99 | + | |
| 100 | + return null; | |
| 101 | + }; | |
| 102 | + | |
| 77 | 103 | return [ |
| 78 | 104 | 'variants.variation_title' => 'required|sanitizeText|maxLength:200', |
| 79 | 105 | 'variants.sku' => 'nullable|sanitizeText|maxLength:30|unique:fct_product_variations,sku' . ($this->get('variants.id') ? ',' . $this->get('variants.id') : ''), |
| 80 | 106 | 'variants.item_price' => 'nullable|numeric|min:0', |
| @@ -92,9 +118,15 @@ | ||
| 92 | 118 | return null; |
| 93 | 119 | }, |
| 94 | 120 | ], |
| 95 | 121 | 'variants.manage_cost' => 'nullable|sanitizeText|maxLength:10', |
| 96 | - 'variants.item_cost' => 'required_if:variants.manage_cost,true', | |
| 122 | + 'variants.item_cost' => [ | |
| 123 | + RequiredWhenRule::make( | |
| 124 | + 'variants.manage_cost', | |
| 125 | + 'true', | |
| 126 | + esc_html__('Item cost is required.', 'fluent-cart') | |
| 127 | + ), | |
| 128 | + ], | |
| 97 | 129 | 'variants.fulfillment_type' => 'required|sanitizeText|maxLength:100', |
| 98 | 130 | 'variants.shipping_class' => function ($attr, $value) { |
| 99 | 131 | if ($value && !(\FluentCart\App\Models\ShippingClass::find(intval($value)))) { |
| 100 | 132 | return __('The selected shipping class does not exist.', 'fluent-cart'); |
| @@ -102,11 +134,35 @@ | ||
| 102 | 134 | return null; |
| 103 | 135 | }, |
| 104 | 136 | |
| 105 | 137 | 'variants.manage_stock' => 'nullable|numeric', |
| 106 | - 'variants.stock_status' => 'required_if:variants.manage_stock,1|sanitizeText|maxLength:50', | |
| 107 | - 'variants.total_stock' => 'required|numeric', | |
| 108 | - 'variants.available' => 'required|numeric', | |
| 138 | + 'variants.stock_status' => [ | |
| 139 | + RequiredWhenRule::make( | |
| 140 | + 'variants.manage_stock', | |
| 141 | + '1', | |
| 142 | + esc_html__('Stock status is required.', 'fluent-cart') | |
| 143 | + ), | |
| 144 | + 'sanitizeText', | |
| 145 | + 'maxLength:50', | |
| 146 | + ], | |
| 147 | + // Quantities are only demanded once tracking is actually on, mirroring | |
| 148 | + // stock_status directly above. | |
| 149 | + 'variants.total_stock' => [ | |
| 150 | + RequiredWhenRule::make( | |
| 151 | + 'variants.manage_stock', | |
| 152 | + '1', | |
| 153 | + esc_html__('Stock quantity is required when inventory tracking is on.', 'fluent-cart') | |
| 154 | + ), | |
| 155 | + $numericWhenProvided, | |
| 156 | + ], | |
| 157 | + 'variants.available' => [ | |
| 158 | + RequiredWhenRule::make( | |
| 159 | + 'variants.manage_stock', | |
| 160 | + '1', | |
| 161 | + esc_html__('Available quantity is required when inventory tracking is on.', 'fluent-cart') | |
| 162 | + ), | |
| 163 | + $numericWhenProvided, | |
| 164 | + ], | |
| 109 | 165 | // 'variants.available' => [ |
| 110 | 166 | // 'required', |
| 111 | 167 | // 'numeric', |
| 112 | 168 | // function ($attribute, $value, $fail) { |
| @@ -115,10 +171,12 @@ | ||
| 115 | 171 | // } |
| 116 | 172 | // return null; |
| 117 | 173 | // }, |
| 118 | 174 | // ], |
| 119 | - 'variants.committed' => 'required|numeric', | |
| 120 | - 'variants.on_hold' => 'required|numeric', | |
| 175 | + // Ledger columns the merchant never edits — they are maintained by the stock | |
| 176 | + // listeners, so they only have to be a number when the payload carries one. | |
| 177 | + 'variants.committed' => [$numericWhenProvided], | |
| 178 | + 'variants.on_hold' => [$numericWhenProvided], | |
| 121 | 179 | |
| 122 | 180 | 'variants.serial_index' => 'nullable|numeric', |
| 123 | 181 | |
| 124 | 182 | 'variants.other_info' => 'required|array', |
| @@ -131,9 +189,9 @@ | ||
| 131 | 189 | } |
| 132 | 190 | if (!empty($value) && !is_numeric($value)) { |
| 133 | 191 | return __('Times must be a number.', 'fluent-cart'); |
| 134 | 192 | } |
| 135 | - return null; | |
| 193 | + return Helper::installmentTimesError($this->get('variants.other_info')); | |
| 136 | 194 | }, |
| 137 | 195 | ], |
| 138 | 196 | 'variants.other_info.trial_days' => [ |
| 139 | 197 | function ($attribute, $value) { |
| @@ -148,13 +206,43 @@ | ||
| 148 | 206 | } |
| 149 | 207 | return null; |
| 150 | 208 | }, |
| 151 | 209 | ], |
| 152 | - 'variants.other_info.repeat_interval' => 'required_if:variants.other_info.payment_type,subscription|sanitizeText|maxLength:100', | |
| 210 | + 'variants.other_info.repeat_interval' => [ | |
| 211 | + RequiredWhenRule::make( | |
| 212 | + 'variants.other_info.payment_type', | |
| 213 | + 'subscription', | |
| 214 | + esc_html__('Interval is required.', 'fluent-cart') | |
| 215 | + ), | |
| 216 | + 'sanitizeText', | |
| 217 | + 'maxLength:100', | |
| 218 | + ], | |
| 153 | 219 | 'variants.other_info.billing_summary' => 'nullable|sanitizeTextArea|maxLength:255', |
| 154 | - 'variants.other_info.manage_setup_fee' => 'required_if:variants.other_info.payment_type,subscription|sanitizeText|maxLength:100', | |
| 155 | - 'variants.other_info.signup_fee' => 'required_if:variants.other_info.manage_setup_fee,yes', | |
| 156 | - 'variants.other_info.signup_fee_name' => 'required_if:variants.other_info.manage_setup_fee,yes|sanitizeText|maxLength:100', | |
| 220 | + 'variants.other_info.manage_setup_fee' => [ | |
| 221 | + RequiredWhenRule::make( | |
| 222 | + 'variants.other_info.payment_type', | |
| 223 | + 'subscription', | |
| 224 | + esc_html__('Setup Fee option is required.', 'fluent-cart') | |
| 225 | + ), | |
| 226 | + 'sanitizeText', | |
| 227 | + 'maxLength:100', | |
| 228 | + ], | |
| 229 | + 'variants.other_info.signup_fee' => [ | |
| 230 | + RequiredWhenRule::make( | |
| 231 | + 'variants.other_info.manage_setup_fee', | |
| 232 | + 'yes', | |
| 233 | + esc_html__('Setup Fee Amount is required.', 'fluent-cart') | |
| 234 | + ), | |
| 235 | + ], | |
| 236 | + 'variants.other_info.signup_fee_name' => [ | |
| 237 | + RequiredWhenRule::make( | |
| 238 | + 'variants.other_info.manage_setup_fee', | |
| 239 | + 'yes', | |
| 240 | + esc_html__('Setup Fee Name is required.', 'fluent-cart') | |
| 241 | + ), | |
| 242 | + 'sanitizeText', | |
| 243 | + 'maxLength:100', | |
| 244 | + ], | |
| 157 | 245 | 'variants.other_info.package_slug' => 'nullable|sanitizeText|maxLength:100', |
| 158 | 246 | 'variants.other_info.weight' => 'nullable|numeric', |
| 159 | 247 | 'variants.other_info.weight_unit' => 'nullable|sanitizeText|maxLength:10', |
| 160 | 248 | 'variants.other_info.length' => 'nullable|numeric', |
| @@ -203,18 +291,13 @@ | ||
| 203 | 291 | 'variants.sku.unique' => esc_html__('The SKU must be unique.', 'fluent-cart'), |
| 204 | 292 | 'variants.item_price.required' => esc_html__('Price is required.', 'fluent-cart'), |
| 205 | 293 | 'variants.item_price.numeric' => esc_html__('Price must be a number.', 'fluent-cart'), |
| 206 | 294 | 'variants.item_price.min' => esc_html__('Price must be a positive number greater than 0.', 'fluent-cart'), |
| 207 | - 'variants.stock_status.required_if' => esc_html__('Stock status is required.', 'fluent-cart'), | |
| 208 | - 'variants.item_cost.required_if' => esc_html__('Item cost is required.', 'fluent-cart'), | |
| 209 | 295 | 'variants.fulfillment_type.required' => esc_html__('Fulfilment Type is required.', 'fluent-cart'), |
| 210 | 296 | |
| 211 | 297 | 'variants.other_info.description.max' => esc_html__('Description may not be greater than 255 characters.', 'fluent-cart'), |
| 212 | 298 | 'variants.other_info.payment_type.required' => esc_html__('Payment Type is required.', 'fluent-cart'), |
| 213 | 299 | 'variants.other_info.times.required_if' => esc_html__('Times is required.', 'fluent-cart'), |
| 214 | - 'variants.other_info.repeat_interval.required_if' => esc_html__('Interval is required.', 'fluent-cart'), | |
| 215 | - 'variants.other_info.signup_fee.required_if' => esc_html__('Setup Fee Amount is required.', 'fluent-cart'), | |
| 216 | - 'variants.other_info.signup_fee_name.required_if' => esc_html__('Setup Fee Name is required.', 'fluent-cart'), | |
| 217 | 300 | 'variants.other_info.trial_days.numeric' => esc_html__('Trial days must be a number.', 'fluent-cart'), |
| 218 | 301 | 'variants.other_info.trial_days.max' => esc_html__('Trial period cannot exceed 365 days.', 'fluent-cart'), |
| 219 | 302 | ]; |
| 220 | 303 | } |