PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.6
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.6
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/Models/Cart.php +96 -22 1.5.1 → 1.6.6 View file →
@@ -136,16 +136,61 @@
136 136 public function setCartDataAttribute($settings)
137 137 {
138 138 $items = Arr::wrap($settings);
139 139
140 - // variation_display_title is a presentation-only value derived on read
141 - // (getCartDataAttribute). Strip it before persisting so mutation paths
142 - // that round-trip cart_data never bake stale denormalized text into the
143 - // stored JSON.
140 + // Collect object_ids of items still missing the snapshot so the relations
141 + // are fetched in ONE batched query instead of one per item — cart writes
142 + // are user-facing and can carry several unsnapshotted items (legacy/admin
143 + // carts). generateCartItemFromVariation already sets it for storefront
144 + // adds, and simple products get an empty snapshot stored once.
145 + $productIdByVariation = [];
146 + foreach ($items as $item) {
147 + if (!is_array($item) || Arr::get($item, 'is_custom')) {
148 + // Custom/manual items aren't product variations — their object_id
149 + // is not a variation id, so never resolve attribute relations for
150 + // them (a coincidental id match would corrupt their snapshot).
151 + continue;
152 + }
153 + $objectId = (int) Arr::get($item, 'object_id', 0);
154 + $otherInfo = Arr::get($item, 'other_info', []);
155 + if ($objectId && (!is_array($otherInfo) || !array_key_exists('item_attributes', $otherInfo))) {
156 + $productIdByVariation[$objectId] = (int) Arr::get($item, 'post_id', 0);
157 + }
158 + }
159 +
160 + $snapshotByVariation = $productIdByVariation
161 + ? AttributeHelper::getProductItemsAttributes(array_keys($productIdByVariation), $productIdByVariation)
162 + : [];
163 +
144 164 foreach ($items as &$item) {
145 - if (is_array($item)) {
146 - unset($item['variation_display_title']);
165 + if (!is_array($item)) {
166 + continue;
147 167 }
168 +
169 + // variation_display_title is a presentation-only value derived on
170 + // read (getCartDataAttribute). Strip it so mutation paths that
171 + // round-trip cart_data never bake stale denormalized text into JSON.
172 + unset($item['variation_display_title']);
173 +
174 + // Custom/manual items are not product variations — skip the backfill
175 + // so a coincidental object_id match can't bleed a variation snapshot.
176 + if (Arr::get($item, 'is_custom')) {
177 + continue;
178 + }
179 +
180 + // Persist the item_attributes snapshot from the batched lookup so
181 + // every cart (frontend, admin, pay-now) resolves the labeled
182 + // combination from the DB. Only items that were missing it appear
183 + // in the map; simple products store an empty snapshot once.
184 + $objectId = (int) Arr::get($item, 'object_id', 0);
185 + $otherInfo = Arr::get($item, 'other_info', []);
186 + if (!is_array($otherInfo)) {
187 + $otherInfo = [];
188 + }
189 + if ($objectId && array_key_exists($objectId, $snapshotByVariation) && !array_key_exists('item_attributes', $otherInfo)) {
190 + $otherInfo['item_attributes'] = $snapshotByVariation[$objectId];
191 + $item['other_info'] = $otherInfo;
192 + }
148 193 }
149 194 unset($item);
150 195
151 196 $this->attributes['cart_data'] = json_encode($items);
@@ -163,9 +208,9 @@
163 208 return [];
164 209 }
165 210
166 211 $key = $this->getKey();
167 -
212 +
168 213 if ($key && isset(static::$cache[$key])) {
169 214 return static::$cache[$key];
170 215 }
171 216
@@ -197,22 +242,17 @@
197 242 */
198 243 protected static function appendVariationDisplayTitle(array $items): array
199 244 {
200 245 return array_map(function ($item) {
201 - if (!is_array($item)) {
202 - return $item;
246 + // Single resolver: snapshot -> live-resolve when missing -> title.
247 + if (is_array($item)) {
248 + $item['variation_display_title'] = AttributeHelper::getDisplayAttributesString(
249 + Arr::get($item, 'other_info.item_attributes', []),
250 + $item,
251 + 'cart'
252 + );
203 253 }
204 254
205 - $itemAttributes = Arr::get($item, 'other_info.item_attributes', []);
206 -
207 - $variationDisplayTitle = (is_array($itemAttributes) && $itemAttributes)
208 - ? AttributeHelper::getDisplayAttributesString($itemAttributes, $item, 'cart')
209 - : '';
210 -
211 - $item['variation_display_title'] = $variationDisplayTitle !== ''
212 - ? $variationDisplayTitle
213 - : (string) Arr::get($item, 'title', '');
214 -
215 255 return $item;
216 256 }, $items);
217 257 }
218 258
@@ -261,8 +301,38 @@
261 301 {
262 302 return Arr::get($this->checkout_data, 'is_locked') === 'yes' && $this->order_id;
263 303 }
264 304
305 + /**
306 + * Whether this cart can still take an additional item, such as an order bump.
307 + *
308 + * False when the cart is locked to an existing payment (custom payment link,
309 + * renewal invoice, early installment) or already carries an upgrade.
310 + *
311 + * `is_locked` is a 'yes'/'no' string, so it must be compared explicitly —
312 + * `!empty()` treats the string 'no' as locked.
313 + *
314 + * Deliberately distinct from isLocked(), which additionally requires order_id
315 + * and is therefore false for renewal and early-installment carts, which never
316 + * set that column.
317 + *
318 + * Filterable so an integration that locks its cart only to pin its own item
319 + * (e.g. a booking) can still take order bumps. The filter only decides the
320 + * lock; a cart carrying an upgrade is refused after it either way. The upgrade
321 + * swap in WebCheckoutHandler::handleOrderBumpRequest() ignores this filter and
322 + * keeps refusing any `is_locked` cart.
323 + */
324 + public function acceptsAdditionalItems()
325 + {
326 + $accepts = (bool) apply_filters(
327 + 'fluent_cart/cart/accepts_additional_items',
328 + Arr::get($this->checkout_data, 'is_locked') !== 'yes',
329 + ['cart' => $this]
330 + );
331 +
332 + return $accepts && empty(Arr::get($this->checkout_data, 'upgrade_data'));
333 + }
334 +
265 335 public function addItem($item = [], $replacingIndex = null)
266 336 {
267 337 if ($this->isLocked()) {
268 338 return new \WP_Error('cart_locked', __('This cart is locked and cannot be modified.', 'fluent-cart'));
@@ -349,8 +419,12 @@
349 419 // that means we have to remove it
350 420 return $this->removeItem($variation->id, Arr::get($config, 'remove_args', []), true);
351 421 }
352 422
423 + if (!$variation->product) {
424 + return new \WP_Error('product_not_found', __('This product is no longer available.', 'fluent-cart'));
425 + }
426 +
353 427 $validate = Arr::get($config, 'will_validate', false);
354 428
355 429 $replacingIndex = null;
356 430
@@ -467,10 +541,10 @@
467 541 return $this->removeItem($variationId);
468 542 }
469 543 }
470 544
471 - // Subscription items may exist in cart,
472 - // but checkout must be initiated via direct checkout flow to ensure proper handling.
545 + // Subscription items may exist in cart,
546 + // but checkout must be initiated via direct checkout flow to ensure proper handling.
473 547 if (Arr::get($variation, 'payment_type', null) === 'subscription') {
474 548 return new \WP_Error('invalid_item', __('Subscription items must be purchased via direct checkout.', 'fluent-cart'));
475 549
476 550 }
@@ -475,9 +549,9 @@
475 549
476 550 }
477 551
478 552 // Find existing item in cart
479 - $replacingIndex = null;
553 + $replacingIndex = null;
480 554 $existingItem = $this->findExistingItemAndIndex(
481 555 $variationId,
482 556 Arr::get($config, 'matched_args', [])
483 557 );