15 KiB
15 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
[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.ProductFiltersDTO: optionalcollectionId,brand,minPrice,maxPrice, translated into a Meilisearchfilterexpression.- Listing results are locale-aware:
withLocalizedFields()resolvesname/descriptionfrom the indexer's per-locale fields, falling back to the store's default language (viaLocaleMiddleware::defaultLocale()) when the current locale has no translation yet, instead of rendering blank. Modules\Core\Search\ProductIndexerexpanded 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 +metafor swatches, per-currency prices, variant media), and reviews (reviews,review_count,average_rating— public-safe fields only,reviewer_emaildeliberately 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 theProductrow 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 fromProductService, which is for browsing/filtering without a query term.docs/product-listing.mdanddocs/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::nameisn'tattribute_data(sotranslateAttribute()silently returnsnullfor it), a runningqueue:workprocess not picking up an edited Scout indexer class, and Scout'spaginateRaw()->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. Aback()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): alocaleroute-middleware alias, opt-in per shop (not pushed onto thewebgroup globally, since admin/Livewire/webhook routes must not be locale-redirected). Reads the first URL segment against Lunar's ownlanguagestable, setsApp::setLocale(), and redirects unprefixed/unknown-locale requests to a resolved locale (Accept-Languagematch → 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 viaModules\Core\Localization\LanguageCacheObserverdispatchingLanguageCreated/LanguageUpdated/LanguageDeletedevents (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 onLanguageUpdated) migrates every affectedLanguageLine.textkey 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 existingLanguageLinetranslations permanently unreachable.
- Language list cached with
- Storefront UI label translations: pulled in
spatie/laravel-translation-loader(self-registers via Composer package auto-discovery; its loader extends Laravel's file-basedFileLoaderand merges DB translations on top — existing Filament/Lunar vendorlang/strings are unaffected). Labels are looked up via Laravel's native__('storefront.nav.cart'), kept in its ownstorefrontgroup so nothing collides with Lunar/Filament's own translation groups.Modules\Core\Command\InstallLunarCommand(overridinglunar: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 byLanguageLine's own forever-cache) — for sharing to a view as$labelsor@json()-ing to JS, on top of__()for single-key Blade lookups.- Admin UI:
Modules\Core\Localization\Filament\Resources\LanguageLineResource(registered inCorePlugin) lists/searches/filterslanguage_linesand edits each row'sgroup,key, and one text input per locale currently inlunar_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 forLanguageLine— the Filament resource's Create/Edit/Delete pages route through it rather than Filament's default direct-model writes. DispatchesTranslationCreated/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 inLanguageLine'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 fromtext, or agroup/keyrename, can't leave a stale cached array behind.LogTranslationActivity— audits every write via the existingModules\Core\Logging\ActivityLogService(lunaractivity log channel), samecreated/updated/deletedshape as every other domain write in this project. Properties are flattened withArr::dot()before logging (text.en,text.elinstead of a nestedtextobject) since Filament's Activity resource renderspropertieswith a flatKeyValuefield that can't display nested arrays.
Modules\Core\Providers\LocalizationServiceProvider— split out of the growingCoreServiceProvider(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/typesenseengines, selectable via Scout's ownSCOUT_DRIVERconfig) andlunarphp/meilisearch, wiring Meilisearch in as the search engine for products.Search\ProductIndexeroverrides 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-coreconcern: the actual Meilisearch container, host port, and master key live in each consuming app's owndocker-compose.yml/.env(e.g.3dealer), the same way Postgres and Valkey do —boboko-coreonly declares the PHP package dependency and the indexing code.
[0.2.0] - 2026-07-10
Added
- Product reviews (
Modules\Core\Review): a newProductReviewmodel +product_reviewstable (plain, unprefixed — same convention asimport_mappings), linked to Lunar'sProductvia aProduct::reviews()macro (registered inCorePlugin, sinceLunar\Models\Productis a vendor model and can't be edited directly).- JudgeMe CSV review importer (
MigrateImport\JudgeMe\JudgeMeExportImporter), wired into the existingboboko:migrate:import --source=judgeme --type=exportcommand: reads a Judge.me review export, resolves each row'sproduct_handleto a Lunar product viaLunar\Models\Url, and creates/updatesProductReviewrows idempotently viaimport_mappings(source=judgeme,source_type=review, keyed on Judge.me'smetaobject_handle). Rows with no matching product are skipped with a logged warning rather than failing the whole import. - Review images (
picture_urlsin 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 viaReview\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, settingreplied_at. The View modal shows full review detail (body, reviewer email, location, source, dates, reply, downloaded images).
- JudgeMe CSV review importer (
Fixed
Shopify\ShopifyExportImporternever wrote a LunarUrl(slug) row for imported products, despitedocs/shopify-import.mdspecifying it should — meaning no code outside the importer itself could resolve "which Lunar product has handle X" (only the importer's own privateimport_mappingsbookkeeping could). It now creates/updates a defaultUrlrow (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-shippingplugin (ShippingPlugin) directly onCorePlugin, 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:importArtisan 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 tostorage/app/private/imports/, so answering e.g.shopifypicks up the first CSV found inimports/shopify/automatically.ImportSpec,Importerinterface, andImporterFactory(source+type → importer class) as the extension points for future sources (WooCommerce, etc.) and mechanisms (API vs. file export).import_mappingstable +ImportMappingmodel: a polymorphic (source, source_type, external_id) → model mapping used by every resolver to make imports idempotent and safely re-runnable.DefaultLocalehelper wrapping Lunar'sLanguage::getDefault()->code, used anywhere a translatable field needs a locale key, instead of assumingapp()->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+ProductGroupgroup 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 viaProduct::addMedia(), matches local export images by UUID first, filename fallback),PriceResolver(minor-unit conversion per currency),ImportAttributeResolverandProductAttributeResolver(customcost_per_item/seo_title/seo_descriptionattributes, field-type-awareattribute_datawriting).
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, requiredProductOption.handle, per-groupAttribute.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
ProductOptionResolvercreated duplicateProductOption/ProductOptionValuerows 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:importcould 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
Userinstead ofCustomer(UserOtpService,UserOtpMail), replacing the earlier customer-scoped OTP flow. UserCreatedevent with aCreateCustomerForUserlistener to provision a Lunar customer automatically when a user is created.UserRelationManagerfor 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 (seeStoic::class). config/core.phpfor module-level configuration.AuthServiceProviderandCustomerServiceProvidernow register alongsideCoreServiceProvider.- Migrations: add OTP to
users, drop OTP from Lunarcustomers, droppasswordfromusers, makenamenullable onusersand Lunarcustomers. docs/modules.mddocumenting module structure.
Removed
CustomerOtpMailandCustomerOtpService, superseded by the user-based OTP flow.
Dependencies
- Added explicit
symfony/yamlrequirement (used directly byStoic::loadConfig()).