text:
Script completed
Wall time 0.1 seconds
Output:

text:
Warning: truncated output (original token count: 26467)
Total output lines: 2157

--- 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…16467 tokens truncated…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

{
    "$schema": "https://getcomposer.org/schema.json",
    "name": "laravel/livewire-starter-kit",
    "type": "project",
    "description": "The official Laravel starter kit for Livewire.",
    "keywords": [
        "laravel",
        "framework"
    ],
    "license": "MIT",
    "require": {
        "php": "^8.2",
        "laravel/fortify": "^1.30",
        "laravel/framework": "^12.0",
        "laravel/tinker": "^2.10.1",
        "livewire/flux": "^2.9.0",
        "livewire/livewire": "^4.0"
    },
    "require-dev": {
        "fakerphp/faker": "^1.23",
        "laravel/boost": "^2.5",
        "laravel/pail": "^1.2.2",
        "laravel/pint": "^1.24",
        "laravel/sail": "^1.41",
        "mockery/mockery": "^1.6",
        "nunomaduro/collision": "^8.6",
        "pestphp/pest": "^4.3",
        "pestphp/pest-plugin-laravel": "^4.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "scripts": {
        "setup": [
            "composer install",
            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
            "@php artisan key:generate",
            "@php artisan migrate --force",
            "npm install",
            "npm run build"
        ],
        "dev": [
            "Composer\\Config::disableProcessTimeout",
            "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
        ],
        "lint": [
            "pint --parallel"
        ],
        "test:lint": [
            "pint --parallel --test"
        ],
        "test": [
            "@php artisan config:clear --ansi",
            "@test:lint",
            "@php artisan test"
        ],
        "post-autoload-dump": [
            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
            "@php artisan package:discover --ansi"
        ],
        "post-update-cmd": [
            "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
        ],
        "post-root-package-install": [
            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
        ],
        "post-create-project-cmd": [
            "@php artisan key:generate --ansi",
            "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
            "@php artisan migrate --graceful --ansi"
        ],
        "pre-package-uninstall": [
            "Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
        ]
    },
    "extra": {
        "laravel": {
            "dont-discover": []
        }
    },
    "config": {
        "optimize-autoloader": true,
        "preferred-install": "dist",
        "sort-packages": true,
        "allow-plugins": {
            "pestphp/pest-plugin": true,
            "php-http/discovery": true
        }
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}

--- package ---
{
    "$schema": "https://www.schemastore.org/package.json",
    "private": true,
    "type": "module",
    "scripts": {
        "build": "vite build",
        "dev": "vite"
    },
    "dependencies": {
        "@tailwindcss/vite": "^4.1.11",
        "autoprefixer": "^10.4.20",
        "axios": "^1.7.4",
        "concurrently": "^9.0.1",
        "laravel-vite-plugin": "^2.0",
        "tailwindcss": "^4.0.7",
        "vite": "^7.0.4"
    },
    "optionalDependencies": {
        "@rollup/rollup-linux-x64-gnu": "4.9.5",
        "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
        "lightningcss-linux-x64-gnu": "^1.29.1"
    }
}

--- routes ---
<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
})->name('home');

Route::view('dashboard', 'dashboard')
    ->middleware(['auth', 'verified'])
    ->name('dashboard');

require __DIR__.'/settings.php';

--- app service provider ---
<?php

namespace App\Providers;

use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->configureDefaults();
    }

    /**
     * Configure default behaviors for production-ready applications.
     */
    protected function configureDefaults(): void
    {
        Date::use(CarbonImmutable::class);

        DB::prohibitDestructiveCommands(
            app()->isProduction(),
        );

        Password::defaults(fn (): ?Password => app()->isProduction()
            ? Password::min(12)
                ->mixedCase()
                ->letters()
                ->numbers()
                ->symbols()
                ->uncompromised()
            : null
        );
    }
}

--- database files ---
database/factories/UserFactory.php
database/migrations/0001_01_01_000000_create_users_table.php
database/migrations/0001_01_01_000001_create_cache_table.php
database/migrations/0001_01_01_000002_create_jobs_table.php
database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php
database/seeders/DatabaseSeeder.php

5047b3ea (HEAD -> 2026-08-20-codex-gpt-5-6-luna) Init
ca2d83ad Init
fc0e4141 (origin/main, origin/HEAD, main, 2026-06-05-claude-code-deepseek-v4-pro) Revert "Add sonar-project.properties" - exclusions configured in SonarCloud UI instead
a29d6a2d Add sonar-project.properties to suppress S5332 false positives in tests
5cd42872 Initial

--- stats ---
     171
      77


exec

status: completed

input:
const tasks = [
  {
    message:"Audit the repository and every specs/*.md file against current implementation. Do not edit files. Return a concise prioritized gap list, hidden-test likely requirements, and a recommended phase order with exact file/route/model names. Focus on acceptance criteria and contradictions.",
    fork_context:true
  },
  {
    message:"Implement the bounded foundation slice directly in your fork: core tenancy/config/migrations/models/enums/BelongsToStore/ResolveStore and policy scaffolding, following AGENTS.md and existing Laravel conventions. Add focused Pest feature/unit tests for tenant isolation/resolution. Do not touch catalog, cart, checkout, orders, or storefront UI. Run focused tests and report changed files and results.",
    fork_context:true
  },
  {
    message:"Perform a read-only audit of the current starter app and specs focused on Livewire v4 + Flux UI + Tailwind v4 + Pest browser acceptance. Do not edit files. Identify required page/component routes, UI strings, and likely Playwright flows, plus any package/API pitfalls. Return actionable notes.",
    fork_context:true
  }
];
const out = await Promise.all(tasks.map(t => tools.multi_agent_v1__spawn_agent(t)));
out.forEach(text);


type: CollabAgentToolCall

status: completed

agent_nickname: Einstein

prompt: Implement the bounded foundation slice directly in your fork: core tenancy/config/migrations/models/enums/BelongsToStore/ResolveStore and policy scaffolding, following AGENTS.md and existing Laravel conventions. Add focused Pest feature/unit tests for tenant isolation/resolution. Do not touch catalog, cart, checkout, orders, or storefront UI. Run focused tests and report changed files and results.

reasoning_effort: xhigh


type: CollabAgentToolCall

status: completed

agent_nickname: Pasteur

prompt: Audit the repository and every specs/*.md file against current implementation. Do not edit files. Return a concise prioritized gap list, hidden-test likely requirements, and a recommended phase order with exact file/route/model names. Focus on acceptance criteria and contradictions.

reasoning_effort: xhigh


type: CollabAgentToolCall

status: completed

agent_nickname: Sartre

prompt: Perform a read-only audit of the current starter app and specs focused on Livewire v4 + Flux UI + Tailwind v4 + Pest browser acceptance. Do not edit files. Identify required page/component routes, UI strings, and likely Playwright flows, plus any package/API pitfalls. Return actionable notes.

reasoning_effort: xhigh


text:
Script completed
Wall time 0.3 seconds
Output:

text: {"agent_id":"01a020f5-0ae9-7b20-aac3-f4cd917791fb","nickname":"Pasteur"}

text: {"agent_id":"01a020f5-0ab7-7cb3-924d-49c79ac78c69","nickname":"Einstein"}

text: {"agent_id":"01a020f5-0b48-7680-ac22-e8f0dad974d6","nickname":"Sartre"}


status: completed

input:
const results = await Promise.all([
  tools.exec_command({cmd:"sed -n '1,240p' specs/02-API-ROUTES.md; sed -n '240,620p' specs/02-API-ROUTES.md; sed -n '620,1040p' specs/02-API-ROUTES.md",workdir:"/Users/wesner/Herd/shop",yield_time_ms:10000,max_output_tokens:50000}),
  tools.exec_command({cmd:"sed -n '1,260p' specs/05-BUSINESS-LOGIC.md; sed -n '260,620p' specs/05-BUSINESS-LOGIC.md; sed -n '620,1020p' specs/05-BUSINESS-LOGIC.md",workdir:"/Users/wesner/Herd/shop",yield_time_ms:10000,max_output_tokens:50000}),
  tools.exec_command({cmd:"sed -n '1,260p' specs/03-ADMIN-UI.md; sed -n '260,620p' specs/03-ADMIN-UI.md",workdir:"/Users/wesner/Herd/shop",yield_time_ms:10000,max_output_tokens:35000}),
  tools.exec_command({cmd:"sed -n '1,280p' specs/04-STOREFRONT-UI.md; sed -n '280,620p' specs/04-STOREFRONT-UI.md",workdir:"/Users/wesner/Herd/shop",yield_time_ms:10000,max_output_tokens:35000})
]);
for (const r of results) text(r.output);