1	# Implementation Progress
2
3	Tracking implementation of the shop system per `specs/09-IMPLEMENTATION-ROADMAP.md`.
4
5	| Phase | Scope | Status | Tests |
6	|-------|-------|--------|-------|
7	| 1 | Foundation: config, tenancy migrations/models, enums, ResolveStore, BelongsToStore, admin+customer auth, policies, Pest helpers | DONE (commit 5b743cde) | 30 passed, 3 todos |
8	| 2 | Catalog: products, options, variants, inventory, collections, media | DONE | full suite 79 passed, 5 todos (4 deferred to P5, 1 to P4) |
9	| 3 | Themes, pages, navigation, storefront layout + components | DONE (commit 5aee8dc4) | full suite 98 passed, 5 todos; Storefront/* rendering tests added |
10	| 4 | Cart, checkout, discounts, shipping, taxes + storefront cart/checkout UI | DONE | full suite 202 passed, 8 todos; Unit/* (48 new), Cart/CartServiceTest (12), Checkout/* (34), Storefront/CartUiTest (13) |
11	| 5 | Payments (mock PSP), orders, refunds, fulfillments, events | DONE | full suite 259 passed, 0 todos; Orders/* (28), Payments/* (17), Storefront/CheckoutUiTest (4), all 8 deferred todos resolved |
12	| 6 | Customer accounts | DONE | full suite 273 passed, 0 todos; Customers/CustomerAccountTest (6), Customers/AddressManagementTest (7), checkout prefill case added to Storefront/CheckoutUiTest |
13	| 7a | Admin panel core: layout shell (sidebar/top bar/breadcrumbs/toasts), dashboard, products (index + shared form with variants builder + media), orders (index + detail with fulfillment/refund/cancel/confirm-payment), customers (index + detail with address CRUD) | DONE | full suite 290 passed, 0 todos; Admin/DashboardTest (4), Admin/ProductManagementTest (8), Admin/OrderManagementTest (5) |
14	| 7b | Admin panel remaining sections: collections, inventory, discounts, settings (general/domains/checkout/notifications/shipping/taxes), themes (cards + editor), pages, navigation (apps/developers/analytics stay "Coming soon" until Phases 9/10) | DONE | full suite 338 passed, 0 todos; Admin/DiscountManagementTest (6), Admin/SettingsTest (6), Admin/CollectionManagementTest (7), Admin/InventoryManagementTest (7), Admin/ThemeManagementTest (8), Admin/PageManagementTest (7), Admin/NavigationManagementTest (8) |
15	| API | Spec 02 REST APIs + Sanctum: storefront cart/checkout API, admin products/orders API with token abilities, rate limiters, Developers admin page | DONE | full suite 386 passed, 0 todos; Auth/SanctumTokenTest (5), Cart/CartApiTest (8), Api/StorefrontCartApiTest (8), Api/StorefrontCheckoutApiTest (9), Api/AdminProductApiTest (7), Api/AdminOrderApiTest (6), Admin/DevelopersTest (5) |
16	| 8 | Search (FTS5) | DONE | full suite 422 passed, 0 todos; Search/SearchTest (5), Search/AutocompleteTest (3), Storefront/SearchUiTest (8), Admin/SearchSettingsTest (5) |
17	| 9 | Analytics | DONE | (same run as Phase 8) Analytics/EventIngestionTest (5), Analytics/AggregationTest (3), Admin/AnalyticsTest (7) |
18	| 10 | Apps & webhooks: 6 migrations (apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries), WebhookService + DeliverWebhook job (HMAC, backoff, circuit breaker), DispatchWebhooks listener + ProductObserver wiring, Admin Apps directory/detail, Developers webhook CRUD, AppSeeder | DONE | full suite 444 passed, 0 todos; Webhooks/WebhookDeliveryTest (5), Webhooks/WebhookSignatureTest (4), Admin/AppsTest (7), Admin/DevelopersTest (+6 webhook cases, 11 total) |
19	| 11 | Polish: seeder audit vs spec 07, a11y (skip links, focus management, aria-live, labels), responsive + dark mode pass, error pages (404/419/500/503), structured business-event logging, SmokeTest | DONE | full suite 450 passed, 0 todos; Orders/StructuredLoggingTest (3), SmokeTest (3 tests, ~50 routes) |
20	| 12 | Full suite + browser tests (spec 08) + Playwright MCP acceptance verification | DONE: all 18 suites, 143 browser tests complete | full suite 593 passed (2054 assertions), 0 todos; Browser: SmokeTest (10), Admin/AuthenticationTest (10), Admin/ProductManagementTest (7), Admin/OrderManagementTest (11), Admin/DiscountManagementTest (6), Admin/SettingsTest (7), Admin/CollectionManagementTest (3), Admin/CustomerManagementTest (3), Admin/PageManagementTest (3), Admin/AnalyticsTest (3), Storefront/BrowsingTest (15), CartTest (12), CheckoutTest (13), CustomerAccountTest (12), InventoryTest (4), TenantIsolationTest (5), ResponsiveTest (8), AccessibilityTest (11) |
21
22	## Log
23
24	- 2026-06-09: Project start. Fresh Livewire starter kit (Fortify). Created progress tracker, started Phase 1.
25	- 2026-06-09: Phase 2 done. Catalog migrations (9 tables), models, enums, ProductService/VariantMatrixService/InventoryService/MediaService, HandleGenerator, ProcessMediaUpload job (GD), Collection+Product seeders (25 products, 127 variants). Order-reference checks implemented behind Schema::hasTable('order_lines') guard; 3 order-dependent test cases todo'd for Phase 5.
26	- 2026-06-09: Phase 4 done. Migrations for carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts. Models + factories + 7 enums. Services: CartService (session key `cart_id`, version bump on every mutation, sum-quantities merge on login), DiscountService (reason-coded InvalidDiscountException, largest-remainder allocation), ShippingCalculator (specificity-ordered zone matching, flat/weight/price rates), TaxCalculator (basis points, intdiv for exclusive add and inclusive extract), PricingEngine (deterministic pipeline, snapshot to totals_json; inclusive total = discounted subtotal + shipping), CheckoutService state machine (payment_selected reserves inventory + 24h expires_at; completeCheckout guarded, throws LogicException until Phase 5). Jobs ExpireAbandonedCheckouts (15 min) + CleanupAbandonedCarts (daily) scheduled in routes/console.php. Storefront UI: CartDrawer (global, listens cart-updated/open-cart), /cart page with shipping estimate, /checkout 4-step stepper through payment method selection (pay button is the Phase 5 mount point), live header badge, real addToCart. Seeders: TaxSettingsSeeder, ShippingSeeder, DiscountSeeder (WELCOME10/FLAT5/FREESHIP/EXPIRED20/MAXED). Cart merge wired into CustomerLoginController; CustomerAuthTest merge case implemented. 4 test cases todo'd for Phase 5 (order completion paths).
27	- 2026-06-09: Phase 5 done. Migrations for orders (+ nullable unique checkout_id for idempotency), order_lines, payments, refunds, fulfillments (+ delivered_at), fulfillment_lines. Models + factories (paid/pending/fulfilled/cancelled states) + 7 enums (OrderStatus, FinancialStatus, FulfillmentStatus, PaymentMethod, PaymentStatus, RefundStatus, FulfillmentShipmentStatus). PaymentProvider contract bound to MockPaymentProvider (magic cards 4242/0002/9995, paypal instant capture, bank_transfer pending, mock_ reference IDs); PaymentResult/RefundResult value objects. OrderService (createFromCheckout atomic with snapshots + payment record + inventory commit for captured / keep-reserved for bank transfer + discount usage increment + cart converted; generateOrderNumber sequential per store from #1001 inside the creation transaction with unique (store_id, order_number) backstop; cancel releases or restocks per financial status; confirmBankTransferPayment commits inventory + digital auto-fulfillment). RefundService (refundable validation, partial/full financial status, restock flag). FulfillmentService (FulfillmentGuardException unless paid/partially_refunded, qty <= unfulfilled validation, markAsShipped/markAsDelivered, autoFulfillDigital delivered). CheckoutService::completeCheckout implemented: idempotent via orders.checkout_id, PaymentFailedException on decline (reservation kept, released by 24h expiry). Events: OrderCreated/OrderPaid/OrderFulfilled/OrderCancelled/OrderRefunded/CheckoutCompleted/FulfillmentDelivered. CancelUnpaidBankTransferOrders daily job (store settings bank_transfer_cancel_days, default 7). Storefront pay step (card form / paypal / bank transfer) + /checkout/{checkoutId}/confirmation page with bank transfer instructions. CustomerSeeder (12 customers + addresses) and OrderSeeder (15 fashion + 3 electronics orders) wired into DatabaseSeeder, idempotent. All 8 deferred todos resolved; suite has ZERO todos.
28	- 2026-06-09: Phase 6 done. Livewire account area behind `auth:customer` inside the storefront group: `Storefront\Account\Dashboard` (GET /account: welcome heading, quick-link cards, last 5 orders, profile form with name + marketing_opt_in via `updateProfile`), `Account\Orders\Index` (GET /account/orders: paginated table desktop / cards mobile, status + fulfillment badges), `Account\Orders\Show` (GET /account/orders/{orderNumber}: bare number in URL matched against the stored `#`-prefixed order_number, scoped to the auth'd customer so foreign orders 404; timeline built from placed_at + captured payment + fulfillments shipped/delivered + cancelled, line items with snapshots, shipping/billing/payment grid, totals, tracking links target=_blank rel=noopener), `Account\Addresses\Index` (GET /account/addresses: modal CRUD reusing x-storefront.address-form with prefix="form", postal_code<->zip mapping to the spec 01 JSON shape, first address auto-default, setDefault flips others off, deleting the default promotes the newest remaining, foreign address ids abort 404). New components: x-storefront.account-nav (tabs + logout form), x-storefront.order-status-badge (spec 04 colors). `App\Support\Storefront\Countries` extracted (address-form now uses it). CustomerAddress gained toCheckoutAddress()/summaryLine(). Checkout: address step prefills from the default address when checkout has no address, saved-address dropdown ("Use a new address" resets) per spec 04; email prefill existed. Logout now session()->invalidate() + regenerateToken (spec 06). Confirmation page "View order" links to the order detail. Header account icon already linked correctly (Phase 3). Verified in browser via Playwright on acme-fashion.test (login, dashboard, orders, order #1002 timeline, address create modal, checkout prefill + dropdown switch).- 2026-06-09: Phase 7a done (admin panel core). Layout: `resources/views/layouts/admin.blade.php` (component layout, used via `#[Layout('layouts::admin')]`) with `Admin\Layout\Sidebar` (fixed 256px desktop, Alpine-driven mobile overlay via `toggle-admin-sidebar` window event, nav groups per spec 03 section 1.2; unimplemented sections render as disabled "Coming soon" items and auto-activate once `Route::has()` finds them) and `Admin\Layout\TopBar` (store selector dropdown -> `switchStore()` validates membership, writes session `current_store_id`, redirects to dashboard; profile menu with logout; mobile hamburger). Global toast container (Alpine, top-right, 5s auto-dismiss, success/error/info left borders) listens for the Livewire `toast` event via `x-on:toast.window` and replays `session('toast')` flashes after redirects; components use the `Admin\Concerns\SendsToasts` trait. Breadcrumbs/cards/status badges are anonymous Blade components `x-admin.breadcrumbs` / `x-admin.card` / `x-admin.status-badge`. Dashboard (GET /admin): KPI tiles (total sales/orders/AOV + conversion rate approximated as orders/carts until Phase 9 analytics; change badges vs previous period), dependency-free inline SVG line chart of daily order counts, recent orders table (last 10), 7/30/90-day presets. Products: `Admin\Products\Index` (search, status tabs, type filter, sortable columns incl. inventory subquery sum, bulk set-active/archive/delete with confirm modal, empty states, 15/page) and shared `Admin\Products\Form` for create+edit (route param `{productId}` because a `{product}` param collides with the `$product` model property during Livewire param auto-assignment): options builder (comma-separated values, live matrix regeneration preserving row edits by combination key), per-variant sku/barcode/price/compare-at/weight/qty/ship rows applied post-`VariantMatrixService::rebuildMatrix` via case-insensitive sorted value-combination keys, diff-based option/value sync so untouched combinations keep variants+inventory, media upload (WithFileUploads, immediate attach in edit mode, pending until save in create mode, alt text inline edit, wire:sort reorder, delete), collections checkboxes, SEO handle (blank -> HandleGenerator), publishing datetime, status transitions through `ProductService::transitionStatus`. Orders: `Admin\Orders\Index` (status tabs all/pending/paid/fulfilled/cancelled/refunded, search order#/email, date range, sortable) and `Admin\Orders\Show` (computed `order` keyed by locked `orderId`, timeline incl. refunds/cancellation, line items with per-line fulfillment badges, totals incl. refunded, payment panel with Confirm Payment for pending bank transfers -> `OrderService::confirmBankTransferPayment`, fulfillment guard callout, fulfillment modal -> `FulfillmentService::create`, mark shipped/delivered, refund modal (selected lines sum or custom amount, reason, restock) -> `RefundService::create`, cancel modal -> `OrderService::cancel`; service ValidationException/FulfillmentGuardException surfaced as error toasts). Customers: `Admin\Customers\Index` (search, orders count + total spent) and `Admin\Customers\Show` (info card, paginated order history, address CRUD modal with default handling per spec 03 section 9.2). Routes registered in the existing `['auth','admin']` group; placeholder dashboard route/view removed. All actions policy-guarded (`@can` in views + `authorize()` in actions; Support read-only, Staff cannot delete/archive). Pest helper `createStoreMember(Store, StoreUserRole)` added. Tests: Admin/DashboardTest (4), Admin/ProductManagementTest (8), Admin/OrderManagementTest (5), exactly per roadmap tables; full suite 290 passed, 0 todos; pint clean; npm run build ok; migrate:fresh --seed ok. Browser-verified via Playwright on shop.test/admin: login, dashboard KPIs+chart, products list, Leather Belt edit form (options/variants prefilled), order #1005 bank-transfer confirm -> paid, fulfillment modal -> DHL fulfillment -> shipped -> delivered with timeline updates, toast rendering, customer detail with addresses, mobile sidebar overlay at 375px, store switcher (verified with a temporary second membership, then reverted; revoked membership correctly 403s).
29	- 2026-06-10: Phase 7b done (remaining admin sections). Collections: `Admin\Collections\Index` (search, status filter, products_count, delete modal) + shared `Admin\Collections\Form` ({collectionId} edit route; title/handle/description/status, handle unique per store + auto-slug from title, product picker with debounced search dropdown + Add buttons, assigned list with thumbnails, `wire:sort` drag reorder buffered in `assignedProductIds` and synced to `collection_products.position` on save). Discounts: `Admin\Discounts\Index` (code search, schedule-aware status filter incl. scheduled/expired derived states, type filter, value column %/fixed-currency/free-shipping, usage "n / limit-or-unlimited" column) + shared `Admin\Discounts\Form` (code/automatic radios, Generate code, percent/fixed/free_shipping value types with unit conversion percent-whole-number vs fixed-minor-units, per-store code uniqueness `Rule::unique`, min purchase + specific products/collections + one-per-customer persisted into `rules_json` per DiscountService keys, usage limit, start/end datetimes with after-validation, Active/Disabled switch -> status). Inventory: `Admin\Inventory\Index` (search by product title or SKU, stock filter in/low(<=5)/out via on_hand - reserved expressions, inline on-hand number inputs `wire:change` -> `updateQuantity` clamped >= 0 and guarded by ProductPolicy::update so Support is read-only, policy badges, available column color-coded). Settings: `Admin\Settings\Index` page with `?tab=` (general/domains/checkout/notifications rendered as child Livewire components `Settings\General/Domains/Checkout/Notifications`; shared `x-admin.settings-tabs` bar links Shipping/Taxes to their own routes per spec 02). General: store name/contact email/order number prefix + currency/locale/timezone (writes both `stores` columns and `store_settings.settings_json`). Domains: table with type/primary/TLS badges, add-domain modal (hostname regex + global uniqueness), set-primary scoped per domain type, refuses removing the last domain. Checkout: guest checkout toggle + `bank_transfer_cancel_days` (consumed by the Phase 5 job). Notifications: notification email + transactional toggles in settings_json. `Settings\Shipping`: zone cards (countries checklist of 22 ISO codes + optional comma-separated region codes), nested rate tables (flat amount or multi-range weight/price configs matching ShippingCalculator's `ranges`/`min_g`/`min_amount` shape; carrier intentionally omitted as unimplemented in the calculator), inline active toggles, zone/rate modals, test-address tool calling `ShippingCalculator::getAvailableRates`. `Settings\Taxes`: manual/provider radios, manual rate % -> `default_rate_bps`, tax name, prices_include_tax + shipping_taxable switches, provider select + API key into config_json. All settings pages authorize via StorePolicy viewSettings/updateSettings (owner/admin only; staff 403s). Themes: `Admin\Themes\Index` (card grid, published card gets blue ring, Customize button, ellipsis menu Publish/Duplicate/Delete with wire:confirm; publish demotes other published themes inside a transaction and calls `ThemeSettingsService::forget`; duplicate copies settings as a draft "(Copy)"; published theme cannot be deleted) and `Admin\Themes\Editor` (three-panel: left fixed groups header/colors/catalog/footer plus orderable+toggleable home sections hero/featured-collections/featured-products/newsletter/rich-text driving the persisted `sections` array via `wire:sort` + eye toggles; center iframe preview of the primary storefront domain; right dynamic fields text/textarea/color/select/number/checkbox bound to `settings.*` from `ThemeSettingsService::defaults()`, featured_collection_handles edited as comma list; Save persists theme_settings (model event invalidates cache), Save & publish also publishes). Pages: `Admin\Pages\Index` (search, handle column) + shared `Admin\Pages\Form` (title/handle unique per store/body textarea 16 rows/status draft-published-archived/published_at backfilled on publish, delete modal owner-admin only). Navigation: `NavigationMenuPolicy` added (view owner/admin/staff, update owner/admin per spec 06 matrix), `Admin\Navigation\Index` (menu cards -> selectMenu buffers items in state; item modal with label, type select link/page/collection/product and conditional URL input or resource select; `wire:sort` reorder; Save menu diff-deletes/upserts with positions inside a transaction then `NavigationService::forget` so storefront trees refresh; nesting omitted because navigation_items has no parent_id in spec 01). Routes registered for all sections ({collectionId}/{discountId}/{pageId}/{themeId} params following the Phase 7a productId convention); sidebar items for Collections/Inventory/Discounts/Pages/Navigation/Themes/Settings auto-enabled via the existing Route::has() mechanism (Analytics/Apps/Developers still disabled). Tests exactly per roadmap tables (DiscountManagementTest 6 cases, SettingsTest 6 cases incl. staff 403 + domains) plus CollectionManagementTest (7), InventoryManagementTest (7), ThemeManagementTest (8, incl. cache invalidation end-to-end via ThemeSettingsService and staff 403), PageManagementTest (7), NavigationManagementTest (8, incl. cached tree invalidation and support 403 / staff read-only). Full suite 338 passed, 0 todos; migrate:fresh --seed ok; pint clean; npm run build ok.
30	- 2026-06-10: API phase done. Installed laravel/sanctum ^4.3 (personal access tokens on User via HasApiTokens; config/sanctum.php with token_prefix `shop_` and 1-year default expiration 525600 min; published personal_access_tokens migration). routes/api.php registered in bootstrap/app.php withRouting: Storefront API `/api/storefront/v1` (middleware `store.resolve:storefront`; carts under `throttle:api.storefront`, checkouts under `throttle:checkout`) with carts CRUD (POST 201, line add returns 200 full cart per roadmap table; optimistic concurrency accepts both `cart_version` (spec 02) and `expected_version` (roadmap) and answers 409 with `error_code: version_conflict`, `current_version`, and the full current cart state; CartVersionMismatchException now carries expected/current version properties) and the full checkout flow (create 201/`started`, address with available_shipping_methods, shipping-method with recalculated totals, apply-discount/DELETE discount with InvalidDiscountException mapped to 400 `discount_expired`/`discount_usage_exceeded` or 422, payment-method, pay -> completeCheckout via mock PSP returning order payload + bank transfer instructions, 422 + error_code on decline, 409 on wrong state, 410 on expired checkouts). Admin API `/api/admin/v1/stores/{storeId}` (middleware `auth:sanctum`, `store.resolve:api-admin`, `throttle:api.admin`, Sanctum `abilities:` per endpoint): products index (filters/sort, default per_page 15 per roadmap table, data+meta envelope)/store (options+explicit variants with option_values, inventory quantity+policy, collections sync, store-scoped handle+SKU uniqueness)/show/update (partial; status via ProductService::transitionStatus mapped to 422)/destroy (archives, returns id/status/updated_at), orders index (status/financial/fulfillment/customer/date/query filters)/show (lines+payments+fulfillments+refunds)/fulfillments (creates + marks shipped per spec 02 "mark items as shipped"; FulfillmentGuardException -> 409)/refunds (RefundService against latest captured payment; 409 when nothing refundable). ResolveStore gained `api-admin` context resolving the store from {storeId} and verifying token-user membership (404 unknown store, 403 non-member, 403 mutations on suspended). bootstrap/app.php: `abilities`/`ability` middleware aliases; JSON error envelopes for api/* (404 "The requested resource was not found.", 403 "You do not have permission to perform this action.", InsufficientInventoryException -> 422 validation shape, FulfillmentGuardException -> 409). AppServiceProvider rate limiters per spec 02 section 7: api.admin 60/min per token id, api.storefront 120/min per IP, checkout 10/min per session (IP fallback for stateless API), search 30/min, analytics 60/min, webhooks 100/min, all returning the spec 429 body {message, retry_after}. API Resources: Storefront CartResource/CheckoutResource (unwrapped, spec shapes incl. line snapshots, totals, available_shipping_methods), Admin ProductListResource/ProductResource/OrderListResource/OrderResource/FulfillmentResource/RefundResource (data-wrapped). Admin Developers page (`/admin/developers`, Admin\Developers\Index, sidebar auto-enabled): token table (name, ability badges, last used, created, revoke with wire:confirm), generate-token modal (name + 18 ability checkboxes from App\Support\TokenAbilities), plaintext shown once in amber callout; StorePolicy::manageDevelopers gates to owner/admin (staff 403); webhooks section deferred to Phase 10. Storefront search/analytics endpoints deferred to Phases 8/9 per roadmap; GET /orders/{orderNumber} tokenized order status endpoint deferred with them. Tests exactly per roadmap tables (SanctumTokenTest 5, CartApiTest 8 incl. 121-request rate limit, StorefrontCartApiTest 8, StorefrontCheckoutApiTest 9, AdminProductApiTest 7 incl. default page size 15, AdminOrderApiTest 6) plus Admin/DevelopersTest (5). Full suite 386 passed, 0 todos; migrate:fresh --seed ok; pint clean.
31	- 2026-06-10: Phases 8+9 done (Search + Analytics). Search: migrations `search_settings` (store_id PK, synonyms_json/stop_words_json), `search_queries` (store-scoped log with results_count), and a raw-SQL `products_fts` FTS5 virtual table (CREATE VIRTUAL TABLE IF NOT EXISTS, sqlite-driver-guarded, rowid = product id so upserts are delete+insert by rowid; KEY QUIRK: FTS5 stores column values as text, so the UNINDEXED `store_id` is always filtered with `CAST(store_id AS INTEGER) = ?` because an integer binding/literal silently matches nothing). `SearchService`: `search()` (tokenize -> stop-word removal (falls back to original tokens if all are stop words) -> synonym OR-groups from search_settings with multi-word synonyms as quoted phrases -> trailing prefix `*` on the last token; MATCH subquery joined via joinSub so `fts.rank` drives relevance; filters vendor(s)/product_types/collection_id/price range (minor units)/in_stock/tags; sorts relevance/price_asc/price_desc/newest/best_selling; published+published_at scoping at query time per spec 05 16.3; every search logged to search_queries + a `search` analytics event, with a `logQuery` flag so Livewire filter/pagination re-renders only log when the query text changes), `autocomplete()` (min prefix 2), `countMatches()`, `facetValues()`, `syncProduct()`/`removeProduct()`/`reindexStore()` (last-indexed timestamp kept in cache). `ProductObserver` (created/updated -> sync, deleted -> remove) registered in AppServiceProvider, so seeded/created products index themselves; SearchSettingsSeeder also reindexes both stores defensively. Storefront UI: `Storefront\Search\Modal` (global in storefront layout, opened by the header icon dispatching `open-search-modal`, 300ms debounce, products max 5 + collections max 5 + "View all N results", skeleton loading, Alpine arrow-key/Enter/Escape keyboard nav, dialog/listbox ARIA) and `Storefront\Search\Index` at GET /search (?q= via #[Url], same filter sidebar partial + sort + pagination as collection pages, result heading 'N results for "q"', empty state with retry input); 404 page got the spec search form. Storefront API: GET /api/storefront/v1/search (spec JSON with results/facets/pagination, filters as URL-encoded JSON) and /search/suggest (products + collections), both `throttle:search`. Admin: `Admin\Search\Settings` at /admin/search/settings (synonym group rows add/remove, comma-separated stop words textarea, Reindex now button calling reindexStore synchronously with last-indexed timestamp; viewSettings/updateSettings policies so staff 403). Analytics: migrations `analytics_events` (unique (store_id, client_event_id) dedupe, occurred_at, properties_json) and `analytics_daily` (composite PK store_id+date). `AnalyticsService` (`track()` swallowing UniqueConstraintViolationException for idempotent ingestion, `getDailyMetrics()`, `eventCountsBetween()` for funnels). `AggregateAnalytics` job (optional Y-m-d constructor arg, defaults to yesterday; per-store recompute + upsert so reruns are idempotent; orders/revenue/AOV from checkout_completed event `total_amount` properties, visits = distinct page_view sessions; scheduled dailyAt 01:00 in routes/console.php). Ingestion endpoint POST /api/storefront/v1/analytics/events (`throttle:analytics`, batch 1-50, occurred_at within +/-1h, 202 {accepted, rejected} where dupes count as rejected, customer guard picked up via $request->user('customer')). Server-side tracking: product_view in Products\Show::mount, add_to_cart in Products\Show::addToCart, remove_from_cart in InteractsWithCart::removeLine, checkout_started in CheckoutService::createFromCart, checkout_completed via new `LogOrderAnalyticsEvent` listener on OrderCreated (spec 05 17); page_view via a tiny vanilla fetch snippet in the storefront layout (sessionStorage session id, crypto.randomUUID client_event_id). `Admin\Analytics\Index` at /admin/analytics (today/7d/30d/custom ranges, KPI tiles with vs-previous-period badges from analytics_daily, SVG sales chart reusing the Phase 7a dependency-free geometry approach, funnel page_view->product_view->add_to_cart->checkout_started->checkout_completed from raw events with proportional Tailwind bars, top products from order_lines with % of total, top referrers table attributing sessions to first page_view referrer host with per-source conversion); StorePolicy::viewAnalytics owner/admin/staff (support 403); sidebar Analytics item auto-enabled via Route::has(). Dashboard integration point switched: conversion rate now uses analytics_daily visits for the range, falling back to the cart-based approximation when a store has no analytics data. Seeders: AnalyticsSeeder (31 daily rows with ~3% growth + 220 events across ~35 sessions/7 days with spec type distribution and 30% customer attribution) + SearchSettingsSeeder (spec 07 3.18 synonym/stop-word sets for both stores), wired into DatabaseSeeder. Tests exactly per roadmap tables (SearchTest 5, AutocompleteTest 3, EventIngestionTest 5, AggregationTest 3) plus Storefront/SearchUiTest (8 incl. both search APIs), Admin/AnalyticsTest (7 incl. role matrix), Admin/SearchSettingsTest (5 incl. reindex rebuild + staff 403). Full suite 422 passed, 0 todos; migrate:fresh --seed ok (25 products indexed, 31 daily rows, 220 events); pint clean; npm run build ok; live-smoke-tested search page (synonym "tee" finds t-shirts), suggest API, and analytics ingestion (202) on shop.test.
32	- 2026-06-10: Phase 10 done (Apps and Webhooks). Migrations per spec 01 Epic 8: apps (name/status/created_at), app_installations (unique store+app, scopes_json, active/suspended/uninstalled, installed_at), oauth_clients (client_id unique, encrypted secret, redirect URIs), oauth_tokens (hashed access/refresh, expires_at), webhook_subscriptions (store-scoped, nullable app_installation_id, event_type, target_url, encrypted signing secret, active/paused/disabled, plus a `consecutive_failures` counter column - the one deliberate addition over spec 01 because spec 05 13.4 requires a persisted circuit breaker counter that resets on success), webhook_deliveries (event_id, attempt_count, pending/success/failed, last_attempt_at, response_code, response_body_snippet). OAuth2 flow itself stays deferred per roadmap tech decisions: tables + models + factories only, no Passport. Models: `App\Models\App` (explicit $table='apps', UPDATED_AT=null; import as `App as AppModel` where the facade collides), AppInstallation, OauthClient, OauthToken, WebhookSubscription (BelongsToStore, 'encrypted' cast on signing_secret_encrypted, latestDelivery ofMany relation), WebhookDelivery; 4 new enums (AppStatus, AppInstallationStatus, WebhookSubscriptionStatus, WebhookDeliveryStatus). `WebhookService`: EVENT_TYPES catalog (order.created/paid/fulfilled/cancelled/refunded, product.created/updated/deleted, checkout.completed), dispatch(store,eventType,payload) queries active subscriptions withoutGlobalScope + explicit store_id (queue context has no current_store), creates pending deliveries (attempt_count 0) sharing one event UUID, queues DeliverWebhook with a versioned envelope {id, event, api_version:'v1' per spec 02 sec 9, store_id, created_at, data}; sign() HMAC-SHA256 hex; verify() hash_equals timing-safe. `DeliverWebhook` job: tries=6, backoff [60,300,1800,7200,43200] (spec 05 13.3); POSTs JSON via Http facade with X-Platform-Signature/-Event/-Delivery-Id (fresh UUID per attempt)/-Timestamp headers; DB attempt_count is the attempt source of truth (incremented per run, robust under sync driver); records response code + 500-char body snippet + last_attempt_at; non-2xx/connection error throws RuntimeException to trigger queue retry except on the 6th attempt which dead-letters as failed; circuit breaker increments subscription.consecutive_failures per failed attempt, pauses at 5 with a logged warning, success resets to 0; paused/disabled/missing subscriptions cause pending deliveries to be marked failed and skipped. Event wiring: `DispatchWebhooks` listener with a union-typed handle (auto-discovered for all six events, verified via event:list) maps OrderCreated/OrderPaid/OrderFulfilled/OrderCancelled/OrderRefunded (payload incl. refund block)/CheckoutCompleted; product.created/updated/deleted dispatched from ProductObserver (product changes are model events, not domain event classes; archiving via status change emits product.deleted per spec 05 13.1). Admin UI: `Admin\Apps\Index` at /admin/apps (installed app cards with icon/installed-ago/status badge/uninstall linking to detail, plus available-apps directory with Install; install reactivates uninstalled rows, uninstall flips status and disables the installation's webhook subscriptions), `Admin\Apps\Show` at /admin/apps/{installation} (granted scopes badges, webhook subscriptions table with last delivery + response code badge, usage panel with token count and "Never" for API calls since OAuth is stubbed, uninstall), both behind new StorePolicy::manageApps (owner/admin per spec 05 1.3 matrix); sidebar Apps item auto-enabled via existing Route::has(). Developers page webhooks section replaces the placeholder: subscription table (event type, URL, status badge active=green/paused=red, last delivery + response code), Add/Edit modal (event type select from EVENT_TYPES, URL input, secret `whsec_` + 32 random chars generated server-side and shown once in an amber callout), pause/resume (resume resets consecutive_failures per spec 05 13.4 manual re-enable), delete with confirm, recent-deliveries panel (last 10 attempts with status/response/attempts/timestamp). AppSeeder (idempotent, wired into DatabaseSeeder): 3 registry apps each with an oauth client, Loyalty Rewards installed on acme-fashion with an order.created subscription, one store-level order.paid subscription, sample success/pending deliveries. Tests exactly per roadmap tables: Webhooks/WebhookDeliveryTest (5: delivery via real OrderCreated event + Http::fake, HMAC header matches hash_hmac of raw body, retry increments attempt_count + asserts tries/backoff config, 6th attempt dead-letters as failed, 5 consecutive failures pause the subscription and paused subscriptions get no new deliveries) and Webhooks/WebhookSignatureTest (4: known-hash signature, verify round-trip, tampered payload rejected, wrong secret rejected); plus Admin/AppsTest (7: render, install, reinstall reactivation, uninstall disables subscriptions, detail page scopes+webhooks, cross-store 404, staff 403/admin 200) and 6 new Admin/DevelopersTest cases (create with one-time whsec_ secret, event-type/URL validation, edit prefill + update without secret rotation, pause/resume resets failures, delete, deliveries panel render). Full suite 444 passed, 0 todos; migrate:fresh --seed ok (3 apps, 1 installation, 3 oauth clients, 2 subscriptions, 4 deliveries); pint clean.
33	- 2026-06-10: Phase 11 done (Polish). Seeder audit against spec 07: all 19 seeders matched the spec'd dataset (2 stores, 4 domains incl. shop.test, 5 users/roles, 25 products with full option/variant/inventory graphs, 6 collections, 5 discounts, 12 customers, 18 orders incl. the full status matrix, themes/pages/navigation, 31 analytics_daily rows + 220 events, search settings, apps/webhooks); one gap fixed: Mechanical Keyboard now assigned to the Electronics "Featured" collection per spec 07 section 4. A11y: admin layout gained a skip link + main#main-content (storefront already had one); cart drawer and mobile nav drawer now move focus into the panel on open; account address modal focuses its first control on open; sr-only aria-live cart-count region added to the header; aria-labels added to the admin theme color inputs and the product media alt-text editor. Responsive + dark mode audits found no real gaps (all admin tables wrapped in overflow-x-auto, storefront tables have mobile card alternatives, only intentional light-only classes remain). Error pages: generic styled errors/404, 419, 500, 503 views added (admin/non-tenant contexts; storefront keeps its themed 404/503 via the exception render hooks, API keeps JSON envelopes). Structured logging: new LogStructuredBusinessEvent listener writes order.created/order.paid/order.cancelled/order.refunded to the JSON `structured` channel; payment.failed logged in CheckoutService on declined charges; webhook.delivery_failed logged in DeliverWebhook when attempts are exhausted. New tests: Orders/StructuredLoggingTest (3) and tests/Feature/SmokeTest.php (3 tests iterating every public storefront GET route, customer account pages, and 29 admin pages against the demo seed). phpunit.xml memory_limit raised to 512M (full-suite smoke seeding exceeded the 128M default). Verification checklist: composer install clean, npm run build clean, migrate:fresh --seed clean, route:list 93 routes no duplicates, config:cache + config:clear OK, optimize:clear OK, pint clean on whole codebase, full suite 450 passed / 0 todos.
34	- 2026-06-10: Phase 12 part 1 done (browser test infrastructure + suites 1, 2, 7, 8, 9 of spec 08). Installed pestphp/pest-plugin-browser ^4.3 (Playwright 1.59 already present via node_modules/MCP). Infrastructure decisions: Pest's browser plugin serves the Laravel app IN-PROCESS (Amp socket server on 127.0.0.1:random-port inside the test process), so the existing phpunit.xml in-memory SQLite + RefreshDatabase setup is shared by the browser-issued HTTP requests and stays untouched - all 450 existing tests keep their config. `.env.testing` created per spec 08 (APP_ENV/APP_URL/PAYMENT_PROVIDER=mock/MAIL_MAILER=array/QUEUE_CONNECTION=sync + APP_KEY copied from .env because creating .env.testing stops Laravel from falling back to .env); phpunit.xml `<env>` values still win for the test run (DB stays :memory:). Host/tenancy strategy: the spec's "browse acme-fashion.test via Herd" approach doesn't work because the plugin's Amp DNS resolver bypasses macOS /etc/resolver (Herd dnsmasq), so instead the Browser suite's beforeEach seeds the full DatabaseSeeder and registers an extra store_domains row mapping hostname `127.0.0.1` to the Acme Fashion store - ResolveStore resolves the tenant through the real domain-table mechanism. `Http::fake()` in the Browser beforeEach prevents the seeded webhook subscriptions (AppSeeder) from making real outbound HTTP on the sync queue during checkout completion (was 500ing the pay step). tests/Browser registered in phpunit.xml testsuites + tests/Pest.php (TestCase + RefreshDatabase + seeded beforeEach); browser helpers added to Pest.php (browserLoginAsAdmin, browserAddClassicTeeToCart, browserFillCheckoutAddress, browserReachCheckoutPaymentStep). App changes made to satisfy spec 08 (spec-vs-implementation deviations fixed toward spec): (1) admin login heading/button "Login" -> "Sign in" and customer login "Login" -> "Log in" (specs 08 1.5/1.6/2.x; AdminAuthTest/CustomerAuthTest updated); (2) checkout payment steps 4+5 merged into the single spec-04 "Payment Method & Pay" step - radio fieldset (wire:model.live) + method-specific form/pay button render together, the "Continue to payment" intermediate click is gone, payNow() now calls selectPaymentMethod when the checkout is still shipping_selected and persists a switched method when already payment_selected (Livewire selectPayment action removed; CheckoutUiTest still green); (3) DE postal-code format validation (regex ^\d{5}$) + friendly validationAttributes added to the checkout address step (spec 08 9.7); (4) `novalidate` added to admin login + checkout contact/address forms so server-side validation messages render in the browser instead of native bubbles (spec 08 2.3/2.4/9.5/9.6); password @error block added to admin login; (5) InvalidDiscountException::notFound message "This discount code does not exist." -> "Invalid discount code." per spec 04 section "Invalid discount code" toast/error (CartUiTest updated); (6) ThemeSeeder: acme-fashion gets featured_products_collection_handle=new-arrivals so the home page actually shows Classic Cotton T-Shirt per spec 08 7.1 (default was latest-by-id which excluded product #1). Browser test gotchas learned: bare tag selectors like 'h1' are NOT treated as CSS by the plugin's locator guesser (use 'h1[data-flux-heading]' or other explicit selectors with CSS metacharacters); explicit selectors are strict-mode (scope duplicates like 'nav a:visible:has-text(...)' / 'table [aria-label=...]'); the cart drawer auto-opens on cart-updated after applying a discount and overlays the page Checkout link (close it via [aria-label="Close cart"]). tests/Browser/Screenshots + database/testing.sqlite gitignored. Suites: SmokeTest 10, Admin/AuthenticationTest 10, Storefront/BrowsingTest 15, Storefront/CartTest 12, Storefront/CheckoutTest 13 (incl. magic cards 4242/0002 declined/9995 insufficient, PayPal, bank transfer IBAN/BIC/reference instructions, FLAT5-in-checkout totals 24.98, DE vs US shipping zones). Full suite 510 passed (1641 assertions), 0 todos, ~81s; pint clean. Remaining for part 2: suites 3-6 and 10-18 (83 tests) + Playwright MCP acceptance verification.
35	- 2026-06-10: Phase 12 part 2 done (browser test suites 3-6 and 10-18, 83 new tests; spec 08 complete at 143 browser tests / 18 files). New files: Browser/Admin/ProductManagementTest (7), OrderManagementTest (11), DiscountManagementTest (6), SettingsTest (7), CollectionManagementTest (3), CustomerManagementTest (3), PageManagementTest (3), AnalyticsTest (3); Browser/Storefront/CustomerAccountTest (12), InventoryTest (4), TenantIsolationTest (5), ResponsiveTest (8), AccessibilityTest (11). New Pest.php helpers: browserLoginAsCustomer, browserOpenAdminOrder, browserCreateFulfillment, switchBrowserTestDomainToStore (re-points the 127.0.0.1 store_domains row at another store and forgets the ResolveStore hostname cache so tenant isolation is exercised through the REAL domain-resolution path in both directions; the plugin's in-process HTTP server always reports 127.0.0.1 as the request host, so a second hostname is impossible). REAL BUG found+fixed: discounts/form.blade.php used a Blade `@if` directive inside the <flux:input> component tag attribute list, which is unsupported by the component-tag compiler and silently broke rendering of the discount value input (admin could never enter a discount value in the browser); replaced with a bound `:max` attribute. Spec-08 alignment changes to the app (noted where spec 03/04 said otherwise): refund toast "Refund issued" -> "Refund processed" (08 4.6; 03's toast table said "Refund issued"), shipping rate save toast -> "Shipping rate saved" (08 6.4), tax save toast -> "Tax settings saved" + "Tax Settings" secondary heading on the taxes tab (08 6.5/6.6; 03 only had generic "Settings saved"), customer register heading/button "Register" -> "Create an account"/"Create account" (per BOTH 04 10.2 and 08 10.1), account-nav dashboard tab "Account" -> "My Account" (08 10.1/10.4 expect visible "My Account"; 04 doesn't spec the tab labels), address book now shows an "Address saved" status message after save (08 10.10/10.11; component previously closed the modal silently). Test-side adaptations to seeded reality (not weakenings): product list paginates at 15 of 20 products sorted by updated_at, so 3.1 sorts by Title and 3.3 uses the admin search; "FAQ" page already seeded so 17.2 supplies a unique handle (faq-e2e); pages list shows "About Us" (seeded title) for 17.3; 3.4 clicks the Active tab explicitly because the app's default product filter is All. Gotchas learned: Playwright fill() does not trigger Alpine x-model on the entangled PDP quantity input (click + Backspace + type() does - used in 11.4 with assertScript on Alpine.$data to prove qty 15 before the over-stock increment); '#1001'-style text starts with '#' so the locator guesser treats it as CSS - click via 'a:has-text("#1001")'; getByText is case-insensitive substring ("Fulfilled" matches "Unfulfilled" - 4.11 asserts the badge text via assertScript; "Visits" matches "unique visits" on analytics); resize() returns AwaitableWebpage so browserFillCheckoutAddress now accepts/returns the union type; keys('body', ...) fails the locator guesser (bare tag) - use 'body:first-of-type'; Flux labels are <ui-label> custom elements associated at runtime, so a11y label checks must accept aria-labelledby. Full suite 593 passed (2054 assertions), 0 todos, ~156s; pint clean.
36

# Implementation Progress

Tracking implementation of the shop system per `specs/09-IMPLEMENTATION-ROADMAP.md`.

| Phase | Scope | Status | Tests |
|-------|-------|--------|-------|
| 1 | Foundation: config, tenancy migrations/models, enums, ResolveStore, BelongsToStore, admin+customer auth, policies, Pest helpers | DONE (commit 5b743cde) | 30 passed, 3 todos |
| 2 | Catalog: products, options, variants, inventory, collections, media | DONE | full suite 79 passed, 5 todos (4 deferred to P5, 1 to P4) |
| 3 | Themes, pages, navigation, storefront layout + components | DONE (commit 5aee8dc4) | full suite 98 passed, 5 todos; Storefront/* rendering tests added |
| 4 | Cart, checkout, discounts, shipping, taxes + storefront cart/checkout UI | DONE | full suite 202 passed, 8 todos; Unit/* (48 new), Cart/CartServiceTest (12), Checkout/* (34), Storefront/CartUiTest (13) |
| 5 | Payments (mock PSP), orders, refunds, fulfillments, events | DONE | full suite 259 passed, 0 todos; Orders/* (28), Payments/* (17), Storefront/CheckoutUiTest (4), all 8 deferred todos resolved |
| 6 | Customer accounts | DONE | full suite 273 passed, 0 todos; Customers/CustomerAccountTest (6), Customers/AddressManagementTest (7), checkout prefill case added to Storefront/CheckoutUiTest |
| 7a | Admin panel core: layout shell (sidebar/top bar/breadcrumbs/toasts), dashboard, products (index + shared form with variants builder + media), orders (index + detail with fulfillment/refund/cancel/confirm-payment), customers (index + detail with address CRUD) | DONE | full suite 290 passed, 0 todos; Admin/DashboardTest (4), Admin/ProductManagementTest (8), Admin/OrderManagementTest (5) |
| 7b | Admin panel remaining sections: collections, inventory, discounts, settings (general/domains/checkout/notifications/shipping/taxes), themes (cards + editor), pages, navigation (apps/developers/analytics stay "Coming soon" until Phases 9/10) | DONE | full suite 338 passed, 0 todos; Admin/DiscountManagementTest (6), Admin/SettingsTest (6), Admin/CollectionManagementTest (7), Admin/InventoryManagementTest (7), Admin/ThemeManagementTest (8), Admin/PageManagementTest (7), Admin/NavigationManagementTest (8) |
| API | Spec 02 REST APIs + Sanctum: storefront cart/checkout API, admin products/orders API with token abilities, rate limiters, Developers admin page | DONE | full suite 386 passed, 0 todos; Auth/SanctumTokenTest (5), Cart/CartApiTest (8), Api/StorefrontCartApiTest (8), Api/StorefrontCheckoutApiTest (9), Api/AdminProductApiTest (7), Api/AdminOrderApiTest (6), Admin/DevelopersTest (5) |
| 8 | Search (FTS5) | DONE | full suite 422 passed, 0 todos; Search/SearchTest (5), Search/AutocompleteTest (3), Storefront/SearchUiTest (8), Admin/SearchSettingsTest (5) |
| 9 | Analytics | DONE | (same run as Phase 8) Analytics/EventIngestionTest (5), Analytics/AggregationTest (3), Admin/AnalyticsTest (7) |
| 10 | Apps & webhooks: 6 migrations (apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries), WebhookService + DeliverWebhook job (HMAC, backoff, circuit breaker), DispatchWebhooks listener + ProductObserver wiring, Admin Apps directory/detail, Developers webhook CRUD, AppSeeder | DONE | full suite 444 passed, 0 todos; Webhooks/WebhookDeliveryTest (5), Webhooks/WebhookSignatureTest (4), Admin/AppsTest (7), Admin/DevelopersTest (+6 webhook cases, 11 total) |
| 11 | Polish: seeder audit vs spec 07, a11y (skip links, focus management, aria-live, labels), responsive + dark mode pass, error pages (404/419/500/503), structured business-event logging, SmokeTest | DONE | full suite 450 passed, 0 todos; Orders/StructuredLoggingTest (3), SmokeTest (3 tests, ~50 routes) |
| 12 | Full suite + browser tests (spec 08) + Playwright MCP acceptance verification | DONE: all 18 suites, 143 browser tests complete | full suite 593 passed (2054 assertions), 0 todos; Browser: SmokeTest (10), Admin/AuthenticationTest (10), Admin/ProductManagementTest (7), Admin/OrderManagementTest (11), Admin/DiscountManagementTest (6), Admin/SettingsTest (7), Admin/CollectionManagementTest (3), Admin/CustomerManagementTest (3), Admin/PageManagementTest (3), Admin/AnalyticsTest (3), Storefront/BrowsingTest (15), CartTest (12), CheckoutTest (13), CustomerAccountTest (12), InventoryTest (4), TenantIsolationTest (5), ResponsiveTest (8), AccessibilityTest (11) |

## Log

- 2026-06-09: Project start. Fresh Livewire starter kit (Fortify). Created progress tracker, started Phase 1.
- 2026-06-09: Phase 2 done. Catalog migrations (9 tables), models, enums, ProductService/VariantMatrixService/InventoryService/MediaService, HandleGenerator, ProcessMediaUpload job (GD), Collection+Product seeders (25 products, 127 variants). Order-reference checks implemented behind Schema::hasTable('order_lines') guard; 3 order-dependent test cases todo'd for Phase 5.
- 2026-06-09: Phase 4 done. Migrations for carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts. Models + factories + 7 enums. Services: CartService (session key `cart_id`, version bump on every mutation, sum-quantities merge on login), DiscountService (reason-coded InvalidDiscountException, largest-remainder allocation), ShippingCalculator (specificity-ordered zone matching, flat/weight/price rates), TaxCalculator (basis points, intdiv for exclusive add and inclusive extract), PricingEngine (deterministic pipeline, snapshot to totals_json; inclusive total = discounted subtotal + shipping), CheckoutService state machine (payment_selected reserves inventory + 24h expires_at; completeCheckout guarded, throws LogicException until Phase 5). Jobs ExpireAbandonedCheckouts (15 min) + CleanupAbandonedCarts (daily) scheduled in routes/console.php. Storefront UI: CartDrawer (global, listens cart-updated/open-cart), /cart page with shipping estimate, /checkout 4-step stepper through payment method selection (pay button is the Phase 5 mount point), live header badge, real addToCart. Seeders: TaxSettingsSeeder, ShippingSeeder, DiscountSeeder (WELCOME10/FLAT5/FREESHIP/EXPIRED20/MAXED). Cart merge wired into CustomerLoginController; CustomerAuthTest merge case implemented. 4 test cases todo'd for Phase 5 (order completion paths).
- 2026-06-09: Phase 5 done. Migrations for orders (+ nullable unique checkout_id for idempotency), order_lines, payments, refunds, fulfillments (+ delivered_at), fulfillment_lines. Models + factories (paid/pending/fulfilled/cancelled states) + 7 enums (OrderStatus, FinancialStatus, FulfillmentStatus, PaymentMethod, PaymentStatus, RefundStatus, FulfillmentShipmentStatus). PaymentProvider contract bound to MockPaymentProvider (magic cards 4242/0002/9995, paypal instant capture, bank_transfer pending, mock_ reference IDs); PaymentResult/RefundResult value objects. OrderService (createFromCheckout atomic with snapshots + payment record + inventory commit for captured / keep-reserved for bank transfer + discount usage increment + cart converted; generateOrderNumber sequential per store from #1001 inside the creation transaction with unique (store_id, order_number) backstop; cancel releases or restocks per financial status; confirmBankTransferPayment commits inventory + digital auto-fulfillment). RefundService (refundable validation, partial/full financial status, restock flag). FulfillmentService (FulfillmentGuardException unless paid/partially_refunded, qty <= unfulfilled validation, markAsShipped/markAsDelivered, autoFulfillDigital delivered). CheckoutService::completeCheckout implemented: idempotent via orders.checkout_id, PaymentFailedException on decline (reservation kept, released by 24h expiry). Events: OrderCreated/OrderPaid/OrderFulfilled/OrderCancelled/OrderRefunded/CheckoutCompleted/FulfillmentDelivered. CancelUnpaidBankTransferOrders daily job (store settings bank_transfer_cancel_days, default 7). Storefront pay step (card form / paypal / bank transfer) + /checkout/{checkoutId}/confirmation page with bank transfer instructions. CustomerSeeder (12 customers + addresses) and OrderSeeder (15 fashion + 3 electronics orders) wired into DatabaseSeeder, idempotent. All 8 deferred todos resolved; suite has ZERO todos.
- 2026-06-09: Phase 6 done. Livewire account area behind `auth:customer` inside the storefront group: `Storefront\Account\Dashboard` (GET /account: welcome heading, quick-link cards, last 5 orders, profile form with name + marketing_opt_in via `updateProfile`), `Account\Orders\Index` (GET /account/orders: paginated table desktop / cards mobile, status + fulfillment badges), `Account\Orders\Show` (GET /account/orders/{orderNumber}: bare number in URL matched against the stored `#`-prefixed order_number, scoped to the auth'd customer so foreign orders 404; timeline built from placed_at + captured payment + fulfillments shipped/delivered + cancelled, line items with snapshots, shipping/billing/payment grid, totals, tracking links target=_blank rel=noopener), `Account\Addresses\Index` (GET /account/addresses: modal CRUD reusing x-storefront.address-form with prefix="form", postal_code<->zip mapping to the spec 01 JSON shape, first address auto-default, setDefault flips others off, deleting the default promotes the newest remaining, foreign address ids abort 404). New components: x-storefront.account-nav (tabs + logout form), x-storefront.order-status-badge (spec 04 colors). `App\Support\Storefront\Countries` extracted (address-form now uses it). CustomerAddress gained toCheckoutAddress()/summaryLine(). Checkout: address step prefills from the default address when checkout has no address, saved-address dropdown ("Use a new address" resets) per spec 04; email prefill existed. Logout now session()->invalidate() + regenerateToken (spec 06). Confirmation page "View order" links to the order detail. Header account icon already linked correctly (Phase 3). Verified in browser via Playwright on acme-fashion.test (login, dashboard, orders, order #1002 timeline, address create modal, checkout prefill + dropdown switch).- 2026-06-09: Phase 7a done (admin panel core). Layout: `resources/views/layouts/admin.blade.php` (component layout, used via `#[Layout('layouts::admin')]`) with `Admin\Layout\Sidebar` (fixed 256px desktop, Alpine-driven mobile overlay via `toggle-admin-sidebar` window event, nav groups per spec 03 section 1.2; unimplemented sections render as disabled "Coming soon" items and auto-activate once `Route::has()` finds them) and `Admin\Layout\TopBar` (store selector dropdown -> `switchStore()` validates membership, writes session `current_store_id`, redirects to dashboard; profile menu with logout; mobile hamburger). Global toast container (Alpine, top-right, 5s auto-dismiss, success/error/info left borders) listens for the Livewire `toast` event via `x-on:toast.window` and replays `session('toast')` flashes after redirects; components use the `Admin\Concerns\SendsToasts` trait. Breadcrumbs/cards/status badges are anonymous Blade components `x-admin.breadcrumbs` / `x-admin.card` / `x-admin.status-badge`. Dashboard (GET /admin): KPI tiles (total sales/orders/AOV + conversion rate approximated as orders/carts until Phase 9 analytics; change badges vs previous period), dependency-free inline SVG line chart of daily order counts, recent orders table (last 10), 7/30/90-day presets. Products: `Admin\Products\Index` (search, status tabs, type filter, sortable columns incl. inventory subquery sum, bulk set-active/archive/delete with confirm modal, empty states, 15/page) and shared `Admin\Products\Form` for create+edit (route param `{productId}` because a `{product}` param collides with the `$product` model property during Livewire param auto-assignment): options builder (comma-separated values, live matrix regeneration preserving row edits by combination key), per-variant sku/barcode/price/compare-at/weight/qty/ship rows applied post-`VariantMatrixService::rebuildMatrix` via case-insensitive sorted value-combination keys, diff-based option/value sync so untouched combinations keep variants+inventory, media upload (WithFileUploads, immediate attach in edit mode, pending until save in create mode, alt text inline edit, wire:sort reorder, delete), collections checkboxes, SEO handle (blank -> HandleGenerator), publishing datetime, status transitions through `ProductService::transitionStatus`. Orders: `Admin\Orders\Index` (status tabs all/pending/paid/fulfilled/cancelled/refunded, search order#/email, date range, sortable) and `Admin\Orders\Show` (computed `order` keyed by locked `orderId`, timeline incl. refunds/cancellation, line items with per-line fulfillment badges, totals incl. refunded, payment panel with Confirm Payment for pending bank transfers -> `OrderService::confirmBankTransferPayment`, fulfillment guard callout, fulfillment modal -> `FulfillmentService::create`, mark shipped/delivered, refund modal (selected lines sum or custom amount, reason, restock) -> `RefundService::create`, cancel modal -> `OrderService::cancel`; service ValidationException/FulfillmentGuardException surfaced as error toasts). Customers: `Admin\Customers\Index` (search, orders count + total spent) and `Admin\Customers\Show` (info card, paginated order history, address CRUD modal with default handling per spec 03 section 9.2). Routes registered in the existing `['auth','admin']` group; placeholder dashboard route/view removed. All actions policy-guarded (`@can` in views + `authorize()` in actions; Support read-only, Staff cannot delete/archive). Pest helper `createStoreMember(Store, StoreUserRole)` added. Tests: Admin/DashboardTest (4), Admin/ProductManagementTest (8), Admin/OrderManagementTest (5), exactly per roadmap tables; full suite 290 passed, 0 todos; pint clean; npm run build ok; migrate:fresh --seed ok. Browser-verified via Playwright on shop.test/admin: login, dashboard KPIs+chart, products list, Leather Belt edit form (options/variants prefilled), order #1005 bank-transfer confirm -> paid, fulfillment modal -> DHL fulfillment -> shipped -> delivered with timeline updates, toast rendering, customer detail with addresses, mobile sidebar overlay at 375px, store switcher (verified with a temporary second membership, then reverted; revoked membership correctly 403s).
- 2026-06-10: Phase 7b done (remaining admin sections). Collections: `Admin\Collections\Index` (search, status filter, products_count, delete modal) + shared `Admin\Collections\Form` ({collectionId} edit route; title/handle/description/status, handle unique per store + auto-slug from title, product picker with debounced search dropdown + Add buttons, assigned list with thumbnails, `wire:sort` drag reorder buffered in `assignedProductIds` and synced to `collection_products.position` on save). Discounts: `Admin\Discounts\Index` (code search, schedule-aware status filter incl. scheduled/expired derived states, type filter, value column %/fixed-currency/free-shipping, usage "n / limit-or-unlimited" column) + shared `Admin\Discounts\Form` (code/automatic radios, Generate code, percent/fixed/free_shipping value types with unit conversion percent-whole-number vs fixed-minor-units, per-store code uniqueness `Rule::unique`, min purchase + specific products/collections + one-per-customer persisted into `rules_json` per DiscountService keys, usage limit, start/end datetimes with after-validation, Active/Disabled switch -> status). Inventory: `Admin\Inventory\Index` (search by product title or SKU, stock filter in/low(<=5)/out via on_hand - reserved expressions, inline on-hand number inputs `wire:change` -> `updateQuantity` clamped >= 0 and guarded by ProductPolicy::update so Support is read-only, policy badges, available column color-coded). Settings: `Admin\Settings\Index` page with `?tab=` (general/domains/checkout/notifications rendered as child Livewire components `Settings\General/Domains/Checkout/Notifications`; shared `x-admin.settings-tabs` bar links Shipping/Taxes to their own routes per spec 02). General: store name/contact email/order number prefix + currency/locale/timezone (writes both `stores` columns and `store_settings.settings_json`). Domains: table with type/primary/TLS badges, add-domain modal (hostname regex + global uniqueness), set-primary scoped per domain type, refuses removing the last domain. Checkout: guest checkout toggle + `bank_transfer_cancel_days` (consumed by the Phase 5 job). Notifications: notification email + transactional toggles in settings_json. `Settings\Shipping`: zone cards (countries checklist of 22 ISO codes + optional comma-separated region codes), nested rate tables (flat amount or multi-range weight/price configs matching ShippingCalculator's `ranges`/`min_g`/`min_amount` shape; carrier intentionally omitted as unimplemented in the calculator), inline active toggles, zone/rate modals, test-address tool calling `ShippingCalculator::getAvailableRates`. `Settings\Taxes`: manual/provider radios, manual rate % -> `default_rate_bps`, tax name, prices_include_tax + shipping_taxable switches, provider select + API key into config_json. All settings pages authorize via StorePolicy viewSettings/updateSettings (owner/admin only; staff 403s). Themes: `Admin\Themes\Index` (card grid, published card gets blue ring, Customize button, ellipsis menu Publish/Duplicate/Delete with wire:confirm; publish demotes other published themes inside a transaction and calls `ThemeSettingsService::forget`; duplicate copies settings as a draft "(Copy)"; published theme cannot be deleted) and `Admin\Themes\Editor` (three-panel: left fixed groups header/colors/catalog/footer plus orderable+toggleable home sections hero/featured-collections/featured-products/newsletter/rich-text driving the persisted `sections` array via `wire:sort` + eye toggles; center iframe preview of the primary storefront domain; right dynamic fields text/textarea/color/select/number/checkbox bound to `settings.*` from `ThemeSettingsService::defaults()`, featured_collection_handles edited as comma list; Save persists theme_settings (model event invalidates cache), Save & publish also publishes). Pages: `Admin\Pages\Index` (search, handle column) + shared `Admin\Pages\Form` (title/handle unique per store/body textarea 16 rows/status draft-published-archived/published_at backfilled on publish, delete modal owner-admin only). Navigation: `NavigationMenuPolicy` added (view owner/admin/staff, update owner/admin per spec 06 matrix), `Admin\Navigation\Index` (menu cards -> selectMenu buffers items in state; item modal with label, type select link/page/collection/product and conditional URL input or resource select; `wire:sort` reorder; Save menu diff-deletes/upserts with positions inside a transaction then `NavigationService::forget` so storefront trees refresh; nesting omitted because navigation_items has no parent_id in spec 01). Routes registered for all sections ({collectionId}/{discountId}/{pageId}/{themeId} params following the Phase 7a productId convention); sidebar items for Collections/Inventory/Discounts/Pages/Navigation/Themes/Settings auto-enabled via the existing Route::has() mechanism (Analytics/Apps/Developers still disabled). Tests exactly per roadmap tables (DiscountManagementTest 6 cases, SettingsTest 6 cases incl. staff 403 + domains) plus CollectionManagementTest (7), InventoryManagementTest (7), ThemeManagementTest (8, incl. cache invalidation end-to-end via ThemeSettingsService and staff 403), PageManagementTest (7), NavigationManagementTest (8, incl. cached tree invalidation and support 403 / staff read-only). Full suite 338 passed, 0 todos; migrate:fresh --seed ok; pint clean; npm run build ok.
- 2026-06-10: API phase done. Installed laravel/sanctum ^4.3 (personal access tokens on User via HasApiTokens; config/sanctum.php with token_prefix `shop_` and 1-year default expiration 525600 min; published personal_access_tokens migration). routes/api.php registered in bootstrap/app.php withRouting: Storefront API `/api/storefront/v1` (middleware `store.resolve:storefront`; carts under `throttle:api.storefront`, checkouts under `throttle:checkout`) with carts CRUD (POST 201, line add returns 200 full cart per roadmap table; optimistic concurrency accepts both `cart_version` (spec 02) and `expected_version` (roadmap) and answers 409 with `error_code: version_conflict`, `current_version`, and the full current cart state; CartVersionMismatchException now carries expected/current version properties) and the full checkout flow (create 201/`started`, address with available_shipping_methods, shipping-method with recalculated totals, apply-discount/DELETE discount with InvalidDiscountException mapped to 400 `discount_expired`/`discount_usage_exceeded` or 422, payment-method, pay -> completeCheckout via mock PSP returning order payload + bank transfer instructions, 422 + error_code on decline, 409 on wrong state, 410 on expired checkouts). Admin API `/api/admin/v1/stores/{storeId}` (middleware `auth:sanctum`, `store.resolve:api-admin`, `throttle:api.admin`, Sanctum `abilities:` per endpoint): products index (filters/sort, default per_page 15 per roadmap table, data+meta envelope)/store (options+explicit variants with option_values, inventory quantity+policy, collections sync, store-scoped handle+SKU uniqueness)/show/update (partial; status via ProductService::transitionStatus mapped to 422)/destroy (archives, returns id/status/updated_at), orders index (status/financial/fulfillment/customer/date/query filters)/show (lines+payments+fulfillments+refunds)/fulfillments (creates + marks shipped per spec 02 "mark items as shipped"; FulfillmentGuardException -> 409)/refunds (RefundService against latest captured payment; 409 when nothing refundable). ResolveStore gained `api-admin` context resolving the store from {storeId} and verifying token-user membership (404 unknown store, 403 non-member, 403 mutations on suspended). bootstrap/app.php: `abilities`/`ability` middleware aliases; JSON error envelopes for api/* (404 "The requested resource was not found.", 403 "You do not have permission to perform this action.", InsufficientInventoryException -> 422 validation shape, FulfillmentGuardException -> 409). AppServiceProvider rate limiters per spec 02 section 7: api.admin 60/min per token id, api.storefront 120/min per IP, checkout 10/min per session (IP fallback for stateless API), search 30/min, analytics 60/min, webhooks 100/min, all returning the spec 429 body {message, retry_after}. API Resources: Storefront CartResource/CheckoutResource (unwrapped, spec shapes incl. line snapshots, totals, available_shipping_methods), Admin ProductListResource/ProductResource/OrderListResource/OrderResource/FulfillmentResource/RefundResource (data-wrapped). Admin Developers page (`/admin/developers`, Admin\Developers\Index, sidebar auto-enabled): token table (name, ability badges, last used, created, revoke with wire:confirm), generate-token modal (name + 18 ability checkboxes from App\Support\TokenAbilities), plaintext shown once in amber callout; StorePolicy::manageDevelopers gates to owner/admin (staff 403); webhooks section deferred to Phase 10. Storefront search/analytics endpoints deferred to Phases 8/9 per roadmap; GET /orders/{orderNumber} tokenized order status endpoint deferred with them. Tests exactly per roadmap tables (SanctumTokenTest 5, CartApiTest 8 incl. 121-request rate limit, StorefrontCartApiTest 8, StorefrontCheckoutApiTest 9, AdminProductApiTest 7 incl. default page size 15, AdminOrderApiTest 6) plus Admin/DevelopersTest (5). Full suite 386 passed, 0 todos; migrate:fresh --seed ok; pint clean.
- 2026-06-10: Phases 8+9 done (Search + Analytics). Search: migrations `search_settings` (store_id PK, synonyms_json/stop_words_json), `search_queries` (store-scoped log with results_count), and a raw-SQL `products_fts` FTS5 virtual table (CREATE VIRTUAL TABLE IF NOT EXISTS, sqlite-driver-guarded, rowid = product id so upserts are delete+insert by rowid; KEY QUIRK: FTS5 stores column values as text, so the UNINDEXED `store_id` is always filtered with `CAST(store_id AS INTEGER) = ?` because an integer binding/literal silently matches nothing). `SearchService`: `search()` (tokenize -> stop-word removal (falls back to original tokens if all are stop words) -> synonym OR-groups from search_settings with multi-word synonyms as quoted phrases -> trailing prefix `*` on the last token; MATCH subquery joined via joinSub so `fts.rank` drives relevance; filters vendor(s)/product_types/collection_id/price range (minor units)/in_stock/tags; sorts relevance/price_asc/price_desc/newest/best_selling; published+published_at scoping at query time per spec 05 16.3; every search logged to search_queries + a `search` analytics event, with a `logQuery` flag so Livewire filter/pagination re-renders only log when the query text changes), `autocomplete()` (min prefix 2), `countMatches()`, `facetValues()`, `syncProduct()`/`removeProduct()`/`reindexStore()` (last-indexed timestamp kept in cache). `ProductObserver` (created/updated -> sync, deleted -> remove) registered in AppServiceProvider, so seeded/created products index themselves; SearchSettingsSeeder also reindexes both stores defensively. Storefront UI: `Storefront\Search\Modal` (global in storefront layout, opened by the header icon dispatching `open-search-modal`, 300ms debounce, products max 5 + collections max 5 + "View all N results", skeleton loading, Alpine arrow-key/Enter/Escape keyboard nav, dialog/listbox ARIA) and `Storefront\Search\Index` at GET /search (?q= via #[Url], same filter sidebar partial + sort + pagination as collection pages, result heading 'N results for "q"', empty state with retry input); 404 page got the spec search form. Storefront API: GET /api/storefront/v1/search (spec JSON with results/facets/pagination, filters as URL-encoded JSON) and /search/suggest (products + collections), both `throttle:search`. Admin: `Admin\Search\Settings` at /admin/search/settings (synonym group rows add/remove, comma-separated stop words textarea, Reindex now button calling reindexStore synchronously with last-indexed timestamp; viewSettings/updateSettings policies so staff 403). Analytics: migrations `analytics_events` (unique (store_id, client_event_id) dedupe, occurred_at, properties_json) and `analytics_daily` (composite PK store_id+date). `AnalyticsService` (`track()` swallowing UniqueConstraintViolationException for idempotent ingestion, `getDailyMetrics()`, `eventCountsBetween()` for funnels). `AggregateAnalytics` job (optional Y-m-d constructor arg, defaults to yesterday; per-store recompute + upsert so reruns are idempotent; orders/revenue/AOV from checkout_completed event `total_amount` properties, visits = distinct page_view sessions; scheduled dailyAt 01:00 in routes/console.php). Ingestion endpoint POST /api/storefront/v1/analytics/events (`throttle:analytics`, batch 1-50, occurred_at within +/-1h, 202 {accepted, rejected} where dupes count as rejected, customer guard picked up via $request->user('customer')). Server-side tracking: product_view in Products\Show::mount, add_to_cart in Products\Show::addToCart, remove_from_cart in InteractsWithCart::removeLine, checkout_started in CheckoutService::createFromCart, checkout_completed via new `LogOrderAnalyticsEvent` listener on OrderCreated (spec 05 17); page_view via a tiny vanilla fetch snippet in the storefront layout (sessionStorage session id, crypto.randomUUID client_event_id). `Admin\Analytics\Index` at /admin/analytics (today/7d/30d/custom ranges, KPI tiles with vs-previous-period badges from analytics_daily, SVG sales chart reusing the Phase 7a dependency-free geometry approach, funnel page_view->product_view->add_to_cart->checkout_started->checkout_completed from raw events with proportional Tailwind bars, top products from order_lines with % of total, top referrers table attributing sessions to first page_view referrer host with per-source conversion); StorePolicy::viewAnalytics owner/admin/staff (support 403); sidebar Analytics item auto-enabled via Route::has(). Dashboard integration point switched: conversion rate now uses analytics_daily visits for the range, falling back to the cart-based approximation when a store has no analytics data. Seeders: AnalyticsSeeder (31 daily rows with ~3% growth + 220 events across ~35 sessions/7 days with spec type distribution and 30% customer attribution) + SearchSettingsSeeder (spec 07 3.18 synonym/stop-word sets for both stores), wired into DatabaseSeeder. Tests exactly per roadmap tables (SearchTest 5, AutocompleteTest 3, EventIngestionTest 5, AggregationTest 3) plus Storefront/SearchUiTest (8 incl. both search APIs), Admin/AnalyticsTest (7 incl. role matrix), Admin/SearchSettingsTest (5 incl. reindex rebuild + staff 403). Full suite 422 passed, 0 todos; migrate:fresh --seed ok (25 products indexed, 31 daily rows, 220 events); pint clean; npm run build ok; live-smoke-tested search page (synonym "tee" finds t-shirts), suggest API, and analytics ingestion (202) on shop.test.
- 2026-06-10: Phase 10 done (Apps and Webhooks). Migrations per spec 01 Epic 8: apps (name/status/created_at), app_installations (unique store+app, scopes_json, active/suspended/uninstalled, installed_at), oauth_clients (client_id unique, encrypted secret, redirect URIs), oauth_tokens (hashed access/refresh, expires_at), webhook_subscriptions (store-scoped, nullable app_installation_id, event_type, target_url, encrypted signing secret, active/paused/disabled, plus a `consecutive_failures` counter column - the one deliberate addition over spec 01 because spec 05 13.4 requires a persisted circuit breaker counter that resets on success), webhook_deliveries (event_id, attempt_count, pending/success/failed, last_attempt_at, response_code, response_body_snippet). OAuth2 flow itself stays deferred per roadmap tech decisions: tables + models + factories only, no Passport. Models: `App\Models\App` (explicit $table='apps', UPDATED_AT=null; import as `App as AppModel` where the facade collides), AppInstallation, OauthClient, OauthToken, WebhookSubscription (BelongsToStore, 'encrypted' cast on signing_secret_encrypted, latestDelivery ofMany relation), WebhookDelivery; 4 new enums (AppStatus, AppInstallationStatus, WebhookSubscriptionStatus, WebhookDeliveryStatus). `WebhookService`: EVENT_TYPES catalog (order.created/paid/fulfilled/cancelled/refunded, product.created/updated/deleted, checkout.completed), dispatch(store,eventType,payload) queries active subscriptions withoutGlobalScope + explicit store_id (queue context has no current_store), creates pending deliveries (attempt_count 0) sharing one event UUID, queues DeliverWebhook with a versioned envelope {id, event, api_version:'v1' per spec 02 sec 9, store_id, created_at, data}; sign() HMAC-SHA256 hex; verify() hash_equals timing-safe. `DeliverWebhook` job: tries=6, backoff [60,300,1800,7200,43200] (spec 05 13.3); POSTs JSON via Http facade with X-Platform-Signature/-Event/-Delivery-Id (fresh UUID per attempt)/-Timestamp headers; DB attempt_count is the attempt source of truth (incremented per run, robust under sync driver); records response code + 500-char body snippet + last_attempt_at; non-2xx/connection error throws RuntimeException to trigger queue retry except on the 6th attempt which dead-letters as failed; circuit breaker increments subscription.consecutive_failures per failed attempt, pauses at 5 with a logged warning, success resets to 0; paused/disabled/missing subscriptions cause pending deliveries to be marked failed and skipped. Event wiring: `DispatchWebhooks` listener with a union-typed handle (auto-discovered for all six events, verified via event:list) maps OrderCreated/OrderPaid/OrderFulfilled/OrderCancelled/OrderRefunded (payload incl. refund block)/CheckoutCompleted; product.created/updated/deleted dispatched from ProductObserver (product changes are model events, not domain event classes; archiving via status change emits product.deleted per spec 05 13.1). Admin UI: `Admin\Apps\Index` at /admin/apps (installed app cards with icon/installed-ago/status badge/uninstall linking to detail, plus available-apps directory with Install; install reactivates uninstalled rows, uninstall flips status and disables the installation's webhook subscriptions), `Admin\Apps\Show` at /admin/apps/{installation} (granted scopes badges, webhook subscriptions table with last delivery + response code badge, usage panel with token count and "Never" for API calls since OAuth is stubbed, uninstall), both behind new StorePolicy::manageApps (owner/admin per spec 05 1.3 matrix); sidebar Apps item auto-enabled via existing Route::has(). Developers page webhooks section replaces the placeholder: subscription table (event type, URL, status badge active=green/paused=red, last delivery + response code), Add/Edit modal (event type select from EVENT_TYPES, URL input, secret `whsec_` + 32 random chars generated server-side and shown once in an amber callout), pause/resume (resume resets consecutive_failures per spec 05 13.4 manual re-enable), delete with confirm, recent-deliveries panel (last 10 attempts with status/response/attempts/timestamp). AppSeeder (idempotent, wired into DatabaseSeeder): 3 registry apps each with an oauth client, Loyalty Rewards installed on acme-fashion with an order.created subscription, one store-level order.paid subscription, sample success/pending deliveries. Tests exactly per roadmap tables: Webhooks/WebhookDeliveryTest (5: delivery via real OrderCreated event + Http::fake, HMAC header matches hash_hmac of raw body, retry increments attempt_count + asserts tries/backoff config, 6th attempt dead-letters as failed, 5 consecutive failures pause the subscription and paused subscriptions get no new deliveries) and Webhooks/WebhookSignatureTest (4: known-hash signature, verify round-trip, tampered payload rejected, wrong secret rejected); plus Admin/AppsTest (7: render, install, reinstall reactivation, uninstall disables subscriptions, detail page scopes+webhooks, cross-store 404, staff 403/admin 200) and 6 new Admin/DevelopersTest cases (create with one-time whsec_ secret, event-type/URL validation, edit prefill + update without secret rotation, pause/resume resets failures, delete, deliveries panel render). Full suite 444 passed, 0 todos; migrate:fresh --seed ok (3 apps, 1 installation, 3 oauth clients, 2 subscriptions, 4 deliveries); pint clean.
- 2026-06-10: Phase 11 done (Polish). Seeder audit against spec 07: all 19 seeders matched the spec'd dataset (2 stores, 4 domains incl. shop.test, 5 users/roles, 25 products with full option/variant/inventory graphs, 6 collections, 5 discounts, 12 customers, 18 orders incl. the full status matrix, themes/pages/navigation, 31 analytics_daily rows + 220 events, search settings, apps/webhooks); one gap fixed: Mechanical Keyboard now assigned to the Electronics "Featured" collection per spec 07 section 4. A11y: admin layout gained a skip link + main#main-content (storefront already had one); cart drawer and mobile nav drawer now move focus into the panel on open; account address modal focuses its first control on open; sr-only aria-live cart-count region added to the header; aria-labels added to the admin theme color inputs and the product media alt-text editor. Responsive + dark mode audits found no real gaps (all admin tables wrapped in overflow-x-auto, storefront tables have mobile card alternatives, only intentional light-only classes remain). Error pages: generic styled errors/404, 419, 500, 503 views added (admin/non-tenant contexts; storefront keeps its themed 404/503 via the exception render hooks, API keeps JSON envelopes). Structured logging: new LogStructuredBusinessEvent listener writes order.created/order.paid/order.cancelled/order.refunded to the JSON `structured` channel; payment.failed logged in CheckoutService on declined charges; webhook.delivery_failed logged in DeliverWebhook when attempts are exhausted. New tests: Orders/StructuredLoggingTest (3) and tests/Feature/SmokeTest.php (3 tests iterating every public storefront GET route, customer account pages, and 29 admin pages against the demo seed). phpunit.xml memory_limit raised to 512M (full-suite smoke seeding exceeded the 128M default). Verification checklist: composer install clean, npm run build clean, migrate:fresh --seed clean, route:list 93 routes no duplicates, config:cache + config:clear OK, optimize:clear OK, pint clean on whole codebase, full suite 450 passed / 0 todos.
- 2026-06-10: Phase 12 part 1 done (browser test infrastructure + suites 1, 2, 7, 8, 9 of spec 08). Installed pestphp/pest-plugin-browser ^4.3 (Playwright 1.59 already present via node_modules/MCP). Infrastructure decisions: Pest's browser plugin serves the Laravel app IN-PROCESS (Amp socket server on 127.0.0.1:random-port inside the test process), so the existing phpunit.xml in-memory SQLite + RefreshDatabase setup is shared by the browser-issued HTTP requests and stays untouched - all 450 existing tests keep their config. `.env.testing` created per spec 08 (APP_ENV/APP_URL/PAYMENT_PROVIDER=mock/MAIL_MAILER=array/QUEUE_CONNECTION=sync + APP_KEY copied from .env because creating .env.testing stops Laravel from falling back to .env); phpunit.xml `<env>` values still win for the test run (DB stays :memory:). Host/tenancy strategy: the spec's "browse acme-fashion.test via Herd" approach doesn't work because the plugin's Amp DNS resolver bypasses macOS /etc/resolver (Herd dnsmasq), so instead the Browser suite's beforeEach seeds the full DatabaseSeeder and registers an extra store_domains row mapping hostname `127.0.0.1` to the Acme Fashion store - ResolveStore resolves the tenant through the real domain-table mechanism. `Http::fake()` in the Browser beforeEach prevents the seeded webhook subscriptions (AppSeeder) from making real outbound HTTP on the sync queue during checkout completion (was 500ing the pay step). tests/Browser registered in phpunit.xml testsuites + tests/Pest.php (TestCase + RefreshDatabase + seeded beforeEach); browser helpers added to Pest.php (browserLoginAsAdmin, browserAddClassicTeeToCart, browserFillCheckoutAddress, browserReachCheckoutPaymentStep). App changes made to satisfy spec 08 (spec-vs-implementation deviations fixed toward spec): (1) admin login heading/button "Login" -> "Sign in" and customer login "Login" -> "Log in" (specs 08 1.5/1.6/2.x; AdminAuthTest/CustomerAuthTest updated); (2) checkout payment steps 4+5 merged into the single spec-04 "Payment Method & Pay" step - radio fieldset (wire:model.live) + method-specific form/pay button render together, the "Continue to payment" intermediate click is gone, payNow() now calls selectPaymentMethod when the checkout is still shipping_selected and persists a switched method when already payment_selected (Livewire selectPayment action removed; CheckoutUiTest still green); (3) DE postal-code format validation (regex ^\d{5}$) + friendly validationAttributes added to the checkout address step (spec 08 9.7); (4) `novalidate` added to admin login + checkout contact/address forms so server-side validation messages render in the browser instead of native bubbles (spec 08 2.3/2.4/9.5/9.6); password @error block added to admin login; (5) InvalidDiscountException::notFound message "This discount code does not exist." -> "Invalid discount code." per spec 04 section "Invalid discount code" toast/error (CartUiTest updated); (6) ThemeSeeder: acme-fashion gets featured_products_collection_handle=new-arrivals so the home page actually shows Classic Cotton T-Shirt per spec 08 7.1 (default was latest-by-id which excluded product #1). Browser test gotchas learned: bare tag selectors like 'h1' are NOT treated as CSS by the plugin's locator guesser (use 'h1[data-flux-heading]' or other explicit selectors with CSS metacharacters); explicit selectors are strict-mode (scope duplicates like 'nav a:visible:has-text(...)' / 'table [aria-label=...]'); the cart drawer auto-opens on cart-updated after applying a discount and overlays the page Checkout link (close it via [aria-label="Close cart"]). tests/Browser/Screenshots + database/testing.sqlite gitignored. Suites: SmokeTest 10, Admin/AuthenticationTest 10, Storefront/BrowsingTest 15, Storefront/CartTest 12, Storefront/CheckoutTest 13 (incl. magic cards 4242/0002 declined/9995 insufficient, PayPal, bank transfer IBAN/BIC/reference instructions, FLAT5-in-checkout totals 24.98, DE vs US shipping zones). Full suite 510 passed (1641 assertions), 0 todos, ~81s; pint clean. Remaining for part 2: suites 3-6 and 10-18 (83 tests) + Playwright MCP acceptance verification.
- 2026-06-10: Phase 12 part 2 done (browser test suites 3-6 and 10-18, 83 new tests; spec 08 complete at 143 browser tests / 18 files). New files: Browser/Admin/ProductManagementTest (7), OrderManagementTest (11), DiscountManagementTest (6), SettingsTest (7), CollectionManagementTest (3), CustomerManagementTest (3), PageManagementTest (3), AnalyticsTest (3); Browser/Storefront/CustomerAccountTest (12), InventoryTest (4), TenantIsolationTest (5), ResponsiveTest (8), AccessibilityTest (11). New Pest.php helpers: browserLoginAsCustomer, browserOpenAdminOrder, browserCreateFulfillment, switchBrowserTestDomainToStore (re-points the 127.0.0.1 store_domains row at another store and forgets the ResolveStore hostname cache so tenant isolation is exercised through the REAL domain-resolution path in both directions; the plugin's in-process HTTP server always reports 127.0.0.1 as the request host, so a second hostname is impossible). REAL BUG found+fixed: discounts/form.blade.php used a Blade `@if` directive inside the <flux:input> component tag attribute list, which is unsupported by the component-tag compiler and silently broke rendering of the discount value input (admin could never enter a discount value in the browser); replaced with a bound `:max` attribute. Spec-08 alignment changes to the app (noted where spec 03/04 said otherwise): refund toast "Refund issued" -> "Refund processed" (08 4.6; 03's toast table said "Refund issued"), shipping rate save toast -> "Shipping rate saved" (08 6.4), tax save toast -> "Tax settings saved" + "Tax Settings" secondary heading on the taxes tab (08 6.5/6.6; 03 only had generic "Settings saved"), customer register heading/button "Register" -> "Create an account"/"Create account" (per BOTH 04 10.2 and 08 10.1), account-nav dashboard tab "Account" -> "My Account" (08 10.1/10.4 expect visible "My Account"; 04 doesn't spec the tab labels), address book now shows an "Address saved" status message after save (08 10.10/10.11; component previously closed the modal silently). Test-side adaptations to seeded reality (not weakenings): product list paginates at 15 of 20 products sorted by updated_at, so 3.1 sorts by Title and 3.3 uses the admin search; "FAQ" page already seeded so 17.2 supplies a unique handle (faq-e2e); pages list shows "About Us" (seeded title) for 17.3; 3.4 clicks the Active tab explicitly because the app's default product filter is All. Gotchas learned: Playwright fill() does not trigger Alpine x-model on the entangled PDP quantity input (click + Backspace + type() does - used in 11.4 with assertScript on Alpine.$data to prove qty 15 before the over-stock increment); '#1001'-style text starts with '#' so the locator guesser treats it as CSS - click via 'a:has-text("#1001")'; getByText is case-insensitive substring ("Fulfilled" matches "Unfulfilled" - 4.11 asserts the badge text via assertScript; "Visits" matches "unique visits" on analytics); resize() returns AwaitableWebpage so browserFillCheckoutAddress now accepts/returns the union type; keys('body', ...) fails the locator guesser (bare tag) - use 'body:first-of-type'; Flux labels are <ui-label> custom elements associated at runtime, so a11y label checks must accept aria-labelledby. Full suite 593 passed (2054 assertions), 0 todos, ~156s; pint clean.


Your mission is to implement an entire shop system based on the specifications im specs/*. You must do in one go without stopping. You might use sub-agents or team mode! You must test everything via P…


Implement complete shop system with tests and review


Edit

## Final status (2026-06-10)

ALL PHASES COMPLETE. Full suite: 594 passed (2057 assertions), 0 todos - 451 unit/feature + 143 browser tests.
Live Playwright MCP acceptance simulation on shop.test completed: storefront browse, variant selection, cart drawer + discount (WELCOME10), full checkout with mock credit card -> order #1016 confirmed (idempotency verified, single order), search autocomplete, customer registration + account dashboard, admin dashboard/order detail (fulfillment with DHL tracking), bank-transfer confirmation (#1005 -> paid/captured), products, product form, discounts, shipping settings, analytics, theme editor, developers. Zero console errors after fix.
One production bug found during live simulation and fixed: DeliverWebhook threw on the sync queue when a webhook target was unreachable, which 500'd checkout completion (browser tests had masked it with Http::fake). Fix: inline (sync) execution dead-letters after the first failed attempt instead of throwing; background queues keep retry-by-throwing with backoff. Regression test added (WebhookDeliveryTest, 6 cases now).

## Log

- 2026-06-09: Project start.


The file /Users/fabianwesner/Herd/shop/specs/progress.md has been updated successfully. (file state is current in your context — no need to Read it back)

## Final status (2026-06-10)

ALL PHASES COMPLETE. Full suite: 594 passed (2057 assertions), 0 todos - 451 unit/feature + 143 browser tests.
Live Playwright MCP acceptance simulation on shop.test completed: storefront browse, variant selection, cart drawer + discount (WELCOME10), full checkout with mock credit card -> order #1016 confirmed (idempotency verified, single order), search autocomplete, customer registration + account dashboard, admin dashboard/order detail (fulfillment with DHL tracking), bank-transfer confirmation (#1005 -> paid/captured), products, product form, discounts, shipping settings, analytics, theme editor, developers. Zero console errors after fix.
One production bug found during live simulation and fixed: DeliverWebhook threw on the sync queue when a webhook target was unreachable, which 500'd checkout completion (browser tests had masked it with Http::fake). Fix: inline (sync) execution dead-letters after the first failed attempt instead of throwing; background queues keep retry-by-throwing with backoff. Regression test added (WebhookDeliveryTest, 6 cases now).

## Log

- 2026-06-09: Project start.

| 11 | Polish: seeder audit vs spec 07, a11y (skip links, focus management, aria-live, labels), responsive + dark mode pass, error pages (404/419/500/503), structured business-event logging, SmokeTest | DONE | full suite 450 passed, 0 todos; Orders/StructuredLoggingTest (3), SmokeTest (3 tests, ~50 routes) |

| 12 | Full suite + browser tests (spec 08) + Playwright MCP acceptance verification | DONE: all 18 suites, 143 browser tests complete | full suite 593 passed (2054 assertions), 0 todos; Browser: SmokeTest (10), Admin/AuthenticationTest (10), Admin/ProductManagementTest (7), Admin/OrderManagementTest (11), Admin/DiscountManagementTest (6), Admin/SettingsTest (7), Admin/CollectionManagementTest (3), Admin/CustomerManagementTest (3), Admin/PageManagementTest (3), Admin/AnalyticsTest (3), Storefront/BrowsingTest (15), CartTest (12), CheckoutTest (13), CustomerAccountTest (12), InventoryTest (4), TenantIsolationTest (5), ResponsiveTest (8), AccessibilityTest (11) |

+ALL PHASES COMPLETE. Full suite: 594 passed (2057 assertions), 0 todos - 451 unit/feature + 143 browser tests.

+Live Playwright MCP acceptance simulation on shop.test completed: storefront browse, variant selection, cart drawer + discount (WELCOME10), full checkout with mock credit card -> order #1016 confirmed (idempotency verified, single order), search autocomplete, customer registration + account dashboard, admin dashboard/order detail (fulfillment with DHL tracking), bank-transfer confirmation (#1005 -> paid/captured), products, product form, discounts, shipping settings, analytics, theme editor, developers. Zero console errors after fix.

+One production bug found during live simulation and fixed: DeliverWebhook threw on the sync queue when a webhook target was unreachable, which 500'd checkout completion (browser tests had masked it with Http::fake). Fix: inline (sync) execution dead-letters after the first failed attempt instead of throwing; background queues keep retry-by-throwing with backoff. Regression test added (WebhookDeliveryTest, 6 cases now).

- 2026-06-09: Project start. Fresh Livewire starter kit (Fortify). Created progress tracker, started Phase 1.


Bash

git add -A && git commit -q -m "Fix: webhook delivery on sync queue dead-letters instead of throwing into the triggering request

Found via live Playwright simulation: an unreachable webhook target 500'd
checkout completion because DeliverWebhook's retry-by-throwing propagated
through the sync queue into the customer's request. Inline execution now
records the failure and returns; background queues keep backoff retries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>" && git log --oneline | head -3 && git log --oneline | wc -l

Commit webhook fix and final progress


872ac91e Fix: webhook delivery on sync queue dead-letters instead of throwing into the triggering request
f9e5c73e Phase 12b: Browser suites 3-6, 10-18 (83 tests, 143 total); fix discount value input rendering bug, spec 08 label alignments
1734bc9e Phase 12a: Pest browser test infrastructure + suites 1,2,7,8,9 (60 tests); checkout single pay step, validation fixes
      18


All work is committed. One memory worth saving for future sessions, then the review meeting.


Write

---
name: project-shop-live-verification-gotchas
description: Shop project - test-environment fakes can mask live bugs; always verify checkout on shop.test, and browser tests run in-process on 127.0.0.1
metadata:
  type: project
---

In the shop project, the Pest browser suite uses `Http::fake()` in its Browser `beforeEach`, which masked a real production bug: `DeliverWebhook` on the sync queue threw into the checkout request when the seeded webhook target was unreachable, 500ing "Pay now" on the live Herd site while all 143 browser tests were green (fixed 2026-06-10, commit 872ac91e).

**Why:** Test-environment fakes (Http, Queue, Mail) hide failures on the real sync-queue/HTTP path.

**How to apply:** After test-suite green, always do a live Playwright MCP pass on http://shop.test covering checkout completion. Also: Pest browser tests serve the app in-process on 127.0.0.1 (a seeded `store_domains` row maps 127.0.0.1 to Acme Fashion); Herd hostnames like acme-fashion.test are not resolvable from the plugin. Run the suite unsandboxed (`socket_create_listen` is blocked by the sandbox). Related: [[project_shop_stale_compiled_views]].


File created successfully at: /Users/fabianwesner/.claude/projects/-Users-fabianwesner-Herd-shop/memory/project_shop_live_verification_gotchas.md (file state is current in your context — no need to Read it back)

---
name: project-shop-live-verification-gotchas
description: "Shop project - test-environment fakes can mask live bugs; always verify checkout on shop.test, and browser tests run in-process on 127.0.0.1"
metadata:
  node_type: memory
  type: project
  originSessionId: 8088dd0b-29e3-446e-a9be-e76d2404af3a
---

In the shop project, the Pest browser suite uses `Http::fake()` in its Browser `beforeEach`, which masked a real production bug: `DeliverWebhook` on the sync queue threw into the checkout request when the seeded webhook target was unreachable, 500ing "Pay now" on the live Herd site while all 143 browser tests were green (fixed 2026-06-10, commit 872ac91e).

**Why:** Test-environment fakes (Http, Queue, Mail) hide failures on the real sync-queue/HTTP path.

**How to apply:** After test-suite green, always do a live Playwright MCP pass on http://shop.test covering checkout completion. Also: Pest browser tests serve the app in-process on 127.0.0.1 (a seeded `store_domains` row maps 127.0.0.1 to Acme Fashion); Herd hostnames like acme-fashion.test are not resolvable from the plugin. Run the suite unsandboxed (`socket_create_listen` is blocked by the sandbox). Related: [[project_shop_stale_compiled_views]].


Bash

cat >> /Users/fabianwesner/.claude/projects/-Users-fabianwesner-Herd-shop/memory/MEMORY.md <<'EOF'
- [project_shop_live_verification_gotchas.md](project_shop_live_verification_gotchas.md) - Http::fake masked a live checkout 500 (webhook sync-queue bug); always live-verify checkout on shop.test after green suites; browser tests run in-process on 127.0.0.1, run unsandboxed.
EOF

Index new memory in MEMORY.md


Error: Exit code 1
(eval):1: operation not permitted: /Users/fabianwesner/.claude/projects/-Users-fabianwesner-Herd-shop/memory/MEMORY.md


<system-reminder>This memory is 9 days old. Memories are point-in-time observations, not live state — claims about code behavior or file:line citations may be outdated. Verify against current code before asserting as fact.</system-reminder>
1	# Memory Index
2
3	## Feedback
4	- [feedback_never_skip_verification.md](feedback_never_skip_verification.md) - Never skip browser verification, phase gating, or plan mode review. Learned from first implementation attempt.
5	- [feedback_consistent_standards.md](feedback_consistent_standards.md) - Do not lower quality gate standards due to momentum or fatigue at the finish line. Learned from granting final approval despite 143 missing browser test files.
6
7	## Project
8	- [project_shop_foundation_conventions.md](project_shop_foundation_conventions.md) - Phase 1 foundation conventions (BelongsToStore, money/enums, 3 guards, policies, Pest helpers, DemoStoreSeeder).
9	- [project_storefront_conventions.md](project_storefront_conventions.md) - Phase 3 storefront conventions: x-storefront:: component namespace, PriceFormatter, ProductCardPresenter, Theme/Navigation services, StorefrontSeeder.
10	- [project_shop_starter_kit_auth_decision.md](project_shop_starter_kit_auth_decision.md) - What was removed/adapted from the starter kit (password_hash, custom admin auth, deleted starter tests).
11	- [project_shop_stale_compiled_views.md](project_shop_stale_compiled_views.md) - Shared tree: stale compiled Blade views cause phantom failures; run `optimize:clear` before trusting a view-render failure.
12

# Memory Index

## Feedback
- [feedback_never_skip_verification.md](feedback_never_skip_verification.md) - Never skip browser verification, phase gating, or plan mode review. Learned from first implementation attempt.
- [feedback_consistent_standards.md](feedback_consistent_standards.md) - Do not lower quality gate standards due to momentum or fatigue at the finish line. Learned from granting final approval despite 143 missing browser test files.

## Project
- [project_shop_foundation_conventions.md](project_shop_foundation_conventions.md) - Phase 1 foundation conventions (BelongsToStore, money/enums, 3 guards, policies, Pest helpers, DemoStoreSeeder).
- [project_storefront_conventions.md](project_storefront_conventions.md) - Phase 3 storefront conventions: x-storefront:: component namespace, PriceFormatter, ProductCardPresenter, Theme/Navigation services, StorefrontSeeder.
- [project_shop_starter_kit_auth_decision.md](project_shop_starter_kit_auth_decision.md) - What was removed/adapted from the starter kit (password_hash, custom admin auth, deleted starter tests).
- [project_shop_stale_compiled_views.md](project_shop_stale_compiled_views.md) - Shared tree: stale compiled Blade views cause phantom failures; run `optimize:clear` before trusting a view-render failure.