text:
Script completed
Wall time 0.1 seconds
Output:

text:
Warning: truncated output (original token count: 21646)
Total output lines: 1086

specs/08-PLAYWRIGHT-E2E-PLAN.md-1772-16. Verify "Standard Shipping" and "4.99" are displayed
specs/08-PLAYWRIGHT-E2E-PLAN.md-1773-17. Click "Standard Shipping" to select it
specs/08-PLAYWRIGHT-E2E-PLAN.md-1774-18. Click "Continue"
specs/08-PLAYWRIGHT-E2E-PLAN.md-1775-19. Verify "Credit Card" is pre-selected as payment method
specs/08-PLAYWRIGHT-E2E-PLAN.md:1776:20. Enter card number: `4242 4242 4242 4242`
specs/08-PLAYWRIGHT-E2E-PLAN.md-1777-21. Enter cardholder name: `Test Buyer`
specs/08-PLAYWRIGHT-E2E-PLAN.md-1778-22. Enter expiry: `12/28`
specs/08-PLAYWRIGHT-E2E-PLAN.md-1779-23. Enter CVC: `123`
specs/08-PLAYWRIGHT-E2E-PLAN.md-1780-24. Verify total is 29.98 (24.99 + 4.99 shipping)
--
specs/08-PLAYWRIGHT-E2E-PLAN.md-2014-5. Enter email: `test@example.com`, click "Continue"
specs/08-PLAYWRIGHT-E2E-PLAN.md-2015-6. Fill shipping address (DE), click "Continue"
specs/08-PLAYWRIGHT-E2E-PLAN.md-2016-7. Select "Standard Shipping", click "Continue"
specs/08-PLAYWRIGHT-E2E-PLAN.md-2017-8. Verify "Credit Card" is selected
specs/08-PLAYWRIGHT-E2E-PLAN.md:2018:9. Enter card number: `4000 0000 0000 0002` (magic decline card)
specs/08-PLAYWRIGHT-E2E-PLAN.md-2019-10. Enter cardholder name: `Test Buyer`
specs/08-PLAYWRIGHT-E2E-PLAN.md-2020-11. Enter expiry: `12/28`
specs/08-PLAYWRIGHT-E2E-PLAN.md-2021-12. Enter CVC: `123`
specs/08-PLAYWRIGHT-E2E-PLAN.md-2022-13. Click "Pay now"
--
specs/08-PLAYWRIGHT-E2E-PLAN.md-2039-4. Navigate to `/cart`, click "Checkout"
specs/08-PLAYWRIGHT-E2E-PLAN.md-2040-5. Enter email: `test@example.com`, click "Continue"
specs/08-PLAYWRIGHT-E2E-PLAN.md-2041-6. Fill shipping address (DE), click "Continue"
specs/08-PLAYWRIGHT-E2E-PLAN.md-2042-7. Select "Standard Shipping", click "Continue"
specs/08-PLAYWRIGHT-E2E-PLAN.md:2043:8. Enter card number: `4000 0000 0000 9995` (magic insufficient funds card)
specs/08-PLAYWRIGHT-E2E-PLAN.md-2044-9. Enter cardholder name: `Test Buyer`
specs/08-PLAYWRIGHT-E2E-PLAN.md-2045-10. Enter expiry: `12/28`
specs/08-PLAYWRIGHT-E2E-PLAN.md-2046-11. Enter CVC: `123`
specs/08-PLAYWRIGHT-E2E-PLAN.md-2047-12. Click "Pay now"
--
specs/08-PLAYWRIGHT-E2E-PLAN.md-3163-3. **Pest v4 syntax:** All tests use Pest's `it()` function, NOT class-based PHPUnit. No `$this->` within closures unless accessing Laravel test methods. Use `visit()`, `fill()`, `click()`, `assertSee()` as the primary browser interaction API.
specs/08-PLAYWRIGHT-E2E-PLAN.md-3164-
specs/08-PLAYWRIGHT-E2E-PLAN.md-3165-4. **Variant selection:** When adding products to cart, always select variant options first (e.g., click "M" for size, click "Black" for color) before clicking "Add to cart".
specs/08-PLAYWRIGHT-E2E-PLAN.md-3166-
specs/08-PLAYWRIGHT-E2E-PLAN.md:3167:5. **Payment testing:** All payments are processed in-process via the Mock PSP. For credit card tests, use the magic card number `4242 4242 4242 4242` for success, `4000 0000 0000 0002` for decline, and `4000 0000 0000 9995` for insufficient funds. No external API calls, webhooks, or redirects are involved.
specs/08-PLAYWRIGHT-E2E-PLAN.md-3168-
specs/08-PLAYWRIGHT-E2E-PLAN.md-3169-6. **Viewport testing (Suite 13):** The coding agent should use Pest v4 / Playwright's viewport configuration to set browser dimensions. The exact API for setting viewport in Pest v4 browser tests should be looked up via documentation search.
specs/08-PLAYWRIGHT-E2E-PLAN.md-3170-
specs/08-PLAYWRIGHT-E2E-PLAN.md-3171-7. **Form field names:** The field names used in `fill()` calls (e.g., "email", "password", "first_name", "variants.0.price") are logical names. The coding agent must use the actual HTML field names/labels as rendered by the Livewire components.
--
specs/06-AUTH-AND-SECURITY.md-30-    participant S as Session
specs/06-AUTH-AND-SECURITY.md-31-
specs/06-AUTH-AND-SECURITY.md-32-    B->>LW: Submit email + password
specs/06-AUTH-AND-SECURITY.md-33-    LW->>LW: Validate input (email required, password required)
specs/06-AUTH-AND-SECURITY.md:34:    LW->>LW: Check rate limiter (5/min per IP)
specs/06-AUTH-AND-SECURITY.md-35-    alt Rate limit exceeded
specs/06-AUTH-AND-SECURITY.md-36-        LW-->>B: Flash "Too many attempts. Try again in X seconds."
specs/06-AUTH-AND-SECURITY.md-37-    end
specs/06-AUTH-AND-SECURITY.md-38-    LW->>Auth: attempt(credentials, remember)
--
specs/06-AUTH-AND-SECURITY.md-52-
specs/06-AUTH-AND-SECURITY.md-53-- The login form collects `email` and `password`.
specs/06-AUTH-AND-SECURITY.md-54-- On successful login, the session MUST be regenerated to prevent session fixation attacks.
specs/06-AUTH-AND-SECURITY.md-55-- On failed login, only the generic message "Invalid credentials" is shown. The system must not indicate whether the email or password was wrong.
specs/06-AUTH-AND-SECURITY.md:56:- Rate limiting: maximum 5 attempts per minute, keyed by IP address. Uses the `login` rate limiter.
specs/06-AUTH-AND-SECURITY.md-57-- After login: redirect to `/admin` (the dashboard).
specs/06-AUTH-AND-SECURITY.md-58-
specs/06-AUTH-AND-SECURITY.md-59-#### Remember Me
specs/06-AUTH-AND-SECURITY.md-60-
--
specs/06-AUTH-AND-SECURITY.md-76-    App-->>B: Render forgot-password form
specs/06-AUTH-AND-SECURITY.md-77-    B->>App: POST /admin/forgot-password (email)
specs/06-AUTH-AND-SECURITY.md-78-    App->>DB: Look up user by email
specs/06-AUTH-AND-SECURITY.md-79-    alt User exists
specs/06-AUTH-AND-SECURITY.md:80:        App->>DB: Store reset token in password_reset_tokens table
specs/06-AUTH-AND-SECURITY.md-81-        App->>Mail: Send reset link email
specs/06-AUTH-AND-SECURITY.md-82-    end
specs/06-AUTH-AND-SECURITY.md-83-    App-->>B: "If that email exists, we sent a reset link." (generic)
specs/06-AUTH-AND-SECURITY.md-84-    B->>App: GET /admin/reset-password/{token}
--
specs/06-AUTH-AND-SECURITY.md-96-
specs/06-AUTH-AND-SECURITY.md-97-**Requirements:**
specs/06-AUTH-AND-SECURITY.md-98-
specs/06-AUTH-AND-SECURITY.md-99-- Uses Laravel's built-in password broker (`users` broker).
specs/06-AUTH-AND-SECURITY.md:100:- Reset tokens stored in the `password_reset_tokens` table.
specs/06-AUTH-AND-SECURITY.md-101-- Token expiration: 60 minutes (configurable in `config/auth.php` under `passwords.users.expire`).
specs/06-AUTH-AND-SECURITY.md-102-- Throttle: one reset email per 60 seconds per email address.
specs/06-AUTH-AND-SECURITY.md-103-- The "forgot password" response must be generic (do not reveal whether email exists).
specs/06-AUTH-AND-SECURITY.md-104-
--
specs/06-AUTH-AND-SECURITY.md-140-
specs/06-AUTH-AND-SECURITY.md-141-    Note over Store: Store already resolved from hostname
specs/06-AUTH-AND-SECURITY.md-142-    B->>LW: Submit email + password
specs/06-AUTH-AND-SECURITY.md-143-    LW->>LW: Validate input
specs/06-AUTH-AND-SECURITY.md:144:    LW->>LW: Check rate limiter (5/min per IP)
specs/06-AUTH-AND-SECURITY.md-145-    LW->>Auth: attempt(credentials + store_id, remember)
specs/06-AUTH-AND-SECURITY.md-146-    Auth->>DB: SELECT from customers WHERE email = ? AND store_id = ?
specs/06-AUTH-AND-SECURITY.md-147-    Auth->>Auth: Verify bcrypt hash
specs/06-AUTH-AND-SECURITY.md-148-    alt Valid
--
specs/06-AUTH-AND-SECURITY.md-158-
specs/06-AUTH-AND-SECURITY.md-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.
specs/06-AUTH-AND-SECURITY.md-160-- The custom `CustomerUserProvider` must inject `store_id` into the credential check so the query filters by both `email` and `store_id`.
specs/06-AUTH-AND-SECURITY.md-161-- The same email address can exist in different stores (multi-tenant isolation).
specs/06-AUTH-AND-SECURITY.md:162:- Rate limiting: maximum 5 attempts per minute per IP, using the same `login` rate limiter.
specs/06-AUTH-AND-SECURITY.md-163-- After login: redirect to `/account` or the intended URL (if the customer was redirected from a protected page).
specs/06-AUTH-AND-SECURITY.md-164-
specs/06-AUTH-AND-SECURITY.md-165-#### Customer Registration Flow
specs/06-AUTH-AND-SECURITY.md-166-
--
specs/06-AUTH-AND-SECURITY.md-193-
specs/06-AUTH-AND-SECURITY.md-194-#### Customer Password Reset
specs/06-AUTH-AND-SECURITY.md-195-
specs/06-AUTH-AND-SECURITY.md-196-- Uses a separate password broker: `customers` broker.
specs/06-AUTH-AND-SECURITY.md:197:- Tokens stored in the `customer_password_reset_tokens` table (separate from admin tokens).
specs/06-AUTH-AND-SECURITY.md-198-- Token records include `store_id` to scope resets to the correct store.
specs/06-AUTH-AND-SECURITY.md-199-- Routes: `GET /forgot-password`, `POST /forgot-password`, `GET /reset-password/{token}`, `POST /reset-password`.
specs/06-AUTH-AND-SECURITY.md-200-- Token expiration: 60 minutes.
specs/06-AUTH-AND-SECURITY.md-201-- Throttle: one reset email per 60 seconds.
--
specs/06-AUTH-AND-SECURITY.md-288-#### Password Brokers Data Dictionary
specs/06-AUTH-AND-SECURITY.md-289-
specs/06-AUTH-AND-SECURITY.md-290-| Broker Name | Provider | Token Table | Expiry | Throttle |
specs/06-AUTH-AND-SECURITY.md-291-|-------------|----------|-------------|--------|----------|
specs/06-AUTH-AND-SECURITY.md:292:| `users` | `users` | `password_reset_tokens` | 60 min | 60 sec |
specs/06-AUTH-AND-SECURITY.md:293:| `customers` | `customers` | `customer_password_reset_tokens` | 60 min | 60 sec |
specs/06-AUTH-AND-SECURITY.md-294-
specs/06-AUTH-AND-SECURITY.md-295-**Defaults:** The default guard is `web` and the default password broker is `users`.
specs/06-AUTH-AND-SECURITY.md-296-
specs/06-AUTH-AND-SECURITY.md-297----
--
specs/06-AUTH-AND-SECURITY.md-548-| Gate Name | Required Roles | Purpose |
specs/06-AUTH-AND-SECURITY.md-549-|-----------|---------------|---------|
specs/06-AUTH-AND-SECURITY.md-550-| `manage-store-settings` | Owner, Admin | Access store settings pages |
specs/06-AUTH-AND-SECURITY.md-551-| `manage-staff` | Owner, Admin | Invite, update, remove staff members |
specs/06-AUTH-AND-SECURITY.md:552:| `manage-developers` | Owner, Admin | Create/revoke API tokens, manage webhooks |
specs/06-AUTH-AND-SECURITY.md-553-| `view-analytics` | Owner, Admin, Staff | View analytics dashboard and reports |
specs/06-AUTH-AND-SECURITY.md-554-| `manage-shipping` | Owner, Admin | Manage shipping zones, rates, profiles |
specs/06-AUTH-AND-SECURITY.md-555-| `manage-taxes` | Owner, Admin | Manage tax settings and rates |
specs/06-AUTH-AND-SECURITY.md-556-| `manage-search-settings` | Owner, Admin | Configure search weights and synonyms |
--
specs/06-AUTH-AND-SECURITY.md-578-#### In API Controllers
specs/06-AUTH-AND-SECURITY.md-579-
specs/06-AUTH-AND-SECURITY.md-580-- Use `$this->authorize('action', $model)` at the start of each controller method.
specs/06-AUTH-AND-SECURITY.md-581-- Alternatively, use `can` middleware on routes for simpler checks.
specs/06-AUTH-AND-SECURITY.md:582:- Form Requests also implement `authorize()` to check the relevant policy or gate before validation runs.
specs/06-AUTH-AND-SECURITY.md-583-
specs/06-AUTH-AND-SECURITY.md-584-#### In Blade Templates
specs/06-AUTH-AND-SECURITY.md-585-
specs/06-AUTH-AND-SECURITY.md-586-- Use `@can('gate-name')` / `@cannot` directives to conditionally show/hide navigation items and action buttons.
--
specs/06-AUTH-AND-SECURITY.md-613-| Admin API | `api`, `auth:sanctum`, `store.resolve`, `throttle:api.admin` | REST API endpoints |
specs/06-AUTH-AND-SECURITY.md-614-| Storefront web (public) | `web`, `store.resolve` | Public store pages (catalog, product, cart) |
specs/06-AUTH-AND-SECURITY.md-615-| Storefront web (customer) | `web`, `store.resolve`, `auth:customer` | Customer account pages |
specs/06-AUTH-AND-SECURITY.md-616-| Storefront API | `api`, `store.resolve`, `throttle:api.storefront` | Cart and checkout API |
specs/06-AUTH-AND-SECURITY.md:617:| Webhook routes | `api`, `throttle:webhooks` | Reserved for outbound app webhooks (no inbound payment webhooks - Mock PSP is in-process) |
specs/06-AUTH-AND-SECURITY.md-618-
specs/06-AUTH-AND-SECURITY.md-619----
specs/06-AUTH-AND-SECURITY.md-620-
specs/06-AUTH-AND-SECURITY.md-621-### 3.3 Custom Middleware Specifications
--
specs/06-AUTH-AND-SECURITY.md-730----
specs/06-AUTH-AND-SECURITY.md-731-
specs/06-AUTH-AND-SECURITY.md-732-### 4.2 Rate Limiting
specs/06-AUTH-AND-SECURITY.md-733-
specs/06-AUTH-AND-SECURITY.md:734:All rate limiters are defined in `bootstrap/app.php` (or `AppServiceProvider::boot()`).
specs/06-AUTH-AND-SECURITY.md-735-
specs/06-AUTH-AND-SECURITY.md-736-| Limiter Name | Limit | Keyed By | Applied To |
specs/06-AUTH-AND-SECURITY.md-737-|-------------|-------|----------|------------|
specs/06-AUTH-AND-SECURITY.md-738-| `login` | 5 per minute | IP address | Admin login, customer login |
--
specs/06-AUTH-AND-SECURITY.md-740-| `api.storefront` | 120 per minute | IP address | Public storefront API (cart, search) |
specs/06-AUTH-AND-SECURITY.md-741-| `checkout` | 10 per minute | Session ID | Checkout-related operations |
specs/06-AUTH-AND-SECURITY.md-742-| `search` | 30 per minute | IP address | Search queries |
specs/06-AUTH-AND-SECURITY.md-743-| `analytics` | 60 per minute | IP address | Analytics event ingestion |
specs/06-AUTH-AND-SECURITY.md:744:| `webhooks` | 100 per minute | IP address | Outbound app webhook endpoints (reserved; no inbound payment webhooks - Mock PSP is in-process) |
specs/06-AUTH-AND-SECURITY.md-745-
specs/06-AUTH-AND-SECURITY.md-746-**Rate limit response headers** (returned on all API responses by Laravel default):
specs/06-AUTH-AND-SECURITY.md-747-
specs/06-AUTH-AND-SECURITY.md-748-| Header | Description |
specs/06-AUTH-AND-SECURITY.md-749-|--------|-------------|
specs/06-AUTH-AND-SECURITY.md-750-| `X-RateLimit-Limit` | Maximum requests allowed in the window |
specs/06-AUTH-AND-SECURITY.md-751-| `X-RateLimit-Remaining` | Requests remaining in the current window |
specs/06-AUTH-AND-SECURITY.md:752:| `Retry-After` | Seconds until the rate limit resets (only on 429 responses) |
specs/06-AUTH-AND-SECURITY.md-753-
specs/06-AUTH-AND-SECURITY.md:754:When a rate limit is exceeded, the response is HTTP 429 Too Many Requests.
specs/06-AUTH-AND-SECURITY.md-755-
specs/06-AUTH-AND-SECURITY.md-756----
specs/06-AUTH-AND-SECURITY.md-757-
specs/06-AUTH-AND-SECURITY.md-758-### 4.3 Encryption at Rest
--
specs/06-AUTH-AND-SECURITY.md-763-
specs/06-AUTH-AND-SECURITY.md-764-| Model | Column | Cast Type | Purpose |
specs/06-AUTH-AND-SECURITY.md-765-|-------|--------|-----------|---------|
specs/06-AUTH-AND-SECURITY.md-766-| OauthClient | `client_secret_encrypted` | `encrypted` | Third-party OAuth client secrets |
specs/06-AUTH-AND-SECURITY.md:767:| WebhookSubscription | `signing_secret_encrypted` | `encrypted` | Per-subscription signing secrets for outbound webhooks |
specs/06-AUTH-AND-SECURITY.md-768-| Payment | `raw_json` | `encrypted:array` | Full payment gateway response (may contain sensitive card/account data) |
specs/06-AUTH-AND-SECURITY.md-769-
specs/06-AUTH-AND-SECURITY.md-770-**Requirements:**
specs/06-AUTH-AND-SECURITY.md-771-
--
specs/06-AUTH-AND-SECURITY.md-776----
specs/06-AUTH-AND-SECURITY.md-777-
specs/06-AUTH-AND-SECURITY.md-778-### 4.4 Input Validation
specs/06-AUTH-AND-SECURITY.md-779-
specs/06-AUTH-AND-SECURITY.md:780:All write endpoints must use dedicated Form Request classes. Validation rules use string syntax per project convention.
specs/06-AUTH-AND-SECURITY.md-781-
specs/06-AUTH-AND-SECURITY.md:782:#### Form Request Inventory
specs/06-AUTH-AND-SECURITY.md-783-
specs/06-AUTH-AND-SECURITY.md:784:| Form Request Class | Used By | Key Validation Rules |
specs/06-AUTH-AND-SECURITY.md-785-|-------------------|---------|---------------------|
specs/06-AUTH-AND-SECURITY.md-786-| `StoreProductRequest` | Create product | `title`: required, string, max:255. `description_html`: nullable, string. `status`: required, in:active,draft,archived. `vendor`: nullable, string. Price fields: required, integer, min:0. |
specs/06-AUTH-AND-SECURITY.md-787-| `UpdateProductRequest` | Update product | Same fields as StoreProductRequest but all optional (sometimes rule). |
specs/06-AUTH-AND-SECURITY.md-788-| `StoreCollectionRequest` | Create collection | `title`: required, string, max:255. `type`: required, in:manual,automated. `rules_json`: required_if:type,automated, valid JSON. |
--
specs/06-AUTH-AND-SECURITY.md-797-| `InviteStaffRequest` | Invite staff member | `email`: required, valid email. `role`: required, in:admin,staff,support (not owner). |
specs/06-AUTH-AND-SECURITY.md-798-| `RegisterCustomerRequest` | Customer registration | `name`: required. `email`: required, email, unique per store (composite). `password`: required, min:8, confirmed. |
specs/06-AUTH-AND-SECURITY.md-799-| `LoginRequest` | Admin/customer login | `email`: required, email. `password`: required, string. |
specs/06-AUTH-AND-SECURITY.md-800-
specs/06-AUTH-AND-SECURITY.md:801:**Authorization in Form Requests:**
specs/06-AUTH-AND-SECURITY.md-802-
specs/06-AUTH-AND-SECURITY.md:803:Each Form Request's `authorize()` method must check the relevant policy or gate. For example, `StoreProductRequest::authorize()` checks whether the user can create a Product. If authorization fails, the request is rejected with 403 before validation even runs.
specs/06-AUTH-AND-SECURITY.md-804-
specs/06-AUTH-AND-SECURITY.md-805----
specs/06-AUTH-AND-SECURITY.md-806-
specs/06-AUTH-AND-SECURITY.md-807-### 4.5 XSS Prevention
--
specs/06-AUTH-AND-SECURITY.md-912-| `partitioned` | `false` | Not needed for this use case. |
specs/06-AUTH-AND-SECURITY.md-913-
specs/06-AUTH-AND-SECURITY.md-914----
specs/06-AUTH-AND-SECURITY.md-915-
specs/06-AUTH-AND-SECURITY.md:916:## 6. CORS Configuration
specs/06-AUTH-AND-SECURITY.md-917-
specs/06-AUTH-AND-SECURITY.md:918:File: `config/cors.php`
specs/06-AUTH-AND-SECURITY.md-919-
specs/06-AUTH-AND-SECURITY.md-920-| Setting | Value | Rationale |
specs/06-AUTH-AND-SECURITY.md-921-|---------|-------|-----------|
specs/06-AUTH-AND-SECURITY.md:922:| `paths` | `['api/*', 'sanctum/csrf-cookie']` | Only API and Sanctum CSRF cookie endpoints are CORS-enabled. Web routes are not. |
specs/06-AUTH-AND-SECURITY.md-923-| `allowed_methods` | `['*']` | All HTTP methods allowed for API. |
specs/06-AUTH-AND-SECURITY.md-924-| `allowed_origins` | `[FRONTEND_URL env var, default '*']` | Restrict to store domain in production. Wildcard for development. |
specs/06-AUTH-AND-SECURITY.md-925-| `allowed_origins_patterns` | `[]` | No pattern-based origin matching needed. |
specs/06-AUTH-AND-SECURITY.md-926-| `allowed_headers` | `['*']` | All headers allowed (Authorization, Content-Type, etc.). |
specs/06-AUTH-AND-SECURITY.md:927:| `exposed_headers` | `['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'Retry-After']` | Expose rate limit headers so API clients can implement backoff. |
specs/06-AUTH-AND-SECURITY.md-928-| `max_age` | `0` | No preflight caching (conservative default). |
specs/06-AUTH-AND-SECURITY.md-929-| `supports_credentials` | `true` | Required for Sanctum SPA auth (cookies). |
specs/06-AUTH-AND-SECURITY.md-930-
specs/06-AUTH-AND-SECURITY.md-931----
--
specs/06-AUTH-AND-SECURITY.md-933-## 7. Webhook Signature Verification
specs/06-AUTH-AND-SECURITY.md-934-
specs/06-AUTH-AND-SECURITY.md-935-### 7.1 Inbound Payment Webhooks
specs/06-AUTH-AND-SECURITY.md-936-
specs/06-AUTH-AND-SECURITY.md:937:> **Not applicable.** All payment processing happens in-process via the Mock PSP. There are no inbound payment webhooks from external providers (no Stripe, no PayPal). This section is preserved for reference only -- if a real PSP is integrated in the future, inbound webhook verification would follow the patterns described in the outbound webhook section below (HMAC signature verification).
specs/06-AUTH-AND-SECURITY.md-938-
specs/06-AUTH-AND-SECURITY.md-939----
specs/06-AUTH-AND-SECURITY.md-940-
specs/06-AUTH-AND-SECURITY.md-941-### 7.2 Platform Webhooks (Outbound)
specs/06-AUTH-AND-SECURITY.md-942-
specs/06-AUTH-AND-SECURITY.md-943-Service: `App\Services\WebhookDispatcher`
specs/06-AUTH-AND-SECURITY.md-944-
specs/06-AUTH-AND-SECURITY.md:945:When the platform sends we…11646 tokens truncated…ABASE-SCHEMA.md-1176-| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
specs/01-DATABASE-SCHEMA.md-1177-| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
specs/01-DATABASE-SCHEMA.md-1178-
specs/01-DATABASE-SCHEMA.md:1179:### password_reset_tokens
specs/01-DATABASE-SCHEMA.md-1180-
specs/01-DATABASE-SCHEMA.md-1181-| Column | Type | Nullable | Default | Constraints | Notes |
specs/01-DATABASE-SCHEMA.md-1182-|---|---|---|---|---|---|
specs/01-DATABASE-SCHEMA.md-1183-| email | TEXT | No | - | PRIMARY KEY | Admin user email |
--
specs/01-DATABASE-SCHEMA.md-1185-| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
specs/01-DATABASE-SCHEMA.md-1186-
specs/01-DATABASE-SCHEMA.md-1187----
specs/01-DATABASE-SCHEMA.md-1188-
specs/01-DATABASE-SCHEMA.md:1189:### customer_password_reset_tokens
specs/01-DATABASE-SCHEMA.md-1190-
specs/01-DATABASE-SCHEMA.md-1191-| Column | Type | Nullable | Default | Constraints | Notes |
specs/01-DATABASE-SCHEMA.md-1192-|---|---|---|---|---|---|
specs/01-DATABASE-SCHEMA.md-1193-| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
--
specs/01-DATABASE-SCHEMA.md-1320-```mermaid
specs/01-DATABASE-SCHEMA.md-1321-erDiagram
specs/01-DATABASE-SCHEMA.md-1322-    stores ||--o{ themes : "has"
specs/01-DATABASE-SCHEMA.md-1323-    themes ||--o{ theme_files : "contains"
specs/01-DATABASE-SCHEMA.md:1324:    themes ||--|| theme_settings : "configured by"
specs/01-DATABASE-SCHEMA.md-1325-    stores ||--o{ pages : "has"
specs/01-DATABASE-SCHEMA.md-1326-    stores ||--o{ navigation_menus : "has"
specs/01-DATABASE-SCHEMA.md-1327-    navigation_menus ||--o{ navigation_items : "contains"
specs/01-DATABASE-SCHEMA.md-1328-
--
specs/01-DATABASE-SCHEMA.md-1337-        INTEGER theme_id FK
specs/01-DATABASE-SCHEMA.md-1338-        TEXT path
specs/01-DATABASE-SCHEMA.md-1339-        TEXT storage_key
specs/01-DATABASE-SCHEMA.md-1340-    }
specs/01-DATABASE-SCHEMA.md:1341:    theme_settings {
specs/01-DATABASE-SCHEMA.md-1342-        INTEGER theme_id PK
specs/01-DATABASE-SCHEMA.md-1343-        TEXT settings_json
specs/01-DATABASE-SCHEMA.md-1344-    }
specs/01-DATABASE-SCHEMA.md-1345-    pages {
--
specs/01-DATABASE-SCHEMA.md-1435-    apps ||--o{ app_installations : "installed via"
specs/01-DATABASE-SCHEMA.md-1436-    stores ||--o{ app_installations : "has"
specs/01-DATABASE-SCHEMA.md-1437-    apps ||--o{ oauth_clients : "authenticates via"
specs/01-DATABASE-SCHEMA.md-1438-    app_installations ||--o{ oauth_tokens : "issues"
specs/01-DATABASE-SCHEMA.md:1439:    stores ||--o{ webhook_subscriptions : "registers"
specs/01-DATABASE-SCHEMA.md:1440:    app_installations ||--o{ webhook_subscriptions : "owns"
specs/01-DATABASE-SCHEMA.md:1441:    webhook_subscriptions ||--o{ webhook_deliveries : "delivers"
specs/01-DATABASE-SCHEMA.md-1442-
specs/01-DATABASE-SCHEMA.md-1443-    analytics_events {
specs/01-DATABASE-SCHEMA.md-1444-        INTEGER id PK
specs/01-DATABASE-SCHEMA.md-1445-        INTEGER store_id FK
--
specs/01-DATABASE-SCHEMA.md-1462-        INTEGER store_id FK
specs/01-DATABASE-SCHEMA.md-1463-        INTEGER app_id FK
specs/01-DATABASE-SCHEMA.md-1464-        TEXT status
specs/01-DATABASE-SCHEMA.md-1465-    }
specs/01-DATABASE-SCHEMA.md:1466:    webhook_subscriptions {
specs/01-DATABASE-SCHEMA.md-1467-        INTEGER id PK
specs/01-DATABASE-SCHEMA.md-1468-        INTEGER store_id FK
specs/01-DATABASE-SCHEMA.md-1469-        TEXT event_type
specs/01-DATABASE-SCHEMA.md-1470-        TEXT target_url
specs/01-DATABASE-SCHEMA.md-1471-    }
specs/01-DATABASE-SCHEMA.md:1472:    webhook_deliveries {
specs/01-DATABASE-SCHEMA.md-1473-        INTEGER id PK
specs/01-DATABASE-SCHEMA.md-1474-        INTEGER subscription_id FK
specs/01-DATABASE-SCHEMA.md-1475-        TEXT event_id
specs/01-DATABASE-SCHEMA.md-1476-        TEXT status
--
specs/01-DATABASE-SCHEMA.md-1546-        customer_addresses
specs/01-DATABASE-SCHEMA.md-1547-        carts
specs/01-DATABASE-SCHEMA.md-1548-        navigation_items
specs/01-DATABASE-SCHEMA.md-1549-        theme_files
specs/01-DATABASE-SCHEMA.md:1550:        theme_settings
specs/01-DATABASE-SCHEMA.md-1551-        search_queries
specs/01-DATABASE-SCHEMA.md-1552-        shipping_rates
specs/01-DATABASE-SCHEMA.md-1553-        oauth_tokens
specs/01-DATABASE-SCHEMA.md:1554:        webhook_subscriptions
specs/01-DATABASE-SCHEMA.md-1555-    end
specs/01-DATABASE-SCHEMA.md-1556-
specs/01-DATABASE-SCHEMA.md-1557-    subgraph "Batch 5 - Depends on Batch 4"
specs/01-DATABASE-SCHEMA.md-1558-        product_option_values
--
specs/01-DATABASE-SCHEMA.md-1561-        checkouts
specs/01-DATABASE-SCHEMA.md-1562-        orders
specs/01-DATABASE-SCHEMA.md-1563-        analytics_events
specs/01-DATABASE-SCHEMA.md-1564-        analytics_daily
specs/01-DATABASE-SCHEMA.md:1565:        webhook_deliveries
specs/01-DATABASE-SCHEMA.md-1566-    end
specs/01-DATABASE-SCHEMA.md-1567-
specs/01-DATABASE-SCHEMA.md-1568-    subgraph "Batch 6 - Depends on Batch 5"
specs/01-DATABASE-SCHEMA.md-1569-        variant_option_values
--
specs/01-DATABASE-SCHEMA.md-1604-    stores --> carts
specs/01-DATABASE-SCHEMA.md-1605-    customers --> carts
specs/01-DATABASE-SCHEMA.md-1606-    navigation_menus --> navigation_items
specs/01-DATABASE-SCHEMA.md-1607-    themes --> theme_files
specs/01-DATABASE-SCHEMA.md:1608:    themes --> theme_settings
specs/01-DATABASE-SCHEMA.md-1609-    stores --> search_queries
specs/01-DATABASE-SCHEMA.md-1610-    shipping_zones --> shipping_rates
specs/01-DATABASE-SCHEMA.md-1611-    app_installations --> oauth_tokens
specs/01-DATABASE-SCHEMA.md:1612:    stores --> webhook_subscriptions
specs/01-DATABASE-SCHEMA.md:1613:    app_installations --> webhook_subscriptions
specs/01-DATABASE-SCHEMA.md-1614-    product_options --> product_option_values
specs/01-DATABASE-SCHEMA.md-1615-    stores --> inventory_items
specs/01-DATABASE-SCHEMA.md-1616-    product_variants --> inventory_items
specs/01-DATABASE-SCHEMA.md-1617-    carts --> cart_lines
--
specs/01-DATABASE-SCHEMA.md-1623-    customers --> orders
specs/01-DATABASE-SCHEMA.md-1624-    stores --> analytics_events
specs/01-DATABASE-SCHEMA.md-1625-    customers --> analytics_events
specs/01-DATABASE-SCHEMA.md-1626-    stores --> analytics_daily
specs/01-DATABASE-SCHEMA.md:1627:    webhook_subscriptions --> webhook_deliveries
specs/01-DATABASE-SCHEMA.md-1628-    product_variants --> variant_option_values
specs/01-DATABASE-SCHEMA.md-1629-    product_option_values --> variant_option_values
specs/01-DATABASE-SCHEMA.md-1630-    orders --> order_lines
specs/01-DATABASE-SCHEMA.md-1631-    products --> order_lines
--
specs/01-DATABASE-SCHEMA.md-1642-
specs/01-DATABASE-SCHEMA.md-1643-- **Batch 1** (no dependencies): `organizations`, `apps`
specs/01-DATABASE-SCHEMA.md-1644-- **Batch 2** (depends on Batch 1): `stores` (references organizations), `users` (standalone, but logically grouped here)
specs/01-DATABASE-SCHEMA.md-1645-- **Batch 3** (depends on Batch 2): `store_domains`, `store_users`, `store_settings`, `customers`, `themes`, `pages`, `navigation_menus`, `search_settings`, `shipping_zones`, `tax_settings`, `discounts`, `products`, `collections`, `app_installations`, `oauth_clients`
specs/01-DATABASE-SCHEMA.md:1646:- **Batch 4** (depends on Batch 3): `product_options`, `product_variants`, `product_media`, `collection_products`, `customer_addresses`, `carts`, `navigation_items`, `theme_files`, `theme_settings`, `search_queries`, `shipping_rates`, `oauth_tokens`, `webhook_subscriptions`
specs/01-DATABASE-SCHEMA.md:1647:- **Batch 5** (depends on Batch 4): `product_option_values`, `inventory_items`, `cart_lines`, `checkouts`, `orders`, `analytics_events`, `analytics_daily`, `webhook_deliveries`
specs/01-DATABASE-SCHEMA.md-1648-- **Batch 6** (depends on Batch 5): `variant_option_values`, `order_lines`, `payments`, `fulfillments`
specs/01-DATABASE-SCHEMA.md-1649-- **Batch 7** (depends on Batch 6): `refunds`, `fulfillment_lines`
specs/01-DATABASE-SCHEMA.md-1650-
specs/01-DATABASE-SCHEMA.md-1651----
--
specs/01-DATABASE-SCHEMA.md-1659-3. **Foreign key indexes** - Every foreign key column has an index to avoid full table scans during JOIN and CASCADE operations. SQLite does not auto-create indexes for foreign keys like some other databases.
specs/01-DATABASE-SCHEMA.md-1660-
specs/01-DATABASE-SCHEMA.md-1661-4. **Composite primary keys for pivot tables** - Tables like `store_users`, `collection_products`, `variant_option_values`, and `analytics_daily` use composite primary keys to naturally enforce uniqueness and serve as their own covering index for the most common query pattern.
specs/01-DATABASE-SCHEMA.md-1662-
specs/01-DATABASE-SCHEMA.md:1663:5. **Date-based indexes for time series** - Tables with temporal queries (orders, analytics, search queries, webhook deliveries) include indexes on date/timestamp columns scoped by store for efficient range scans.
specs/01-DATABASE-SCHEMA.md-1664-
specs/01-DATABASE-SCHEMA.md-1665-6. **Selective indexing** - Not every column is indexed. Columns queried only via their parent's primary key (e.g., `cart_lines` always accessed through `cart_id`) rely on the foreign key index alone. This keeps write overhead and database size manageable.
specs/01-DATABASE-SCHEMA.md-1666-
specs/01-DATABASE-SCHEMA.md-1667----
--
specs/01-DATABASE-SCHEMA.md-1671-| Epic | Tables | Count |
specs/01-DATABASE-SCHEMA.md-1672-|---|---|---|
specs/01-DATABASE-SCHEMA.md-1673-| 1 - Foundation and Multi-Store Tenancy | organizations, stores, store_domains, users, store_users, store_settings | 6 |
specs/01-DATABASE-SCHEMA.md-1674-| 2 - Catalog, Collections, Inventory, Media | products, product_options, product_option_values, product_variants, variant_option_values, inventory_items, collections, collection_products, product_media | 9 |
specs/01-DATABASE-SCHEMA.md:1675:| 3 - Themes, Storefront, Localization | themes, theme_files, theme_settings, pages, navigation_menus, navigation_items | 6 |
specs/01-DATABASE-SCHEMA.md-1676-| 4 - Search | search_settings, search_queries | 2 |
specs/01-DATABASE-SCHEMA.md-1677-| 5 - Cart, Checkout, Discounts, Shipping, Taxes | carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts | 7 |
specs/01-DATABASE-SCHEMA.md-1678-| 6 - Payments, Orders, Fulfillment, Customers | customers, customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines | 8 |
specs/01-DATABASE-SCHEMA.md-1679-| 7 - Analytics | analytics_events, analytics_daily | 2 |
specs/01-DATABASE-SCHEMA.md:1680:| 8 - Apps and Webhooks | apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries | 6 |
specs/01-DATABASE-SCHEMA.md-1681-| **Total application tables** | | **46** |
specs/01-DATABASE-SCHEMA.md:1682:| Laravel framework tables | sessions, cache, cache_locks, jobs, job_batches, failed_jobs, personal_access_tokens, password_reset_tokens, customer_password_reset_tokens | 9 |
specs/01-DATABASE-SCHEMA.md-1683-| **Grand total** | | **55** |
--
specs/02-API-ROUTES.md-1-# API and Routing Specification
specs/02-API-ROUTES.md-2-
specs/02-API-ROUTES.md:3:This document defines every route, endpoint, middleware stack, request/response schema, and rate limit for the e-commerce platform. The admin panel is built with Livewire v4 + Flux UI (server-rendered), while the storefront uses Blade + Livewire. REST API endpoints serve cart/checkout operations and programmatic admin access.
specs/02-API-ROUTES.md-4-
specs/02-API-ROUTES.md-5-All monetary amounts are stored and transmitted in **minor units** (cents). All IDs are auto-incrementing integers. All monetary amounts are in minor units (cents).
specs/02-API-ROUTES.md-6-
specs/02-API-ROUTES.md-7----
--
specs/02-API-ROUTES.md-17-| GET | `/admin/login` | Admin login page | `Livewire: Admin\Auth\Login` |
specs/02-API-ROUTES.md-18-| POST | `/admin/login` | Process admin login | Handled by Livewire action |
specs/02-API-ROUTES.md-19-| POST | `/admin/logout` | Logout and invalidate session | Handled by Livewire action |
specs/02-API-ROUTES.md-20-| GET | `/admin/forgot-password` | Show forgot password form | `Livewire: Admin\Auth\ForgotPassword` |
specs/02-API-ROUTES.md:21:| POST | `/admin/forgot-password` | Send password reset link | Handled by Livewire action |
specs/02-API-ROUTES.md-22-| GET | `/admin/reset-password/{token}` | Show reset password form | `Livewire: Admin\Auth\ResetPassword` |
specs/02-API-ROUTES.md-23-| POST | `/admin/reset-password` | Reset password with token | Handled by Livewire action |
specs/02-API-ROUTES.md-24-
specs/02-API-ROUTES.md-25-Rate limit: `login` tier (5 per minute per IP).
--
specs/02-API-ROUTES.md-118-
specs/02-API-ROUTES.md-119-| Method | Path | Description | Component |
specs/02-API-ROUTES.md-120-|--------|------|-------------|-----------|
specs/02-API-ROUTES.md-121-| GET | `/admin/apps` | Apps directory - list installed and available apps | `Admin\Apps\Index` |
specs/02-API-ROUTES.md:122:| GET | `/admin/apps/{installation}` | Installed app detail - scopes, webhooks, usage | `Admin\Apps\Show` |
specs/02-API-ROUTES.md-123-
specs/02-API-ROUTES.md-124-#### Developers
specs/02-API-ROUTES.md-125-
specs/02-API-ROUTES.md-126-| Method | Path | Description | Component |
specs/02-API-ROUTES.md-127-|--------|------|-------------|-----------|
specs/02-API-ROUTES.md:128:| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |
specs/02-API-ROUTES.md-129-
specs/02-API-ROUTES.md:130:> **Note:** Webhook subscription management (CRUD) is available exclusively through the admin UI at `/admin/developers`. There are no REST API endpoints for programmatic webhook subscription management in the initial implementation.
specs/02-API-ROUTES.md-131-
specs/02-API-ROUTES.md-132-#### Analytics
specs/02-API-ROUTES.md-133-
specs/02-API-ROUTES.md-134-| Method | Path | Description | Component |
--
specs/02-API-ROUTES.md-255-#### GET /api/storefront/v1/carts/{cartId}
specs/02-API-ROUTES.md-256-
specs/02-API-ROUTES.md-257-Retrieve a cart with all lines and computed totals.
specs/02-API-ROUTES.md-258-
specs/02-API-ROUTES.md:259:- **Authentication:** None (cart ID acts as token)
specs/02-API-ROUTES.md-260-- **Rate limit:** `api.storefront` (120/min per IP)
specs/02-API-ROUTES.md-261-
specs/02-API-ROUTES.md-262-**URL parameters:**
specs/02-API-ROUTES.md-263-
--
specs/02-API-ROUTES.md-846-### 2.3 Payment Endpoint
specs/02-API-ROUTES.md-847-
specs/02-API-ROUTES.md-848-#### POST /api/storefront/v1/checkouts/{checkoutId}/pay
specs/02-API-ROUTES.md-849-
specs/02-API-ROUTES.md:850:Process payment for a checkout using the selected payment method. All payment processing happens in-process via the Mock PSP - no external API calls, no redirects, no webhooks.
specs/02-API-ROUTES.md-851-
specs/02-API-ROUTES.md-852-- **Authentication:** None
specs/02-API-ROUTES.md-853-- **Rate limit:** `checkout` (10/min per session)
specs/02-API-ROUTES.md-854-
--
specs/02-API-ROUTES.md-943-  "error_code": "card_declined"
specs/02-API-ROUTES.md-944-}
specs/02-API-ROUTES.md-945-```
specs/02-API-ROUTES.md-946-
specs/02-API-ROUTES.md:947:**Magic card number error codes:**
specs/02-API-ROUTES.md-948-
specs/02-API-ROUTES.md-949-| Card Number | Error Code | Message |
specs/02-API-ROUTES.md-950-|---|---|---|
specs/02-API-ROUTES.md-951-| `4000000000000002` | `card_declined` | "Your card was declined." |
--
specs/02-API-ROUTES.md-2895----
specs/02-API-ROUTES.md-2896-
specs/02-API-ROUTES.md-2897-## 4. Webhook Receiver Endpoints
specs/02-API-ROUTES.md-2898-
specs/02-API-ROUTES.md:2899:> **Note:** No inbound webhook endpoints are needed. All payment processing happens in-process via the Mock PSP -- there are no external payment provider callbacks. The webhook infrastructure defined in this section is reserved for **outbound** app webhooks (see Section 5, OAuth / App Ecosystem).
specs/02-API-ROUTES.md-2900-
specs/02-API-ROUTES.md-2901----
specs/02-API-ROUTES.md-2902-
specs/02-API-ROUTES.md-2903-## 5. OAuth Endpoints (App Ecosystem)
--
specs/02-API-ROUTES.md-3082-| `api.storefront` | 120 requests | 1 minute | IP address | Storefront REST API (carts, etc.) |
specs/02-API-ROUTES.md-3083-| `checkout` | 10 requests | 1 minute | Session ID | Checkout creation and mutation endpoints |
specs/02-API-ROUTES.md-3084-| `search` | 30 requests | 1 minute | IP address | Search and autocomplete endpoints |
specs/02-API-ROUTES.md-3085-| `analytics` | 60 requests | 1 minute | IP address | Analytics event ingestion |
specs/02-API-ROUTES.md:3086:| `webhooks` | 100 requests | 1 minute | IP address | Reserved for future outbound app webhook endpoints |
specs/02-API-ROUTES.md-3087-
specs/02-API-ROUTES.md-3088-### Rate Limit Response
specs/02-API-ROUTES.md-3089-
specs/02-API-ROUTES.md:3090:When a rate limit is exceeded, the server returns `429 Too Many Requests`:
specs/02-API-ROUTES.md-3091-
specs/02-API-ROUTES.md-3092-```json
specs/02-API-ROUTES.md-3093-{
specs/02-API-ROUTES.md-3094-  "message": "Too many requests. Please try again later.",
--
specs/02-API-ROUTES.md-3101-| Header | Description |
specs/02-API-ROUTES.md-3102-|--------|-------------|
specs/02-API-ROUTES.md-3103-| `X-RateLimit-Limit` | Maximum requests allowed in the window |
specs/02-API-ROUTES.md-3104-| `X-RateLimit-Remaining` | Requests remaining in the current window |
specs/02-API-ROUTES.md:3105:| `Retry-After` | Seconds until the rate limit resets (only on 429) |
specs/02-API-ROUTES.md-3106-
specs/02-API-ROUTES.md-3107-### Per-Endpoint Rate Limit Mapping
specs/02-API-ROUTES.md-3108-
specs/02-API-ROUTES.md-3109-| Endpoint Group | Tier |
--
specs/02-API-ROUTES.md-3126-| `GET /api/admin/v1/*` | `api.admin` |
specs/02-API-ROUTES.md-3127-| `POST /api/admin/v1/*` | `api.admin` |
specs/02-API-ROUTES.md-3128-| `PUT /api/admin/v1/*` | `api.admin` |
specs/02-API-ROUTES.md-3129-| `DELETE /api/admin/v1/*` | `api.admin` |
specs/02-API-ROUTES.md:3130:| *(no inbound webhook endpoints - mock PSP)* | - |
specs/02-API-ROUTES.md-3131-
specs/02-API-ROUTES.md-3132----
specs/02-API-ROUTES.md-3133-
specs/02-API-ROUTES.md-3134-## 8. Checkout State Machine
--
specs/02-API-ROUTES.md-3158-| `started` | `addressed` | PUT /checkouts/{id}/address with valid address |
specs/02-API-ROUTES.md-3159-| `addressed` | `shipping_selected` | PUT /checkouts/{id}/shipping-method with valid method |
specs/02-API-ROUTES.md-3160-| `shipping_selected` | `payment_selected` | PUT /checkouts/{id}/payment-method with valid method |
specs/02-API-ROUTES.md-3161-| `payment_selected` | `completed` | POST /checkouts/{id}/pay - Mock PSP processes payment in-process (instant for card/paypal, deferred for bank_transfer) |
specs/02-API-ROUTES.md:3162:| `payment_selected` | `shipping_selected` | Payment declined by Mock PSP (magic card number), customer retries |
specs/02-API-ROUTES.md-3163-| `*` (any active) | `expired` | Checkout expires_at reached (24-hour TTL, checked via scheduled job) |
specs/02-API-ROUTES.md-3164-
specs/02-API-ROUTES.md-3165-Applying or removing a discount does not change the checkout status but triggers totals recalculation at the current state.
specs/02-API-ROUTES.md-3166-
--
specs/02-API-ROUTES.md-3170-
specs/02-API-ROUTES.md-3171-- **Storefront API:** Versioned via URL path (`/api/storefront/v1/`). Breaking changes require a new version.
specs/02-API-ROUTES.md-3172-- **Admin API:** Versioned via URL path (`/api/admin/v1/`). Same strategy.
specs/02-API-ROUTES.md-3173-- **App API:** Versioned via URL path (`/api/apps/v1/`).
specs/02-API-ROUTES.md:3174:- **Outbound webhook payloads:** Include a `api_version` field in outbound webhook deliveries to app installations. Each app installation pins to a version.
specs/02-API-ROUTES.md-3175-
specs/02-API-ROUTES.md-3176-Non-breaking changes (new optional fields, new endpoints) can be added to existing versions. Breaking changes (removed fields, changed types, renamed endpoints) require version bumps.
specs/02-API-ROUTES.md-3177-
specs/02-API-ROUTES.md-3178----


exec

status: completed

input:
const tasks = [
  {
    message:"Fix only API/security transport issues found by the final audit. Work directly in your fork, do not touch files outside this write set unless strictly required: app/Http/Middleware/ResolveStore.php, app/Http/Middleware/EnsureApiAbility.php, routes/api.php, app/Http/Controllers/Api/StorefrontCartController.php, and focused tests under tests/Feature. Requirements: admin API must require Sanctum bearer auth and abilities (no session bypass); resolve admin API tenant from {storeId} and verify authenticated user membership; storefront cart endpoints must allow guest cartId token access without relying on the session while preserving tenant ownership. Add/update focused Pest tests. Run Pint and affected tests. Do not edit migrations, config/session.php, config/cors.php, or UI.",
    fork_context:true
  },
  {
    message:"Fix only schema/auth/config/webhook contract issues from the audit. Work directly in your fork, with disjoint files: create a migration and update ThemeSetting model/relations so theme_settings is one-to-one keyed by theme_id with settings_json while preserving existing data; make customer_password_reset_tokens store_id non-null with FK to stores and ensure scoped repository payloads/queries remain correct; set session encryption and CORS credentials to the spec defaults; update WebhookSubscription model and DeliverWebhook to use signing_secret_encrypted consistently; set audit log retention to 90 days. Add focused Pest tests for schema/config/webhook behavior. Do not edit ResolveStore, routes/api.php, checkout controllers, or cleanup jobs. Run Pint and affected tests.",
    fork_context:true
  },
  {
    message:"Fix only commerce validation/rate-limit/cleanup issues from the audit. Work directly in your fork, with disjoint files: app/Http/Controllers/Api/StorefrontCheckoutController.php, app/Services/MockPaymentProvider.php only if needed, app/Jobs/CleanupAbandonedCarts.php, app/Providers/AppServiceProvider.php, and focused tests under tests/Feature or tests/Unit. Enforce nested shipping/billing address validation and conditional card fields for card payments according to specs, make reservation cleanup idempotent/ownership-safe when checkout expiry already released reservations, and correct search=30/min and webhooks=100/min. Do not edit auth middleware, migrations, webhook model/job, or routes. Run Pint and affected tests.",
    fork_context:true
  }
];
const out = await Promise.all(tasks.map(args => tools.multi_agent_v1__spawn_agent(args)));
out.forEach(text);


type: CollabAgentToolCall

status: completed

agent_nickname: Nash

prompt: Fix only schema/auth/config/webhook contract issues from the audit. Work directly in your fork, with disjoint files: create a migration and update ThemeSetting model/relations so theme_settings is one-to-one keyed by theme_id with settings_json while preserving existing data; make customer_password_reset_tokens store_id non-null with FK to stores and ensure scoped repository payloads/queries remain correct; set session encryption and CORS credentials to the spec defaults; update WebhookSubscription model and DeliverWebhook to use signing_secret_encrypted consistently; set audit log retention to 90 days. Add focused Pest tests for schema/config/webhook behavior. Do not edit ResolveStore, routes/api.php, checkout controllers, or cleanup jobs. Run Pint and affected tests.

reasoning_effort: xhigh


type: CollabAgentToolCall

status: completed

agent_nickname: Arendt

prompt: Fix only API/security transport issues found by the final audit. Work directly in your fork, do not touch files outside this write set unless strictly required: app/Http/Middleware/ResolveStore.php, app/Http/Middleware/EnsureApiAbility.php, routes/api.php, app/Http/Controllers/Api/StorefrontCartController.php, and focused tests under tests/Feature. Requirements: admin API must require Sanctum bearer auth and abilities (no session bypass); resolve admin API tenant from {storeId} and verify authenticated user membership; storefront cart endpoints must allow guest cartId token access without relying on the session while preserving tenant ownership. Add/update focused Pest tests. Run Pint and affected tests. Do not edit migrations, config/session.php, config/cors.php, or UI.

reasoning_effort: xhigh


type: CollabAgentToolCall

status: completed

agent_nickname: Halley

prompt: Fix only commerce validation/rate-limit/cleanup issues from the audit. Work directly in your fork, with disjoint files: app/Http/Controllers/Api/StorefrontCheckoutController.php, app/Services/MockPaymentProvider.php only if needed, app/Jobs/CleanupAbandonedCarts.php, app/Providers/AppServiceProvider.php, and focused tests under tests/Feature or tests/Unit. Enforce nested shipping/billing address validation and conditional card fields for card payments according to specs, make reservation cleanup idempotent/ownership-safe when checkout expiry already released reservations, and correct search=30/min and webhooks=100/min. Do not edit auth middleware, migrations, webhook model/job, or routes. Run Pint and affected tests.

reasoning_effort: xhigh


text:
Script completed
Wall time 0.4 seconds
Output:

text: {"agent_id":"01a0218c-0039-7f51-959b-d5b50efb5e7a","nickname":"Arendt"}

text: {"agent_id":"01a0218b-ffc7-74f1-9ca6-1853e56b0b86","nickname":"Nash"}

text: {"agent_id":"01a0218c-00c8-7bb3-8389-240bad1414fd","nickname":"Halley"}


status: completed

input:
const r = await tools.exec_command({cmd:"sed -n '778,806p' specs/06-AUTH-AND-SECURITY.md; sed -n '1180,1210p' specs/01-DATABASE-SCHEMA.md; sed -n '810,970p' specs/02-API-ROUTES.md",workdir:"/Users/wesner/Herd/shop",yield_time_ms:10000,max_output_tokens:26000});
text(r.output);


type: CommandExecution

command: sed -n '778,806p' specs/06-AUTH-AND-SECURITY.md; sed -n '1180,1210p' specs/01-DATABASE-SCHEMA.md; sed -n '810,970p' specs/02-API-ROUTES.md

type: read

status: completed

stdout:
### 4.4 Input Validation

All write endpoints must use dedicated Form Request classes. Validation rules use string syntax per project convention.

#### Form Request Inventory

| Form Request Class | Used By | Key Validation Rules |
|-------------------|---------|---------------------|
| `StoreProductRequest` | Create product | `title`: required, string, max:255. `description_html`: nullable, string. `status`: required, in:active,draft,archived. `vendor`: nullable, string. Price fields: required, integer, min:0. |
| `UpdateProductRequest` | Update product | Same fields as StoreProductRequest but all optional (sometimes rule). |
| `StoreCollectionRequest` | Create collection | `title`: required, string, max:255. `type`: required, in:manual,automated. `rules_json`: required_if:type,automated, valid JSON. |
| `UpdateCollectionRequest` | Update collection | Same as StoreCollectionRequest, all optional. |
| `StoreDiscountRequest` | Create discount | `type`: required, in:code,automatic. `code`: required_if:type,code, string, max:50, unique per store (case-insensitive). `value_type`: required, in:fixed,percent,free_shipping. `value_amount`: required_unless:value_type,free_shipping, integer, min:0; when value_type is `percent`, max:100. `minimum_order_amount`: nullable, integer, min:0. `usage_limit`: nullable, integer, min:1. `starts_at`: nullable, date, ISO-8601. `ends_at`: nullable, date, after:starts_at. |
| `UpdateDiscountRequest` | Update discount | Same as StoreDiscountRequest, code unique ignoring self. |
| `SetCheckoutAddressRequest` | Set shipping/billing address | `first_name`, `last_name`, `address1`, `city`, `country_code`: required. `province_code`: required when applicable. `postal_code`: required. `phone`: nullable. |
| `ApplyDiscountRequest` | Apply discount to checkout | `code`: required, string, must exist in discounts table for current store. |
| `CreateFulfillmentRequest` | Create fulfillment | `line_item_ids`: required, array of existing line item IDs. `tracking_company`: nullable, string. `tracking_number`: nullable, string. `tracking_url`: nullable, valid URL. |
| `CreateRefundRequest` | Create refund | `line_items`: required, array with `id` and `quantity` per item. `reason`: required, string. `amount`: required, integer, min:0, max:original payment amount. |
| `UpdateStoreSettingsRequest` | Update store settings | `name`: required, string. `default_currency`: in allowed currencies. `default_locale`: in allowed locales. `timezone`: valid timezone string. |
| `InviteStaffRequest` | Invite staff member | `email`: required, valid email. `role`: required, in:admin,staff,support (not owner). |
| `RegisterCustomerRequest` | Customer registration | `name`: required. `email`: required, email, unique per store (composite). `password`: required, min:8, confirmed. |
| `LoginRequest` | Admin/customer login | `email`: required, email. `password`: required, string. |

**Authorization in Form Requests:**

Each Form Request's `authorize()` method must check the relevant policy or gate. For example, `StoreProductRequest::authorize()` checks whether the user can create a Product. If authorization fails, the request is rejected with 403 before validation even runs.

---

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| email | TEXT | No | - | PRIMARY KEY | Admin user email |
| token | TEXT | No | - | - | Hashed reset token |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

---

### customer_password_reset_tokens

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| email | TEXT | No | - | - | Customer email |
| token | TEXT | No | - | - | Hashed reset token |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Primary Key:** (`store_id`, `email`)

---

## Entity Relationship Diagrams

### Epic 1 - Foundation and Tenancy

```mermaid
erDiagram
    organizations ||--o{ stores : "owns"
    stores ||--o{ store_domains : "has"
    stores ||--o{ store_users : "links"
}
```

**Error responses:**
- `400` - Discount code expired (error code: `discount_expired`)
- `400` - Discount usage limit reached (error code: `discount_usage_exceeded`)
- `404` - Checkout not found
- `410` - Checkout expired
- `422` - Invalid code (not found, does not apply to these items)

```json
{
  "message": "This discount code has expired.",
  "error_code": "discount_expired"
}
```

---

#### DELETE /api/storefront/v1/checkouts/{checkoutId}/discount

Remove the applied discount code from the checkout.

- **Authentication:** None
- **Rate limit:** `checkout` (10/min per session)

**Success response:** `200 OK`

Returns the full checkout object with discount removed and totals recalculated.

**Error responses:**
- `404` - Checkout not found or no discount applied
- `410` - Checkout expired

---

### 2.3 Payment Endpoint

#### POST /api/storefront/v1/checkouts/{checkoutId}/pay

Process payment for a checkout using the selected payment method. All payment processing happens in-process via the Mock PSP - no external API calls, no redirects, no webhooks.

- **Authentication:** None
- **Rate limit:** `checkout` (10/min per session)

**Request body (Credit Card):**

```json
{
  "payment_method": "credit_card",
  "card_number": "4242424242424242",
  "card_expiry": "12/28",
  "card_cvc": "123",
  "card_holder": "Jane Doe"
}
```

**Request body (PayPal):**

```json
{
  "payment_method": "paypal"
}
```

**Request body (Bank Transfer):**

```json
{
  "payment_method": "bank_transfer"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `payment_method` | string | Yes | Must be one of: `credit_card`, `paypal`, `bank_transfer`. |
| `card_number` | string | Conditional | Required when `payment_method = credit_card`. Digits only (spaces stripped). |
| `card_expiry` | string | Conditional | Required when `payment_method = credit_card`. Format: MM/YY. |
| `card_cvc` | string | Conditional | Required when `payment_method = credit_card`. 3-4 digits. |
| `card_holder` | string | Conditional | Required when `payment_method = credit_card`. Max 255 chars. |

**Success response (Credit Card / PayPal - instant capture):** `200 OK`

```json
{
  "checkout_id": 7,
  "status": "completed",
  "order": {
    "id": 42,
    "order_number": "#1016",
    "status": "paid",
    "financial_status": "paid",
    "payment_method": "credit_card",
    "total_amount": 6545,
    "currency": "EUR"
  }
}
```

**Success response (Bank Transfer - deferred payment):** `200 OK`

```json
{
  "checkout_id": 7,
  "status": "completed",
  "order": {
    "id": 42,
    "order_number": "#1016",
    "status": "pending",
    "financial_status": "pending",
    "payment_method": "bank_transfer",
    "total_amount": 6545,
    "currency": "EUR"
  },
  "bank_transfer_instructions": {
    "bank_name": "Mock Bank AG",
    "iban": "DE89 3704 0044 0532 0130 00",
    "bic": "COBADEFFXXX",
    "reference": "#1016",
    "amount_formatted": "65.45 EUR"
  }
}
```

**Error responses:**
- `404` - Checkout not found
- `409` - Checkout not in a valid state for payment (must be `payment_selected`)
- `422` - Validation errors (missing card fields, invalid payment method)
- `422` - Payment declined (credit card magic numbers)

```json
{
  "message": "Your card was declined.",
  "error_code": "card_declined"
}
```

**Magic card number error codes:**

| Card Number | Error Code | Message |
|---|---|---|
| `4000000000000002` | `card_declined` | "Your card was declined." |
| `4000000000009995` | `insufficient_funds` | "Your card has insufficient funds." |

---

### 2.4 Order Endpoints

#### GET /api/storefront/v1/orders/{orderNumber}

Retrieve order status. Access is controlled via a signed token (included in confirmation emails and the confirmation page URL).

- **Authentication:** Tokenized access (`token` query parameter with HMAC signature)
- **Rate limit:** `api.storefront` (120/min per IP)

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `token` | string | Yes | HMAC-signed access token for this order |


text:
Script completed
Wall time 0.1 seconds
Output:

text:
### 4.4 Input Validation

All write endpoints must use dedicated Form Request classes. Validation rules use string syntax per project convention.

#### Form Request Inventory

| Form Request Class | Used By | Key Validation Rules |
|-------------------|---------|---------------------|
| `StoreProductRequest` | Create product | `title`: required, string, max:255. `description_html`: nullable, string. `status`: required, in:active,draft,archived. `vendor`: nullable, string. Price fields: required, integer, min:0. |
| `UpdateProductRequest` | Update product | Same fields as StoreProductRequest but all optional (sometimes rule). |
| `StoreCollectionRequest` | Create collection | `title`: required, string, max:255. `type`: required, in:manual,automated. `rules_json`: required_if:type,automated, valid JSON. |
| `UpdateCollectionRequest` | Update collection | Same as StoreCollectionRequest, all optional. |
| `StoreDiscountRequest` | Create discount | `type`: required, in:code,automatic. `code`: required_if:type,code, string, max:50, unique per store (case-insensitive). `value_type`: required, in:fixed,percent,free_shipping. `value_amount`: required_unless:value_type,free_shipping, integer, min:0; when value_type is `percent`, max:100. `minimum_order_amount`: nullable, integer, min:0. `usage_limit`: nullable, integer, min:1. `starts_at`: nullable, date, ISO-8601. `ends_at`: nullable, date, after:starts_at. |
| `UpdateDiscountRequest` | Update discount | Same as StoreDiscountRequest, code unique ignoring self. |
| `SetCheckoutAddressRequest` | Set shipping/billing address | `first_name`, `last_name`, `address1`, `city`, `country_code`: required. `province_code`: required when applicable. `postal_code`: required. `phone`: nullable. |
| `ApplyDiscountRequest` | Apply discount to checkout | `code`: required, string, must exist in discounts table for current store. |
| `CreateFulfillmentRequest` | Create fulfillment | `line_item_ids`: required, array of existing line item IDs. `tracking_company`: nullable, string. `tracking_number`: nullable, string. `tracking_url`: nullable, valid URL. |
| `CreateRefundRequest` | Create refund | `line_items`: required, array with `id` and `quantity` per item. `reason`: required, string. `amount`: required, integer, min:0, max:original payment amount. |
| `UpdateStoreSettingsRequest` | Update store settings | `name`: required, string. `default_currency`: in allowed currencies. `default_locale`: in allowed locales. `timezone`: valid timezone string. |
| `InviteStaffRequest` | Invite staff member | `email`: required, valid email. `role`: required, in:admin,staff,support (not owner). |
| `RegisterCustomerRequest` | Customer registration | `name`: required. `email`: required, email, unique per store (composite). `password`: required, min:8, confirmed. |
| `LoginRequest` | Admin/customer login | `email`: required, email. `password`: required, string. |

**Authorization in Form Requests:**

Each Form Request's `authorize()` method must check the relevant policy or gate. For example, `StoreProductRequest::authorize()` checks whether the user can create a Product. If authorization fails, the request is rejected with 403 before validation even runs.

---

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| email | TEXT | No | - | PRIMARY KEY | Admin user email |
| token | TEXT | No | - | - | Hashed reset token |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

---

### customer_password_reset_tokens

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| email | TEXT | No | - | - | Customer email |
| token | TEXT | No | - | - | Hashed reset token |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Primary Key:** (`store_id`, `email`)

---

## Entity Relationship Diagrams

### Epic 1 - Foundation and Tenancy

```mermaid
erDiagram
    organizations ||--o{ stores : "owns"
    stores ||--o{ store_domains : "has"
    stores ||--o{ store_users : "links"
}
```

**Error responses:**
- `400` - Discount code expired (error code: `discount_expired`)
- `400` - Discount usage limit reached (error code: `discount_usage_exceeded`)
- `404` - Checkout not found
- `410` - Checkout expired
- `422` - Invalid code (not found, does not apply to these items)

```json
{
  "message": "This discount code has expired.",
  "error_code": "discount_expired"
}
```

---

#### DELETE /api/storefront/v1/checkouts/{checkoutId}/discount

Remove the applied discount code from the checkout.

- **Authentication:** None
- **Rate limit:** `checkout` (10/min per session)

**Success response:** `200 OK`

Returns the full checkout object with discount removed and totals recalculated.

**Error responses:**
- `404` - Checkout not found or no discount applied
- `410` - Checkout expired

---

### 2.3 Payment Endpoint

#### POST /api/storefront/v1/checkouts/{checkoutId}/pay

Process payment for a checkout using the selected payment method. All payment processing happens in-process via the Mock PSP - no external API calls, no redirects, no webhooks.

- **Authentication:** None
- **Rate limit:** `checkout` (10/min per session)

**Request body (Credit Card):**

```json
{
  "payment_method": "credit_card",
  "card_number": "4242424242424242",
  "card_expiry": "12/28",
  "card_cvc": "123",
  "card_holder": "Jane Doe"
}
```

**Request body (PayPal):**

```json
{
  "payment_method": "paypal"
}
```

**Request body (Bank Transfer):**

```json
{
  "payment_method": "bank_transfer"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `payment_method` | string | Yes | Must be one of: `credit_card`, `paypal`, `bank_transfer`. |
| `card_number` | string | Conditional | Required when `payment_method = credit_card`. Digits only (spaces stripped). |
| `card_expiry` | string | Conditional | Required when `payment_method = credit_card`. Format: MM/YY. |
| `card_cvc` | string | Conditional | Required when `payment_method = credit_card`. 3-4 digits. |
| `card_holder` | string | Conditional | Required when `payment_method = credit_card`. Max 255 chars. |

**Success response (Credit Card / PayPal - instant capture):** `200 OK`

```json
{
  "checkout_id": 7,
  "status": "completed",
  "order": {
    "id": 42,
    "order_number": "#1016",
    "status": "paid",
    "financial_status": "paid",
    "payment_method": "credit_card",
    "total_amount": 6545,
    "currency": "EUR"
  }
}
```

**Success response (Bank Transfer - deferred payment):** `200 OK`

```json
{
  "checkout_id": 7,
  "status": "completed",
  "order": {
    "id": 42,
    "order_number": "#1016",
    "status": "pending",
    "financial_status": "pending",
    "payment_method": "bank_transfer",
    "total_amount": 6545,
    "currency": "EUR"
  },
  "bank_transfer_instructions": {
    "bank_name": "Mock Bank AG",
    "iban": "DE89 3704 0044 0532 0130 00",
    "bic": "COBADEFFXXX",
    "reference": "#1016",
    "amount_formatted": "65.45 EUR"
  }
}
```

**Error responses:**
- `404` - Checkout not found
- `409` - Checkout not in a valid state for payment (must be `payment_selected`)
- `422` - Validation errors (missing card fields, invalid payment method)
- `422` - Payment declined (credit card magic numbers)

```json
{
  "message": "Your card was declined.",
  "error_code": "card_declined"
}
```

**Magic card number error codes:**

| Card Number | Error Code | Message |
|---|---|---|
| `4000000000000002` | `card_declined` | "Your card was declined." |
| `4000000000009995` | `insufficient_funds` | "Your card has insufficient funds." |

---

### 2.4 Order Endpoints

#### GET /api/storefront/v1/orders/{orderNumber}

Retrieve order status. Access is controlled via a signed token (included in confirmation emails and the confirmation page URL).

- **Authentication:** Tokenized access (`token` query parameter with HMAC signature)
- **Rate limit:** `api.storefront` (120/min per IP)

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `token` | string | Yes | HMAC-signed access token for this order |


status: completed

input:
const r = await tools.exec_command({cmd:"rg -n 'pest-plugin-browser|playwright|browser' composer.json composer.lock package.json tests | head -100; sed -n '1,220p' specs/09-IMPLEMENTATION-ROADMAP.md | tail -120",workdir:"/Users/wesner/Herd/shop",yield_time_ms:10000,max_output_tokens:12000});
text(r.output);


command: rg -n 'pest-plugin-browser|playwright|browser' composer.json composer.lock package.json tests | head -100; sed -n '1,220p' specs/09-IMPLEMENTATION-ROADMAP.md | tail -120

type: search

query: pest-plugin-browser|playwright|browser

stdout:
composer.lock:4452:                "symfony/browser-kit": "<6.4",
composer.lock:4476:                "symfony/browser-kit": "^6.4|^7.0|^8.0",
composer.lock:7643:                "pestphp/pest-plugin-browser": "^4.2.1",

Create in this dependency order:

1. `create_organizations_table` - no dependencies
2. `create_stores_table` - FK: `organization_id`
3. `create_store_domains_table` - FK: `store_id`
4. Modify default `users` migration - add `status`, `last_login_at`, `two_factor_*` columns; rename `password` to `password_hash` internally
5. `create_store_users_table` - composite PK: `store_id` + `user_id`, with `role` column
6. `create_store_settings_table` - PK: `store_id`

All monetary amounts stored as INTEGER in minor units (cents). All enums stored as TEXT with CHECK constraints. All timestamps as TEXT (ISO-8601) via Laravel.

#### Step 1.3: Core Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Organization` | Top-level tenant entity |
| Model (with migration, factory, seeder) | `Store` | Individual store within an organization |
| Model (with migration, factory, seeder) | `StoreDomain` | Domain mapping for a store |
| Model (pivot) | `StoreUser` | Pivot model with `role` attribute, custom Pivot class |
| Model (with migration, factory, seeder) | `StoreSettings` | Per-store configuration |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Organization` | `hasMany(Store::class)` |
| `Store` | `belongsTo(Organization::class)`, `hasMany(StoreDomain::class)`, `belongsToMany(User::class)` through `store_users`, `hasOne(StoreSettings::class)` |
| `StoreDomain` | `belongsTo(Store::class)` |
| `User` | `belongsToMany(Store::class)` through `store_users` |
| `StoreUser` | Pivot model with `role` attribute, custom `Pivot` class |
| `StoreSettings` | `belongsTo(Store::class)` |

Each model must have:
- Explicit return type declarations on all methods
- Proper `$fillable` or `$guarded` arrays
- `casts()` method for JSON/enum columns
- Factory with meaningful default values
- Seeder that creates sample data

#### Step 1.4: Enums

**Files to create in `app/Enums/`:**

| Enum | Values |
|------|--------|
| `StoreStatus` | `Active`, `Suspended` |
| `StoreUserRole` | `Owner`, `Admin`, `Staff`, `Support` |
| `StoreDomainType` | `Storefront`, `Admin`, `Api` |

#### Step 1.5: Tenant Resolution Middleware

**File to create:** `App\Http\Middleware\ResolveStore`

Requirements:
1. Storefront routes: extract hostname from the request, look up `store_domains`, cache hostname-to-store_id for 5 minutes, bind `Store` as singleton via container instance `current_store`
2. Admin routes: read `current_store_id` from session, verify user has `store_users` record
3. Return 404 if hostname not found
4. Return 503 for suspended stores on storefront routes

**Registration in `bootstrap/app.php`:**
- Register middleware group `storefront` containing `ResolveStore`
- Register middleware group `admin` containing `ResolveStore`

#### Step 1.6: BelongsToStore Trait and Global Scope

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Trait | `App\Models\Concerns\BelongsToStore` | Applies StoreScope and auto-sets store_id on creating event |
| Scope | `App\Models\Scopes\StoreScope` | Applies `where('store_id', app('current_store')->id)` to all queries |

Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.

#### Step 1.7: Authentication

**Admin auth requirements:**

| Component | Description |
|-----------|-------------|
| Livewire component `Admin\Auth\Login` | Admin login form |
| Livewire component `Admin\Auth\Logout` | Admin logout action |
| Auth mechanism | Standard Laravel session auth via `Auth::guard('web')->attempt()` |
| Rate limiting | 5 attempts per minute per IP via `RateLimiter::for('login')` |
| Password reset | Standard Laravel flow with `Password::broker('users')` |

**Customer auth requirements:**

| Component | Description |
|-----------|-------------|
| Custom guard | `customer` guard with `customers` provider |
| Custom user provider | `CustomerUserProvider` that injects `store_id` into queries |
| Livewire component `Storefront\Account\Auth\Login` | Customer login form |
| Livewire component `Storefront\Account\Auth\Register` | Customer registration form |
| Uniqueness | Customer email unique per store, not globally |
| Rate limiting | 5 attempts per minute per IP |

**Rate limiter registration:**
- Register rate limiters in `AppServiceProvider::boot()` for login (5 per minute per IP)

#### Step 1.8: Authorization

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Policy | `ProductPolicy` | Product authorization based on store role |
| Policy | `OrderPolicy` | Order authorization based on store role |
| Policy | `CollectionPolicy` | Collection authorization based on store role |
| Policy | `DiscountPolicy` | Discount authorization based on store role |
| Policy | `CustomerPolicy` | Customer authorization based on store role |
| Policy | `StorePolicy` | Store authorization based on store role |
| Policy | `PagePolicy` | Page authorization based on store role |
| Policy | `ThemePolicy` | Theme authorization based on store role |
| Policy | `FulfillmentPolicy` | Fulfillment authorization based on store role |
| Policy | `RefundPolicy` | Refund authorization based on store role |


text:
Script completed
Wall time 0.1 seconds
Output:

text:
composer.lock:4452:                "symfony/browser-kit": "<6.4",
composer.lock:4476:                "symfony/browser-kit": "^6.4|^7.0|^8.0",
composer.lock:7643:                "pestphp/pest-plugin-browser": "^4.2.1",

Create in this dependency order:

1. `create_organizations_table` - no dependencies
2. `create_stores_table` - FK: `organization_id`
3. `create_store_domains_table` - FK: `store_id`
4. Modify default `users` migration - add `status`, `last_login_at`, `two_factor_*` columns; rename `password` to `password_hash` internally
5. `create_store_users_table` - composite PK: `store_id` + `user_id`, with `role` column
6. `create_store_settings_table` - PK: `store_id`

All monetary amounts stored as INTEGER in minor units (cents). All enums stored as TEXT with CHECK constraints. All timestamps as TEXT (ISO-8601) via Laravel.

#### Step 1.3: Core Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Organization` | Top-level tenant entity |
| Model (with migration, factory, seeder) | `Store` | Individual store within an organization |
| Model (with migration, factory, seeder) | `StoreDomain` | Domain mapping for a store |
| Model (pivot) | `StoreUser` | Pivot model with `role` attribute, custom Pivot class |
| Model (with migration, factory, seeder) | `StoreSettings` | Per-store configuration |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Organization` | `hasMany(Store::class)` |
| `Store` | `belongsTo(Organization::class)`, `hasMany(StoreDomain::class)`, `belongsToMany(User::class)` through `store_users`, `hasOne(StoreSettings::class)` |
| `StoreDomain` | `belongsTo(Store::class)` |
| `User` | `belongsToMany(Store::class)` through `store_users` |
| `StoreUser` | Pivot model with `role` attribute, custom `Pivot` class |
| `StoreSettings` | `belongsTo(Store::class)` |

Each model must have:
- Explicit return type declarations on all methods
- Proper `$fillable` or `$guarded` arrays
- `casts()` method for JSON/enum columns
- Factory with meaningful default values
- Seeder that creates sample data

#### Step 1.4: Enums

**Files to create in `app/Enums/`:**

| Enum | Values |
|------|--------|
| `StoreStatus` | `Active`, `Suspended` |
| `StoreUserRole` | `Owner`, `Admin`, `Staff`, `Support` |
| `StoreDomainType` | `Storefront`, `Admin`, `Api` |

#### Step 1.5: Tenant Resolution Middleware

**File to create:** `App\Http\Middleware\ResolveStore`

Requirements:
1. Storefront routes: extract hostname from the request, look up `store_domains`, cache hostname-to-store_id for 5 minutes, bind `Store` as singleton via container instance `current_store`
2. Admin routes: read `current_store_id` from session, verify user has `store_users` record
3. Return 404 if hostname not found
4. Return 503 for suspended stores on storefront routes

**Registration in `bootstrap/app.php`:**
- Register middleware group `storefront` containing `ResolveStore`
- Register middleware group `admin` containing `ResolveStore`

#### Step 1.6: BelongsToStore Trait and Global Scope

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Trait | `App\Models\Concerns\BelongsToStore` | Applies StoreScope and auto-sets store_id on creating event |
| Scope | `App\Models\Scopes\StoreScope` | Applies `where('store_id', app('current_store')->id)` to all queries |

Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.

#### Step 1.7: Authentication

**Admin auth requirements:**

| Component | Description |
|-----------|-------------|
| Livewire component `Admin\Auth\Login` | Admin login form |
| Livewire component `Admin\Auth\Logout` | Admin logout action |
| Auth mechanism | Standard Laravel session auth via `Auth::guard('web')->attempt()` |
| Rate limiting | 5 attempts per minute per IP via `RateLimiter::for('login')` |
| Password reset | Standard Laravel flow with `Password::broker('users')` |

**Customer auth requirements:**

| Component | Description |
|-----------|-------------|
| Custom guard | `customer` guard with `customers` provider |
| Custom user provider | `CustomerUserProvider` that injects `store_id` into queries |
| Livewire component `Storefront\Account\Auth\Login` | Customer login form |
| Livewire component `Storefront\Account\Auth\Register` | Customer registration form |
| Uniqueness | Customer email unique per store, not globally |
| Rate limiting | 5 attempts per minute per IP |

**Rate limiter registration:**
- Register rate limiters in `AppServiceProvider::boot()` for login (5 per minute per IP)

#### Step 1.8: Authorization

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Policy | `ProductPolicy` | Product authorization based on store role |
| Policy | `OrderPolicy` | Order authorization based on store role |
| Policy | `CollectionPolicy` | Collection authorization based on store role |
| Policy | `DiscountPolicy` | Discount authorization based on store role |
| Policy | `CustomerPolicy` | Customer authorization based on store role |
| Policy | `StorePolicy` | Store authorization based on store role |
| Policy | `PagePolicy` | Page authorization based on store role |
| Policy | `ThemePolicy` | Theme authorization based on store role |
| Policy | `FulfillmentPolicy` | Fulfillment authorization based on store role |
| Policy | `RefundPolicy` | Refund authorization based on store role |


status: completed

input:
const r = await tools.exec_command({cmd:"find tests -maxdepth 2 -type f -print | sort | sed -n '1,160p'; composer show pestphp/pest-plugin-browser --format=json | head -c 3000",workdir:"/Users/wesner/Herd/shop",yield_time_ms:30000,max_output_tokens:8000});
text(r.output);


command: find tests -maxdepth 2 -type f -print | sort | sed -n '1,160p'; composer show pestphp/pest-plugin-browser --format=json | head -c 3000

type: unknown

stdout:
tests/Feature/AdminApiTest.php
tests/Feature/ApiTokenTest.php
tests/Feature/CommerceFlowTest.php
tests/Feature/DashboardTest.php
tests/Feature/ExampleTest.php
tests/Feature/SearchAnalyticsWebhookTest.php
tests/Feature/SeedDataTest.php
tests/Pest.php
tests/TestCase.php
tests/Unit/DomainServicesTest.php
tests/Unit/ExampleTest.php
tests/Unit/PricingEngineTest.php

In ShowCommand.php line 321:

  Package "pestphp/pest-plugin-browser" not found, try using --available (-a)
   to show all available packages.

show [--all] [--locked] [-i|--installed] [-p|--platform] [-a|--available] [-s|--self] [-N|--name-only] [-P|--path] [-t|--tree] [-l|--latest] [-o|--outdated] [--ignore IGNORE] [-M|--major-only] [-m|--minor-only] [--patch-only] [-A|--sort-by-age] [-D|--direct] [--strict] [-f|--format FORMAT] [--no-dev] [--ignore-platform-req IGNORE-PLATFORM-REQ] [--ignore-platform-reqs] [--] [<package> [<version>]]


text:
Script completed
Wall time 0.2 seconds
Output:

text:
tests/Feature/AdminApiTest.php
tests/Feature/ApiTokenTest.php
tests/Feature/CommerceFlowTest.php
tests/Feature/DashboardTest.php
tests/Feature/ExampleTest.php
tests/Feature/SearchAnalyticsWebhookTest.php
tests/Feature/SeedDataTest.php
tests/Pest.php
tests/TestCase.php
tests/Unit/DomainServicesTest.php
tests/Unit/ExampleTest.php
tests/Unit/PricingEngineTest.php

In ShowCommand.php line 321:

  Package "pestphp/pest-plugin-browser" not found, try using --available (-a)
   to show all available packages.

show [--all] [--locked] [-i|--installed] [-p|--platform] [-a|--available] [-s|--self] [-N|--name-only] [-P|--path] [-t|--tree] [-l|--latest] [-o|--outdated] [--ignore IGNORE] [-M|--major-only] [-m|--minor-only] [--patch-only] [-A|--sort-by-age] [-D|--direct] [--strict] [-f|--format FORMAT] [--no-dev] [--ignore-platform-req IGNORE-PLATFORM-REQ] [--ignore-platform-reqs] [--] [<package> [<version>]]


status: completed

input:
const r = await tools.exec_command({cmd:"sed -n '1,420p' app/Http/Controllers/Api/AdminController.php; sed -n '1,260p' app/Http/Controllers/Api/StorefrontCartController.php; sed -n '1,220p' app/Http/Controllers/Api/StorefrontCheckoutController.php",workdir:"/Users/wesner/Herd/shop",yield_time_ms:10000,max_output_tokens:30000});
text(r.output);