Bump version to 0.20.0
This commit is contained in:
@@ -4,6 +4,94 @@ 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.20.0] - 2026-09-23
|
||||
|
||||
### Added
|
||||
- `Modules\Core\Catalog\Support\ProductFilterBuilder::withVisibility()` — every storefront
|
||||
product read (`ProductService::list()`/`getById()`/`getBySlug()`/`random()`/`facets()`/
|
||||
`priceRange()`, and `ProductSearchService`) now excludes `status = "draft"` products unless
|
||||
`APP_DEBUG` is true. Previously nothing filtered by status anywhere in this service — a draft
|
||||
product was fully visible on the storefront in every environment, always.
|
||||
- Per-product custom input fields (`Modules\Core\Catalog\Models\Product::$custom_fields`) — a
|
||||
repeater on the product edit form lets a merchant define extra input a shopper fills in on
|
||||
that product's page before adding it to cart (a reference photo upload, personalization
|
||||
text, etc.), each field a `{key, type: text|textarea|file, label, required}` entry.
|
||||
Deliberately not a Lunar `ProductOption`: an option's values are a fixed, admin-authored list
|
||||
that define variants, which doesn't fit "the shopper uploads their own unique photo."
|
||||
Required a new first-party `Product` model (registered via `ModelManifest::replace()`) purely
|
||||
to add a cast and `$fillable` entry Lunar's own base model doesn't have for this column — see
|
||||
that class's own docblock for two real Lunar-integration bugs this surfaced (below).
|
||||
- `boboko:wipe-catalog` (`Modules\Core\Command\WipeCatalogCommand`) — irreversibly deletes every
|
||||
Product and everything that only exists because of a product (variants, variant prices,
|
||||
product-option assignments, images/media, associations, the `ImportMapping` rows tying them
|
||||
back to an external source, the Meilisearch index), leaving catalog structure other products
|
||||
could still reference untouched (ProductOption/ProductOptionValue definitions, Brands,
|
||||
Collections, Tags, Customer Groups). Gated by an OTP emailed to a real Staff account (reusing
|
||||
`Auth\Services\OtpService`, the same mechanism admin login already uses) plus typing the exact
|
||||
product count back — not a plain yes/no confirm.
|
||||
- `Modules\Core\Auth\Services\OtpService::generateAndSend()` gained an optional `$purpose`
|
||||
parameter (default `'login'`, fully backward compatible) — `OtpMail` picks its subject/intro
|
||||
copy from a small fixed set of known purposes, so a destructive-command confirmation code
|
||||
reads as "Confirm: Wipe Catalog," not the login flow's "Your login code."
|
||||
- `Modules\Core\Catalog\Services\SkuBackfillService` — the actual backfill logic behind
|
||||
`boboko:catalog:backfill-skus`, extracted so `MigrateImport\RunMigrateImportJob` can also call
|
||||
it automatically right after a Shopify import (gated on `$spec->source === 'shopify'`, the
|
||||
only source that creates variants at all) — no separate manual step needed after a migration.
|
||||
- `ProductSort::Popularity` — sorts by a new `order_count` field Meilisearch now indexes per
|
||||
product (trailing-year, physical order lines only, aggregated across a product's variants) —
|
||||
the same "popular" definition Lunar's own admin dashboard "Popular Products" widget already
|
||||
uses. Not wired into the storefront's sort dropdown yet; callable directly via
|
||||
`ProductService::list(sort: ProductSort::Popularity)`.
|
||||
|
||||
### Fixed
|
||||
- `MigrateImport\Shopify\ShopifyExportImporter` created every variant with Lunar's own column
|
||||
default `purchasable = 'always'` (purchasable regardless of stock) rather than respecting the
|
||||
real `Variant Inventory Qty` the import itself provides — now explicitly set to `'in_stock'`.
|
||||
Forward-only; does not retroactively touch variants from a prior import run.
|
||||
- `WipeCatalogCommand::wipe()` used `chunkById()` while deleting rows inside the loop — a known
|
||||
pitfall where `chunkById()` re-queries "id > lastSeenId" every iteration, so deleting rows
|
||||
shrinks the table out from under it and can silently skip products that were never actually
|
||||
deleted at all. Fixed by always re-querying the first N remaining rows instead of advancing
|
||||
an id cursor, so every product is visited exactly once regardless of how many are deleted out
|
||||
from under the query as it goes.
|
||||
- `WipeCatalogCommand` called `delete()`, not `forceDelete()`, on `Product`/`ProductVariant` —
|
||||
both use `SoftDeletes`, so an "irreversible" wipe only trashed rows, leaving them sitting in
|
||||
the table. Combined with the `chunkById` bug above, this left ~185 zero-variant ghost
|
||||
`Product` rows in practice, which then crashed the admin's own global search (Lunar's
|
||||
`ProductResource::getGlobalSearchResultDetails()` assumes every returned product — trashed
|
||||
ones deliberately included, by Lunar's own design — has at least one variant). Fixed to
|
||||
`forceDelete()`; the existing ghost rows were removed directly (none had live order/cart
|
||||
references). `wipe()` also now clears `'image'`-type `ImportMapping` rows, not just
|
||||
`'product'`/`'variant'`.
|
||||
- `ShopifyExportImporter::resolveOrImportImage()` trusted a cached `ImportMapping`'d `Media`
|
||||
object unconditionally — now verifies the row still exists and is still attached to the
|
||||
current product before reusing it, falling through to a fresh import/attach otherwise. Makes
|
||||
a re-import robust to orphaned media regardless of what left them behind (e.g. a prior
|
||||
`WipeCatalogCommand` run, before the fix above).
|
||||
- Cart admin view (`Cart\Filament\Resources\CartResource\Pages\ViewCart`) 500'd
|
||||
(`Lunar\Exceptions\MissingCurrencyPriceException`) for any cart still holding a line whose
|
||||
purchasable no longer exists (e.g. after `boboko:wipe-catalog`) — `Cart::calculate()` now has
|
||||
that exception caught, falling back to an uncalculated cart; every total field already
|
||||
rendered `?->formatted() ?? '—'`, so the page degrades to showing "—" instead of a 500.
|
||||
- Creating or editing a Payment Method offered "Capture mode" (Charge immediately / Hold now,
|
||||
charge later) even for `cash-on-delivery`, whose driver has no `authorize()` method at
|
||||
all — selecting "authorize" there would have fatally errored at checkout. Now hidden/
|
||||
non-required unless the resolved driver implements `SupportsAuthorization`.
|
||||
- `Lunar\Base\Traits\Searchable::indexer()` (and its sibling filterable/sortable-attribute
|
||||
methods) resolve their configured indexer via `$config[self::class]` — but `self::class`
|
||||
inside a trait method is a compile-time literal bound to whichever class first `use`s the
|
||||
trait, so it always evaluates to `Lunar\Models\Product`, never a subclass, regardless of
|
||||
which instance calls it. `config/lunar/search.php`'s `'indexers'` map must stay keyed by
|
||||
`Lunar\Models\Product::class`, not the new `Product` subclass — keying it by the subclass
|
||||
made the lookup miss entirely and silently fall back to a near-empty default indexer, wiping
|
||||
every filterable/sortable attribute the index had. Caught live, reverted; documented in the
|
||||
config file itself so it isn't repeated.
|
||||
- `CustomerServiceProvider`/`CatalogServiceProvider` called `ModelManifest::replace()` directly
|
||||
from `boot()` — `LunarServiceProvider` (lunarphp/core) calls `Facades\ModelManifest::
|
||||
register()` from its OWN `boot()`, re-discovering every `Lunar\Models\*` class and silently
|
||||
overwriting any `replace()` registered earlier in the provider boot order. Both now defer to
|
||||
`$this->app->booted()`, which only runs once every provider's `boot()` has completed.
|
||||
|
||||
## [0.19.0] - 2026-09-18
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "boboko/core",
|
||||
"description": "Core module — authentication and shared panel behaviour",
|
||||
"type": "library",
|
||||
"version": "0.19.0",
|
||||
"version": "0.20.0",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Core\\": "src/"
|
||||
|
||||
Reference in New Issue
Block a user