From c7035d678275a6c7aae6eb2a2ee1b12569da1a3d Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Wed, 23 Sep 2026 09:47:28 +0300 Subject: [PATCH] Bump version to 0.20.0 --- CHANGELOG.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++ composer.json | 2 +- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 150c6e9..b3bb9d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/composer.json b/composer.json index b25c4ff..d0dd581 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.19.0", + "version": "0.20.0", "autoload": { "psr-4": { "Modules\\Core\\": "src/"