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 | api/Resource/ProductDetailResource.php +87 -6 1.4.2 → 1.6.5 View file →
@@ -1,8 +1,9 @@
1 1 <?php
2 2
3 3 namespace FluentCart\Api\Resource;
4 4
5 +use FluentCart\Api\Meta;
5 6 use FluentCart\App\Events\StockChanged;
6 7 use FluentCart\App\Helpers\Helper;
7 8 use FluentCart\App\Helpers\ProductAdminHelper;
8 9 use FluentCart\App\Models\ProductDetail;
@@ -99,8 +100,27 @@
99 100 }
100 101
101 102 $triggeredAction = Arr::get($params, 'action');
102 103
104 + // Advanced Variations is terminal: once a product uses it, variation_type
105 + // can never be changed to Simple / Simple Variations — the attribute
106 + // config and generated combinations are the product's source of truth and
107 + // a downgrade would orphan them. Guarded on ANY update path that writes
108 + // variation_type (not just the change_variation_type action) — the full
109 + // product save also sends variation_type and would otherwise bypass this —
110 + // and for any API client, not just the disabled admin dropdown. Only an
111 + // actual downgrade is blocked: re-saving the same advanced type, or an
112 + // update that omits variation_type, passes through untouched.
113 + if (
114 + Arr::has($data, 'variation_type')
115 + && $detail->variation_type === Helper::PRODUCT_TYPE_ADVANCE_VARIATION
116 + && Arr::get($data, 'variation_type') !== Helper::PRODUCT_TYPE_ADVANCE_VARIATION
117 + ) {
118 + return static::makeErrorResponse([
119 + ['code' => 422, 'message' => __('A product using Advanced Variations cannot be switched back to Simple or Simple Variations.', 'fluent-cart')]
120 + ]);
121 + }
122 +
103 123 // Stock & Price Range Handling
104 124 if ($triggeredAction === 'variant_modified') {
105 125 $manageStock = Arr::has($data, 'manage_stock') ? Arr::get($data, 'manage_stock') : $detail->manage_stock;
106 126
@@ -117,19 +137,77 @@
117 137
118 138 if ($triggeredAction === 'change_variation_type' && Arr::get($data, 'variation_type') === 'simple') {
119 139 $variationIds = Arr::get($data, 'variation_ids', []);
120 140 if (!empty($detail->post_id) && count($variationIds) > 0) {
121 - ProductAdminHelper::deleteOrphanVariant($detail->post_id, $variationIds);
122 -
141 + ProductAdminHelper::deleteOrphanVariant(
142 + $detail->post_id,
143 + $variationIds,
144 + __("the product variation type was changed to 'Simple'", 'fluent-cart')
145 + );
146 +
147 + // The surviving variant (variationIds[0]) is kept, but a Simple
148 + // product has no editor control to view, replace, or clear its
149 + // image (VariantTitleMedia only shows for simple_variations /
150 + // advanced_variations). Leaving that image attached would let it
151 + // keep rendering on the storefront gallery with no way for the
152 + // merchant to find or remove it, so it is cleared with the same
153 + // switch the admin UI now warns about.
154 + //
155 + // variation_ids is client-supplied, so confirm the surviving id
156 + // actually belongs to this product before deleting its media —
157 + // otherwise a caller could point it at an unrelated product's
158 + // variation and wipe that variation's image instead.
159 + $keptVariantBelongsToProduct = \FluentCart\App\Models\ProductVariation::query()
160 + ->where('id', $variationIds[0])
161 + ->where('post_id', $detail->post_id)
162 + ->exists();
163 +
164 + if ($keptVariantBelongsToProduct) {
165 + Meta::deleteVariationMedia($variationIds[0]);
166 + }
123 167 }
124 168 }
125 169
170 + // Switching INTO Advanced Variations (from Simple or Simple Variations)
171 + // deletes the existing variants now. They have no place in an
172 + // attribute-based product (the merchant builds fresh combinations from
173 + // attribute options), and Advanced Variations is terminal so there is
174 + // nothing to preserve them for — matching the destructive admin confirm
175 + // ("delete all current variations ... cannot be undone") and the editor
176 + // clearing them client-side. An empty keep-list deletes every variant for
177 + // the product; an unconfigured advanced product is hidden on the
178 + // storefront until the merchant generates combinations, so the empty
179 + // variant set never leaks. Keyed on the non-advanced -> advanced
180 + // transition itself, NOT the change_variation_type action, so the side
181 + // effect is identical on every write path that sets variation_type — the
182 + // dedicated detail endpoint AND the full pricing save (which calls update()
183 + // with action=variant_modified). Otherwise a full save or API client could
184 + // land a product on Advanced Variations without the deletion, leaving
185 + // inconsistent variant state. Mirrors the downgrade guard above.
186 + if (
187 + Arr::get($data, 'variation_type') === Helper::PRODUCT_TYPE_ADVANCE_VARIATION
188 + && $detail->variation_type !== Helper::PRODUCT_TYPE_ADVANCE_VARIATION
189 + && !empty($detail->post_id)
190 + ) {
191 + ProductAdminHelper::deleteOrphanVariant(
192 + $detail->post_id,
193 + [],
194 + __("the product variation type was changed to 'Advanced Variations'", 'fluent-cart')
195 + );
196 + }
197 +
126 198 $data['min_price'] = Arr::get($data, 'min_price') ?: ($detail->min_price ?? 0);
127 199 $data['max_price'] = Arr::get($data, 'max_price') ?: ($detail->max_price ?? 0);
128 200
129 - // Handle Default Variation
130 - if (empty(Arr::get($data, 'default_variation_id'))) {
131 - $data['default_variation_id'] = NULL;
201 + // Handle Default Variation. Only act when the caller actually supplied the
202 + // key: a partial update that never mentions it must leave the stored value
203 + // alone, while an explicitly empty value still clears it.
204 + if (Arr::has($data, 'default_variation_id')) {
205 + if (empty(Arr::get($data, 'default_variation_id'))) {
206 + $data['default_variation_id'] = NULL;
207 + }
208 + } else {
209 + unset($data['default_variation_id']);
132 210 }
133 211
134 212 // Handle other_info merge
135 213 if (Arr::has($data, 'other_info')) {
@@ -140,9 +218,12 @@
140 218 $mergedOtherInfo = array_merge($existingOtherInfo, $newOtherInfo);
141 219
142 220 // Handle subscription-specific logic
143 221 if (Arr::get($mergedOtherInfo, 'payment_type') == 'subscription' && Arr::get($mergedOtherInfo, 'manage_setup_fee') == 'yes') {
144 - $signupFee = Helper::toCent(floatval(Arr::get($mergedOtherInfo, 'signup_fee', 0)));
222 + // Cents in, and $mergedOtherInfo may carry the already-cents stored
223 + // value when the caller did not resend signup_fee — roundCent is
224 + // idempotent, so neither case is rescaled.
225 + $signupFee = Helper::roundCent(Arr::get($mergedOtherInfo, 'signup_fee', 0));
145 226 $mergedOtherInfo['signup_fee'] = $signupFee;
146 227 }
147 228
148 229 $data['other_info'] = $mergedOtherInfo;