| @@ -10,8 +10,10 @@ | ||
| 10 | 10 | use FluentCart\App\Models\ProductVariation; |
| 11 | 11 | use FluentCart\App\Services\Filter\ProductFilter; |
| 12 | 12 | use FluentCart\Framework\Http\Request\Request; |
| 13 | 13 | use FluentCart\Framework\Support\Arr; |
| 14 | +use FluentCart\App\Http\Rules\RequiredWhenRule; | |
| 15 | +use FluentCart\App\Http\Rules\WhenFilledRule; | |
| 14 | 16 | use FluentCart\Framework\Validator\Validator; |
| 15 | 17 | |
| 16 | 18 | class BulkProductUpdateService |
| 17 | 19 | { |
| @@ -16,9 +18,11 @@ | ||
| 16 | 18 | class BulkProductUpdateService |
| 17 | 19 | { |
| 18 | 20 | /** |
| 19 | 21 | * Fetch products formatted for bulk editing. |
| 20 | - * Returns products with decimal prices and category terms. | |
| 22 | + * Returns money in CENTS and category terms. It said "decimal prices" back | |
| 23 | + * when a temporary adapter divided here for a dollars-based grid; the grid | |
| 24 | + * renders cents through PriceInput now, so nothing is scaled on the way out. | |
| 21 | 25 | */ |
| 22 | 26 | public function fetchForBulkEdit(Request $request): array |
| 23 | 27 | { |
| 24 | 28 | $products = ProductFilter::fromRequest($request)->paginate(); |
| @@ -203,13 +207,53 @@ | ||
| 203 | 207 | |
| 204 | 208 | return Helper::installmentTimesError(Arr::get($allData, "variants.$index.other_info")); |
| 205 | 209 | }, |
| 206 | 210 | ], |
| 207 | - 'variants.*.other_info.repeat_interval' => 'nullable|required_if:variants.*.other_info.payment_type,subscription|sanitizeText|in:yearly,half_yearly,quarterly,monthly,weekly,daily', | |
| 211 | + // Conditional requirements here are closures, not `required_if`, and | |
| 212 | + // the attributes carrying one have no `nullable`: filterExcludeables() | |
| 213 | + // drops EVERY rule — closures included — when `nullable` meets a falsy | |
| 214 | + // value, which is what left these requirements dead. WhenFilledRule | |
| 215 | + // therefore carries the value checks that `nullable` used to guard. | |
| 216 | + // | |
| 217 | + // manage_setup_fee keeps `nullable` on purpose: it is an optional flag | |
| 218 | + // that both services already default to 'no', so demanding it would | |
| 219 | + // reject payloads that simply omit it. | |
| 220 | + 'variants.*.other_info.repeat_interval' => [ | |
| 221 | + RequiredWhenRule::make( | |
| 222 | + 'variants.*.other_info.payment_type', | |
| 223 | + 'subscription', | |
| 224 | + __('Interval is required for subscriptions.', 'fluent-cart') | |
| 225 | + ), | |
| 226 | + WhenFilledRule::in( | |
| 227 | + ['yearly', 'half_yearly', 'quarterly', 'monthly', 'weekly', 'daily'], | |
| 228 | + __('Interval must be a valid frequency.', 'fluent-cart') | |
| 229 | + ), | |
| 230 | + ], | |
| 208 | 231 | 'variants.*.other_info.trial_days' => 'nullable|numeric|min:0|max:365', |
| 209 | - 'variants.*.other_info.manage_setup_fee' => 'nullable|required_if:variants.*.other_info.payment_type,subscription|sanitizeText|in:no,yes', | |
| 210 | - 'variants.*.other_info.signup_fee' => 'nullable|required_if:variants.*.other_info.manage_setup_fee,yes|numeric|min:0', | |
| 211 | - 'variants.*.other_info.signup_fee_name' => 'nullable|required_if:variants.*.other_info.manage_setup_fee,yes|sanitizeText|maxLength:100', | |
| 232 | + 'variants.*.other_info.manage_setup_fee' => 'nullable|sanitizeText|in:no,yes', | |
| 233 | + 'variants.*.other_info.signup_fee' => [ | |
| 234 | + RequiredWhenRule::make( | |
| 235 | + 'variants.*.other_info.manage_setup_fee', | |
| 236 | + 'yes', | |
| 237 | + __('Setup Fee Amount is required.', 'fluent-cart') | |
| 238 | + ), | |
| 239 | + WhenFilledRule::numericAtLeast( | |
| 240 | + 0, | |
| 241 | + __('Setup Fee must be a number.', 'fluent-cart'), | |
| 242 | + __('Setup Fee must be 0 or more.', 'fluent-cart') | |
| 243 | + ), | |
| 244 | + ], | |
| 245 | + 'variants.*.other_info.signup_fee_name' => [ | |
| 246 | + RequiredWhenRule::make( | |
| 247 | + 'variants.*.other_info.manage_setup_fee', | |
| 248 | + 'yes', | |
| 249 | + __('Setup Fee Name is required.', 'fluent-cart') | |
| 250 | + ), | |
| 251 | + WhenFilledRule::text( | |
| 252 | + 100, | |
| 253 | + __('Setup Fee Name must be plain text of 100 characters or fewer.', 'fluent-cart') | |
| 254 | + ), | |
| 255 | + ], | |
| 212 | 256 | ]; |
| 213 | 257 | |
| 214 | 258 | $messages = [ |
| 215 | 259 | 'post_title.required' => __('Title is required.', 'fluent-cart'), |
| @@ -222,17 +266,12 @@ | ||
| 222 | 266 | 'variants.*.item_price.numeric' => __('Price must be a number.', 'fluent-cart'), |
| 223 | 267 | 'variants.*.item_price.min' => __('Price must be a positive number.', 'fluent-cart'), |
| 224 | 268 | 'variants.*.other_info.payment_type.required' => __('Payment Type is required.', 'fluent-cart'), |
| 225 | 269 | 'variants.*.other_info.payment_type.in' => __('Payment Type must be onetime or subscription.', 'fluent-cart'), |
| 226 | - 'variants.*.other_info.repeat_interval.required_if' => __('Interval is required for subscriptions.', 'fluent-cart'), | |
| 227 | - 'variants.*.other_info.repeat_interval.in' => __('Interval must be a valid frequency.', 'fluent-cart'), | |
| 228 | 270 | 'variants.*.other_info.trial_days.numeric' => __('Trial days must be a number.', 'fluent-cart'), |
| 229 | 271 | 'variants.*.other_info.trial_days.min' => __('Trial days must be 0 or more.', 'fluent-cart'), |
| 230 | 272 | 'variants.*.other_info.trial_days.max' => __('Trial days may not be greater than 365.', 'fluent-cart'), |
| 231 | 273 | 'variants.*.other_info.manage_setup_fee.in' => __('Setup fee option must be yes or no.', 'fluent-cart'), |
| 232 | - 'variants.*.other_info.signup_fee.required_if' => __('Setup Fee Amount is required.', 'fluent-cart'), | |
| 233 | - 'variants.*.other_info.signup_fee.numeric' => __('Setup Fee must be a number.', 'fluent-cart'), | |
| 234 | - 'variants.*.other_info.signup_fee_name.required_if' => __('Setup Fee Name is required.', 'fluent-cart'), | |
| 235 | 274 | ]; |
| 236 | 275 | |
| 237 | 276 | $validator = Validator::make($data, $rules, $messages); |
| 238 | 277 | |
| @@ -563,9 +602,13 @@ | ||
| 563 | 602 | wp_set_post_terms($postId, $termIds, 'product-categories'); |
| 564 | 603 | } |
| 565 | 604 | |
| 566 | 605 | /** |
| 567 | - * Convert cents-based fields in other_info to dollars for frontend display. | |
| 606 | + * Normalize the money fields in other_info for the bulk edit grid. | |
| 607 | + * | |
| 608 | + * Despite what this used to say, nothing is converted to dollars: signup_fee | |
| 609 | + * stays in CENTS and is only int-cast, because PriceInput does the rendering. | |
| 610 | + * Reintroducing a division here would halve-by-100 every setup fee in the grid. | |
| 568 | 611 | */ |
| 569 | 612 | protected function formatOtherInfoForEdit(array $otherInfo): array |
| 570 | 613 | { |
| 571 | 614 | if (!empty($otherInfo['signup_fee']) && is_numeric($otherInfo['signup_fee'])) { |