Files
core/CHANGELOG.md
T
2026-08-26 18:34:26 +03:00

17 KiB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog.

[0.5.4] - 2026-08-26

Added

  • Modules\Core\Catalog\ProductService::list() accepts a sort parameter (new ProductSort enum: PriceAsc, PriceDesc, Newest), translated into a Meilisearch sort clause — list() previously had no way to order results, since it always searches with an empty query string and so has no relevance score to fall back on. Modules\Core\Search\ProductIndexer::getSortableFields() now also marks price sortable (Lunar's base indexer only marks created_at/updated_at/skus/status). Requires re-syncing index settings (php artisan lunar:meilisearch:setup) on existing stores. Documented in docs/product-listing.md ("Sorting").

[0.5.3] - 2026-08-26

Fixed

  • Modules\Core\Search\ProductIndexer::toSearchableArray() threw column reference "id" is ambiguous on Postgres when computing channel_ids — $model->channels()->wherePivot('enabled', true)->pluck('id') joins lunar_channels and lunar_channelables, both of which have an id column, and the unqualified pluck('id') left Postgres unable to resolve which table's column to select (SQLite/MySQL tolerated the ambiguity). Qualified as pluck('lunar_channels.id').

[0.5.2] - 2026-08-26

Fixed

  • Modules\Core\Localization\LocaleMiddleware's shared view data only ever surfaced a single alternate locale (altLocale/altLocaleUrl, found via firstWhere('code', '!=', $current)) — correct by coincidence for a 2-language store, but silently dropped every locale past the first "other" one found for a 3+ language store, with no error. Replaced with altLocales, a collection of every other configured language (code, name, url for the current route each), so a language switcher or hreflang tags scale to any number of locales. Documented in docs/localization.md ("Shared view data — language switcher and hreflang tags").

[0.5.1] - 2026-08-25

Added

  • Modules\Core\Search\ProductIndexer now indexes channel_ids (filterable) — Lunar's base indexer only marks status as filterable, not channel assignment, so storefront search couldn't otherwise scope results to products actually assigned and enabled on the current sales channel. Computed from $product->channels()->wherePivot('enabled', true). Ported from an older Products branch whose remote had been deleted; the branch's other, now-superseded ProductIndexer changes were dropped in favor of the richer indexer already on master (collections, price, variants, reviews — see 0.5.0).

[0.5.0] - 2026-08-24

Added

  • Modules\Core\Catalog\ProductService: storefront product listing/filtering (list()) and single-product lookup (getById(), getBySlug()), reading directly from the Meilisearch index rather than the database — one data source, no ->get() model hydration. Returns plain arrays (not Eloquent models), meant to be called directly from a consuming app's controllers.
    • ProductFilters DTO: optional collectionId, brand, minPrice, maxPrice, translated into a Meilisearch filter expression.
    • Listing results are locale-aware: withLocalizedFields() resolves name/description from the indexer's per-locale fields, falling back to the store's default language (via LocaleMiddleware::defaultLocale()) when the current locale has no translation yet, instead of rendering blank.
    • Modules\Core\Search\ProductIndexer expanded well beyond its original collection/price additions to carry everything a detail page needs: id/slugs (filterable — getById()/getBySlug() resolve purely from the index, no database read), collection_names, tags, the full media gallery, per-variant data (sku, stock, purchasable, translated option/value names + meta for swatches, per-currency prices, variant media), and reviews (reviews, review_count, average_rating — public-safe fields only, reviewer_email deliberately excluded).
    • Modules\Core\Providers\ReviewServiceProvider (newly registered): re-indexes a product whenever one of its reviews is created/updated/deleted, since a review write doesn't touch the Product row and so never fires the product's own model events.
  • Modules\Core\Search\ProductSearchService: locale-aware full-text product search on top of the same Meilisearch index, for use by a storefront's search bar — separate from ProductService, which is for browsing/filtering without a query term.
  • docs/product-listing.md and docs/product-search.md — usage, full field reference, and design notes for the two services above.
  • docs/lunar.md "Gotchas": three new entries hit while building this — ProductOption/ProductOptionValue::name isn't attribute_data (so translateAttribute() silently returns null for it), a running queue:work process not picking up an edited Scout indexer class, and Scout's paginateRaw()->items() on the Meilisearch driver returning the whole raw response rather than a hit list.

Fixed

  • The admin login form (Modules\Core\Auth\Filament\Pages\Login) had no way back from the OTP-entry step to the email step short of reloading the page. A back() method resets to the email step; a "← Back" link/button is shown on the OTP step only.

[0.4.0] - 2026-08-06

Added

  • Locale-prefixed routing (Modules\Core\Localization\LocaleMiddleware): a locale route-middleware alias, opt-in per shop (not pushed onto the web group globally, since admin/Livewire/webhook routes must not be locale-redirected). Reads the first URL segment against Lunar's own languages table, sets App::setLocale(), and redirects unprefixed/unknown-locale requests to a resolved locale (Accept-Language match → default language → first language). Every locale is prefixed, including the default (/el/..., /en/...), never a bare root — avoids the hreflang/duplicate-content ambiguity of a bare-root default locale.
    • Language list cached with Cache::rememberForever(), invalidated via Modules\Core\Localization\LanguageCacheObserver dispatching LanguageCreated/LanguageUpdated/LanguageDeleted events (see below) rather than doing the work itself.
    • Language rename safety: renaming a Language::code (e.g. el → gr) no longer strands existing translations. MigrateTranslationsForRenamedLanguage (listening on LanguageUpdated) migrates every affected LanguageLine.text key from the old code to the new one and flushes both codes' translation caches — closing a real data-loss gap where a rename would otherwise make existing LanguageLine translations permanently unreachable.
  • Storefront UI label translations: pulled in spatie/laravel-translation-loader (self-registers via Composer package auto-discovery; its loader extends Laravel's file-based FileLoader and merges DB translations on top — existing Filament/Lunar vendor lang/ strings are unaffected). Labels are looked up via Laravel's native __('storefront.nav.cart'), kept in its own storefront group so nothing collides with Lunar/Filament's own translation groups.
    • Modules\Core\Command\InstallLunarCommand (overriding lunar:install) seeds a starter set of ~15 common e-shop labels (nav.*, cart.*, product.*, auth.*, search.*, English + Greek), idempotently guarded so it's safe on every boot.
    • Modules\Core\Localization\TranslationReader::group('storefront') returns the whole reduced/cached label array for a locale (backed by LanguageLine's own forever-cache) — for sharing to a view as $labels or @json()-ing to JS, on top of __() for single-key Blade lookups.
    • Admin UI: Modules\Core\Localization\Filament\Resources\LanguageLineResource (registered in CorePlugin) lists/searches/filters language_lines and edits each row's group, key, and one text input per locale currently in lunar_languages — locale columns/inputs are generated dynamically from the language list, so a new language needs no resource changes.
    • Event-driven writes: Modules\Core\Localization\TranslationService (create/update/delete) is the single write path for LanguageLine — the Filament resource's Create/Edit/Delete pages route through it rather than Filament's default direct-model writes. Dispatches TranslationCreated/TranslationUpdated (carries the full pre-update {group, key, text} snapshot, so a bare rename is tracked the same as a text edit)/TranslationDeleted, each handled by two listeners:
      • FlushTranslationCache — closes a real gap in LanguageLine's own self-invalidation, which only flushes locales/groups present after a save. Flushes the union of old and new group+locale combinations, so a locale removed from text, or a group/key rename, can't leave a stale cached array behind.
      • LogTranslationActivity — audits every write via the existing Modules\Core\Logging\ActivityLogService (lunar activity log channel), same created/updated/deleted shape as every other domain write in this project. Properties are flattened with Arr::dot() before logging (text.en, text.el instead of a nested text object) since Filament's Activity resource renders properties with a flat KeyValue field that can't display nested arrays.
  • Modules\Core\Providers\LocalizationServiceProvider — split out of the growing CoreServiceProvider (per this project's own "split when a provider does too much" convention) to own all locale/translation middleware, observer, and event-listener registration.

[0.3.0] - 2026-07-12

Added

  • Meilisearch product search: pulled in lunarphp/search (Lunar's driver-agnostic search abstraction — database/meilisearch/typesense engines, selectable via Scout's own SCOUT_DRIVER config) and lunarphp/meilisearch, wiring Meilisearch in as the search engine for products.
    • Search\ProductIndexer overrides Lunar's own indexer to strip HTML tags from string fields (e.g. name_en, description_en) before they reach the search index — Lunar's default indexer sends raw attribute HTML straight through, which pollutes relevance ranking and highlighting with markup.
    • Meilisearch itself is treated as app-level infrastructure, not a boboko-core concern: the actual Meilisearch container, host port, and master key live in each consuming app's own docker-compose.yml/.env (e.g. 3dealer), the same way Postgres and Valkey do — boboko-core only declares the PHP package dependency and the indexing code.

[0.2.0] - 2026-07-10

Added

  • Product reviews (Modules\Core\Review): a new ProductReview model + product_reviews table (plain, unprefixed — same convention as import_mappings), linked to Lunar's Product via a Product::reviews() macro (registered in CorePlugin, since Lunar\Models\Product is a vendor model and can't be edited directly).
    • JudgeMe CSV review importer (MigrateImport\JudgeMe\JudgeMeExportImporter), wired into the existing boboko:migrate:import --source=judgeme --type=export command: reads a Judge.me review export, resolves each row's product_handle to a Lunar product via Lunar\Models\Url, and creates/updates ProductReview rows idempotently via import_mappings (source=judgeme, source_type=review, keyed on Judge.me's metaobject_handle). Rows with no matching product are skipped with a logged warning rather than failing the whole import.
    • Review images (picture_urls in the CSV) are downloaded and stored as real media via Spatie MediaLibrary (ProductReview::IMAGES_COLLECTION), not just linked by URL — consistent with how product images are handled.
    • Admin UI: a new "Reviews" sub-navigation page on the product edit screen (Review\Pages\ManageProductReviews, wired via Review\Extensions\ProductResourceExtension), listing rating/title/reviewer with View, Reply, and Delete actions. The Reply action lets staff write/edit a reply directly from the table, setting replied_at. The View modal shows full review detail (body, reviewer email, location, source, dates, reply, downloaded images).

Fixed

  • Shopify\ShopifyExportImporter never wrote a Lunar Url (slug) row for imported products, despite docs/shopify-import.md specifying it should — meaning no code outside the importer itself could resolve "which Lunar product has handle X" (only the importer's own private import_mappings bookkeeping could). It now creates/updates a default Url row (slug = Shopify handle) per product on every import, which the new JudgeMe review importer depends on for product resolution.

[0.1.0] - 2026-07-09

Added

  • Shipping: registered Lunar's lunarphp/table-rate-shipping plugin (ShippingPlugin) directly on CorePlugin, so table-rate shipping is available to every consumer app without per-app wiring.
  • Product migration/import framework (Modules\Core\MigrateImport): a source-agnostic pipeline for importing a vendor's product catalog into Lunar.
    • boboko:migrate:import Artisan command — interactively prompts for source, type (export/API), and credentials or file path, then dispatches the import as a queued job (RunMigrateImportJob) on the default queue. The file-path prompt resolves relative to storage/app/private/imports/, so answering e.g. shopify picks up the first CSV found in imports/shopify/ automatically.
    • ImportSpec, Importer interface, and ImporterFactory (source+type → importer class) as the extension points for future sources (WooCommerce, etc.) and mechanisms (API vs. file export).
    • import_mappings table + ImportMapping model: a polymorphic (source, source_type, external_id) → model mapping used by every resolver to make imports idempotent and safely re-runnable.
    • DefaultLocale helper wrapping Lunar's Language::getDefault()->code, used anywhere a translatable field needs a locale key, instead of assuming app()->getLocale() matches Lunar's configured default.
    • Shopify CSV export importer (Shopify\ShopifyExportImporter), the first working source/type combination, verified end-to-end against a real 183-product/693-variant/332-image Shopify export (row counts in the CSV match 1:1 with imported Products/Variants/Media):
      • ShopifyCsvReader + ProductGroup group Shopify's flat, repeated-handle CSV rows into one row-group per product (product row, variant rows, image rows).
      • Ten resolvers under Shopify\Resolvers, each responsible for idempotently resolving-or-creating one Lunar entity: TaxClassResolver, ProductTypeResolver (auto-attaches system attributes to new types), BrandResolver, TagResolver, CollectionResolver (multi-level, multi-collection support via >-delimited breadcrumbs), ProductOptionResolver (dedupes options/values by slugified name so case variants like "Size"/"size" resolve to one row), AssetResolver (Spatie MediaLibrary via Product::addMedia(), matches local export images by UUID first, filename fallback), PriceResolver (minor-unit conversion per currency), ImportAttributeResolver and ProductAttributeResolver (custom cost_per_item/seo_title/seo_description attributes, field-type-aware attribute_data writing).
    • docs/shopify-import.md — full CSV-to-Lunar field mapping reference and import design notes.
    • docs/lunar.md — new "Gotchas" section documenting non-obvious Lunar behavior hit while building the importer (table-prefix/nested-set race, required ProductOption.handle, per-group Attribute.position, etc.).
    • CONTRIBUTE.md — local dev setup (path-repo + bin/dc-core.sh), and the manual DB-verification workflow used to build this feature.

Fixed

  • ProductOptionResolver created duplicate ProductOption/ProductOptionValue rows when the same option or value appeared with different casing across products (e.g. Shopify export rows using both "Size" and "size"), and could create a duplicate value within a single product's own variant rows due to relying on a stale lazy-loaded relation. Both now resolve by normalized (slugified) identity queried fresh from the database.
  • boboko:migrate:import could dispatch an import job with a blank file path (silent no-op failure) if the file-path prompt was answered empty; it now re-prompts until a valid, existing file is given.

[0.0.1] - 2026-07-03

First release.

Added

  • OTP-based authentication built around User instead of Customer (UserOtpService, UserOtpMail), replacing the earlier customer-scoped OTP flow.
  • UserCreated event with a CreateCustomerForUser listener to provision a Lunar customer automatically when a user is created.
  • UserRelationManager for managing users from the customer resource in the panel.
  • Stoic image UI component (resources/views/ui/stoic-image.blade.php) and its YAML-driven config/service (see Stoic::class).
  • config/core.php for module-level configuration.
  • AuthServiceProvider and CustomerServiceProvider now register alongside CoreServiceProvider.
  • Migrations: add OTP to users, drop OTP from Lunar customers, drop password from users, make name nullable on users and Lunar customers.
  • docs/modules.md documenting module structure.

Removed

  • CustomerOtpMail and CustomerOtpService, superseded by the user-based OTP flow.

Dependencies

  • Added explicit symfony/yaml requirement (used directly by Stoic::loadConfig()).