Bump Version to 0.15.0
This commit is contained in:
+159
@@ -4,6 +4,165 @@ 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.15.0] - 2026-09-07
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Breaking:** `Modules\Core\Payment\Models\PaymentMethod` is now the full DB-instance layer for
|
||||||
|
Payment, same three-layer split (registry / DB instance / cross-cutting config) `Shipping`
|
||||||
|
already has via `ShippingMethod` — see `docs/payments.md`. Every value that used to live in
|
||||||
|
`config('lunar.payments.types.{type}.*')` (`payment_driver`, `capture_mode`, `captured_status`)
|
||||||
|
moves onto the `PaymentMethod` row itself as real columns: `driver` (the new
|
||||||
|
`PaymentDriverRegistry` key — NOT the same as `type`; two rows can share one driver), `name`
|
||||||
|
(admin-facing label, nothing played this role before), `capture_mode`, `captured_status`,
|
||||||
|
`authorized_status`, `position` (admin-controlled ordering, new — reorderable in the Filament
|
||||||
|
table), `driver_missing_at`. `config('lunar.payments.types')` is gone entirely; `config/
|
||||||
|
payment.php` now holds only `cart_pipeline` (genuinely cross-cutting — every store gets the
|
||||||
|
same pipeline wiring regardless of how many payment methods it configures).
|
||||||
|
- **Breaking:** `Modules\Core\Payment\Services\PaymentDriverResolver` is deleted, replaced by
|
||||||
|
`Modules\Core\Payment\Services\PaymentDriverRegistry` — `register(string $key, string
|
||||||
|
$driverClass)`/`resolve(string $key): ?object`/`all(): array<string, string>`. Deliberately
|
||||||
|
knows nothing about `PaymentMethod` or the database (mirrors `Lunar\Shipping\Managers\
|
||||||
|
ShippingManager`'s built-in-methods + `Manager::extend()` split, purpose-built rather than
|
||||||
|
extending `Illuminate\Support\Manager` — Payment's drivers implement several independent
|
||||||
|
capability interfaces at once, not one uniform contract). Built-ins (`OfflinePaymentDriver`
|
||||||
|
as `'offline'`, `StripePaymentDriver` as `'stripe'`) registered in
|
||||||
|
`PaymentServiceProvider::boot()`, exactly how `Shipping::extend('acs', ...)` already works.
|
||||||
|
- **Breaking:** `Modules\Core\Checkout\Services\CheckoutService::getPaymentMethods()` now returns
|
||||||
|
`Illuminate\Support\Collection<int, PaymentMethod>` (ordered by `position`), not
|
||||||
|
`array<string>`. A method is offered only once three independent checks all pass — `enabled`
|
||||||
|
(admin turned it on), `driver_missing_at` is null (the driver class still exists), and the
|
||||||
|
resolved driver's own `Configurable::isConfigured()` (its runtime requirements are met) — each
|
||||||
|
failure meaning something different to an admin diagnosing why a method isn't showing up.
|
||||||
|
`initiatePayment()` resolves the driver via the selected row's own `driver` column, not `type`.
|
||||||
|
- `Modules\Core\Payment\Filament\Resources\PaymentMethodResource` — `canCreate()`/`canDelete()`
|
||||||
|
now both `true` (previously hardcoded `false`, since a row could only ever be a config-defined
|
||||||
|
type before this release). New create/edit form (`name`, `type`, `driver` — a `Select`
|
||||||
|
populated live from `PaymentDriverRegistry::all()`, `capture_mode`, `captured_status`,
|
||||||
|
`authorized_status`); reorderable table (`->reorderable('position')`); a distinct "Driver
|
||||||
|
status" icon column (separate from the `enabled` toggle) showing whether `driver_missing_at`
|
||||||
|
is set.
|
||||||
|
- `Modules\Core\Order\Listeners\ApplyResolvedPaymentStatus` reads `captured_status`/
|
||||||
|
`authorized_status` off the `PaymentMethod` row (`where('type', $event->type)`) instead of
|
||||||
|
`config(...)`.
|
||||||
|
- `Modules\Core\Command\InstallLunarCommand::seedPaymentMethods()` no longer iterates
|
||||||
|
`config('lunar.payments.types')` — it seeds exactly one opinionated `cash-on-delivery` starter
|
||||||
|
row, every value a plain literal in the command itself (not sourced from config or the
|
||||||
|
registry — a driver has no business carrying opinions about what its captured order status
|
||||||
|
should be called; that's a merchant decision). Skip-if-exists, same as before.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `php artisan boboko:payment:sync-drivers` — reconciles every `PaymentMethod` row's `driver`
|
||||||
|
against `PaymentDriverRegistry`, setting `driver_missing_at` when a driver no longer resolves
|
||||||
|
(a package removed, a custom `register()` call deleted) and clearing it automatically if that
|
||||||
|
driver is registered again in a later deploy. Deliberately its own standalone command, meant to
|
||||||
|
run unconditionally on every container start/deploy (Dockerfile entrypoint, alongside
|
||||||
|
`migrate`) — "did the set of registered drivers change" is a deploy-time event, cheap enough to
|
||||||
|
check every time regardless of whether anything actually changed. Verified live: flags a row
|
||||||
|
whose `driver` was manually corrupted, and auto-clears the flag once the driver resolves again.
|
||||||
|
- `docs/payments.md` — new "Registry, DB instance, and cross-cutting config" section: the
|
||||||
|
three-layer split researched against `Shipping`'s own already-existing pattern and three real
|
||||||
|
e-commerce platforms (Shopify, WooCommerce, Medusa.js), the "would a store ever plausibly want
|
||||||
|
two different answers to this" test for deciding config vs. DB-column placement, and the
|
||||||
|
three-check availability chain.
|
||||||
|
|
||||||
|
- `Modules\Core\Payment\Services\PaymentMethodCache` (`Cache::rememberForever`, same pattern as
|
||||||
|
`Localization\Services\LanguageCache`) + `Modules\Core\Payment\Services\PaymentMethodService`
|
||||||
|
(`create`/`update`/`delete`/`list`) — the single read/write gateway for `PaymentMethod` now used
|
||||||
|
by every Filament resource action (create, edit, edit-fee, delete, the inline `enabled` toggle)
|
||||||
|
instead of the Eloquent model directly, so the cache is invalidated and
|
||||||
|
`PaymentMethodCreated`/`PaymentMethodUpdated`/`PaymentMethodDeleted`/`PaymentMethodsReordered`
|
||||||
|
dispatch on every write, with no exceptions other than Filament's own drag-to-reorder (which
|
||||||
|
does a raw bulk SQL `UPDATE` on the position column directly via
|
||||||
|
`CanReorderRecords`/`reorderTable()`, before `afterReordering()` fires — a confirmed, unavoidable
|
||||||
|
Filament limitation; the reorder hook only clears the cache and dispatches
|
||||||
|
`PaymentMethodsReordered` afterward). `CheckoutService::getPaymentMethods()` and
|
||||||
|
`ApplyResolvedPaymentStatus` both now read through the cache instead of querying `PaymentMethod`
|
||||||
|
directly.
|
||||||
|
- `Modules\Core\Payment\Models\CoreTransaction` (a `Lunar\Models\Transaction` subclass) +
|
||||||
|
`Modules\Core\Payment\Support\TransactionDriverAdapter`, registered via
|
||||||
|
`Lunar\Facades\ModelManifest::replace(Lunar\Models\Contracts\Transaction::class,
|
||||||
|
CoreTransaction::class)` — the same contract-swap mechanism already used elsewhere for
|
||||||
|
`Customer`/`Staff`. Fixes a real crash (`InvalidArgumentException: Driver [cash-on-delivery] not
|
||||||
|
supported`) the first time anything called `$transaction->refund()`/`->capture()`:
|
||||||
|
`Lunar\Models\Transaction::driver()` calls Lunar's own, entirely separate
|
||||||
|
`Lunar\Facades\Payments::driver()` manager, which had never heard of any of this codebase's
|
||||||
|
driver keys. `CoreTransaction::driver()` returns `TransactionDriverAdapter` instead, which
|
||||||
|
resolves the transaction's real `PaymentMethod`/`PaymentDriverRegistry` driver and calls it —
|
||||||
|
Lunar's own admin panel "Refund"/"Capture" buttons now transparently reach the real payment
|
||||||
|
system underneath, including correctly reporting failure (not a silently-faked success) when
|
||||||
|
the resolved driver doesn't implement `SupportsRefunds`/`SupportsCaptures`.
|
||||||
|
- `TransactionDriverAdapter::refundVia(Transaction $transaction, ?string $driverKey, int $amount,
|
||||||
|
?string $notes = null)` — refund through an explicitly chosen driver, independent of the one
|
||||||
|
the original payment went through (e.g. a cash-on-delivery order refunded via Bank Transfer,
|
||||||
|
which has no notion of the original offline payment at all). The order page's refund action
|
||||||
|
gained a "Refund via" `Select` (every `PaymentDriverRegistry` driver implementing
|
||||||
|
`SupportsRefunds`, defaulting to the transaction's own driver) that routes through this method
|
||||||
|
instead of `Lunar\Models\Transaction::refund()`, whose fixed signature has no room for a driver
|
||||||
|
override.
|
||||||
|
- `Modules\Core\Payment\Drivers\BankTransferPaymentDriver` (registered as `'bank-transfer'`) —
|
||||||
|
manual/attested, same trust model as `OfflinePaymentDriver`: no gateway call, `pay()`/`refund()`
|
||||||
|
decide success immediately on a staff member's say-so. Implements both `SupportsPay` and
|
||||||
|
`SupportsRefunds`; exists specifically so a payment taken through a different method can still
|
||||||
|
be refunded via bank transfer. The admin UI for receiving a payment this way (bank reference,
|
||||||
|
notes, proof-of-transfer upload) is a follow-up — the driver itself is complete and usable via
|
||||||
|
the registry today.
|
||||||
|
- `Modules\Core\Order\Filament\Infolists\TransactionEntry` (swapped in for Lunar's own
|
||||||
|
`Lunar\Admin\Support\Infolists\Components\Transaction` via a new
|
||||||
|
`OrderTransactionsExtension::extendTransactionsRepeatableEntry()` hook) — the order page's
|
||||||
|
transaction cards now also show a note recorded in `Transaction.meta['notes']` when the `notes`
|
||||||
|
column itself is empty. `Order\Services\TransactionRecorder` only ever wrote `notes` from
|
||||||
|
`PaymentResult::$failureReason`, which is never set on a successful result — a manual driver's
|
||||||
|
staff-entered note (e.g. `BankTransferPaymentDriver`'s) was being recorded but had nowhere to
|
||||||
|
render.
|
||||||
|
- `Modules\Core\Payment\Listeners\LogPaymentMethodActivity` — `PaymentMethod` now has an admin
|
||||||
|
activity trail, unlike `Order`/`Transaction`/`Staff` it previously had none. Routes
|
||||||
|
`PaymentMethodCreated`/`Updated`/`Deleted` through the existing `Logging\ActivityLogService`
|
||||||
|
(the same one `Localization\Listeners\LogTranslationActivity` already uses) rather than adding
|
||||||
|
`PaymentMethod` to `Lunar\Base\Traits\LogsActivity`'s generic model-observer logging —
|
||||||
|
`PaymentMethodUpdated::$old`/`PaymentMethodDeleted::$method`'s snapshot already carry more
|
||||||
|
deliberate before/after context than Eloquent's own dirty-attribute diffing would reconstruct.
|
||||||
|
`PaymentMethodsReordered` is deliberately NOT logged — a multi-row position change doesn't fit
|
||||||
|
`ActivityLogService`'s one-`Model`-subject shape, and isn't worth a new method for a low-stakes,
|
||||||
|
purely-cosmetic setting.
|
||||||
|
- New `payment_methods.refunded_status` column + form field (same `Select` pattern as
|
||||||
|
`captured_status`/`authorized_status`) — `ApplyResolvedPaymentStatus` now also reacts to
|
||||||
|
`PaymentRefunded`, so `Order.status` actually changes on a refund; before this, only the
|
||||||
|
*derived* `Order::paymentStatus()` reflected a refund (reading `transactions` live), while the
|
||||||
|
stored `status` column — what admin filtering, customer emails, etc. actually key off — never
|
||||||
|
moved. Resolves the ORIGINAL payment method for this lookup, not the refund event's own
|
||||||
|
`$type`: a refund routed through a different driver via `refundVia()` (e.g. cash-on-delivery
|
||||||
|
refunded through Bank Transfer) carries the REFUND driver's registry key as `$event->type`,
|
||||||
|
which usually isn't even a real `PaymentMethod.type` — the listener now finds the order's
|
||||||
|
earliest successful `capture`/`intent` transaction instead and reads `refunded_status` off
|
||||||
|
*that* transaction's own `PaymentMethod` row, since that's the payment the refund is actually
|
||||||
|
reversing. Deliberately no `void_status` yet — a void never moved money, so it doesn't carry
|
||||||
|
the same "the customer needs to see this changed" weight a refund does.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Existing `PaymentMethod` rows seeded before this release (`cash-on-delivery`, `cash-in-hand`)
|
||||||
|
had `driver`/`capture_mode`/`captured_status` all `NULL` after the migration ran — a data
|
||||||
|
backfill was required (not automated by the migration itself) to restore them to a resolvable
|
||||||
|
state; flagged here since a consuming app upgrading past this release needs the same backfill
|
||||||
|
for its own pre-existing rows before `getPaymentMethods()` will offer them again.
|
||||||
|
- `Lunar\Admin\Filament\Resources\OrderResource\Pages\ManageOrder::getRefundAction()`/
|
||||||
|
`getCaptureAction()` and `OrderItemsTable::getBulkRefundAction()` report a failed refund/capture
|
||||||
|
by calling `$action->failureNotification(...)`, `$action->failure()`, then `$action->halt()` —
|
||||||
|
but `Filament\Actions\Concerns\InteractsWithActions::callMountedAction()` only ever sends that
|
||||||
|
notification from a code path that runs after the action's closure returns normally; `halt()`
|
||||||
|
throws `Filament\Support\Exceptions\Halt`, caught by an earlier `catch` block that rolls back and
|
||||||
|
returns, so the notification was built but never sent — clicking "Refund" on a payment method
|
||||||
|
that genuinely can't be refunded looked like nothing happened at all, no error, no toast. Real,
|
||||||
|
pre-existing Filament/Lunar bug, invisible until this release's `TransactionDriverAdapter` made
|
||||||
|
an honest failure (rather than a hard crash or a silently-faked success) actually reachable.
|
||||||
|
Fixed via new `Modules\Core\Order\Filament\Extensions\OrderRefundActionsExtension`/
|
||||||
|
`OrderItemsTableExtension`, which wrap the affected actions' closures to send the queued failure
|
||||||
|
notification themselves before re-throwing `Halt`.
|
||||||
|
- `TransactionDriverAdapter::refund()`/`capture()` never included `order_id` in the `$context`
|
||||||
|
passed to the driver, so `Order\Listeners\RecordPaymentTransaction`/`ApplyResolvedPaymentStatus`
|
||||||
|
(both requiring `$context['order_id']`) silently no-op'd for every admin-initiated refund/capture
|
||||||
|
through any driver — no audit `Transaction` row was ever created, regardless of whether the
|
||||||
|
refund/capture itself succeeded. Fixed by passing `$transaction->order_id` through.
|
||||||
|
|
||||||
## [0.14.0] - 2026-09-03
|
## [0.14.0] - 2026-09-03
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+1
-1
@@ -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.14.0",
|
"version": "0.15.0",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Modules\\Core\\": "src/"
|
"Modules\\Core\\": "src/"
|
||||||
|
|||||||
Reference in New Issue
Block a user