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.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 1.3.19 All 49 releases
← All changes | app/Services/BulkProductUpdateService.php +65 -19 1.5.5 → 1.6.5 View file →
@@ -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();
@@ -36,9 +40,9 @@
36 40 }
37 41
38 42 /**
39 43 * Format a single product for the bulk edit spreadsheet.
40 - * Converts prices from cents to decimal and attaches categories.
44 + * Money stays in cents; PriceInput renders it as dollars in the grid.
41 45 */
42 46 protected function formatProductForEdit(Product $product): array
43 47 {
44 48 $product->load([
@@ -70,9 +74,9 @@
70 74 'manage_stock' => (int) $product->detail->manage_stock,
71 75 ];
72 76 }
73 77
74 - // Variants — convert prices from cents to decimal
78 + // Variants — money stays in cents
75 79 $data['variants'] = [];
76 80 if ($product->variants) {
77 81 foreach ($product->variants as $variant) {
78 82 $variantMedia = [];
@@ -84,10 +88,12 @@
84 88 'id' => $variant->id,
85 89 'post_id' => $variant->post_id,
86 90 'variation_title' => $variant->variation_title,
87 91 'sku' => $variant->sku,
88 - 'item_price' => $variant->item_price / 100,
89 - 'compare_price' => $variant->compare_price / 100,
92 + // Cents, as stored and as the write endpoints now expect.
93 + // PriceInput renders these as dollars for the merchant.
94 + 'item_price' => (int) $variant->item_price,
95 + 'compare_price' => (int) $variant->compare_price,
90 96 'payment_type' => $variant->payment_type,
91 97 'manage_stock' => (int) $variant->manage_stock,
92 98 'total_stock' => (int) $variant->total_stock,
93 99 'available' => (int) $variant->available,
@@ -201,13 +207,53 @@
201 207
202 208 return Helper::installmentTimesError(Arr::get($allData, "variants.$index.other_info"));
203 209 },
204 210 ],
205 - '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 + ],
206 231 'variants.*.other_info.trial_days' => 'nullable|numeric|min:0|max:365',
207 - 'variants.*.other_info.manage_setup_fee' => 'nullable|required_if:variants.*.other_info.payment_type,subscription|sanitizeText|in:no,yes',
208 - 'variants.*.other_info.signup_fee' => 'nullable|required_if:variants.*.other_info.manage_setup_fee,yes|numeric|min:0',
209 - '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 + ],
210 256 ];
211 257
212 258 $messages = [
213 259 'post_title.required' => __('Title is required.', 'fluent-cart'),
@@ -220,17 +266,12 @@
220 266 'variants.*.item_price.numeric' => __('Price must be a number.', 'fluent-cart'),
221 267 'variants.*.item_price.min' => __('Price must be a positive number.', 'fluent-cart'),
222 268 'variants.*.other_info.payment_type.required' => __('Payment Type is required.', 'fluent-cart'),
223 269 'variants.*.other_info.payment_type.in' => __('Payment Type must be onetime or subscription.', 'fluent-cart'),
224 - 'variants.*.other_info.repeat_interval.required_if' => __('Interval is required for subscriptions.', 'fluent-cart'),
225 - 'variants.*.other_info.repeat_interval.in' => __('Interval must be a valid frequency.', 'fluent-cart'),
226 270 'variants.*.other_info.trial_days.numeric' => __('Trial days must be a number.', 'fluent-cart'),
227 271 'variants.*.other_info.trial_days.min' => __('Trial days must be 0 or more.', 'fluent-cart'),
228 272 'variants.*.other_info.trial_days.max' => __('Trial days may not be greater than 365.', 'fluent-cart'),
229 273 'variants.*.other_info.manage_setup_fee.in' => __('Setup fee option must be yes or no.', 'fluent-cart'),
230 - 'variants.*.other_info.signup_fee.required_if' => __('Setup Fee Amount is required.', 'fluent-cart'),
231 - 'variants.*.other_info.signup_fee.numeric' => __('Setup Fee must be a number.', 'fluent-cart'),
232 - 'variants.*.other_info.signup_fee_name.required_if' => __('Setup Fee Name is required.', 'fluent-cart'),
233 274 ];
234 275
235 276 $validator = Validator::make($data, $rules, $messages);
236 277
@@ -343,9 +384,9 @@
343 384 if (!$product) {
344 385 throw new \RuntimeException(__('Product not found', 'fluent-cart'));
345 386 }
346 387
347 - // Use ProductResource::update for variants/detail (handles price * 100)
388 + // Use ProductResource::update for variants/detail (amounts are cents)
348 389 $updatePayload = [];
349 390
350 391 // Detail
351 392 if (Arr::has($productData, 'detail')) {
@@ -395,9 +436,9 @@
395 436 if (Arr::get($updatePayload, 'post_status') === 'published') {
396 437 $updatePayload['post_status'] = 'publish';
397 438 }
398 439
399 - // Use ProductResource::update which handles price conversion and variant updates
440 + // Use ProductResource::update which handles the variant and detail writes
400 441 if (!empty($updatePayload['variants']) || !empty($updatePayload['detail'])) {
401 442 ProductResource::update($updatePayload, $postId);
402 443 }
403 444
@@ -483,12 +524,13 @@
483 524 * Create a new variant for an existing product (used when duplicating a variant in bulk edit).
484 525 */
485 526 protected function createVariantForProduct(int $postId, Product $product, array $variantData): void
486 527 {
528 + // Amounts arrive in cents; normalize float artifacts without scaling.
487 529 $priceColumns = ['item_price', 'compare_price', 'item_cost'];
488 530 foreach ($priceColumns as $column) {
489 531 if (Arr::has($variantData, $column)) {
490 - $variantData[$column] = floatval(Arr::get($variantData, $column, 0)) * 100;
532 + $variantData[$column] = Helper::roundCent(Arr::get($variantData, $column, 0));
491 533 }
492 534 }
493 535
494 536 $otherInfo = Arr::get($variantData, 'other_info', []);
@@ -560,14 +602,18 @@
560 602 wp_set_post_terms($postId, $termIds, 'product-categories');
561 603 }
562 604
563 605 /**
564 - * 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.
565 611 */
566 612 protected function formatOtherInfoForEdit(array $otherInfo): array
567 613 {
568 614 if (!empty($otherInfo['signup_fee']) && is_numeric($otherInfo['signup_fee'])) {
569 - $otherInfo['signup_fee'] = (float) $otherInfo['signup_fee'] / 100;
615 + $otherInfo['signup_fee'] = (int) $otherInfo['signup_fee'];
570 616 }
571 617
572 618 return $otherInfo;
573 619 }