Compare commits

..
3 Commits
6 changed files with 45 additions and 12 deletions
+16
View File
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [0.16.2] - 2026-09-09
### Fixed
- `Lunar\Base\ShippingManifest` is a request-lifetime singleton whose `getOptions()` re-runs the
shipping modifier pipeline without ever clearing its `$options` collection first, and whose
`addOption()` keeps the first entry per `getIdentifier()` and silently drops any later one. In
practice, an option resolved for an earlier shipping address (or cart state) shadowed the
correct one after the address/region changed within the same request — e.g. a carrier priced
differently across two zones that both match an address would keep quoting the stale zone's
price, and `ApplyShipping` would price the cart total off that same stale option. Renamed
`Modules\Core\Shipping\Listeners\FlushLivePricingCache` to
`Modules\Core\Shipping\Listeners\InvalidateShippingOptions` and had it additionally call
`ShippingManifest::clearOptions()`, merged in because both invalidations fire on the exact same
event set (`CartLineAdded`, `CartLineUpdated`, `CartLineRemoved`, `CartCleared`,
`ShippingAddressSet`) — the only inputs the shipping modifier pipeline depends on.
## [0.16.1] - 2026-09-09 ## [0.16.1] - 2026-09-09
### Fixed ### Fixed
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "boboko/core", "name": "boboko/core",
"description": "Core module — authentication and shared panel behaviour", "description": "Core module — authentication and shared panel behaviour",
"type": "library", "type": "library",
"version": "0.16.1", "version": "0.16.2",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Modules\\Core\\": "src/" "Modules\\Core\\": "src/"
@@ -24,6 +24,7 @@ class StorefrontLabels
'nav.account' => ['en' => 'Account', 'el' => 'Λογαριασμός'], 'nav.account' => ['en' => 'Account', 'el' => 'Λογαριασμός'],
'nav.back' => ['en' => 'Back', 'el' => 'Πίσω'], 'nav.back' => ['en' => 'Back', 'el' => 'Πίσω'],
'nav.contact' => ['en' => 'Contact', 'el' => 'Επικοινωνία'], 'nav.contact' => ['en' => 'Contact', 'el' => 'Επικοινωνία'],
'nav.close' => ['en' => 'Close', 'el' => 'Κλείσιμο'],
'cart.empty' => ['en' => 'Your cart is empty', 'el' => 'Το καλάθι σας είναι άδειο'], 'cart.empty' => ['en' => 'Your cart is empty', 'el' => 'Το καλάθι σας είναι άδειο'],
'cart.checkout' => ['en' => 'Checkout', 'el' => 'Ολοκλήρωση Παραγγελίας'], 'cart.checkout' => ['en' => 'Checkout', 'el' => 'Ολοκλήρωση Παραγγελίας'],
'cart.total' => ['en' => 'Total', 'el' => 'Σύνολο'], 'cart.total' => ['en' => 'Total', 'el' => 'Σύνολο'],
@@ -67,6 +68,7 @@ class StorefrontLabels
'en' => '{0} No products found|{1} Showing :first–:last of :total result|[2,*] Showing :first–:last of :total results', 'en' => '{0} No products found|{1} Showing :first–:last of :total result|[2,*] Showing :first–:last of :total results',
'el' => '{0} Δεν βρέθηκαν προϊόντα|{1} Εμφάνιση :first–:last από :total αποτέλεσμα|[2,*] Εμφάνιση :first–:last από :total αποτελέσματα', 'el' => '{0} Δεν βρέθηκαν προϊόντα|{1} Εμφάνιση :first–:last από :total αποτέλεσμα|[2,*] Εμφάνιση :first–:last από :total αποτελέσματα',
], ],
'shop.all_products' => ['en' => 'All Products', 'el' => 'Όλα τα Προϊόντα'],
'shop.sort_label' => ['en' => 'Sort products', 'el' => 'Ταξινόμηση προϊόντων'], 'shop.sort_label' => ['en' => 'Sort products', 'el' => 'Ταξινόμηση προϊόντων'],
'shop.sort_default' => ['en' => 'Default sorting', 'el' => 'Προεπιλεγμένη ταξινόμηση'], 'shop.sort_default' => ['en' => 'Default sorting', 'el' => 'Προεπιλεγμένη ταξινόμηση'],
'shop.sort_popularity' => ['en' => 'Popularity', 'el' => 'Δημοφιλή'], 'shop.sort_popularity' => ['en' => 'Popularity', 'el' => 'Δημοφιλή'],
+2 -2
View File
@@ -26,7 +26,7 @@ use Modules\Core\Shipping\Carriers\BoxNow\BoxNowRateDriver;
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface; use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
use Modules\Core\Shipping\Filament\Pages\ManageShippingRates; use Modules\Core\Shipping\Filament\Pages\ManageShippingRates;
use Modules\Core\Shipping\Jobs\PollShipmentTrackingJob; use Modules\Core\Shipping\Jobs\PollShipmentTrackingJob;
use Modules\Core\Shipping\Listeners\FlushLivePricingCache; use Modules\Core\Shipping\Listeners\InvalidateShippingOptions;
use Modules\Core\Shipping\Models\Shipment; use Modules\Core\Shipping\Models\Shipment;
class ShippingServiceProvider extends ServiceProvider class ShippingServiceProvider extends ServiceProvider
@@ -70,7 +70,7 @@ class ShippingServiceProvider extends ServiceProvider
}); });
foreach ([CartLineAdded::class, CartLineUpdated::class, CartLineRemoved::class, CartCleared::class, ShippingAddressSet::class] as $event) { foreach ([CartLineAdded::class, CartLineUpdated::class, CartLineRemoved::class, CartCleared::class, ShippingAddressSet::class] as $event) {
Event::listen($event, [FlushLivePricingCache::class, 'handle']); Event::listen($event, [InvalidateShippingOptions::class, 'handle']);
} }
// Deferred: the Shipping facade resolves a binding registered in // Deferred: the Shipping facade resolves a binding registered in
+1 -1
View File
@@ -17,7 +17,7 @@ use Lunar\Shipping\Models\ShippingRate;
* across carts: the quote depends on cart-specific weight/quantity/ * across carts: the quote depends on cart-specific weight/quantity/
* destination (see docs/checkout.md). * destination (see docs/checkout.md).
* *
* Invalidated by Modules\Core\Shipping\Listeners\FlushLivePricingCache on * Invalidated by Modules\Core\Shipping\Listeners\InvalidateShippingOptions on
* the only two things that can change what this cart's quote should be: a * the only two things that can change what this cart's quote should be: a
* cart line changing (add/update/remove/clear) or the shipping address * cart line changing (add/update/remove/clear) or the shipping address
* changing. Deliberately NOT invalidated on order placement — the price * changing. Deliberately NOT invalidated on order placement — the price
@@ -3,6 +3,7 @@
namespace Modules\Core\Shipping\Listeners; namespace Modules\Core\Shipping\Listeners;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Lunar\Facades\ShippingManifest;
use Lunar\Shipping\Facades\Shipping; use Lunar\Shipping\Facades\Shipping;
use Lunar\Shipping\Models\ShippingRate; use Lunar\Shipping\Models\ShippingRate;
use Modules\Core\Cart\Events\CartCleared; use Modules\Core\Cart\Events\CartCleared;
@@ -13,17 +14,29 @@ use Modules\Core\Checkout\Events\ShippingAddressSet;
use Modules\Core\Shipping\Contracts\SupportsLivePricing; use Modules\Core\Shipping\Contracts\SupportsLivePricing;
/** /**
* Flushes Modules\Core\Shipping\Concerns\CachesLivePricing's cached quotes * Invalidates everything that caches or memoises resolved shipping options
* for a cart on the only two things that can change what they should be: a * for a cart, on the only two things that can change what they should be: a
* cart line changing (weight/quantity) or the shipping address changing * cart line changing (weight/quantity) or the shipping address changing
* (destination). See that trait's docblock for why order placement is * (destination).
* deliberately not a trigger here.
* *
* Only rates whose method's driver implements SupportsLivePricing are ever * Two things need clearing here, both stale for the same reason:
* cached by CachesLivePricing, so only their ids need a forget() call — *
* no need to touch every ShippingRate row on every cart change. * - Modules\Core\Shipping\Concerns\CachesLivePricing's per-rate cache (see
* that trait's docblock for why order placement is deliberately not a
* trigger). Only rates whose method's driver implements
* SupportsLivePricing are ever cached by it, so only their ids need a
* forget() call — no need to touch every ShippingRate row on every cart
* change.
* - Lunar\Base\ShippingManifest's $options collection, which is a
* request-lifetime singleton: ShippingManifest::getOptions() re-runs the
* modifier pipeline on every call but never clears $options first, and
* addOption() keeps the first entry per identifier and silently drops any
* later one — so an option resolved for an earlier address/cart state
* shadows the correct one after that state changes, within the same
* request. clearOptions() forces the next getOptions() call to resolve
* fresh.
*/ */
class FlushLivePricingCache class InvalidateShippingOptions
{ {
public function handle(CartLineAdded|CartLineUpdated|CartLineRemoved|CartCleared|ShippingAddressSet $event): void public function handle(CartLineAdded|CartLineUpdated|CartLineRemoved|CartCleared|ShippingAddressSet $event): void
{ {
@@ -32,6 +45,8 @@ class FlushLivePricingCache
foreach ($this->livePricingRateIds() as $rateId) { foreach ($this->livePricingRateIds() as $rateId) {
Cache::forget("shipping.live_price.{$rateId}.{$cart->id}"); Cache::forget("shipping.live_price.{$rateId}.{$cart->id}");
} }
ShippingManifest::clearOptions();
} }
/** /**