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/Services/BulkProductUpdateService.php +73 -19 1.3.20 → 1.6.5 View file →
@@ -3,8 +3,9 @@
3 3 namespace FluentCart\App\Services;
4 4
5 5 use FluentCart\Api\Resource\ProductResource;
6 6 use FluentCart\Api\Resource\ProductVariationResource;
7 +use FluentCart\App\Helpers\Helper;
7 8 use FluentCart\App\Models\Product;
8 9 use FluentCart\App\Models\ProductDetail;
9 10 use FluentCart\App\Models\ProductVariation;
10 11 use FluentCart\App\Services\Filter\ProductFilter;
@@ -9,8 +10,10 @@
9 10 use FluentCart\App\Models\ProductVariation;
10 11 use FluentCart\App\Services\Filter\ProductFilter;
11 12 use FluentCart\Framework\Http\Request\Request;
12 13 use FluentCart\Framework\Support\Arr;
14 +use FluentCart\App\Http\Rules\RequiredWhenRule;
15 +use FluentCart\App\Http\Rules\WhenFilledRule;
13 16 use FluentCart\Framework\Validator\Validator;
14 17
15 18 class BulkProductUpdateService
16 19 {
@@ -15,9 +18,11 @@
15 18 class BulkProductUpdateService
16 19 {
17 20 /**
18 21 * Fetch products formatted for bulk editing.
19 - * 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.
20 25 */
21 26 public function fetchForBulkEdit(Request $request): array
22 27 {
23 28 $products = ProductFilter::fromRequest($request)->paginate();
@@ -35,9 +40,9 @@
35 40 }
36 41
37 42 /**
38 43 * Format a single product for the bulk edit spreadsheet.
39 - * Converts prices from cents to decimal and attaches categories.
44 + * Money stays in cents; PriceInput renders it as dollars in the grid.
40 45 */
41 46 protected function formatProductForEdit(Product $product): array
42 47 {
43 48 $product->load([
@@ -69,9 +74,9 @@
69 74 'manage_stock' => (int) $product->detail->manage_stock,
70 75 ];
71 76 }
72 77
73 - // Variants — convert prices from cents to decimal
78 + // Variants — money stays in cents
74 79 $data['variants'] = [];
75 80 if ($product->variants) {
76 81 foreach ($product->variants as $variant) {
77 82 $variantMedia = [];
@@ -83,10 +88,12 @@
83 88 'id' => $variant->id,
84 89 'post_id' => $variant->post_id,
85 90 'variation_title' => $variant->variation_title,
86 91 'sku' => $variant->sku,
87 - 'item_price' => $variant->item_price / 100,
88 - '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,
89 96 'payment_type' => $variant->payment_type,
90 97 'manage_stock' => (int) $variant->manage_stock,
91 98 'total_stock' => (int) $variant->total_stock,
92 99 'available' => (int) $variant->available,
@@ -193,13 +200,60 @@
193 200 },
194 201 ],
195 202 'variants.*.other_info' => 'required|array',
196 203 'variants.*.other_info.payment_type' => 'required|sanitizeText|in:onetime,subscription',
197 - 'variants.*.other_info.repeat_interval' => 'nullable|required_if:variants.*.other_info.payment_type,subscription|sanitizeText|in:yearly,half_yearly,quarterly,monthly,weekly,daily',
204 + 'variants.*.other_info.times' => [
205 + function ($attribute, $value, $rules, $allData) {
206 + $index = explode('.', $attribute)[1];
207 +
208 + return Helper::installmentTimesError(Arr::get($allData, "variants.$index.other_info"));
209 + },
210 + ],
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 + ],
198 231 'variants.*.other_info.trial_days' => 'nullable|numeric|min:0|max:365',
199 - 'variants.*.other_info.manage_setup_fee' => 'nullable|required_if:variants.*.other_info.payment_type,subscription|sanitizeText|in:no,yes',
200 - 'variants.*.other_info.signup_fee' => 'nullable|required_if:variants.*.other_info.manage_setup_fee,yes|numeric|min:0',
201 - '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 + ],
202 256 ];
203 257
204 258 $messages = [
205 259 'post_title.required' => __('Title is required.', 'fluent-cart'),
@@ -212,17 +266,12 @@
212 266 'variants.*.item_price.numeric' => __('Price must be a number.', 'fluent-cart'),
213 267 'variants.*.item_price.min' => __('Price must be a positive number.', 'fluent-cart'),
214 268 'variants.*.other_info.payment_type.required' => __('Payment Type is required.', 'fluent-cart'),
215 269 'variants.*.other_info.payment_type.in' => __('Payment Type must be onetime or subscription.', 'fluent-cart'),
216 - 'variants.*.other_info.repeat_interval.required_if' => __('Interval is required for subscriptions.', 'fluent-cart'),
217 - 'variants.*.other_info.repeat_interval.in' => __('Interval must be a valid frequency.', 'fluent-cart'),
218 270 'variants.*.other_info.trial_days.numeric' => __('Trial days must be a number.', 'fluent-cart'),
219 271 'variants.*.other_info.trial_days.min' => __('Trial days must be 0 or more.', 'fluent-cart'),
220 272 'variants.*.other_info.trial_days.max' => __('Trial days may not be greater than 365.', 'fluent-cart'),
221 273 'variants.*.other_info.manage_setup_fee.in' => __('Setup fee option must be yes or no.', 'fluent-cart'),
222 - 'variants.*.other_info.signup_fee.required_if' => __('Setup Fee Amount is required.', 'fluent-cart'),
223 - 'variants.*.other_info.signup_fee.numeric' => __('Setup Fee must be a number.', 'fluent-cart'),
224 - 'variants.*.other_info.signup_fee_name.required_if' => __('Setup Fee Name is required.', 'fluent-cart'),
225 274 ];
226 275
227 276 $validator = Validator::make($data, $rules, $messages);
228 277
@@ -335,9 +384,9 @@
335 384 if (!$product) {
336 385 throw new \RuntimeException(__('Product not found', 'fluent-cart'));
337 386 }
338 387
339 - // Use ProductResource::update for variants/detail (handles price * 100)
388 + // Use ProductResource::update for variants/detail (amounts are cents)
340 389 $updatePayload = [];
341 390
342 391 // Detail
343 392 if (Arr::has($productData, 'detail')) {
@@ -387,9 +436,9 @@
387 436 if (Arr::get($updatePayload, 'post_status') === 'published') {
388 437 $updatePayload['post_status'] = 'publish';
389 438 }
390 439
391 - // Use ProductResource::update which handles price conversion and variant updates
440 + // Use ProductResource::update which handles the variant and detail writes
392 441 if (!empty($updatePayload['variants']) || !empty($updatePayload['detail'])) {
393 442 ProductResource::update($updatePayload, $postId);
394 443 }
395 444
@@ -475,12 +524,13 @@
475 524 * Create a new variant for an existing product (used when duplicating a variant in bulk edit).
476 525 */
477 526 protected function createVariantForProduct(int $postId, Product $product, array $variantData): void
478 527 {
528 + // Amounts arrive in cents; normalize float artifacts without scaling.
479 529 $priceColumns = ['item_price', 'compare_price', 'item_cost'];
480 530 foreach ($priceColumns as $column) {
481 531 if (Arr::has($variantData, $column)) {
482 - $variantData[$column] = floatval(Arr::get($variantData, $column, 0)) * 100;
532 + $variantData[$column] = Helper::roundCent(Arr::get($variantData, $column, 0));
483 533 }
484 534 }
485 535
486 536 $otherInfo = Arr::get($variantData, 'other_info', []);
@@ -552,14 +602,18 @@
552 602 wp_set_post_terms($postId, $termIds, 'product-categories');
553 603 }
554 604
555 605 /**
556 - * 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.
557 611 */
558 612 protected function formatOtherInfoForEdit(array $otherInfo): array
559 613 {
560 614 if (!empty($otherInfo['signup_fee']) && is_numeric($otherInfo['signup_fee'])) {
561 - $otherInfo['signup_fee'] = (float) $otherInfo['signup_fee'] / 100;
615 + $otherInfo['signup_fee'] = (int) $otherInfo['signup_fee'];
562 616 }
563 617
564 618 return $otherInfo;
565 619 }