output:
Chunk ID: f46867
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 11005
Output:
Total output lines: 197

specs/06-AUTH-AND-SECURITY.md:639:    J --> K[Continue to next middleware]
specs/06-AUTH-AND-SECURITY.md:677:    I --> J[Continue to next middleware]
specs/06-AUTH-AND-SECURITY.md:700:    B -- Yes --> C[Continue to next middleware]
specs/progress.md:29:| Database schema | `specs/01-DATABASE-SCHEMA.md` | partial | Phase 1 tenancy/auth tables, Phase 2 catalog tables, Phase 3 theme/content/navigation tables including `navigation_items.parent_id` nesting, Phase 4 cart/checkout/pricing tables, Phase 5 order/payment/fulfillment tables, and Phase 7 search/analytics/apps/webhooks/export tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, analytics_daily, apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries, and data_exports. |
specs/progress.md:30:| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders`, `/account/orders/{order}`, `/account/addresses`, `/account/logout`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped product/customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, order export creation/status, shipping zone/rate settings, tax settings read/update, theme upload/publish/settings update, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. |
specs/progress.md:31:| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product option matrix generation, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, nested navigation menu editor with drag sorting and parent selection, theme grid, theme settings editor with iframe preview, theme file list/editor, general settings, domain management, shipping zone/rate management, tax settings, checkout policy settings, notification settings, search settings/reindexing, installed app list/detail, and developer token/webhook management with auth protection and store scoping. |
specs/progress.md:32:| Storefront UI | `specs/04-STOREFRONT-UI.md` | partial | Storefront layout with desktop navigation and mobile hamburger flyout, home, collections index/detail, product detail, SQLite-backed search with filters/sort/pagination, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus with one-level dropdown children, cached theme settings, product add-to-cart persistence, cart drawer, cart page with discount and shipping estimates plus stock-limit feedback, checkout address/shipping/discount/payment-selection/order-submission UI, order confirmation, customer password reset forms at the spec root paths with account-path aliases, customer account overview/navigation/logout, customer order list/detail views, and customer address book list/create/update/default/delete controls render seeded and runtime data. |
specs/progress.md:33:| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `OrderExportService`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, storefront cart stock-limit feedback for deny-policy inventory, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, customer address book management, customer logout session invalidation, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
specs/progress.md:34:| Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout/reset, customer guard/provider, customer login/register/logout, customer password reset GET/POST routes at `/forgot-password` and `/reset-password/{token}` with store-scoped hashed tokens, reset-link throttling, generic reset responses, login rate limiting, session hardening, concrete resource policies, encrypted app client/webhook secrets, one-time display developer token generation, and a compatible hashed bearer-token admin API middleware with store/ability enforcement are implemented. |
specs/progress.md:37:| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard/customer/discount/content/settings/theme/navigation management, Phase 7 search, Phase 7 analytics, and Phase 7 apps/webhooks are implemented. Final verification and completion audit are next. |
specs/progress.md:45:- 2026-05-03: Planning challenger sub-agent found the repo was still a starter kit and produced the acceptance checklist by vertical slice. Risks added here: missing Sanctum dependency, password_hash/Fortify coordination, route/API absence, and E2E count inconsistencies.
specs/progress.md:46:- 2026-05-03: `php artisan migrate:fresh --seed --no-interaction` passed on real `database/database.sqlite` after creating the missing SQLite file.
specs/progress.md:195:- 2026-05-04: `mcp__laravel_boost__.application_info` and schema summary reconfirmed Laravel 12.51.0/PHP 8.4/Livewire 4.1.4/Flux 2.12.0/Pest 4.3.2/SQLite and showed search/analytics/app tables were still missing before the search slice.
specs/progress.md:231:- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 password reset, notification, validation, and rate limiting docs plus Livewire 4, Flux UI, and Pest 4 docs before the customer password reset gap fix.
specs/progress.md:322:- 2026-05-04: `php artisan make:controller Api/Admin/V1/ProductController --no-interaction`, `php artisan make:controller Api/Admin/V1/CustomerController --no-interaction`, `php artisan make:resource Admin/V1/ProductResource --no-interaction`, `php artisan make:resource Admin/V1/CustomerResource --no-interaction`, `php artisan make:controller Api/OAuthController --no-interaction`, `php artisan make:controller Api/Apps/V1/DeferredEndpointController --no-interaction`, and `php artisan make:test Api/AdminCatalogApiTest --pest --no-interaction` created the admin catalog API controllers/resources, deferred app/OAuth controllers, and tests.
specs/progress.md:325:- 2026-05-04: `php artisan route:list --path=oauth --except-vendor` confirmed `/oauth/authorize` and `/oauth/token`; `php artisan route:list --path=api/apps/v1 --except-vendor` confirmed 3 deferred app API routes.
specs/progress.md:607:- Admin dashboard sales and top-product metrics are derived from paid or partially refunded orders; the dedicated analytics page now reads analytics event and daily aggregate tables.
specs/progress.md:630:- OAuth/Passport app authorization remains deferred per the roadmap; `/oauth/*` and `/api/apps/v1/*` now return explicit `501 Not Implemented` responses, while the developer token UI uses the existing app installation and `oauth_tokens` schema as a self-contained local token store.
specs/progress.md:647:- The admin settings browser suite keeps domains on the general settings page because the implemented UI uses a domains section rather than a separate domains tab; Suite 6 still covers the spec-visible domain list plus general, shipping, and tax settings flows.
specs/progress.md:663:## Open Issues
specs/progress.md:669:Not complete until the final completion audit is closed. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, deterministic Spec 07 customer/address/order/payment/fulfillment/refund seed data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, deferred OAuth/app route stubs, all 18 Spec 08 automated browser suites, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented.
specs/04-STOREFRONT-UI.md:289:| Grid | 2 columns on mobile, 4 columns on large screens, with gap spacing |
specs/04-STOREFRONT-UI.md:326:| Grid | 2 columns on mobile, 3 on tablet, 4 on desktop, with gap spacing |
specs/04-STOREFRONT-UI.md:352:| Form | Horizontal layout with gap. Input takes remaining width, button shrinks to fit |
specs/04-STOREFRONT-UI.md:467:| Layout | Fills remaining width next to the sidebar |
specs/04-STOREFRONT-UI.md:525:| Style | Numbered pagination with previous/next arrow buttons |
specs/04-STOREFRONT-UI.md:528:| Disabled | Reduced opacity and not-allowed cursor on previous (at first page) or next (at last page) |
specs/04-STOREFRONT-UI.md:530:| Mobile | Simplified: previous/next only with current page indicator |
specs/04-STOREFRONT-UI.md:601:| Thumbnail strip | Horizontal row with overflow scroll, small gap between thumbnails |
specs/04-STOREFRONT-UI.md:637:| Sale badge | Inline badge next to the price |
specs/04-STOREFRONT-UI.md:731:- Horizontal wrapping row with gap
specs/04-STOREFRONT-UI.md:1107:No additional form fields. The button text changes to "Place order - {total}" (not "Pay now", since payment is deferred).
specs/04-STOREFRONT-UI.md:1144:| Totals | Line items for subtotal, discount (conditional), shipping ("Calculated at next step" until selected), tax (calculated after address), and total |
specs/04-STOREFRONT-UI.md:1236:| Layout | Centered row with gap |
specs/04-STOREFRONT-UI.md:1265:| Form | Vertical stack with gap |
specs/04-STOREFRONT-UI.md:1297:| Form | Vertical stack with gap |
specs/04-STOREFRONT-UI.md:1719:- Mobile: previous/next only with "Page X of Y" text
specs/progress.md:29:| Database schema | `specs/01-DATABASE-SCHEMA.md` | partial | Phase 1 tenancy/auth tables, Phase 2 catalog tables, Phase 3 theme/content/navigation tables including `navigation_items.parent_id` nesting, Phase 4 cart/checkout/pricing tables, Phase 5 order/payment/fulfillment tables, and Phase 7 search/analytics/apps/webhooks/export tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, analytics_daily, apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries, and data_exports. |
specs/progress.md:30:| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders`, `/account/orders/{order}`, `/account/addresses`, `/account/logout`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped product/customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, order export creation/status, shipping zone/rate settings, tax settings read/update, theme upload/publish/settings update, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. |
specs/progress.md:31:| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product option matrix generation, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, nested navigation menu editor with drag sorting and parent selection, theme grid, theme settings editor with iframe preview, theme file list/editor, general settings, domain management, shipping zone/rate management, tax settings, checkout policy settings, notification settings, search settings/reindexing, installed app list/detail, and developer token/webhook management with auth protection and store scoping. |
specs/progress.md:32:| Storefront UI | `specs/04-STOREFRONT-UI.md` | partial | Storefront layout with desktop navigation and mobile hamburger flyout, home, collections index/detail, product detail, SQLite-backed search with filters/sort/pagination, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus with one-level dropdown children, cached theme settings, product add-to-cart persistence, cart drawer, cart page with discount and shipping estimates plus stock-limit feedback, checkout address/shipping/discount/payment-selection/order-submission UI, order confirmation, customer password reset forms at the spec root paths with account-path aliases, customer account overview/navigation/logout, customer order list/detail views, and customer address book list/create/update/default/delete controls render seeded and runtime data. |
specs/progress.md:33:| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `OrderExportService`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, storefront cart stock-limit feedback for deny-policy inventory, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, customer address book management, customer logout session…1005 tokens truncated…01 Not Implemented` responses, while the developer token UI uses the existing app installation and `oauth_tokens` schema as a self-contained local token store.
specs/progress.md:647:- The admin settings browser suite keeps domains on the general settings page because the implemented UI uses a domains section rather than a separate domains tab; Suite 6 still covers the spec-visible domain list plus general, shipping, and tax settings flows.
specs/progress.md:663:## Open Issues
specs/progress.md:669:Not complete until the final completion audit is closed. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, deterministic Spec 07 customer/address/order/payment/fulfillment/refund seed data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, deferred OAuth/app route stubs, all 18 Spec 08 automated browser suites, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented.
specs/03-ADMIN-UI.md:920:| Fulfillment status | Flux badge: unfulfilled=zinc, partial=yellow, fulfilled=green |
specs/03-ADMIN-UI.md:1019:- "Refund" button (ghost variant, shown when order is paid or partially_refunded)
specs/03-ADMIN-UI.md:1023:A Flux `callout` (variant: warning) is displayed when the order's `financial_status` is NOT `paid` and NOT `partially_refunded`:
specs/03-ADMIN-UI.md:2197:When `isExporting` is true, show a loading indicator next to the export button. When `exportUrl` is set, show a download link.
specs/01-DATABASE-SCHEMA.md:763:| financial_status | TEXT | No | 'pending' | CHECK IN ('pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided') | Payment status |
specs/01-DATABASE-SCHEMA.md:764:| fulfillment_status | TEXT | No | 'unfulfilled' | CHECK IN ('unfulfilled', 'partial', 'fulfilled') | Shipping status |
specs/01-DATABASE-SCHEMA.md:908:**Notes:** An order with multiple line items may be partially fulfilled. This table tracks how many units of each line item are included in each shipment.
specs/02-API-ROUTES.md:451:    else Bank Transfer (deferred)
specs/02-API-ROUTES.md:657:- `422` - Validation errors (missing required fields, invalid country code, address not in any shipping zone)
specs/02-API-ROUTES.md:909:**Success response (Bank Transfer - deferred payment):** `200 OK`
specs/02-API-ROUTES.md:937:- `422` - Validation errors (missing card fields, invalid payment method)
specs/02-API-ROUTES.md:1012:- `401` - Invalid or missing token
specs/02-API-ROUTES.md:1402:| PUT | `/api/admin/v1/stores/{storeId}/products/{productId}` | `write-products` | Update a product (partial updates supported) | `api.admin` (60/min) |
specs/02-API-ROUTES.md:1543:- `422` - Validation errors (duplicate SKU, duplicate handle, missing required fields)
specs/02-API-ROUTES.md:1647:Update a product. Supports partial updates.
specs/02-API-ROUTES.md:1651:**Example partial update request:**
specs/02-API-ROUTES.md: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.
specs/02-API-ROUTES.md:1869:| POST | `/api/admin/v1/stores/{storeId}/orders/{orderId}/refunds` | `write-orders` | Create a refund (partial or full) | `api.admin` (60/min) |
specs/02-API-ROUTES.md:1880:| `financial_status` | string | No | Filter: `pending`, `paid`, `partially_refunded`, `refunded`. |
specs/02-API-ROUTES.md:1881:| `fulfillment_status` | string | No | Filter: `unfulfilled`, `partial`, `fulfilled`. |
specs/02-API-ROUTES.md:2052:    "id": 501,
specs/02-API-ROUTES.md:2079:Create a refund. Supports partial refunds by line or amount.
specs/02-API-ROUTES.md:2245:Update a discount. Supports partial updates.
specs/02-API-ROUTES.md:2531:- `422` - Invalid theme archive (missing required templates, invalid manifest)
specs/02-API-ROUTES.md:2682:Same fields as POST. Supports partial updates.
specs/02-API-ROUTES.md: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.
specs/02-API-ROUTES.md:3054:- **Web (admin):** Reads `store_id` from session. If missing, redirects to store selector (for platform-level users with access to multiple stores).
specs/02-API-ROUTES.md:3145:    payment_selected --> completed : POST /checkouts/{id}/pay (bank_transfer: deferred, order created as pending)
specs/02-API-ROUTES.md:3161:| `payment_selected` | `completed` | POST /checkouts/{id}/pay - Mock PSP processes payment in-process (instant for card/paypal, deferred for bank_transfer) |
specs/09-IMPLEMENTATION-ROADMAP.md:315:| `rebuildMatrix(Product $product): void` | Computes cartesian product, creates missing variants, archives orphaned ones |
specs/09-IMPLEMENTATION-ROADMAP.md:595:| `Storefront\Checkout\Confirmation` | Order confirmation: order number, items, totals, next steps |
specs/09-IMPLEMENTATION-ROADMAP.md:668:| `charge(Checkout $checkout, PaymentMethod $method, array $details): PaymentResult` | Processes payment in-process. For credit_card: validates magic card numbers, returns success/decline. For paypal: always succeeds. For bank_transfer: returns pending (deferred capture). |
specs/09-IMPLEMENTATION-ROADMAP.md:675:- Bank transfer: deferred capture, returns PaymentResult with status=pending
specs/09-IMPLEMENTATION-ROADMAP.md:716:- Partial refunds: `financial_status` -> `partially_refunded`
specs/09-IMPLEMENTATION-ROADMAP.md:726:| `create(Order $order, array $lines, ?array $tracking): Fulfillment` | Creates fulfillment with specified lines and quantities. **Fulfillment guard:** Throws exception if `financial_status` is not `paid` or `partially_refunded`. |
specs/09-IMPLEMENTATION-ROADMAP.md:730:Updates `order.fulfillment_status` based on whether all lines are fulfilled (`fulfilled`) or some (`partial`).
specs/09-IMPLEMENTATION-ROADMAP.md:732:**Fulfillment guard:** Before creating a fulfillment, the service checks that the order's `financial_status` is `paid` or `partially_refunded`. If the guard fails, a `FulfillmentGuardException` is thrown. This prevents shipping items before payment is confirmed (especially important for bank transfer orders).
specs/09-IMPLEMENTATION-ROADMAP.md:826:Refund modal: enter amount (partial or full), reason, restock checkbox, submit.
specs/09-IMPLEMENTATION-ROADMAP.md:1082:| calculates a weight-based rate | rate type: weight, config: ranges 0-500g=499, 501-2000g=899; cart total weight: 750g | shipping = 899 | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1329:| rejects address transition with missing required fields | set address without city | validation error, status remains started | validation |
specs/09-IMPLEMENTATION-ROADMAP.md:1372:| calculates weight-based rate correctly | cart total weight 750g, rate range 501-2000g = 899 | shipping = 899 | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1398:| preserves order data when product is deleted | create order, then archive the product | order_lines still accessible with snapshots | edge case |
specs/09-IMPLEMENTATION-ROADMAP.md:1409:| creates a partial refund | order total 5000; refund 2000 | financial_status = partially_refunded | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1423:| updates order fulfillment status to partial | fulfill only some lines | order.fulfillment_status = partial | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1431:| fulfillment guard allows fulfillment when financial_status is partially_refunded | order with financial_status=partially_refunded | fulfillment created successfully | edge case |
specs/09-IMPLEMENTATION-ROADMAP.md:1497:| validates required address fields | submit with missing address1 | validation error | validation |
specs/09-IMPLEMENTATION-ROADMAP.md:1660:| validates required address fields | PUT address with missing city | 422 response | validation |
specs/07-SEEDERS-AND-TEST-DATA.md:387:| `partiallyFulfilled` | fulfillment_status = "partial" |
specs/07-SEEDERS-AND-TEST-DATA.md:627:| `weightBased` | type = "weight", config_json has ranges array with tiers: 0-500g = 399, 501-2000g = 699, 2001g+ = 1299 |
specs/07-SEEDERS-AND-TEST-DATA.md:1560:| status / financial / fulfillment | paid / paid / partial |
specs/07-SEEDERS-AND-TEST-DATA.md:1675:| status / financial / fulfillment | paid / partially_refunded / fulfilled |
specs/07-SEEDERS-AND-TEST-DATA.md:1691:Refund (partial - for Graphic Print Tee only): amount 2999, reason "Item returned", status "processed", provider_refund_id "mock_re_test_order1008"
specs/07-SEEDERS-AND-TEST-DATA.md:1849:**Test purpose:** Tests a bank transfer order that has been confirmed by the admin (payment status = captured). Contrasts with #1005 which is still pending.
specs/07-SEEDERS-AND-TEST-DATA.md:1859:| #1003 | Jane Smith | credit_card | paid | paid | partial | 11997 | Partial fulfillment |
specs/07-SEEDERS-AND-TEST-DATA.md:1864:| #1008 | David Lee | credit_card | paid | partially_refunded | fulfilled | 8997 | Partial refund |
specs/07-SEEDERS-AND-TEST-DATA.md:2039:| Day 0 (today, partial) | 80-110 | 2-4 | 15000-30000 |
specs/05-BUSINESS-LOGIC.md:41:3. If the store is suspended: return HTTP 403 Forbidden. Admin users can still view settings but cannot perform storefront-affecting mutations.
specs/05-BUSINESS-LOGIC.md:524:    3. Generate next sequential order number for this store
specs/05-BUSINESS-LOGIC.md:925:- The shipping method is still selected (for fulfillment routing), but the customer is not charged.
specs/05-BUSINESS-LOGIC.md:992:    else Bank Transfer (deferred)
specs/05-BUSINESS-LOGIC.md:1107:| `partially_refunded` | Some items or partial amount refunded |
specs/05-BUSINESS-LOGIC.md:1116:| `partial` | Some items shipped, others pending |
specs/05-BUSINESS-LOGIC.md:1158:           SET order.financial_status = 'partially_refunded'
specs/05-BUSINESS-LOGIC.md:1173:**CRITICAL:** A fulfillment cannot be created unless the order's `financial_status` is `paid` or `partially_refunded`. This prevents shipping goods before payment is confirmed.
specs/05-BUSINESS-LOGIC.md:1180:| `partially_refunded` | **Yes** | Payment confirmed, partial refund does not block shipment |
specs/05-BUSINESS-LOGIC.md:1196:    1. GUARD: Verify order.financial_status IN ('paid', 'partially_refunded')
specs/05-BUSINESS-LOGIC.md:1218:           SET order.fulfillment_status = 'partial'
specs/05-BUSINESS-LOGIC.md:1685:| FinancialStatus | Pending (`pending`), Authorized (`authorized`), Paid (`paid`), PartiallyRefunded (`partially_refunded`), Refunded (`refunded`), Voided (`voided`) |
specs/05-BUSINESS-LOGIC.md:1686:| FulfillmentOrderStatus | Unfulfilled (`unfulfilled`), Partial (`partial`), Fulfilled (`fulfilled`) |
specs/08-PLAYWRIGHT-E2E-PLAN.md:176:**Purpose:** Hit every major public and admin page, assert they load without JavaScript errors. This is the fastest way to catch broken routes, missing views, or uncaught JS exceptions.
specs/08-PLAYWRIGHT-E2E-PLAN.md:688:        paid --> partially_refunded: partial refund
specs/08-PLAYWRIGHT-E2E-PLAN.md:692:        unfulfilled --> partial: partial fulfillment
specs/08-PLAYWRIGHT-E2E-PLAN.md:694:        partial --> fulfilled: remaining items fulfilled
specs/08-PLAYWRIGHT-E2E-PLAN.md:804:7. Enter reason: `Customer requested partial refund`
specs/08-PLAYWRIGHT-E2E-PLAN.md:1373:- "Add to cart" button is still enabled
specs/08-PLAYWRIGHT-E2E-PLAN.md:1711:    Step2 -->|missing fields| Step2Error[Validation errors on address fields]
tests/Feature/Orders/RefundServiceTest.php:70:test('refund service processes a partial line refund and restocks inventory', function () {
database/factories/ShippingRateFactory.php:37:                    ['min_g' => 501, 'max_g' => 2000, 'amount' => 899],
database/factories/ShippingRateFactory.php:50:                    ['min_amount' => 7501, 'amount' => 0],
tests/Feature/Orders/FulfillmentServiceTest.php:67:test('fulfillment service creates partial and complete fulfillments', function () {
tests/Feature/Catalog/VariantMatrixServiceTest.php:13:test('variant matrix rebuild creates missing cartesian combinations and inventory', function () {
database/factories/OrderFactory.php:113:    public function partiallyFulfilled(): static
tests/Feature/Api/AdminCatalogApiTest.php:161:test('deferred oauth and app ecosystem routes return not implemented', function (): void {
tests/Feature/Api/AdminCatalogApiTest.php:166:        ->assertStatus(501)
tests/Feature/Api/AdminCatalogApiTest.php:167:        ->assertJsonPath('message', 'OAuth app ecosystem endpoints are deferred for initial implementation.');
tests/Feature/Api/AdminCatalogApiTest.php:176:        ->assertStatus(501)
tests/Feature/Api/AdminCatalogApiTest.php:177:        ->assertJsonPath('message', 'OAuth app ecosystem endpoints are deferred for initial implementation.');
tests/Feature/Api/AdminCatalogApiTest.php:180:        ->assertStatus(501)
tests/Feature/Api/AdminCatalogApiTest.php:181:        ->assertJsonPath('message', 'App API endpoints are deferred for initial implementation.');
tests/Feature/Auth/CustomerPasswordResetTest.php:158:        ->set('email', 'missing@example.test')
app/Services/CartService.php:261:            throw InvalidCartOperationException::because('Variant inventory is missing.');
tests/Browser/Admin/OrderManagementTest.php:52:        abort(500, 'Seeded bank transfer order is missing.');
tests/Browser/Admin/OrderManagementTest.php:164:        ->fill('textarea[wire\\:model="refundReason"]', 'Customer requested partial refund')
tests/Feature/Checkout/PricingServicesTest.php:271:                ['min_g' => 501, 'max_g' => 2000, 'amount' => 899],
tests/Feature/Foundation/DatabaseConstraintTest.php:21:        ->and($tableSql['orders'])->toContain('"financial_status" varchar check ("financial_status" in (\'pending\', \'authorized\', \'paid\', \'partially_refunded\', \'refunded\', \'voided\'))')
app/Http/Controllers/Api/OAuthController.php:23:            'message' => 'OAuth app ecosystem endpoints are deferred for initial implementation.',
app/Http/Controllers/Api/OAuthController.php:24:        ], 501);
app/Http/Controllers/Api/Apps/V1/DeferredEndpointController.php:13:            'message' => 'App API endpoints are deferred for initial implementation.',
app/Http/Controllers/Api/Apps/V1/DeferredEndpointController.php:14:        ], 501);
app/Http/Middleware/ResolveStore.php:18:     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
app/Http/Middleware/ResolveStore.php:20:    public function handle(Request $request, Closure $next): Response
app/Http/Middleware/ResolveStore.php:38:        return $next($request);
app/Http/Middleware/CheckStoreRole.php:16:     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
app/Http/Middleware/CheckStoreRole.php:18:    public function handle(Request $request, Closure $next, string ...$roles): Response
app/Http/Middleware/CheckStoreRole.php:30:            return $next($request);
app/Http/Middleware/CheckStoreRole.php:40:        return $next($request);
app/Http/Middleware/AuthenticateAdminApi.php:17:     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
app/Http/Middleware/AuthenticateAdminApi.php:19:    public function handle(Request $request, Closure $next, string ...$abilities): Response
app/Http/Middleware/AuthenticateAdminApi.php:28:                return $next($request);
app/Http/Middleware/AuthenticateAdminApi.php:66:        return $next($request);
app/Http/Middleware/EnsureUserEmailIsVerified.php:17:     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
app/Http/Middleware/EnsureUserEmailIsVerified.php:19:    public function handle(Request $request, Closure $next, ?string $redirectToRoute = null): Response
app/Http/Middleware/EnsureUserEmailIsVerified.php:31:        return $next($request);
app/Http/Controllers/Api/Admin/V1/ThemeController.php:85:        $missing = array_values(array_diff(ThemeArchiveInstaller::requiredPaths(), $paths));
app/Http/Controllers/Api/Admin/V1/ThemeController.php:87:        if ($missing !== []) {
app/Http/Controllers/Api/Admin/V1/ThemeController.php:89:                'theme' => __('The theme is missing required file: :path', ['path' => $missing[0]]),
app/Http/Controllers/Api/Admin/V1/OrderController.php:22:            'financial_status' => ['nullable', Rule::in(['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])],
app/Http/Controllers/Api/Admin/V1/OrderController.php:23:            'fulfillment_status' => ['nullable', Rule::in(['unfulfilled', 'partial', 'fulfilled'])],
app/Http/Controllers/Api/Admin/V1/OrderExportController.php:24:            'filters.financial_status' => ['nullable', Rule::in(['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])],
app/Http/Controllers/Api/Admin/V1/OrderExportController.php:25:            'filters.fulfillment_status' => ['nullable', Rule::in(['unfulfilled', 'partial', 'fulfilled'])],
app/Jobs/ProcessMediaUpload.php:78:            throw new RuntimeException('Media file is missing from public storage.');
app/Services/ThemeArchiveInstaller.php:236:        $missing = array_values(array_diff(self::REQUIRED_PATHS, array_keys($files)));
app/Services/ThemeArchiveInstaller.php:238:        if ($missing !== []) {
app/Services/ThemeArchiveInstaller.php:240:                'file' => __('The theme archive is missing required file: :path', ['path' => $missing[0]]),
app/Services/OrderService.php:83:                    'order_number' => $this->nextOrderNumber($checkout->store),
app/Services/OrderService.php:227:    private function nextOrderNumber(Store $store): string
database/seeders/ProductSeeder.php:191:            $next = [];
database/seeders/ProductSeeder.php:195:                    $next[] = [
database/seeders/ProductSeeder.php:202:            $combinations = $next;
app/Enums/FulfillmentStatus.php:8:    case Partial = 'partial';
app/Livewire/Admin/Customers/Show.php:174:        $nextAddress = CustomerAddress::query()
app/Livewire/Admin/Customers/Show.php:179:        if ($nextAddress instanceof CustomerAddress) {
app/Livewire/Admin/Customers/Show.php:180:            $nextAddress->forceFill(['is_default' => true])->save();
app/Livewire/Admin/Products/Form.php:611:                $next = [];
app/Livewire/Admin/Products/Form.php:615:                        $next[] = [
app/Livewire/Admin/Products/Form.php:622:                return $next;
database/migrations/2026_05_04_000408_create_orders_table.php:22:            $table->enum('financial_status', ['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])->default('pending');
database/migrations/2026_05_04_000408_create_orders_table.php:23:            $table->enum('fulfillment_status', ['unfulfilled', 'partial', 'fulfilled'])->default('unfulfilled');
app/Enums/FinancialStatus.php:10:    case PartiallyRefunded = 'partially_refunded';
app/Livewire/Storefront/Account/Addresses/Index.php:179:        $nextAddress = CustomerAddress::query()
app/Livewire/Storefront/Account/Addresses/Index.php:184:        if ($nextAddress instanceof CustomerAddress) {
app/Livewire/Storefront/Account/Addresses/Index.php:185:            $nextAddress->forceFill(['is_default' => true])->save();


output:
Chunk ID: ad4a1b
Wall time: 0.2193 seconds
Process exited with code 0
Original token count: 1322
Output:

  POST      account/logout .................................... account.logout
  POST      admin/logout ........................................ admin.logout
  GET|HEAD  api/admin/v1/stores/{store}/analytics/summary api.admin.v1.analyt…
  GET|HEAD  api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  POST      api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  PUT       api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  DELETE    api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  GET|HEAD  api/admin/v1/stores/{store}/customers api.admin.v1.customers.inde…
  GET|HEAD  api/admin/v1/stores/{store}/customers/{customer} api.admin.v1.cus…
  GET|HEAD  api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.inde…
  POST      api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.stor…
  PUT       api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  DELETE    api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  POST      api/admin/v1/stores/{store}/exports/orders api.admin.v1.exports.o…
  GET|HEAD  api/admin/v1/stores/{store}/exports/{dataExport} api.admin.v1.exp…
  GET|HEAD  api/admin/v1/stores/{store}/orders api.admin.v1.orders.index › Ap…
  GET|HEAD  api/admin/v1/stores/{store}/orders/{order} api.admin.v1.orders.sh…
  POST      api/admin/v1/stores/{store}/orders/{order}/fulfillments api.admin…
  POST      api/admin/v1/stores/{store}/orders/{order}/refunds api.admin.v1.o…
  GET|HEAD  api/admin/v1/stores/{store}/pages api.admin.v1.pages.index › Api\…
  POST      api/admin/v1/stores/{store}/pages api.admin.v1.pages.store › Api\…
  PUT       api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.updat…
  DELETE    api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.destr…
  GET|HEAD  api/admin/v1/stores/{store}/products api.admin.v1.products.index …
  GET|HEAD  api/admin/v1/stores/{store}/products/{product} api.admin.v1.produ…
  POST      api/admin/v1/stores/{store}/search/reindex api.admin.v1.search.re…
  GET|HEAD  api/admin/v1/stores/{store}/search/status api.admin.v1.search.sta…
  GET|HEAD  api/admin/v1/stores/{store}/settings api.admin.v1.settings.show  …
  PUT       api/admin/v1/stores/{store}/settings api.admin.v1.settings.update…
  GET|HEAD  api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  POST      api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  PUT       api/admin/v1/stores/{store}/shipping/zones/{shippingZone} api.adm…
  POST      api/admin/v1/stores/{store}/shipping/zones/{shippingZone}/rates a…
  GET|HEAD  api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  PUT       api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  POST      api/admin/v1/stores/{store}/themes api.admin.v1.themes.store › Ap…
  POST      api/admin/v1/stores/{store}/themes/{theme}/publish api.admin.v1.t…
  PUT       api/admin/v1/stores/{store}/themes/{theme}/settings api.admin.v1.…
  GET|HEAD  api/apps/v1/stores/{store}/customers api.apps.v1.customers.index …
  GET|HEAD  api/apps/v1/stores/{store}/orders api.apps.v1.orders.index › Api\…
  GET|HEAD  api/apps/v1/stores/{store}/products api.apps.v1.products.index › …
  POST      api/storefront/v1/analytics/events api.storefront.v1.analytics.ev…
  POST      api/storefront/v1/carts api.storefront.v1.carts.store › Api\Store…
  GET|HEAD  api/storefront/v1/carts/{cart} api.storefront.v1.carts.show › Api…
  POST      api/storefront/v1/carts/{cart}/lines api.storefront.v1.carts.line…
  PUT       api/storefront/v1/carts/{cart}/lines/{cartLine} api.storefront.v1…
  DELETE    api/storefront/v1/carts/{cart}/lines/{cartLine} api.storefront.v1…
  POST      api/storefront/v1/checkouts api.storefront.v1.checkouts.store › A…
  GET|HEAD  api/storefront/v1/checkouts/{checkout} api.storefront.v1.checkout…
  PUT       api/storefront/v1/checkouts/{checkout}/address api.storefront.v1.…
  POST      api/storefront/v1/checkouts/{checkout}/apply-discount api.storefr…
  DELETE    api/storefront/v1/checkouts/{checkout}/discount api.storefront.v1…
  POST      api/storefront/v1/checkouts/{checkout}/pay api.storefront.v1.chec…
  PUT       api/storefront/v1/checkouts/{checkout}/payment-method api.storefr…
  PUT       api/storefront/v1/checkouts/{checkout}/shipping-method api.storef…
  GET|HEAD  api/storefront/v1/orders/{orderNumber} api.storefront.v1.orders.s…
  GET|HEAD  api/storefront/v1/search api.storefront.v1.search.index › Api\Sto…
  GET|HEAD  api/storefront/v1/search/suggest api.storefront.v1.search.suggest…
  ANY       dashboard .... dashboard › Illuminate\Routing › RedirectController
  POST      forgot-password customer.password.email › Storefront\Account\Auth…
  GET|HEAD  oauth/authorize .. oauth.authorize › Api\OAuthController@authorize
  POST      oauth/token .............. oauth.token › Api\OAuthController@token
  POST      reset-password customer.password.update › Storefront\Account\Auth…
  ANY       settings ................. Illuminate\Routing › RedirectController

                                                           Showing [64] routes


command:
/bin/zsh
-lc
php artisan route:list --except-vendor

type: unknown

cmd: php artisan route:list --except-vendor

aggregated_output:
  POST      account/logout .................................... account.logout
  POST      admin/logout ........................................ admin.logout
  GET|HEAD  api/admin/v1/stores/{store}/analytics/summary api.admin.v1.analyt…
  GET|HEAD  api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  POST      api/admin/v1/stores/{store}/collections api.admin.v1.collections.…
  PUT       api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  DELETE    api/admin/v1/stores/{store}/collections/{collection} api.admin.v1…
  GET|HEAD  api/admin/v1/stores/{store}/customers api.admin.v1.customers.inde…
  GET|HEAD  api/admin/v1/stores/{store}/customers/{customer} api.admin.v1.cus…
  GET|HEAD  api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.inde…
  POST      api/admin/v1/stores/{store}/discounts api.admin.v1.discounts.stor…
  PUT       api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  DELETE    api/admin/v1/stores/{store}/discounts/{discount} api.admin.v1.dis…
  POST      api/admin/v1/stores/{store}/exports/orders api.admin.v1.exports.o…
  GET|HEAD  api/admin/v1/stores/{store}/exports/{dataExport} api.admin.v1.exp…
  GET|HEAD  api/admin/v1/stores/{store}/orders api.admin.v1.orders.index › Ap…
  GET|HEAD  api/admin/v1/stores/{store}/orders/{order} api.admin.v1.orders.sh…
  POST      api/admin/v1/stores/{store}/orders/{order}/fulfillments api.admin…
  POST      api/admin/v1/stores/{store}/orders/{order}/refunds api.admin.v1.o…
  GET|HEAD  api/admin/v1/stores/{store}/pages api.admin.v1.pages.index › Api\…
  POST      api/admin/v1/stores/{store}/pages api.admin.v1.pages.store › Api\…
  PUT       api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.updat…
  DELETE    api/admin/v1/stores/{store}/pages/{page} api.admin.v1.pages.destr…
  GET|HEAD  api/admin/v1/stores/{store}/products api.admin.v1.products.index …
  GET|HEAD  api/admin/v1/stores/{store}/products/{product} api.admin.v1.produ…
  POST      api/admin/v1/stores/{store}/search/reindex api.admin.v1.search.re…
  GET|HEAD  api/admin/v1/stores/{store}/search/status api.admin.v1.search.sta…
  GET|HEAD  api/admin/v1/stores/{store}/settings api.admin.v1.settings.show  …
  PUT       api/admin/v1/stores/{store}/settings api.admin.v1.settings.update…
  GET|HEAD  api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  POST      api/admin/v1/stores/{store}/shipping/zones api.admin.v1.shipping.…
  PUT       api/admin/v1/stores/{store}/shipping/zones/{shippingZone} api.adm…
  POST      api/admin/v1/stores/{store}/shipping/zones/{shippingZone}/rates a…
  GET|HEAD  api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  PUT       api/admin/v1/stores/{store}/tax/settings api.admin.v1.tax.setting…
  POST      api/admin/v1/stores/{store}/themes api.admin.v1.themes.store › Ap…
  POST      api/admin/v1/stores/{store}/themes/{theme}/publish api.admin.v1.t…
  PUT       api/admin/v1/stores/{store}/themes/{theme}/settings api.admin.v1.…
  GET|HEAD  api/apps/v1/stores/{store}/customers api.apps.v1.customers.index …
  GET|HEAD  api/apps/v1/stores/{store}/orders api.apps.v1.orders.index › Api\…
  GET|HEAD  api/apps/v1/stores/{store}/products api.apps.v1.products.index › …
  POST      api/storefront/v1/analytics/events api.storefront.v1.analytics.ev…
  POST      api/storefront/v1/carts api.storefront.v1.carts.store › Api\Store…
  GET|HEAD  api/storefront/v1/carts/{cart} api.storefront.v1.carts.show › Api…
  POST      api/storefront/v1/carts/{cart}/lines api.storefront.v1.carts.line…
  PUT       api/storefront/v1/carts/{cart}/lines/{cartLine} api.storefront.v1…
  DELETE    api/storefront/v1/carts/{cart}/lines/{cartLine} api.storefront.v1…
  POST      api/storefront/v1/checkouts api.storefront.v1.checkouts.store › A…
  GET|HEAD  api/storefront/v1/checkouts/{checkout} api.storefront.v1.checkout…
  PUT       api/storefront/v1/checkouts/{checkout}/address api.storefront.v1.…
  POST      api/storefront/v1/checkouts/{checkout}/apply-discount api.storefr…
  DELETE    api/storefront/v1/checkouts/{checkout}/discount api.storefront.v1…
  POST      api/storefront/v1/checkouts/{checkout}/pay api.storefront.v1.chec…
  PUT       api/storefront/v1/checkouts/{checkout}/payment-method api.storefr…
  PUT       api/storefront/v1/checkouts/{checkout}/shipping-method api.storef…
  GET|HEAD  api/storefront/v1/orders/{orderNumber} api.storefront.v1.orders.s…
  GET|HEAD  api/storefront/v1/search api.storefront.v1.search.index › Api\Sto…
  GET|HEAD  api/storefront/v1/search/suggest api.storefront.v1.search.suggest…
  ANY       dashboard .... dashboard › Illuminate\Routing › RedirectController
  POST      forgot-password customer.password.email › Storefront\Account\Auth…
  GET|HEAD  oauth/authorize .. oauth.authorize › Api\OAuthController@authorize
  POST      oauth/token .............. oauth.token › Api\OAuthController@token
  POST      reset-password customer.password.update › Storefront\Account\Auth…
  ANY       settings ................. Illuminate\Routing › RedirectController

                                                           Showing [64] routes

status: completed


server: laravel-boost

tool: database-schema

type: text

engine: sqlite

store_id: integer

date: varchar

orders_count: integer

revenue_amount: integer

aov_amount: integer

visits_count: integer

add_to_cart_count: integer

checkout_started_count: integer

checkout_completed_count: integer

type: varchar

session_id: varchar

customer_id: integer

properties_json: text

client_event_id: varchar

occurred_at: datetime

created_at: datetime

app_id: integer

scopes_json: text

status: varchar

installed_at: datetime

key: varchar

value: text

expiration: integer

owner: varchar

cart_id: integer

variant_id: integer

quantity: integer

unit_price_amount: integer

line_subtotal_amount: integer

line_discount_amount: integer

line_total_amount: integer

currency: varchar

cart_version: integer

updated_at: datetime

payment_method: varchar

email: varchar

shipping_address_json: text

billing_address_json: text

shipping_method_id: integer

discount_code: varchar

tax_provider_snapshot_json: text

totals_json: text

expires_at: datetime

collection_id: integer

product_id: integer

position: integer

title: varchar

handle: varchar

description_html: text

label: varchar

address_json: text

token: varchar

varchar

password_hash: varchar

format: varchar

filters_json: text

row_count: integer

storage_key: varchar

error_message: text

download_expires_at: datetime

completed_at: datetime

failed_at: datetime

title: varchar

title: varchar

title: varchar

query: text


message: The audit is already surfacing one important distinction: the OAuth/app API `501` routes are explicitly allowed by the spec scope note, but some acceptance rows are still marked partial. I’m checking whether those partial labels are just stale progress bookkeeping or actual missing surfaces.

phase: commentary


type: message

role: assistant

text: The audit is already surfacing one important distinction: the OAuth/app API `501` routes are explicitly allowed by the spec scope note, but some acceptance rows are still marked partial. I’m checking whether those partial labels are just stale progress bookkeeping or actual missing surfaces.


cmd: sed -n '1360,1745p' specs/02-API-ROUTES.md


cmd: sed -n '1745,2305p' specs/02-API-ROUTES.md


cmd: find app/Http/Controllers/Api/Admin/V1 app/Http/Requests/Api/Admin/V1 app/Http/Resources/Admin/V1 -maxdepth 1 -type f | sort


command: sed -n '1360,1745p' specs/02-API-ROUTES.md

type: read

aggregated_output:
- **Authentication:** Sanctum token
- **Rate limit:** `api.admin` (60/min per token)

**Success response:** `200 OK`

```json
{
  "data": {
    "user_id": 3,
    "store_id": 1,
    "role": "admin",
    "email": "admin@acme.com",
    "name": "Jane Admin",
    "permissions": [
      "read-products",
      "write-products",
      "read-orders",
      "write-orders",
      "read-settings",
      "write-settings",
      "read-customers"
    ]
  }
}
```

**Error responses:**
- `403` - Not a member of this store
- `404` - Store not found

---

### 3.2 Products

#### Admin Products Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| GET | `/api/admin/v1/stores/{storeId}/products` | `read-products` | List products with filtering and pagination | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/products` | `write-products` | Create a new product | `api.admin` (60/min) |
| GET | `/api/admin/v1/stores/{storeId}/products/{productId}` | `read-products` | Get single product with all relations | `api.admin` (60/min) |
| PUT | `/api/admin/v1/stores/{storeId}/products/{productId}` | `write-products` | Update a product (partial updates supported) | `api.admin` (60/min) |
| DELETE | `/api/admin/v1/stores/{storeId}/products/{productId}` | `write-products` | Archive a product (soft delete) | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/products/{productId}/media/presign-upload` | `write-products` | Generate presigned URL for media upload | `api.admin` (60/min) |

#### GET /api/admin/v1/stores/{storeId}/products

List products with filtering and pagination.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `status` | string | No | Filter by status: `draft`, `active`, `archived`. |
| `query` | string | No | Search by title, vendor, or SKU. |
| `collection_id` | integer | No | Filter by collection membership. |
| `page` | integer | No | Page number. Default: 1. |
| `per_page` | integer | No | Results per page. Default: 25, max: 100. |
| `sort` | string | No | One of: `title_asc`, `title_desc`, `created_at_asc`, `created_at_desc`, `updated_at_desc`. Default: `updated_at_desc`. |

**Success response:** `200 OK`

```json
{
  "data": [
    {
      "id": 10,
      "store_id": 1,
      "title": "Classic T-Shirt",
      "handle": "classic-t-shirt",
      "status": "active",
      "vendor": "Acme Apparel",
      "product_type": "Apparel",
      "tags": ["organic", "cotton"],
      "variants_count": 6,
      "total_inventory": 150,
      "published_at": "2026-02-10T08:00:00Z",
      "created_at": "2026-02-09T14:00:00Z",
      "updated_at": "2026-02-11T09:00:00Z",
      "featured_image": {
        "url": "https://cdn.example.com/products/tshirt.jpg",
        "alt_text": "Classic T-Shirt in Blue"
      }
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 42,
    "last_page": 2
  }
}
```

---

#### POST /api/admin/v1/stores/{storeId}/products

Create a new product.

**Request body:**

```json
{
  "title": "Classic T-Shirt",
  "handle": "classic-t-shirt",
  "description_html": "<p>A comfortable cotton t-shirt.</p>",
  "vendor": "Acme Apparel",
  "product_type": "Apparel",
  "status": "draft",
  "tags": ["organic", "cotton"],
  "options": [
    { "name": "Color", "position": 1 },
    { "name": "Size", "position": 2 }
  ],
  "variants": [
    {
      "sku": "TSH-BLU-S",
      "barcode": "1234567890123",
      "price_amount": 2500,
      "compare_at_amount": 3500,
      "currency": "EUR",
      "weight_g": 200,
      "requires_shipping": true,
      "is_default": true,
      "position": 1,
      "status": "active",
      "option_values": [
        { "option_name": "Color", "value": "Blue" },
        { "option_name": "Size", "value": "Small" }
      ],
      "inventory": {
        "quantity_on_hand": 50,
        "policy": "deny"
      }
    }
  ],
  "collections": [5]
}
```

**Product fields:**

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `title` | string | Yes | Max: 255 |
| `handle` | string | No | Auto-generated from title if not provided. Unique per store. Lowercase, hyphens only. Max: 255. |
| `description_html` | string | No | Max: 65535 |
| `vendor` | string | No | Max: 255 |
| `product_type` | string | No | Max: 255 |
| `status` | string | No | One of: `draft`, `active`. Default: `draft`. |
| `tags` | array of strings | No | Max: 50 tags, each max: 255. |
| `options` | array | No | Max: 3 options. |
| `options[].name` | string | Yes | Max: 255 |
| `options[].position` | integer | Yes | Min: 1, max: 3 |
| `collections` | array of integers | No | Collection IDs to add this product to. |

**Variant fields:**

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `variants` | array | Yes | At least one variant required. Max: 100. |
| `variants[].sku` | string | Yes…1425 chars truncated…   "store_id": 1,
    "title": "Classic T-Shirt",
    "handle": "classic-t-shirt",
    "description_html": "
A comfortable cotton t-shirt.
",
    "vendor": "Acme Apparel",
    "product_type": "Apparel",
    "status": "active",
    "tags": ["organic", "cotton"],
    "published_at": "2026-02-10T08:00:00Z",
    "created_at": "2026-02-09T14:00:00Z",
    "updated_at": "2026-02-11T09:00:00Z",
    "options": [
      {
        "id": 1,
        "name": "Color",
        "position": 1,
        "values": [
          { "id": 1, "value": "Blue", "position": 1 },
          { "id": 2, "value": "Red", "position": 2 }
        ]
      },
      {
        "id": 2,
        "name": "Size",
        "position": 2,
        "values": [
          { "id": 3, "value": "Small", "position": 1 },
          { "id": 4, "value": "Medium", "position": 2 },
          { "id": 5, "value": "Large", "position": 3 }
        ]
      }
    ],
    "variants": [
      {
        "id": 101,
        "sku": "TSH-BLU-S",
        "barcode": "1234567890123",
        "price_amount": 2500,
        "compare_at_amount": 3500,
        "currency": "EUR",
        "weight_g": 200,
        "requires_shipping": true,
        "is_default": true,
        "position": 1,
        "status": "active",
        "option_values": [
          { "option_name": "Color", "value": "Blue" },
          { "option_name": "Size", "value": "Small" }
        ],
        "inventory": {
          "quantity_on_hand": 50,
          "quantity_reserved": 2,
          "policy": "deny"
        }
      }
    ],
    "media": [
      {
        "id": 1,
        "type": "image",
        "storage_key": "stores/1/products/10/main.jpg",
        "url": "https://cdn.example.com/stores/1/products/10/main.jpg",
        "alt_text": "Classic T-Shirt in Blue",
        "width": 1200,
        "height": 1600,
        "mime_type": "image/jpeg",
        "byte_size": 245000,
        "position": 1,
        "status": "ready"
      }
    ],
    "collections": [
      {
        "id": 5,
        "title": "Summer Collection",
        "handle": "summer-collection"
      }
    ]
  }
}
```

**Error responses:**
- `403` - Insufficient permissions
- `404` - Product not found (or belongs to another store)

---

#### PUT /api/admin/v1/stores/{storeId}/products/{productId}

Update a product. Supports partial updates.

Same fields as POST. Only provided fields are updated. Variants can be added, updated (by ID), or removed.

**Example partial update request:**

```json
{
  "title": "Classic Cotton T-Shirt",
  "status": "active",
  "tags": ["organic", "cotton", "bestseller"]
}
```

**Success response:** `200 OK`

Returns the full updated product resource.

**Error responses:**
- `403` - Insufficient permissions
- `404` - Product not found
- `422` - Validation errors

---

#### DELETE /api/admin/v1/stores/{storeId}/products/{productId}

Archive a product (soft delete). Products with existing orders cannot be hard-deleted.

**Success response:** `200 OK`

```json
{
  "data": {
    "id": 10,
    "status": "archived",
    "updated_at": "2026-02-11T11:00:00Z"
  }
}
```

**Error responses:**
- `403` - Insufficient permissions
- `404` - Product not found

---

#### POST /api/admin/v1/stores/{storeId}/products/{productId}/media/presign-upload

Generate a presigned URL for uploading product media directly to object storage (S3).

**Request body:**

```json
{
  "filename": "tshirt-front.jpg",
  "content_type": "image/jpeg",
  "byte_size": 245000
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `filename` | string | Yes | Max: 255. Must have a valid extension. |
| `content_type` | string | Yes | One of: `image/jpeg`, `image/png`, `image/webp`, `image/avif`, `video/mp4`. |
| `byte_size` | integer | Yes | Min: 1, max: configurable (default 50MB for images, 500MB for video). |

**Success response:** `201 Created`

```json
{
  "upload_url": "https://s3.example.com/...presigned...",
  "method": "PUT",
  "headers": {
    "Content-Type": "image/jpeg"
  },
  "storage_key": "stores/1/products/10/media/uuid.jpg",
  "media_id": 2,
  "expires_at": "2026-02-11T10:10:00Z"
}
```

**Error responses:**
- `403` - Insufficient permissions
- `404` - Product not found
- `422` - Validation errors (invalid content type, file too large)

---

### 3.3 Collections

#### Admin Collections Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| GET | `/api/admin/v1/stores/{storeId}/collections` | `read-collections` | List collections | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/collections` | `write-collections` | Create a collection | `api.admin` (60/min) |
| PUT | `/api/admin/v1/stores/{storeId}/collections/{collectionId}` | `write-collections` | Update a collection | `api.admin` (60/min) |
| DELETE | `/api/admin/v1/stores/{storeId}/collections/{collectionId}` | `write-collections` | Delete a collection | `api.admin` (60/min) |


command: sed -n '1745,2305p' specs/02-API-ROUTES.md

aggregated_output:
| DELETE | `/api/admin/v1/stores/{storeId}/collections/{collectionId}` | `write-collections` | Delete a collection | `api.admin` (60/min) |

#### GET /api/admin/v1/stores/{storeId}/collections

List collections.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `status` | string | No | Filter by status: `draft`, `active`, `archived`. |
| `query` | string | No | Search by title. |
| `page` | integer | No | Default: 1. |
| `per_page` | integer | No | Default: 25, max: 100. |

**Success response:** `200 OK`

```json
{
  "data": [
    {
      "id": 5,
      "store_id": 1,
      "title": "Summer Collection",
      "handle": "summer-collection",
      "description_html": "<p>Best items for summer.</p>",
      "type": "manual",
      "status": "active",
      "products_count": 15,
      "created_at": "2026-02-01T10:00:00Z",
      "updated_at": "2026-02-10T14:00:00Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 5,
    "last_page": 1
  }
}
```

---

#### POST /api/admin/v1/stores/{storeId}/collections

Create a collection.

**Request body:**

```json
{
  "title": "Winter Collection",
  "handle": "winter-collection",
  "description_html": "<p>Cozy items for winter.</p>",
  "type": "manual",
  "status": "active",
  "product_ids": [10, 11]
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `title` | string | Yes | Max: 255 |
| `handle` | string | No | Auto-generated from title. Unique per store. |
| `description_html` | string | No | Max: 65535 |
| `type` | string | Yes | One of: `manual`, `automated`. |
| `status` | string | No | One of: `draft`, `active`, `archived`. Default: `active`. |
| `product_ids` | array of integers | No | Products to add to the collection. |

**Success response:** `201 Created`

Returns the full collection resource.

**Error responses:**
- `403` - Insufficient permissions
- `422` - Validation errors

---

#### PUT /api/admin/v1/stores/{storeId}/collections/{collectionId}

Update a collection.

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.

**Success response:** `200 OK`

**Error responses:**
- `403` - Insufficient permissions
- `404` - Collection not found
- `422` - Validation errors

---

#### DELETE /api/admin/v1/stores/{storeId}/collections/{collectionId}

Delete a collection.

**Authentication:** Bearer token with `write-collections` ability.

**Success response:** `200 OK`

```json
{
  "message": "Collection deleted"
}
```

**Error responses:**
- `403` - Insufficient permissions
- `404` - Collection not found

---

### 3.4 Orders

#### Admin Orders Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| GET | `/api/admin/v1/stores/{storeId}/orders` | `read-orders` | List orders with filtering and pagination | `api.admin` (60/min) |
| GET | `/api/admin/v1/stores/{storeId}/orders/{orderId}` | `read-orders` | Get full order details | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/orders/{orderId}/fulfillments` | `write-orders` | Create a fulfillment (ship items) | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/orders/{orderId}/refunds` | `write-orders` | Create a refund (partial or full) | `api.admin` (60/min) |

#### GET /api/admin/v1/stores/{storeId}/orders

List orders with filtering and pagination.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `status` | string | No | Filter: `pending`, `paid`, `fulfilled`, `cancelled`, `refunded`. |
| `financial_status` | string | No | Filter: `pending`, `paid`, `partially_refunded`, `refunded`. |
| `fulfillment_status` | string | No | Filter: `unfulfilled`, `partial`, `fulfilled`. |
| `customer_id` | integer | No | Filter by customer. |
| `created_after` | string (ISO 8601) | No | Orders placed after this date. |
| `created_before` | string (ISO 8601) | No | Orders placed before this date. |
| `query` | string | No | Search by order number or customer email. |
| `page` | integer | No | Default: 1. |
| `per_page` | integer | No | Default: 25, max: 100. |
| `sort` | string | No | One of: `placed_at_desc`, `placed_at_asc`, `total_desc`, `total_asc`. Default: `placed_at_desc`. |

**Success response:** `200 OK`

```json
{
  "data": [
    {
      "id": 50,
      "order_number": "#1001",
      "status": "paid",
      "financial_status": "paid",
      "fulfillment_status": "unfulfilled",
      "customer": {
        "id": 30,
        "name": "Jane Doe",
        "email": "jane@example.com"
      },
      "currency": "EUR",
      "subtotal_amount": 5000,
      "discount_amount": 500,
      "shipping_amount": 500,
      "tax_amount": 950,
      "total_amount": 5950,
      "line_count": 1,
      "placed_at": …5615 chars truncated…e a discount | `api.admin` (60/min) |
| PUT | `/api/admin/v1/stores/{storeId}/discounts/{discountId}` | `write-discounts` | Update a discount | `api.admin` (60/min) |
| DELETE | `/api/admin/v1/stores/{storeId}/discounts/{discountId}` | `write-discounts` | Delete a discount | `api.admin` (60/min) |

#### GET /api/admin/v1/stores/{storeId}/discounts

List discounts.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `type` | string | No | Filter: `code`, `automatic`. |
| `status` | string | No | Filter: `active`, `expired`, `scheduled`. Computed from starts_at/ends_at. |
| `page` | integer | No | Default: 1. |
| `per_page` | integer | No | Default: 25, max: 100. |

**Success response:** `200 OK`

```json
{
  "data": [
    {
      "id": 20,
      "store_id": 1,
      "type": "code",
      "code": "WELCOME10",
      "value_type": "percent",
      "value_amount": 10,
      "starts_at": "2026-01-01T00:00:00Z",
      "ends_at": "2026-12-31T23:59:59Z",
      "usage_limit": 1000,
      "usage_count": 42,
      "rules_json": {
        "minimum_purchase_amount": 2000,
        "applicable_product_ids": [],
        "applicable_collection_ids": [],
        "customer_eligibility": "all"
      },
      "created_at": "2026-01-01T00:00:00Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 8,
    "last_page": 1
  }
}
```

---

#### POST /api/admin/v1/stores/{storeId}/discounts

Create a discount.

**Request body:**

```json
{
  "type": "code",
  "code": "SUMMER20",
  "value_type": "percent",
  "value_amount": 20,
  "starts_at": "2026-06-01T00:00:00Z",
  "ends_at": "2026-08-31T23:59:59Z",
  "usage_limit": 500,
  "rules_json": {
    "minimum_purchase_amount": 5000,
    "applicable_product_ids": [],
    "applicable_collection_ids": [5],
    "customer_eligibility": "all",
    "once_per_customer": true
  }
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `type` | string | Yes | One of: `code`, `automatic`. |
| `code` | string | Conditional | Required when type is `code`. Unique per store. Max: 50. |
| `value_type` | string | Yes | One of: `fixed`, `percent`, `free_shipping`. |
| `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. |
| `starts_at` | string (ISO 8601) | No | Must be a valid datetime. |
| `ends_at` | string (ISO 8601) | No | Must be after starts_at. |
| `usage_limit` | integer | No | Min: 1. Null for unlimited. |
| `rules_json` | object | No | See rules fields below. |
| `rules_json.minimum_purchase_amount` | integer | No | Min: 0. In minor units. |
| `rules_json.applicable_product_ids` | array | No | Empty array means all products. |
| `rules_json.applicable_collection_ids` | array | No | Empty array means all collections. |
| `rules_json.customer_eligibility` | string | No | One of: `all`, `specific_customers`, `specific_segments`. Default: `all`. |
| `rules_json.once_per_customer` | boolean | No | Default: false. |

**Success response:** `201 Created`

Returns the full discount resource.

**Error responses:**
- `403` - Insufficient permissions
- `422` - Validation errors (duplicate code, invalid dates, etc.)

---

#### PUT /api/admin/v1/stores/{storeId}/discounts/{discountId}

Update a discount. Supports partial updates.

Same fields as POST. Only provided fields are updated. The `code` cannot be changed after creation.

**Success response:** `200 OK`

**Error responses:**
- `403` - Insufficient permissions
- `404` - Discount not found
- `422` - Validation errors

---

#### DELETE /api/admin/v1/stores/{storeId}/discounts/{discountId}

Delete a discount.

**Authentication:** Bearer token with `write-discounts` ability.

**Success response:** `200 OK`

```json
{
  "message": "Discount deleted"
}
```

**Error responses:**
- `403` - Insufficient permissions
- `404` - Discount not found

---

### 3.6 Shipping

#### Admin Shipping Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| GET | `/api/admin/v1/stores/{storeId}/shipping/zones` | `read-settings` | List shipping zones with rates | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/shipping/zones` | `write-settings` | Create a shipping zone | `api.admin` (60/min) |
| PUT | `/api/admin/v1/stores/{storeId}/shipping/zones/{zoneId}` | `write-settings` | Update a shipping zone | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/shipping/zones/{zoneId}/rates` | `write-settings` | Add a shipping rate to a zone | `api.admin` (60/min) |

#### GET /api/admin/v1/stores/{storeId}/shipping/zones

List shipping zones with their rates.

**Success response:** `200 OK`

```json
{
  "data": [
    {
      "id": 1,
      "store_id": 1,
      "name": "Germany",
      "countries_json": ["DE"],
      "regions_json": [],
      "rates": [
        {


command: find app/Http/Controllers/Api/Admin/V1 app/Http/Requests/Api/Admin/V1 app/Http/Resources/Admin/V1 -maxdepth 1 -type f | sort


command:
/bin/zsh
-lc
sed -n '1,220p' routes/api.php

api.php

cmd: sed -n '1,220p' routes/api.php

aggregated_output:
<?php

use App\Http\Controllers\Api\Admin\V1\AnalyticsSummaryController as AdminAnalyticsSummaryController;
use App\Http\Controllers\Api\Admin\V1\CollectionController as AdminCollectionController;
use App\Http\Controllers\Api\Admin\V1\CustomerController as AdminCustomerController;
use App\Http\Controllers\Api\Admin\V1\DiscountController as AdminDiscountController;
use App\Http\Controllers\Api\Admin\V1\OrderController as AdminOrderController;
use App\Http\Controllers\Api\Admin\V1\OrderExportController as AdminOrderExportController;
use App\Http\Controllers\Api\Admin\V1\OrderFulfillmentController as AdminOrderFulfillmentController;
use App\Http\Controllers\Api\Admin\V1\OrderRefundController as AdminOrderRefundController;
use App\Http\Controllers\Api\Admin\V1\PageController as AdminPageController;
use App\Http\Controllers\Api\Admin\V1\ProductController as AdminProductController;
use App\Http\Controllers\Api\Admin\V1\SearchIndexController as AdminSearchIndexController;
use App\Http\Controllers\Api\Admin\V1\ShippingRateController as AdminShippingRateController;
use App\Http\Controllers\Api\Admin\V1\ShippingZoneController as AdminShippingZoneController;
use App\Http\Controllers\Api\Admin\V1\StoreSettingsController as AdminStoreSettingsController;
use App\Http\Controllers\Api\Admin\V1\TaxSettingsController as AdminTaxSettingsController;
use App\Http\Controllers\Api\Admin\V1\ThemeController as AdminThemeController;
use App\Http\Controllers\Api\Admin\V1\ThemeSettingsController as AdminThemeSettingsController;
use App\Http\Controllers\Api\Apps\V1\DeferredEndpointController as DeferredAppEndpointController;
use App\Http\Controllers\Api\Storefront\V1\AnalyticsEventController as StorefrontAnalyticsEventController;
use App\Http\Controllers\Api\Storefront\V1\CartController;
use App\Http\Controllers\Api\Storefront\V1\CartLineController;
use App\Http\Controllers\Api\Storefront\V1\CheckoutController;
use App\Http\Controllers\Api\Storefront\V1\OrderController as StorefrontOrderController;
use App\Http\Controllers\Api\Storefront\V1\SearchController as StorefrontSearchController;
use Illuminate\Support\Facades\Route;

Route::middleware('store.resolve')
    ->prefix('storefront/v1')
    ->name('api.storefront.v1.')
    ->group(function (): void {
        Route::middleware('throttle:api.storefront')->group(function (): void {
            Route::post('carts', [CartController::class, 'store'])->name('carts.store');
            Route::get('carts/{cart}', [CartController::class, 'show'])->name('carts.show');
            Route::post('carts/{cart}/lines', [CartLineController::class, 'store'])->name('carts.lines.store');
            Route::put('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'update'])->name('carts.lines.update');
            Route::delete('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'destroy'])->name('carts.lines.destroy');
        });

        Route::middleware('throttle:search')->group(function (): void {
            Route::get('search', [StorefrontSearchController::class, 'index'])->name('search.index');
            Route::get('search/suggest', [StorefrontSearchController::class, 'suggest'])->name('search.suggest');
        });

        Route::middleware('throttle:analytics')->group(function (): void {
            Route::post('analytics/events', [StorefrontAnalyticsEventController::class, 'store'])->name('analytics.events.store');
        });

        Route::middleware('throttle:checkout')->group(function (): void {
            Route::post('checkouts', [CheckoutController::class, 'store'])->name('checkouts.store');
            Route::get('checkouts/{checkout}', [CheckoutController::class, 'show'])->name('checkouts.show');
            Route::put('checkouts/{checkout}/address', [CheckoutController::class, 'address'])->name('checkouts.address');
            Route::put('checkouts/{checkout}/shipping-method', [CheckoutController::class, 'shippingMethod'])->name('checkouts.shipping-method');
            Route::post('checkouts/{checkout}/apply-discount', [CheckoutController::class, 'applyDiscount'])->name('checkouts.apply-discount');
            Route::delete('checkouts/{checkout}/discount', [CheckoutController::class, 'destroyDiscount'])->name('checkouts.discount.destroy');
            Route::put('checkouts/{checkout}/payment-method', [CheckoutController::class, 'paymentMethod'])->name('checkouts.payment-method');
            Route::post('checkouts/{checkout}/pay', [CheckoutController::class, 'pay'])->name('checkouts.pay');
            Route::get('orders/{orderNumber}', [StorefrontOrderController::class, 'show'])->name('orders.show');
        });
    });

Route::middleware('throttle:60,1')
    ->prefix('admin/v1/stores/{store}')
    ->name('api.admin.v1.')
    ->group(function (): void {
        Route::middleware('admin.api:read-products')->group(function (): void {
            Route::get('products', [AdminProductController::class, 'index'])->name('products.index');
            Route::get('prod…649 chars truncated…pi:write-collections')->group(function (): void {
            Route::post('collections', [AdminCollectionController::class, 'store'])->name('collections.store');
            Route::put('collections/{collection}', [AdminCollectionController::class, 'update'])->name('collections.update');
            Route::delete('collections/{collection}', [AdminCollectionController::class, 'destroy'])->name('collections.destroy');
        });

        Route::middleware('admin.api:read-discounts')->group(function (): void {
            Route::get('discounts', [AdminDiscountController::class, 'index'])->name('discounts.index');
        });

        Route::middleware('admin.api:write-discounts')->group(function (): void {
            Route::post('discounts', [AdminDiscountController::class, 'store'])->name('discounts.store');
            Route::put('discounts/{discount}', [AdminDiscountController::class, 'update'])->name('discounts.update');
            Route::delete('discounts/{discount}', [AdminDiscountController::class, 'destroy'])->name('discounts.destroy');
        });

        Route::middleware('admin.api:read-content')->group(function (): void {
            Route::get('pages', [AdminPageController::class, 'index'])->name('pages.index');
        });

        Route::middleware('admin.api:write-content')->group(function (): void {
            Route::post('pages', [AdminPageController::class, 'store'])->name('pages.store');
            Route::put('pages/{page}', [AdminPageController::class, 'update'])->name('pages.update');
            Route::delete('pages/{page}', [AdminPageController::class, 'destroy'])->name('pages.destroy');
        });

        Route::middleware('admin.api:read-settings')->group(function (): void {
            Route::get('search/status', [AdminSearchIndexController::class, 'status'])->name('search.status');
            Route::get('settings', [AdminStoreSettingsController::class, 'show'])->name('settings.show');
            Route::get('shipping/zones', [AdminShippingZoneController::class, 'index'])->name('shipping.zones.index');
            Route::get('tax/settings', [AdminTaxSettingsController::class, 'show'])->name('tax.settings.show');
        });

        Route::middleware('admin.api:write-settings')->group(function (): void {
            Route::post('search/reindex', [AdminSearchIndexController::class, 'reindex'])->name('search.reindex');
            Route::put('settings', [AdminStoreSettingsController::class, 'update'])->name('settings.update');
            Route::post('shipping/zones', [AdminShippingZoneController::class, 'store'])->name('shipping.zones.store');
            Route::put('shipping/zones/{shippingZone}', [AdminShippingZoneController::class, 'update'])->name('shipping.zones.update');
            Route::post('shipping/zones/{shippingZone}/rates', [AdminShippingRateController::class, 'store'])->name('shipping.zones.rates.store');
            Route::put('tax/settings', [AdminTaxSettingsController::class, 'update'])->name('tax.settings.update');
        });

        Route::middleware('admin.api:write-themes')->group(function (): void {
            Route::post('themes', [AdminThemeController::class, 'store'])->name('themes.store');
            Route::post('themes/{theme}/publish', [AdminThemeController::class, 'publish'])->name('themes.publish');
            Route::put('themes/{theme}/settings', [AdminThemeSettingsController::class, 'update'])->name('themes.settings.update');
        });

        Route::middleware('admin.api:read-analytics')->group(function (): void {
            Route::get('analytics/summary', [AdminAnalyticsSummaryController::class, 'show'])->name('analytics.summary');
        });

        Route::middleware('admin.api:read-orders')->group(function (): void {
            Route::post('exports/orders', [AdminOrderExportController::class, 'store'])->name('exports.orders.store');
            Route::get('exports/{dataExport}', [AdminOrderExportController::class, 'show'])->name('exports.show');
            Route::get('orders', [AdminOrderController::class, 'index'])->name('orders.index');
            Route::get('orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
        });

        Route::middleware('admin.api:write-orders')->group(function (): void {
            Route::post('orders/{order}/fulfillments', [AdminOrderFulfillmentController::class, 'store'])->name('orders.fulfillments.store');
            Route::post('orders/{order}/refunds', [AdminOrderRefundController::class, 'store'])->name('orders.refunds.store');
        });
    });

Route::middleware('throttle:60,1')
    ->prefix('apps/v1/stores/{store}')
    ->name('api.apps.v1.')
    ->group(function (): void {
        Route::get('products', DeferredAppEndpointController::class)->name('products.index');
        Route::get('orders', DeferredAppEndpointController::class)->name('orders.index');
        Route::get('customers', DeferredAppEndpointController::class)->name('customers.index');
    });


output:
Chunk ID: cbe368
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2857
Output:

- **Authentication:** Sanctum token
- **Rate limit:** `api.admin` (60/min per token)

**Success response:** `200 OK`

```json
{
  "data": {
    "user_id": 3,
    "store_id": 1,
    "role": "admin",
    "email": "admin@acme.com",
    "name": "Jane Admin",
    "permissions": [
      "read-products",
      "write-products",
      "read-orders",
      "write-orders",
      "read-settings",
      "write-settings",
      "read-customers"
    ]
  }
}
```

**Error responses:**
- `403` - Not a member of this store
- `404` - Store not found

---

### 3.2 Products

#### Admin Products Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| GET | `/api/admin/v1/stores/{storeId}/products` | `read-products` | List products with filtering and pagination | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/products` | `write-products` | Create a new product | `api.admin` (60/min) |
| GET | `/api/admin/v1/stores/{storeId}/products/{productId}` | `read-products` | Get single product with all relations | `api.admin` (60/min) |
| PUT | `/api/admin/v1/stores/{storeId}/products/{productId}` | `write-products` | Update a product (partial updates supported) | `api.admin` (60/min) |
| DELETE | `/api/admin/v1/stores/{storeId}/products/{productId}` | `write-products` | Archive a product (soft delete) | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/products/{productId}/media/presign-upload` | `write-products` | Generate presigned URL for media upload | `api.admin` (60/min) |

#### GET /api/admin/v1/stores/{storeId}/products

List products with filtering and pagination.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `status` | string | No | Filter by status: `draft`, `active`, `archived`. |
| `query` | string | No | Search by title, vendor, or SKU. |
| `collection_id` | integer | No | Filter by collection membership. |
| `page` | integer | No | Page number. Default: 1. |
| `per_page` | integer | No | Results per page. Default: 25, max: 100. |
| `sort` | string | No | One of: `title_asc`, `title_desc`, `created_at_asc`, `created_at_desc`, `updated_at_desc`. Default: `updated_at_desc`. |

**Success response:** `200 OK`

```json
{
  "data": [
    {
      "id": 10,
      "store_id": 1,
      "title": "Classic T-Shirt",
      "handle": "classic-t-shirt",
      "status": "active",
      "vendor": "Acme Apparel",
      "product_type": "Apparel",
      "tags": ["organic", "cotton"],
      "variants_count": 6,
      "total_inventory": 150,
      "published_at": "2026-02-10T08:00:00Z",
      "created_at": "2026-02-09T14:00:00Z",
      "updated_at": "2026-02-11T09:00:00Z",
      "featured_image": {
        "url": "https://cdn.example.com/products/tshirt.jpg",
        "alt_text": "Classic T-Shirt in Blue"
      }
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 42,
    "last_page": 2
  }
}
```

---

#### POST /api/admin/v1/stores/{storeId}/products

Create a new product.

**Request body:**

```json
{
  "title": "Classic T-Shirt",
  "handle": "classic-t-shirt",
  "description_html": "<p>A comfortable cotton t-shirt.</p>",
  "vendor": "Acme Apparel",
  "product_type": "Apparel",
  "status": "draft",
  "tags": ["organic", "cotton"],
  "options": [
    { "name": "Color", "position": 1 },
    { "name": "Size", "position": 2 }
  ],
  "variants": [
    {
      "sku": "TSH-BLU-S",
      "barcode": "1234567890123",
      "price_amount": 2500,
      "compare_at_amount": 3500,
      "currency": "EUR",
      "weight_g": 200,
      "requires_shipping": true,
      "is_default": true,
      "position": 1,
      "status": "active",
      "option_values": [
        { "option_name": "Color", "value": "Blue" },
        { "option_name": "Size", "value": "Small" }
      ],
      "inventory": {
        "quantity_on_hand": 50,
        "policy": "deny"
      }
    }
  ],
  "collections": [5]
}
```

**Product fields:**

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `title` | string | Yes | Max: 255 |
| `handle` | string | No | Auto-generated from title if not provided. Unique per store. Lowercase, hyphens only. Max: 255. |
| `description_html` | string | No | Max: 65535 |
| `vendor` | string | No | Max: 255 |
| `product_type` | string | No | Max: 255 |
| `status` | string | No | One of: `draft`, `active`. Default: `draft`. |
| `tags` | array of strings | No | Max: 50 tags, each max: 255. |
| `options` | array | No | Max: 3 options. |
| `options[].name` | string | Yes | Max: 255 |
| `options[].position` | integer | Yes | Min: 1, max: 3 |
| `collections` | array of integers | No | Collection IDs to add this product to. |

**Variant fields:**

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `variants` | array | Yes | At least one variant required. Max: 100. |
| `variants[].sku` | string | Yes | Max: 255. Unique per store (`store_id`, `sku`). |
| `variants[].barcode` | string | No | Max: 255 |
| `variants[].price_amount` | integer | Yes | Min: 0 (in minor currency units) |
| `variants[].compare_at_amount` | integer | No | Min: 0. Must be > price_amount if set. |
| `variants[].currency` | string | No | Defaults to store currency. |
| `variants[].weight_g` | integer | No | Min: 0 |
| `variants[].requires_shipping` | boolean | No | Default: true |
| `variants[].is_default` | boolean | No | Exactly one variant must be default. |
| `variants[].position` | integer | No | Auto-assigned if not provided. |
| `variants[].status` | string | No | One of: `active`, `archived`. Default: `active`. |
| `variants[].option_values` | array | No | Must match defined options. |
| `variants[].inventory.quantity_on_hand` | integer | No | Default: 0. Min: 0. |
| `variants[].inventory.policy` | string | No | One of: `deny`, `continue`. Default: `deny`. |

**Success response:** `201 Created`

Returns the full product resource with nested variants, options, and media.

**Error responses:**
- `403` - Insufficient permissions
- `422` - Validation errors (duplicate SKU, duplicate handle, missing required fields)

---

#### GET /api/admin/v1/stores/{storeId}/products/{productId}

Get a single product with all variants, options, media, and collections.

**Success response:** `200 OK`

```json
{
  "data": {
    "id": 10,
    "store_id": 1,
    "title": "Classic T-Shirt",
    "handle": "classic-t-shirt",
    "description_html": "<p>A comfortable cotton t-shirt.</p>",
    "vendor": "Acme Apparel",
    "product_type": "Apparel",
    "status": "active",
    "tags": ["organic", "cotton"],
    "published_at": "2026-02-10T08:00:00Z",
    "created_at": "2026-02-09T14:00:00Z",
    "updated_at": "2026-02-11T09:00:00Z",
    "options": [
      {
        "id": 1,
        "name": "Color",
        "position": 1,
        "values": [
          { "id": 1, "value": "Blue", "position": 1 },
          { "id": 2, "value": "Red", "position": 2 }
        ]
      },
      {
        "id": 2,
        "name": "Size",
        "position": 2,
        "values": [
          { "id": 3, "value": "Small", "position": 1 },
          { "id": 4, "value": "Medium", "position": 2 },
          { "id": 5, "value": "Large", "position": 3 }
        ]
      }
    ],
    "variants": [
      {
        "id": 101,
        "sku": "TSH-BLU-S",
        "barcode": "1234567890123",
        "price_amount": 2500,
        "compare_at_amount": 3500,
        "currency": "EUR",
        "weight_g": 200,
        "requires_shipping": true,
        "is_default": true,
        "position": 1,
        "status": "active",
        "option_values": [
          { "option_name": "Color", "value": "Blue" },
          { "option_name": "Size", "value": "Small" }
        ],
        "inventory": {
          "quantity_on_hand": 50,
          "quantity_reserved": 2,
          "policy": "deny"
        }
      }
    ],
    "media": [
      {
        "id": 1,
        "type": "image",
        "storage_key": "stores/1/products/10/main.jpg",
        "url": "https://cdn.example.com/stores/1/products/10/main.jpg",
        "alt_text": "Classic T-Shirt in Blue",
        "width": 1200,
        "height": 1600,
        "mime_type": "image/jpeg",
        "byte_size": 245000,
        "position": 1,
        "status": "ready"
      }
    ],
    "collections": [
      {
        "id": 5,
        "title": "Summer Collection",
        "handle": "summer-collection"
      }
    ]
  }
}
```

**Error responses:**
- `403` - Insufficient permissions
- `404` - Product not found (or belongs to another store)

---

#### PUT /api/admin/v1/stores/{storeId}/products/{productId}

Update a product. Supports partial updates.

Same fields as POST. Only provided fields are updated. Variants can be added, updated (by ID), or removed.

**Example partial update request:**

```json
{
  "title": "Classic Cotton T-Shirt",
  "status": "active",
  "tags": ["organic", "cotton", "bestseller"]
}
```

**Success response:** `200 OK`

Returns the full updated product resource.

**Error responses:**
- `403` - Insufficient permissions
- `404` - Product not found
- `422` - Validation errors

---

#### DELETE /api/admin/v1/stores/{storeId}/products/{productId}

Archive a product (soft delete). Products with existing orders cannot be hard-deleted.

**Success response:** `200 OK`

```json
{
  "data": {
    "id": 10,
    "status": "archived",
    "updated_at": "2026-02-11T11:00:00Z"
  }
}
```

**Error responses:**
- `403` - Insufficient permissions
- `404` - Product not found

---

#### POST /api/admin/v1/stores/{storeId}/products/{productId}/media/presign-upload

Generate a presigned URL for uploading product media directly to object storage (S3).

**Request body:**

```json
{
  "filename": "tshirt-front.jpg",
  "content_type": "image/jpeg",
  "byte_size": 245000
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `filename` | string | Yes | Max: 255. Must have a valid extension. |
| `content_type` | string | Yes | One of: `image/jpeg`, `image/png`, `image/webp`, `image/avif`, `video/mp4`. |
| `byte_size` | integer | Yes | Min: 1, max: configurable (default 50MB for images, 500MB for video). |

**Success response:** `201 Created`

```json
{
  "upload_url": "https://s3.example.com/...presigned...",
  "method": "PUT",
  "headers": {
    "Content-Type": "image/jpeg"
  },
  "storage_key": "stores/1/products/10/media/uuid.jpg",
  "media_id": 2,
  "expires_at": "2026-02-11T10:10:00Z"
}
```

**Error responses:**
- `403` - Insufficient permissions
- `404` - Product not found
- `422` - Validation errors (invalid content type, file too large)

---

### 3.3 Collections

#### Admin Collections Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| GET | `/api/admin/v1/stores/{storeId}/collections` | `read-collections` | List collections | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/collections` | `write-collections` | Create a collection | `api.admin` (60/min) |
| PUT | `/api/admin/v1/stores/{storeId}/collections/{collectionId}` | `write-collections` | Update a collection | `api.admin` (60/min) |
| DELETE | `/api/admin/v1/stores/{storeId}/collections/{collectionId}` | `write-collections` | Delete a collection | `api.admin` (60/min) |