content:
   500→
   501→#### StorePolicy
   502→
   503→File: `app/Policies/StorePolicy.php`
   504→
   505→| Method | Receives Model? | Required Roles | Store ID Source |
   506→|--------|:--------------:|----------------|-----------------|
   507→| `viewSettings(User, Store)` | Yes | OwnerOrAdmin | store.id |
   508→| `updateSettings(User, Store)` | Yes | OwnerOrAdmin | store.id |
   509→| `delete(User, Store)` | Yes | OwnerOnly | store.id |
   510→
   511→#### FulfillmentPolicy
   512→
   513→File: `app/Policies/FulfillmentPolicy.php`
   514→
   515→| Method | Receives Model? | Required Roles | Store ID Source |
   516→|--------|:--------------:|----------------|-----------------|
   517→| `create(User, Order)` | Yes (Order) | OwnerAdminOrStaff | order.store_id |
   518→| `update(User, Fulfillment)` | Yes | OwnerAdminOrStaff | fulfillment -> order.store_id |
   519→| `cancel(User, Fulfillment)` | Yes | OwnerAdminOrStaff | fulfillment -> order.store_id |
   520→
   521→Note: `create` receives the parent Order, not a Fulfillment, since the Fulfillment does not exist yet. The `update` and `cancel` methods navigate through the Fulfillment's order relationship to get the store_id.
   522→
   523→#### RefundPolicy
   524→
   525→File: `app/Policies/RefundPolicy.php`
   526→
   527→| Method | Receives Model? | Required Roles | Store ID Source |
   528→|--------|:--------------:|----------------|-----------------|
   529→| `create(User, Order)` | Yes (Order) | OwnerOrAdmin | order.store_id |
   530→
   531→Note: `create` receives the parent Order for the same reason as FulfillmentPolicy.
   532→
   533→#### NavigationMenuPolicy
   534→
   535→File: `app/Policies/NavigationMenuPolicy.php`
   536→
   537→| Method | Receives Model? | Required Roles | Store ID Source |
   538→|--------|:--------------:|----------------|-----------------|
   539→| `viewAny(User)` | No | OwnerAdminOrStaff | current_store |
   540→| `manage(User)` | No | OwnerOrAdmin | current_store |
   541→
   542→---
   543→
   544→### 2.5 Gates (Non-Model Operations)
   545→
   546→Gates are registered in `AppServiceProvider` (or a dedicated `AuthServiceProvider`). Each gate resolves the current store from the container and checks the user's role via the `store_users` relationship.
   547→
   548→| Gate Name | Required Roles | Purpose |
   549→|-----------|---------------|---------|
   550→| `manage-store-settings` | Owner, Admin | Access store settings pages |
   551→| `manage-staff` | Owner, Admin | Invite, update, remove staff members |
   552→| `manage-developers` | Owner, Admin | Create/revoke API tokens, manage webhooks |
   553→| `view-analytics` | Owner, Admin, Staff | View analytics dashboard and reports |
   554→| `manage-shipping` | Owner, Admin | Manage shipping zones, rates, profiles |
   555→| `manage-taxes` | Owner, Admin | Manage tax settings and rates |
   556→| `manage-search-settings` | Owner, Admin | Configure search weights and synonyms |
   557→| `manage-navigation` | Owner, Admin | Create/edit/delete navigation menus |
   558→| `manage-apps` | Owner, Admin | Install and remove third-party apps |
   559→
   560→**Gate resolution pattern (all gates follow this):**
   561→
   562→1. Resolve the current store from the container
   563→2. Look up the user's `store_users` record for that store
   564→3. If no record exists, return false
   565→4. If the user's role is in the gate's required roles list, return true
   566→5. Otherwise return false
   567→
   568→---
   569→
   570→### 2.6 Policy and Gate Usage Patterns
   571→
   572→#### In Livewire Components
   573→
   574→- List views: call `authorize('viewAny', ModelClass)` in the `mount()` lifecycle method.
   575→- Action methods (delete, update, etc.): call `authorize('action', $modelInstance)` at the start of the action, before performing any mutation.
   576→- If authorization fails, Laravel throws `AuthorizationException`, which renders as 403.
   577→
   578→#### In API Controllers
   579→
   580→- Use `$this->authorize('action', $model)` at the start of each controller method.
   581→- Alternatively, use `can` middleware on routes for simpler checks.
   582→- Form Requests also implement `authorize()` to check the relevant policy or gate before validation runs.
   583→
   584→#### In Blade Templates
   585→
   586→- Use `@can('gate-name')` / `@cannot` directives to conditionally show/hide navigation items and action buttons.
   587→- This is purely cosmetic (UI hiding) - the actual authorization check happens server-side in policies/gates.
   588→
   589→---
   590→
   591→## 3. Middleware Stack
   592→
   593→### 3.1 Middleware Registration
   594→
   595→File: `bootstrap/app.php`
   596→
   597→Three custom middleware aliases must be registered:
   598→
   599→| Alias | Class | Purpose |
   600→|-------|-------|---------|
   601→| `store.resolve` | `App\Http\Middleware\ResolveStore` | Resolves current store from hostname or session |
   602→| `role.check` | `App\Http\Middleware\CheckStoreRole` | Checks user has required role for current store |
   603→| `auth:customer` | `App\Http\Middleware\CustomerAuthenticate` | Ensures customer is authenticated via customer guard |
   604→
   605→---
   606→
   607→### 3.2 Route Middleware Groups
   608→
   609→| Route Group | Middleware Stack | Description |
   610→|-------------|-----------------|-------------|
   611→| Admin web (auth pages) | `web` | Login, forgot-password, reset-password |
   612→| Admin web (authenticated) | `web`, `auth`, `verified`, `store.resolve`, `role.check` | All admin panel pages |
   613→| Admin API | `api`, `auth:sanctum`, `store.resolve`, `throttle:api.admin` | REST API endpoints |
   614→| Storefront web (public) | `web`, `store.resolve` | Public store pages (catalog, product, cart) |
   615→| Storefront web (customer) | `web`, `store.resolve`, `auth:customer` | Customer account pages |
   616→| Storefront API | `api`, `store.resolve`, `throttle:api.storefront` | Cart and checkout API |
   617→| Webhook routes | `api`, `throttle:webhooks` | Reserved for outbound app webhooks (no inbound payment webhooks - Mock PSP is in-process) |
   618→
   619→---
   620→
   621→### 3.3 Custom Middleware Specifications
   622→
   623→#### ResolveStore
   624→
   625→File: `app/Http/Middleware/ResolveStore.php`
   626→
   627→**Purpose:** Resolves the current store from the request hostname and binds it to the service container.
   628→
   629→```mermaid
   630→flowchart TD
   631→    A[Extract hostname from request] --> B{Route type?}
   632→    B -- Storefront --> C{Match hostname in store_domains table?}
   633→    C -- Yes --> F[Store found]
   634→    C -- No --> E[Abort 404: Store not found]
   635→    F --> G{Store status == suspended?}
   636→    G -- Yes --> H[Abort 503: Store unavailable]
   637→    G -- No --> I[Bind Store to container as 'current_store']
   638→    I --> J[Share Store with all views as 'currentStore']
   639→    J --> K[Continue to next middleware]
   640→    B -- Admin --> L[Read store_id from session 'current_store_id']
   641→    L --> M{User has store_users record?}
   642→    M -- No --> N[Abort 403]
   643→    M -- Yes --> I
   644→```
   645→
   646→**Requirements:**
   647→
   648→Storefront requests: Resolve store from the `store_domains` table by matching the request hostname against `store_domains.hostname`. Cache the hostname-to-store mapping using file cache (tag: `store-domains`).
   649→
   650→Admin requests: Store is determined by session key `current_store_id`, set after login or via the store-switcher in the admin panel. Admin routes do NOT use hostname resolution. The `admin.acme-fashion.test` domain in seeders routes requests to the admin panel, but the actual store context within the admin is determined by the session `current_store_id` after login.
   651→
   652→**Shared steps (after store is resolved):**
   653→
   654→1. If the store's `status` column is `'suspended'`: abort with HTTP 503 and message "This store is currently unavailable." (For admin routes: abort 403 for mutations instead.)
   655→2. Bind the resolved Store model instance to the service container with the key `'current_store'`.
   656→3. Share the store with all Blade views under the variable name `currentStore`.
   657→4. Allow the request to proceed.
   658→
   659→#### CheckStoreRole
   660→
   661→File: `app/Http/Middleware/CheckStoreRole.php`
   662→
   663→**Purpose:** Verifies the authenticated user has one of the specified roles for the current store.
   664→
   665→**Route usage:** Applied as `role.check:owner,admin` or `role.check:owner,admin,staff` (comma-separated role values passed as variadic parameters).
   666→
   667→```mermaid
   668→flowchart TD
   669→    A[Resolve current_store from container] --> B[Get authenticated user]
   670→    B --> C[Look up store_users record for user + store]
   671→    C --> D{Record exists?}
   672→    D -- No --> E[Abort 403: No access to this store]
   673→    D -- Yes --> F[Parse allowed roles from middleware parameters]
   674→    F --> G{User's role in allowed list?}
   675→    G -- No --> H[Abort 403: Insufficient permissions]
   676→    G -- Yes --> I[Attach store_user to request attributes]
   677→    I --> J[Continue to next middleware]
   678→```
   679→
   680→**Requirements:**
   681→
   682→1. Resolve the current store from the container.
   683→2. Get the authenticated user from the request.
   684→3. Query the user's `store_users` relationship filtered by `store_id`.
   685→4. If no record exists: abort 403 with message "You do not have access to this store."
   686→5. Convert the middleware parameter strings to `StoreUserRole` enum instances.
   687→6. If the user's role is not in the allowed list: abort 403 with message "Insufficient permissions."
   688→7. Store the resolved `StoreUser` pivot record on the request attributes (key: `store_user`) for downstream use.
   689→8. Allow the request to proceed.
   690→
   691→#### CustomerAuthenticate
   692→
   693→File: `app/Http/Middleware/CustomerAuthenticate.php`
   694→
   695→**Purpose:** Ensures the request is authenticated via the `customer` guard. Redirects unauthenticated customers to login.
   696→
   697→```mermaid
   698→flowchart TD
   699→    A[Check customer guard authentication] --> B{Authenticated?}
   700→    B -- Yes --> C[Continue to next middleware]
   701→    B -- No --> D[Store current URL as intended redirect]
   702→    D --> E[Redirect to /account/login]
   703→```
   704→
   705→**Requirements:**
   706→
   707→1. Check if `Auth::guard('customer')` reports the user as authenticated.
   708→2. If authenticated: allow the request to proceed.
   709→3. If not authenticated: save the current URL in the session as the intended URL (so the customer returns there after login), then redirect to the storefront login route (`/account/login`).
   710→
   711→---
   712→
   713→## 4. Security Controls
   714→
   715→### 4.1 CSRF Protection
   716→
   717→| Context | Mechanism |
   718→|---------|-----------|
   719→| Blade forms | Laravel's `@csrf` directive injects a hidden token field (automatic) |
   720→| Livewire actions | Livewire handles CSRF verification automatically on every request |
   721→| API routes (Sanctum token) | Exempt from CSRF (token-based auth replaces CSRF need) |
   722→| SPA clients | `GET /sanctum/csrf-cookie` sets XSRF-TOKEN cookie for session-based API calls |
   723→
   724→**Requirements:**
   725→
   726→- CSRF verification is enforced globally on all `web` middleware group routes.
   727→- No routes within the `web` group may be excluded from CSRF verification.
   728→- API routes (under `api` middleware group) are inherently exempt since they use token auth.
   729→
   730→---
   731→
   732→### 4.2 Rate Limiting
   733→
   734→All rate limiters are defined in `bootstrap/app.php` (or `AppServiceProvider::boot()`).
   735→
   736→| Limiter Name | Limit | Keyed By | Applied To |
   737→|-------------|-------|----------|------------|
   738→| `login` | 5 per minute | IP address | Admin login, customer login |
   739→| `api.admin` | 60 per minute | User ID (if authenticated), otherwise IP | Admin REST API endpoints |
   740→| `api.storefront` | 120 per minute | IP address | Public storefront API (cart, search) |
   741→| `checkout` | 10 per minute | Session ID | Checkout-related operations |
   742→| `search` | 30 per minute | IP address | Search queries |
   743→| `analytics` | 60 per minute | IP address | Analytics event ingestion |
   744→| `webhooks` | 100 per minute | IP address | Outbound app webhook endpoints (reserved; no inbound payment webhooks - Mock PSP is in-process) |
   745→
   746→**Rate limit response headers** (returned on all API responses by Laravel default):
   747→
   748→| Header | Description |
   749→|--------|-------------|
   750→| `X-RateLimit-Limit` | Maximum requests allowed in the window |
   751→| `X-RateLimit-Remaining` | Requests remaining in the current window |
   752→| `Retry-After` | Seconds until the rate limit resets (only on 429 responses) |
   753→
   754→When a rate limit is exceeded, the response is HTTP 429 Too Many Requests.
   755→
   756→---
   757→
   758→### 4.3 Encryption at Rest
   759→
   760→Encryption algorithm: AES-256-CBC with MAC verification, keyed by the application's `APP_KEY`.
   761→
   762→**Fields requiring encrypted storage (using Laravel's `encrypted` cast):**
   763→
   764→| Model | Column | Cast Type | Purpose |
   765→|-------|--------|-----------|---------|
   766→| OauthClient | `client_secret_encrypted` | `encrypted` | Third-party OAuth client secrets |
   767→| WebhookSubscription | `signing_secret_encrypted` | `encrypted` | Per-subscription signing secrets for outbound webhooks |
   768→| Payment | `raw_json` | `encrypted:array` | Full payment gateway response (may contain sensitive card/account data) |
   769→
   770→**Requirements:**
   771→
   772→- These columns are declared with the `encrypted` (or `encrypted:array`) cast in the model's `casts()` method.
   773→- Data is automatically encrypted before writing to the database and decrypted when reading.
   774→- The encrypted columns store opaque ciphertext (type TEXT in SQLite). They cannot be queried with WHERE clauses at the database level.
   775→
   776→---
   777→
   778→### 4.4 Input Validation
   779→
   780→All write endpoints must use dedicated Form Request classes. Validation rules use string syntax per project convention.
   781→
   782→#### Form Request Inventory
   783→
   784→| Form Request Class | Used By | Key Validation Rules |
   785→|-------------------|---------|---------------------|
   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. |
   787→| `UpdateProductRequest` | Update product | Same fields as StoreProductRequest but all optional (sometimes rule). |
   788→| `StoreCollectionRequest` | Create collection | `title`: required, string, max:255. `type`: required, in:manual,automated. `rules_json`: required_if:type,automated, valid JSON. |
   789→| `UpdateCollectionRequest` | Update collection | Same as StoreCollectionRequest, all optional. |
   790→| `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. |
   791→| `UpdateDiscountRequest` | Update discount | Same as StoreDiscountRequest, code unique ignoring self. |
   792→| `SetCheckoutAddressRequest` | Set shipping/billing address | `first_name`, `last_name`, `address1`, `city`, `country_code`: required. `province_code`: required when applicable. `postal_code`: required. `phone`: nullable. |
   793→| `ApplyDiscountRequest` | Apply discount to checkout | `code`: required, string, must exist in discounts table for current store. |
   794→| `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. |
   795→| `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. |
   796→| `UpdateStoreSettingsRequest` | Update store settings | `name`: required, string. `default_currency`: in allowed currencies. `default_locale`: in allowed locales. `timezone`: valid timezone string. |
   797→| `InviteStaffRequest` | Invite staff member | `email`: required, valid email. `role`: required, in:admin,staff,support (not owner). |
   798→| `RegisterCustomerRequest` | Customer registration | `name`: required. `email`: required, email, unique per store (composite). `password`: required, min:8, confirmed. |
   799→| `LoginRequest` | Admin/customer login | `email`: required, email. `password`: required, string. |
   800→
   801→**Authorization in Form Requests:**
   802→
   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.
   804→
   805→---
   806→
   807→### 4.5 XSS Prevention
   808→
   809→| Context | Strategy |
   810→|---------|----------|
   811→| Standard Blade output | Double-curly-brace syntax auto-escapes all output (Laravel default) |
   812→| Rich HTML output | Unescaped syntax used only for trusted HTML fields: product `description_html`, page `body_html` |
   813→| HTML input sanitization | Use HTMLPurifier (or equivalent) on save, before persisting to database |
   814→| Livewire properties | Auto-escaped by default when rendered in Blade |
   815→| JSON API responses | Laravel escapes JSON output by default |
   816→
   817→#### HTML Sanitization Requirements
   818→
   819→An `SanitizeHtml` action class must be created at `app/Actions/SanitizeHtml.php`.
   820→
   821→**Allowed HTML elements and attributes:**
   822→
   823→| Element | Allowed Attributes |
   824→|---------|-------------------|
   825→| `p`, `br`, `strong`, `em`, `u` | none |
   826→| `ol`, `ul`, `li` | none |
   827→| `a` | `href` |
   828→| `img` | `src`, `alt` |
   829→| `h1` through `h6` | none |
   830→| `blockquote` | none |
   831→| `table`, `thead`, `tbody`, `tr`, `th`, `td` | none |
   832→| `div`, `span` | none |
   833→
   834→All other elements and attributes must be stripped. Empty elements should be removed.
   835→
   836→**Where sanitization is applied:** Before persisting product `description_html` and page `body_html` fields. The sanitization runs during the create and update operations for these models.
   837→
   838→---
   839→
   840→### 4.6 SQL Injection Prevention
   841→
   842→**Requirements:**
   843→
   844→- All database queries must use Eloquent ORM with parameterized bindings (Laravel default behavior).
   845→- No raw query strings may contain unsanitized user input.
   846→- Any use of `whereRaw`, `selectRaw`, or `DB::raw` must use parameter bindings (the `?` placeholder syntax).
   847→- FTS5 (full-text search) queries require special handling: user input must be sanitized before building the MATCH expression.
   848→
   849→#### FTS5 Input Sanitization
   850→
   851→Before using user input in an FTS5 MATCH expression:
   852→
   853→1. Strip all characters that are not Unicode letters, Unicode numbers, or whitespace (remove FTS5 special operators like `*`, `AND`, `OR`, `NOT`, `NEAR`, quotes, parentheses, etc.).
   854→2. Trim leading and trailing whitespace.
   855→3. Pass the sanitized string via a parameterized binding (never concatenate into the query string).
   856→
   857→---
   858→
   859→### 4.7 Mass Assignment Protection
   860→
   861→**Requirements:**
   862→
   863→- Every Eloquent model must define a `$fillable` array listing only the columns that are safe for mass assignment.
   864→- The pattern `$guarded = []` is never permitted in any model.
   865→- All controller and Livewire action code must use `$request->validated()` (not `$request->all()` or `$request->input()`) when passing data to model creation or update methods.
   866→
   867→---
   868→
   869→### 4.8 Password Security
   870→
   871→| Setting | Value |
   872→|---------|-------|
   873→| Minimum length | 8 characters (for both admin and customer passwords) |
   874→| Hashing algorithm | Bcrypt (Laravel default) |
   875→| Password in logs | Never logged - use Laravel's built-in log redaction |
   876→| Password in API responses | Never returned - `password` and `remember_token` must be in the model's `$hidden` array |
   877→
   878→**Model requirements:**
   879→
   880→- Both `User` and `Customer` models must include `password` and `remember_token` in their `$hidden` array.
   881→- Password validation rules must enforce minimum 8 characters and use the `confirmed` rule (requiring a matching `password_confirmation` field).
   882→
   883→**Important:** Both the `User` and `Customer` models use `password_hash` as the database column name instead of Laravel's default `password`. Both models must override `getAuthPassword()`:
   884→
   885→```php
   886→public function getAuthPassword(): string
   887→{
   888→    return $this->password_hash;
   889→}
   890→```
   891→
   892→This is required for `Auth::attempt()` and `Hash::check()` to work correctly with the custom column name.
   893→
   894→---
   895→
   896→## 5. Session Configuration
   897→
   898→File: `config/session.php`
   899→
   900→| Setting | Value | Rationale |
   901→|---------|-------|-----------|
   902→| `driver` | `'file'` | Simple, no external dependencies. Compatible with SQLite-based stack without needing Redis. |
   903→| `lifetime` | 120 (minutes) | Configurable via `SESSION_LIFETIME` env var. |
   904→| `expire_on_close` | `false` | Session persists until lifetime expires, not on browser close. |
   905→| `encrypt` | `true` | Session data encrypted at rest on disk. |
   906→| `cookie` | `'shop_session'` | Custom name to avoid generic fingerprinting. Configurable via `SESSION_COOKIE` env var. |
   907→| `path` | `'/'` | Cookie available across entire domain. |
   908→| `domain` | `null` | Laravel determines from request. |
   909→| `secure` | `true` | HTTPS-only cookies in production. Configurable via `SESSION_SECURE_COOKIE` env var (set to false for local dev). |
   910→| `http_only` | `true` | Cookie inaccessible to JavaScript (mitigates XSS-based cookie theft). |
   911→| `same_site` | `'lax'` | Allows navigation-initiated requests while preventing CSRF from external origins. |
   912→| `partitioned` | `false` | Not needed for this use case. |
   913→
   914→---
   915→
   916→## 6. CORS Configuration
   917→
   918→File: `config/cors.php`
   919→
   920→| Setting | Value | Rationale |
   921→|---------|-------|-----------|
   922→| `paths` | `['api/*', 'sanctum/csrf-cookie']` | Only API and Sanctum CSRF cookie endpoints are CORS-enabled. Web routes are not. |
   923→| `allowed_methods` | `['*']` | All HTTP methods allowed for API. |
   924→| `allowed_origins` | `[FRONTEND_URL env var, default '*']` | Restrict to store domain in production. Wildcard for development. |
   925→| `allowed_origins_patterns` | `[]` | No pattern-based origin matching needed. |
   926→| `allowed_headers` | `['*']` | All headers allowed (Authorization, Content-Type, etc.). |
   927→| `exposed_headers` | `['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'Retry-After']` | Expose rate limit headers so API clients can implement backoff. |
   928→| `max_age` | `0` | No preflight caching (conservative default). |
   929→| `supports_credentials` | `true` | Required for Sanctum SPA auth (cookies). |
   930→
   931→---
   932→
   933→## 7. Webhook Signature Verification
   934→
   935→### 7.1 Inbound Payment Webhooks
   936→
   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).
   938→
   939→---
   940→
   941→### 7.2 Platform Webhooks (Outbound)
   942→
   943→Service: `App\Services\WebhookDispatcher`
   944→
   945→When the platform sends webhooks to external subscribers (third-party apps, integrations).
   946→
   947→```mermaid
   948→sequenceDiagram
   949→    participant App as Platform
   950→    participant Sub as Subscriber URL
   951→
   952→    App->>App: Serialize payload as JSON
   953→    App->>App: Generate Unix timestamp
   954→    App->>App: Concatenate: "{timestamp}.{json_body}"
   955→    App->>App: HMAC-SHA256(signing_secret, concatenated_message)
   956→    App->>Sub: POST to subscriber URL
   957→    Note right of Sub: Headers: X-Platform-Signature, X-Platform-Timestamp, Content-Type: application/json
   958→    Sub-->>App: 200 OK (acknowledged)
   959→```
   960→
   961→**Requirements:**
   962→
   963→1. The payload is serialized as JSON.
   964→2. A Unix timestamp is generated (current time).
   965→3. The message to sign is: `"{timestamp}.{json_body}"` (timestamp, dot, JSON string).
   966→4. The signature is computed as HMAC-SHA256 using the per-subscription `signing_secret` (decrypted from the `signing_secret_encrypted` column).
   967→5. The HTTP request to the subscriber includes three headers:
   968→   - `X-Platform-Signature`: the hex-encoded HMAC digest
   969→   - `X-Platform-Timestamp`: the Unix timestamp as a string
   970→   - `Content-Type`: `application/json`
   971→6. Replay protection guidance for recipients: reject any request where `X-Platform-Timestamp` is more than 5 minutes in the past.
   972→
   973→---
   974→
   975→## 8. Audit Logging
   976→
   977→### 8.1 Logging Channel
   978→
   979→A dedicated `audit` channel must be added to `config/logging.php`.
   980→
   981→| Setting | Value |
   982→|---------|-------|
   983→| Driver | `daily` (file-based, daily rotation) |
   984→| Path | `storage/logs/audit.log` |
   985→| Level | `info` |
   986→| Retention | 90 days |
   987→
   988→---
   989→
   990→### 8.2 Log Entry Format
   991→
   992→All audit log entries are structured JSON objects with the following schema:
   993→
   994→| Field | Type | Required | Description |
   995→|-------|------|:--------:|-------------|
   996→| `timestamp` | string (ISO-8601) | Y | When the event occurred |
   997→| `event` | string | Y | Event identifier (e.g., `product.updated`, `auth.login`) |
   998→| `user_id` | integer or null | N | ID of the admin user who triggered the event |
   999→| `store_id` | integer or null | N | ID of the store context |

prompt: Read the file /Users/fabianwesner/Herd/shop/specs/06-AUTH-AND-SECURITY.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL auth and security specifications including: guards, providers, middleware, rate limiting, permission matrices, CSRF, and session management. I need every detail for implementation.


userType: external

slug: iterative-seeking-pine

type: progress

type: assistant

type: message

role: assistant

service_tier: standard

prompt: Read the file /Users/fabianwesner/Herd/shop/specs/06-AUTH-AND-SECURITY.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL auth and security specifications including: guards, providers, middleware, rate limiting, permission matrices, CSRF, and session management. I need every detail for implementation.


type: user

role: user

content:
  1000→| `resource_type` | string or null | N | Type of resource affected (e.g., `product`, `order`) |
  1001→| `resource_id` | integer or null | N | ID of the affected resource |
  1002→| `ip` | string | Y | Client IP address |
  1003→| `user_agent` | string | Y | Client user agent string |
  1004→| `changes` | object or null | N | For update events: object mapping field names to `[old_value, new_value]` tuples |
  1005→| (additional keys) | varies | N | Event-specific extra data |
  1006→
  1007→---
  1008→
  1009→### 8.3 Events to Log
  1010→
  1011→#### Authentication Events
  1012→
  1013→| Event ID | Trigger | Extra Data |
  1014→|----------|---------|------------|
  1015→| `auth.login` | Successful admin login | user_id, ip, user_agent |
  1016→| `auth.logout` | Admin logout | user_id |
  1017→| `auth.failed_login` | Failed admin login attempt | email (never password), ip, user_agent |
  1018→| `customer.login` | Successful customer login | customer_id, store_id, ip |
  1019→| `customer.registered` | New customer registration | customer_id, store_id |
  1020→| `customer.failed_login` | Failed customer login attempt | email, store_id, ip |
  1021→
  1022→#### Admin Resource Events
  1023→
  1024→For each of the following resource types, log `created`, `updated`, `deleted`, and `restored` (where applicable) events:
  1025→
  1026→**Resource types:** product, collection, discount, page, theme, order, fulfillment, refund, navigation_menu, shipping_zone, tax_setting, staff
  1027→
  1028→| Event Pattern | Trigger | Extra Data |
  1029→|---------------|---------|------------|
  1030→| `{resource}.created` | Resource created | resource_id, key attributes of the new resource |
  1031→| `{resource}.updated` | Resource updated | resource_id, `changes` object with old/new values for each changed field |
  1032→| `{resource}.deleted` | Resource soft-deleted or archived | resource_id |
  1033→| `{resource}.restored` | Resource restored from soft-delete | resource_id |
  1034→
  1035→#### Security Events
  1036→
  1037→| Event ID | Trigger | Extra Data |
  1038→|----------|---------|------------|
  1039→| `api_token.created` | New API token issued | token_name, abilities list, user_id |
  1040→| `api_token.revoked` | API token revoked | token_name, user_id |
  1041→| `webhook.signature_invalid` | Outbound webhook delivery failed signature verification at subscriber | subscriber_url, ip |
  1042→| `store.settings_changed` | Store settings updated | changed fields with old/new values |
  1043→
  1044→---
  1045→
  1046→### 8.4 Audit Logger Service
  1047→
  1048→File: `app/Services/AuditLogger.php`
  1049→
  1050→A service class that encapsulates audit log writing.
  1051→
  1052→**Method signature:**
  1053→
  1054→| Parameter | Type | Required | Description |
  1055→|-----------|------|:--------:|-------------|
  1056→| `event` | string | Y | Event identifier |
  1057→| `userId` | int or null | N | Acting user ID |
  1058→| `storeId` | int or null | N | Store context ID |
  1059→| `resourceType` | string or null | N | Affected resource type |
  1060→| `resourceId` | int or null | N | Affected resource ID |
  1061→| `extra` | associative array | N | Additional key-value pairs to include in the log entry |
  1062→
  1063→**Behavior:**
  1064→
  1065→1. Write to the `audit` logging channel at the `info` level.
  1066→2. Automatically include `timestamp` (ISO-8601), `ip` (from request), and `user_agent` (from request) in every entry.
  1067→3. Merge any `extra` data into the log context.
  1068→
  1069→---
  1070→
  1071→### 8.5 Integration with Laravel Events
  1072→
  1073→The audit logger hooks into Laravel's built-in authentication events and custom model observers.
  1074→
  1075→#### Auth Event Listeners
  1076→
  1077→Register listeners (in `AppServiceProvider` or `EventServiceProvider`) for these Laravel events:
  1078→
  1079→| Laravel Event | Audit Event ID | Data Captured |
  1080→|--------------|----------------|---------------|
  1081→| `Illuminate\Auth\Events\Login` | `auth.login` | user_id from event |
  1082→| `Illuminate\Auth\Events\Failed` | `auth.failed_login` | email from credentials (never password) |
  1083→| `Illuminate\Auth\Events\Logout` | `auth.logout` | user_id from event |
  1084→
  1085→#### Model Observers
  1086→
  1087→For each auditable resource model, create an observer (or use a shared trait) that logs `created`, `updated`, and `deleted` events. The `updated` observer should capture the changed fields (dirty attributes) with their original and new values.
  1088→
  1089→---
  1090→
  1091→## 9. Security Checklist Summary
  1092→
  1093→| # | Control | Implementation Approach | Priority |
  1094→|---|---------|------------------------|----------|
  1095→| 1 | Session-based auth (admin) | Laravel `web` guard with bcrypt, session regeneration on login | Required |
  1096→| 2 | Session-based auth (customer) | Custom `customer` guard, store-scoped via custom user provider | Required |
  1097→| 3 | API token auth | Sanctum personal access tokens with per-token abilities | Required |
  1098→| 4 | Role-based access control | `StoreUserRole` enum, Laravel policies on all models, gates for non-model ops | Required |
  1099→| 5 | CSRF protection | `@csrf` on all session forms, Livewire auto-handles, API routes exempt | Required |
  1100→| 6 | Rate limiting | Seven named limiters (login, api.admin, api.storefront, checkout, search, analytics, webhooks) | Required |
  1101→| 7 | Input validation | Dedicated Form Request class for every write endpoint | Required |
  1102→| 8 | XSS prevention | Blade auto-escaping by default, HTMLPurifier for rich content fields | Required |
  1103→| 9 | SQL injection prevention | Eloquent ORM with parameterized queries, FTS5 input sanitization | Required |
  1104→| 10 | Mass assignment protection | `$fillable` on all models, always use `$request->validated()` | Required |
  1105→| 11 | Encryption at rest | Laravel `encrypted` cast for secrets and sensitive JSON columns | Required |
  1106→| 12 | Password security | Bcrypt, min 8 characters, `$hidden` on User and Customer models | Required |
  1107→| 13 | Session security | Encrypted sessions, HTTP-only cookies, secure flag, SameSite=Lax | Required |
  1108→| 14 | Webhook signature verification | HMAC-SHA256 for outbound platform webhooks (no inbound payment webhooks - Mock PSP is in-process) | Required |
  1109→| 15 | Audit logging | Structured JSON to dedicated daily-rotation log channel, 90-day retention | Required |
  1110→| 16 | CORS | Restricted to `api/*` paths, credentials supported, rate limit headers exposed | Required |
  1111→

prompt: Read the file /Users/fabianwesner/Herd/shop/specs/06-AUTH-AND-SECURITY.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL auth and security specifications including: guards, providers, middleware, rate limiting, permission matrices, CSRF, and session management. I need every detail for implementation.


prompt: Read the file /Users/fabianwesner/Herd/shop/specs/01-DATABASE-SCHEMA.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL table definitions including: table names, column names with types, foreign keys, indexes, constraints, and enums. I need every detail for implementation.


content:
  1000→
  1001→**Indexes:**
  1002→
  1003→- `idx_app_installations_store_app` UNIQUE on (`store_id`, `app_id`) - One installation per app per store
  1004→- `idx_app_installations_store_id` on (`store_id`) - Listing installations per store
  1005→- `idx_app_installations_app_id` on (`app_id`) - Finding all stores with an app installed
  1006→
  1007→---
  1008→
  1009→### oauth_clients
  1010→
  1011→OAuth2 client credentials for apps.
  1012→
  1013→| Column | Type | Nullable | Default | Constraints | Notes |
  1014→|---|---|---|---|---|---|
  1015→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
  1016→| app_id | INTEGER | No | - | FK -> apps(id) ON DELETE CASCADE | Parent app |
  1017→| client_id | TEXT | No | - | UNIQUE per table | OAuth2 client identifier |
  1018→| client_secret_encrypted | TEXT | No | - | - | Encrypted client secret |
  1019→| redirect_uris_json | TEXT | No | '[]' | - | JSON array of allowed redirect URIs |
  1020→
  1021→**Indexes:**
  1022→
  1023→- `idx_oauth_clients_client_id` UNIQUE on (`client_id`) - OAuth2 client lookup
  1024→- `idx_oauth_clients_app_id` on (`app_id`) - FK lookup, listing clients per app
  1025→
  1026→---
  1027→
  1028→### oauth_tokens
  1029→
  1030→Issued access/refresh tokens for app installations.
  1031→
  1032→| Column | Type | Nullable | Default | Constraints | Notes |
  1033→|---|---|---|---|---|---|
  1034→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
  1035→| installation_id | INTEGER | No | - | FK -> app_installations(id) ON DELETE CASCADE | Parent installation |
  1036→| access_token_hash | TEXT | No | - | UNIQUE per table | Hashed access token |
  1037→| refresh_token_hash | TEXT | Yes | NULL | - | Hashed refresh token |
  1038→| expires_at | TEXT | No | - | - | ISO-8601 expiration timestamp |
  1039→
  1040→**Indexes:**
  1041→
  1042→- `idx_oauth_tokens_installation_id` on (`installation_id`) - FK lookup, listing tokens per installation
  1043→- `idx_oauth_tokens_access_hash` UNIQUE on (`access_token_hash`) - Token validation lookup
  1044→- `idx_oauth_tokens_expires_at` on (`expires_at`) - Cleanup of expired tokens
  1045→
  1046→---
  1047→
  1048→### webhook_subscriptions
  1049→
  1050→Registered webhook endpoints for event delivery.
  1051→
  1052→| Column | Type | Nullable | Default | Constraints | Notes |
  1053→|---|---|---|---|---|---|
  1054→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
  1055→| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
  1056→| app_installation_id | INTEGER | Yes | NULL | FK -> app_installations(id) ON DELETE CASCADE | Nullable for store-level hooks |
  1057→| event_type | TEXT | No | - | - | Event to subscribe to |
  1058→| target_url | TEXT | No | - | - | Delivery endpoint URL |
  1059→| signing_secret_encrypted | TEXT | No | - | - | Encrypted HMAC signing secret |
  1060→| status | TEXT | No | 'active' | CHECK IN ('active', 'paused', 'disabled') | |
  1061→
  1062→**Indexes:**
  1063→
  1064→- `idx_webhook_subscriptions_store_id` on (`store_id`) - FK lookup
  1065→- `idx_webhook_subscriptions_store_event` on (`store_id`, `event_type`) - Finding subscriptions for a given event
  1066→- `idx_webhook_subscriptions_installation` on (`app_installation_id`) - Listing subscriptions per app installation
  1067→
  1068→---
  1069→
  1070→### webhook_deliveries
  1071→
  1072→Delivery log for webhook attempts.
  1073→
  1074→| Column | Type | Nullable | Default | Constraints | Notes |
  1075→|---|---|---|---|---|---|
  1076→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
  1077→| subscription_id | INTEGER | No | - | FK -> webhook_subscriptions(id) ON DELETE CASCADE | Parent subscription |
  1078→| event_id | TEXT | No | - | - | Unique event identifier |
  1079→| attempt_count | INTEGER | No | 1 | - | Number of delivery attempts |
  1080→| status | TEXT | No | 'pending' | CHECK IN ('pending', 'success', 'failed') | |
  1081→| last_attempt_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
  1082→| response_code | INTEGER | Yes | NULL | - | HTTP response code |
  1083→| response_body_snippet | TEXT | Yes | NULL | - | Truncated response body for debugging |
  1084→
  1085→**Indexes:**
  1086→
  1087→- `idx_webhook_deliveries_subscription_id` on (`subscription_id`) - FK lookup, listing deliveries per subscription
  1088→- `idx_webhook_deliveries_event_id` on (`event_id`) - Deduplication and lookup by event
  1089→- `idx_webhook_deliveries_status` on (`status`) - Finding pending/failed deliveries for retry
  1090→- `idx_webhook_deliveries_last_attempt` on (`last_attempt_at`) - Time-based retry scheduling
  1091→
  1092→---
  1093→
  1094→## Laravel Framework Tables
  1095→
  1096→The following tables are managed by Laravel's built-in migrations and should not be modified.
  1097→
  1098→### sessions
  1099→
  1100→| Column | Type | Nullable | Default | Constraints | Notes |
  1101→|---|---|---|---|---|---|
  1102→| id | TEXT | No | - | PRIMARY KEY | Session ID |
  1103→| user_id | INTEGER | Yes | NULL | - | Associated user |
  1104→| ip_address | TEXT | Yes | NULL | - | Client IP |
  1105→| user_agent | TEXT | Yes | NULL | - | Client user agent |
  1106→| payload | TEXT | No | - | - | Serialized session data |
  1107→| last_activity | INTEGER | No | - | - | Unix timestamp |
  1108→
  1109→### cache
  1110→
  1111→| Column | Type | Nullable | Default | Constraints | Notes |
  1112→|---|---|---|---|---|---|
  1113→| key | TEXT | No | - | PRIMARY KEY | Cache key |
  1114→| value | TEXT | No | - | - | Serialized cache value |
  1115→| expiration | INTEGER | No | - | - | Unix timestamp |
  1116→
  1117→### cache_locks
  1118→
  1119→| Column | Type | Nullable | Default | Constraints | Notes |
  1120→|---|---|---|---|---|---|
  1121→| key | TEXT | No | - | PRIMARY KEY | Lock key |
  1122→| owner | TEXT | No | - | - | Lock owner identifier |
  1123→| expiration | INTEGER | No | - | - | Unix timestamp |
  1124→
  1125→### jobs
  1126→
  1127→| Column | Type | Nullable | Default | Constraints | Notes |
  1128→|---|---|---|---|---|---|
  1129→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
  1130→| queue | TEXT | No | - | - | Queue name |
  1131→| payload | TEXT | No | - | - | Serialized job payload |
  1132→| attempts | INTEGER | No | - | - | Number of attempts |
  1133→| reserved_at | INTEGER | Yes | NULL | - | Unix timestamp |
  1134→| available_at | INTEGER | No | - | - | Unix timestamp |
  1135→| created_at | INTEGER | No | - | - | Unix timestamp |
  1136→
  1137→### job_batches
  1138→
  1139→| Column | Type | Nullable | Default | Constraints | Notes |
  1140→|---|---|---|---|---|---|
  1141→| id | TEXT | No | - | PRIMARY KEY | Batch UUID |
  1142→| name | TEXT | No | - | - | Batch name |
  1143→| total_jobs | INTEGER | No | - | - | Total jobs in batch |
  1144→| pending_jobs | INTEGER | No | - | - | Pending count |
  1145→| failed_jobs | INTEGER | No | - | - | Failed count |
  1146→| failed_job_ids | TEXT | No | - | - | Serialized list of failed IDs |
  1147→| options | TEXT | Yes | NULL | - | Serialized batch options |
  1148→| cancelled_at | INTEGER | Yes | NULL | - | Unix timestamp |
  1149→| created_at | INTEGER | No | - | - | Unix timestamp |
  1150→| finished_at | INTEGER | Yes | NULL | - | Unix timestamp |
  1151→
  1152→### failed_jobs
  1153→
  1154→| Column | Type | Nullable | Default | Constraints | Notes |
  1155→|---|---|---|---|---|---|
  1156→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
  1157→| uuid | TEXT | No | - | UNIQUE | Job UUID |
  1158→| connection | TEXT | No | - | - | Queue connection name |
  1159→| queue | TEXT | No | - | - | Queue name |
  1160→| payload | TEXT | No | - | - | Serialized job payload |
  1161→| exception | TEXT | No | - | - | Exception trace |
  1162→| failed_at | TEXT | No | CURRENT_TIMESTAMP | - | ISO-8601 timestamp |
  1163→
  1164→### personal_access_tokens
  1165→
  1166→| Column | Type | Nullable | Default | Constraints | Notes |
  1167→|---|---|---|---|---|---|
  1168→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
  1169→| tokenable_type | TEXT | No | - | - | Polymorphic type |
  1170→| tokenable_id | INTEGER | No | - | - | Polymorphic ID |
  1171→| name | TEXT | No | - | - | Token name |
  1172→| token | TEXT | No | - | UNIQUE | SHA-256 hash of the token |
  1173→| abilities | TEXT | Yes | NULL | - | JSON array of abilities |
  1174→| last_used_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
  1175→| expires_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
  1176→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
  1177→| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
  1178→
  1179→### password_reset_tokens
  1180→
  1181→| Column | Type | Nullable | Default | Constraints | Notes |
  1182→|---|---|---|---|---|---|
  1183→| email | TEXT | No | - | PRIMARY KEY | Admin user email |
  1184→| token | TEXT | No | - | - | Hashed reset token |
  1185→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
  1186→
  1187→---
  1188→
  1189→### customer_password_reset_tokens
  1190→
  1191→| Column | Type | Nullable | Default | Constraints | Notes |
  1192→|---|---|---|---|---|---|
  1193→| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
  1194→| email | TEXT | No | - | - | Customer email |
  1195→| token | TEXT | No | - | - | Hashed reset token |
  1196→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
  1197→
  1198→**Primary Key:** (`store_id`, `email`)
  1199→
  1200→---
  1201→
  1202→## Entity Relationship Diagrams
  1203→
  1204→### Epic 1 - Foundation and Tenancy
  1205→
  1206→```mermaid
  1207→erDiagram
  1208→    organizations ||--o{ stores : "owns"
  1209→    stores ||--o{ store_domains : "has"
  1210→    stores ||--o{ store_users : "links"
  1211→    users ||--o{ store_users : "links"
  1212→    stores ||--|| store_settings : "has"
  1213→
  1214→    organizations {
  1215→        INTEGER id PK
  1216→        TEXT name
  1217→        TEXT billing_email
  1218→    }
  1219→    stores {
  1220→        INTEGER id PK
  1221→        INTEGER organization_id FK
  1222→        TEXT name
  1223→        TEXT handle
  1224→        TEXT status
  1225→    }
  1226→    store_domains {
  1227→        INTEGER id PK
  1228→        INTEGER store_id FK
  1229→        TEXT hostname
  1230→        TEXT type
  1231→        INTEGER is_primary
  1232→    }
  1233→    users {
  1234→        INTEGER id PK
  1235→        TEXT email
  1236→        TEXT password_hash
  1237→        TEXT name
  1238→        TEXT status
  1239→    }
  1240→    store_users {
  1241→        INTEGER store_id PK
  1242→        INTEGER user_id PK
  1243→        TEXT role
  1244→    }
  1245→    store_settings {
  1246→        INTEGER store_id PK
  1247→        TEXT settings_json
  1248→    }
  1249→```
  1250→
  1251→### Epic 2 - Catalog, Collections, Inventory, and Media
  1252→
  1253→```mermaid
  1254→erDiagram
  1255→    stores ||--o{ products : "has"
  1256→    stores ||--o{ collections : "has"
  1257→    products ||--o{ product_options : "defines"
  1258→    product_options ||--o{ product_option_values : "contains"
  1259→    products ||--o{ product_variants : "has"
  1260→    product_variants }o--o{ product_option_values : "variant_option_values"
  1261→    product_variants ||--|| inventory_items : "tracked by"
  1262→    stores ||--o{ inventory_items : "has"
  1263→    collections }o--o{ products : "collection_products"
  1264→    products ||--o{ product_media : "has"
  1265→
  1266→    products {
  1267→        INTEGER id PK
  1268→        INTEGER store_id FK
  1269→        TEXT title
  1270→        TEXT handle
  1271→        TEXT status
  1272→    }
  1273→    product_options {
  1274→        INTEGER id PK
  1275→        INTEGER product_id FK
  1276→        TEXT name
  1277→        INTEGER position
  1278→    }
  1279→    product_option_values {
  1280→        INTEGER id PK
  1281→        INTEGER product_option_id FK
  1282→        TEXT value
  1283→        INTEGER position
  1284→    }
  1285→    product_variants {
  1286→        INTEGER id PK
  1287→        INTEGER product_id FK
  1288→        TEXT sku
  1289→        INTEGER price_amount
  1290→        TEXT status
  1291→    }
  1292→    inventory_items {
  1293→        INTEGER id PK
  1294→        INTEGER store_id FK
  1295→        INTEGER variant_id FK
  1296→        INTEGER quantity_on_hand
  1297→        INTEGER quantity_reserved
  1298→        TEXT policy
  1299→    }
  1300→    collections {
  1301→        INTEGER id PK
  1302→        INTEGER store_id FK
  1303→        TEXT title
  1304→        TEXT handle
  1305→        TEXT type
  1306→        TEXT status
  1307→    }
  1308→    product_media {
  1309→        INTEGER id PK
  1310→        INTEGER product_id FK
  1311→        TEXT type
  1312→        TEXT storage_key
  1313→        INTEGER position
  1314→        TEXT status
  1315→    }
  1316→```
  1317→
  1318→### Epic 3 - Themes, Storefront, and Localization
  1319→
  1320→```mermaid
  1321→erDiagram
  1322→    stores ||--o{ themes : "has"
  1323→    themes ||--o{ theme_files : "contains"
  1324→    themes ||--|| theme_settings : "configured by"
  1325→    stores ||--o{ pages : "has"
  1326→    stores ||--o{ navigation_menus : "has"
  1327→    navigation_menus ||--o{ navigation_items : "contains"
  1328→
  1329→    themes {
  1330→        INTEGER id PK
  1331→        INTEGER store_id FK
  1332→        TEXT name
  1333→        TEXT status
  1334→    }
  1335→    theme_files {
  1336→        INTEGER id PK
  1337→        INTEGER theme_id FK
  1338→        TEXT path
  1339→        TEXT storage_key
  1340→    }
  1341→    theme_settings {
  1342→        INTEGER theme_id PK
  1343→        TEXT settings_json
  1344→    }
  1345→    pages {
  1346→        INTEGER id PK
  1347→        INTEGER store_id FK
  1348→        TEXT title
  1349→        TEXT handle
  1350→        TEXT status
  1351→    }
  1352→    navigation_menus {
  1353→        INTEGER id PK
  1354→        INTEGER store_id FK
  1355→        TEXT handle
  1356→        TEXT title
  1357→    }
  1358→    navigation_items {
  1359→        INTEGER id PK
  1360→        INTEGER menu_id FK
  1361→        TEXT type
  1362→        TEXT label
  1363→        INTEGER position
  1364→    }
  1365→```
  1366→
  1367→### Epics 5-6 - Cart, Checkout, Orders, and Payments
  1368→
  1369→```mermaid
  1370→erDiagram
  1371→    stores ||--o{ customers : "has"
  1372→    customers ||--o{ customer_addresses : "has"
  1373→    stores ||--o{ carts : "has"
  1374→    customers ||--o{ carts : "owns"
  1375→    carts ||--o{ cart_lines : "contains"
  1376→    product_variants ||--o{ cart_lines : "referenced by"
  1377→    stores ||--o{ checkouts : "has"
  1378→    carts ||--o{ checkouts : "converted to"
  1379→    stores ||--o{ orders : "has"
  1380→    customers ||--o{ orders : "placed"
  1381→    orders ||--o{ order_lines : "contains"
  1382→    orders ||--o{ payments : "paid via"
  1383→    orders ||--o{ refunds : "refunded via"
  1384→    payments ||--o{ refunds : "source of"
  1385→    orders ||--o{ fulfillments : "shipped via"
  1386→    fulfillments ||--o{ fulfillment_lines : "contains"
  1387→    order_lines ||--o{ fulfillment_lines : "fulfilled by"
  1388→
  1389→    customers {
  1390→        INTEGER id PK
  1391→        INTEGER store_id FK
  1392→        TEXT email
  1393→        TEXT name
  1394→    }
  1395→    carts {
  1396→        INTEGER id PK
  1397→        INTEGER store_id FK
  1398→        INTEGER customer_id FK
  1399→        TEXT status
  1400→    }
  1401→    orders {
  1402→        INTEGER id PK
  1403→        INTEGER store_id FK
  1404→        INTEGER customer_id FK
  1405→        TEXT order_number
  1406→        TEXT payment_method
  1407→        TEXT status
  1408→        TEXT financial_status
  1409→        TEXT fulfillment_status
  1410→        INTEGER total_amount
  1411→    }
  1412→    payments {
  1413→        INTEGER id PK
  1414→        INTEGER order_id FK
  1415→        TEXT provider
  1416→        TEXT method
  1417→        TEXT status
  1418→        INTEGER amount
  1419→    }
  1420→    fulfillments {
  1421→        INTEGER id PK
  1422→        INTEGER order_id FK
  1423→        TEXT status
  1424→        TEXT tracking_number
  1425→    }
  1426→```
  1427→
  1428→### Epics 7-8 - Analytics, Apps, and Webhooks
  1429→
  1430→```mermaid
  1431→erDiagram
  1432→    stores ||--o{ analytics_events : "records"
  1433→    stores ||--o{ analytics_daily : "aggregates"
  1434→    customers ||--o{ analytics_events : "generates"
  1435→    apps ||--o{ app_installations : "installed via"
  1436→    stores ||--o{ app_installations : "has"
  1437→    apps ||--o{ oauth_clients : "authenticates via"
  1438→    app_installations ||--o{ oauth_tokens : "issues"
  1439→    stores ||--o{ webhook_subscriptions : "registers"
  1440→    app_installations ||--o{ webhook_subscriptions : "owns"
  1441→    webhook_subscriptions ||--o{ webhook_deliveries : "delivers"
  1442→
  1443→    analytics_events {
  1444→        INTEGER id PK
  1445→        INTEGER store_id FK
  1446→        TEXT type
  1447→        TEXT session_id
  1448→    }
  1449→    analytics_daily {
  1450→        INTEGER store_id PK
  1451→        TEXT date PK
  1452→        INTEGER orders_count
  1453→        INTEGER revenue_amount
  1454→    }
  1455→    apps {
  1456→        INTEGER id PK
  1457→        TEXT name
  1458→        TEXT status
  1459→    }
  1460→    app_installations {
  1461→        INTEGER id PK
  1462→        INTEGER store_id FK
  1463→        INTEGER app_id FK
  1464→        TEXT status
  1465→    }
  1466→    webhook_subscriptions {
  1467→        INTEGER id PK
  1468→        INTEGER store_id FK
  1469→        TEXT event_type
  1470→        TEXT target_url
  1471→    }
  1472→    webhook_deliveries {
  1473→        INTEGER id PK
  1474→        INTEGER subscription_id FK
  1475→        TEXT event_id
  1476→        TEXT status
  1477→    }
  1478→```
  1479→
  1480→---
  1481→
  1482→## SQLite-Specific Configuration Requirements
  1483→
  1484→The application requires specific SQLite PRAGMA settings configured in the Laravel database configuration file (`config/database.php`). These settings must be applied for correct and performant operation.
  1485→
  1486→### Required PRAGMA Settings
  1487→
  1488→| Pragma | Value | Purpose |
  1489→|---|---|---|
  1490→| `foreign_keys` | ON | Enforce referential integrity on every connection. Without this, foreign key constraints are silently ignored by SQLite. |
  1491→| `journal_mode` | WAL | Enable Write-Ahead Logging to allow concurrent reads during writes. Critical for web application performance. |
  1492→| `busy_timeout` | 5000 | Wait up to 5 seconds for a write lock before throwing an error. Prevents immediate failures under concurrent load. |
  1493→| `synchronous` | NORMAL | Balances durability and performance. Safe when used with WAL mode; avoids fsync on every commit. |
  1494→| `cache_size` | -20000 | Use up to 20 MB of page cache. Set in AppServiceProvider if needed. Negative value means kilobytes. |
  1495→
  1496→### Laravel Configuration Requirements
  1497→
  1498→- The SQLite driver connection must set `foreign_key_constraints` to `true` so that every new database connection enables foreign key enforcement.
  1499→- The `journal_mode` must be set to `wal` (Write-Ahead Logging) for concurrent read support.

prompt: Read the file /Users/fabianwesner/Herd/shop/specs/01-DATABASE-SCHEMA.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL table definitions including: table names, column names with types, foreign keys, indexes, constraints, and enums. I need every detail for implementation.


prompt: Read the file /Users/fabianwesner/Herd/shop/specs/02-API-ROUTES.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL API routes including: method, URL, middleware, controller/handler, request/response formats, and validation rules. I need every detail for implementation.


content:
   500→      "unit_price_amount": 2500,
   501→      "line_total_amount": 5000
   502→    }
   503→  ],
   504→  "totals": {
   505→    "subtotal": 5000,
   506→    "discount": 0,
   507→    "shipping": 0,
   508→    "tax": 0,
   509→    "total": 5000,
   510→    "currency": "EUR"
   511→  },
   512→  "available_shipping_methods": [],
   513→  "expires_at": "2026-02-12T10:00:00Z",
   514→  "created_at": "2026-02-11T10:00:00Z"
   515→}
   516→```
   517→
   518→**Error responses:**
   519→- `404` - Cart not found
   520→- `422` - Validation errors (empty cart, invalid email)
   521→
   522→---
   523→
   524→#### GET /api/storefront/v1/checkouts/{checkoutId}
   525→
   526→Retrieve the current checkout state and computed totals.
   527→
   528→- **Authentication:** None (checkout ID acts as token)
   529→- **Rate limit:** `checkout` (10/min per session)
   530→
   531→**Success response:** `200 OK`
   532→
   533→Returns the full checkout object (same schema as POST response, with updated fields reflecting the current state).
   534→
   535→**Error responses:**
   536→- `404` - Checkout not found
   537→- `410` - Checkout expired
   538→
   539→---
   540→
   541→#### PUT /api/storefront/v1/checkouts/{checkoutId}/address
   542→
   543→Set the shipping and/or billing address on a checkout.
   544→
   545→- **Authentication:** None
   546→- **Rate limit:** `checkout` (10/min per session)
   547→
   548→**Request body:**
   549→
   550→```json
   551→{
   552→  "shipping_address": {
   553→    "first_name": "Jane",
   554→    "last_name": "Doe",
   555→    "address1": "123 Main St",
   556→    "address2": "Apt 4B",
   557→    "city": "Berlin",
   558→    "province": "Berlin",
   559→    "province_code": "BE",
   560→    "country": "DE",
   561→    "country_code": "DE",
   562→    "postal_code": "10115",
   563→    "phone": "+49301234567"
   564→  },
   565→  "billing_address": null,
   566→  "use_shipping_as_billing": true
   567→}
   568→```
   569→
   570→**Shipping address fields:**
   571→
   572→| Field | Type | Required | Validation |
   573→|-------|------|----------|------------|
   574→| `shipping_address` | object | Yes (if any line requires shipping) | See fields below |
   575→| `shipping_address.first_name` | string | Yes | Max: 255 |
   576→| `shipping_address.last_name` | string | Yes | Max: 255 |
   577→| `shipping_address.address1` | string | Yes | Max: 500 |
   578→| `shipping_address.address2` | string | No | Max: 500 |
   579→| `shipping_address.city` | string | Yes | Max: 255 |
   580→| `shipping_address.province` | string | No | Max: 255 |
   581→| `shipping_address.province_code` | string | No | Max: 10 |
   582→| `shipping_address.country` | string | Yes | Max: 255 |
   583→| `shipping_address.country_code` | string | Yes | Valid ISO 3166-1 alpha-2 |
   584→| `shipping_address.postal_code` | string | Yes | Max: 20 |
   585→| `shipping_address.phone` | string | No | Max: 50 |
   586→| `billing_address` | object | No | Same fields as shipping_address |
   587→| `use_shipping_as_billing` | boolean | No | Default: true. If true, billing_address is ignored and copied from shipping. |
   588→
   589→**Success response:** `200 OK`
   590→
   591→Returns the full checkout object. Status transitions to `addressed`. The response includes `available_shipping_methods` populated based on the shipping address and store's shipping zones.
   592→
   593→```json
   594→{
   595→  "id": 7,
   596→  "status": "addressed",
   597→  "shipping_address_json": {
   598→    "first_name": "Jane",
   599→    "last_name": "Doe",
   600→    "address1": "123 Main St",
   601→    "address2": "Apt 4B",
   602→    "city": "Berlin",
   603→    "province": "Berlin",
   604→    "province_code": "BE",
   605→    "country": "DE",
   606→    "country_code": "DE",
   607→    "postal_code": "10115",
   608→    "phone": "+49301234567"
   609→  },
   610→  "billing_address_json": {
   611→    "first_name": "Jane",
   612→    "last_name": "Doe",
   613→    "address1": "123 Main St",
   614→    "address2": "Apt 4B",
   615→    "city": "Berlin",
   616→    "province": "Berlin",
   617→    "province_code": "BE",
   618→    "country": "DE",
   619→    "country_code": "DE",
   620→    "postal_code": "10115",
   621→    "phone": "+49301234567"
   622→  },
   623→  "available_shipping_methods": [
   624→    {
   625→      "id": 1,
   626→      "name": "Standard Shipping",
   627→      "type": "flat",
   628→      "price_amount": 500,
   629→      "currency": "EUR",
   630→      "estimated_days_min": 3,
   631→      "estimated_days_max": 5
   632→    },
   633→    {
   634→      "id": 2,
   635→      "name": "Express Shipping",
   636→      "type": "flat",
   637→      "price_amount": 1200,
   638→      "currency": "EUR",
   639→      "estimated_days_min": 1,
   640→      "estimated_days_max": 2
   641→    }
   642→  ],
   643→  "totals": {
   644→    "subtotal": 5000,
   645→    "discount": 0,
   646→    "shipping": 0,
   647→    "tax": 0,
   648→    "total": 5000,
   649→    "currency": "EUR"
   650→  }
   651→}
   652→```
   653→
   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)
   658→
   659→---
   660→
   661→#### PUT /api/storefront/v1/checkouts/{checkoutId}/shipping-method
   662→
   663→Select a shipping method for the checkout.
   664→
   665→- **Authentication:** None
   666→- **Rate limit:** `checkout` (10/min per session)
   667→
   668→**Request body:**
   669→
   670→```json
   671→{
   672→  "shipping_method_id": 1
   673→}
   674→```
   675→
   676→| Field | Type | Required | Validation |
   677→|-------|------|----------|------------|
   678→| `shipping_method_id` | integer | Yes | Must be one of the available shipping methods for this checkout's address. |
   679→
   680→**Success response:** `200 OK`
   681→
   682→Returns the full checkout object. Status transitions to `shipping_selected`. Totals are recalculated including the shipping cost and taxes (if applicable).
   683→
   684→```json
   685→{
   686→  "id": 7,
   687→  "status": "shipping_selected",
   688→  "shipping_method_id": 1,
   689→  "totals": {
   690→    "subtotal": 5000,
   691→    "discount": 0,
   692→    "shipping": 500,
   693→    "tax": 1045,
   694→    "total": 6545,
   695→    "currency": "EUR"
   696→  },
   697→  "tax_provider_snapshot_json": {
   698→    "provider": "manual",
   699→    "calculated_at": "2026-02-11T10:05:00Z",
   700→    "lines": [
   701→      {
   702→        "variant_id": 101,
   703→        "tax_amount": 950,
   704→        "rate": 1900,
   705→        "jurisdiction": "DE"
   706→      }
   707→    ],
   708→    "shipping_tax_amount": 95,
   709→    "shipping_tax_rate": 1900
   710→  }
   711→}
   712→```
   713→
   714→**Error responses:**
   715→- `404` - Checkout not found
   716→- `410` - Checkout expired
   717→- `422` - Invalid shipping method (not available for this address/checkout)
   718→
   719→---
   720→
   721→#### PUT /api/storefront/v1/checkouts/{checkoutId}/payment-method
   722→
   723→Select a payment method for the checkout. This step records the chosen method without processing payment.
   724→
   725→- **Authentication:** None
   726→- **Rate limit:** `checkout` (10/min per session)
   727→
   728→**Request body:**
   729→
   730→```json
   731→{
   732→  "payment_method": "credit_card"
   733→}
   734→```
   735→
   736→| Field | Type | Required | Validation |
   737→|-------|------|----------|------------|
   738→| `payment_method` | string | Yes | Must be one of: `credit_card`, `paypal`, `bank_transfer`. |
   739→
   740→**Success response:** `200 OK`
   741→
   742→Returns the full checkout object. Status transitions to `payment_selected`.
   743→
   744→```json
   745→{
   746→  "id": 7,
   747→  "status": "payment_selected",
   748→  "payment_method": "credit_card",
   749→  "totals": {
   750→    "subtotal": 5000,
   751→    "discount": 0,
   752→    "shipping": 500,
   753→    "tax": 1045,
   754→    "total": 6545,
   755→    "currency": "EUR"
   756→  }
   757→}
   758→```
   759→
   760→**Error responses:**
   761→- `404` - Checkout not found
   762→- `410` - Checkout expired
   763→- `422` - Invalid payment method or checkout not in `shipping_selected` state
   764→
   765→---
   766→
   767→#### POST /api/storefront/v1/checkouts/{checkoutId}/apply-discount
   768→
   769→Apply a discount code to the checkout.
   770→
   771→- **Authentication:** None
   772→- **Rate limit:** `checkout` (10/min per session)
   773→
   774→**Request body:**
   775→
   776→```json
   777→{
   778→  "code": "WELCOME10"
   779→}
   780→```
   781→
   782→| Field | Type | Required | Validation |
   783→|-------|------|----------|------------|
   784→| `code` | string | Yes | Max: 50. Must be a valid, active discount code for this store. Case-insensitivity determined by store setting. |
   785→
   786→**Success response:** `200 OK`
   787→
   788→```json
   789→{
   790→  "id": 7,
   791→  "status": "shipping_selected",
   792→  "discount_code": "WELCOME10",
   793→  "totals": {
   794→    "subtotal": 5000,
   795→    "discount": 500,
   796→    "shipping": 500,
   797→    "tax": 950,
   798→    "total": 5950,
   799→    "currency": "EUR"
   800→  },
   801→  "applied_discounts": [
   802→    {
   803→      "code": "WELCOME10",
   804→      "type": "percent",
   805→      "value_amount": 10,
   806→      "applied_amount": 500,
   807→      "description": "10% off your first order"
   808→    }
   809→  ]
   810→}
   811→```
   812→
   813→**Error responses:**
   814→- `400` - Discount code expired (error code: `discount_expired`)
   815→- `400` - Discount usage limit reached (error code: `discount_usage_exceeded`)
   816→- `404` - Checkout not found
   817→- `410` - Checkout expired
   818→- `422` - Invalid code (not found, does not apply to these items)
   819→
   820→```json
   821→{
   822→  "message": "This discount code has expired.",
   823→  "error_code": "discount_expired"
   824→}
   825→```
   826→
   827→---
   828→
   829→#### DELETE /api/storefront/v1/checkouts/{checkoutId}/discount
   830→
   831→Remove the applied discount code from the checkout.
   832→
   833→- **Authentication:** None
   834→- **Rate limit:** `checkout` (10/min per session)
   835→
   836→**Success response:** `200 OK`
   837→
   838→Returns the full checkout object with discount removed and totals recalculated.
   839→
   840→**Error responses:**
   841→- `404` - Checkout not found or no discount applied
   842→- `410` - Checkout expired
   843→
   844→---
   845→
   846→### 2.3 Payment Endpoint
   847→
   848→#### POST /api/storefront/v1/checkouts/{checkoutId}/pay
   849→
   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.
   851→
   852→- **Authentication:** None
   853→- **Rate limit:** `checkout` (10/min per session)
   854→
   855→**Request body (Credit Card):**
   856→
   857→```json
   858→{
   859→  "payment_method": "credit_card",
   860→  "card_number": "4242424242424242",
   861→  "card_expiry": "12/28",
   862→  "card_cvc": "123",
   863→  "card_holder": "Jane Doe"
   864→}
   865→```
   866→
   867→**Request body (PayPal):**
   868→
   869→```json
   870→{
   871→  "payment_method": "paypal"
   872→}
   873→```
   874→
   875→**Request body (Bank Transfer):**
   876→
   877→```json
   878→{
   879→  "payment_method": "bank_transfer"
   880→}
   881→```
   882→
   883→| Field | Type | Required | Validation |
   884→|-------|------|----------|------------|
   885→| `payment_method` | string | Yes | Must be one of: `credit_card`, `paypal`, `bank_transfer`. |
   886→| `card_number` | string | Conditional | Required when `payment_method = credit_card`. Digits only (spaces stripped). |
   887→| `card_expiry` | string | Conditional | Required when `payment_method = credit_card`. Format: MM/YY. |
   888→| `card_cvc` | string | Conditional | Required when `payment_method = credit_card`. 3-4 digits. |
   889→| `card_holder` | string | Conditional | Required when `payment_method = credit_card`. Max 255 chars. |
   890→
   891→**Success response (Credit Card / PayPal - instant capture):** `200 OK`
   892→
   893→```json
   894→{
   895→  "checkout_id": 7,
   896→  "status": "completed",
   897→  "order": {
   898→    "id": 42,
   899→    "order_number": "#1016",
   900→    "status": "paid",
   901→    "financial_status": "paid",
   902→    "payment_method": "credit_card",
   903→    "total_amount": 6545,
   904→    "currency": "EUR"
   905→  }
   906→}
   907→```
   908→
   909→**Success response (Bank Transfer - deferred payment):** `200 OK`
   910→
   911→```json
   912→{
   913→  "checkout_id": 7,
   914→  "status": "completed",
   915→  "order": {
   916→    "id": 42,
   917→    "order_number": "#1016",
   918→    "status": "pending",
   919→    "financial_status": "pending",
   920→    "payment_method": "bank_transfer",
   921→    "total_amount": 6545,
   922→    "currency": "EUR"
   923→  },
   924→  "bank_transfer_instructions": {
   925→    "bank_name": "Mock Bank AG",
   926→    "iban": "DE89 3704 0044 0532 0130 00",
   927→    "bic": "COBADEFFXXX",
   928→    "reference": "#1016",
   929→    "amount_formatted": "65.45 EUR"
   930→  }
   931→}
   932→```
   933→
   934→**Error responses:**
   935→- `404` - Checkout not found
   936→- `409` - Checkout not in a valid state for payment (must be `payment_selected`)
   937→- `422` - Validation errors (missing card fields, invalid payment method)
   938→- `422` - Payment declined (credit card magic numbers)
   939→
   940→```json
   941→{
   942→  "message": "Your card was declined.",
   943→  "error_code": "card_declined"
   944→}
   945→```
   946→
   947→**Magic card number error codes:**
   948→
   949→| Card Number | Error Code | Message |
   950→|---|---|---|
   951→| `4000000000000002` | `card_declined` | "Your card was declined." |
   952→| `4000000000009995` | `insufficient_funds` | "Your card has insufficient funds." |
   953→
   954→---
   955→
   956→### 2.4 Order Endpoints
   957→
   958→#### GET /api/storefront/v1/orders/{orderNumber}
   959→
   960→Retrieve order status. Access is controlled via a signed token (included in confirmation emails and the confirmation page URL).
   961→
   962→- **Authentication:** Tokenized access (`token` query parameter with HMAC signature)
   963→- **Rate limit:** `api.storefront` (120/min per IP)
   964→
   965→**Query parameters:**
   966→
   967→| Parameter | Type | Required | Description |
   968→|-----------|------|----------|-------------|
   969→| `token` | string | Yes | HMAC-signed access token for this order |
   970→
   971→**Success response:** `200 OK`
   972→
   973→```json
   974→{
   975→  "order_number": "#1001",
   976→  "status": "paid",
   977→  "financial_status": "paid",
   978→  "fulfillment_status": "unfulfilled",
   979→  "email": "customer@example.com",
   980→  "currency": "EUR",
   981→  "placed_at": "2026-02-11T10:10:00Z",
   982→  "lines": [
   983→    {
   984→      "title_snapshot": "Classic T-Shirt",
   985→      "variant_title": "Blue / Medium",
   986→      "sku_snapshot": "TSH-BLU-M",
   987→      "quantity": 2,
   988→      "unit_price_amount": 2500,
   989→      "total_amount": 5000
   990→    }
   991→  ],
   992→  "totals": {
   993→    "subtotal_amount": 5000,
   994→    "discount_amount": 500,
   995→    "shipping_amount": 500,
   996→    "tax_amount": 950,
   997→    "total_amount": 5950
   998→  },
   999→  "shipping_address": {

prompt: Read the file /Users/fabianwesner/Herd/shop/specs/02-API-ROUTES.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL API routes including: method, URL, middleware, controller/handler, request/response formats, and validation rules. I need every detail for implementation.


prompt: Read the file /Users/fabianwesner/Herd/shop/specs/05-BUSINESS-LOGIC.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL business logic rules including: services, state machines, validation rules, pricing calculations, inventory management, payment processing, and fulfillment rules. I need every detail for implementation.


content:
  1000→### 10.5 Payment State Machine
  1001→
  1002→**Enum:** `App\Enums\PaymentStatus` - values: `pending`, `captured`, `failed`, `refunded`
  1003→
  1004→```mermaid
  1005→stateDiagram-v2
  1006→    [*] --> captured : Credit card / PayPal (success)
  1007→    [*] --> failed : Credit card (decline)
  1008→    [*] --> pending : Bank transfer (awaiting payment)
  1009→    pending --> captured : Admin confirms payment received
  1010→    pending --> failed : Auto-cancel job (unpaid timeout)
  1011→    captured --> refunded : Full refund processed
  1012→```
  1013→
  1014→### 10.6 Bank Transfer Instructions
  1015→
  1016→When `payment_method = bank_transfer`, the order confirmation page and email display mock bank transfer details:
  1017→
  1018→| Field | Value |
  1019→|---|---|
  1020→| Bank name | Mock Bank AG |
  1021→| IBAN | DE89 3704 0044 0532 0130 00 |
  1022→| BIC | COBADEFFXXX |
  1023→| Reference | Order number (e.g., #1001) |
  1024→| Amount | Order total formatted in EUR |
  1025→
  1026→These are static mock values. No real bank interaction occurs.
  1027→
  1028→### 10.7 Admin: Confirm Payment (Bank Transfer)
  1029→
  1030→Admin users can confirm that a bank transfer payment has been received via a "Confirm Payment" button on the order detail page (visible only when `payment_method = bank_transfer` AND `financial_status = pending`).
  1031→
  1032→**Action flow:**
  1033→1. Admin clicks "Confirm Payment" button.
  1034→2. Backend updates `payments.status` from `pending` to `captured`.
  1035→3. Backend updates `orders.financial_status` from `pending` to `paid`.
  1036→4. Backend updates `orders.status` from `pending` to `paid`.
  1037→5. Backend calls `InventoryService.commit()` for each order line (converts reserved stock to committed).
  1038→6. If all order lines are digital (`requires_shipping = false`): auto-create fulfillment with `status = delivered` (see Section 11.7).
  1039→7. Dispatch `OrderPaid` event.
  1040→
  1041→### 10.8 Auto-Cancel Unpaid Bank Transfer Orders
  1042→
  1043→**Job:** `App\Jobs\CancelUnpaidBankTransferOrders`
  1044→
  1045→- Schedule: daily
  1046→- Finds orders where `payment_method = bank_transfer` AND `financial_status = pending` AND `placed_at < (now - configurable days)`.
  1047→- Configurable timeout: `store_settings.settings_json` key `bank_transfer_cancel_days`, default = 7.
  1048→
  1049→**Cancel action:**
  1050→1. Release reserved inventory via `InventoryService.release()`.
  1051→2. Update `orders.financial_status` to `voided`.
  1052→3. Update `orders.status` to `cancelled`.
  1053→4. Update `payments.status` to `failed`.
  1054→5. Dispatch `OrderCancelled` event.
  1055→
  1056→---
  1057→
  1058→## 11. Order Management
  1059→
  1060→### 11.1 Order Creation (from Completed Checkout)
  1061→
  1062→See Section 6.2 "Transition: payment_selected -> completed" for the full procedure. The order creation is the central action within the completeCheckout transaction.
  1063→
  1064→**Key requirements for the order record:**
  1065→- Snapshot all checkout data (addresses, email, totals) at time of order creation.
  1066→- Order lines must contain snapshot data (`title_snapshot`, `sku_snapshot`) so that order history survives product changes.
  1067→- The mock payment response is encrypted before storage.
  1068→- The entire operation must be atomic (single database transaction).
  1069→- The `payment_method` from the checkout is copied to the order record.
  1070→
  1071→**Payment-method-specific order creation:**
  1072→
  1073→| Payment Method | `orders.status` | `orders.financial_status` | Inventory Action | Payment Record Status |
  1074→|---|---|---|---|---|
  1075→| `credit_card` | `paid` | `paid` | Commit (on_hand and reserved both decremented) | `captured` |
  1076→| `paypal` | `paid` | `paid` | Commit (on_hand and reserved both decremented) | `captured` |
  1077→| `bank_transfer` | `pending` | `pending` | Reserve only (reserved incremented, on_hand unchanged) | `pending` |
  1078→
  1079→### 11.2 Order Numbering
  1080→
  1081→- Sequential per store, starting at 1001.
  1082→- Format: configurable prefix (default: `#`) stored in `store_settings.settings_json` key `order_number_prefix`.
  1083→- Generation: within the order creation transaction, find the maximum existing order number for the store and increment by 1.
  1084→- Since this runs inside a SQLite transaction, there is no risk of duplicate numbers.
  1085→
  1086→### 11.3 Order Statuses
  1087→
  1088→Three independent status dimensions tracked on each order:
  1089→
  1090→**Order status** (`orders.status`):
  1091→
  1092→| Status | Description |
  1093→|---|---|
  1094→| `pending` | Order created, awaiting processing |
  1095→| `paid` | Payment confirmed |
  1096→| `fulfilled` | All items shipped |
  1097→| `cancelled` | Order cancelled before fulfillment |
  1098→| `refunded` | Fully refunded |
  1099→
  1100→**Financial status** (`orders.financial_status`):
  1101→
  1102→| Status | Description |
  1103→|---|---|
  1104→| `pending` | Payment not yet confirmed |
  1105→| `authorized` | Payment authorized but not captured |
  1106→| `paid` | Payment captured successfully |
  1107→| `partially_refunded` | Some items or partial amount refunded |
  1108→| `refunded` | Fully refunded |
  1109→| `voided` | Authorization voided (no capture occurred) |
  1110→
  1111→**Fulfillment status** (`orders.fulfillment_status`):
  1112→
  1113→| Status | Description |
  1114→|---|---|
  1115→| `unfulfilled` | No items have been shipped |
  1116→| `partial` | Some items shipped, others pending |
  1117→| `fulfilled` | All items shipped |
  1118→
  1119→### 11.4 Refund Processing
  1120→
  1121→**Service:** `App\Services\RefundService`
  1122→
  1123→**Input (RefundRequest):**
  1124→
  1125→| Field | Type | Required | Description |
  1126→|---|---|---|---|
  1127→| `amount` | integer (cents) | No | Custom refund amount. If not set, calculated from lines. |
  1128→| `lines` | map of order_line_id to quantity | No | Which lines and quantities to refund (for line-level refunds). |
  1129→| `reason` | string | No | Text reason for the refund. |
  1130→| `restock` | boolean | No | Whether to restock inventory for refunded items. |
  1131→
  1132→**Process:**
  1133→
  1134→```
  1135→PROCEDURE processRefund(order, request):
  1136→    1. Calculate remaining refundable amount:
  1137→       refundable = order.total_amount - SUM(existing refunds for this order)
  1138→
  1139→    2. IF request.amount is set:
  1140→           VALIDATE request.amount <= refundable
  1141→       ELSE IF request.lines is set:
  1142→           CALCULATE amount from specified lines and quantities
  1143→       ELSE:
  1144→           amount = refundable (full refund)
  1145→
  1146→    3. CALL PaymentProvider.refund(order's payment, amount)
  1147→
  1148→    4. CREATE Refund record with status = 'pending'
  1149→
  1150→    5. On provider confirmation:
  1151→       UPDATE refund status = 'processed'
  1152→
  1153→    6. Calculate total refunded for this order:
  1154→       IF total refunded = order.total_amount:
  1155→           SET order.financial_status = 'refunded'
  1156→           SET order.status = 'refunded'
  1157→       ELSE:
  1158→           SET order.financial_status = 'partially_refunded'
  1159→
  1160→    7. IF request.restock = true:
  1161→       FOR EACH refunded line and quantity:
  1162→           CALL InventoryService.restock(variant, quantity)
  1163→
  1164→    8. DISPATCH OrderRefunded event
  1165→```
  1166→
  1167→### 11.5 Fulfillment
  1168→
  1169→**Service:** `App\Services\FulfillmentService`
  1170→
  1171→#### Fulfillment Guard
  1172→
  1173→**CRITICAL:** A fulfillment cannot be created unless the order's `financial_status` is `paid` or `partially_refunded`. This prevents shipping goods before payment is confirmed.
  1174→
  1175→| `financial_status` | Fulfillment Allowed? | Reason |
  1176→|---|---|---|
  1177→| `pending` | **No** | Payment not yet received (e.g., bank transfer awaiting confirmation) |
  1178→| `authorized` | **No** | Payment authorized but not captured |
  1179→| `paid` | **Yes** | Payment confirmed |
  1180→| `partially_refunded` | **Yes** | Payment confirmed, partial refund does not block shipment |
  1181→| `refunded` | **No** | Fully refunded, nothing to ship |
  1182→| `voided` | **No** | Payment voided / cancelled |
  1183→
  1184→When fulfillment is blocked by the guard, the admin UI shows an info callout: "Fulfillment cannot be created until payment is confirmed." The "Create fulfillment" button is disabled.
  1185→
  1186→#### Creating a Fulfillment
  1187→
  1188→Input:
  1189→- `lines` - map of order_line_id to quantity, specifying which items are in this shipment
  1190→- `trackingData` - tracking_company, tracking_number, tracking_url
  1191→
  1192→**Process:**
  1193→
  1194→```
  1195→PROCEDURE createFulfillment(order, lines, trackingData):
  1196→    1. GUARD: Verify order.financial_status IN ('paid', 'partially_refunded')
  1197→       IF NOT: throw FulfillmentGuardException
  1198→
  1199→    2. FOR EACH line in request:
  1200→       fulfilledSoFar = SUM(fulfillment_lines quantities for this order_line)
  1201→       unfulfilled = order_line.quantity - fulfilledSoFar
  1202→       VALIDATE requested quantity <= unfulfilled
  1203→
  1204→    3. CREATE Fulfillment record with status = 'pending' and tracking data
  1205→
  1206→    4. CREATE fulfillment_lines for each order line and quantity
  1207→
  1208→    5. Determine new fulfillment status:
  1209→       FOR EACH order_line in order:
  1210→           totalFulfilled = SUM(all fulfillment_lines for this order_line)
  1211→           IF totalFulfilled < order_line.quantity:
  1212→               allFulfilled = false
  1213→
  1214→       IF allFulfilled:
  1215→           SET order.fulfillment_status = 'fulfilled'
  1216→           SET order.status = 'fulfilled'
  1217→       ELSE:
  1218→           SET order.fulfillment_status = 'partial'
  1219→
  1220→    6. DISPATCH FulfillmentCreated event
  1221→```
  1222→
  1223→#### Fulfillment Status Transitions (Admin Button Actions)
  1224→
  1225→Fulfillment status transitions are triggered by explicit admin button clicks. There are no automatic transitions for physical products.
  1226→
  1227→```mermaid
  1228→stateDiagram-v2
  1229→    [*] --> pending : Fulfillment created (admin clicks "Create fulfillment")
  1230→    pending --> shipped : Admin clicks "Mark as shipped"
  1231→    shipped --> delivered : Admin clicks "Mark as delivered"
  1232→
  1233→    note right of pending : Fulfillment record created with tracking data
  1234→    note right of shipped : Sets shipped_at timestamp, sends tracking email to customer
  1235→    note right of delivered : Sets delivered_at timestamp
  1236→```
  1237→
  1238→**Admin buttons on each fulfillment card:**
  1239→
  1240→| Button | Shown When | Action |
  1241→|---|---|---|
  1242→| "Mark as shipped" | `fulfillment.status = pending` | Transitions to `shipped`, sets `shipped_at`, dispatches `FulfillmentShipped` event, sends tracking email |
  1243→| "Mark as delivered" | `fulfillment.status = shipped` | Transitions to `delivered`, dispatches `FulfillmentDelivered` event |
  1244→
  1245→When transitioning to `shipped`:
  1246→- Set `shipped_at` timestamp.
  1247→- Dispatch `FulfillmentShipped` event.
  1248→- Send email notification to customer with tracking information.
  1249→
  1250→When transitioning to `delivered`:
  1251→- Dispatch `FulfillmentDelivered` event.
  1252→
  1253→### 11.6 Fulfillment Guard - Inventory Behavior per Payment Method
  1254→
  1255→The fulfillment guard interacts with inventory differently depending on the payment method:
  1256→
  1257→| Payment Method | At Order Creation | At Payment Confirmation | At Fulfillment |
  1258→|---|---|---|---|
  1259→| `credit_card` / `paypal` | Inventory committed (on_hand decremented, reserved decremented) | N/A (already committed) | No inventory change |
  1260→| `bank_transfer` | Inventory reserved only (reserved incremented) | Inventory committed (on_hand decremented, reserved decremented) | No inventory change |
  1261→
  1262→For bank transfer orders, inventory is held in a reserved state until the admin confirms payment. If the order is auto-cancelled (see Section 10.8), the reserved inventory is released back.
  1263→
  1264→### 11.7 Auto-Fulfillment for Digital Products
  1265→
  1266→When a payment is confirmed (either instant for credit_card/paypal, or via admin confirmation for bank_transfer):
  1267→
  1268→1. Check if **all** order lines reference variants with `requires_shipping = false` (digital products).
  1269→2. If yes: automatically create a fulfillment record covering all order lines:
  1270→   - `status = 'delivered'` (skip pending and shipped)
  1271→   - `tracking_company = null`, `tracking_number = null`, `tracking_url = null`
  1272→   - `shipped_at = now`
  1273→   - Set `order.fulfillment_status = 'fulfilled'`
  1274→   - Set `order.status = 'fulfilled'`
  1275→3. If the order contains a mix of physical and digital products: do **not** auto-fulfill. The admin must create fulfillments manually (they can fulfill digital items immediately and physical items when shipped).
  1276→
  1277→This ensures digital product orders are immediately marked as complete after payment.
  1278→
  1279→### 11.6 Order Export
  1280→
  1281→`GET /api/admin/v1/orders/export` returns a CSV file of orders matching the current filter criteria.
  1282→
  1283→**CSV columns:** `order_number`, `created_at`, `status`, `financial_status`, `fulfillment_status`, `customer_email`, `customer_name`, `subtotal_amount`, `discount_amount`, `shipping_amount`, `tax_amount`, `total_amount`, `currency`, `shipping_method`, `tracking_number`
  1284→
  1285→All monetary values are exported in minor units (cents). Dates use ISO-8601 format. The response uses `Content-Type: text/csv` with `Content-Disposition: attachment; filename="orders-{date}.csv"`.
  1286→
  1287→---
  1288→
  1289→## 12. Customer Accounts
  1290→
  1291→### 12.1 Registration
  1292→
  1293→**Service:** `App\Services\CustomerService`
  1294→
  1295→**Required fields:**
  1296→
  1297→| Field | Validation |
  1298→|---|---|
  1299→| `name` | Required, string |
  1300→| `email` | Required, valid email, unique per store |
  1301→| `password` | Required, minimum 8 characters |
  1302→| `marketing_opt_in` | Optional, boolean, default false |
  1303→
  1304→- Email uniqueness enforced by the database unique index on `(store_id, email)`.
  1305→- Password hashed using Laravel's Hash facade (bcrypt by default).
  1306→
  1307→### 12.2 Authentication
  1308→
  1309→**Separate auth guard:** `customer`
  1310→
  1311→Requirements:
  1312→- Configure a dedicated `customer` guard in auth config, using session driver.
  1313→- Configure a dedicated `customers` provider using the Customer model.
  1314→- Login: email + password, scoped to the current store.
  1315→- Password reset: standard Laravel password reset flow adapted for the customer guard and broker.
  1316→
  1317→**Store isolation:** Customer accounts are per-store. The same email address can exist in different stores as separate accounts. Authentication always scopes the query to the current store.
  1318→
  1319→### 12.3 Data Access
  1320→
  1321→- Customers can only view their own orders (scoped by customer_id match).
  1322→- Customers can only manage their own addresses (scoped by customer_id match).
  1323→- All customer queries include the StoreScope global scope for tenant isolation.
  1324→
  1325→### 12.4 Guest Cart to Customer Linking
  1326→
  1327→```
  1328→PROCEDURE linkGuestToCustomer(checkoutEmail, storeId, order):
  1329→    existingCustomer = find customer by email in store
  1330→    IF existingCustomer:
  1331→        LINK order to existingCustomer
  1332→    ELSE:
  1333→        CREATE new customer with:
  1334→            - email = checkoutEmail
  1335→            - password_hash = null (guest customer)
  1336→            - store_id = storeId
  1337→        LINK order to new customer
  1338→
  1339→    // Guest customer can later "claim" their account
  1340→    // by setting a password via registration or account-claim flow
  1341→```
  1342→
  1343→---
  1344→
  1345→## 13. Webhook Delivery System
  1346→
  1347→### 13.1 Subscription Model
  1348→
  1349→Stores (or app installations) subscribe to event types via `webhook_subscriptions`.
  1350→
  1351→**Supported event types:**
  1352→
  1353→| Event Type | Triggered By |
  1354→|---|---|
  1355→| `order.created` | OrderCreated event |
  1356→| `order.paid` | Payment captured successfully |
  1357→| `order.fulfilled` | All order lines fulfilled |
  1358→| `order.refunded` | Refund processed |
  1359→| `product.created` | New product saved |
  1360→| `product.updated` | Product modified |
  1361→| `product.deleted` | Product deleted or archived |
  1362→| `checkout.completed` | Checkout successfully completed |
  1363→
  1364→### 13.2 Delivery Process
  1365→
  1366→**Listener:** `App\Listeners\DispatchWebhooks` - registered to listen for all domain events that have webhook counterparts.
  1367→
  1368→**Job:** `App\Jobs\DeliverWebhook`
  1369→
  1370→```mermaid
  1371→sequenceDiagram
  1372→    participant DomainEvent
  1373→    participant Listener
  1374→    participant Queue
  1375→    participant Job
  1376→    participant TargetURL
  1377→
  1378→    DomainEvent->>Listener: Event fired (e.g., OrderCreated)
  1379→    Listener->>Listener: Query webhook_subscriptions for store_id + event_type where status = active
  1380→    loop For each matching subscription
  1381→        Listener->>Queue: Dispatch DeliverWebhook job
  1382→    end
  1383→    Queue->>Job: Process job
  1384→    Job->>Job: Serialize event payload as JSON
  1385→    Job->>Job: Generate HMAC-SHA256 signature using subscription's signing_secret (decrypted)
  1386→    Job->>TargetURL: HTTP POST with headers and payload
  1387→    TargetURL-->>Job: Response
  1388→    Job->>Job: Record result in webhook_deliveries
  1389→```
  1390→
  1391→**Required HTTP headers on delivery:**
  1392→
  1393→| Header | Value |
  1394→|---|---|
  1395→| `X-Platform-Signature` | HMAC-SHA256 hex digest of the payload |
  1396→| `X-Platform-Event` | Event type string (e.g., `order.created`) |
  1397→| `X-Platform-Delivery-Id` | Unique UUID for this delivery attempt |
  1398→| `X-Platform-Timestamp` | Unix timestamp of the event |
  1399→| `Content-Type` | `application/json` |
  1400→
  1401→### 13.3 Retry Logic
  1402→
  1403→On non-2xx response or connection timeout:
  1404→
  1405→| Attempt | Backoff Delay |
  1406→|---|---|
  1407→| 1 (initial) | Immediate |
  1408→| 2 | 1 minute |
  1409→| 3 | 5 minutes |
  1410→| 4 | 30 minutes |
  1411→| 5 | 2 hours |
  1412→| 6 (final) | 12 hours |
  1413→
  1414→After 5 retry attempts (6 total), mark the delivery as `failed` (dead letter).
  1415→
  1416→Use Laravel's built-in job retry mechanism with the backoff delays configured as: 60, 300, 1800, 7200, 43200 seconds.
  1417→
  1418→### 13.4 Circuit Breaker
  1419→
  1420→- Track consecutive failures per subscription (across different events).
  1421→- If a subscription's endpoint fails 5 consecutive times: automatically set `webhook_subscriptions.status = 'paused'`.
  1422→- Log a warning and (future) send an admin notification.
  1423→- Manual re-enable required by the merchant through the admin UI.
  1424→- On each successful delivery, reset the consecutive failure counter to 0.
  1425→
  1426→**Service:** `App\Services\WebhookService`
  1427→
  1428→---
  1429→
  1430→## 14. Analytics
  1431→
  1432→### 14.1 Event Ingestion
  1433→
  1434→**Endpoint:** `POST /api/storefront/v1/analytics/events`
  1435→
  1436→**Request structure:**
  1437→
  1438→| Field | Type | Required | Description |
  1439→|---|---|---|---|
  1440→Events are submitted in batches:
  1441→
  1442→```json
  1443→{
  1444→  "events": [
  1445→    {
  1446→      "type": "page_view",
  1447→      "session_id": "...",
  1448→      "client_event_id": "...",
  1449→      "occurred_at": "2025-01-15T10:30:00Z",
  1450→      "properties": { ... }
  1451→    }
  1452→  ]
  1453→}
  1454→```
  1455→
  1456→**Event fields:**
  1457→
  1458→| Field | Type | Required | Description |
  1459→|---|---|---|---|
  1460→| `type` | string | Yes | One of: `page_view`, `product_view`, `add_to_cart`, `checkout_started`, `checkout_completed` |
  1461→| `session_id` | string | Yes | Client session identifier |
  1462→| `client_event_id` | string | Yes | Client-generated unique ID for deduplication |
  1463→| `occurred_at` | string (ISO-8601) | Yes | When the event occurred on the client |
  1464→| `properties` | object | No | Event-specific data (product_id, variant_id, etc.) |
  1465→
  1466→**Deduplication:** Events are deduplicated by `client_event_id` within the store scope. If a `client_event_id` has already been recorded for the current store (enforced by the unique index on `store_id, client_event_id`), the duplicate event is silently dropped.
  1467→
  1468→**Rate limiting:** 60 requests per minute per IP address, using Laravel's throttle middleware.
  1469→
  1470→### 14.2 Aggregation
  1471→
  1472→**Job:** `App\Jobs\AggregateAnalytics`
  1473→
  1474→- Schedule: daily at 01:00 UTC
  1475→- Aggregates the previous day's raw `analytics_events` into `analytics_daily`.
  1476→
  1477→**Aggregated metrics:**
  1478→
  1479→| Metric | Calculation |
  1480→|---|---|
  1481→| `orders_count` | Count of `checkout_completed` events for the day |
  1482→| `revenue_amount` | Sum of order totals from `checkout_completed` event properties |
  1483→| `aov_amount` | `revenue_amount / orders_count` (integer division, 0 if no orders) |
  1484→| `visits_count` | Count of distinct `session_id` values with `page_view` events |
  1485→| `add_to_cart_count` | Count of `add_to_cart` events |
  1486→| `checkout_started_count` | Count of `checkout_started` events |
  1487→
  1488→---
  1489→
  1490→## 15. Media Processing
  1491→
  1492→### 15.1 Upload Flow
  1493→
  1494→```mermaid
  1495→flowchart TD
  1496→    A[Admin uploads file via Livewire] --> B[Store original in storage/app/public/media/originals/]
  1497→    B --> C[Create product_media record with status = processing]
  1498→    C --> D[Dispatch ProcessMediaUpload job to queue]
  1499→    D --> E{Processing succeeds?}

prompt: Read the file /Users/fabianwesner/Herd/shop/specs/05-BUSINESS-LOGIC.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL business logic rules including: services, state machines, validation rules, pricing calculations, inventory management, payment processing, and fulfillment rules. I need every detail for implementation.


prompt: Read the file /Users/fabianwesner/Herd/shop/specs/02-API-ROUTES.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL API routes including: method, URL, middleware, controller/handler, request/response formats, and validation rules. I need every detail for implementation.