| @@ -3,11 +3,14 @@ | ||
| 3 | 3 | namespace FluentCart\App\Services; |
| 4 | 4 | |
| 5 | 5 | use FluentCart\Api\Resource\ProductVariationResource; |
| 6 | 6 | use FluentCart\App\CPT\FluentProducts; |
| 7 | +use FluentCart\App\Helpers\Helper; | |
| 7 | 8 | use FluentCart\App\Models\ProductDetail; |
| 8 | 9 | use FluentCart\App\Models\ProductVariation; |
| 9 | 10 | use FluentCart\Framework\Support\Arr; |
| 11 | +use FluentCart\App\Http\Rules\RequiredWhenRule; | |
| 12 | +use FluentCart\App\Http\Rules\WhenFilledRule; | |
| 10 | 13 | use FluentCart\Framework\Validator\Validator; |
| 11 | 14 | |
| 12 | 15 | class BulkProductInsertService |
| 13 | 16 | { |
| @@ -103,13 +106,60 @@ | ||
| 103 | 106 | }, |
| 104 | 107 | ], |
| 105 | 108 | 'variants.*.other_info' => 'required|array', |
| 106 | 109 | 'variants.*.other_info.payment_type' => 'required|sanitizeText|in:onetime,subscription', |
| 107 | - 'variants.*.other_info.repeat_interval' => 'nullable|required_if:variants.*.other_info.payment_type,subscription|sanitizeText|in:yearly,half_yearly,quarterly,monthly,weekly,daily', | |
| 110 | + 'variants.*.other_info.times' => [ | |
| 111 | + function ($attribute, $value, $rules, $allData) { | |
| 112 | + $index = explode('.', $attribute)[1]; | |
| 113 | + | |
| 114 | + return Helper::installmentTimesError(Arr::get($allData, "variants.$index.other_info")); | |
| 115 | + }, | |
| 116 | + ], | |
| 117 | + // Conditional requirements here are closures, not `required_if`, and | |
| 118 | + // the attributes carrying one have no `nullable`: filterExcludeables() | |
| 119 | + // drops EVERY rule — closures included — when `nullable` meets a falsy | |
| 120 | + // value, which is what left these requirements dead. WhenFilledRule | |
| 121 | + // therefore carries the value checks that `nullable` used to guard. | |
| 122 | + // | |
| 123 | + // manage_setup_fee keeps `nullable` on purpose: it is an optional flag | |
| 124 | + // that both services already default to 'no', so demanding it would | |
| 125 | + // reject payloads that simply omit it. | |
| 126 | + 'variants.*.other_info.repeat_interval' => [ | |
| 127 | + RequiredWhenRule::make( | |
| 128 | + 'variants.*.other_info.payment_type', | |
| 129 | + 'subscription', | |
| 130 | + __('Interval is required for subscriptions.', 'fluent-cart') | |
| 131 | + ), | |
| 132 | + WhenFilledRule::in( | |
| 133 | + ['yearly', 'half_yearly', 'quarterly', 'monthly', 'weekly', 'daily'], | |
| 134 | + __('Interval must be a valid frequency.', 'fluent-cart') | |
| 135 | + ), | |
| 136 | + ], | |
| 108 | 137 | 'variants.*.other_info.trial_days' => 'nullable|numeric|min:0|max:365', |
| 109 | - 'variants.*.other_info.manage_setup_fee' => 'nullable|required_if:variants.*.other_info.payment_type,subscription|sanitizeText|in:no,yes', | |
| 110 | - 'variants.*.other_info.signup_fee' => 'nullable|required_if:variants.*.other_info.manage_setup_fee,yes|numeric|min:0', | |
| 111 | - 'variants.*.other_info.signup_fee_name' => 'nullable|required_if:variants.*.other_info.manage_setup_fee,yes|sanitizeText|maxLength:100', | |
| 138 | + 'variants.*.other_info.manage_setup_fee' => 'nullable|sanitizeText|in:no,yes', | |
| 139 | + 'variants.*.other_info.signup_fee' => [ | |
| 140 | + RequiredWhenRule::make( | |
| 141 | + 'variants.*.other_info.manage_setup_fee', | |
| 142 | + 'yes', | |
| 143 | + __('Setup Fee Amount is required.', 'fluent-cart') | |
| 144 | + ), | |
| 145 | + WhenFilledRule::numericAtLeast( | |
| 146 | + 0, | |
| 147 | + __('Setup Fee must be a number.', 'fluent-cart'), | |
| 148 | + __('Setup Fee must be 0 or more.', 'fluent-cart') | |
| 149 | + ), | |
| 150 | + ], | |
| 151 | + 'variants.*.other_info.signup_fee_name' => [ | |
| 152 | + RequiredWhenRule::make( | |
| 153 | + 'variants.*.other_info.manage_setup_fee', | |
| 154 | + 'yes', | |
| 155 | + __('Setup Fee Name is required.', 'fluent-cart') | |
| 156 | + ), | |
| 157 | + WhenFilledRule::text( | |
| 158 | + 100, | |
| 159 | + __('Setup Fee Name must be plain text of 100 characters or fewer.', 'fluent-cart') | |
| 160 | + ), | |
| 161 | + ], | |
| 112 | 162 | ]; |
| 113 | 163 | |
| 114 | 164 | $messages = [ |
| 115 | 165 | 'post_title.required' => __('Title is required.', 'fluent-cart'), |
| @@ -126,17 +176,12 @@ | ||
| 126 | 176 | 'variants.*.item_price.numeric' => __('Price must be a number.', 'fluent-cart'), |
| 127 | 177 | 'variants.*.item_price.min' => __('Price must be a positive number.', 'fluent-cart'), |
| 128 | 178 | 'variants.*.other_info.payment_type.required' => __('Payment Type is required.', 'fluent-cart'), |
| 129 | 179 | 'variants.*.other_info.payment_type.in' => __('Payment Type must be onetime or subscription.', 'fluent-cart'), |
| 130 | - 'variants.*.other_info.repeat_interval.required_if' => __('Interval is required for subscriptions.', 'fluent-cart'), | |
| 131 | - 'variants.*.other_info.repeat_interval.in' => __('Interval must be a valid frequency.', 'fluent-cart'), | |
| 132 | 180 | 'variants.*.other_info.trial_days.numeric' => __('Trial days must be a number.', 'fluent-cart'), |
| 133 | 181 | 'variants.*.other_info.trial_days.min' => __('Trial days must be 0 or more.', 'fluent-cart'), |
| 134 | 182 | 'variants.*.other_info.trial_days.max' => __('Trial days may not be greater than 365.', 'fluent-cart'), |
| 135 | 183 | 'variants.*.other_info.manage_setup_fee.in' => __('Setup fee option must be yes or no.', 'fluent-cart'), |
| 136 | - 'variants.*.other_info.signup_fee.required_if' => __('Setup Fee Amount is required.', 'fluent-cart'), | |
| 137 | - 'variants.*.other_info.signup_fee.numeric' => __('Setup Fee must be a number.', 'fluent-cart'), | |
| 138 | - 'variants.*.other_info.signup_fee_name.required_if' => __('Setup Fee Name is required.', 'fluent-cart'), | |
| 139 | 184 | ]; |
| 140 | 185 | |
| 141 | 186 | $validator = Validator::make($data, $rules, $messages); |
| 142 | 187 | |
| @@ -486,13 +531,18 @@ | ||
| 486 | 531 | } |
| 487 | 532 | |
| 488 | 533 | /** |
| 489 | 534 | * Sanitize a price value to ensure it's a valid integer (cents). |
| 535 | + * | |
| 536 | + * The value ARRIVES in cents — this only normalizes float artifacts and | |
| 537 | + * rejects negatives. CSV imports carry dollars, so Importer.vue converts | |
| 538 | + * at parse time, keeping this endpoint on the same cents contract as every | |
| 539 | + * other write. See dev-docs/PRICING-AND-TAX.md §6. | |
| 490 | 540 | */ |
| 491 | 541 | protected function sanitizePrice($value): int |
| 492 | 542 | { |
| 493 | 543 | if (is_numeric($value)) { |
| 494 | - return absint(round(floatval($value) * 100)); | |
| 544 | + return absint(Helper::roundCent($value)); | |
| 495 | 545 | } |
| 496 | 546 | |
| 497 | 547 | return 0; |
| 498 | 548 | } |