content:
1000→    "first_name": "Jane",
  1001→    "last_name": "Doe",
  1002→    "address1": "123 Main St",
  1003→    "city": "Berlin",
  1004→    "country": "DE",
  1005→    "postal_code": "10115"
  1006→  },
  1007→  "fulfillments": []
  1008→}
  1009→```
  1010→
  1011→**Error responses:**
  1012→- `401` - Invalid or missing token
  1013→- `404` - Order not found
  1014→
  1015→---
  1016→
  1017→### 2.5 Search Endpoints
  1018→
  1019→#### GET /api/storefront/v1/search
  1020→
  1021→Search products within the current store.
  1022→
  1023→- **Authentication:** None
  1024→- **Rate limit:** `search` (30/min per IP)
  1025→
  1026→**Query parameters:**
  1027→
  1028→| Parameter | Type | Required | Description |
  1029→|-----------|------|----------|-------------|
  1030→| `q` | string | Yes | Search query. Min: 1, max: 200 characters. |
  1031→| `filters` | json | No | URL-encoded JSON object with filter facets. |
  1032→| `sort` | string | No | One of: `relevance`, `price_asc`, `price_desc`, `newest`, `best_selling`. Default: `relevance`. |
  1033→| `page` | integer | No | Page number. Default: 1. |
  1034→| `per_page` | integer | No | Results per page. Min: 1, max: 50. Default: 24. |
  1035→
  1036→**Filters JSON schema:**
  1037→
  1038→```json
  1039→{
  1040→  "collection_id": 5,
  1041→  "price_min": 1000,
  1042→  "price_max": 5000,
  1043→  "in_stock": true,
  1044→  "tags": ["organic", "cotton"],
  1045→  "vendor": "Acme Apparel"
  1046→}
  1047→```
  1048→
  1049→**Success response:** `200 OK`
  1050→
  1051→```json
  1052→{
  1053→  "query": "t-shirt",
  1054→  "results": [
  1055→    {
  1056→      "id": 10,
  1057→      "title": "Classic T-Shirt",
  1058→      "handle": "classic-t-shirt",
  1059→      "vendor": "Acme Apparel",
  1060→      "product_type": "Apparel",
  1061→      "price_amount": 2500,
  1062→      "compare_at_amount": 3500,
  1063→      "currency": "EUR",
  1064→      "image_url": "https://cdn.example.com/products/tshirt.jpg",
  1065→      "in_stock": true,
  1066→      "tags": ["organic", "cotton"]
  1067→    }
  1068→  ],
  1069→  "facets": {
  1070→    "vendors": [
  1071→      { "value": "Acme Apparel", "count": 12 }
  1072→    ],
  1073→    "tags": [
  1074→      { "value": "organic", "count": 8 },
  1075→      { "value": "cotton", "count": 15 }
  1076→    ],
  1077→    "price_range": {
  1078→      "min": 1500,
  1079→      "max": 8500
  1080→    }
  1081→  },
  1082→  "pagination": {
  1083→    "current_page": 1,
  1084→    "per_page": 24,
  1085→    "total": 42,
  1086→    "last_page": 2
  1087→  }
  1088→}
  1089→```
  1090→
  1091→**Error responses:**
  1092→- `400` - Query too long (max 200 characters) or malformed filters
  1093→- `422` - Validation error
  1094→
  1095→---
  1096→
  1097→#### GET /api/storefront/v1/search/suggest
  1098→
  1099→Autocomplete suggestions as the user types.
  1100→
  1101→- **Authentication:** None
  1102→- **Rate limit:** `search` (30/min per IP)
  1103→
  1104→**Query parameters:**
  1105→
  1106→| Parameter | Type | Required | Description |
  1107→|-----------|------|----------|-------------|
  1108→| `q` | string | Yes | Search prefix. Min: 1, max: 100 characters. |
  1109→| `limit` | integer | No | Max suggestions. Default: 5, max: 10. |
  1110→
  1111→**Success response:** `200 OK`
  1112→
  1113→```json
  1114→{
  1115→  "query": "t-sh",
  1116→  "suggestions": [
  1117→    {
  1118→      "type": "product",
  1119→      "title": "Classic T-Shirt",
  1120→      "handle": "classic-t-shirt",
  1121→      "image_url": "https://cdn.example.com/products/tshirt-thumb.jpg",
  1122→      "price_amount": 2500,
  1123→      "currency": "EUR"
  1124→    },
  1125→    {
  1126→      "type": "collection",
  1127→      "title": "T-Shirts",
  1128→      "handle": "t-shirts",
  1129→      "image_url": null
  1130→    }
  1131→  ]
  1132→}
  1133→```
  1134→
  1135→**Error responses:**
  1136→- `400` - Query too long
  1137→- `422` - Validation error
  1138→
  1139→---
  1140→
  1141→### 2.6 Analytics Endpoints
  1142→
  1143→#### POST /api/storefront/v1/analytics/events
  1144→
  1145→Batch submit storefront analytics events.
  1146→
  1147→- **Authentication:** None
  1148→- **Rate limit:** `analytics` (60/min per IP)
  1149→
  1150→**Request body:**
  1151→
  1152→```json
  1153→{
  1154→  "events": [
  1155→    {
  1156→      "type": "page_view",
  1157→      "session_id": "sess_abc123",
  1158→      "client_event_id": "evt_001",
  1159→      "properties": {
  1160→        "url": "/products/classic-t-shirt",
  1161→        "referrer": "https://google.com",
  1162→        "user_agent": "Mozilla/5.0..."
  1163→      },
  1164→      "occurred_at": "2026-02-11T10:00:00Z"
  1165→    },
  1166→    {
  1167→      "type": "add_to_cart",
  1168→      "session_id": "sess_abc123",
  1169→      "client_event_id": "evt_002",
  1170→      "properties": {
  1171→        "product_id": 10,
  1172→        "variant_id": 101,
  1173→        "quantity": 1,
  1174→        "price_amount": 2500,
  1175→        "currency": "EUR"
  1176→      },
  1177→      "occurred_at": "2026-02-11T10:01:00Z"
  1178→    }
  1179→  ]
  1180→}
  1181→```
  1182→
  1183→| Field | Type | Required | Validation |
  1184→|-------|------|----------|------------|
  1185→| `events` | array | Yes | Min: 1, max: 50 events per batch. |
  1186→| `events[].type` | string | Yes | One of: `page_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. |
  1187→| `events[].session_id` | string | Yes | Max: 100 characters. |
  1188→| `events[].client_event_id` | string | Yes | Max: 100 characters. If a `client_event_id` has already been recorded for the current store, the duplicate event is silently dropped (unique on `(store_id, client_event_id)`). |
  1189→| `events[].properties` | object | No | Freeform JSON. Max depth: 3 levels. |
  1190→| `events[].occurred_at` | string (ISO 8601) | Yes | Must not be more than 1 hour in the past or in the future. |
  1191→
  1192→**Success response:** `202 Accepted`
  1193→
  1194→```json
  1195→{
  1196→  "accepted": 2,
  1197→  "rejected": 0
  1198→}
  1199→```
  1200→
  1201→**Error responses:**
  1202→- `422` - Validation errors (invalid event types, too many events, etc.)
  1203→
  1204→---
  1205→
  1206→## 3. Admin REST API
  1207→
  1208→**Prefix:** `/api/admin/v1`
  1209→**Middleware:** `auth:sanctum`, `store.resolve`, `throttle:api.admin`
  1210→**Content-Type:** `application/json`
  1211→
  1212→These endpoints provide programmatic access for integrations, CLI tools, and third-party apps. The admin web UI uses Livewire actions directly and does not call these endpoints.
  1213→
  1214→All endpoints require a valid Sanctum API token with appropriate scopes. Responses follow Laravel's API Resource conventions.
  1215→
  1216→---
  1217→
  1218→### 3.1 Platform Management (Super-Admin Only)
  1219→
  1220→These endpoints are restricted to users with the `owner` (platform) role.
  1221→
  1222→#### POST /api/admin/v1/platform/organizations
  1223→
  1224→Create a new organization.
  1225→
  1226→- **Authentication:** Sanctum token with `manage-platform` scope
  1227→- **Rate limit:** `api.admin` (60/min per token)
  1228→
  1229→**Request body:**
  1230→
  1231→```json
  1232→{
  1233→  "name": "Acme Corp",
  1234→  "billing_email": "billing@acme.com"
  1235→}
  1236→```
  1237→
  1238→| Field | Type | Required | Validation |
  1239→|-------|------|----------|------------|
  1240→| `name` | string | Yes | Max: 255 |
  1241→| `billing_email` | string | Yes | Valid email, max: 255 |
  1242→
  1243→**Success response:** `201 Created`
  1244→
  1245→```json
  1246→{
  1247→  "data": {
  1248→    "id": 1,
  1249→    "name": "Acme Corp",
  1250→    "billing_email": "billing@acme.com",
  1251→    "created_at": "2026-02-11T10:00:00Z",
  1252→    "updated_at": "2026-02-11T10:00:00Z"
  1253→  }
  1254→}
  1255→```
  1256→
  1257→**Error responses:**
  1258→- `403` - Insufficient permissions
  1259→- `422` - Validation errors
  1260→
  1261→---
  1262→
  1263→#### POST /api/admin/v1/platform/stores
  1264→
  1265→Create a new store within an organization.
  1266→
  1267→- **Authentication:** Sanctum token with `manage-platform` scope
  1268→- **Rate limit:** `api.admin` (60/min per token)
  1269→
  1270→**Request body:**
  1271→
  1272→```json
  1273→{
  1274→  "organization_id": 1,
  1275→  "name": "Acme Store",
  1276→  "handle": "acme",
  1277→  "default_currency": "EUR",
  1278→  "default_locale": "en",
  1279→  "timezone": "Europe/Berlin"
  1280→}
  1281→```
  1282→
  1283→| Field | Type | Required | Validation |
  1284→|-------|------|----------|------------|
  1285→| `organization_id` | integer | Yes | Must exist |
  1286→| `name` | string | Yes | Max: 255 |
  1287→| `handle` | string | Yes | Unique, lowercase, alphanumeric with hyphens. Max: 63. Regex: `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$` |
  1288→| `default_currency` | string | Yes | Valid ISO 4217 code |
  1289→| `default_locale` | string | Yes | Valid locale code (e.g., `en`, `de`, `fr`) |
  1290→| `timezone` | string | Yes | Valid IANA timezone |
  1291→
  1292→**Success response:** `201 Created`
  1293→
  1294→```json
  1295→{
  1296→  "data": {
  1297→    "id": 1,
  1298→    "organization_id": 1,
  1299→    "name": "Acme Store",
  1300→    "handle": "acme",
  1301→    "status": "active",
  1302→    "default_currency": "EUR",
  1303→    "default_locale": "en",
  1304→    "timezone": "Europe/Berlin",
  1305→    "created_at": "2026-02-11T10:00:00Z"
  1306→  }
  1307→}
  1308→```
  1309→
  1310→**Error responses:**
  1311→- `403` - Insufficient permissions
  1312→- `422` - Validation errors (duplicate handle, invalid timezone, etc.)
  1313→
  1314→---
  1315→
  1316→#### POST /api/admin/v1/stores/{storeId}/invites
  1317→
  1318→Invite a user to a store with a specific role.
  1319→
  1320→- **Authentication:** Sanctum token with `manage-platform` scope
  1321→- **Rate limit:** `api.admin` (60/min per token)
  1322→
  1323→**Request body:**
  1324→
  1325→```json
  1326→{
  1327→  "email": "staff@acme.com",
  1328→  "role": "staff"
  1329→}
  1330→```
  1331→
  1332→| Field | Type | Required | Validation |
  1333→|-------|------|----------|------------|
  1334→| `email` | string | Yes | Valid email, max: 255 |
  1335→| `role` | string | Yes | One of: `owner`, `admin`, `staff`, `support` |
  1336→
  1337→**Success response:** `201 Created`
  1338→
  1339→```json
  1340→{
  1341→  "data": {
  1342→    "email": "staff@acme.com",
  1343→    "role": "staff",
  1344→    "invited_at": "2026-02-11T10:00:00Z",
  1345→    "expires_at": "2026-02-18T10:00:00Z"
  1346→  }
  1347→}
  1348→```
  1349→
  1350→**Error responses:**
  1351→- `403` - Insufficient permissions (only `owner` and `admin` can invite)
  1352→- `409` - User already a member of this store
  1353→- `422` - Validation errors
  1354→
  1355→---
  1356→
  1357→#### GET /api/admin/v1/stores/{storeId}/me
  1358→
  1359→Get the current user's membership details for a store.
  1360→
  1361→- **Authentication:** Sanctum token
  1362→- **Rate limit:** `api.admin` (60/min per token)
  1363→
  1364→**Success response:** `200 OK`
  1365→
  1366→```json
  1367→{
  1368→  "data": {
  1369→    "user_id": 3,
  1370→    "store_id": 1,
  1371→    "role": "admin",
  1372→    "email": "admin@acme.com",
  1373→    "name": "Jane Admin",
  1374→    "permissions": [
  1375→      "read-products",
  1376→      "write-products",
  1377→      "read-orders",
  1378→      "write-orders",
  1379→      "read-settings",
  1380→      "write-settings",
  1381→      "read-customers"
  1382→    ]
  1383→  }
  1384→}
  1385→```
  1386→
  1387→**Error responses:**
  1388→- `403` - Not a member of this store
  1389→- `404` - Store not found
  1390→
  1391→---
  1392→
  1393→### 3.2 Products
  1394→
  1395→#### Admin Products Endpoint Summary
  1396→
  1397→| Method | Path | Auth Scope | Description | Rate Limit |
  1398→|--------|------|------------|-------------|------------|
  1399→| GET | `/api/admin/v1/stores/{storeId}/products` | `read-products` | List products with filtering and pagination | `api.admin` (60/min) |
  1400→| POST | `/api/admin/v1/stores/{storeId}/products` | `write-products` | Create a new product | `api.admin` (60/min) |
  1401→| GET | `/api/admin/v1/stores/{storeId}/products/{productId}` | `read-products` | Get single product with all relations | `api.admin` (60/min) |
  1402→| PUT | `/api/admin/v1/stores/{storeId}/products/{productId}` | `write-products` | Update a product (partial updates supported) | `api.admin` (60/min) |
  1403→| DELETE | `/api/admin/v1/stores/{storeId}/products/{productId}` | `write-products` | Archive a product (soft delete) | `api.admin` (60/min) |
  1404→| POST | `/api/admin/v1/stores/{storeId}/products/{productId}/media/presign-upload` | `write-products` | Generate presigned URL for media upload | `api.admin` (60/min) |
  1405→
  1406→#### GET /api/admin/v1/stores/{storeId}/products
  1407→
  1408→List products with filtering and pagination.
  1409→
  1410→**Query parameters:**
  1411→
  1412→| Parameter | Type | Required | Description |
  1413→|-----------|------|----------|-------------|
  1414→| `status` | string | No | Filter by status: `draft`, `active`, `archived`. |
  1415→| `query` | string | No | Search by title, vendor, or SKU. |
  1416→| `collection_id` | integer | No | Filter by collection membership. |
  1417→| `page` | integer | No | Page number. Default: 1. |
  1418→| `per_page` | integer | No | Results per page. Default: 25, max: 100. |
  1419→| `sort` | string | No | One of: `title_asc`, `title_desc`, `created_at_asc`, `created_at_desc`, `updated_at_desc`. Default: `updated_at_desc`. |
  1420→
  1421→**Success response:** `200 OK`
  1422→
  1423→```json
  1424→{
  1425→  "data": [
  1426→    {
  1427→      "id": 10,
  1428→      "store_id": 1,
  1429→      "title": "Classic T-Shirt",
  1430→      "handle": "classic-t-shirt",
  1431→      "status": "active",
  1432→      "vendor": "Acme Apparel",
  1433→      "product_type": "Apparel",
  1434→      "tags": ["organic", "cotton"],
  1435→      "variants_count": 6,
  1436→      "total_inventory": 150,
  1437→      "published_at": "2026-02-10T08:00:00Z",
  1438→      "created_at": "2026-02-09T14:00:00Z",
  1439→      "updated_at": "2026-02-11T09:00:00Z",
  1440→      "featured_image": {
  1441→        "url": "https://cdn.example.com/products/tshirt.jpg",
  1442→        "alt_text": "Classic T-Shirt in Blue"
  1443→      }
  1444→    }
  1445→  ],
  1446→  "meta": {
  1447→    "current_page": 1,
  1448→    "per_page": 25,
  1449→    "total": 42,
  1450→    "last_page": 2
  1451→  }
  1452→}
  1453→```
  1454→
  1455→---
  1456→
  1457→#### POST /api/admin/v1/stores/{storeId}/products
  1458→
  1459→Create a new product.
  1460→
  1461→**Request body:**
  1462→
  1463→```json
  1464→{
  1465→  "title": "Classic T-Shirt",
  1466→  "handle": "classic-t-shirt",
  1467→  "description_html": "
A comfortable cotton t-shirt.
",
  1468→  "vendor": "Acme Apparel",
  1469→  "product_type": "Apparel",
  1470→  "status": "draft",
  1471→  "tags": ["organic", "cotton"],
  1472→  "options": [
  1473→    { "name": "Color", "position": 1 },
  1474→    { "name": "Size", "position": 2 }
  1475→  ],
  1476→  "variants": [
  1477→    {
  1478→      "sku": "TSH-BLU-S",
  1479→      "barcode": "1234567890123",
  1480→      "price_amount": 2500,
  1481→      "compare_at_amount": 3500,
  1482→      "currency": "EUR",
  1483→      "weight_g": 200,
  1484→      "requires_shipping": true,
  1485→      "is_default": true,
  1486→      "position": 1,
  1487→      "status": "active",
  1488→      "option_values": [
  1489→        { "option_name": "Color", "value": "Blue" },
  1490→        { "option_name": "Size", "value": "Small" }
  1491→      ],
  1492→      "inventory": {
  1493→        "quantity_on_hand": 50,
  1494→        "policy": "deny"
  1495→      }
  1496→    }
  1497→  ],
  1498→  "collections": [5]
  1499→}

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.


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/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.


type: user

role: user

content:
  1500→    E -->|Yes| F[Store resized versions, set status = ready]
  1501→    E -->|No| G[Set status = failed, log error, keep original]
  1502→```
  1503→
  1504→### 15.2 Processing
  1505→
  1506→**Job:** `App\Jobs\ProcessMediaUpload`
  1507→
  1508→**Resize targets:**
  1509→
  1510→| Key | Max Dimensions | Use Case |
  1511→|---|---|---|
  1512→| `thumbnail` | 150x150 px | Admin grid, small previews |
  1513→| `small` | 300x300 px | Product cards, listings |
  1514→| `medium` | 600x600 px | Product detail page |
  1515→| `large` | 1200x1200 px | Full-size product image, zoom |
  1516→
  1517→**Processing rules:**
  1518→- Maintain aspect ratio (contain within target dimensions, no cropping).
  1519→- Generate WebP version alongside the original format if the server supports it.
  1520→- Store all sizes with predictable path keys: `media/{product_id}/{media_id}/{size}.{ext}`
  1521→- Update the `product_media` record with: `width`, `height`, `mime_type`, `byte_size` from the original, and `status = 'ready'`.
  1522→- On failure: set `status = 'failed'` and log the error. Do not delete the original file.
  1523→- Maximum 3 retry attempts before permanent failure.
  1524→
  1525→### 15.3 Cleanup
  1526→
  1527→When a `product_media` record is deleted, a model event listener removes all associated files from storage (all sizes and the original).
  1528→
  1529→---
  1530→
  1531→## 16. Search (SQLite FTS5)
  1532→
  1533→### 16.1 Index Setup
  1534→
  1535→Create an FTS5 virtual table named `products_fts` with these columns:
  1536→
  1537→| Column | Indexed | Description |
  1538→|---|---|---|
  1539→| `store_id` | No (UNINDEXED) | Used for filtering, not searching |
  1540→| `product_id` | No (UNINDEXED) | Foreign key reference for loading results |
  1541→| `title` | Yes | Product title, fully searchable |
  1542→| `description` | Yes | Plain-text version of description_html (HTML tags stripped) |
  1543→| `vendor` | Yes | Vendor name |
  1544→| `product_type` | Yes | Product type |
  1545→| `tags` | Yes | Space-separated tag values |
  1546→
  1547→### 16.2 Index Synchronization
  1548→
  1549→**Observer:** `App\Observers\ProductObserver`
  1550→
  1551→| Model Event | FTS Action |
  1552→|---|---|
  1553→| `created` | Insert row into products_fts |
  1554→| `updated` | Delete old row, insert updated row (FTS5 does not support UPDATE) |
  1555→| `deleted` | Delete row from products_fts |
  1556→
  1557→**Data preparation before insert:**
  1558→- Strip HTML tags from `description_html` to get plain text.
  1559→- Convert JSON tags array to space-separated string.
  1560→- Use empty string for null vendor and product_type fields.
  1561→
  1562→### 16.3 Search Query
  1563→
  1564→**Service:** `App\Services\SearchService`
  1565→
  1566→**Search algorithm:**
  1567→
  1568→```
  1569→PROCEDURE search(store, queryString, limit = 20):
  1570→    1. Sanitize queryString: escape FTS5 special characters
  1571→    2. Append '*' to last token for prefix matching (autocomplete)
  1572→       Example: "running sh" becomes "running sh*"
  1573→    3. Execute FTS5 MATCH query:
  1574→       - Filter by store_id
  1575→       - Order by FTS5 relevance rank
  1576→       - Limit results
  1577→    4. Load matching Product models
  1578→    5. Exclude products with status != 'active' or published_at IS NULL
  1579→    6. RETURN ordered collection
  1580→```
  1581→
  1582→### 16.4 Search Logging
  1583→
  1584→Each search query is logged to `search_queries` with: store_id, query text, results_count, created_at. This data feeds into analytics and can inform autocomplete suggestions.
  1585→
  1586→---
  1587→
  1588→## 17. Event and Listener Registry
  1589→
  1590→Summary of all domain events and their listeners/side effects:
  1591→
  1592→| Event | Listeners / Side Effects |
  1593→|---|---|
  1594→| `OrderCreated` | Send confirmation email, dispatch webhooks (`order.created`), log analytics event |
  1595→| `OrderRefunded` | Send refund notification email, dispatch webhooks (`order.refunded`), optionally restock |
  1596→| `CheckoutAddressed` | Recalculate pricing |
  1597→| `CheckoutShippingSelected` | Recalculate pricing |
  1598→| `CheckoutCompleted` | Dispatch webhooks (`checkout.completed`) |
  1599→| `CheckoutExpired` | Release reserved inventory |
  1600→| `ProductStatusChanged` | Sync FTS5 index if transitioning to/from active |
  1601→| `ProductCreated` | Sync FTS5 index, dispatch webhooks (`product.created`) |
  1602→| `ProductUpdated` | Sync FTS5 index, dispatch webhooks (`product.updated`) |
  1603→| `ProductDeleted` | Remove from FTS5 index, dispatch webhooks (`product.deleted`) |
  1604→| `OrderPaid` | Update order status, commit inventory (bank transfer confirmation), auto-fulfill digital products |
  1605→| `FulfillmentCreated` | Update order fulfillment status |
  1606→| `FulfillmentShipped` | Send shipping notification email with tracking info, dispatch webhooks (`order.fulfilled` if fully fulfilled) |
  1607→| `FulfillmentDelivered` | Update fulfillment record, dispatch webhooks |
  1608→| `OrderCancelled` | Release reserved inventory, send cancellation email |
  1609→
  1610→All events should be placed in `App\Events`. All listeners in `App\Listeners`.
  1611→
  1612→---
  1613→
  1614→## 18. Scheduled Jobs Registry
  1615→
  1616→| Job Class | Schedule | Description |
  1617→|---|---|---|
  1618→| `App\Jobs\CleanupAbandonedCarts` | Daily | Mark stale active carts as abandoned (14-day threshold) |
  1619→| `App\Jobs\ExpireAbandonedCheckouts` | Every 15 minutes | Expire checkouts idle for 24+ hours, release reserved inventory |
  1620→| `App\Jobs\AggregateAnalytics` | Daily at 01:00 UTC | Roll up raw analytics events into daily aggregates |
  1621→| `App\Jobs\CancelUnpaidBankTransferOrders` | Daily | Cancel bank transfer orders that remain unpaid after configurable days (default: 7) |
  1622→
  1623→**Queued jobs (dispatched on demand, not scheduled):**
  1624→
  1625→| Job Class | Description |
  1626→|---|---|
  1627→| `App\Jobs\ProcessMediaUpload` | Resize uploaded images to standard sizes |
  1628→| `App\Jobs\DeliverWebhook` | POST webhook payload to subscriber endpoint |
  1629→
  1630→Schedule registration belongs in `routes/console.php`.
  1631→
  1632→---
  1633→
  1634→## 19. Service Class Registry
  1635→
  1636→| Service | Namespace | Responsibility |
  1637→|---|---|---|
  1638→| ProductService | `App\Services` | Product CRUD, status transitions |
  1639→| VariantMatrixService | `App\Services` | Option/variant matrix management |
  1640→| InventoryService | `App\Services` | Reserve, release, commit, restock operations |
  1641→| CartService | `App\Services` | Cart CRUD, line management, merge on login |
  1642→| PricingEngine | `App\Services` | Deterministic total calculation pipeline |
  1643→| CheckoutService | `App\Services` | Checkout state machine transitions |
  1644→| DiscountService | `App\Services` | Discount validation and application |
  1645→| TaxCalculator | `App\Services` | Tax calculation orchestration (delegates to providers) |
  1646→| ManualTaxProvider | `App\Services\Tax` | Manual tax rate calculation |
  1647→| StripeTaxProvider | `App\Services\Tax` | Stripe Tax API integration (stub) |
  1648→| ShippingCalculator | `App\Services` | Zone matching, rate calculation |
  1649→| PaymentService | `App\Services` | Payment processing orchestration |
  1650→| MockPaymentProvider | `App\Services\Payment` | Mock PSP - simulates card/PayPal/bank transfer payments in-process |
  1651→| RefundService | `App\Services` | Refund processing and inventory restock |
  1652→| FulfillmentService | `App\Services` | Fulfillment creation and status management |
  1653→| CustomerService | `App\Services` | Customer registration, account management |
  1654→| WebhookService | `App\Services` | Webhook delivery, circuit breaker |
  1655→| SearchService | `App\Services` | FTS5 search queries |
  1656→| HandleGenerator | `App\Support` | Unique handle/slug generation |
  1657→
  1658→---
  1659→
  1660→## 20. Contracts (Interfaces)
  1661→
  1662→| Interface | Namespace | Implementing Classes |
  1663→|---|---|---|
  1664→| TaxProvider | `App\Contracts` | ManualTaxProvider, StripeTaxProvider |
  1665→| PaymentProvider | `App\Contracts` | MockPaymentProvider |
  1666→
  1667→---
  1668→
  1669→## 21. Enum Registry
  1670→
  1671→All enums live in `App\Enums` and are string-backed.
  1672→
  1673→| Enum | Values (TitleCase keys, lowercase string values) |
  1674→|---|---|
  1675→| StoreUserRole | Owner (`owner`), Admin (`admin`), Staff (`staff`), Support (`support`) |
  1676→| ProductStatus | Draft (`draft`), Active (`active`), Archived (`archived`) |
  1677→| VariantStatus | Active (`active`), Archived (`archived`) |
  1678→| DiscountType | Code (`code`), Automatic (`automatic`) |
  1679→| DiscountValueType | Percent (`percent`), Fixed (`fixed`), FreeShipping (`free_shipping`) |
  1680→| CartStatus | Active (`active`), Converted (`converted`), Abandoned (`abandoned`) |
  1681→| CheckoutStatus | Started (`started`), Addressed (`addressed`), ShippingSelected (`shipping_selected`), PaymentSelected (`payment_selected`), Completed (`completed`), Expired (`expired`) |
  1682→| PaymentMethod | CreditCard (`credit_card`), Paypal (`paypal`), BankTransfer (`bank_transfer`) |
  1683→| PaymentStatus | Pending (`pending`), Captured (`captured`), Failed (`failed`), Refunded (`refunded`) |
  1684→| OrderStatus | Pending (`pending`), Paid (`paid`), Fulfilled (`fulfilled`), Cancelled (`cancelled`), Refunded (`refunded`) |
  1685→| FinancialStatus | Pending (`pending`), Authorized (`authorized`), Paid (`paid`), PartiallyRefunded (`partially_refunded`), Refunded (`refunded`), Voided (`voided`) |
  1686→| FulfillmentOrderStatus | Unfulfilled (`unfulfilled`), Partial (`partial`), Fulfilled (`fulfilled`) |
  1687→| FulfillmentShipmentStatus | Pending (`pending`), Shipped (`shipped`), Delivered (`delivered`) |
  1688→| ShippingRateType | Flat (`flat`), Weight (`weight`), Price (`price`), Carrier (`carrier`) |
  1689→| MediaStatus | Processing (`processing`), Ready (`ready`), Failed (`failed`) |
  1690→| WebhookSubscriptionStatus | Active (`active`), Paused (`paused`), Disabled (`disabled`) |
  1691→
  1692→---
  1693→
  1694→## 22. Value Object Registry
  1695→
  1696→All value objects live in `App\ValueObjects`.
  1697→
  1698→| Value Object | Fields | Purpose |
  1699→|---|---|---|
  1700→| PricingResult | subtotal (int), discount (int), shipping (int), taxLines (array of TaxLine), taxTotal (int), total (int), currency (string) | Complete pricing calculation output |
  1701→| TaxLine | name (string), rate (int, basis points), amount (int, cents) | Individual tax line item |
  1702→| TaxCalculationRequest | lineItems (array), shippingAmount (int), address (Address), taxSettings (object) | Input for tax providers |
  1703→| TaxCalculationResult | taxLines (array of TaxLine), totalAmount (int) | Output from tax providers |
  1704→| DiscountValidationResult | valid (bool), discount (nullable), errorCode (nullable string), errorMessage (nullable string) | Discount validation outcome |
  1705→| PaymentResult | success (bool), referenceId (string), status (string: pending/captured/failed), errorCode (nullable string), errorMessage (nullable string) | Mock payment processing result |
  1706→| WebhookResult | success (bool), message (string) | Webhook processing result |
  1707→| RefundResult | success (bool), providerRefundId (string), status (string) | Refund processing result |
  1708→| Address | firstName, lastName, company, address1, address2, city, province, provinceCode, country, countryCode, postalCode, phone | Structured address fields |
  1709→| ShippingRate | id (int), name (string), amount (int, cents), type (string) | Calculated shipping rate option |
  1710→

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.


content:
1500→```
  1501→
  1502→**Product fields:**
  1503→
  1504→| Field | Type | Required | Validation |
  1505→|-------|------|----------|------------|
  1506→| `title` | string | Yes | Max: 255 |
  1507→| `handle` | string | No | Auto-generated from title if not provided. Unique per store. Lowercase, hyphens only. Max: 255. |
  1508→| `description_html` | string | No | Max: 65535 |
  1509→| `vendor` | string | No | Max: 255 |
  1510→| `product_type` | string | No | Max: 255 |
  1511→| `status` | string | No | One of: `draft`, `active`. Default: `draft`. |
  1512→| `tags` | array of strings | No | Max: 50 tags, each max: 255. |
  1513→| `options` | array | No | Max: 3 options. |
  1514→| `options[].name` | string | Yes | Max: 255 |
  1515→| `options[].position` | integer | Yes | Min: 1, max: 3 |
  1516→| `collections` | array of integers | No | Collection IDs to add this product to. |
  1517→
  1518→**Variant fields:**
  1519→
  1520→| Field | Type | Required | Validation |
  1521→|-------|------|----------|------------|
  1522→| `variants` | array | Yes | At least one variant required. Max: 100. |
  1523→| `variants[].sku` | string | Yes | Max: 255. Unique per store (`store_id`, `sku`). |
  1524→| `variants[].barcode` | string | No | Max: 255 |
  1525→| `variants[].price_amount` | integer | Yes | Min: 0 (in minor currency units) |
  1526→| `variants[].compare_at_amount` | integer | No | Min: 0. Must be > price_amount if set. |
  1527→| `variants[].currency` | string | No | Defaults to store currency. |
  1528→| `variants[].weight_g` | integer | No | Min: 0 |
  1529→| `variants[].requires_shipping` | boolean | No | Default: true |
  1530→| `variants[].is_default` | boolean | No | Exactly one variant must be default. |
  1531→| `variants[].position` | integer | No | Auto-assigned if not provided. |
  1532→| `variants[].status` | string | No | One of: `active`, `archived`. Default: `active`. |
  1533→| `variants[].option_values` | array | No | Must match defined options. |
  1534→| `variants[].inventory.quantity_on_hand` | integer | No | Default: 0. Min: 0. |
  1535→| `variants[].inventory.policy` | string | No | One of: `deny`, `continue`. Default: `deny`. |
  1536→
  1537→**Success response:** `201 Created`
  1538→
  1539→Returns the full product resource with nested variants, options, and media.
  1540→
  1541→**Error responses:**
  1542→- `403` - Insufficient permissions
  1543→- `422` - Validation errors (duplicate SKU, duplicate handle, missing required fields)
  1544→
  1545→---
  1546→
  1547→#### GET /api/admin/v1/stores/{storeId}/products/{productId}
  1548→
  1549→Get a single product with all variants, options, media, and collections.
  1550→
  1551→**Success response:** `200 OK`
  1552→
  1553→```json
  1554→{
  1555→  "data": {
  1556→    "id": 10,
  1557→    "store_id": 1,
  1558→    "title": "Classic T-Shirt",
  1559→    "handle": "classic-t-shirt",
  1560→    "description_html": "
A comfortable cotton t-shirt.
",
  1561→    "vendor": "Acme Apparel",
  1562→    "product_type": "Apparel",
  1563→    "status": "active",
  1564→    "tags": ["organic", "cotton"],
  1565→    "published_at": "2026-02-10T08:00:00Z",
  1566→    "created_at": "2026-02-09T14:00:00Z",
  1567→    "updated_at": "2026-02-11T09:00:00Z",
  1568→    "options": [
  1569→      {
  1570→        "id": 1,
  1571→        "name": "Color",
  1572→        "position": 1,
  1573→        "values": [
  1574→          { "id": 1, "value": "Blue", "position": 1 },
  1575→          { "id": 2, "value": "Red", "position": 2 }
  1576→        ]
  1577→      },
  1578→      {
  1579→        "id": 2,
  1580→        "name": "Size",
  1581→        "position": 2,
  1582→        "values": [
  1583→          { "id": 3, "value": "Small", "position": 1 },
  1584→          { "id": 4, "value": "Medium", "position": 2 },
  1585→          { "id": 5, "value": "Large", "position": 3 }
  1586→        ]
  1587→      }
  1588→    ],
  1589→    "variants": [
  1590→      {
  1591→        "id": 101,
  1592→        "sku": "TSH-BLU-S",
  1593→        "barcode": "1234567890123",
  1594→        "price_amount": 2500,
  1595→        "compare_at_amount": 3500,
  1596→        "currency": "EUR",
  1597→        "weight_g": 200,
  1598→        "requires_shipping": true,
  1599→        "is_default": true,
  1600→        "position": 1,
  1601→        "status": "active",
  1602→        "option_values": [
  1603→          { "option_name": "Color", "value": "Blue" },
  1604→          { "option_name": "Size", "value": "Small" }
  1605→        ],
  1606→        "inventory": {
  1607→          "quantity_on_hand": 50,
  1608→          "quantity_reserved": 2,
  1609→          "policy": "deny"
  1610→        }
  1611→      }
  1612→    ],
  1613→    "media": [
  1614→      {
  1615→        "id": 1,
  1616→        "type": "image",
  1617→        "storage_key": "stores/1/products/10/main.jpg",
  1618→        "url": "https://cdn.example.com/stores/1/products/10/main.jpg",
  1619→        "alt_text": "Classic T-Shirt in Blue",
  1620→        "width": 1200,
  1621→        "height": 1600,
  1622→        "mime_type": "image/jpeg",
  1623→        "byte_size": 245000,
  1624→        "position": 1,
  1625→        "status": "ready"
  1626→      }
  1627→    ],
  1628→    "collections": [
  1629→      {
  1630→        "id": 5,
  1631→        "title": "Summer Collection",
  1632→        "handle": "summer-collection"
  1633→      }
  1634→    ]
  1635→  }
  1636→}
  1637→```
  1638→
  1639→**Error responses:**
  1640→- `403` - Insufficient permissions
  1641→- `404` - Product not found (or belongs to another store)
  1642→
  1643→---
  1644→
  1645→#### PUT /api/admin/v1/stores/{storeId}/products/{productId}
  1646→
  1647→Update a product. Supports partial updates.
  1648→
  1649→Same fields as POST. Only provided fields are updated. Variants can be added, updated (by ID), or removed.
  1650→
  1651→**Example partial update request:**
  1652→
  1653→```json
  1654→{
  1655→  "title": "Classic Cotton T-Shirt",
  1656→  "status": "active",
  1657→  "tags": ["organic", "cotton", "bestseller"]
  1658→}
  1659→```
  1660→
  1661→**Success response:** `200 OK`
  1662→
  1663→Returns the full updated product resource.
  1664→
  1665→**Error responses:**
  1666→- `403` - Insufficient permissions
  1667→- `404` - Product not found
  1668→- `422` - Validation errors
  1669→
  1670→---
  1671→
  1672→#### DELETE /api/admin/v1/stores/{storeId}/products/{productId}
  1673→
  1674→Archive a product (soft delete). Products with existing orders cannot be hard-deleted.
  1675→
  1676→**Success response:** `200 OK`
  1677→
  1678→```json
  1679→{
  1680→  "data": {
  1681→    "id": 10,
  1682→    "status": "archived",
  1683→    "updated_at": "2026-02-11T11:00:00Z"
  1684→  }
  1685→}
  1686→```
  1687→
  1688→**Error responses:**
  1689→- `403` - Insufficient permissions
  1690→- `404` - Product not found
  1691→
  1692→---
  1693→
  1694→#### POST /api/admin/v1/stores/{storeId}/products/{productId}/media/presign-upload
  1695→
  1696→Generate a presigned URL for uploading product media directly to object storage (S3).
  1697→
  1698→**Request body:**
  1699→
  1700→```json
  1701→{
  1702→  "filename": "tshirt-front.jpg",
  1703→  "content_type": "image/jpeg",
  1704→  "byte_size": 245000
  1705→}
  1706→```
  1707→
  1708→| Field | Type | Required | Validation |
  1709→|-------|------|----------|------------|
  1710→| `filename` | string | Yes | Max: 255. Must have a valid extension. |
  1711→| `content_type` | string | Yes | One of: `image/jpeg`, `image/png`, `image/webp`, `image/avif`, `video/mp4`. |
  1712→| `byte_size` | integer | Yes | Min: 1, max: configurable (default 50MB for images, 500MB for video). |
  1713→
  1714→**Success response:** `201 Created`
  1715→
  1716→```json
  1717→{
  1718→  "upload_url": "https://s3.example.com/...presigned...",
  1719→  "method": "PUT",
  1720→  "headers": {
  1721→    "Content-Type": "image/jpeg"
  1722→  },
  1723→  "storage_key": "stores/1/products/10/media/uuid.jpg",
  1724→  "media_id": 2,
  1725→  "expires_at": "2026-02-11T10:10:00Z"
  1726→}
  1727→```
  1728→
  1729→**Error responses:**
  1730→- `403` - Insufficient permissions
  1731→- `404` - Product not found
  1732→- `422` - Validation errors (invalid content type, file too large)
  1733→
  1734→---
  1735→
  1736→### 3.3 Collections
  1737→
  1738→#### Admin Collections Endpoint Summary
  1739→
  1740→| Method | Path | Auth Scope | Description | Rate Limit |
  1741→|--------|------|------------|-------------|------------|
  1742→| GET | `/api/admin/v1/stores/{storeId}/collections` | `read-collections` | List collections | `api.admin` (60/min) |
  1743→| POST | `/api/admin/v1/stores/{storeId}/collections` | `write-collections` | Create a collection | `api.admin` (60/min) |
  1744→| PUT | `/api/admin/v1/stores/{storeId}/collections/{collectionId}` | `write-collections` | Update a collection | `api.admin` (60/min) |
  1745→| DELETE | `/api/admin/v1/stores/{storeId}/collections/{collectionId}` | `write-collections` | Delete a collection | `api.admin` (60/min) |
  1746→
  1747→#### GET /api/admin/v1/stores/{storeId}/collections
  1748→
  1749→List collections.
  1750→
  1751→**Query parameters:**
  1752→
  1753→| Parameter | Type | Required | Description |
  1754→|-----------|------|----------|-------------|
  1755→| `status` | string | No | Filter by status: `draft`, `active`, `archived`. |
  1756→| `query` | string | No | Search by title. |
  1757→| `page` | integer | No | Default: 1. |
  1758→| `per_page` | integer | No | Default: 25, max: 100. |
  1759→
  1760→**Success response:** `200 OK`
  1761→
  1762→```json
  1763→{
  1764→  "data": [
  1765→    {
  1766→      "id": 5,
  1767→      "store_id": 1,
  1768→      "title": "Summer Collection",
  1769→      "handle": "summer-collection",
  1770→      "description_html": "
Best items for summer.
",
  1771→      "type": "manual",
  1772→      "status": "active",
  1773→      "products_count": 15,
  1774→      "created_at": "2026-02-01T10:00:00Z",
  1775→      "updated_at": "2026-02-10T14:00:00Z"
  1776→    }
  1777→  ],
  1778→  "meta": {
  1779→    "current_page": 1,
  1780→    "per_page": 25,
  1781→    "total": 5,
  1782→    "last_page": 1
  1783→  }
  1784→}
  1785→```
  1786→
  1787→---
  1788→
  1789→#### POST /api/admin/v1/stores/{storeId}/collections
  1790→
  1791→Create a collection.
  1792→
  1793→**Request body:**
  1794→
  1795→```json
  1796→{
  1797→  "title": "Winter Collection",
  1798→  "handle": "winter-collection",
  1799→  "description_html": "
Cozy items for winter.
",
  1800→  "type": "manual",
  1801→  "status": "active",
  1802→  "product_ids": [10, 11]
  1803→}
  1804→```
  1805→
  1806→| Field | Type | Required | Validation |
  1807→|-------|------|----------|------------|
  1808→| `title` | string | Yes | Max: 255 |
  1809→| `handle` | string | No | Auto-generated from title. Unique per store. |
  1810→| `description_html` | string | No | Max: 65535 |
  1811→| `type` | string | Yes | One of: `manual`, `automated`. |
  1812→| `status` | string | No | One of: `draft`, `active`, `archived`. Default: `active`. |
  1813→| `product_ids` | array of integers | No | Products to add to the collection. |
  1814→
  1815→**Success response:** `201 Created`
  1816→
  1817→Returns the full collection resource.
  1818→
  1819→**Error responses:**
  1820→- `403` - Insufficient permissions
  1821→- `422` - Validation errors
  1822→
  1823→---
  1824→
  1825→#### PUT /api/admin/v1/stores/{storeId}/collections/{collectionId}
  1826→
  1827→Update a collection.
  1828→
  1829→Same fields as POST (all optional). Supports partial updates. Use `product_ids` to replace the full product set, or `add_product_ids` / `remove_product_ids` for incremental changes.
  1830→
  1831→**Success response:** `200 OK`
  1832→
  1833→**Error responses:**
  1834→- `403` - Insufficient permissions
  1835→- `404` - Collection not found
  1836→- `422` - Validation errors
  1837→
  1838→---
  1839→
  1840→#### DELETE /api/admin/v1/stores/{storeId}/collections/{collectionId}
  1841→
  1842→Delete a collection.
  1843→
  1844→**Authentication:** Bearer token with `write-collections` ability.
  1845→
  1846→**Success response:** `200 OK`
  1847→
  1848→```json
  1849→{
  1850→  "message": "Collection deleted"
  1851→}
  1852→```
  1853→
  1854→**Error responses:**
  1855→- `403` - Insufficient permissions
  1856→- `404` - Collection not found
  1857→
  1858→---
  1859→
  1860→### 3.4 Orders
  1861→
  1862→#### Admin Orders Endpoint Summary
  1863→
  1864→| Method | Path | Auth Scope | Description | Rate Limit |
  1865→|--------|------|------------|-------------|------------|
  1866→| GET | `/api/admin/v1/stores/{storeId}/orders` | `read-orders` | List orders with filtering and pagination | `api.admin` (60/min) |
  1867→| GET | `/api/admin/v1/stores/{storeId}/orders/{orderId}` | `read-orders` | Get full order details | `api.admin` (60/min) |
  1868→| POST | `/api/admin/v1/stores/{storeId}/orders/{orderId}/fulfillments` | `write-orders` | Create a fulfillment (ship items) | `api.admin` (60/min) |
  1869→| POST | `/api/admin/v1/stores/{storeId}/orders/{orderId}/refunds` | `write-orders` | Create a refund (partial or full) | `api.admin` (60/min) |
  1870→
  1871→#### GET /api/admin/v1/stores/{storeId}/orders
  1872→
  1873→List orders with filtering and pagination.
  1874→
  1875→**Query parameters:**
  1876→
  1877→| Parameter | Type | Required | Description |
  1878→|-----------|------|----------|-------------|
  1879→| `status` | string | No | Filter: `pending`, `paid`, `fulfilled`, `cancelled`, `refunded`. |
  1880→| `financial_status` | string | No | Filter: `pending`, `paid`, `partially_refunded`, `refunded`. |
  1881→| `fulfillment_status` | string | No | Filter: `unfulfilled`, `partial`, `fulfilled`. |
  1882→| `customer_id` | integer | No | Filter by customer. |
  1883→| `created_after` | string (ISO 8601) | No | Orders placed after this date. |
  1884→| `created_before` | string (ISO 8601) | No | Orders placed before this date. |
  1885→| `query` | string | No | Search by order number or customer email. |
  1886→| `page` | integer | No | Default: 1. |
  1887→| `per_page` | integer | No | Default: 25, max: 100. |
  1888→| `sort` | string | No | One of: `placed_at_desc`, `placed_at_asc`, `total_desc`, `total_asc`. Default: `placed_at_desc`. |
  1889→
  1890→**Success response:** `200 OK`
  1891→
  1892→```json
  1893→{
  1894→  "data": [
  1895→    {
  1896→      "id": 50,
  1897→      "order_number": "#1001",
  1898→      "status": "paid",
  1899→      "financial_status": "paid",
  1900→      "fulfillment_status": "unfulfilled",
  1901→      "customer": {
  1902→        "id": 30,
  1903→        "name": "Jane Doe",
  1904→        "email": "jane@example.com"
  1905→      },
  1906→      "currency": "EUR",
  1907→      "subtotal_amount": 5000,
  1908→      "discount_amount": 500,
  1909→      "shipping_amount": 500,
  1910→      "tax_amount": 950,
  1911→      "total_amount": 5950,
  1912→      "line_count": 1,
  1913→      "placed_at": "2026-02-11T10:10:00Z",
  1914→      "created_at": "2026-02-11T10:10:00Z"
  1915→    }
  1916→  ],
  1917→  "meta": {
  1918→    "current_page": 1,
  1919→    "per_page": 25,
  1920→    "total": 156,
  1921→    "last_page": 7
  1922→  }
  1923→}
  1924→```
  1925→
  1926→---
  1927→
  1928→#### GET /api/admin/v1/stores/{storeId}/orders/{orderId}
  1929→
  1930→Get full order details.
  1931→
  1932→**Success response:** `200 OK`
  1933→
  1934→```json
  1935→{
  1936→  "data": {
  1937→    "id": 50,
  1938→    "store_id": 1,
  1939→    "order_number": "#1001",
  1940→    "status": "paid",
  1941→    "financial_status": "paid",
  1942→    "fulfillment_status": "unfulfilled",
  1943→    "customer": {
  1944→      "id": 30,
  1945→      "name": "Jane Doe",
  1946→      "email": "jane@example.com"
  1947→    },
  1948→    "email": "jane@example.com",
  1949→    "currency": "EUR",
  1950→    "subtotal_amount": 5000,
  1951→    "discount_amount": 500,
  1952→    "shipping_amount": 500,
  1953→    "tax_amount": 950,
  1954→    "total_amount": 5950,
  1955→    "billing_address_json": {
  1956→      "first_name": "Jane",
  1957→      "last_name": "Doe",
  1958→      "address1": "123 Main St",
  1959→      "city": "Berlin",
  1960→      "country_code": "DE",
  1961→      "postal_code": "10115"
  1962→    },
  1963→    "shipping_address_json": {
  1964→      "first_name": "Jane",
  1965→      "last_name": "Doe",
  1966→      "address1": "123 Main St",
  1967→      "city": "Berlin",
  1968→      "country_code": "DE",
  1969→      "postal_code": "10115"
  1970→    },
  1971→    "lines": [
  1972→      {
  1973→        "id": 301,
  1974→        "product_id": 10,
  1975→        "variant_id": 101,
  1976→        "title_snapshot": "Classic T-Shirt",
  1977→        "sku_snapshot": "TSH-BLU-M",
  1978→        "quantity": 2,
  1979→        "unit_price_amount": 2500,
  1980→        "total_amount": 5000,
  1981→        "tax_lines_json": [
  1982→          { "rate": 1900, "amount": 950, "jurisdiction": "DE" }
  1983→        ],
  1984→        "discount_allocations_json": [
  1985→          { "code": "WELCOME10", "amount": 500 }
  1986→        ]
  1987→      }
  1988→    ],
  1989→    "payments": [
  1990→      {
  1991→        "id": 401,
  1992→        "provider": "mock",
  1993→        "method": "credit_card",
  1994→        "provider_payment_id": "mock_abc123",
  1995→        "status": "captured",
  1996→        "amount": 5950,
  1997→        "currency": "EUR",
  1998→        "created_at": "2026-02-11T10:10:00Z"
  1999→      }

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:
  1700→| PricingResult | subtotal (int), discount (int), shipping (int), taxLines (array of TaxLine), taxTotal (int), total (int), currency (string) | Complete pricing calculation output |
  1701→| TaxLine | name (string), rate (int, basis points), amount (int, cents) | Individual tax line item |
  1702→| TaxCalculationRequest | lineItems (array), shippingAmount (int), address (Address), taxSettings (object) | Input for tax providers |
  1703→| TaxCalculationResult | taxLines (array of TaxLine), totalAmount (int) | Output from tax providers |
  1704→| DiscountValidationResult | valid (bool), discount (nullable), errorCode (nullable string), errorMessage (nullable string) | Discount validation outcome |
  1705→| PaymentResult | success (bool), referenceId (string), status (string: pending/captured/failed), errorCode (nullable string), errorMessage (nullable string) | Mock payment processing result |
  1706→| WebhookResult | success (bool), message (string) | Webhook processing result |
  1707→| RefundResult | success (bool), providerRefundId (string), status (string) | Refund processing result |
  1708→| Address | firstName, lastName, company, address1, address2, city, province, provinceCode, country, countryCode, postalCode, phone | Structured address fields |
  1709→| ShippingRate | id (int), name (string), amount (int, cents), type (string) | Calculated shipping rate option |
  1710→

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.


content:
  2000→    ],
  2001→    "fulfillments": [],
  2002→    "refunds": [],
  2003→    "placed_at": "2026-02-11T10:10:00Z",
  2004→    "created_at": "2026-02-11T10:10:00Z",
  2005→    "updated_at": "2026-02-11T10:10:00Z"
  2006→  }
  2007→}
  2008→```
  2009→
  2010→**Error responses:**
  2011→- `403` - Insufficient permissions
  2012→- `404` - Order not found
  2013→
  2014→---
  2015→
  2016→#### POST /api/admin/v1/stores/{storeId}/orders/{orderId}/fulfillments
  2017→
  2018→Create a fulfillment (mark items as shipped).
  2019→
  2020→**Request body:**
  2021→
  2022→```json
  2023→{
  2024→  "tracking_company": "DHL",
  2025→  "tracking_number": "1234567890",
  2026→  "tracking_url": "https://www.dhl.com/track/1234567890",
  2027→  "line_items": [
  2028→    {
  2029→      "order_line_id": 301,
  2030→      "quantity": 2
  2031→    }
  2032→  ],
  2033→  "notify_customer": true
  2034→}
  2035→```
  2036→
  2037→| Field | Type | Required | Validation |
  2038→|-------|------|----------|------------|
  2039→| `tracking_company` | string | No | Max: 255 |
  2040→| `tracking_number` | string | No | Max: 255 |
  2041→| `tracking_url` | string | No | Valid URL, max: 2048 |
  2042→| `line_items` | array | Yes | At least one line item. |
  2043→| `line_items[].order_line_id` | integer | Yes | Must belong to this order. |
  2044→| `line_items[].quantity` | integer | Yes | Min: 1. Must not exceed unfulfilled quantity. |
  2045→| `notify_customer` | boolean | No | Default: true. Sends shipping notification email. |
  2046→
  2047→**Success response:** `201 Created`
  2048→
  2049→```json
  2050→{
  2051→  "data": {
  2052→    "id": 501,
  2053→    "order_id": 50,
  2054→    "status": "shipped",
  2055→    "tracking_company": "DHL",
  2056→    "tracking_number": "1234567890",
  2057→    "tracking_url": "https://www.dhl.com/track/1234567890",
  2058→    "shipped_at": "2026-02-12T08:00:00Z",
  2059→    "line_items": [
  2060→      {
  2061→        "order_line_id": 301,
  2062→        "quantity": 2
  2063→      }
  2064→    ]
  2065→  }
  2066→}
  2067→```
  2068→
  2069→**Error responses:**
  2070→- `403` - Insufficient permissions
  2071→- `404` - Order not found
  2072→- `409` - Order is not in a fulfillable state (cancelled, already fully fulfilled)
  2073→- `422` - Validation errors (exceeds unfulfilled quantity)
  2074→
  2075→---
  2076→
  2077→#### POST /api/admin/v1/stores/{storeId}/orders/{orderId}/refunds
  2078→
  2079→Create a refund. Supports partial refunds by line or amount.
  2080→
  2081→**Request body:**
  2082→
  2083→```json
  2084→{
  2085→  "amount": 2500,
  2086→  "reason": "Customer requested return for 1 item",
  2087→  "line_items": [
  2088→    {
  2089→      "order_line_id": 301,
  2090→      "quantity": 1
  2091→    }
  2092→  ],
  2093→  "notify_customer": true
  2094→}
  2095→```
  2096→
  2097→| Field | Type | Required | Validation |
  2098→|-------|------|----------|------------|
  2099→| `amount` | integer | Yes | Min: 1. Must not exceed total paid amount minus already-refunded amount. In minor units. |
  2100→| `reason` | string | No | Max: 1000 |
  2101→| `line_items` | array | No | Specific lines being refunded. |
  2102→| `line_items[].order_line_id` | integer | Yes | Must belong to this order. |
  2103→| `line_items[].quantity` | integer | Yes | Min: 1. Must not exceed line quantity minus already-refunded quantity. |
  2104→| `notify_customer` | boolean | No | Default: true. |
  2105→
  2106→**Success response:** `201 Created`
  2107→
  2108→```json
  2109→{
  2110→  "data": {
  2111→    "id": 601,
  2112→    "order_id": 50,
  2113→    "payment_id": 401,
  2114→    "provider_refund_id": "re_abc123",
  2115→    "amount": 2500,
  2116→    "reason": "Customer requested return for 1 item",
  2117→    "status": "completed",
  2118→    "created_at": "2026-02-13T09:00:00Z"
  2119→  }
  2120→}
  2121→```
  2122→
  2123→**Error responses:**
  2124→- `403` - Insufficient permissions
  2125→- `404` - Order not found
  2126→- `409` - Order cannot be refunded (not paid, fully refunded already)
  2127→- `422` - Validation errors (amount exceeds refundable total)
  2128→- `502` - Payment provider refund failed
  2129→
  2130→---
  2131→
  2132→### 3.5 Discounts
  2133→
  2134→#### Admin Discounts Endpoint Summary
  2135→
  2136→| Method | Path | Auth Scope | Description | Rate Limit |
  2137→|--------|------|------------|-------------|------------|
  2138→| GET | `/api/admin/v1/stores/{storeId}/discounts` | `read-discounts` | List discounts | `api.admin` (60/min) |
  2139→| POST | `/api/admin/v1/stores/{storeId}/discounts` | `write-discounts` | Create a discount | `api.admin` (60/min) |
  2140→| PUT | `/api/admin/v1/stores/{storeId}/discounts/{discountId}` | `write-discounts` | Update a discount | `api.admin` (60/min) |
  2141→| DELETE | `/api/admin/v1/stores/{storeId}/discounts/{discountId}` | `write-discounts` | Delete a discount | `api.admin` (60/min) |
  2142→
  2143→#### GET /api/admin/v1/stores/{storeId}/discounts
  2144→
  2145→List discounts.
  2146→
  2147→**Query parameters:**
  2148→
  2149→| Parameter | Type | Required | Description |
  2150→|-----------|------|----------|-------------|
  2151→| `type` | string | No | Filter: `code`, `automatic`. |
  2152→| `status` | string | No | Filter: `active`, `expired`, `scheduled`. Computed from starts_at/ends_at. |
  2153→| `page` | integer | No | Default: 1. |
  2154→| `per_page` | integer | No | Default: 25, max: 100. |
  2155→
  2156→**Success response:** `200 OK`
  2157→
  2158→```json
  2159→{
  2160→  "data": [
  2161→    {
  2162→      "id": 20,
  2163→      "store_id": 1,
  2164→      "type": "code",
  2165→      "code": "WELCOME10",
  2166→      "value_type": "percent",
  2167→      "value_amount": 10,
  2168→      "starts_at": "2026-01-01T00:00:00Z",
  2169→      "ends_at": "2026-12-31T23:59:59Z",
  2170→      "usage_limit": 1000,
  2171→      "usage_count": 42,
  2172→      "rules_json": {
  2173→        "minimum_purchase_amount": 2000,
  2174→        "applicable_product_ids": [],
  2175→        "applicable_collection_ids": [],
  2176→        "customer_eligibility": "all"
  2177→      },
  2178→      "created_at": "2026-01-01T00:00:00Z"
  2179→    }
  2180→  ],
  2181→  "meta": {
  2182→    "current_page": 1,
  2183→    "per_page": 25,
  2184→    "total": 8,
  2185→    "last_page": 1
  2186→  }
  2187→}
  2188→```
  2189→
  2190→---
  2191→
  2192→#### POST /api/admin/v1/stores/{storeId}/discounts
  2193→
  2194→Create a discount.
  2195→
  2196→**Request body:**
  2197→
  2198→```json
  2199→{
  2200→  "type": "code",
  2201→  "code": "SUMMER20",
  2202→  "value_type": "percent",
  2203→  "value_amount": 20,
  2204→  "starts_at": "2026-06-01T00:00:00Z",
  2205→  "ends_at": "2026-08-31T23:59:59Z",
  2206→  "usage_limit": 500,
  2207→  "rules_json": {
  2208→    "minimum_purchase_amount": 5000,
  2209→    "applicable_product_ids": [],
  2210→    "applicable_collection_ids": [5],
  2211→    "customer_eligibility": "all",
  2212→    "once_per_customer": true
  2213→  }
  2214→}
  2215→```
  2216→
  2217→| Field | Type | Required | Validation |
  2218→|-------|------|----------|------------|
  2219→| `type` | string | Yes | One of: `code`, `automatic`. |
  2220→| `code` | string | Conditional | Required when type is `code`. Unique per store. Max: 50. |
  2221→| `value_type` | string | Yes | One of: `fixed`, `percent`, `free_shipping`. |
  2222→| `value_amount` | integer | Yes | For `percent`: whole number 1-100 (e.g. 10 = 10%). For `fixed`: min 1, in minor units. For `free_shipping`: ignored. |
  2223→| `starts_at` | string (ISO 8601) | No | Must be a valid datetime. |
  2224→| `ends_at` | string (ISO 8601) | No | Must be after starts_at. |
  2225→| `usage_limit` | integer | No | Min: 1. Null for unlimited. |
  2226→| `rules_json` | object | No | See rules fields below. |
  2227→| `rules_json.minimum_purchase_amount` | integer | No | Min: 0. In minor units. |
  2228→| `rules_json.applicable_product_ids` | array | No | Empty array means all products. |
  2229→| `rules_json.applicable_collection_ids` | array | No | Empty array means all collections. |
  2230→| `rules_json.customer_eligibility` | string | No | One of: `all`, `specific_customers`, `specific_segments`. Default: `all`. |
  2231→| `rules_json.once_per_customer` | boolean | No | Default: false. |
  2232→
  2233→**Success response:** `201 Created`
  2234→
  2235→Returns the full discount resource.
  2236→
  2237→**Error responses:**
  2238→- `403` - Insufficient permissions
  2239→- `422` - Validation errors (duplicate code, invalid dates, etc.)
  2240→
  2241→---
  2242→
  2243→#### PUT /api/admin/v1/stores/{storeId}/discounts/{discountId}
  2244→
  2245→Update a discount. Supports partial updates.
  2246→
  2247→Same fields as POST. Only provided fields are updated. The `code` cannot be changed after creation.
  2248→
  2249→**Success response:** `200 OK`
  2250→
  2251→**Error responses:**
  2252→- `403` - Insufficient permissions
  2253→- `404` - Discount not found
  2254→- `422` - Validation errors
  2255→
  2256→---
  2257→
  2258→#### DELETE /api/admin/v1/stores/{storeId}/discounts/{discountId}
  2259→
  2260→Delete a discount.
  2261→
  2262→**Authentication:** Bearer token with `write-discounts` ability.
  2263→
  2264→**Success response:** `200 OK`
  2265→
  2266→```json
  2267→{
  2268→  "message": "Discount deleted"
  2269→}
  2270→```
  2271→
  2272→**Error responses:**
  2273→- `403` - Insufficient permissions
  2274→- `404` - Discount not found
  2275→
  2276→---
  2277→
  2278→### 3.6 Shipping
  2279→
  2280→#### Admin Shipping Endpoint Summary
  2281→
  2282→| Method | Path | Auth Scope | Description | Rate Limit |
  2283→|--------|------|------------|-------------|------------|
  2284→| GET | `/api/admin/v1/stores/{storeId}/shipping/zones` | `read-settings` | List shipping zones with rates | `api.admin` (60/min) |
  2285→| POST | `/api/admin/v1/stores/{storeId}/shipping/zones` | `write-settings` | Create a shipping zone | `api.admin` (60/min) |
  2286→| PUT | `/api/admin/v1/stores/{storeId}/shipping/zones/{zoneId}` | `write-settings` | Update a shipping zone | `api.admin` (60/min) |
  2287→| POST | `/api/admin/v1/stores/{storeId}/shipping/zones/{zoneId}/rates` | `write-settings` | Add a shipping rate to a zone | `api.admin` (60/min) |
  2288→
  2289→#### GET /api/admin/v1/stores/{storeId}/shipping/zones
  2290→
  2291→List shipping zones with their rates.
  2292→
  2293→**Success response:** `200 OK`
  2294→
  2295→```json
  2296→{
  2297→  "data": [
  2298→    {
  2299→      "id": 1,
  2300→      "store_id": 1,
  2301→      "name": "Germany",
  2302→      "countries_json": ["DE"],
  2303→      "regions_json": [],
  2304→      "rates": [
  2305→        {
  2306→          "id": 1,
  2307→          "name": "Standard Shipping",
  2308→          "type": "flat",
  2309→          "config_json": {
  2310→            "price_amount": 500,
  2311→            "currency": "EUR"
  2312→          },
  2313→          "is_active": true
  2314→        },
  2315→        {
  2316→          "id": 3,
  2317→          "name": "Weight-Based Shipping",
  2318→          "type": "weight",
  2319→          "config_json": {
  2320→            "currency": "EUR",
  2321→            "tiers": [
  2322→              { "min_weight_g": 0, "max_weight_g": 1000, "price_amount": 500 },
  2323→              { "min_weight_g": 1001, "max_weight_g": 5000, "price_amount": 800 },
  2324→              { "min_weight_g": 5001, "max_weight_g": null, "price_amount": 1200 }
  2325→            ]
  2326→          },
  2327→          "is_active": true
  2328→        }
  2329→      ]
  2330→    }
  2331→  ]
  2332→}
  2333→```
  2334→
  2335→---
  2336→
  2337→#### POST /api/admin/v1/stores/{storeId}/shipping/zones
  2338→
  2339→Create a shipping zone.
  2340→
  2341→**Request body:**
  2342→
  2343→```json
  2344→{
  2345→  "name": "Europe (EU)",
  2346→  "countries_json": ["DE", "FR", "NL", "BE", "AT", "IT", "ES"],
  2347→  "regions_json": []
  2348→}
  2349→```
  2350→
  2351→| Field | Type | Required | Validation |
  2352→|-------|------|----------|------------|
  2353→| `name` | string | Yes | Max: 255 |
  2354→| `countries_json` | array of strings | Yes | Valid ISO 3166-1 alpha-2 codes. At least one country. |
  2355→| `regions_json` | array of strings | No | Region/province codes for more granular zones. |
  2356→
  2357→**Success response:** `201 Created`
  2358→
  2359→**Error responses:**
  2360→- `403` - Insufficient permissions
  2361→- `422` - Validation errors (overlapping countries with existing zones)
  2362→
  2363→---
  2364→
  2365→#### PUT /api/admin/v1/stores/{storeId}/shipping/zones/{zoneId}
  2366→
  2367→Update a shipping zone.
  2368→
  2369→Same fields as POST.
  2370→
  2371→**Success response:** `200 OK`
  2372→
  2373→**Error responses:**
  2374→- `403` - Insufficient permissions
  2375→- `404` - Zone not found
  2376→- `422` - Validation errors
  2377→
  2378→---
  2379→
  2380→#### POST /api/admin/v1/stores/{storeId}/shipping/zones/{zoneId}/rates
  2381→
  2382→Add a shipping rate to a zone.
  2383→
  2384→**Request body:**
  2385→
  2386→```json
  2387→{
  2388→  "name": "Express Shipping",
  2389→  "type": "flat",
  2390→  "config_json": {
  2391→    "price_amount": 1200,
  2392→    "currency": "EUR"
  2393→  },
  2394→  "is_active": true
  2395→}
  2396→```
  2397→
  2398→| Field | Type | Required | Validation |
  2399→|-------|------|----------|------------|
  2400→| `name` | string | Yes | Max: 255 |
  2401→| `type` | string | Yes | One of: `flat`, `weight`, `price`, `carrier`. |
  2402→| `config_json` | object | Yes | Schema depends on type. |
  2403→| `is_active` | boolean | No | Default: true. |
  2404→
  2405→**Success response:** `201 Created`
  2406→
  2407→**Error responses:**
  2408→- `403` - Insufficient permissions
  2409→- `404` - Zone not found
  2410→- `422` - Validation errors
  2411→
  2412→---
  2413→
  2414→### 3.7 Tax Settings
  2415→
  2416→#### Admin Tax Endpoint Summary
  2417→
  2418→| Method | Path | Auth Scope | Description | Rate Limit |
  2419→|--------|------|------------|-------------|------------|
  2420→| GET | `/api/admin/v1/stores/{storeId}/tax/settings` | `read-settings` | Get tax configuration | `api.admin` (60/min) |
  2421→| PUT | `/api/admin/v1/stores/{storeId}/tax/settings` | `write-settings` | Update tax configuration | `api.admin` (60/min) |
  2422→
  2423→#### GET /api/admin/v1/stores/{storeId}/tax/settings
  2424→
  2425→Get tax configuration for a store.
  2426→
  2427→**Success response:** `200 OK`
  2428→
  2429→```json
  2430→{
  2431→  "data": {
  2432→    "store_id": 1,
  2433→    "mode": "manual",
  2434→    "provider": "none",
  2435→    "prices_include_tax": true,
  2436→    "config_json": {
  2437→      "default_tax_rate": 1900,
  2438→      "tax_rates": [
  2439→        {
  2440→          "country_code": "DE",
  2441→          "rate": 1900,
  2442→          "name": "MwSt",
  2443→          "shipping_taxed": true
  2444→        },
  2445→        {
  2446→          "country_code": "AT",
  2447→          "rate": 2000,
  2448→          "name": "USt",
  2449→          "shipping_taxed": true
  2450→        }
  2451→      ]
  2452→    },
  2453→    "updated_at": "2026-02-10T14:00:00Z"
  2454→  }
  2455→}
  2456→```
  2457→
  2458→---
  2459→
  2460→#### PUT /api/admin/v1/stores/{storeId}/tax/settings
  2461→
  2462→Update tax configuration.
  2463→
  2464→**Request body:**
  2465→
  2466→```json
  2467→{
  2468→  "mode": "provider",
  2469→  "provider": "stripe_tax",
  2470→  "prices_include_tax": false,
  2471→  "config_json": {
  2472→    "stripe_tax_settings_id": "txr_abc123"
  2473→  }
  2474→}
  2475→```
  2476→
  2477→| Field | Type | Required | Validation |
  2478→|-------|------|----------|------------|
  2479→| `mode` | string | Yes | One of: `manual`, `provider`. |
  2480→| `provider` | string | Conditional | Required when mode is `provider`. One of: `stripe_tax`, `none`. |
  2481→| `prices_include_tax` | boolean | Yes | Whether product prices are tax-inclusive. |
  2482→| `config_json` | object | Yes | Schema depends on mode/provider. |
  2483→
  2484→**Success response:** `200 OK`
  2485→
  2486→**Error responses:**
  2487→- `403` - Insufficient permissions
  2488→- `422` - Validation errors
  2489→
  2490→---
  2491→
  2492→### 3.8 Themes
  2493→
  2494→#### Admin Themes Endpoint Summary
  2495→
  2496→| Method | Path | Auth Scope | Description | Rate Limit |
  2497→|--------|------|------------|-------------|------------|
  2498→| POST | `/api/admin/v1/stores/{storeId}/themes` | `write-themes` | Upload or install a new theme | `api.admin` (60/min) |
  2499→| POST | `/api/admin/v1/stores/{storeId}/themes/{themeId}/publish` | `write-themes` | Publish a theme (make active) | `api.admin` (60/min) |

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/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:
  2500→| PUT | `/api/admin/v1/stores/{storeId}/themes/{themeId}/settings` | `write-themes` | Update theme customization settings | `api.admin` (60/min) |
  2501→
  2502→#### POST /api/admin/v1/stores/{storeId}/themes
  2503→
  2504→Upload or install a new theme.
  2505→
  2506→**Request body (multipart/form-data):**
  2507→
  2508→| Field | Type | Required | Validation |
  2509→|-------|------|----------|------------|
  2510→| `file` | file | Yes | ZIP archive. Max: 50MB. Must contain valid theme structure. |
  2511→| `name` | string | No | Max: 255. Defaults to theme manifest name. |
  2512→
  2513→**Success response:** `201 Created`
  2514→
  2515→```json
  2516→{
  2517→  "data": {
  2518→    "id": 1,
  2519→    "store_id": 1,
  2520→    "name": "Dawn",
  2521→    "version": "1.0.0",
  2522→    "status": "draft",
  2523→    "published_at": null,
  2524→    "created_at": "2026-02-11T10:00:00Z"
  2525→  }
  2526→}
  2527→```
  2528→
  2529→**Error responses:**
  2530→- `403` - Insufficient permissions
  2531→- `422` - Invalid theme archive (missing required templates, invalid manifest)
  2532→
  2533→---
  2534→
  2535→#### POST /api/admin/v1/stores/{storeId}/themes/{themeId}/publish
  2536→
  2537→Publish a theme, making it the active storefront theme. The previously published theme transitions to `draft`.
  2538→
  2539→**Success response:** `200 OK`
  2540→
  2541→```json
  2542→{
  2543→  "data": {
  2544→    "id": 1,
  2545→    "status": "published",
  2546→    "published_at": "2026-02-11T10:05:00Z"
  2547→  }
  2548→}
  2549→```
  2550→
  2551→**Error responses:**
  2552→- `403` - Insufficient permissions
  2553→- `404` - Theme not found
  2554→- `422` - Theme has validation errors preventing publish
  2555→
  2556→---
  2557→
  2558→#### PUT /api/admin/v1/stores/{storeId}/themes/{themeId}/settings
  2559→
  2560→Update theme settings (customization values).
  2561→
  2562→**Request body:**
  2563→
  2564→```json
  2565→{
  2566→  "settings_json": {
  2567→    "colors": {
  2568→      "primary": "#1a73e8",
  2569→      "secondary": "#fbbc04",
  2570→      "background": "#ffffff",
  2571→      "text": "#202124"
  2572→    },
  2573→    "typography": {
  2574→      "heading_font": "Inter",
  2575→      "body_font": "Inter"
  2576→    },
  2577→    "header": {
  2578→      "show_announcement_bar": true,
  2579→      "announcement_text": "Free shipping on orders over 50 EUR!"
  2580→    }
  2581→  }
  2582→}
  2583→```
  2584→
  2585→| Field | Type | Required | Validation |
  2586→|-------|------|----------|------------|
  2587→| `settings_json` | object | Yes | Must conform to the theme's settings schema. |
  2588→
  2589→**Success response:** `200 OK`
  2590→
  2591→**Error responses:**
  2592→- `403` - Insufficient permissions
  2593→- `404` - Theme not found
  2594→- `422` - Settings do not conform to theme schema
  2595→
  2596→---
  2597→
  2598→### 3.9 Pages
  2599→
  2600→#### Admin Pages Endpoint Summary
  2601→
  2602→| Method | Path | Auth Scope | Description | Rate Limit |
  2603→|--------|------|------------|-------------|------------|
  2604→| GET | `/api/admin/v1/stores/{storeId}/pages` | `read-content` | List content pages | `api.admin` (60/min) |
  2605→| POST | `/api/admin/v1/stores/{storeId}/pages` | `write-content` | Create a content page | `api.admin` (60/min) |
  2606→| PUT | `/api/admin/v1/stores/{storeId}/pages/{pageId}` | `write-content` | Update a content page | `api.admin` (60/min) |
  2607→| DELETE | `/api/admin/v1/stores/{storeId}/pages/{pageId}` | `write-content` | Delete a content page | `api.admin` (60/min) |
  2608→
  2609→#### GET /api/admin/v1/stores/{storeId}/pages
  2610→
  2611→List content pages.
  2612→
  2613→**Query parameters:**
  2614→
  2615→| Parameter | Type | Required | Description |
  2616→|-----------|------|----------|-------------|
  2617→| `status` | string | No | Filter: `draft`, `published`. |
  2618→| `page` | integer | No | Default: 1. |
  2619→| `per_page` | integer | No | Default: 25, max: 100. |
  2620→
  2621→**Success response:** `200 OK`
  2622→
  2623→```json
  2624→{
  2625→  "data": [
  2626→    {
  2627→      "id": 1,
  2628→      "store_id": 1,
  2629→      "title": "About Us",
  2630→      "handle": "about-us",
  2631→      "status": "published",
  2632→      "published_at": "2026-02-01T10:00:00Z",
  2633→      "created_at": "2026-01-15T10:00:00Z",
  2634→      "updated_at": "2026-02-01T10:00:00Z"
  2635→    }
  2636→  ],
  2637→  "meta": {
  2638→    "current_page": 1,
  2639→    "per_page": 25,
  2640→    "total": 3,
  2641→    "last_page": 1
  2642→  }
  2643→}
  2644→```
  2645→
  2646→---
  2647→
  2648→#### POST /api/admin/v1/stores/{storeId}/pages
  2649→
  2650→Create a content page.
  2651→
  2652→**Request body:**
  2653→
  2654→```json
  2655→{
  2656→  "title": "Shipping Policy",
  2657→  "handle": "shipping-policy",
  2658→  "body_html": "<h2>Shipping Policy</h2><p>We ship worldwide...</p>",
  2659→  "status": "published"
  2660→}
  2661→```
  2662→
  2663→| Field | Type | Required | Validation |
  2664→|-------|------|----------|------------|
  2665→| `title` | string | Yes | Max: 255 |
  2666→| `handle` | string | No | Auto-generated. Unique per store. Max: 255. |
  2667→| `body_html` | string | No | Max: 65535 |
  2668→| `status` | string | No | One of: `draft`, `published`. Default: `draft`. |
  2669→
  2670→**Success response:** `201 Created`
  2671→
  2672→**Error responses:**
  2673→- `403` - Insufficient permissions
  2674→- `422` - Validation errors
  2675→
  2676→---
  2677→
  2678→#### PUT /api/admin/v1/stores/{storeId}/pages/{pageId}
  2679→
  2680→Update a content page.
  2681→
  2682→Same fields as POST. Supports partial updates.
  2683→
  2684→**Success response:** `200 OK`
  2685→
  2686→**Error responses:**
  2687→- `403` - Insufficient permissions
  2688→- `404` - Page not found
  2689→- `422` - Validation errors
  2690→
  2691→---
  2692→
  2693→#### DELETE /api/admin/v1/stores/{storeId}/pages/{pageId}
  2694→
  2695→Delete a content page.
  2696→
  2697→**Authentication:** Bearer token with `write-content` ability.
  2698→
  2699→**Success response:** `200 OK`
  2700→
  2701→```json
  2702→{
  2703→  "message": "Page deleted"
  2704→}
  2705→```
  2706→
  2707→**Error responses:**
  2708→- `403` - Insufficient permissions
  2709→- `404` - Page not found
  2710→
  2711→---
  2712→
  2713→### 3.10 Search Admin
  2714→
  2715→#### Admin Search Endpoint Summary
  2716→
  2717→| Method | Path | Auth Scope | Description | Rate Limit |
  2718→|--------|------|------------|-------------|------------|
  2719→| POST | `/api/admin/v1/stores/{storeId}/search/reindex` | `write-settings` | Trigger full search reindex (async) | `api.admin` (60/min) |
  2720→| GET | `/api/admin/v1/stores/{storeId}/search/status` | `read-settings` | Get current search index status | `api.admin` (60/min) |
  2721→
  2722→#### POST /api/admin/v1/stores/{storeId}/search/reindex
  2723→
  2724→Trigger a full search reindex for the store. Runs asynchronously via a queued job.
  2725→
  2726→**Success response:** `202 Accepted`
  2727→
  2728→```json
  2729→{
  2730→  "message": "Reindex job queued.",
  2731→  "job_id": "job_reindex_abc123",
  2732→  "status": "queued"
  2733→}
  2734→```
  2735→
  2736→**Error responses:**
  2737→- `403` - Insufficient permissions
  2738→- `409` - Reindex already in progress
  2739→- `429` - Rate limited
  2740→
  2741→---
  2742→
  2743→#### GET /api/admin/v1/stores/{storeId}/search/status
  2744→
  2745→Get the current search index status.
  2746→
  2747→**Success response:** `200 OK`
  2748→
  2749→```json
  2750→{
  2751→  "data": {
  2752→    "store_id": 1,
  2753→    "index_status": "ready",
  2754→    "last_reindex_at": "2026-02-11T08:00:00Z",
  2755→    "last_reindex_duration_seconds": 45,
  2756→    "documents_count": 342,
  2757→    "pending_updates": 0
  2758→  }
  2759→}
  2760→```
  2761→
  2762→---
  2763→
  2764→### 3.11 Analytics and Exports
  2765→
  2766→#### Admin Analytics Endpoint Summary
  2767→
  2768→| Method | Path | Auth Scope | Description | Rate Limit |
  2769→|--------|------|------------|-------------|------------|
  2770→| GET | `/api/admin/v1/stores/{storeId}/analytics/summary` | `read-analytics` | Get analytics summary for date range | `api.admin` (60/min) |
  2771→| POST | `/api/admin/v1/stores/{storeId}/exports/orders` | `read-orders` | Queue an order export job (async) | `api.admin` (60/min) |
  2772→| GET | `/api/admin/v1/stores/{storeId}/exports/{exportId}` | `read-orders` | Check export status and get download URL | `api.admin` (60/min) |
  2773→
  2774→#### GET /api/admin/v1/stores/{storeId}/analytics/summary
  2775→
  2776→Get analytics summary for a date range.
  2777→
  2778→**Query parameters:**
  2779→
  2780→| Parameter | Type | Required | Description |
  2781→|-----------|------|----------|-------------|
  2782→| `from` | string (YYYY-MM-DD) | Yes | Start date. |
  2783→| `to` | string (YYYY-MM-DD) | Yes | End date. Max range: 365 days. |
  2784→| `granularity` | string | No | One of: `day`, `week`, `month`. Default: `day`. |
  2785→
  2786→**Success response:** `200 OK`
  2787→
  2788→```json
  2789→{
  2790→  "data": {
  2791→    "period": {
  2792→      "from": "2026-02-01",
  2793→      "to": "2026-02-11"
  2794→    },
  2795→    "summary": {
  2796→      "orders_count": 156,
  2797→      "revenue_amount": 1250000,
  2798→      "aov_amount": 8012,
  2799→      "visits_count": 4520,
  2800→      "add_to_cart_count": 890,
  2801→      "checkout_started_count": 420,
  2802→      "conversion_rate": 0.0345,
  2803→      "currency": "EUR"
  2804→    },
  2805→    "daily": [
  2806→      {
  2807→        "date": "2026-02-01",
  2808→        "orders_count": 12,
  2809→        "revenue_amount": 95000,
  2810→        "aov_amount": 7917,
  2811→        "visits_count": 380,
  2812→        "add_to_cart_count": 72,
  2813→        "checkout_started_count": 35
  2814→      }
  2815→    ],
  2816→    "top_products": [
  2817→      {
  2818→        "product_id": 10,
  2819→        "title": "Classic T-Shirt",
  2820→        "units_sold": 45,
  2821→        "revenue_amount": 112500
  2822→      }
  2823→    ]
  2824→  }
  2825→}
  2826→```
  2827→
  2828→**Error responses:**
  2829→- `403` - Insufficient permissions
  2830→- `422` - Invalid date range
  2831→
  2832→---
  2833→
  2834→#### POST /api/admin/v1/stores/{storeId}/exports/orders
  2835→
  2836→Queue an order export job. The export runs asynchronously and produces a downloadable CSV.
  2837→
  2838→**Request body:**
  2839→
  2840→```json
  2841→{
  2842→  "format": "csv",
  2843→  "filters": {
  2844→    "status": "paid",
  2845→    "created_after": "2026-01-01T00:00:00Z",
  2846→    "created_before": "2026-02-11T23:59:59Z"
  2847→  }
  2848→}
  2849→```
  2850→
  2851→| Field | Type | Required | Validation |
  2852→|-------|------|----------|------------|
  2853→| `format` | string | No | One of: `csv`. Default: `csv`. |
  2854→| `filters` | object | No | Same filter options as GET /orders. |
  2855→
  2856→**Success response:** `202 Accepted`
  2857→
  2858→```json
  2859→{
  2860→  "export_id": 1,
  2861→  "status": "queued",
  2862→  "created_at": "2026-02-11T10:00:00Z"
  2863→}
  2864→```
  2865→
  2866→---
  2867→
  2868→#### GET /api/admin/v1/stores/{storeId}/exports/{exportId}
  2869→
  2870→Check export status and get download URL.
  2871→
  2872→**Success response:** `200 OK`
  2873→
  2874→```json
  2875→{
  2876→  "data": {
  2877→    "id": 1,
  2878→    "status": "completed",
  2879→    "format": "csv",
  2880→    "row_count": 156,
  2881→    "download_url": "https://s3.example.com/...presigned...",
  2882→    "download_expires_at": "2026-02-11T11:00:00Z",
  2883→    "created_at": "2026-02-11T10:00:00Z",
  2884→    "completed_at": "2026-02-11T10:01:30Z"
  2885→  }
  2886→}
  2887→```
  2888→
  2889→**Export status values:** `queued`, `processing`, `completed`, `failed`.
  2890→
  2891→**Error responses:**
  2892→- `403` - Insufficient permissions
  2893→- `404` - Export not found
  2894→
  2895→---
  2896→
  2897→## 4. Webhook Receiver Endpoints
  2898→
  2899→> **Note:** No inbound webhook endpoints are needed. All payment processing happens in-process via the Mock PSP -- there are no external payment provider callbacks. The webhook infrastructure defined in this section is reserved for **outbound** app webhooks (see Section 5, OAuth / App Ecosystem).
  2900→
  2901→---
  2902→
  2903→## 5. OAuth Endpoints (App Ecosystem)
  2904→
  2905→> **Scope note:** OAuth and app ecosystem endpoints are deferred for initial implementation. The route structure below is defined for future use but implementations should return 501 Not Implemented.
  2906→
  2907→These endpoints implement OAuth2 authorization for third-party apps, powered by Laravel Passport.
  2908→
  2909→---
  2910→
  2911→### GET /oauth/authorize
  2912→
  2913→Display the authorization prompt to the merchant.
  2914→
  2915→- **Middleware:** `web`, `auth`
  2916→
  2917→**Query parameters:**
  2918→
  2919→| Parameter | Type | Required | Description |
  2920→|-----------|------|----------|-------------|
  2921→| `client_id` | string | Yes | The app's OAuth client ID |
  2922→| `redirect_uri` | string | Yes | Must match one of the registered redirect URIs |
  2923→| `response_type` | string | Yes | Must be `code` |
  2924→| `scope` | string | Yes | Space-separated list of requested scopes |
  2925→| `state` | string | Yes | CSRF token for the client |
  2926→
  2927→**Flow:** Merchant reviews requested scopes and approves/denies. On approval, redirects to `redirect_uri` with `code` and `state` parameters. Authorization code is single-use.
  2928→
  2929→---
  2930→
  2931→### POST /oauth/token
  2932→
  2933→Exchange an authorization code for access and refresh tokens.
  2934→
  2935→- **Middleware:** None (public endpoint)
  2936→
  2937→**Authorization code exchange request:**
  2938→
  2939→```json
  2940→{
  2941→  "grant_type": "authorization_code",
  2942→  "client_id": "app_client_abc123",
  2943→  "client_secret": "secret_xyz",
  2944→  "redirect_uri": "https://myapp.example.com/callback",
  2945→  "code": "auth_code_abc123"
  2946→}
  2947→```
  2948→
  2949→**Success response:** `200 OK`
  2950→
  2951→```json
  2952→{
  2953→  "token_type": "Bearer",
  2954→  "expires_in": 3600,
  2955→  "access_token": "eyJ...",
  2956→  "refresh_token": "def..."
  2957→}
  2958→```
  2959→
  2960→**Error responses:**
  2961→- `400` - Invalid grant (code already used, expired, etc.)
  2962→- `401` - Invalid client credentials
  2963→
  2964→**Refresh token request:**
  2965→
  2966→```json
  2967→{
  2968→  "grant_type": "refresh_token",
  2969→  "client_id": "app_client_abc123",
  2970→  "client_secret": "secret_xyz",
  2971→  "refresh_token": "def..."
  2972→}
  2973→```
  2974→
  2975→---
  2976→
  2977→### App API Endpoints
  2978→
  2979→Third-party apps use OAuth tokens to access store data via scoped endpoints.
  2980→
  2981→| Method | Path | Scope Required | Description |
  2982→|--------|------|----------------|-------------|
  2983→| GET | `/api/apps/v1/stores/{storeId}/products` | `read-products` | List products (same schema as admin API) |
  2984→| GET | `/api/apps/v1/stores/{storeId}/orders` | `read-orders` | List orders |
  2985→| GET | `/api/apps/v1/stores/{storeId}/customers` | `read-customers` | List customers |
  2986→
  2987→Token scopes are enforced on every endpoint. Requests with insufficient scopes return `403 Forbidden`.
  2988→
  2989→---
  2990→
  2991→## 6. Middleware Stack
  2992→
  2993→### 6.1 Route Group Middleware
  2994→
  2995→#### Web (default)
  2996→
  2997→Applied to all web routes via the `web` middleware group:
  2998→
  2999→| Middleware | Purpose |

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/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:
  3000→|-----------|---------|
  3001→| `EncryptCookies` | Encrypt/decrypt cookies |
  3002→| `AddQueuedCookiesToResponse` | Attach queued cookies |
  3003→| `StartSession` | Session handling |
  3004→| `ShareErrorsFromSession` | Flash validation errors |
  3005→| `VerifyCsrfToken` | CSRF protection on POST/PUT/PATCH/DELETE |
  3006→| `SubstituteBindings` | Route model binding |
  3007→
  3008→#### Admin
  3009→
  3010→Applied to `/admin/*` routes in addition to `web`:
  3011→
  3012→| Middleware | Purpose |
  3013→|-----------|---------|
  3014→| `auth` | Require authenticated user (Laravel's built-in guard) |
  3015→| `verified` | Require verified email address |
  3016→| `store.resolve` | Resolve current store from session/domain and set `app.store_id` |
  3017→| `role.check` | Verify user has required role for the current store |
  3018→
  3019→#### Storefront
  3020→
  3021→Applied to storefront routes in addition to `web`:
  3022→
  3023→| Middleware | Purpose |
  3024→|-----------|---------|
  3025→| `store.resolve` | Resolve store from request hostname via `store_domains` table |
  3026→
  3027→#### API Storefront
  3028→
  3029→Applied to `/api/storefront/v1/*` routes:
  3030→
  3031→| Middleware | Purpose |
  3032→|-----------|---------|
  3033→| `throttle:api.storefront` | Rate limiting (120/min per IP) |
  3034→| `store.resolve` | Resolve store from request hostname |
  3035→
  3036→#### API Admin
  3037→
  3038→Applied to `/api/admin/v1/*` routes:
  3039→
  3040→| Middleware | Purpose |
  3041→|-----------|---------|
  3042→| `auth:sanctum` | Sanctum token authentication |
  3043→| `store.resolve` | Resolve store from route parameter or token context |
  3044→| `throttle:api.admin` | Rate limiting (60/min per token) |
  3045→
  3046→---
  3047→
  3048→### 6.2 Custom Middleware Definitions
  3049→
  3050→#### `store.resolve`
  3051→
  3052→Resolves the current store context. The resolution strategy depends on the route group:
  3053→
  3054→- **Web (admin):** Reads `store_id` from session. If missing, redirects to store selector (for platform-level users with access to multiple stores).
  3055→- **Web (storefront):** Looks up hostname in `store_domains` table (cached via file cache). Hostname must match a domain with type `storefront`.
  3056→- **API (storefront):** Same as storefront web - hostname-based resolution.
  3057→- **API (admin):** Reads `storeId` from route parameter and verifies the authenticated user has membership.
  3058→
  3059→Sets `app.store_id` in the service container and binds the `Store` model to the request.
  3060→
  3061→**Failure responses:**
  3062→- `404` if store cannot be resolved from hostname.
  3063→- `403` if user does not have access to the requested store.
  3064→- `503` if store status is `suspended` on storefront (shows maintenance page); `403` on admin mutation endpoints for suspended stores.
  3065→
  3066→#### `role.check`
  3067→
  3068→Checks that the authenticated user has the required role for the current store. Roles hierarchy: `owner` > `admin` > `staff` > `support`. The `support` role grants read-only access for customer service purposes. Specific route permission requirements are configured in the route definition or controller.
  3069→
  3070→**Failure response:** `403 Forbidden`
  3071→
  3072→---
  3073→
  3074→## 7. Rate Limiting Configuration
  3075→
  3076→### Rate Limit Tiers
  3077→
  3078→| Tier | Limit | Window | Key | Description |
  3079→|------|-------|--------|-----|-------------|
  3080→| `login` | 5 requests | 1 minute | IP address | Admin and customer login attempts |
  3081→| `api.admin` | 60 requests | 1 minute | Sanctum token ID | Admin REST API endpoints |
  3082→| `api.storefront` | 120 requests | 1 minute | IP address | Storefront REST API (carts, etc.) |
  3083→| `checkout` | 10 requests | 1 minute | Session ID | Checkout creation and mutation endpoints |
  3084→| `search` | 30 requests | 1 minute | IP address | Search and autocomplete endpoints |
  3085→| `analytics` | 60 requests | 1 minute | IP address | Analytics event ingestion |
  3086→| `webhooks` | 100 requests | 1 minute | IP address | Reserved for future outbound app webhook endpoints |
  3087→
  3088→### Rate Limit Response
  3089→
  3090→When a rate limit is exceeded, the server returns `429 Too Many Requests`:
  3091→
  3092→```json
  3093→{
  3094→  "message": "Too many requests. Please try again later.",
  3095→  "retry_after": 42
  3096→}
  3097→```
  3098→
  3099→**Headers included on all rate-limited responses:**
  3100→
  3101→| Header | Description |
  3102→|--------|-------------|
  3103→| `X-RateLimit-Limit` | Maximum requests allowed in the window |
  3104→| `X-RateLimit-Remaining` | Requests remaining in the current window |
  3105→| `Retry-After` | Seconds until the rate limit resets (only on 429) |
  3106→
  3107→### Per-Endpoint Rate Limit Mapping
  3108→
  3109→| Endpoint Group | Tier |
  3110→|---------------|------|
  3111→| `POST /admin/login` | `login` |
  3112→| `POST /account/login` | `login` |
  3113→| `POST /api/storefront/v1/carts*` | `api.storefront` |
  3114→| `GET /api/storefront/v1/carts*` | `api.storefront` |
  3115→| `PUT /api/storefront/v1/carts*` | `api.storefront` |
  3116→| `DELETE /api/storefront/v1/carts*` | `api.storefront` |
  3117→| `POST /api/storefront/v1/checkouts*` | `checkout` |
  3118→| `GET /api/storefront/v1/checkouts*` | `checkout` |
  3119→| `PUT /api/storefront/v1/checkouts*` | `checkout` |
  3120→| `DELETE /api/storefront/v1/checkouts*` | `checkout` |
  3121→| `POST /api/storefront/v1/checkouts/*/pay` | `checkout` |
  3122→| `GET /api/storefront/v1/orders*` | `api.storefront` |
  3123→| `GET /api/storefront/v1/search` | `search` |
  3124→| `GET /api/storefront/v1/search/suggest` | `search` |
  3125→| `POST /api/storefront/v1/analytics/events` | `analytics` |
  3126→| `GET /api/admin/v1/*` | `api.admin` |
  3127→| `POST /api/admin/v1/*` | `api.admin` |
  3128→| `PUT /api/admin/v1/*` | `api.admin` |
  3129→| `DELETE /api/admin/v1/*` | `api.admin` |
  3130→| *(no inbound webhook endpoints - mock PSP)* | - |
  3131→
  3132→---
  3133→
  3134→## 8. Checkout State Machine
  3135→
  3136→The checkout follows a strict state machine with explicit transitions:
  3137→
  3138→```mermaid
  3139→stateDiagram-v2
  3140→    [*] --> started : POST /checkouts (create from cart)
  3141→    started --> addressed : PUT /checkouts/{id}/address
  3142→    addressed --> shipping_selected : PUT /checkouts/{id}/shipping-method
  3143→    shipping_selected --> payment_selected : PUT /checkouts/{id}/payment-method
  3144→    payment_selected --> completed : POST /checkouts/{id}/pay (instant capture: credit_card/paypal)
  3145→    payment_selected --> completed : POST /checkouts/{id}/pay (bank_transfer: deferred, order created as pending)
  3146→    payment_selected --> shipping_selected : Payment declined, customer retries
  3147→    started --> expired : 24-hour TTL reached
  3148→    addressed --> expired : 24-hour TTL reached
  3149→    shipping_selected --> expired : 24-hour TTL reached
  3150→    payment_selected --> expired : 24-hour TTL reached
  3151→    completed --> [*]
  3152→```
  3153→
  3154→### Checkout Status Transition Table
  3155→
  3156→| From | To | Trigger |
  3157→|------|----|---------|
  3158→| `started` | `addressed` | PUT /checkouts/{id}/address with valid address |
  3159→| `addressed` | `shipping_selected` | PUT /checkouts/{id}/shipping-method with valid method |
  3160→| `shipping_selected` | `payment_selected` | PUT /checkouts/{id}/payment-method with valid method |
  3161→| `payment_selected` | `completed` | POST /checkouts/{id}/pay - Mock PSP processes payment in-process (instant for card/paypal, deferred for bank_transfer) |
  3162→| `payment_selected` | `shipping_selected` | Payment declined by Mock PSP (magic card number), customer retries |
  3163→| `*` (any active) | `expired` | Checkout expires_at reached (24-hour TTL, checked via scheduled job) |
  3164→
  3165→Applying or removing a discount does not change the checkout status but triggers totals recalculation at the current state.
  3166→
  3167→---
  3168→
  3169→## 9. API Versioning Strategy
  3170→
  3171→- **Storefront API:** Versioned via URL path (`/api/storefront/v1/`). Breaking changes require a new version.
  3172→- **Admin API:** Versioned via URL path (`/api/admin/v1/`). Same strategy.
  3173→- **App API:** Versioned via URL path (`/api/apps/v1/`).
  3174→- **Outbound webhook payloads:** Include a `api_version` field in outbound webhook deliveries to app installations. Each app installation pins to a version.
  3175→
  3176→Non-breaking changes (new optional fields, new endpoints) can be added to existing versions. Breaking changes (removed fields, changed types, renamed endpoints) require version bumps.
  3177→
  3178→---
  3179→
  3180→## 10. Error Response Format
  3181→
  3182→All API error responses follow a consistent JSON structure:
  3183→
  3184→### Validation Errors (422)
  3185→
  3186→```json
  3187→{
  3188→  "message": "The given data was invalid.",
  3189→  "errors": {
  3190→    "title": ["The title field is required."],
  3191→    "variants.0.sku": ["The sku has already been taken."]
  3192→  }
  3193→}
  3194→```
  3195→
  3196→### Authorization Errors (403)
  3197→
  3198→```json
  3199→{
  3200→  "message": "You do not have permission to perform this action."
  3201→}
  3202→```
  3203→
  3204→### Not Found Errors (404)
  3205→
  3206→```json
  3207→{
  3208→  "message": "The requested resource was not found."
  3209→}
  3210→```
  3211→
  3212→### Conflict Errors (409)
  3213→
  3214→```json
  3215→{
  3216→  "message": "The cart has been modified. Please refresh and try again.",
  3217→  "error_code": "version_conflict",
  3218→  "current_version": 4
  3219→}
  3220→```
  3221→
  3222→### Business Logic Errors (400)
  3223→
  3224→```json
  3225→{
  3226→  "message": "This discount code has expired.",
  3227→  "error_code": "discount_expired"
  3228→}
  3229→```
  3230→
  3231→### Server Errors (500, 502)
  3232→
  3233→```json
  3234→{
  3235→  "message": "An internal server error occurred. Please try again later.",
  3236→  "reference_id": "err_abc123"
  3237→}
  3238→```
  3239→
  3240→The `reference_id` correlates with structured log entries for debugging.
  3241→

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.