stdout:
--- specs/01-DATABASE-SCHEMA.md ---
1:# 01 - Database Schema Specification
7:## Conventions
23:## Epic 1 - Foundation and Multi-Store Tenancy
25:### organizations
37:**Indexes:**
39:- `idx_organizations_billing_email` on (`billing_email`) - Supports lookup by billing email
43:### stores
60:**Indexes:**
62:- `idx_stores_handle` UNIQUE on (`handle`) - Ensures globally unique store handles for URL routing
63:- `idx_stores_organization_id` on (`organization_id`) - FK lookup and listing stores by org
64:- `idx_stores_status` on (`status`) - Filtering stores by active/suspended status
68:### store_domains
82:**Indexes:**
84:- `idx_store_domains_hostname` UNIQUE on (`hostname`) - Globally unique hostname for routing
85:- `idx_store_domains_store_id` on (`store_id`) - FK lookup, listing domains per store
86:- `idx_store_domains_store_primary` on (`store_id`, `is_primary`) - Quick lookup of a store's primary domain
90:### users
110:**Indexes:**
112:- `idx_users_email` UNIQUE on (`email`) - Enforces unique email, supports login lookup
113:- `idx_users_status` on (`status`) - Filtering users by active/disabled
115:**Notes:** The `two_factor_*` columns align with the existing migration `2025_08_14_170933_add_two_factor_columns_to_users_table.php`. The `password_hash` column maps to Laravel's default `password` field internally but stores a bcrypt/argon2 hash.
119:### store_users
130:**Primary Key:** (`store_id`, `user_id`)
132:**Indexes:**
134:- `idx_store_users_user_id` on (`user_id`) - Reverse lookup: find all stores for a user
135:- `idx_store_users_role` on (`store_id`, `role`) - Listing users by role within a store
139:### store_settings
149:**Notes:** No additional indexes needed - primary key on `store_id` covers all queries.
153:## Epic 2 - Catalog, Collections, Inventory, and Media
155:### products
174:**Indexes:**
176:- `idx_products_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store for URL routing
177:- `idx_products_store_id` on (`store_id`) - FK lookup, listing all products in a store
178:- `idx_products_store_status` on (`store_id`, `status`) - Filtering products by status within a store
179:- `idx_products_published_at` on (`store_id`, `published_at`) - Sorting/filtering by publication date
180:- `idx_products_vendor` on (`store_id`, `vendor`) - Filtering products by vendor
181:- `idx_products_product_type` on (`store_id`, `product_type`) - Filtering products by type
183:**Notes:** `tags` is a JSON-encoded array (e.g. `["summer","sale"]`). The `handle` is unique per store, not globally.
187:### product_options
198:**Indexes:**
200:- `idx_product_options_product_id` on (`product_id`) - FK lookup, listing options per product
201:- `idx_product_options_product_position` UNIQUE on (`product_id`, `position`) - Enforces unique ordering per product
205:### product_option_values
216:**Indexes:**
218:- `idx_product_option_values_option_id` on (`product_option_id`) - FK lookup, listing values per option
219:- `idx_product_option_values_option_position` UNIQUE on (`product_option_id`, `position`) - Enforces unique ordering per option
223:### product_variants
244:**Indexes:**
246:- `idx_product_variants_product_id` on (`product_id`) - FK lookup, listing variants per product
247:- `idx_product_variants_sku` on (`sku`) - SKU lookup across all variants
248:- `idx_product_variants_barcode` on (`barcode`) - Barcode scanning lookup
249:- `idx_product_variants_product_position` on (`product_id`, `position`) - Ordered variant listing
250:- `idx_product_variants_product_default` on (`product_id`, `is_default`) - Quick lookup of default variant
252:**Notes:** `price_amount` and `compare_at_amount` are in minor currency units (cents). A product always has at least one variant (the default).
256:### variant_option_values
265:**Primary Key:** (`variant_id`, `product_option_value_id`)
267:**Indexes:**
269:- `idx_variant_option_values_value_id` on (`product_option_value_id`) - Reverse lookup: find all variants with a given option value
271:**Notes:** This is a junction table that links variants to the specific option values they represent. Required to resolve which combination of options each variant corresponds to.
275:### inventory_items
288:**Indexes:**
290:- `idx_inventory_items_variant_id` UNIQUE on (`variant_id`) - Enforces one inventory record per variant, fast variant lookup
291:- `idx_inventory_items_store_id` on (`store_id`) - Listing all inventory for a store
293:**Notes:** `quantity_on_hand` is the total physical stock. `quantity_reserved` is stock allocated to open orders not yet fulfilled. Available = on_hand - reserved. Policy `deny` prevents overselling; `continue` allows selling below zero.
297:### collections
313:**Indexes:**
315:- `idx_collections_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store for URL routing
316:- `idx_collections_store_id` on (`store_id`) - FK lookup, listing all collections in a store
317:- `idx_collections_store_status` on (`store_id`, `status`) - Filtering collections by status
321:### collection_products
331:**Primary Key:** (`collection_id`, `product_id`)
333:**Indexes:**
335:- `idx_collection_products_product_id` on (`product_id`) - Reverse lookup: find all collections containing a product
336:- `idx_collection_products_position` on (`collection_id`, `position`) - Ordered product listing within a collection
340:### product_media
359:**Indexes:**
361:- `idx_product_media_product_id` on (`product_id`) - FK lookup, listing media per product
362:- `idx_product_media_product_position` on (`product_id`, `position`) - Ordered media listing
363:- `idx_product_media_status` on (`status`) - Finding media by processing status
367:## Epic 3 - Themes, Storefront, and Localization
369:### themes
384:**Indexes:**
386:- `idx_themes_store_id` on (`store_id`) - FK lookup, listing themes per store
387:- `idx_themes_store_status` on (`store_id`, `status`) - Finding published/draft themes per store
391:### theme_files
404:**Indexes:**
406:- `idx_theme_files_theme_path` UNIQUE on (`theme_id`, `path`) - Enforces unique file paths per theme
407:- `idx_theme_files_theme_id` on (`theme_id`) - FK lookup, listing files per theme
411:### theme_settings
423:### pages
439:**Indexes:**
441:- `idx_pages_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store for URL routing
442:- `idx_pages_store_id` on (`store_id`) - FK lookup, listing pages per store
443:- `idx_pages_store_status` on (`store_id`, `status`) - Filtering pages by status
447:### navigation_menus
460:**Indexes:**
462:- `idx_navigation_menus_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store
463:- `idx_navigation_menus_store_id` on (`store_id`) - FK lookup, listing menus per store
467:### navigation_items
481:**Indexes:**
483:- `idx_navigation_items_menu_id` on (`menu_id`) - FK lookup, listing items per menu
484:- `idx_navigation_items_menu_position` on (`menu_id`, `position`) - Ordered item listing
486:**Notes:** When `type` is `link`, the `url` field is used directly. When `type` is `page`, `collection`, or `product`, the `resource_id` points to the respective table's `id`. The URL is resolved at render time.
490:## Epic 4 - Search
492:### search_settings
505:### search_queries
518:**Indexes:**
520:- `idx_search_queries_store_id` on (`store_id`) - FK lookup
521:- `idx_search_queries_store_created` on (`store_id`, `created_at`) - Time-range queries per store
522:- `idx_search_queries_store_query` on (`store_id`, `query`) - Autocomplete and popular search lookups
526:## Epic 5 - Cart, Checkout, Discounts, Shipping, and Taxes
528:### carts
543:**Indexes:**
545:- `idx_carts_store_id` on (`store_id`) - FK lookup
546:- `idx_carts_customer_id` on (`customer_id`) - Finding carts by customer
547:- `idx_carts_store_status` on (`store_id`, `status`) - Filtering carts by status (e.g. abandoned cart recovery)
549:**Notes:** `cart_version` is incremented on every mutation and used for optimistic concurrency control.
553:### cart_lines
568:**Indexes:**
570:- `idx_cart_lines_cart_id` on (`cart_id`) - FK lookup, listing lines per cart
571:- `idx_cart_lines_cart_variant` UNIQUE on (`cart_id`, `variant_id`) - Prevents duplicate variants in the same cart
573:**Notes:** All `_amount` columns are in minor units (cents). `line_subtotal_amount = unit_price_amount * quantity`. `line_total_amount = line_subtotal_amount - line_discount_amount`.
577:### checkouts
600:**Indexes:**
602:- `idx_checkouts_store_id` on (`store_id`) - FK lookup
603:- `idx_checkouts_cart_id` on (`cart_id`) - Finding checkout for a cart
604:- `idx_checkouts_customer_id` on (`customer_id`) - Listing checkouts per customer
605:- `idx_checkouts_status` on (`store_id`, `status`) - Filtering checkouts by status
606:- `idx_checkouts_expires_at` on (`expires_at`) - Cleanup of expired checkouts
608:**Notes:** `totals_json` contains a snapshot: `{"subtotal": 5000, "discount": 500, "shipping": 799, "tax": 430, "total": 5729}`. `tax_provider_snapshot_json` stores the full tax calculation response for audit purposes.
612:### shipping_zones
624:**Indexes:**
626:- `idx_shipping_zones_store_id` on (`store_id`) - FK lookup, listing zones per store
628:**Notes:** `countries_json` is an array of ISO 3166-1 alpha-2 codes. `regions_json` is an array of region/state codes.
632:### shipping_rates
645:**Indexes:**
647:- `idx_shipping_rates_zone_id` on (`zone_id`) - FK lookup, listing rates per zone
648:- `idx_shipping_rates_zone_active` on (`zone_id`, `is_active`) - Finding active rates per zone
650:**Notes:** `config_json` structure depends on `type`:
651:- Flat: `{"amount": 799}`
652:- Weight: `{"ranges": [{"min_g": 0, "max_g": 500, "amount": 499}]}`
653:- Price: `{"ranges": [{"min_amount": 0, "max_amount": 5000, "amount": 799}]}`
654:- Carrier: `{"carrier": "ups", "service": "ground"}`
658:### tax_settings
672:### discounts
693:**Indexes:**
695:- `idx_discounts_store_code` UNIQUE on (`store_id`, `code`) - Unique discount code per store
696:- `idx_discounts_store_id` on (`store_id`) - FK lookup
697:- `idx_discounts_store_status` on (`store_id`, `status`) - Filtering discounts by status
698:- `idx_discounts_store_type` on (`store_id`, `type`) - Filtering by code vs automatic
700:**Notes:** `value_amount` interpretation depends on `value_type`. For `percent`, it is a whole number (e.g. 15 = 15%). For `fixed`, it is in minor units (cents). For `free_shipping`, it is ignored. `rules_json` may contain minimum purchase amounts, applicable product/collection IDs, customer eligibility, etc.
704:## Epic 6 - Payments, Orders, Fulfillment, and Customers
706:### customers
721:**Indexes:**
723:- `idx_customers_store_email` UNIQUE on (`store_id`, `email`) - Unique email per store
724:- `idx_customers_store_id` on (`store_id`) - FK lookup, listing customers per store
726:**Notes:** `password_hash` is nullable to allow guest checkout customers who may later claim their account. Email is unique per store, not globally.
730:### customer_addresses
742:**Indexes:**
744:- `idx_customer_addresses_customer_id` on (`customer_id`) - FK lookup, listing addresses per customer
745:- `idx_customer_addresses_default` on (`customer_id`, `is_default`) - Quick lookup of default address
747:**Notes:** `address_json` structure: `{"first_name": "", "last_name": "", "company": "", "address1": "", "address2": "", "city": "", "province": "", "province_code": "", "country": "", "country_code": "", "zip": "", "phone": ""}`.
751:### orders
778:**Indexes:**
780:- `idx_orders_store_order_number` UNIQUE on (`store_id`, `order_number`) - Unique order number per store
781:- `idx_orders_store_id` on (`store_id`) - FK lookup
782:- `idx_orders_customer_id` on (`customer_id`) - Listing orders per customer
783:- `idx_orders_store_status` on (`store_id`, `status`) - Filtering by overall status
784:- `idx_orders_store_financial` on (`store_id`, `financial_status`) - Filtering by payment status
785:- `idx_orders_store_fulfillment` on (`store_id`, `fulfillment_status`) - Filtering by shipping status
786:- `idx_orders_placed_at` on (`store_id`, `placed_at`) - Date-range order queries
790:### order_lines
808:**Indexes:**
810:- `idx_order_lines_order_id` on (`order_id`) - FK lookup, listing lines per order
811:- `idx_order_lines_product_id` on (`product_id`) - Finding order lines by product
812:- `idx_order_lines_variant_id` on (`variant_id`) - Finding order lines by variant
814:**Notes:** `product_id` and `variant_id` are nullable with SET NULL on delete so that order history survives product deletion. `tax_lines_json` example: `[{"title": "State Tax", "rate": 800, "amount": 400}]` (rate in basis points, e.g. 800 = 8%). `discount_allocations_json` example: `[{"discount_id": 5, "amount": 250}]`.
818:### payments
835:**Indexes:**
837:- `idx_payments_order_id` on (`order_id`) - FK lookup, listing payments per order
838:- `idx_payments_provider_id` on (`provider`, `provider_payment_id`) - Payment reconciliation by provider ID
839:- `idx_payments_method` on (`method`) - Filtering payments by method
840:- `idx_payments_status` on (`status`) - Filtering payments by status
842:**Notes:** `raw_json_encrypted` stores the mock payment provider response, encrypted at the application layer. `amount` is in minor units (cents). The `provider` is always `mock` since the platform uses a mock PSP (no real payment gateway integration). The `method` column records which payment method the customer selected (credit_card, paypal, or bank_transfer).
846:### refunds
861:**Indexes:**
863:- `idx_refunds_order_id` on (`order_id`) - FK lookup, listing refunds per order
864:- `idx_refunds_payment_id` on (`payment_id`) - Finding refunds per payment
865:- `idx_refunds_status` on (`status`) - Filtering refunds by status
869:### fulfillments
884:**Indexes:**
886:- `idx_fulfillments_order_id` on (`order_id`) - FK lookup, listing fulfillments per order
887:- `idx_fulfillments_status` on (`status`) - Filtering fulfillments by status
888:- `idx_fulfillments_tracking` on (`tracking_company`, `tracking_number`) - Tracking number lookup
892:### fulfillment_lines
903:**Indexes:**
905:- `idx_fulfillment_lines_fulfillment_id` on (`fulfillment_id`) - FK lookup, listing lines per fulfillment
906:- `idx_fulfillment_lines_fulfillment_order_line` UNIQUE on (`fulfillment_id`, `order_line_id`) - Prevents duplicate line entries per fulfillment
908:**Notes:** An order with multiple line items may be partially fulfilled. This table tracks how many units of each line item are included in each shipment.
912:## Epic 7 - Analytics
914:### analytics_events
930:**Indexes:**
932:- `idx_analytics_events_store_id` on (`store_id`) - FK lookup
933:- `idx_analytics_events_store_type` on (`store_id`, `type`) - Filtering events by type per store
934:- `idx_analytics_events_store_created` on (`store_id`, `created_at`) - Time-range event queries
935:- `idx_analytics_events_session` on (`session_id`) - Session-based event grouping
936:- `idx_analytics_events_customer` on (`customer_id`) - Customer activity history
937:- `idx_analytics_events_client_event` UNIQUE on (`store_id`, `client_event_id`) - Client-side event deduplication
939:**Notes:** Event types include: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. This table will grow fast - consider periodic archiving or partitioning by date for production workloads beyond SQLite.
943:### analytics_daily
959:**Primary Key:** (`store_id`, `date`)
961:**Indexes:**
963:- `idx_analytics_daily_store_date` on (`store_id`, `date`) - Time-range metric queries (covered by PK but explicit for clarity)
965:**Notes:** `date` is an ISO-8601 date string (YYYY-MM-DD). `revenue_amount` and `aov_amount` (average order value) are in minor units (cents). This table is populated by a scheduled aggregation job.
969:## Epic 8 - Apps and Webhooks
971:### apps
982:**Indexes:**
984:- `idx_apps_status` on (`status`) - Filtering apps by status
988:### app_installations
1001:**Indexes:**
--- specs/02-API-ROUTES.md ---
1:# API and Routing Specification
9:## 1. Web Routes (Blade/Livewire Pages)
11:### 1.1 Admin Auth Routes
29:### 1.2 Admin Routes
31:**Prefix:** `/admin`
32:**Middleware:** `web`, `auth`, `verified`, `store.resolve`, `role.check`
38:#### Dashboard
44:#### Products
52:#### Inventory
58:#### Collections
66:#### Orders
73:#### Customers
80:#### Discounts
88:#### Settings
96:#### Themes and Online Store
103:#### Pages
111:#### Navigation
117:#### Apps
124:#### Developers
132:#### Analytics
138:#### Search Settings
146:### 1.3 Storefront Routes
148:**Middleware:** `web`, `store.resolve`
162:#### Customer Account Routes
164:**Additional middleware:** `auth:customer`
173:#### Customer Auth Routes (no auth required)
186:### 1.4 Checkout Routes
188:**Middleware:** `web`, `store.resolve`
197:## 2. Storefront REST API
199:**Prefix:** `/api/storefront/v1`
200:**Middleware:** `store.resolve`, `throttle:api.storefront`
201:**Content-Type:** `application/json`
207:### 2.1 Cart Endpoints
209:#### POST /api/storefront/v1/carts
213:- **Authentication:** None (guest) or session-based (logged-in customer)
214:- **Rate limit:** `api.storefront` (120/min per IP)
216:**Request body:**
228:**Success response:** `201 Created`
250:**Error responses:**
251:- `422` - Validation error (invalid currency)
255:#### GET /api/storefront/v1/carts/{cartId}
259:- **Authentication:** None (cart ID acts as token)
260:- **Rate limit:** `api.storefront` (120/min per IP)
262:**URL parameters:**
268:**Success response:** `200 OK`
308:**Error responses:**
309:- `404` - Cart not found
313:#### POST /api/storefront/v1/carts/{cartId}/lines
317:- **Authentication:** None
318:- **Rate limit:** `api.storefront` (120/min per IP)
320:**Request body:**
334:**Success response:** `201 Created`
338:**Error responses:**
339:- `404` - Cart not found
340:- `409` - Cart version conflict (concurrent modification, retry with fresh cart_version)
341:- `422` - Validation error
354:#### PUT /api/storefront/v1/carts/{cartId}/lines/{lineId}
358:- **Authentication:** None
359:- **Rate limit:** `api.storefront` (120/min per IP)
361:**Request body:**
375:**Success response:** `200 OK`
379:**Error responses:**
380:- `404` - Cart or line not found
381:- `409` - Version conflict
382:- `422` - Validation error (e.g., exceeds inventory)
386:#### DELETE /api/storefront/v1/carts/{cartId}/lines/{lineId}
390:- **Authentication:** None
391:- **Rate limit:** `api.storefront` (120/min per IP)
393:**Request body:**
405:**Success response:** `200 OK`
409:**Error responses:**
410:- `404` - Cart or line not found
411:- `409` - Version conflict
415:### 2.2 Checkout Endpoints
417:#### Checkout Flow Sequence Diagram
458:#### POST /api/storefront/v1/checkouts
462:- **Authentication:** None or session-based
463:- **Rate limit:** `checkout` (10/min per session)
465:**Request body:**
479:**Success response:** `201 Created`
518:**Error responses:**
519:- `404` - Cart not found
520:- `422` - Validation errors (empty cart, invalid email)
524:#### GET /api/storefront/v1/checkouts/{checkoutId}
528:- **Authentication:** None (checkout ID acts as token)
529:- **Rate limit:** `checkout` (10/min per session)
531:**Success response:** `200 OK`
535:**Error responses:**
536:- `404` - Checkout not found
537:- `410` - Checkout expired
541:#### PUT /api/storefront/v1/checkouts/{checkoutId}/address
545:- **Authentication:** None
546:- **Rate limit:** `checkout` (10/min per session)
548:**Request body:**
570:**Shipping address fields:**
589:**Success response:** `200 OK`
654:**Error responses:**
655:- `404` - Checkout not found
656:- `410` - Checkout expired
657:- `422` - Validation errors (missing required fields, invalid country code, address not in any shipping zone)
661:#### PUT /api/storefront/v1/checkouts/{checkoutId}/shipping-method
665:- **Authentication:** None
666:- **Rate limit:** `checkout` (10/min per session)
668:**Request body:**
680:**Success response:** `200 OK`
714:**Error responses:**
715:- `404` - Checkout not found
716:- `410` - Checkout expired
717:- `422` - Invalid shipping method (not available for this address/checkout)
721:#### PUT /api/storefront/v1/checkouts/{checkoutId}/payment-method
725:- **Authentication:** None
726:- **Rate limit:** `checkout` (10/min per session)
728:**Request body:**
740:**Success response:** `200 OK`
760:**Error responses:**
761:- `404` - Checkout not found
762:- `410` - Checkout expired
763:- `422` - Invalid payment method or checkout not in `shipping_selected` state
767:#### POST /api/storefront/v1/checkouts/{checkoutId}/apply-discount
771:- **Authentication:** None
772:- **Rate limit:** `checkout` (10/min per session)
774:**Request body:**
786:**Success response:** `200 OK`
813:**Error responses:**
814:- `400` - Discount code expired (error code: `discount_expired`)
815:- `400` - Discount usage limit reached (error code: `discount_usage_exceeded`)
816:- `404` - Checkout not found
817:- `410` - Checkout expired
818:- `422` - Invalid code (not found, does not apply to these items)
829:#### DELETE /api/storefront/v1/checkouts/{checkoutId}/discount
833:- **Authentication:** None
834:- **Rate limit:** `checkout` (10/min per session)
836:**Success response:** `200 OK`
840:**Error responses:**
841:- `404` - Checkout not found or no discount applied
842:- `410` - Checkout expired
846:### 2.3 Payment Endpoint
848:#### POST /api/storefront/v1/checkouts/{checkoutId}/pay
852:- **Authentication:** None
853:- **Rate limit:** `checkout` (10/min per session)
855:**Request body (Credit Card):**
867:**Request body (PayPal):**
875:**Request body (Bank Transfer):**
891:**Success response (Credit Card / PayPal - instant capture):** `200 OK`
909:**Success response (Bank Transfer - deferred payment):** `200 OK`
934:**Error responses:**
935:- `404` - Checkout not found
936:- `409` - Checkout not in a valid state for payment (must be `payment_selected`)
937:- `422` - Validation errors (missing card fields, invalid payment method)
938:- `422` - Payment declined (credit card magic numbers)
947:**Magic card number error codes:**
956:### 2.4 Order Endpoints
958:#### GET /api/storefront/v1/orders/{orderNumber}
962:- **Authentication:** Tokenized access (`token` query parameter with HMAC signature)
963:- **Rate limit:** `api.storefront` (120/min per IP)
965:**Query parameters:**
971:**Success response:** `200 OK`
1011:**Error responses:**
1012:- `401` - Invalid or missing token
1013:- `404` - Order not found
1017:### 2.5 Search Endpoints
1019:#### GET /api/storefront/v1/search
1023:- **Authentication:** None
1024:- **Rate limit:** `search` (30/min per IP)
1026:**Query parameters:**
1036:**Filters JSON schema:**
1049:**Success response:** `200 OK`
1091:**Error responses:**
1092:- `400` - Query too long (max 200 characters) or malformed filters
1093:- `422` - Validation error
1097:#### GET /api/storefront/v1/search/suggest
1101:- **Authentication:** None
1102:- **Rate limit:** `search` (30/min per IP)
1104:**Query parameters:**
1111:**Success response:** `200 OK`
1135:**Error responses:**
1136:- `400` - Query too long
1137:- `422` - Validation error
1141:### 2.6 Analytics Endpoints
1143:#### POST /api/storefront/v1/analytics/events
1147:- **Authentication:** None
1148:- **Rate limit:** `analytics` (60/min per IP)
1150:**Request body:**
1192:**Success response:** `202 Accepted`
1201:**Error responses:**
1202:- `422` - Validation errors (invalid event types, too many events, etc.)
1206:## 3. Admin REST API
1208:**Prefix:** `/api/admin/v1`
1209:**Middleware:** `auth:sanctum`, `store.resolve`, `throttle:api.admin`
1210:**Content-Type:** `application/json`
1218:### 3.1 Platform Management (Super-Admin Only)
1222:#### POST /api/admin/v1/platform/organizations
1226:- **Authentication:** Sanctum token with `manage-platform` scope
1227:- **Rate limit:** `api.admin` (60/min per token)
1229:**Request body:**
1243:**Success response:** `201 Created`
1257:**Error responses:**
1258:- `403` - Insufficient permissions
1259:- `422` - Validation errors
1263:#### POST /api/admin/v1/platform/stores
1267:- **Authentication:** Sanctum token with `manage-platform` scope
1268:- **Rate limit:** `api.admin` (60/min per token)
1270:**Request body:**
1292:**Success response:** `201 Created`
1310:**Error responses:**
1311:- `403` - Insufficient permissions
1312:- `422` - Validation errors (duplicate handle, invalid timezone, etc.)
1316:#### POST /api/admin/v1/stores/{storeId}/invites
1320:- **Authentication:** Sanctum token with `manage-platform` scope
1321:- **Rate limit:** `api.admin` (60/min per token)
1323:**Request body:**
1337:**Success response:** `201 Created`
1350:**Error responses:**
1351:- `403` - Insufficient permissions (only `owner` and `admin` can invite)
1352:- `409` - User already a member of this store
1353:- `422` - Validation errors
1357:#### GET /api/admin/v1/stores/{storeId}/me
1361:- **Authentication:** Sanctum token
1362:- **Rate limit:** `api.admin` (60/min per token)
1364:**Success response:** `200 OK`
--- specs/03-ADMIN-UI.md ---
1:# 03 - Admin Panel UI/UX Specification
5:**Flux UI Free components available:** avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip
11:## 1. Admin Layout Shell
15:### 1.1 Route Prefix
19:### 1.2 Sidebar Navigation
21:**Livewire component:** `App\Livewire\Admin\Layout\Sidebar`
23:**Blade view:** `resources/views/livewire/admin/layout/sidebar.blade.php`
25:#### Component Requirements
36:#### Wireframe
77:#### Navigation Structure
103:#### Flux Component Mapping
111:#### Responsive Behavior
113:- **Desktop (lg and above):** Sidebar is always visible, fixed left, 256px wide. Main content has a left margin of 256px.
114:- **Mobile (below lg):** Sidebar is hidden off-screen. A hamburger button (icon: bars-3) appears in the top bar. When user clicks it, the sidebar slides in as an overlay with a semi-transparent backdrop. Clicking the backdrop or a nav item closes it.
116:### 1.3 Top Bar
118:**Livewire component:** `App\Livewire\Admin\Layout\TopBar`
120:**Blade view:** `resources/views/livewire/admin/layout/top-bar.blade.php`
122:#### Component Requirements
134:#### Wireframe
153:#### Interaction Flows
155:- When user clicks the store name, a dropdown appears listing all stores. Clicking a store calls `switchStore()` which updates the session and redirects to the dashboard.
156:- Notification bell shows an unread count badge (positioned absolutely over the icon) when `unreadNotificationCount > 0`.
157:- Profile dropdown is triggered by a Flux profile component showing the user's avatar and name.
159:#### Flux Component Mapping
171:### 1.4 Main Content Area
175:**Breadcrumbs:** Every page includes a Flux breadcrumbs component at the top of the content area. The first item is always "Home" linking to `/admin`. Subsequent items reflect the page hierarchy. Example for product edit: Home > Products > "Blue T-Shirt".
177:**Layout wrapper:** The content area has consistent padding (larger on desktop, smaller on mobile) and a max-width container centered horizontally.
179:### 1.5 Toast Notifications
194:### 1.6 Dark Mode
202:## 2. Dashboard
204:**Route:** `GET /admin`
206:**Livewire component:** `App\Livewire\Admin\Dashboard`
208:**Blade view:** `resources/views/livewire/admin/dashboard.blade.php`
210:### Component Requirements
242:### Wireframe
271:### Layout Description
273:**Page heading:** "Dashboard" displayed as a Flux heading (size xl).
275:**Date range filter:** Positioned top-right. A Flux dropdown triggered by a button showing the current range label and a calendar icon. Options: Today, Last 7 days, Last 30 days, Custom range (reveals two date input fields when selected).
277:### KPI Tiles Row
282:- A muted-color text label (e.g., "Total Sales")
283:- A large heading with the formatted number value
284:- A row with a Flux badge showing percentage change (green if positive, red if negative) and a directional arrow icon
288:### Orders Chart
291:- A Flux heading (size lg): "Orders over time"
292:- A Chart.js canvas (line or bar chart) showing daily order counts
293:- Chart.js initialized via Alpine.js; data passed from Livewire
294:- A spinner overlay shown during chart data loading
296:### Top Products Table
299:- A Flux heading (size lg): "Top products"
300:- A plain table with columns: Product Title, Units Sold, Revenue
301:- Maximum 5 rows
302:- Empty state text: "No sales data for this period."
304:### Conversion Funnel
307:- A Flux heading (size lg): "Conversion funnel"
308:- Horizontal bar visualization using Tailwind utility widths
309:- Each step: label on the left, proportional-width bar on the right, count displayed
310:- Steps: Visits, Add to Cart, Checkout Started, Checkout Completed
311:- Bars use decreasing widths and progressively darker colors
313:### Flux Component Mapping
326:### View States
337:## 3. Products List
339:**Route:** `GET /admin/products`
341:**Livewire component:** `App\Livewire\Admin\Products\Index`
343:**Blade view:** `resources/views/livewire/admin/products/index.blade.php`
345:### Component Requirements
374:### Wireframe
396:### Table Column Definitions
409:### Bulk Action Bar
412:- Text showing "X products selected"
413:- "Set Active" button (ghost variant)
414:- "Archive" button (ghost variant)
415:- "Delete" button (danger variant) - opens confirmation modal
417:### Empty State
420:- Centered container with a large muted placeholder icon (cube)
421:- Heading: "Add your first product"
422:- Text: "Start building your catalog by adding products."
423:- Primary button: "Add product" linking to the create page
425:### Delete Confirmation Modal
436:### Flux Component Mapping
453:### View States
464:## 4. Product Create/Edit
466:**Route:** `GET /admin/products/create` and `GET /admin/products/{product}/edit`
468:**Livewire component:** `App\Livewire\Admin\Products\Form`
470:**Blade view:** `resources/views/livewire/admin/products/form.blade.php`
472:### Component Requirements
511:### Validation Rules
527:### Two-Column Layout Wireframe
579:### Left Column Sections
581:#### Title Input
582:- Flux field with label "Title"
583:- Flux input, placeholder "Short Sleeve T-Shirt"
584:- Flux error display for validation
586:#### Description
587:- Flux field with label "Description"
588:- Flux textarea, 8 rows, placeholder "Describe your product..."
589:- Future enhancement: WYSIWYG toolbar via Alpine.js; baseline is plain textarea
591:#### Media Section
594:**Upload area:** A dashed-border drop zone with upload icon and text "Drag and drop images or click to upload". A hidden file input (multiple, accept images) triggered by clicking the zone.
596:**Upload progress:** A progress bar shown during file upload.
598:**Media grid:** A 4-column grid (responsive: 2 on small, 3 on sm, 4 on lg) of thumbnails:
599:- Each item: square aspect ratio, object-cover, rounded
600:- On hover overlay shows: delete button (X icon, top-right), alt text edit button (pencil icon, bottom-left)
601:- Drag-to-reorder supported via Livewire sortable or Alpine.js Sortable
603:#### Variants Section
606:**Options builder:** For each option, a row containing:
607:- "Option name" input field (placeholder: "Size")
608:- "Values" input field (placeholder: "S, M, L, XL" - comma-separated)
609:- Trash button to remove the option
615:**Variants table:** Shown when variants exist. Scrollable table with inline-editable fields:
626:#### SEO Section
628:- Toggle button: chevron icon + "Search engine listing" text
629:- When expanded: URL handle input field with placeholder "short-sleeve-t-shirt" and validation error display
631:### Right Column Sections
633:#### Status Card
634:- Flux field with label "Status"
635:- Flux select with options: Draft, Active, Archived
637:#### Publishing Card
638:- Flux field with label "Published at"
639:- Datetime-local input
641:#### Product Organization Card
642:- Vendor: Flux input, placeholder "Nike"
643:- Product type: Flux input, placeholder "T-Shirts"
644:- Tags: Flux input, placeholder "summer, cotton, sale" with description "Separate tags with commas"
646:#### Collections Card
647:- Flux field with label "Collections"
648:- For each available collection: a row with checkbox and collection title text
649:- Checkbox values bound to `collectionIds`
651:### Sticky Save Bar
654:- Left-aligned to account for sidebar width on desktop
655:- "Discard" button (ghost variant) - links back to products index
656:- "Save" button (primary variant) - calls `save()`, shows "Saving..." text during loading, disabled while loading
658:### Delete Confirmation Modal (Edit Only)
669:### Flux Component Mapping
688:### View States
700:## 5. Collections List and Edit
702:### 5.1 Collections List
704:**Route:** `GET /admin/collections`
706:**Livewire component:** `App\Livewire\Admin\Collections\Index`
708:**Blade view:** `resources/views/livewire/admin/collections/index.blade.php`
710:#### Component Requirements
726:#### Layout
729:- Heading "Collections" with "Add collection" primary button
730:- Search input
731:- Status filter dropdown
733:#### Table Columns
742:**Empty state:** "Create your first collection" with CTA button.
744:### 5.2 Collection Edit/Create
746:**Route:** `GET /admin/collections/create` and `GET /admin/collections/{collection}/edit`
748:**Livewire component:** `App\Livewire\Admin\Collections\Form`
750:**Blade view:** `resources/views/livewire/admin/collections/form.blade.php`
752:#### Component Requirements
776:#### Layout Wireframe
804:**Left column:**
805:- Title input
806:- Handle input
807:- Description textarea
808:- Products assignment section:
813:**Right column:**
814:- Status: Flux select with Active / Archived
815:- Sticky save bar (same pattern as product form)
819:## 6. Inventory Management
821:**Route:** `GET /admin/inventory`
823:**Livewire component:** `App\Livewire\Admin\Inventory\Index`
825:**Blade view:** `resources/views/livewire/admin/inventory/index.blade.php`
827:### Component Requirements
838:### Wireframe
854:### Layout
856:- Heading "Inventory"
857:- Search input and stock filter dropdown
858:- Table columns: Product, Variant, SKU, On Hand (inline-editable number input), Reserved, Policy (shown as Flux badge: "deny" or "continue"), Actions
862:## 7. Orders List
864:**Route:** `GET /admin/orders`
866:**Livewire component:** `App\Livewire\Admin\Orders\Index`
868:**Blade view:** `resources/views/livewire/admin/orders/index.blade.php`
870:### Component Requirements
889:### Wireframe
908:### Filter Tabs
912:### Table Column Definitions
925:### Flux Component Mapping
936:## 8. Order Detail
938:**Route:** `GET /admin/orders/{order}`
940:**Livewire component:** `App\Livewire\Admin\Orders\Show`
942:**Blade view:** `resources/views/livewire/admin/orders/show.blade.php`
944:### Component Requirements
968:### Two-Column Layout Wireframe
1007:### Left Column Sections
1009:#### Order Heading
1010:- Order number as a large heading
1011:- Financial status badge (color mapped: paid=green, default=zinc)
1012:- Fulfillment status badge (color mapped: fulfilled=green, default=zinc)
1013:- Placed-at date as muted text
1015:#### Action Buttons
1017:- "Confirm payment" button (primary, shown only when `payment_method = bank_transfer` AND `financial_status = pending`). On click, calls `confirmPayment()` which transitions the order to `paid`, commits inventory, and dispatches `OrderPaid`.
1018:- "Create fulfillment" button (shown when order is not fully fulfilled AND fulfillment guard passes - see below)
1019:- "Refund" button (ghost variant, shown when order is paid or partially_refunded)
1021:#### Fulfillment Guard
1029:#### Timeline
1031:- Each event: title text (bold) and timestamp text (small, muted)
1032:- Events include: Order placed, Payment received, Fulfillment created, Shipped, Delivered, Refunded (as applicable)
1034:#### Order Lines Table
1044:#### Order Summary
1046:- Subtotal, Discount (negative), Shipping, Tax, Total (bold)
1047:- Each row is a flex container with label left and amount right
1049:#### Payment Details
1051:- Payment method as text (Credit Card / PayPal / Bank Transfer), derived from `order.payment_method` enum
1052:- Status as Flux badge (pending=zinc, captured=green, failed=red, refunded=yellow)
1053:- Amount formatted as currency
1054:- Mock reference ID (provider_payment_id)
1055:- **"Confirm payment" button** (primary variant): Shown only when `payment_method = bank_transfer` AND `financial_status = pending`. Clicking calls `confirmPayment()`.
1057:#### Fulfillment Cards
1060:- Fulfillment status badge (pending=zinc, shipped=blue, delivered=green)
1061:- Tracking info (company, number, URL link)
1062:- Line items included in the fulfillment
1063:- **Action buttons on each fulfillment card:**
1067:### Right Column Sections
1069:#### Customer Card
1070:- Heading "Customer" (size md)
1071:- Separator
1072:- Customer name (or "Guest")
1073:- Customer email (muted text)
--- specs/04-STOREFRONT-UI.md ---
1:# 04 - Storefront UI/UX Specification
7:## Conventions
21:## Currency Formatting
34:**Helper:** Use a shared Blade component or helper (e.g., `<x-price :amount="$amount" :currency="$currency" />`) to ensure consistent formatting across all storefront pages.
36:**Examples:**
37:- 2499 cents -> `24.99 EUR`
38:- 0 cents -> `0.00 EUR`
39:- 149900 cents -> `1,499.00 EUR`
43:## 1. Theme System Architecture
45:### 1.1 View Organization
58:### 1.2 Layout Hierarchy
62:### 1.3 Theme Settings
66:- Announcement bar visibility, text, link, and background color
67:- Sticky header toggle
68:- Home page section order and visibility
69:- Color palette overrides (primary, secondary, accent)
70:- Typography scale
71:- Footer content and social links
72:- Dark mode behavior (system preference, toggle, or forced)
74:### 1.4 Section-Based Architecture
86:### 1.5 Blade Components
92:## 2. Base Layout
94:### 2.1 Document Head
110:### 2.2 Skip Link
114:**Accessibility:** Allows keyboard users to bypass the header navigation and jump directly to the main content area.
116:### 2.3 Announcement Bar
129:### 2.4 Header
133:**Desktop Layout (large breakpoint and above):**
149:**Mobile Layout (below large breakpoint):**
163:**Sticky Header (optional):**
167:**Mobile Navigation Drawer:**
181:### 2.5 Main Content Area
185:### 2.6 Footer
189:**Layout:**
210:### 2.7 Cart Drawer (Livewire)
214:### 2.8 Dark Mode
224:**Color Token Patterns:**
237:## 3. Home Page (GET /)
247:### 3.1 Hero Banner Section
249:**Layout:** Full-width container (no max-width on the outer wrapper). Inner content constrained to standard max-width.
251:**Desktop (large breakpoint and above):**
275:**Mobile:** Same structure but stacked, with reduced font sizes and shorter minimum height. Full-bleed background image remains.
277:**Accessibility:**
279:- If background is an image, it is decorative (the content provides meaning)
280:- CTA button has descriptive text (e.g., "Shop the summer collection")
282:### 3.2 Featured Collections Section
284:**Layout:** Section with heading and grid of collection cards.
292:**Collection Card:**
312:**Accessibility:**
314:- Each card link has the collection title as accessible name
315:- Images have alt text matching the collection title
317:### 3.3 Featured Products Section
319:**Livewire Component:** FeaturedProducts
332:### 3.4 Newsletter Signup Section
334:**Livewire Component:** NewsletterSignup
336:**Layout:**
359:**Accessibility:**
361:- Label associated with email input (visually hidden if using placeholder)
362:- Success/error messages in a polite live region for screen readers
364:### 3.5 Rich Text Section
376:## 4. Collection Page (GET /collections/{handle})
378:**Livewire Component:** CollectionPage
380:### 4.1 Collection Header
389:### 4.2 Toolbar
393:**Desktop:**
401:**Mobile:**
414:**Sort Options:**
424:### 4.3 Filter Sidebar
426:**Desktop (large breakpoint and above):** Persistent sidebar on the left, approximately 256px wide.
427:**Mobile (below large breakpoint):** Slide-out drawer from the left, triggered by "Filter" button with filter icon.
429:**Filter Groups:**
440:**Clear All:**
442:- Link at the top of the filter sidebar: "Clear all filters"
443:- Only visible when at least one filter is active
444:- Small, blue, interactive text
446:**Active Filter Pills:**
448:- Displayed above the product grid (below toolbar) when filters are active
449:- Each pill: rounded tag with filter value text and a remove (X) button
450:- "Clear all" link at the end of the pills row
452:**Mobile Filter Drawer:**
463:### 4.4 Product Grid
471:### 4.5 Product Card (Component)
490:**Image Behavior:**
500:**Badges:**
507:**Text Content:**
515:**Accessibility:**
517:- Card link wraps the entire card; title serves as the accessible name
518:- Badges have screen reader labels (e.g., "On sale")
519:- Price changes announced via parent live region when filtering changes results
521:### 4.6 Pagination
532:### 4.7 Empty State
555:## 5. Product Page (GET /products/{handle})
557:**Livewire Component:** ProductPage
559:### 5.1 Layout
561:**Desktop (large breakpoint and above):** Two-column, 50/50 split. Left column (images) is sticky while the right column (product info) scrolls.
563:**Mobile:** Single column, stacked. Images first, then product info.
582:### 5.2 Image Gallery (Left Column)
584:**Desktop:**
605:**Mobile:**
613:**Accessibility:**
621:### 5.3 Product Info (Right Column)
623:#### Breadcrumbs
627:#### Product Title
631:#### Price Display
641:#### Variant Selector
645:**Radio button style (for options with 6 or fewer values):**
658:**Select dropdown style:** Used when the option has more than 6 values.
660:**Color swatches (when option name is "Color"):**
669:**Livewire behavior:**
671:- Selected options are a key-value map (e.g., Size = M, Color = Red)
672:- Selected variant is computed by matching all selected options to the variant option values
673:- On change: updates price, stock messaging, available images, and add-to-cart availability
675:#### Stock Messaging
688:#### Quantity Selector
705:#### Add to Cart Button
716:**Livewire action (addToCart):**
723:#### Product Description
725:- Separated from above content by a horizontal rule with generous vertical spacing
726:- Rendered HTML styled with prose/typography styling
728:#### Tags
730:- Displayed below the description (if tags exist)
731:- Horizontal wrapping row with gap
732:- Each tag: rounded pill, gray background, small text
734:### 5.4 Accessibility Summary
746:## 6. Cart Drawer
748:**Livewire Component:** CartDrawer
750:### 6.1 Trigger and Animation
760:### 6.2 Drawer Header
775:### 6.3 Cart Line Items
801:### 6.4 Discount Code Section
805:**No discount applied:**
813:**Discount applied:**
829:### 6.5 Totals Section
848:### 6.6 Action Buttons
855:### 6.7 Empty Cart State
874:### 6.8 Accessibility
887:## 7. Full Cart Page (GET /cart)
889:**Livewire Component:** CartPage
893:### 7.1 Desktop Layout
924:### 7.2 Totals and Actions
926:- Right-aligned on desktop, full-width on mobile
927:- Same discount code input/apply functionality as drawer
928:- Same totals breakdown
929:- "Checkout" button: primary, aligned right
930:- "Continue shopping" link: below checkout button
932:### 7.3 Empty State
938:## 8. Checkout Page (GET /checkout/{checkoutId})
940:**Livewire Component:** CheckoutPage
942:### 8.1 Layout
944:**Desktop (large breakpoint and above):** Two-column layout. Left column (wider, approx 60%) contains the form sections. Right column (narrower, approx 40%) contains the sticky order summary.
946:**Mobile:** Single column. Order summary is a collapsible section at the top (collapsed by default, showing a toggle with "Show order summary" / "Hide order summary" + chevron + total amount).
964:### 8.2 Checkout Steps
968:**Step State Behavior:**
978:#### Step 1 - Contact Information
986:#### Step 2 - Shipping Address
990:**Fields:**
1004:- Layout: 2-column grid, full-width fields span both columns
1005:- Country select: on change, State/Province updates to show relevant regions
1006:- All fields validate on blur
1008:**Billing address:**
1010:- Checkbox: "Billing address same as shipping" (checked by default)
1011:- When unchecked, a second address form appears for billing
1013:**Saved addresses (logged-in customers):**
1015:- Dropdown above the form: "Select a saved address" with saved addresses listed
1016:- "Use a new address" option to show the blank form
1017:- On selection, form fields populate with the saved address data
1021:#### Step 3 - Shipping Method
1042:#### Step 4 - Payment Method & Pay
1046:**Payment method selection (radio buttons):**
1064:**Credit Card form (shown when Credit Card is selected):**
1083:**PayPal form (shown when PayPal is selected):**
1095:**Bank Transfer form (shown when Bank Transfer is selected):**
1116:### 8.3 Order Summary Sidebar (Right Column)
1147:### 8.4 Accessibility
1160:## 9. Order Confirmation (GET /checkout/{checkoutId}/confirmation)
1162:### 9.1 Layout
1198:### 9.2 Order Details
1207:#### Bank Transfer Instructions (shown only when `payment_method = bank_transfer`)
1230:### 9.3 Actions
1240:## 10. Customer Account Pages
1244:### 10.1 Login (GET /account/login)
1246:**Livewire Component:** Login
1274:### 10.2 Register (GET /account/register)
1276:**Livewire Component:** Register
1306:### 10.3 Account Dashboard (GET /account)
1332:### 10.4 Order History (GET /account/orders)
1342:### 10.5 Order Detail (GET /account/orders/{orderNumber})
1379:### 10.6 Address Book (GET /account/addresses)
1381:**Livewire Component:** AddressBook
1408:## 11. Search
1410:### 11.1 Search Modal
1412:**Livewire Component:** SearchModal
1448:### 11.2 Search Results Page (GET /search?q=)
1450:**Livewire Component:** SearchResults
1466:## 12. Content Pages (GET /pages/{handle})
1468:### 12.1 Layout
1479:## 13. Error Pages
1481:### 13.1 404 Page
1504:### 13.2 503 / Maintenance Page
1525:## 14. Responsive Breakpoints
1537:### Key Responsive Behaviors
1550:### Touch Considerations
1560:## 15. Accessibility Checklist
1562:### Global (All Pages)
1576:### Page-Specific Accessibility
1587:### Keyboard Interactions
1605:## 16. Component Library
1607:### storefront-layout
1616:### storefront-product-card
1626:**Eager-load requirements:** product.variants.inventoryItem, product.media (sorted by position).
1628:### storefront-price
1638:**Rendering:** Formats amount using Laravel's Number currency helper. If compareAtAmount is set and greater than amount, shows compare-at price with strikethrough styling and a "Sale" badge.
1640:### storefront-badge
1649:**Variant Styles:**
1658:**Base appearance:** Inline pill shape, rounded-full, extra-small medium-weight text with horizontal padding.
1660:### storefront-quantity-selector
1672:### storefront-address-form
1681:### storefront-order-summary
1690:### storefront-breadcrumbs
1698:**Rendering:**
--- specs/05-BUSINESS-LOGIC.md ---
1:# 05 - Business Logic Specification
7:## Conventions
20:## 1. Multi-Tenancy and Store Isolation
22:### 1.1 Tenant Resolution Middleware
24:**Class:** `App\Http\Middleware\ResolveStore`
26:**Register** in `bootstrap/app.php` under both the `storefront` and `admin` middleware groups.
28:#### Storefront Route Resolution (from request hostname)
37:#### Admin Route Resolution (from session)
66:### 1.2 Store Scoping
68:**Trait:** `App\Models\Concerns\BelongsToStore`
69:**Global Scope:** `App\Models\Scopes\StoreScope`
73:**Behavior:**
75:- On the model's `creating` event, automatically set `store_id` from the current store context (defense-in-depth).
76:- All SELECT, UPDATE, DELETE queries on tenant-scoped models include the store filter.
77:- In platform-admin contexts where cross-store access is needed, the scope can be temporarily removed.
79:**Models that must use this trait:**
82:### 1.3 Role-Based Access Control
84:**Enum:** `App\Enums\StoreUserRole` - backed by string values: `owner`, `admin`, `staff`, `support`
86:**Permission Matrix:**
102:**Implementation requirements:**
104:- Create a Laravel policy for each resource model (e.g., `ProductPolicy`, `OrderPolicy`).
105:- Register gate definitions in a service provider.
106:- Each policy method receives the User, looks up the user's role for the current store via the `store_users` pivot table, and checks the matrix above.
107:- Add a helper method on the User model - `roleForStore(store)` - that returns the user's role for a given store or null if the user has no access.
111:## 2. Product Lifecycle
113:### 2.1 Status Transitions
115:**Enum:** `App\Enums\ProductStatus` - values: `draft`, `active`, `archived`
128:**Transition rules:**
139:**Deletion rules:**
140:- Only `draft` products with zero `order_lines` references may be hard-deleted.
141:- Products with existing order references must be archived, never deleted, to preserve order history integrity.
143:**Service:** `App\Services\ProductService`
144:- Method `transitionStatus(product, newStatus)` validates the current state, checks preconditions, throws `InvalidProductTransitionException` on failure.
145:- On activation: set `published_at` to current time if not already set.
146:- Dispatch `ProductStatusChanged` event after successful transition.
148:### 2.2 Variant and Option Matrix
150:**Rules:**
152:- A product has 0 to 3 options (e.g., Size, Color, Material), stored in `product_options`.
153:- Each option has 1 or more values, stored in `product_option_values`.
154:- Variants represent the cartesian product of option values (or a manually curated subset).
155:- When a product has no options, a single "Default" variant is auto-created with `is_default = 1`.
157:**Matrix rebuild logic (when options change):**
178:**Service:** `App\Services\VariantMatrixService`
180:### 2.3 SKU Uniqueness
182:- Combination of store_id (derived through the product's store) and SKU must be unique.
183:- Enforced at application level before save.
184:- Null or empty SKU values are exempt from the uniqueness check (multiple variants may have no SKU).
185:- Validation: query all variants belonging to products in the same store, check for SKU collision excluding the current variant's ID.
187:### 2.4 Handle/Slug Generation
189:**Helper class:** `App\Support\HandleGenerator`
191:- Auto-generate from the title by converting to a URL-safe slug (lowercase, hyphens for spaces, strip special characters).
192:- Uniqueness enforced per store via the database unique index on `(store_id, handle)`.
193:- On collision, append incrementing suffix: `my-product`, `my-product-1`, `my-product-2`, etc.
194:- Merchants can manually edit the handle; on save, re-validate uniqueness.
195:- Same logic applies to collections and pages.
199:## 3. Inventory Management
201:### 3.1 Tracking Model
211:### 3.2 Inventory Policy
220:### 3.3 Inventory Operations
222:**Service:** `App\Services\InventoryService`
236:**Operation definitions:**
245:### 3.4 Concurrency
251:## 4. Cart Service
253:### 4.1 Cart Lifecycle
255:**Service:** `App\Services\CartService`
264:**Cart identification:**
265:- Guest users: cart ID stored in session cookie.
266:- Authenticated customers: cart linked via `customer_id` foreign key.
270:**Cart merge on login:**
285:### 4.2 Cart Operations
287:**Create cart:**
288:- Accepts: store, optional customer
289:- Sets: store_id, currency from store's default_currency, initial cart_version = 1
291:**Add line - validations (in order):**
304:**Update quantity:**
305:- Validate new quantity > 0. If 0, remove the line instead.
306:- Re-check inventory policy for the new quantity.
307:- Recalculate line amounts.
308:- Increment `cart_version`.
310:**Remove line:**
311:- Delete the `cart_lines` record.
312:- Increment `cart_version`.
314:### 4.3 Cart Version (Optimistic Concurrency)
318:**For API clients:**
319:- Client sends `expected_version` with each mutation request.
320:- If cart's current version does not match the expected version, return HTTP 409 Conflict with the current cart state in the response body.
321:- This prevents race conditions when multiple browser tabs or devices modify the same cart.
323:**For Livewire/session-based flows:**
324:- Still implemented for consistency, but less critical since requests are serialized per session.
326:### 4.4 Line Amount Calculation
336:### 4.5 Cart Expiration
338:**Job:** `App\Jobs\CleanupAbandonedCarts`
340:- Schedule: daily
341:- Finds carts where status = `active` AND updated_at < (now - 14 days). The 14-day threshold is configurable via `store_settings`.
342:- Marks them as `abandoned`.
343:- Releases any reserved inventory associated with active checkouts for these carts.
347:## 5. Pricing Engine
349:### 5.1 Calculation Order (deterministic pipeline)
351:**Service:** `App\Services\PricingEngine`
374:### 5.2 Output Structure
376:**Value Object:** `App\ValueObjects\PricingResult`
388:**Value Object:** `App\ValueObjects\TaxLine`
396:### 5.3 Rounding Strategy
398:- Each line's discount allocation, tax, and total are rounded independently.
399:- Totals are computed by summing already-rounded line amounts.
400:- This prevents accumulation of fractional cents across lines.
401:- Any remainder from proportional discount allocation is added to the last qualifying line (largest-remainder method).
403:### 5.4 Determinism and Snapshotting
405:- After each calculation, store the full result in `checkouts.totals_json` as a JSON snapshot.
406:- Recalculate on every significant state change: address change (may affect tax/shipping zones), shipping method change, discount code change.
407:- The snapshot structure mirrors the PricingResult value object.
411:## 6. Checkout State Machine
413:### 6.1 States
415:**Enum:** `App\Enums\CheckoutStatus` - values: `started`, `addressed`, `shipping_selected`, `payment_selected`, `completed`, `expired`
437:### 6.2 Transitions and Validation
439:**Service:** `App\Services\CheckoutService`
441:#### Transition: started -> addressed
443:**Method:** `setAddress(checkout, addressData)`
445:**Required data:**
446:- `email` - valid email address
447:- `shipping_address` with required fields:
463:**Actions:**
471:#### Transition: addressed -> shipping_selected
473:**Method:** `setShippingMethod(checkout, shippingRateId)`
475:**Requires:**
476:- A valid shipping_rate_id that belongs to a shipping zone matching the checkout's shipping address.
477:- **Skip condition:** If no cart lines have `requires_shipping = true` on their variant, skip this step. Set status to `shipping_selected` with shipping_method_id = null and shipping_amount = 0.
479:**Actions:**
487:#### Transition: shipping_selected -> payment_selected
489:**Method:** `selectPaymentMethod(checkout, paymentMethod)` - validates and stores the selected payment method
491:**Requires:**
492:- All previous data is valid (address, shipping method).
493:- Totals have been calculated and stored.
494:- `paymentMethod` must be one of: `credit_card`, `paypal`, `bank_transfer`.
496:**Actions:**
502:#### Transition: payment_selected -> completed
504:**Method:** `completeCheckout(checkout, paymentMethodData)` - returns an Order
506:**Triggered by:** Customer submitting the payment form (onsite, no external redirect).
508:**CRITICAL: This transition must be idempotent.** If called multiple times for the same checkout (e.g., double-click), it must not create duplicate orders. Always check if an order already exists for this checkout before proceeding.
510:**Actions (all within a single database transaction):**
578:#### Transition: Any active state -> expired
580:**Triggered by:** `App\Jobs\ExpireAbandonedCheckouts` scheduled job (every 15 minutes).
582:**Conditions:** Checkouts where status is NOT `completed` or `expired`, AND `updated_at` < now - 24 hours.
584:**Actions:**
589:### 6.3 Events
600:## 7. Discount Engine
602:### 7.1 Discount Types
604:**Enum:** `App\Enums\DiscountType` - values: `code`, `automatic`
611:**Stacking rules:**
612:- Only one code discount per checkout.
613:- Multiple automatic discounts can stack (each applied sequentially to the remaining undiscounted amount).
615:### Discount Status Lifecycle
624:**Allowed transitions:**
625:- `draft` -> `active`
626:- `active` -> `disabled`
627:- `active` -> `expired` (automatic)
628:- `disabled` -> `active`
630:### 7.2 Value Types
632:**Enum:** `App\Enums\DiscountValueType` - values: `percent`, `fixed`, `free_shipping`
640:### 7.3 Rules
651:**Applicability logic:** When both `applicable_product_ids` and `applicable_collection_ids` are set, a product qualifies if it matches **either** condition (union, not intersection). When both are null or empty, the discount applies to the entire order.
653:### 7.4 Validation Flow
655:**Service:** `App\Services\DiscountService`
657:**Method:** `validate(code, cart)` - returns a DiscountValidationResult value object
679:### 7.5 Error Codes
690:### 7.6 Discount Allocation
718:**Storage of allocations:**
719:- During checkout: update `cart_lines.line_discount_amount`.
720:- On order creation: store in `order_lines.discount_allocations_json` as an array of `{discount_id, amount}`.
722:### 7.7 Usage Tracking
725:- Increment `discounts.usage_count` by 1.
726:- This happens within the order creation transaction (see Section 6.2, completeCheckout procedure, step 7).
730:## 8. Tax Calculation
732:### 8.1 Tax Provider Architecture
734:**Interface:** `App\Contracts\TaxProvider`
736:- Must implement a `calculate(request)` method that accepts a TaxCalculationRequest and returns a TaxCalculationResult.
737:- The TaxCalculationRequest contains: line items (with discounted amounts), shipping amount, shipping address, store tax settings.
738:- The TaxCalculationResult contains: array of TaxLine value objects, total tax amount.
740:**Service:** `App\Services\TaxCalculator` - orchestrates provider selection and delegates to the appropriate implementation.
742:**Implementations:**
743:- `App\Services\Tax\ManualTaxProvider` - default, uses manually configured rates
744:- `App\Services\Tax\StripeTaxProvider` - stub for Stripe Tax API integration
746:### 8.2 Manual Tax Mode
748:**Rate resolution:**
753:**Calculation per line:**
761:### 8.3 Prices Include Tax
765:#### Tax-inclusive pricing (prices_include_tax = true)
767:- Displayed prices on the storefront are gross (include tax).
768:- Subtotal shown to customer is gross.
769:- Tax is "extracted" from the gross amount:
778:**Example:** Price = 1190 cents, rate = 1900 (19%).
779:- net = intdiv(1190 * 10000, 11900) = intdiv(11900000, 11900) = 1000
780:- tax = 1190 - 1000 = 190
782:#### Tax-exclusive pricing (prices_include_tax = false)
784:- Displayed prices are net (excluding tax).
785:- Tax added on top:
792:**Example:** Price = 1000 cents, rate = 1900.
793:- tax = ROUND(1000 * 1900 / 10000) = 190
794:- total = 1190
796:### 8.4 Stripe Tax Provider (Stub)
798:**Implementation:** `App\Services\Tax\StripeTaxProvider`
800:- Sends line items and shipping address to Stripe Tax API.
801:- Stores the full response in `checkouts.tax_provider_snapshot_json` for audit.
802:- Fallback behavior (configurable per store in `tax_settings.config_json`):
809:### 8.5 Discount-Before-Tax Semantics
815:**Calculation order within the pricing pipeline:**
823:## 9. Shipping Calculation
825:### 9.1 Zone Matching
827:**Service:** `App\Services\ShippingCalculator`
829:**Zone matching algorithm:**
858:**When no zone matches:** The address is unserviceable. Block checkout progression with error "Cannot ship to this address".
860:### 9.2 Rate Types
864:#### Flat rate
868:- Cost = fixed amount regardless of cart contents.
870:#### Weight-based rate
888:#### Price-based rate
904:#### Carrier-calculated rate (stub)
908:- Placeholder for future carrier API integration.
909:- Returns a hardcoded rate or error until implemented.
911:### 9.3 Requires Shipping Check
--- specs/06-AUTH-AND-SECURITY.md ---
1:# 06 - Authentication and Security Specification
7:## 1. Authentication
11:### 1.1 Admin Authentication
22:#### Admin Login Flow
51:**Requirements:**
53:- The login form collects `email` and `password`.
54:- On successful login, the session MUST be regenerated to prevent session fixation attacks.
55:- On failed login, only the generic message "Invalid credentials" is shown. The system must not indicate whether the email or password was wrong.
56:- Rate limiting: maximum 5 attempts per minute, keyed by IP address. Uses the `login` rate limiter.
57:- After login: redirect to `/admin` (the dashboard).
59:#### Remember Me
61:- An optional "Remember me" checkbox on the login form.
62:- When enabled, Laravel sets a long-lived remember token cookie.
63:- The `remember_token` column on the `users` table stores the hashed token.
64:- Cookie name follows Laravel default convention: `remember_web_{hash}`.
66:#### Admin Password Reset Flow
97:**Requirements:**
99:- Uses Laravel's built-in password broker (`users` broker).
100:- Reset tokens stored in the `password_reset_tokens` table.
101:- Token expiration: 60 minutes (configurable in `config/auth.php` under `passwords.users.expire`).
102:- Throttle: one reset email per 60 seconds per email address.
103:- The "forgot password" response must be generic (do not reveal whether email exists).
105:#### Admin Logout
107:- Route: `POST /admin/logout` (via Livewire action or standard route).
108:- Steps: invalidate the entire session, regenerate the CSRF token, redirect to `/admin/login`.
109:- The response must not cache.
111:#### Session Lifetime
120:### 1.2 Customer Authentication
131:#### Customer Login Flow
157:**Requirements:**
159:- Customer login is always scoped to the current store. The store is resolved from the request hostname by the `ResolveStore` middleware before the login attempt.
160:- The custom `CustomerUserProvider` must inject `store_id` into the credential check so the query filters by both `email` and `store_id`.
161:- The same email address can exist in different stores (multi-tenant isolation).
162:- Rate limiting: maximum 5 attempts per minute per IP, using the same `login` rate limiter.
163:- After login: redirect to `/account` or the intended URL (if the customer was redirected from a protected page).
165:#### Customer Registration Flow
177:**Registration fields:**
187:#### Customer Store Scoping
189:- The `customers` auth provider requires a custom `CustomerUserProvider` class.
190:- This provider overrides the default Eloquent provider to always include `store_id` in credential queries.
191:- The `store_id` is obtained from the container-bound current store (resolved by `ResolveStore` middleware).
192:- This ensures that `Auth::guard('customer')->attempt()` never crosses store boundaries.
194:#### Customer Password Reset
196:- Uses a separate password broker: `customers` broker.
197:- Tokens stored in the `customer_password_reset_tokens` table (separate from admin tokens).
198:- Token records include `store_id` to scope resets to the correct store.
199:- Routes: `GET /forgot-password`, `POST /forgot-password`, `GET /reset-password/{token}`, `POST /reset-password`.
200:- Token expiration: 60 minutes.
201:- Throttle: one reset email per 60 seconds.
202:- Generic response on the "forgot password" form (do not reveal whether email exists in the store).
206:### 1.3 API Authentication (Sanctum)
218:#### Token Lifecycle
232:**Requirements:**
234:- Tokens are created in the admin panel, under the Developers section.
235:- Each token has: a human-readable name, an optional expiration date, and a list of abilities (scopes).
236:- On creation, the plain-text token (prefixed with `shop_`) is displayed exactly once. Only the SHA-256 hash is persisted.
237:- Tokens are sent as `Authorization: Bearer shop_xxxxxxxx`.
238:- Sanctum middleware validates the hash and resolves the associated user.
239:- Ability checks are performed per-endpoint (e.g., the products list endpoint requires `read-products`).
240:- Revoking a token deletes its row from the `personal_access_tokens` table.
242:#### Token Abilities (Scopes)
269:### 1.4 Guard Configuration
273:#### Guards Data Dictionary
281:#### Providers Data Dictionary
288:#### Password Brokers Data Dictionary
295:**Defaults:** The default guard is `web` and the default password broker is `users`.
299:## 2. Authorization
301:### 2.1 Role Enum
314:**Constraints:**
316:- Each store must have exactly one Owner.
317:- Ownership transfer is a dedicated action (not a simple role change).
318:- A User can hold different roles in different stores (via the `store_users` pivot table).
322:### 2.2 Permission Matrix
375:#### Role Group Shorthand
388:### 2.3 Role Checking Trait
402:**Store ID resolution:** When the policy method receives a model instance (e.g., Product), the store_id comes from the model's `store_id` attribute. When the policy method does not receive a model (e.g., `viewAny`, `create`), the store_id is resolved from the container-bound `current_store`.
406:### 2.4 Laravel Policies
415:#### ProductPolicy
429:#### OrderPolicy
442:#### CollectionPolicy
454:#### DiscountPolicy
466:#### CustomerPolicy
476:#### ThemePolicy
489:#### PagePolicy
501:#### StorePolicy
511:#### FulfillmentPolicy
523:#### RefundPolicy
533:#### NavigationMenuPolicy
544:### 2.5 Gates (Non-Model Operations)
560:**Gate resolution pattern (all gates follow this):**
570:### 2.6 Policy and Gate Usage Patterns
572:#### In Livewire Components
574:- List views: call `authorize('viewAny', ModelClass)` in the `mount()` lifecycle method.
575:- Action methods (delete, update, etc.): call `authorize('action', $modelInstance)` at the start of the action, before performing any mutation.
576:- If authorization fails, Laravel throws `AuthorizationException`, which renders as 403.
578:#### In API Controllers
580:- Use `$this->authorize('action', $model)` at the start of each controller method.
581:- Alternatively, use `can` middleware on routes for simpler checks.
582:- Form Requests also implement `authorize()` to check the relevant policy or gate before validation runs.
584:#### In Blade Templates
586:- Use `@can('gate-name')` / `@cannot` directives to conditionally show/hide navigation items and action buttons.
587:- This is purely cosmetic (UI hiding) - the actual authorization check happens server-side in policies/gates.
591:## 3. Middleware Stack
593:### 3.1 Middleware Registration
607:### 3.2 Route Middleware Groups
621:### 3.3 Custom Middleware Specifications
623:#### ResolveStore
627:**Purpose:** Resolves the current store from the request hostname and binds it to the service container.
646:**Requirements:**
652:**Shared steps (after store is resolved):**
659:#### CheckStoreRole
663:**Purpose:** Verifies the authenticated user has one of the specified roles for the current store.
665:**Route usage:** Applied as `role.check:owner,admin` or `role.check:owner,admin,staff` (comma-separated role values passed as variadic parameters).
680:**Requirements:**
691:#### CustomerAuthenticate
695:**Purpose:** Ensures the request is authenticated via the `customer` guard. Redirects unauthenticated customers to login.
705:**Requirements:**
713:## 4. Security Controls
715:### 4.1 CSRF Protection
724:**Requirements:**
726:- CSRF verification is enforced globally on all `web` middleware group routes.
727:- No routes within the `web` group may be excluded from CSRF verification.
728:- API routes (under `api` middleware group) are inherently exempt since they use token auth.
732:### 4.2 Rate Limiting
746:**Rate limit response headers** (returned on all API responses by Laravel default):
758:### 4.3 Encryption at Rest
762:**Fields requiring encrypted storage (using Laravel's `encrypted` cast):**
770:**Requirements:**
772:- These columns are declared with the `encrypted` (or `encrypted:array`) cast in the model's `casts()` method.
773:- Data is automatically encrypted before writing to the database and decrypted when reading.
774:- The encrypted columns store opaque ciphertext (type TEXT in SQLite). They cannot be queried with WHERE clauses at the database level.
778:### 4.4 Input Validation
782:#### Form Request Inventory
801:**Authorization in Form Requests:**
807:### 4.5 XSS Prevention
817:#### HTML Sanitization Requirements
821:**Allowed HTML elements and attributes:**
836:**Where sanitization is applied:** Before persisting product `description_html` and page `body_html` fields. The sanitization runs during the create and update operations for these models.
840:### 4.6 SQL Injection Prevention
842:**Requirements:**
844:- All database queries must use Eloquent ORM with parameterized bindings (Laravel default behavior).
845:- No raw query strings may contain unsanitized user input.
846:- Any use of `whereRaw`, `selectRaw`, or `DB::raw` must use parameter bindings (the `?` placeholder syntax).
847:- FTS5 (full-text search) queries require special handling: user input must be sanitized before building the MATCH expression.
849:#### FTS5 Input Sanitization
859:### 4.7 Mass Assignment Protection
861:**Requirements:**
863:- Every Eloquent model must define a `$fillable` array listing only the columns that are safe for mass assignment.
864:- The pattern `$guarded = []` is never permitted in any model.
865:- All controller and Livewire action code must use `$request->validated()` (not `$request->all()` or `$request->input()`) when passing data to model creation or update methods.
869:### 4.8 Password Security
878:**Model requirements:**
880:- Both `User` and `Customer` models must include `password` and `remember_token` in their `$hidden` array.
881:- Password validation rules must enforce minimum 8 characters and use the `confirmed` rule (requiring a matching `password_confirmation` field).
883:**Important:** Both the `User` and `Customer` models use `password_hash` as the database column name instead of Laravel's default `password`. Both models must override `getAuthPassword()`:
896:## 5. Session Configuration
916:## 6. CORS Configuration
933:## 7. Webhook Signature Verification
935:### 7.1 Inbound Payment Webhooks
941:### 7.2 Platform Webhooks (Outbound)
961:**Requirements:**
975:## 8. Audit Logging
977:### 8.1 Logging Channel
990:### 8.2 Log Entry Format
1009:### 8.3 Events to Log
1011:#### Authentication Events
1022:#### Admin Resource Events
1026:**Resource types:** product, collection, discount, page, theme, order, fulfillment, refund, navigation_menu, shipping_zone, tax_setting, staff
1035:#### Security Events
1046:### 8.4 Audit Logger Service
1052:**Method signature:**
1063:**Behavior:**
1071:### 8.5 Integration with Laravel Events
1075:#### Auth Event Listeners
1085:#### Model Observers
1091:## 9. Security Checklist Summary
--- specs/07-SEEDERS-AND-TEST-DATA.md ---
1:# 07 - Seeders and Test Data Specification
7:## 1. DatabaseSeeder Orchestration
11:**Execution order:**
34:**Dependency diagram (Mermaid):**
62:**Running seeders:**
63:- All seeders: `php artisan db:seed`
64:- Single seeder: `php artisan db:seed --class=ProductSeeder`
65:- Fresh database with seed data: `php artisan migrate:fresh --seed`
69:## 2. Factory Definitions
75:### 2.1 OrganizationFactory
77:**Model:** Organization
79:**Default definition:**
86:**States:** None.
90:### 2.2 StoreFactory
92:**Model:** Store
94:**Default definition:**
106:**States:**
114:### 2.3 StoreDomainFactory
116:**Model:** StoreDomain
118:**Default definition:**
128:**States:**
138:### 2.4 UserFactory
140:**Model:** User
142:**Default definition:**
152:**States:**
160:### 2.5 ProductFactory
162:**Model:** Product
164:**Default definition:**
178:**States:**
185:**Relationship hooks:**
194:### 2.6 ProductVariantFactory
196:**Model:** ProductVariant
198:**Default definition:**
214:**States:**
225:### 2.7 InventoryItemFactory
227:**Model:** InventoryItem
229:**Default definition:**
239:**States:**
249:### 2.8 CollectionFactory
251:**Model:** Collection
253:**Default definition:**
264:**States:**
274:### 2.9 CartFactory
276:**Model:** Cart
278:**Default definition:**
288:**States:**
298:### 2.10 CartLineFactory
300:**Model:** CartLine
302:**Default definition:**
314:**States:** None.
318:### 2.11 CheckoutFactory
320:**Model:** Checkout
322:**Default definition:**
340:**States:**
352:### 2.12 OrderFactory
354:**Model:** Order
356:**Default definition:**
378:**States:**
394:### 2.13 OrderLineFactory
396:**Model:** OrderLine
398:**Default definition:**
413:**States:** None.
417:### 2.14 PaymentFactory
419:**Model:** Payment
421:**Default definition:**
434:**States:**
447:### 2.15 RefundFactory
449:**Model:** Refund
451:**Default definition:**
462:**States:**
471:### 2.16 FulfillmentFactory
473:**Model:** Fulfillment
475:**Default definition:**
486:**States:**
495:### 2.16b FulfillmentLineFactory
497:**Model:** FulfillmentLine
499:**Default definition:**
507:**States:** None.
511:### 2.17 CustomerFactory
513:**Model:** Customer
515:**Default definition:**
525:**States:**
534:### 2.18 CustomerAddressFactory
536:**Model:** CustomerAddress
538:**Default definition:**
547:**States:**
555:### 2.19 DiscountFactory
557:**Model:** Discount
559:**Default definition:**
575:**Note on value_amount for percent type:** Stored as whole number percentages. 10 = 10%, 20 = 20%.
577:**States:**
591:### 2.20 ShippingZoneFactory
593:**Model:** ShippingZone
595:**Default definition:**
604:**States:** None.
608:### 2.21 ShippingRateFactory
610:**Model:** ShippingRate
612:**Default definition:**
622:**States:**
631:### 2.22 ThemeFactory
633:**Model:** Theme
635:**Default definition:**
645:**States:**
653:### 2.23 PageFactory
655:**Model:** Page
657:**Default definition:**
668:**States:**
677:### 2.24 NavigationMenuFactory
679:**Model:** NavigationMenu
681:**Default definition:**
689:**States:** None.
693:### 2.25 NavigationItemFactory
695:**Model:** NavigationItem
697:**Default definition:**
708:**States:**
718:### 2.26 AnalyticsEventFactory
720:**Model:** AnalyticsEvent
722:**Default definition:**
733:**States:**
743:## 3. Demo Seed Scenario (Exact Data)
749:### 3.1 OrganizationSeeder
760:### 3.2 StoreSeeder
764:**Store 1: Acme Fashion**
775:**Store 2: Acme Electronics**
788:### 3.3 StoreDomainSeeder
790:**Acme Fashion domains:**
797:**Note:** The admin domain (`admin.acme-fashion.test`) routes HTTP requests to the admin panel. However, the actual store context within the admin panel is determined by the session key `current_store_id`, set after login or via the store-switcher. The admin domain itself does not determine store scope.
799:**Acme Electronics domains:**
807:### 3.4 UserSeeder
819:### 3.5 StoreUserSeeder
831:### 3.6 StoreSettingsSeeder
833:**Acme Fashion - settings_json contents:**
842:**Acme Electronics - settings_json contents:**
853:### 3.7 TaxSettingsSeeder
855:**Acme Fashion:**
866:**Acme Electronics:** Same as Acme Fashion.
870:### 3.8 ShippingSeeder
872:**Acme Fashion - Shipping Zones and Rates:**
874:**Zone 1: "Domestic"**
875:- countries_json: ["DE"]
876:- regions_json: []
883:**Zone 2: "EU"**
884:- countries_json: ["AT", "FR", "IT", "ES", "NL", "BE", "PL"]
885:- regions_json: []
891:**Zone 3: "Rest of World"**
892:- countries_json: ["US", "GB", "CA", "AU"]
893:- regions_json: []
899:**Acme Electronics - One zone:**
901:**Zone: "Germany"**
902:- countries_json: ["DE"]
910:### 3.9 CollectionSeeder
912:**Acme Fashion - 4 collections:**
921:**Acme Electronics - 2 collections:**
930:### 3.10 ProductSeeder
932:#### Acme Fashion - 20 Products
938:**Product 1: "Classic Cotton T-Shirt"**
960:- SKU pattern: ACME-CTSH-{SIZE}-{COLOR} (e.g. ACME-CTSH-S-WHT, ACME-CTSH-S-BLK, etc.)
961:- First variant (S/White) is marked is_default = 1
962:- Position increments 0 through 11
976:**Product 2: "Premium Slim Fit Jeans"**
1010:**Product 3: "Organic Hoodie"**
1030:**Product 4: "Leather Belt"**
1050:**Product 5: "Running Sneakers"**
1070:**Product 6: "Graphic Print Tee"**
1090:**Product 7: "V-Neck Linen Tee"**
1110:**Product 8: "Striped Polo Shirt"**
1130:**Product 9: "Cargo Pants"**
1150:**Product 10: "Chino Shorts"**
1170:**Product 11: "Wide Leg Trousers"**
1190:**Product 12: "Wool Scarf"**
1210:**Product 13: "Canvas Tote Bag"**
1230:**Product 14: "Bucket Hat"**
1250:**Product 15: "Unreleased Winter Jacket" -- DRAFT**
1268:**Test purpose:** Verifies draft products do not appear on the storefront.
1272:**Product 16: "Discontinued Raincoat" -- ARCHIVED**
1290:**Test purpose:** Verifies archived products do not appear on the storefront.
1294:**Product 17: "Limited Edition Sneakers" -- SOLD OUT (deny policy)**
1312:**Test purpose:** Tests "sold out" display and add-to-cart prevention.
1316:**Product 18: "Backorder Denim Jacket" -- OUT OF STOCK (continue policy)**
1334:**Test purpose:** Tests that items with "continue" inventory policy can be added to cart despite zero stock.
1338:**Product 19: "Gift Card" -- DIGITAL (no shipping)**
1362:**Test purpose:** Tests digital product flow (no shipping step in checkout).
1366:**Product 20: "Cashmere Overcoat" -- EXPENSIVE**
1384:**Test purpose:** Tests high-value price display (499.99 EUR).
1388:#### Product Summary Table (Acme Fashion)
1413:**Total variants across Acme Fashion:** 107
1417:#### Acme Electronics - 5 Products (minimal seed for tenant isolation)
1431:**Note on product media:** No `ProductMedia` records are seeded. The storefront must gracefully handle products without images by displaying a placeholder (e.g., a neutral SVG placeholder with the store's brand color). E2E tests should not assert on specific image URLs.
1435:### 3.11 DiscountSeeder
1437:**Acme Fashion - 5 discount codes:**
1447:**Behavioral expectations:**
1448:- WELCOME10: Applies 10% off to orders with subtotal >= 20.00 EUR. Already used 3 times (has capacity remaining since no usage_limit).
1449:- FLAT5: Applies flat 5.00 EUR discount. Never used.
1450:- FREESHIP: Removes shipping cost. Used once.
1451:- EXPIRED20: Expired - checkout must reject this code with "discount expired" error.
1452:- MAXED: Usage limit of 5 reached - checkout must reject this code with "discount limit reached" error.
1456:### 3.12 CustomerSeeder
1458:**Acme Fashion - 10 customers:**
1473:**Customer 1 (customer@acme.test) addresses:**
1480:**Customer 2 (jane@example.com) addresses:**
1486:**Customers 3-10:** Each gets one default address with Faker-generated German addresses.
1488:**Acme Electronics - 2 customers (tenant isolation testing):**
1499:### 3.13 OrderSeeder
1501:**Acme Fashion - 15 orders:**
1507:**Order #1001 - Awaiting fulfillment (Credit Card)**
1530:**Order #1002 - Fully delivered (Credit Card)**
1554:**Order #1003 - Partially fulfilled (Credit Card)**
1578:**Order #1004 - Cancelled with full refund (Credit Card)**
1601:**Order #1005 - Bank transfer awaiting payment**
1620:**Test purpose:** Tests the bank transfer flow - admin "Confirm Payment" button, fulfillment guard (cannot fulfill until payment confirmed), and auto-cancel job scenario.
1624:**Order #1006 - Standard paid order (Credit Card)**
1645:**Order #1007 - Multi-item delivered (PayPal)**
1669:**Order #1008 - Partial refund (Credit Card)**
1695:**Order #1009 - Accessories order (Credit Card)**
1717:**Order #1010 - High-value order (PayPal)**
1738:**Order #1011 - Single item delivered (Credit Card)**
1761:**Order #1012 - Multi-quantity order (Credit Card)**
1782:**Order #1013 - Multi-item order (Credit Card)**
1804:**Order #1014 - Digital product order (Credit Card)**
1827:**Order #1015 - Order with discount (Bank Transfer, confirmed)**
--- specs/08-PLAYWRIGHT-E2E-PLAN.md ---
1:# 08 - Playwright E2E Test Plan
7:## Test Suite Summary
33:## Test Infrastructure Requirements
35:### Framework
37:- **Test runner:** Pest v4 with browser testing capabilities (Playwright under the hood)
38:- **Test traits:** Each browser test file must use `TestCase` and `RefreshDatabase`
39:- **Configuration:** Register the `Browser` test directory in `tests/Pest.php` with appropriate base classes
40:- **Base URL:** Configured via `APP_URL` in `.env.testing`, set to `http://acme-fashion.test`
42:### Pest v4 Browser Testing API Reference
58:### Environment Configuration
72:### Prerequisites Before Running
81:## Seeded Data Dependency Table
108:## Test Execution Matrix
144:## Test Directory Structure
172:## Suite 1: Smoke Tests
174:**File:** `tests/Browser/SmokeTest.php`
176:**Purpose:** Hit every major public and admin page, assert they load without JavaScript errors. This is the fastest way to catch broken routes, missing views, or uncaught JS exceptions.
180:### Test 1.1: Loads the storefront home page
182:**Preconditions:** Seeded store
184:**Steps:**
187:**Expected Results:**
188:- Page displays "Acme Fashion"
189:- No JavaScript errors
193:### Test 1.2: Loads a collection page
195:**Preconditions:** Seeded collections
197:**Steps:**
200:**Expected Results:**
201:- Page displays "T-Shirts"
202:- No JavaScript errors
206:### Test 1.3: Loads a product page
208:**Preconditions:** Seeded products
210:**Steps:**
213:**Expected Results:**
214:- Page displays "Classic Cotton T-Shirt"
215:- Page displays "24.99"
216:- No JavaScript errors
220:### Test 1.4: Loads the cart page
222:**Preconditions:** None
224:**Steps:**
227:**Expected Results:**
228:- Page displays "Your Cart"
229:- No JavaScript errors
233:### Test 1.5: Loads the customer login page
235:**Preconditions:** None
237:**Steps:**
240:**Expected Results:**
241:- Page displays "Log in"
242:- No JavaScript errors
246:### Test 1.6: Loads the admin login page
248:**Preconditions:** None
250:**Steps:**
253:**Expected Results:**
254:- Page displays "Sign in"
255:- No JavaScript errors
259:### Test 1.7: Loads the about page
261:**Preconditions:** Seeded pages
263:**Steps:**
266:**Expected Results:**
267:- Page displays "About"
268:- No JavaScript errors
272:### Test 1.8: Loads the search page
274:**Preconditions:** Seeded products
276:**Steps:**
279:**Expected Results:**
280:- Page displays "shirt"
281:- No JavaScript errors
285:### Test 1.9: Loads all collections listing
287:**Preconditions:** Seeded collections
289:**Steps:**
292:**Expected Results:**
293:- Page displays "Collections"
294:- No JavaScript errors
298:### Test 1.10: Has no errors on critical pages (batch)
300:**Preconditions:** All seed data
302:**Steps:**
305:**Expected Results:**
306:- No JavaScript errors on any of the visited pages
308:**Total: 10 tests**
312:## Suite 2: Admin Authentication
314:**File:** `tests/Browser/Admin/AuthenticationTest.php`
316:**Purpose:** Verify the admin login flow, credential validation, session management, logout, and sidebar navigation after successful login.
318:### Authentication Flow
338:### Test 2.1: Can log in as admin
340:**Preconditions:** Seeded admin user
342:**Steps:**
348:**Expected Results:**
349:- Page displays "Dashboard"
350:- No JavaScript errors
354:### Test 2.2: Shows error for invalid credentials
356:**Preconditions:** Seeded admin user
358:**Steps:**
364:**Expected Results:**
365:- Page displays "Invalid credentials"
366:- No JavaScript errors
370:### Test 2.3: Shows error for empty email
372:**Preconditions:** None
374:**Steps:**
379:**Expected Results:**
380:- Validation error referencing "email" is visible
381:- No JavaScript errors
385:### Test 2.4: Shows error for empty password
387:**Preconditions:** None
389:**Steps:**
394:**Expected Results:**
395:- Validation error referencing "password" is visible
396:- No JavaScript errors
400:### Test 2.5: Redirects unauthenticated users to login from dashboard
402:**Preconditions:** Not logged in
404:**Steps:**
407:**Expected Results:**
408:- Page displays "Sign in" (redirected to login form)
409:- No JavaScript errors
413:### Test 2.6: Redirects unauthenticated users to login from products
415:**Preconditions:** Not logged in
417:**Steps:**
420:**Expected Results:**
421:- Page displays "Sign in" (redirected to login form)
422:- No JavaScript errors
426:### Test 2.7: Can log out
428:**Preconditions:** Seeded admin user
430:**Steps:**
439:**Expected Results:**
440:- Page displays "Sign in" (returned to login form)
444:### Test 2.8: Can navigate through admin sidebar sections
446:**Preconditions:** Seeded admin user
448:**Steps:**
464:**Expected Results:**
465:- Each section loads without JavaScript errors
466:- Each section heading is visible after clicking its sidebar link
470:### Test 2.9: Can navigate to analytics from sidebar
472:**Preconditions:** Seeded admin user
474:**Steps:**
481:**Expected Results:**
482:- Page displays "Analytics"
483:- No JavaScript errors
487:### Test 2.10: Can navigate to themes from sidebar
489:**Preconditions:** Seeded admin user
491:**Steps:**
498:**Expected Results:**
499:- Page displays "Themes"
500:- No JavaScript errors
502:**Login Helper Pattern:** Tests 2.7-2.10 require the admin to be logged in first. Each test should perform the full login sequence (visit login page, fill credentials, click sign in, assert dashboard) before proceeding. This ensures test independence.
504:**Total: 10 tests**
508:## Suite 3: Admin Product Management
510:**File:** `tests/Browser/Admin/ProductManagementTest.php`
512:**Purpose:** Verify CRUD operations for products through the admin interface - listing, creating, editing, archiving, searching, and filtering by status.
514:### Product Lifecycle States
528:### Test 3.1: Shows the product list with seeded products
530:**Preconditions:** Logged in as admin, seeded products
532:**Steps:**
539:**Expected Results:**
540:- Page displays "Classic Cotton T-Shirt"
541:- Page displays "Premium Slim Fit Jeans"
542:- No JavaScript errors
546:### Test 3.2: Can create a new product
548:**Preconditions:** Logged in as admin
550:**Steps:**
565:**Expected Results:**
566:- After step 12: page displays "Product saved", no JavaScript errors
567:- After step 13: page displays "Test Product Created by E2E" in the list
571:### Test 3.3: Can edit an existing product title
573:**Preconditions:** Logged in as admin, seeded products
575:**Steps:**
585:**Expected Results:**
586:- After step 7: page displays "Product saved", no JavaScript errors
587:- After step 8: page displays "Classic Cotton T-Shirt Updated" in the list
591:### Test 3.4: Can archive a product
593:**Preconditions:** Logged in as admin
595:**Steps:**
612:**Expected Results:**
613:- After step 9: page displays "Product saved"
614:- After step 14: page displays "Product saved"
615:- After step 15: with default "Active" filter, the archived product should not appear in the list
619:### Test 3.5: Shows draft products only in admin, not storefront
621:**Preconditions:** Logged in as admin, Product #15 exists with draft status
623:**Steps:**
633:**Expected Results:**
634:- Step 4: draft product visible in admin, no JavaScript errors
635:- Step 6: draft product absent from storefront collection, no JavaScript errors
636:- Step 8: draft product absent from search results, no JavaScript errors
640:### Test 3.6: Can search products in admin
642:**Preconditions:** Logged in as admin, seeded products
644:**Steps:**
650:**Expected Results:**
651:- Page displays "Classic Cotton T-Shirt" in filtered results
652:- No JavaScript errors
656:### Test 3.7: Can filter products by status in admin
658:**Preconditions:** Logged in as admin, seeded products including drafts
660:**Steps:**
668:**Expected Results:**
669:- After step 4: only draft products are visible, no JavaScript errors
670:- After step 6: "Classic Cotton T-Shirt" is visible, no JavaScript errors
672:**Total: 7 tests**
676:## Suite 4: Admin Order Management
678:**File:** `tests/Browser/Admin/OrderManagementTest.php`
680:**Purpose:** Verify order listing, status filtering, order detail with timeline, fulfillment creation, and refund processing.
682:### Order State Transitions (via admin actions)
700:### Test 4.1: Shows the order list with seeded orders
702:**Preconditions:** Logged in as admin, seeded orders
704:**Steps:**
709:**Expected Results:**
710:- Page displays "#1001"
711:- No JavaScript errors
715:### Test 4.2: Can filter orders by status
717:**Preconditions:** Logged in as admin, seeded orders
719:**Steps:**
728:**Expected Results:**
729:- After step 4: "#1001" is visible, no JavaScript errors
730:- After step 6: filtered results shown, no JavaScript errors
731:- After step 7: full list shown, no JavaScript errors
735:### Test 4.3: Shows order detail with line items and totals
737:**Preconditions:** Logged in as admin, Order #1001 exists
739:**Steps:**
745:**Expected Results:**
746:- Page displays "#1001"
747:- Page displays "Paid" (financial status)
748:- Page displays "Unfulfilled" (fulfillment status)
749:- Page displays "Classic Cotton T-Shirt" (line item)
750:- Page displays "Subtotal", "Shipping", "Tax", and "Total" sections
751:- No JavaScript errors
--- specs/09-IMPLEMENTATION-ROADMAP.md ---
1:# 09 - Implementation Roadmap and Pest Test Specification
7:## 1. Technology Decisions (Final)
33:## 2. Implementation Phases (Build Order)
37:### Phase Dependency Graph
74:### Phase 1: Foundation (Migrations, Models, Middleware, Auth)
76:**Priority:** CRITICAL - everything depends on this.
78:#### Step 1.1: Environment and Config
100:#### Step 1.2: Core Migrations (Batch 1-2)
113:#### Step 1.3: Core Models
115:**Files to create:**
125:**Relationship definitions:**
137:- Explicit return type declarations on all methods
138:- Proper `$fillable` or `$guarded` arrays
139:- `casts()` method for JSON/enum columns
140:- Factory with meaningful default values
141:- Seeder that creates sample data
143:#### Step 1.4: Enums
145:**Files to create in `app/Enums/`:**
153:#### Step 1.5: Tenant Resolution Middleware
155:**File to create:** `App\Http\Middleware\ResolveStore`
163:**Registration in `bootstrap/app.php`:**
164:- Register middleware group `storefront` containing `ResolveStore`
165:- Register middleware group `admin` containing `ResolveStore`
167:#### Step 1.6: BelongsToStore Trait and Global Scope
169:**Files to create:**
178:#### Step 1.7: Authentication
180:**Admin auth requirements:**
190:**Customer auth requirements:**
201:**Rate limiter registration:**
202:- Register rate limiters in `AppServiceProvider::boot()` for login (5 per minute per IP)
204:#### Step 1.8: Authorization
206:**Files to create:**
223:**Permission Matrix:**
245:### Phase 2: Catalog (Products, Variants, Inventory, Collections, Media)
247:**Priority:** HIGH - storefront and orders depend on this.
249:#### Step 2.1: Migrations (Batch 3-5)
263:#### Step 2.2: Models with Relationships
265:**Files to create:**
277:**Relationship definitions:**
289:**Enums to create:**
300:#### Step 2.3: Product Service
302:**File to create:** `App\Services\ProductService`
311:**File to create:** `App\Services\VariantMatrixService`
317:**File to create:** `App\Support\HandleGenerator`
323:#### Step 2.4: Inventory Service
325:**File to create:** `App\Services\InventoryService`
337:#### Step 2.5: Media Upload
339:**File to create:** `App\Jobs\ProcessMediaUpload`
342:- Resizes images to standard sizes (thumbnail 150x150, medium 600x600, large 1200x1200)
343:- Updates `ProductMedia` status from `processing` to `ready` (or `failed`)
349:### Phase 3: Themes, Pages, Navigation, Storefront Layout
351:**Priority:** HIGH - storefront rendering depends on this.
353:#### Step 3.1: Migrations (Batch 3)
362:#### Step 3.2: Models
364:**Files to create:**
375:**Relationship definitions:**
386:**Enums to create:**
394:#### Step 3.3: Storefront Blade Layout
396:**Directory structure for `resources/views/storefront/`:**
411:**ThemeSettings Service:** Singleton registered in `AppServiceProvider`, loads and caches active theme settings for the current store.
413:#### Step 3.4: Storefront Livewire Components
415:**Files to create:**
429:#### Step 3.5: Navigation Service
431:**File to create:** `App\Services\NavigationService`
442:### Phase 4: Cart, Checkout, Discounts, Shipping, Taxes
444:**Priority:** HIGH - core shopping flow.
446:#### Step 4.1: Migrations (Batch 4-5)
456:#### Step 4.2: Models
458:**Files to create:**
470:**Relationship definitions:**
482:**Enums to create:**
494:#### Step 4.3: Cart Service
496:**File to create:** `App\Services\CartService`
509:#### Step 4.4: Discount Service
511:**File to create:** `App\Services\DiscountService`
520:#### Step 4.5: Shipping Calculator
522:**File to create:** `App\Services\ShippingCalculator`
531:#### Step 4.6: Tax Calculator
533:**File to create:** `App\Services\TaxCalculator`
543:#### Step 4.7: Pricing Engine
545:**File to create:** `App\Services\PricingEngine`
549:**Pipeline steps:**
560:**Value objects to create:**
567:#### Step 4.8: Checkout State Machine
569:**File to create:** `App\Services\CheckoutService`
579:**Files to create:**
586:#### Step 4.9: Storefront Cart/Checkout UI
588:**Files to create:**
599:### Phase 5: Payments, Orders, Fulfillment
601:**Priority:** HIGH - completes the purchase flow.
603:#### Step 5.1: Migrations (Batch 5-7)
614:#### Step 5.2: Models
616:**Files to create:**
629:**Relationship definitions:**
642:**Enums to create:**
654:#### Step 5.3: Payment Service (Mock PSP)
656:**Files to create:**
664:**PaymentProvider interface methods:**
671:**MockPaymentProvider requirements:**
672:- No external API calls, no webhooks, no redirects
673:- Magic card numbers: `4242424242424242` (success), `4000000000000002` (decline), `4000000000009995` (insufficient funds)
674:- Credit card / PayPal: instant capture, returns PaymentResult with status=captured
675:- Bank transfer: deferred capture, returns PaymentResult with status=pending
676:- Generates mock reference IDs (`mock_` + random string)
678:**Registration:** Bind `PaymentProvider` interface to `MockPaymentProvider` in `AppServiceProvider`.
680:#### Step 5.3b: Admin Payment Confirmation
682:**Files to create:**
688:**Admin "Confirm Payment" action** (in OrderService or dedicated action class):
696:#### Step 5.4: Order Service
698:**File to create:** `App\Services\OrderService`
708:#### Step 5.5: Refund Service
710:**File to create:** `App\Services\RefundService`
716:- Partial refunds: `financial_status` -> `partially_refunded`
717:- Full refunds: `financial_status` -> `refunded`
718:- Restock flag: calls `InventoryService::restock()` for each line
720:#### Step 5.6: Fulfillment Service
722:**File to create:** `App\Services\FulfillmentService`
732:**Fulfillment guard:** Before creating a fulfillment, the service checks that the order's `financial_status` is `paid` or `partially_refunded`. If the guard fails, a `FulfillmentGuardException` is thrown. This prevents shipping items before payment is confirmed (especially important for bank transfer orders).
734:**Events to create:**
746:### Phase 6: Customer Accounts
748:**Priority:** MEDIUM - enhances the shopping experience.
750:#### Step 6.1: Customer Auth
752:- `Customer` model with custom `customer` guard (already configured in Phase 1)
753:- Custom `CustomerUserProvider` - scopes queries by `store_id`
754:- Customer email unique per store, not globally
756:#### Step 6.2: Customer Account Pages (Livewire)
758:**Files to create:**
773:### Phase 7: Admin Panel
775:**Priority:** MEDIUM - merchant management interface.
777:#### Step 7.1: Admin Layout
779:**File to create:** `resources/views/livewire/admin/layout/app.blade.php`
781:**Components to create:**
791:#### Step 7.2: Dashboard
793:**File to create:** `Admin\Dashboard` Livewire component
796:- KPI tiles: total sales (period), order count, average order value, conversion rate
797:- Orders chart (last 30 days line chart)
798:- Recent orders table (last 10)
799:- Date range filtering via Livewire properties
801:#### Step 7.3: Product Management
803:**Files to create:**
816:#### Step 7.4: Order Management
818:**Files to create:**
828:#### Step 7.5: Other Admin Sections
830:**Files to create:**
856:### Phase 8: Search
858:**Priority:** LOW - enhances product discovery.
860:#### Step 8.1: Migrations
866:#### Step 8.2: Search Service
868:**File to create:** `App\Services\SearchService`
877:**File to create:** `App\Observers\ProductObserver` - calls `SearchService::syncProduct()` on create/update, `removeProduct()` on delete.
879:#### Step 8.3: Search UI
881:**Files to create:**
890:### Phase 9: Analytics
892:**Priority:** LOW - reporting and insights.
894:#### Step 9.1: Migrations
899:#### Step 9.2: Analytics Service
901:**File to create:** `App\Services\AnalyticsService`
910:**File to create:** `App\Jobs\AggregateAnalytics`
913:- Runs daily via `routes/console.php`
914:- Aggregates raw events into `analytics_daily` rows
915:- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count
919:### Phase 10: Apps and Webhooks
921:**Priority:** LOW - extensibility.
923:#### Step 10.1: Migrations
932:#### Step 10.2: Webhook Service
934:**File to create:** `App\Services\WebhookService`
942:**File to create:** `App\Jobs\DeliverWebhook`
945:- HTTP POST to target URL with JSON payload
946:- HMAC signature in `X-Platform-Signature` header, event type in `X-Platform-Event`, delivery ID in `X-Platform-Delivery-Id`, timestamp in `X-Platform-Timestamp`
947:- Retry with exponential backoff: 1 min, 5 min, 30 min, 2 h, 12 h (6 total attempts: 1 initial + 5 retries; Laravel backoff config `[60, 300, 1800, 7200, 43200]`)
948:- Records response in `webhook_deliveries`
949:- Circuit breaker: after 5 consecutive failures, pause subscription
953:### Phase 11: Polish
955:**Priority:** LOW but important for completeness.
966:### Phase 12: Full Test Suite Execution
968:**Priority:** Runs after all phases are complete.
970:**Verification steps:**
981:## 3. Pest Unit/Feature Test Specification
985:### Test Directory Structure
1007:### Unit Tests
1011:#### tests/Unit/PricingEngineTest.php
1013:**Setup:** Instantiate `PricingEngine` directly. Mock dependencies or use simple value objects.
1034:#### tests/Unit/DiscountCalculatorTest.php
1036:**Setup:** Instantiate `DiscountService` with mock store and cart.
1056:#### tests/Unit/TaxCalculatorTest.php
1058:**Setup:** Instantiate `TaxCalculator` directly.
1072:#### tests/Unit/ShippingCalculatorTest.php
1074:**Setup:** Instantiate `ShippingCalculator` with mock zone/rate data.
1090:#### tests/Unit/CartVersionTest.php
1092:**Setup:** Instantiate Cart model or CartService.
1104:#### tests/Unit/HandleGeneratorTest.php
1106:**Setup:** Instantiate `HandleGenerator` with mock database queries.
1119:### Feature Tests
1123:**Shared helper setup** (in `tests/Pest.php` or trait):
1124:- `createStoreContext()`: creates Organization, Store, StoreDomain, User with Owner role, binds `current_store` in container
1125:- `actingAsAdmin(User $user)`: authenticates as admin and sets store in session
1126:- `actingAsCustomer(Customer $customer)`: authenticates as customer with customer guard
1130:#### tests/Feature/Tenancy/TenantResolutionTest.php
1143:#### tests/Feature/Tenancy/StoreIsolationTest.php
1155:#### tests/Feature/Auth/AdminAuthTest.php
1172:#### tests/Feature/Auth/CustomerAuthTest.php
1189:#### tests/Feature/Auth/SanctumTokenTest.php
1201:#### tests/Feature/Products/ProductCrudTest.php
1221:#### tests/Feature/Products/VariantTest.php
1236:#### tests/Feature/Products/InventoryTest.php
1251:#### tests/Feature/Products/CollectionTest.php
1265:#### tests/Feature/Products/MediaUploadTest.php
1278:#### tests/Feature/Cart/CartServiceTest.php
1297:#### tests/Feature/Cart/CartApiTest.php
1312:#### tests/Feature/Checkout/CheckoutFlowTest.php
1324:#### tests/Feature/Checkout/CheckoutStateTest.php
1340:#### tests/Feature/Checkout/PricingIntegrationTest.php
1352:#### tests/Feature/Checkout/DiscountTest.php
1365:#### tests/Feature/Checkout/ShippingTest.php
1377:#### tests/Feature/Checkout/TaxTest.php
1388:#### tests/Feature/Orders/OrderCreationTest.php
1404:#### tests/Feature/Orders/RefundTest.php
1418:#### tests/Feature/Orders/FulfillmentTest.php
1437:#### tests/Feature/Payments/MockPaymentProviderTest.php
1450:#### tests/Feature/Payments/PaymentServiceTest.php

formatted_output:
Warning: truncated output (original token count: 24971)
Total output lines: 1945

--- specs/01-DATABASE-SCHEMA.md ---
1:# 01 - Database Schema Specification
7:## Conventions
23:## Epic 1 - Foundation and Multi-Store Tenancy
25:### organizations
37:**Indexes:**
39:- `idx_organizations_billing_email` on (`billing_email`) - Supports lookup by billing email
43:### stores
60:**Indexes:**
62:- `idx_stores_handle` UNIQUE on (`handle`) - Ensures globally unique store handles for URL routing
63:- `idx_stores_organization_id` on (`organization_id`) - FK lookup and listing stores by org
64:- `idx_stores_status` on (`status`) - Filtering stores by active/suspended status
68:### store_domains
82:**Indexes:**
84:- `idx_store_domains_hostname` UNIQUE on (`hostname`) - Globally unique hostname for routing
85:- `idx_store_domains_store_id` on (`store_id`) - FK lookup, listing domains per store
86:- `idx_store_domains_store_primary` on (`store_id`, `is_primary`) - Quick lookup of a store's primary domain
90:### users
110:**Indexes:**
112:- `idx_users_email` UNIQUE on (`email`) - Enforces unique email, supports login lookup
113:- `idx_users_status` on (`status`) - Filtering users by active/disabled
115:**Notes:** The `two_factor_*` columns align with the existing migration `2025_08_14_170933_add_two_factor_columns_to_users_table.php`. The `password_hash` column maps to Laravel's default `password` field internally but stores a bcrypt/argon2 hash.
119:### store_users
130:**Primary Key:** (`store_id`, `user_id`)
132:**Indexes:**
134:- `idx_store_users_user_id` on (`user_id`) - Reverse lookup: find all stores for a user
135:- `idx_store_users_role` on (`store_id`, `role`) - Listing users by role within a store
139:### store_settings
149:**Notes:** No additional indexes needed - primary key on `store_id` covers all queries.
153:## Epic 2 - Catalog, Collections, Inventory, and Media
155:### products
174:**Indexes:**
176:- `idx_products_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store for URL routing
177:- `idx_products_store_id` on (`store_id`) - FK lookup, listing all products in a store
178:- `idx_products_store_status` on (`store_id`, `status`) - Filtering products by status within a store
179:- `idx_products_published_at` on (`store_id`, `published_at`) - Sorting/filtering by publication date
180:- `idx_products_vendor` on (`store_id`, `vendor`) - Filtering products by vendor
181:- `idx_products_product_type` on (`store_id`, `product_type`) - Filtering products by type
183:**Notes:** `tags` is a JSON-encoded array (e.g. `["summer","sale"]`). The `handle` is unique per store, not globally.
187:### product_options
198:**Indexes:**
200:- `idx_product_options_product_id` on (`product_id`) - FK lookup, listing options per product
201:- `idx_product_options_product_position` UNIQUE on (`product_id`, `position`) - Enforces unique ordering per product
205:### product_option_values
216:**Indexes:**
218:- `idx_product_option_values_option_id` on (`product_option_id`) - FK lookup, listing values per option
219:- `idx_product_option_values_option_position` UNIQUE on (`product_option_id`, `position`) - Enforces unique ordering per option
223:### product_variants
244:**Indexes:**
246:- `idx_product_variants_product_id` on (`product_id`) - FK lookup, listing variants per product
247:- `idx_product_variants_sku` on (`sku`) - SKU lookup across all variants
248:- `idx_product_variants_barcode` on (`barcode`) - Barcode scanning lookup
249:- `idx_product_variants_product_position` on (`product_id`, `position`) - Ordered variant listing
250:- `idx_product_variants_product_default` on (`product_id`, `is_default`) - Quick lookup of default variant
252:**Notes:** `price_amount` and `compare_at_amount` are in minor currency units (cents). A product always has at least one variant (the default).
256:### variant_option_values
265:**Primary Key:** (`variant_id`, `product_option_value_id`)
267:**Indexes:**
269:- `idx_variant_option_values_value_id` on (`product_option_value_id`) - Reverse lookup: find all variants with a given option value
271:**Notes:** This is a junction table that links variants to the specific option values they represent. Required to resolve which combination of options each variant corresponds to.
275:### inventory_items
288:**Indexes:**
290:- `idx_inventory_items_variant_id` UNIQUE on (`variant_id`) - Enforces one inventory record per variant, fast variant lookup
291:- `idx_inventory_items_store_id` on (`store_id`) - Listing all inventory for a store
293:**Notes:** `quantity_on_hand` is the total physical stock. `quantity_reserved` is stock allocated to open orders not yet fulfilled. Available = on_hand - reserved. Policy `deny` prevents overselling; `continue` allows selling below zero.
297:### collections
313:**Indexes:**
315:- `idx_collections_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store for URL routing
316:- `idx_collections_store_id` on (`store_id`) - FK lookup, listing all collections in a store
317:- `idx_collections_store_status` on (`store_id`, `status`) - Filtering collections by status
321:### collection_products
331:**Primary Key:** (`collection_id`, `product_id`)
333:**Indexes:**
335:- `idx_collection_products_product_id` on (`product_id`) - Reverse lookup: find all collections containing a product
336:- `idx_collection_products_position` on (`collection_id`, `position`) - Ordered product listing within a collection
340:### product_media
359:**Indexes:**
361:- `idx_product_media_product_id` on (`product_id`) - FK lookup, listing media per product
362:- `idx_product_media_product_position` on (`product_id`, `position`) - Ordered media listing
363:- `idx_product_media_status` on (`status`) - Finding media by processing status
367:## Epic 3 - Themes, Storefront, and Localization
369:### themes
384:**Indexes:**
386:- `idx_themes_store_id` on (`store_id`) - FK lookup, listing themes per store
387:- `idx_themes_store_status` on (`store_id`, `status`) - Finding published/draft themes per store
391:### theme_files
404:**Indexes:**
406:- `idx_theme_files_theme_path` UNIQUE on (`theme_id`, `path`) - Enforces unique file paths per theme
407:- `idx_theme_files_theme_id` on (`theme_id`) - FK lookup, listing files per theme
411:### theme_settings
423:### pages
439:**Indexes:**
441:- `idx_pages_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store for URL routing
442:- `idx_pages_store_id` on (`store_id`) - FK lookup, listing pages per store
443:- `idx_pages_store_status` on (`store_id`, `status`) - Filtering pages by status
447:### navigation_menus
460:**Indexes:**
462:- `idx_navigation_menus_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store
463:- `idx_navigation_menus_store_id` on (`store_id`) - FK lookup, listing menus per store
467:### navigation_items
481:**Indexes:**
483:- `idx_navigation_items_menu_id` on (`menu_id`) - FK lookup, listing items per menu
484:- `idx_navigation_items_menu_position` on (`menu_id`, `position`) - Ordered item listing
486:**Notes:** When `type` is `link`, the `url` field is used directly. When `type` is `page`, `collection`, or `product`, the `resource_id` points to the respective table's `id`. The URL is resolved at render time.
490:## Epic 4 - Search
492:### search_settings
505:### search_queries
518:**Indexes:**
520:- `idx_search_queries_store_id` on (`store_id`) - FK lookup
521:- `idx_search_queries_store_created` on (`store_id`, `created_at`) - Time-range queries per store
522:- `idx_search_queries_store_query` on (`store_id`, `query`) - Autocomplete and popular search lookups
526:## Epic 5 - Cart, Checkout, Discounts, Shipping, and Taxes
528:### carts
543:**Indexes:**
545:- `idx_carts_store_id` on (`store_id`) - FK lookup
546:- `idx_carts_customer_id` on (`customer_id`) - Finding carts by customer
547:- `idx_carts_store_status` on (`store_id`, `status`) - Filtering carts by status (e.g. abandoned cart recovery)
549:**Notes:** `cart_version` is incremented on every mutation and used for optimistic concurrency control.
553:### cart_lines
568:**Indexes:**
570:- `idx_cart_lines_cart_id` on (`cart_id`) - FK lookup, listing lines per cart
571:- `idx_cart_lines_cart_variant` UNIQUE on (`cart_id`, `variant_id`) - Prevents duplicate variants in the same cart
573:**Notes:** All `_amount` columns are in minor units (cents). `line_subtotal_amount = unit_price_amount * quantity`. `line_total_amount = line_subtotal_amount - line_discount_amount`.
577:### checkouts
600:**Indexes:**
602:- `idx_checkouts_store_id` on (`store_id`) - FK lookup
603:- `idx_checkouts_cart_id` on (`cart_id`) - Finding checkout for a cart
604:- `idx_checkouts_customer_id` on (`customer_id`) - Listing checkouts per customer
605:- `idx_checkouts_status` on (`store_id`, `status`) - Filtering checkouts by status
606:- `idx_checkouts_expires_at` on (`expires_at`) - Cleanup of expired checkouts
608:**Notes:** `totals_json` contains a snapshot: `{"subtotal": 5000, "discount": 500, "shipping": 799, "tax": 430, "total": 5729}`. `tax_provider_snapshot_json` stores the full tax calculation response for audit purposes.
612:### shipping_zones
624:**Indexes:**
626:- `idx_shipping_zones_store_id` on (`store_id`) - FK lookup, listing zones per store
628:**Notes:** `countries_json` is an array of ISO 3166-1 alpha-2 codes. `regions_json` is an array of region/state codes.
632:### shipping_rates
645:**Indexes:**
647:- `idx_shipping_rates_zone_id` on (`zone_id`) - FK lookup, listing rates per zone
648:- `idx_shipping_rates_zone_active` on (`zone_id`, `is_active`) - Finding active rates per zone
650:**Notes:** `config_json` structure depends on `type`:
651:- Flat: `{"amount": 799}`
652:- Weight: `{"ranges": [{"min_g": 0, "max_g": 500, "amount": 499}]}`
653:- Price: `{"ranges": [{"min_amount": 0, "max_amount": 5000, "amount": 799}]}`
654:- Carrier: `{"carrier": "ups", "service": "ground"}`
658:### tax_settings
672:### discounts
693:**Indexes:**
695:- `idx_discounts_store_code` UNIQUE on (`store_id`, `code`) - Unique discount code per store
696:- `idx_discounts_store_id` on (`store_id`) - FK lookup
697:- `idx_discounts_store_status` on (`store_id`, `status`) - Filtering discounts by status
698:- `idx_discounts_store_type` on (`store_id`, `type`) - Filtering by code vs automatic
700:**Notes:** `value_amount` interpretation depends on `value_type`. For `percent`, it is a whole number (e.g. 15 = 15%). For `fixed`, it is in minor units (cents). For `free_shipping`, it is ignored. `rules_json` may contain minimum purchase amounts, applicable product/collection IDs, customer eligibility, etc.
704:## Epic 6 - Payments, Orders, Fulfillment, and Customers
706:### customers
721:**Indexes:**
723:- `idx_customers_store_email` UNIQUE on (`store_id`, `email`) - Unique email per store
724:- `idx_customers_store_id` on (`store_id`) - FK lookup, listing customers per store
726:**Notes:** `password_hash` is nullable to allow guest checkout customers who may later claim their account. Email is unique per store, not globally.
730:### customer_addresses
742:**Indexes:**
744:- `idx_customer_addresses_customer_id` on (`customer_id`) - FK lookup, listing addresses per customer
745:- `idx_customer_addresses_default` on (`customer_id`, `is_default`) - Quick lookup of default address
747:**Notes:** `address_json` structure: `{"first_name": "", "last_name": "", "company": "", "address1": "", "address2": "", "city": "", "province": "", "province_code": "", "country": "", "country_code": "", "zip": "", "phone": ""}`.
751:### orders
778:**Indexes:**
780:- `idx_orders_store_order_number` UNIQUE on (`store_id`, `order_number`) - Unique order number per store
781:- `idx_orders_store_id` on (`store_id`) - FK lookup
782:- `idx_orders_customer_id` on (`customer_id`) - Listing orders per customer
783:- `idx_orders_store_status` on (`store_id`, `status`) - Filtering by overall status
784:- `idx_orders_store_financial` on (`store_id`, `financial_status`) - Filtering by payment status
785:- `idx_orders_store_fulfillment` on (`store_id`, `fulfillment_status`) - Filtering by shipping status
786:- `idx_orders_placed_at` on (`store_id`, `placed_at`) - Date-range order queries
790:### order_lines
808:**Indexes:**
810:- `idx_order_lines_order_id` on (`order_id`) - FK lookup, listing lines per order
811:- `idx_order_lines_product_id` on (`product_id`) - Finding order lines by product
812:- `idx_order_lines_variant_id` on (`variant_id`) - Finding order lines by variant
814:**Notes:** `product_id` and `variant_id` are nullable with SET NULL on delete so that order history survives product deletion. `tax_lines_json` example: `[{"title": "State Tax", "rate": 800, "amount": 400}]` (rate in basis points, e.g. 800 = 8%). `discount_allocations_json` example: `[{"discount_id": 5, "amount": 250}]`.
818:### payments
835:**Indexes:**
837:- `idx_payments_order_id` on (`order_id`) - FK lookup, listing payments per order
838:- `idx_payments_provider_id` on (`provider`, `provider_payment_id`) - Payment reconciliation by provider ID
839:- `idx_payments_method` on (`method`) - Filtering payments by method
840:- `idx_payments_status` on (`status`) - Filtering payments by status
842:**Notes:** `raw_json_encrypted` stores the mock payment provider response, encrypted at the application layer. `amount` is in minor units (cents). The `provider` is always `mock` since the platform uses a mock PSP (no real payment gateway integration). The `method` column records which payment method the customer selected (credit_card, paypal, or bank_transfer).
846:### refunds
861:**Indexes:**
863:- `idx_refunds_order_id` on (`order_id`) - FK lookup, listing refunds per order
864:- `idx_refunds_payment_id` on (`payment_id`) - Finding refunds per payment
865:- `idx_refunds_status` on (`status`) - Filtering refunds by status
869:### fulfillments
884:**Indexes:**
886:- `idx_fulfillments_order_id` on (`order_id`) - FK lookup, listing fulfillments per order
887:- `idx_fulfillments_status` on (`status`) - Filtering fulfillments by status
888:- `idx_fulfillments_tracking` on (`tracking_company`, `tracking_number`) - Tracking number lookup
892:### fulfillment_lines
903:**Indexes:**
905:- `idx_fulfillment_lines_fulfillment_id` on (`fulfillment_id`) - FK lookup, listing lines per fulfillment
906:- `idx_fulfillment_lines_fulfillment_order_line` UNIQUE on (`fulfillment_id`, `order_line_id`) - Prevents duplicate line entries per fulfillment
908:**Notes:** An order with multiple line items may be partially fulfilled. This table tracks how many units of each line item are included in each shipment.
912:## Epic 7 - Analytics
914:### analytics_events
930:**Indexes:**
932:- `idx_analytics_events_store_id` on (`store_id`) - FK lookup
933:- `idx_analytics_events_store_type` on (`store_id`, `type`) - Filtering events by type per store
934:- `idx_analytics_events_store_created` on (`store_id`, `created_at`) - Time-range event queries
935:- `idx_analytics_events_session` on (`session_id`) - Session-based event grouping
936:- `idx_analytics_events_customer` on (`customer_id`) - Customer activity history
937:- `idx_analytics_events_client_event` UNIQUE on (`store_id`, `client_event_id`) - Client-side event deduplication
939:**Notes:** Event types include: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. This table will grow fast - consider periodic archiving or partitioning by date for production workloads beyond SQLite.
943:### analytics_daily
959:**Primary Key:** (`store_id`, `date`)
961:**Indexes:**
963:- `idx_analytics_daily_store_date` on (`store_id`, `date`) - Time-range metric queries (covered by PK but explicit for clarity)
965:**Notes:** `date` is an ISO-8601 date string (YYYY-MM-DD). `revenue_amount` and `aov_amount` (average order value) are in minor units (cents). This table is populated by a scheduled aggregation job.
969:## Epic 8 - Apps and Webhooks
971:### apps
982:**Indexes:**
984:- `idx_apps_status` on (`status`) - Filtering apps by status
988:### app_installations
1001:**Indexes:**
--- specs/02-API-ROUTES.md ---
1:# API and Routing Specification
9:## 1. Web Routes (Blade/Livewire Pages)
11:### 1.1 Admin Auth Routes
29:### 1.2 Admin Routes
31:**Prefix:** `/admin`
32:**Middleware:** `web`, `auth`, `verified`, `store.resolve`, `role.check`
38:#### Dashboard
44:#### Products
52:#### Inventory
58:#### Collections
66:#### Orders
73:#### Customers
80:#### Discounts
88:#### Settings
96:#### Themes and Online Store
103:#### Pages
111:#### Navigation
117:#### Apps
124:#### Developers
132:#### Analytics
138:#### Search Settings
146:### 1.3 Storefront Routes
148:**Middleware:** `web`, `store.resolve`
162:#### Customer Account Routes
164:**Additional middleware:** `auth:customer`
173:#### Customer Auth Routes (no auth required)
186:### 1.4 Checkout Routes
188:**Middleware:** `web`, `store.resolve`
197:## 2. Storefront REST API
199:**Prefix:** `/api/storefront/v1`
200:**Middleware:** `store.resolve`, `throttle:api.storefront`
201:**Content-Type:** `application/json`
207:### 2.1 Cart Endpoints
209:#### POST /api/storefront/v1/carts
213:- **Authentication:** None (guest) or session-based (logged-in customer)
214:- **Rate limit:** `api.storefront` (120/min per IP)
216:**Request body:**
228:**Success response:** `201 Created`
250:**Error responses:**
251:- `422` - Validation error (invalid currency)
255:#### GET /api/storefront/v1/carts/{cartId}
259:- **Authentication:** None (cart ID acts as token)
260:- **Rate limit:** `api.storefront` (120/min per IP)
262:**URL parameters:**
268:**Success response:** `200 OK`
308:**Error responses:**
309:- `404` - Cart not found
313:#### POST /api/storefront/v1/carts/{cartId}/lines
317:- **Authentication:** None
318:- **Rate limit:** `api.storefront` (120/min per IP)
320:**Request body:**
334:**Success response:** `201 Created`
338:**Error responses:**
339:- `404` - Cart not found
340:- `409` - Cart version conflict (concurrent modification, retry with fresh cart_version)
341:- `422` - Validation error
354:#### PUT /api/storefront/v1/carts/{cartId}/lines/{lineId}
358:- **Authentication:** None
359:- **Rate limit:** `api.storefront` (120/min per IP)
361:**Request body:**
375:**Success response:** `200 OK`
379:**Error responses:**
380:- `404` - Cart or line not found
381:- `409` - Version conflict
382:- `422` - Validation error (e.g., exceeds inventory)
386:#### DELETE /api/storefront/v1/carts/{cartId}/lines/{lineId}
390:- **Authentication:** None
391:- **Rate limit:** `api.storefront` (120/min per IP)
393:**Request body:**
405:**Success response:** `200 OK`
409:**Error responses:**
410:- `404` - Cart or line not found
411:- `409` - Version conflict
415:### 2.2 Checkout Endpoints
417:#### Checkout Flow Sequence Diagram
458:#### POST /api/storefront/v1/checkouts
462:- **Authentication:** None or session-based
463:- **Rate limit:** `checkout` (10/min per session)
465:**Request body:**
479:**Success response:** `201 Created`
518:**Error responses:**
519:- `404` - Cart not found
520:- `422` - Validation errors (empty cart, invalid email)
524:#### GET /api/storefront/v1/checkouts/{checkoutId}
528:- **Authentication:** None (checkout ID acts as token)
529:- **Rate limit:** `checkout` (10/min per session)
531:**Success response:** `200 OK`
535:**Error responses:**
536:- `404` - Checkout not found
537:- `410` - Checkout expired
541:#### PUT /api/storefront/v1/checkouts/{checkoutId}/address
545:- **Authentication:** None
546:- **Rate limit:** `checkout` (10/min per session)
548:**Request body:**
570:**Shipping address fields:**
589:**Success response:** `200 OK`
654:**Error responses:**
655:- `404` - Checkout not found
656:- `410` - Checkout expired
657:- `422` - Validation errors (missing required fields, invalid country code, address not in any shipping zone)
661:#### PUT /api/storefront/v1/checkouts/{checkoutId}/shipping-method
665:- **Authentication:** None
666:- **Rate limit:** `checkout` (10/min per session)
668:**Request body:**
680:**Success response:** `200 OK`
7…14971 tokens truncated…L:** Configured via `APP_URL` in `.env.testing`, set to `http://acme-fashion.test`
42:### Pest v4 Browser Testing API Reference
58:### Environment Configuration
72:### Prerequisites Before Running
81:## Seeded Data Dependency Table
108:## Test Execution Matrix
144:## Test Directory Structure
172:## Suite 1: Smoke Tests
174:**File:** `tests/Browser/SmokeTest.php`
176:**Purpose:** Hit every major public and admin page, assert they load without JavaScript errors. This is the fastest way to catch broken routes, missing views, or uncaught JS exceptions.
180:### Test 1.1: Loads the storefront home page
182:**Preconditions:** Seeded store
184:**Steps:**
187:**Expected Results:**
188:- Page displays "Acme Fashion"
189:- No JavaScript errors
193:### Test 1.2: Loads a collection page
195:**Preconditions:** Seeded collections
197:**Steps:**
200:**Expected Results:**
201:- Page displays "T-Shirts"
202:- No JavaScript errors
206:### Test 1.3: Loads a product page
208:**Preconditions:** Seeded products
210:**Steps:**
213:**Expected Results:**
214:- Page displays "Classic Cotton T-Shirt"
215:- Page displays "24.99"
216:- No JavaScript errors
220:### Test 1.4: Loads the cart page
222:**Preconditions:** None
224:**Steps:**
227:**Expected Results:**
228:- Page displays "Your Cart"
229:- No JavaScript errors
233:### Test 1.5: Loads the customer login page
235:**Preconditions:** None
237:**Steps:**
240:**Expected Results:**
241:- Page displays "Log in"
242:- No JavaScript errors
246:### Test 1.6: Loads the admin login page
248:**Preconditions:** None
250:**Steps:**
253:**Expected Results:**
254:- Page displays "Sign in"
255:- No JavaScript errors
259:### Test 1.7: Loads the about page
261:**Preconditions:** Seeded pages
263:**Steps:**
266:**Expected Results:**
267:- Page displays "About"
268:- No JavaScript errors
272:### Test 1.8: Loads the search page
274:**Preconditions:** Seeded products
276:**Steps:**
279:**Expected Results:**
280:- Page displays "shirt"
281:- No JavaScript errors
285:### Test 1.9: Loads all collections listing
287:**Preconditions:** Seeded collections
289:**Steps:**
292:**Expected Results:**
293:- Page displays "Collections"
294:- No JavaScript errors
298:### Test 1.10: Has no errors on critical pages (batch)
300:**Preconditions:** All seed data
302:**Steps:**
305:**Expected Results:**
306:- No JavaScript errors on any of the visited pages
308:**Total: 10 tests**
312:## Suite 2: Admin Authentication
314:**File:** `tests/Browser/Admin/AuthenticationTest.php`
316:**Purpose:** Verify the admin login flow, credential validation, session management, logout, and sidebar navigation after successful login.
318:### Authentication Flow
338:### Test 2.1: Can log in as admin
340:**Preconditions:** Seeded admin user
342:**Steps:**
348:**Expected Results:**
349:- Page displays "Dashboard"
350:- No JavaScript errors
354:### Test 2.2: Shows error for invalid credentials
356:**Preconditions:** Seeded admin user
358:**Steps:**
364:**Expected Results:**
365:- Page displays "Invalid credentials"
366:- No JavaScript errors
370:### Test 2.3: Shows error for empty email
372:**Preconditions:** None
374:**Steps:**
379:**Expected Results:**
380:- Validation error referencing "email" is visible
381:- No JavaScript errors
385:### Test 2.4: Shows error for empty password
387:**Preconditions:** None
389:**Steps:**
394:**Expected Results:**
395:- Validation error referencing "password" is visible
396:- No JavaScript errors
400:### Test 2.5: Redirects unauthenticated users to login from dashboard
402:**Preconditions:** Not logged in
404:**Steps:**
407:**Expected Results:**
408:- Page displays "Sign in" (redirected to login form)
409:- No JavaScript errors
413:### Test 2.6: Redirects unauthenticated users to login from products
415:**Preconditions:** Not logged in
417:**Steps:**
420:**Expected Results:**
421:- Page displays "Sign in" (redirected to login form)
422:- No JavaScript errors
426:### Test 2.7: Can log out
428:**Preconditions:** Seeded admin user
430:**Steps:**
439:**Expected Results:**
440:- Page displays "Sign in" (returned to login form)
444:### Test 2.8: Can navigate through admin sidebar sections
446:**Preconditions:** Seeded admin user
448:**Steps:**
464:**Expected Results:**
465:- Each section loads without JavaScript errors
466:- Each section heading is visible after clicking its sidebar link
470:### Test 2.9: Can navigate to analytics from sidebar
472:**Preconditions:** Seeded admin user
474:**Steps:**
481:**Expected Results:**
482:- Page displays "Analytics"
483:- No JavaScript errors
487:### Test 2.10: Can navigate to themes from sidebar
489:**Preconditions:** Seeded admin user
491:**Steps:**
498:**Expected Results:**
499:- Page displays "Themes"
500:- No JavaScript errors
502:**Login Helper Pattern:** Tests 2.7-2.10 require the admin to be logged in first. Each test should perform the full login sequence (visit login page, fill credentials, click sign in, assert dashboard) before proceeding. This ensures test independence.
504:**Total: 10 tests**
508:## Suite 3: Admin Product Management
510:**File:** `tests/Browser/Admin/ProductManagementTest.php`
512:**Purpose:** Verify CRUD operations for products through the admin interface - listing, creating, editing, archiving, searching, and filtering by status.
514:### Product Lifecycle States
528:### Test 3.1: Shows the product list with seeded products
530:**Preconditions:** Logged in as admin, seeded products
532:**Steps:**
539:**Expected Results:**
540:- Page displays "Classic Cotton T-Shirt"
541:- Page displays "Premium Slim Fit Jeans"
542:- No JavaScript errors
546:### Test 3.2: Can create a new product
548:**Preconditions:** Logged in as admin
550:**Steps:**
565:**Expected Results:**
566:- After step 12: page displays "Product saved", no JavaScript errors
567:- After step 13: page displays "Test Product Created by E2E" in the list
571:### Test 3.3: Can edit an existing product title
573:**Preconditions:** Logged in as admin, seeded products
575:**Steps:**
585:**Expected Results:**
586:- After step 7: page displays "Product saved", no JavaScript errors
587:- After step 8: page displays "Classic Cotton T-Shirt Updated" in the list
591:### Test 3.4: Can archive a product
593:**Preconditions:** Logged in as admin
595:**Steps:**
612:**Expected Results:**
613:- After step 9: page displays "Product saved"
614:- After step 14: page displays "Product saved"
615:- After step 15: with default "Active" filter, the archived product should not appear in the list
619:### Test 3.5: Shows draft products only in admin, not storefront
621:**Preconditions:** Logged in as admin, Product #15 exists with draft status
623:**Steps:**
633:**Expected Results:**
634:- Step 4: draft product visible in admin, no JavaScript errors
635:- Step 6: draft product absent from storefront collection, no JavaScript errors
636:- Step 8: draft product absent from search results, no JavaScript errors
640:### Test 3.6: Can search products in admin
642:**Preconditions:** Logged in as admin, seeded products
644:**Steps:**
650:**Expected Results:**
651:- Page displays "Classic Cotton T-Shirt" in filtered results
652:- No JavaScript errors
656:### Test 3.7: Can filter products by status in admin
658:**Preconditions:** Logged in as admin, seeded products including drafts
660:**Steps:**
668:**Expected Results:**
669:- After step 4: only draft products are visible, no JavaScript errors
670:- After step 6: "Classic Cotton T-Shirt" is visible, no JavaScript errors
672:**Total: 7 tests**
676:## Suite 4: Admin Order Management
678:**File:** `tests/Browser/Admin/OrderManagementTest.php`
680:**Purpose:** Verify order listing, status filtering, order detail with timeline, fulfillment creation, and refund processing.
682:### Order State Transitions (via admin actions)
700:### Test 4.1: Shows the order list with seeded orders
702:**Preconditions:** Logged in as admin, seeded orders
704:**Steps:**
709:**Expected Results:**
710:- Page displays "#1001"
711:- No JavaScript errors
715:### Test 4.2: Can filter orders by status
717:**Preconditions:** Logged in as admin, seeded orders
719:**Steps:**
728:**Expected Results:**
729:- After step 4: "#1001" is visible, no JavaScript errors
730:- After step 6: filtered results shown, no JavaScript errors
731:- After step 7: full list shown, no JavaScript errors
735:### Test 4.3: Shows order detail with line items and totals
737:**Preconditions:** Logged in as admin, Order #1001 exists
739:**Steps:**
745:**Expected Results:**
746:- Page displays "#1001"
747:- Page displays "Paid" (financial status)
748:- Page displays "Unfulfilled" (fulfillment status)
749:- Page displays "Classic Cotton T-Shirt" (line item)
750:- Page displays "Subtotal", "Shipping", "Tax", and "Total" sections
751:- No JavaScript errors
--- specs/09-IMPLEMENTATION-ROADMAP.md ---
1:# 09 - Implementation Roadmap and Pest Test Specification
7:## 1. Technology Decisions (Final)
33:## 2. Implementation Phases (Build Order)
37:### Phase Dependency Graph
74:### Phase 1: Foundation (Migrations, Models, Middleware, Auth)
76:**Priority:** CRITICAL - everything depends on this.
78:#### Step 1.1: Environment and Config
100:#### Step 1.2: Core Migrations (Batch 1-2)
113:#### Step 1.3: Core Models
115:**Files to create:**
125:**Relationship definitions:**
137:- Explicit return type declarations on all methods
138:- Proper `$fillable` or `$guarded` arrays
139:- `casts()` method for JSON/enum columns
140:- Factory with meaningful default values
141:- Seeder that creates sample data
143:#### Step 1.4: Enums
145:**Files to create in `app/Enums/`:**
153:#### Step 1.5: Tenant Resolution Middleware
155:**File to create:** `App\Http\Middleware\ResolveStore`
163:**Registration in `bootstrap/app.php`:**
164:- Register middleware group `storefront` containing `ResolveStore`
165:- Register middleware group `admin` containing `ResolveStore`
167:#### Step 1.6: BelongsToStore Trait and Global Scope
169:**Files to create:**
178:#### Step 1.7: Authentication
180:**Admin auth requirements:**
190:**Customer auth requirements:**
201:**Rate limiter registration:**
202:- Register rate limiters in `AppServiceProvider::boot()` for login (5 per minute per IP)
204:#### Step 1.8: Authorization
206:**Files to create:**
223:**Permission Matrix:**
245:### Phase 2: Catalog (Products, Variants, Inventory, Collections, Media)
247:**Priority:** HIGH - storefront and orders depend on this.
249:#### Step 2.1: Migrations (Batch 3-5)
263:#### Step 2.2: Models with Relationships
265:**Files to create:**
277:**Relationship definitions:**
289:**Enums to create:**
300:#### Step 2.3: Product Service
302:**File to create:** `App\Services\ProductService`
311:**File to create:** `App\Services\VariantMatrixService`
317:**File to create:** `App\Support\HandleGenerator`
323:#### Step 2.4: Inventory Service
325:**File to create:** `App\Services\InventoryService`
337:#### Step 2.5: Media Upload
339:**File to create:** `App\Jobs\ProcessMediaUpload`
342:- Resizes images to standard sizes (thumbnail 150x150, medium 600x600, large 1200x1200)
343:- Updates `ProductMedia` status from `processing` to `ready` (or `failed`)
349:### Phase 3: Themes, Pages, Navigation, Storefront Layout
351:**Priority:** HIGH - storefront rendering depends on this.
353:#### Step 3.1: Migrations (Batch 3)
362:#### Step 3.2: Models
364:**Files to create:**
375:**Relationship definitions:**
386:**Enums to create:**
394:#### Step 3.3: Storefront Blade Layout
396:**Directory structure for `resources/views/storefront/`:**
411:**ThemeSettings Service:** Singleton registered in `AppServiceProvider`, loads and caches active theme settings for the current store.
413:#### Step 3.4: Storefront Livewire Components
415:**Files to create:**
429:#### Step 3.5: Navigation Service
431:**File to create:** `App\Services\NavigationService`
442:### Phase 4: Cart, Checkout, Discounts, Shipping, Taxes
444:**Priority:** HIGH - core shopping flow.
446:#### Step 4.1: Migrations (Batch 4-5)
456:#### Step 4.2: Models
458:**Files to create:**
470:**Relationship definitions:**
482:**Enums to create:**
494:#### Step 4.3: Cart Service
496:**File to create:** `App\Services\CartService`
509:#### Step 4.4: Discount Service
511:**File to create:** `App\Services\DiscountService`
520:#### Step 4.5: Shipping Calculator
522:**File to create:** `App\Services\ShippingCalculator`
531:#### Step 4.6: Tax Calculator
533:**File to create:** `App\Services\TaxCalculator`
543:#### Step 4.7: Pricing Engine
545:**File to create:** `App\Services\PricingEngine`
549:**Pipeline steps:**
560:**Value objects to create:**
567:#### Step 4.8: Checkout State Machine
569:**File to create:** `App\Services\CheckoutService`
579:**Files to create:**
586:#### Step 4.9: Storefront Cart/Checkout UI
588:**Files to create:**
599:### Phase 5: Payments, Orders, Fulfillment
601:**Priority:** HIGH - completes the purchase flow.
603:#### Step 5.1: Migrations (Batch 5-7)
614:#### Step 5.2: Models
616:**Files to create:**
629:**Relationship definitions:**
642:**Enums to create:**
654:#### Step 5.3: Payment Service (Mock PSP)
656:**Files to create:**
664:**PaymentProvider interface methods:**
671:**MockPaymentProvider requirements:**
672:- No external API calls, no webhooks, no redirects
673:- Magic card numbers: `4242424242424242` (success), `4000000000000002` (decline), `4000000000009995` (insufficient funds)
674:- Credit card / PayPal: instant capture, returns PaymentResult with status=captured
675:- Bank transfer: deferred capture, returns PaymentResult with status=pending
676:- Generates mock reference IDs (`mock_` + random string)
678:**Registration:** Bind `PaymentProvider` interface to `MockPaymentProvider` in `AppServiceProvider`.
680:#### Step 5.3b: Admin Payment Confirmation
682:**Files to create:**
688:**Admin "Confirm Payment" action** (in OrderService or dedicated action class):
696:#### Step 5.4: Order Service
698:**File to create:** `App\Services\OrderService`
708:#### Step 5.5: Refund Service
710:**File to create:** `App\Services\RefundService`
716:- Partial refunds: `financial_status` -> `partially_refunded`
717:- Full refunds: `financial_status` -> `refunded`
718:- Restock flag: calls `InventoryService::restock()` for each line
720:#### Step 5.6: Fulfillment Service
722:**File to create:** `App\Services\FulfillmentService`
732:**Fulfillment guard:** Before creating a fulfillment, the service checks that the order's `financial_status` is `paid` or `partially_refunded`. If the guard fails, a `FulfillmentGuardException` is thrown. This prevents shipping items before payment is confirmed (especially important for bank transfer orders).
734:**Events to create:**
746:### Phase 6: Customer Accounts
748:**Priority:** MEDIUM - enhances the shopping experience.
750:#### Step 6.1: Customer Auth
752:- `Customer` model with custom `customer` guard (already configured in Phase 1)
753:- Custom `CustomerUserProvider` - scopes queries by `store_id`
754:- Customer email unique per store, not globally
756:#### Step 6.2: Customer Account Pages (Livewire)
758:**Files to create:**
773:### Phase 7: Admin Panel
775:**Priority:** MEDIUM - merchant management interface.
777:#### Step 7.1: Admin Layout
779:**File to create:** `resources/views/livewire/admin/layout/app.blade.php`
781:**Components to create:**
791:#### Step 7.2: Dashboard
793:**File to create:** `Admin\Dashboard` Livewire component
796:- KPI tiles: total sales (period), order count, average order value, conversion rate
797:- Orders chart (last 30 days line chart)
798:- Recent orders table (last 10)
799:- Date range filtering via Livewire properties
801:#### Step 7.3: Product Management
803:**Files to create:**
816:#### Step 7.4: Order Management
818:**Files to create:**
828:#### Step 7.5: Other Admin Sections
830:**Files to create:**
856:### Phase 8: Search
858:**Priority:** LOW - enhances product discovery.
860:#### Step 8.1: Migrations
866:#### Step 8.2: Search Service
868:**File to create:** `App\Services\SearchService`
877:**File to create:** `App\Observers\ProductObserver` - calls `SearchService::syncProduct()` on create/update, `removeProduct()` on delete.
879:#### Step 8.3: Search UI
881:**Files to create:**
890:### Phase 9: Analytics
892:**Priority:** LOW - reporting and insights.
894:#### Step 9.1: Migrations
899:#### Step 9.2: Analytics Service
901:**File to create:** `App\Services\AnalyticsService`
910:**File to create:** `App\Jobs\AggregateAnalytics`
913:- Runs daily via `routes/console.php`
914:- Aggregates raw events into `analytics_daily` rows
915:- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count
919:### Phase 10: Apps and Webhooks
921:**Priority:** LOW - extensibility.
923:#### Step 10.1: Migrations
932:#### Step 10.2: Webhook Service
934:**File to create:** `App\Services\WebhookService`
942:**File to create:** `App\Jobs\DeliverWebhook`
945:- HTTP POST to target URL with JSON payload
946:- HMAC signature in `X-Platform-Signature` header, event type in `X-Platform-Event`, delivery ID in `X-Platform-Delivery-Id`, timestamp in `X-Platform-Timestamp`
947:- Retry with exponential backoff: 1 min, 5 min, 30 min, 2 h, 12 h (6 total attempts: 1 initial + 5 retries; Laravel backoff config `[60, 300, 1800, 7200, 43200]`)
948:- Records response in `webhook_deliveries`
949:- Circuit breaker: after 5 consecutive failures, pause subscription
953:### Phase 11: Polish
955:**Priority:** LOW but important for completeness.
966:### Phase 12: Full Test Suite Execution
968:**Priority:** Runs after all phases are complete.
970:**Verification steps:**
981:## 3. Pest Unit/Feature Test Specification
985:### Test Directory Structure
1007:### Unit Tests
1011:#### tests/Unit/PricingEngineTest.php
1013:**Setup:** Instantiate `PricingEngine` directly. Mock dependencies or use simple value objects.
1034:#### tests/Unit/DiscountCalculatorTest.php
1036:**Setup:** Instantiate `DiscountService` with mock store and cart.
1056:#### tests/Unit/TaxCalculatorTest.php
1058:**Setup:** Instantiate `TaxCalculator` directly.
1072:#### tests/Unit/ShippingCalculatorTest.php
1074:**Setup:** Instantiate `ShippingCalculator` with mock zone/rate data.
1090:#### tests/Unit/CartVersionTest.php
1092:**Setup:** Instantiate Cart model or CartService.
1104:#### tests/Unit/HandleGeneratorTest.php
1106:**Setup:** Instantiate `HandleGenerator` with mock database queries.
1119:### Feature Tests
1123:**Shared helper setup** (in `tests/Pest.php` or trait):
1124:- `createStoreContext()`: creates Organization, Store, StoreDomain, User with Owner role, binds `current_store` in container
1125:- `actingAsAdmin(User $user)`: authenticates as admin and sets store in session
1126:- `actingAsCustomer(Customer $customer)`: authenticates as customer with customer guard
1130:#### tests/Feature/Tenancy/TenantResolutionTest.php
1143:#### tests/Feature/Tenancy/StoreIsolationTest.php
1155:#### tests/Feature/Auth/AdminAuthTest.php
1172:#### tests/Feature/Auth/CustomerAuthTest.php
1189:#### tests/Feature/Auth/SanctumTokenTest.php
1201:#### tests/Feature/Products/ProductCrudTest.php
1221:#### tests/Feature/Products/VariantTest.php
1236:#### tests/Feature/Products/InventoryTest.php
1251:#### tests/Feature/Products/CollectionTest.php
1265:#### tests/Feature/Products/MediaUploadTest.php
1278:#### tests/Feature/Cart/CartServiceTest.php
1297:#### tests/Feature/Cart/CartApiTest.php
1312:#### tests/Feature/Checkout/CheckoutFlowTest.php
1324:#### tests/Feature/Checkout/CheckoutStateTest.php
1340:#### tests/Feature/Checkout/PricingIntegrationTest.php
1352:#### tests/Feature/Checkout/DiscountTest.php
1365:#### tests/Feature/Checkout/ShippingTest.php
1377:#### tests/Feature/Checkout/TaxTest.php
1388:#### tests/Feature/Orders/OrderCreationTest.php
1404:#### tests/Feature/Orders/RefundTest.php
1418:#### tests/Feature/Orders/FulfillmentTest.php
1437:#### tests/Feature/Payments/MockPaymentProviderTest.php
1450:#### tests/Feature/Payments/PaymentServiceTest.php