From 0c169daf55422a4f49a8036442d8d49efed976ed Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Fri, 18 Sep 2026 01:29:54 +0300 Subject: [PATCH] Bump version to 0.19.0 --- CHANGELOG.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ composer.json | 2 +- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1508545..150c6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,109 @@ 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/). +## [0.19.0] - 2026-09-18 + +### Added +- `Modules\Core\Payment\Contracts\RequiresFulfillmentType` — a payment driver can now declare + it only makes sense for one fulfillment type (carrier delivery vs. store pickup), the + payment-side mirror of `Shipping\Contracts\DeclaresFulfillmentType`. `CheckoutService:: + getPaymentMethods()` excludes a method whose driver disagrees with the cart's currently + selected shipping method — `OfflinePaymentDriver` ("pay in store") now requires + `store_pickup`, `CashOnDeliveryPaymentDriver` requires `carrier`. Previously every enabled, + configured payment method was offered regardless of shipping choice, so a shopper picking a + courier delivery could still see "Pay in store" (no staff member present to take cash), and a + store-pickup shopper could see cash-on-delivery (meaningless — there is no delivery to collect + payment on). No constraint is imposed before a shipping option is selected. +- `Modules\Core\Payment\Events\PaymentDeferred` — dispatched by any payment driver whose + `Pending` result will never resolve via a later gateway callback (currently only + `CashOnDeliveryPaymentDriver`), distinct from a Stripe-style `Pending` that a webhook will + still resolve. Handled by the new `Modules\Core\Order\Listeners\ + MarkOrderPlacedOnDeferredPayment`, which sets `Order::placed_at`, dispatches `OrderPlaced`, + and advances `status` past `awaiting_payment` — without ever touching `Order::paid`, which + still only flips via staff explicitly marking a COD order received. +- `Modules\Core\Order\Services\OrderPaymentResolutionService::resolveDeferredPayment()` — the + status-advance half of the above, reusing the same "advance past `awaiting_payment`" logic a + captured payment already uses. +- `Modules\Core\Checkout\Exceptions\NoShippingAddressException`. +- `Modules\Core\Shipping\Carriers\BoxNow\BoxNowClient::destinations()` — lists available Box + Now lockers (`GET /destinations`), backing a plain, self-hosted locker picker on checkout; + Box Now's own Destination Map JS widget only talks to their Production environment, making it + unusable while developing against Stage credentials. +- `config/shippingCarriers/boxnow.php`: `BOXNOW_PARTNER_ID` — issued alongside Box Now + credentials, consumed only by their client-side map widget, never by `BoxNowClient`'s own + REST authentication. +- A "Tracking history" list under each shipment on the order page (`Shipping\Extensions\ + OrderShipmentsExtension`) — every recorded carrier checkpoint, oldest first, not just the + latest status. +- `Modules\Core\Review\Services\ReviewService` and `ReviewEvents\ReviewReplied` — extracted + from `ManageProductReviews`'s inline `$record->update()`, following the write-then-dispatch + pattern used everywhere else. +- `Modules\Core\Catalog\Services\StockService::decrementForOrder()` — extracted from + `DecrementStockOnOrderPlaced`, isolating the atomic stock-decrement SQL and Meilisearch + reindex from the listener itself. +- `Modules\Core\Order\Services\OrderStatusFlow::isValidTransition()` — the single source of + truth for "is this a legal next status," replacing several listeners' own hardcoded "only + fire from status X" comparisons. + +### Fixed +- Cash-on-delivery orders were placed but never left `awaiting_payment`, were invisible in + customer order history, never decremented stock, and the storefront's own post-checkout + confirmation could never find them — `CashOnDeliveryPaymentDriver::pay()` returns `Pending`, + which dispatched no event at all, so nothing ever set `Order::placed_at` or advanced + `status`. Fixed by `PaymentDeferred`/`MarkOrderPlacedOnDeferredPayment` above. +- Staff marking a COD order "paid" (`OrderFulfillmentService::markPaid()`) flipped + `Order::paid`/`paid_at` but never recorded a `Transaction` row — no audit trail, and anything + reading `$order->transactions` (paid-amount displays included) saw nothing. Now records a + `capture` transaction via `TransactionRecorder`, the exact call site its own docblock had + already anticipated ("a future admin action ... can write a row the same way"). +- A confirmed cash-on-delivery shipment dispatched via ACS or Box Now never actually told the + carrier to collect payment — `ShipmentRequest::$paymentMode`/`$amountToCollect` were defined + on the DTO but no caller ever populated them, permanently dead-coding both carriers' COD + branches (`AcsFulfillmentService`'s `Cod_Ammount`/`Cod_Payment_Way`, Box Now's + `amountToBeCollected`). `OrderViewExtension`'s "Create Shipment" action now derives both from + `OrderStatusFlow::isCod($order)` at dispatch time — never left to staff to remember. +- `Modules\Core\Shipping\Jobs\PollShipmentTrackingJob`: one shipment's tracking lookup failing + (a carrier 500, a malformed parcel response) aborted the rest of that carrier's shipments in + the same batch — now individually caught and reported per shipment. +- Every Box Now delivery request 400'd (`P405`, invalid phone number) for any customer whose + phone was stored in local Greek format rather than full international — `contactNumber` is + now normalized to `+30...` before every request. +- Creating a Box Now shipment 400'd (`P401`/`P402`) whenever `BOXNOW_ORIGIN_LOCATION_ID` or the + sender contact fields were unset — documented and confirmed against a live sandbox account. +- Selecting a Box Now locker at checkout, then making any unrelated address-form edit + afterward (even a delivery-instructions keystroke), silently discarded the locker choice — + `Lunar\Actions\Carts\AddAddress` deletes and recreates the cart's shipping address row on + every save, wiping whatever `meta` a prior save had written onto it. + `CheckoutService::setShippingAddress()` now carries the locker forward across that + recreation; `selectShippingOption()` clears it when switching away from Box Now, so a stale + locker never resurfaces if the shopper switches back later. + `Shipping\Extensions\OrderViewExtension`'s "Box Now locker ID" field is no longer locked + read-only once a customer choice exists — staff can override it. +- Creating a Box Now shipping method 500'd (`Array to string conversion` / invalid JSON insert) + — the vendor `ListShippingMethod` page's `CreateAction` builds its form inline, bypassing + `ShippingMethodResourceExtension`'s translated-name field entirely; `Filament\Pages\ + ManageShippingRates`'s method picker and "Shipping Method" table column also queried/sorted + the now-JSON `name` column directly in SQL (`could not identify an ordering operator for type + json`), both resolved app-side instead. +- Creating or editing a Payment Method: `capture_mode` ("Charge immediately" / "Hold now, + charge later") was offered even for a driver with no `authorize()` method at all + (`CashOnDeliveryPaymentDriver`), which would have fatally errored at checkout had "authorize" + ever been selected — now hidden/non-required unless the driver implements + `SupportsAuthorization`. A spurious `validation.required` on the translated Name field, and + every new Payment Method silently saving at `position` 0 regardless of the intended + "last in the list" default — both traced to the same cause: an `Action::schema()` modal only + dehydrates fields backed by a real form component, so `fillForm()`'s defaults for `name`/ + `position` were computed but never actually reached the saved record. +- `Modules\Core\Auth\Services\UserOtpService::generateAndSend()` now dispatches `UserCreated` + when a new `User` row is created — this event was previously never dispatched anywhere in + this package at all, despite listeners existing for it. +- Applied a deliberate queueing policy across every Order/Localization/Customer/Payment/ + Catalog listener, judged case-by-case on "if the queue stalls for minutes/hours, does this + cause a real functional break, not just cosmetic staleness" — `RecordPaymentTransaction`, + `CompleteOrderOnPickedUp`, `CreateCustomerForUser`, and `DecrementStockOnOrderPlaced` stay + synchronous (a stalled queue would mean a real ordering violation or oversell risk); cache + flushes, activity logging, and carrier-checkpoint-driven fulfillment listeners are now queued. + ## [0.18.1] - 2026-09-16 ### Added diff --git a/composer.json b/composer.json index 4f67d15..b25c4ff 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "boboko/core", "description": "Core module — authentication and shared panel behaviour", "type": "library", - "version": "0.18.1", + "version": "0.19.0", "autoload": { "psr-4": { "Modules\\Core\\": "src/"