output:
Chunk ID: 1590f6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4747
Output:
# Shop Build Progress

## Objective

Build a complete, self-contained Laravel shop system from `specs/*`, with implemented acceptance criteria mapped to evidence, passing automated checks, verified browser flows, independent QA, and meaningful commits.

## Current Status

- Status: in progress
- Active slice: Phase 4 - Storefront cart/checkout API surface and remaining cart estimates, after UI connection
- Started from: Laravel Livewire/Fortify starter kit with no shop domain tables or models present
- Last updated: 2026-05-03

## Phased Plan

1. Foundation: SQLite config, tenancy tables/models/factories/seeders, store resolution middleware, store scoping, admin/customer auth foundations, policies, and Phase 1 tests.
2. Catalog: product, variant, inventory, collection, media schema/models/services, admin catalog CRUD, storefront browsing, and tests.
3. Storefront theme/layout: theme/page/navigation data, storefront layout, product/collection/search/page views, and smoke browser verification.
4. Cart/checkout/pricing: cart API/UI, discount, shipping, tax, checkout state machine, and tests.
5. Payments/orders/fulfillment: mock PSP, order creation, refunds, fulfillments, customer account order views, and tests.
6. Admin panel: dashboard and all admin list/form/detail pages from Spec 03, with Livewire/Flux interactions.
7. Search/analytics/apps/webhooks: SQLite search, event aggregation, developer token/webhook UI, delivery jobs, and tests.
8. Final verification: full Pest suite, Pint, frontend build, Playwright MCP customer/admin flows, independent QA, fixes, and completion audit.

## Acceptance Checklist

| Area | Criteria Source | Status | Evidence |
| --- | --- | --- | --- |
| Database schema | `specs/01-DATABASE-SCHEMA.md` | partial | Phase 1 tenancy/auth tables, Phase 2 catalog tables, Phase 3 theme/content/navigation tables, and Phase 4 cart/checkout/pricing tables are implemented: carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts. Order/payment/search/analytics/app tables are still missing. |
| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/search`, `/pages/{handle}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`. Storefront/admin API routes are still missing. |
| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin catalog shell now includes product index/form, collection index/form, and inventory list with auth protection, filtering, pagination, status changes, SKU uniqueness checks, and collection assignment. Order/customer/content/settings admin pages are still missing. |
| Storefront UI | `specs/04-STOREFRONT-UI.md` | partial | Storefront layout, home, collections index/detail, product detail, search, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus, cached theme settings, product add-to-cart persistence, cart drawer, cart page, and checkout address/shipping/discount/payment-selection UI render seeded data. Cart page shipping estimate and discount entry are still pending. |
| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `CartService`, `DiscountService`, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through payment selection, checkout expiration, abandoned cart cleanup, session cart lookup, customer login cart merge, and product add-to-cart mutations are implemented. Order/payment services are still missing. |
| Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout, customer guard/provider, customer login/register, login rate limiting, session hardening, and resource policies implemented. Customer password reset and Sanctum token auth still missing. |
| Seed/test data | `specs/07-SEEDERS-AND-TEST-DATA.md` | partial | Seeders create Acme Fashion and Acme Electronics stores/domains/settings/admin access, 6 collections, 25 products, 127 variants, 127 inventory rows, 206 variant option pivots, 2 themes, 6 theme files, 2 theme settings rows, 6 pages, 4 navigation menus, 17 navigation items, 2 tax settings rows, 2 shipping zones, 6 shipping rates, 6 discounts, and no product media/runtime carts. Order/payment seed data is still missing. |
| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing, product detail, add-to-cart, cart drawer/page, checkout through payment selection, search, DB-backed content pages, seeded navigation, admin product/collection/inventory pages, and auth flows without console warnings/errors on successful pages. Automated browser tests are still missing. |
| 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, and Phase 4 storefront cart/checkout UI through `payment_selected` are implemented. Phase 4 API/remaining cart estimates and Phase 5 orders/payments are next. |

## Verification Evidence

- 2026-05-03: `mcp__laravel_boost__.application_info` confirmed PHP 8.4, Laravel 12.51.0, Livewire 4.1.4, Flux 2.12.0, Pest 4.3.2, SQLite.
- 2026-05-03: `mcp__laravel_boost__.database_schema(summary: true)` returned no tables for the current configured database.
- 2026-05-03: `git status --short` showed no current worktree changes before implementation.
- 2026-05-03: `mcp__laravel_boost__.search_docs` consulted Laravel 12 middleware/auth/authorization/migration docs plus Livewire 4 and Pest 4 docs before code changes.
- 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.
- 2026-05-03: `php artisan migrate:fresh --seed --no-interaction` passed on real `database/database.sqlite` after creating the missing SQLite file.
- 2026-05-03: `php artisan test --compact` passed: 45 tests, 121 assertions.
- 2026-05-03: `vendor/bin/pint --dirty --format agent` passed after formatting changes.
- 2026-05-03: `php artisan route:list --path=admin` showed `/admin`, `/admin/login`, `/admin/logout`; `php artisan route:list --path=account` showed `/account`, `/account/login`, `/account/register`.
- 2026-05-03: Boost schema summary confirmed Phase 1 tables: organizations, stores, store_domains, store_users, store_settings, customers, customer_password_reset_tokens, users with password_hash.
- 2026-05-03: Playwright MCP verified `http://shop.test/` loads, `http://shop.test/admin/login` renders, admin login with `admin@acme.test` / `password` reaches `/admin`, `http://shop.test/account/login` renders with no console warnings/errors after the Livewire layout fix, and customer login with `customer@acme.test` / `password` reaches `/account`.
- 2026-05-03: Independent Phase 2 QA agents reported no critical findings. High findings on lifecycle bypass, child tenant scoping, inventory store mismatch, variant option mapping, simplified seed data, and media status-only processing were addressed in the catalog data layer.
- 2026-05-03: `php artisan test --compact tests/Feature/Catalog` passed: 19 tests, 71 assertions.
- 2026-05-03: `php artisan test --compact` passed: 64 tests, 192 assertions.
- 2026-05-03: `vendor/bin/pint --dirty --format agent` fixed import/order/spacing, followed by passing catalog and full Pest suites.
- 2026-05-03: `php artisan migrate:fresh --seed --no-interaction` passed after the Phase 2 seed update.
- 2026-05-03: Boost schema summary confirmed Phase 2 catalog tables. Boost query counts after fresh seed: stores 2, products 25, variants 127, inventory_items 127, collections 6, product_media 0, variant_option_values 206.
- 2026-05-03: `mcp__laravel_boost__.search_docs` consulted Livewire 4 full-page component routing/testing docs and Flux UI form/input/select/checkbox/button docs before catalog UI changes.
- 2026-05-03: `php artisan route:list --path=admin` showed 10 admin routes including product, collection, and inventory catalog pages. `php artisan route:list --path=collections`, `--path=products`, `--path=search`, and `--path=pages` showed storefront catalog/content routes.
- 2026-05-03: `php artisan test --compact tests/Feature/Catalog/CatalogUiTest.php` passed: 4 tests, 44 assertions.
- 2026-05-03: `php artisan test --compact tests/Feature/Catalog` passed: 23 tests, 115 assertions.
- 2026-05-03: `php artisan test --compact` passed: 68 tests, 236 assertions.
- 2026-05-03: `vendor/bin/pint --dirty --format agent` passed after formatting catalog UI changes.
- 2026-05-03: `php artisan migrate:fresh --seed --no-interaction` passed after catalog UI test changes.
- 2026-05-03: `npm run build` passed with Vite production assets generated under ignored `public/build`.
- 2026-05-03: Playwright MCP verified `http://shop.test/`, `/collections/t-shirts`, `/products/classic-cotton-t-shirt`, `/search?q=Cotton`, `/pages/about`, `/admin/products`, `/admin/products/create`, `/admin/collections`, and `/admin/inventory` render with no console warnings/errors.
- 2026-05-03: `mcp__laravel_boost__.search_docs` consulted Laravel 12 migration/model/factory/seeder/cache docs and Pest 4 database testing docs before Phase 3 data-layer changes.
- 2026-05-03: Phase 3 explorer QA confirmed the required theme/page/navigation migrations, models, enums, factories, seeders, services, and tests; noted the spec tree-navigation mismatch because `navigation_items` has no `parent_id`.
- 2026-05-03: `php artisan migrate:fresh --seed --no-interaction` passed after adding Phase 3 migrations and seeders.
- 2026-05-03: Boost schema summaries confirmed Phase 3 tables: themes, theme_files, theme_settings, pages, navigation_menus, navigation_items. Boost query counts after fresh seed: themes 2, theme_files 6, theme_settings 2, pages 6, navigation_menus 4, navigation_items 17.
- 2026-05-03: `php artisan test --compact tests/Feature/Storefront/ThemeDataTest.php` passed: 6 tests, 31 assertions.
- 2026-05-03: `php artisan test --compact tests/Feature/Storefront tests/Feature/Catalog` passed: 29 tests, 146 assertions.
- 2026-05-03: `php artisan test --compact` passed: 74 tests, 267 assertions.
- 2026-05-03: `vendor/bin/pint --dirty --format agent` passed after Phase 3 changes.
- 2026-05-03: `npm run build` passed after Phase 3 layout changes.
- 2026-05-03: Playwright MCP verified `http://shop.test/` renders seeded main navigation and `http://shop.test/pages/faq` renders DB-backed page content with no console warnings/errors.
- 2026-05-03: `mcp__laravel_boost__.search_docs` consulted Laravel 12 migration/job/scheduling/transaction docs and Pest 4 database testing docs before Phase 4 backend changes.
- 2026-05-03: Phase 4 explorer QA mapped the cart, checkout, discount, shipping, tax, pricing, and cleanup-job boundary; order creation/payment capture were kept for Phase 5 because order/payment tables are not implemented yet.
- 2026-05-03: `php artisan migrate:fresh --seed --no-interaction` passed after adding Phase 4 migrations and seeders.
- 2026-05-03: Boost schema summary confirmed Phase 4 tables: carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts. Boost query counts after fresh seed: carts 0, cart_lines 0, checkouts 0, shipping_zones 2, shipping_rates 6, tax_settings 2, discounts 6.
- 2026-05-03: `php artisan test --compact tests/Feature/Cart tests/Feature/Checkout` passed: 11 tests, 55 assertions.
- 2026-05-03: `vendor/bin/pint --dirty --format agent` fixed checkout/pricing style and import order, then passed after job test coverage was added.
- 2026-05-03: `php artisan test --compact` passed: 85 tests, 322 assertions.
- 2026-05-03: `mcp__laravel_boost__.search_docs` consulted Livewire 4 full-page components, forms, validation, session, events, redirects, Flux modal/form controls, and Pest docs before Phase 4 cart/checkout UI changes.
- 2026-05-03: `php artisan route:list --path=cart` and `php artisan route:list --path=checkout` confirmed `/cart` and `/checkout` Livewire routes.
- 2026-05-03: `php artisan test --compact tests/Feature/Storefront/CartCheckoutUiTest.php` passed: 4 tests, 19 assertions.
- 2026-05-03: `php artisan test --compact tests/Feature/Storefront tests/Feature/Cart tests/Feature/Checkout tests/Feature/Catalog tests/Feature/Foundation/CustomerAuthTest.php` passed: 47 tests, 236 assertions.
- 2026-05-03: `vendor/bin/pint --dirty --format agent` passed after Phase 4 UI changes.
- 2026-05-03: `php artisan test --compact` passed: 89 tests, 341 assertions.
- 2026-05-03: `php artisan migrate:fresh --seed --no-interaction` passed after the Phase 4 UI changes.
- 2026-05-03: `npm run build` passed after the cart/checkout Blade changes.
- 2026-05-03: Playwright MCP verified `http://shop.test/products/classic-cotton-t-shirt` add-to-cart, `http://shop.test/cart`, and `http://shop.test/checkout` render and progress through shipping, discount, and reserve-items payment selection with Livewire update requests returning 200 and no new console warnings/errors. Missing favicon links were fixed with `public/favicon.svg`.

## Decisions

- Follow the roadmap order strictly. Phase 1 is required before catalog, checkout, admin, storefront, and QA flows can be meaningfully implemented.
- Preserve the starter Fortify conventions where they do not conflict with the shop specs.
- Use integer minor units for all money columns and JSON text columns cast through Eloquent as specified.
- Keep `password_hash` as the database column for admin users and customers while exposing `password`/`getAuthPassword()` at the model layer for Fortify/Laravel auth compatibility.
- Seed both `shop.test` and `acme-fashion.test` as storefront domains for the first store so the goal URL and E2E spec URL can both resolve.
- Customer Livewire auth components persist `storeId` from the initial storefront request because Livewire update requests do not run through the original storefront route middleware.
- The seed specification contains an arithmetic conflict: Acme Fashion's per-product variant table sums to 117 variants, while the prose says 107. The implementation follows the concrete per-product table, resulting in 117 Fashion variants and 10 Electronics variants, 127 total.
- Child catalog models without a `store_id` column are tenant-scoped through their parent product relationship. Inventory keeps its denormalized `store_id` and enforces that it matches the variant product store at save time.
- Storefront content pages now resolve from the `pages` table and only published pages are rendered.
- The catalog admin form intentionally blocks active products without a priced variant and duplicate in-store SKUs during UI edits, mirroring service-layer invariants where the current CRUD surface touches product variants directly.
- Navigation tree support is flat for now because the Phase 3 schema defines `navigation_items.position` but no `parent_id`; `NavigationService::buildTree()` returns a flat tree-compatible array with empty children.
- Cart, checkout, and pricing runtime records are not seeded; only deterministic tax, shipping, and discount configuration is seeded. Runtime carts/checkouts are created by services and tests.
- Checkout completion intentionally stops before order creation in this slice. `CheckoutService::completeCheckout()` is a Phase 5 boundary until order, payment, and fulfillment tables/services exist.
- The cart drawer reads the current session/customer cart without creating an empty cart on every storefront render; carts are created on first add-to-cart or checkout/cart service mutation.
- The checkout UI currently reserves inventory by selecting a payment method and leaves payment capture/order creation for Phase 5.

## Open Issues

- Composer does not currently include Sanctum, while Spec 06 requires Sanctum personal access tokens. This will need either an approved dependency addition or a documented compatible alternative before final completion.
- The Herd app URL in the goal is `http://shop.test/`, while the E2E spec also references `http://acme-fashion.test`; domain handling must support seeded store domains and Herd verification.
- Phase 1 still lacks admin/customer password reset at the spec paths and a custom customer password reset token repository that scopes by store_id.
- Policy classes for later resources use generic object parameters until the concrete catalog/order/content models exist.
- `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
- Phase 2 media upload/admin UI is still missing, even though the media schema/job layer exists.
- Product options can be displayed and edited as text in the admin form, but full option matrix generation and option-value reassignment UI are not complete.
- Theme/page/navigation admin management UI is still missing even though the Phase 3 data layer, seeders, services, and storefront consumption exist.
- Storefront cart page shipping estimate and cart-level discount entry are still missing; discounts can be applied during checkout.
- Storefront cart and checkout API endpoints are still missing.
- Checkout order creation, payment capture, discount usage-count increment on successful order completion, and fulfillment are still pending Phase 5.
- Automated browser tests from Spec 08 are still missing; current browser coverage is manual Playwright MCP smoke verification.
- SQLite enum/check constraints from the schema spec are not yet explicitly enforced as database `CHECK` constraints; enum validation is currently enforced through casts/services/model invariants.
- Product media processing now validates and resizes image uploads with GD and cleans predictable generated paths, but browser/admin upload UI is still pending.

## Completion Summary

Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, and cart/checkout storefront UI through payment selection are implemented, with known auth/token, media UI, theme admin UI, cart/checkout API, cart estimates, order/payment, browser-test, and database CHECK constraint gaps tracked above.


cmd: sed -n '209,420p' specs/02-API-ROUTES.md


cmd: sed -n '458,790p' specs/02-API-ROUTES.md


cmd: sed -n '790,860p' specs/02-API-ROUTES.md


command: sed -n '209,420p' specs/02-API-ROUTES.md

type: read

aggregated_output:
#### POST /api/storefront/v1/carts

Create a new cart.

- **Authentication:** None (guest) or session-based (logged-in customer)
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "currency": "EUR"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `currency` | string | No | Valid ISO 4217 code. Defaults to store's `default_currency`. |

**Success response:** `201 Created`

```json
{
  "id": 42,
  "store_id": 1,
  "customer_id": null,
  "currency": "EUR",
  "cart_version": 1,
  "status": "active",
  "lines": [],
  "totals": {
    "subtotal": 0,
    "discount": 0,
    "total": 0,
    "currency": "EUR"
  },
  "created_at": "2026-02-11T10:00:00Z",
  "updated_at": "2026-02-11T10:00:00Z"
}
```

**Error responses:**
- `422` - Validation error (invalid currency)

---

#### GET /api/storefront/v1/carts/{cartId}

Retrieve a cart with all lines and computed totals.

- **Authentication:** None (cart ID acts as token)
- **Rate limit:** `api.storefront` (120/min per IP)

**URL parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `cartId` | integer | Yes | The cart identifier |

**Success response:** `200 OK`

```json
{
  "id": 42,
  "store_id": 1,
  "customer_id": null,
  "currency": "EUR",
  "cart_version": 3,
  "status": "active",
  "lines": [
    {
      "id": 201,
      "variant_id": 101,
      "product_title": "Classic T-Shirt",
      "variant_title": "Blue / Medium",
      "sku": "TSH-BLU-M",
      "quantity": 2,
      "unit_price_amount": 2500,
      "line_subtotal_amount": 5000,
      "line_discount_amount": 0,
      "line_total_amount": 5000,
      "image_url": "https://cdn.example.com/products/tshirt-blue.jpg",
      "requires_shipping": true,
      "available_quantity": 50
    }
  ],
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "total": 5000,
    "currency": "EUR",
    "line_count": 1,
    "item_count": 2
  },
  "created_at": "2026-02-11T10:00:00Z",
  "updated_at": "2026-02-11T10:05:00Z"
}
```

**Error responses:**
- `404` - Cart not found

---

#### POST /api/storefront/v1/carts/{cartId}/lines

Add a line item to the cart.

- **Authentication:** None
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "variant_id": 101,
  "quantity": 2
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `variant_id` | integer | Yes | Must exist, belong to an active product in the current store |
| `quantity` | integer | Yes | Min: 1, max: 9999. Must not exceed available inventory when policy is `deny`. |

**Success response:** `201 Created`

Returns the full cart object (same schema as GET /carts/{cartId}).

**Error responses:**
- `404` - Cart not found
- `409` - Cart version conflict (concurrent modification, retry with fresh cart_version)
- `422` - Validation error

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "variant_id": ["The selected variant is out of stock."]
  }
}
```

---

#### PUT /api/storefront/v1/carts/{cartId}/lines/{lineId}

Update the quantity of a cart line.

- **Authentication:** None
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "quantity": 3,
  "cart_version": 3
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `quantity` | integer | Yes | Min: 1, max: 9999. Must not exceed available inventory when policy is `deny`. |
| `cart_version` | integer | Yes | Must match current cart version for optimistic concurrency. |

**Success response:** `200 OK`

Returns the full cart object with updated totals.

**Error responses:**
- `404` - Cart or line not found
- `409` - Version conflict
- `422` - Validation error (e.g., exceeds inventory)

---

#### DELETE /api/storefront/v1/carts/{cartId}/lines/{lineId}

Remove a line item from the cart.

- **Authentication:** None
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "cart_version": 3
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `cart_version` | integer | Yes | Must match current cart version. |

**Success response:** `200 OK`

Returns the full cart object.

**Error responses:**
- `404` - Cart or line not found
- `409` - Version conflict

---

### 2.2 Checkout Endpoints

#### Checkout Flow Sequence Diagram

```mermaid
sequenceDiagram

status: completed


command: sed -n '790,860p' specs/02-API-ROUTES.md

aggregated_output:
  "id": 7,
  "status": "shipping_selected",
  "discount_code": "WELCOME10",
  "totals": {
    "subtotal": 5000,
    "discount": 500,
    "shipping": 500,
    "tax": 950,
    "total": 5950,
    "currency": "EUR"
  },
  "applied_discounts": [
    {
      "code": "WELCOME10",
      "type": "percent",
      "value_amount": 10,
      "applied_amount": 500,
      "description": "10% off your first order"
    }
  ]
}
```

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

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

---

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

Remove the applied discount code from the checkout.

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

**Success response:** `200 OK`

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

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

---

### 2.3 Payment Endpoint

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

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

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

**Request body (Credit Card):**

```json
{
  "payment_method": "credit_card",
  "card_number": "4242424242424242",


command: sed -n '458,790p' specs/02-API-ROUTES.md

aggregated_output:
#### POST /api/storefront/v1/checkouts

Create a checkout from a cart.

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

**Request body:**

```json
{
  "cart_id": 42,
  "email": "customer@example.com"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `cart_id` | integer | Yes | Must reference an active cart with at least one line. |
| `email` | string | Yes | Valid email address. |

**Success response:** `201 Created`

```json
{
  "id": 7,
  "store_id": 1,
  "cart_id": 42,
  "customer_id": null,
  "status": "started",
  "email": "customer@example.com",
  "shipping_address_json": null,
  "billing_address_json": null,
  "shipping_method_id": null,
  "discount_code": null,
  "lines": [
    {
      "variant_id": 101,
      "product_title": "Classic T-Shirt",
      "variant_title": "Blue / Medium",
      "sku": "TSH-BLU-M",
      "quantity": 2,
      "unit_price_amount": 2500,
      "line_total_amount": 5000
    }
  ],
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "shipping": 0,
    "tax": 0,
    "total": 5000,
    "currency": "EUR"
  },
  "available_shipping_methods": [],
  "expires_at": "2026-02-12T10:00:00Z",
  "created_at": "2026-02-11T10:00:00Z"
}
```

**Error responses:**
- `404` - Cart not found
- `422` - Validation errors (empty cart, invalid email)

---

#### GET /api/storefront/v1/checkouts/{checkoutId}

Retrieve the current checkout state and computed totals.

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

**Success response:** `200 OK`

Returns the full checkout object (same schema as POST response, with updated fields reflecting the current state).

**Error responses:**
- `404` - Checkout not found
- `410` - Checkout expired

---

#### PUT /api/storefront/v1/checkouts/{checkoutId}/address

Set the shipping and/or billing address on a checkout.

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

**Request body:**

```json
{
  "shipping_address": {
    "first_name": "Jane",
    "last_name": "Doe",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Berlin",
    "province": "Berlin",
    "province_code": "BE",
    "country": "DE",
    "country_code": "DE",
    "postal_code": "10115",
    "phone": "+49301234567"
  },
  "billing_address": null,
  "use_shipping_as_billing": true
}
```

**Shipping address fields:**

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `shipping_address` | object | Yes (if any line requires shipping) | See fields below |
| `shipping_address.first_name` | string | Yes | Max: 255 |
| `shipping_address.last_name` | string | Yes | Max: 255 |
| `shipping_address.address1` | string | Yes | Max: 500 |
| `shipping_address.address2` | string | No | Max: 500 |
| `shipping_address.city` | string | Yes | Max: 255 |
| `shipping_address.province` | string | No | Max: 255 |
| `shipping_address.province_code` | string | No | Max: 10 |
| `shipping_address.country` | string | Yes | Max: 255 |
| `shipping_address.country_code` | string | Yes | Valid ISO 3166-1 alpha-2 |
| `shipping_address.postal_code` | string | Yes | Max: 20 |
| `shipping_address.phone` | string | No | Max: 50 |
| `billing_address` | object | No | Same fields as shipping_address |
| `use_shipping_as_billing` | boolean | No | Default: true. If true, billing_address is ignored and copied from shipping. |

**Success response:** `200 OK`

Returns the full checkout object. Status transitions to `addressed`. The response includes `available_shipping_methods` populated based on the shipping address and store's shipping zones.

```json
{
  "id": 7,
  "status": "addressed",
  "shipping_address_json": {
    "first_name": "Jane",
    "last_name": "Doe",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Berlin",
    "province": "Berlin",
    "province_code": "BE",
    "country": "DE",
    "country_code": "DE",
    "postal_code": "10115",
    "phone": "+49301234567"
  },
  "billing_address_json": {
    "first_name": "Jane",
    "last_name": "Doe",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Berlin",
    "province": "Berlin",
    "province_code": "BE",
    "country": "DE",
    "country_code": "DE",
    "postal_code": "10115",
    "phone": "+49301234567"
  },
  "available_shipping_methods": [
    {
      "id": 1,
      "name": "Standard Shipping",
      "type": "flat",
      "price_amount": 500,
      "currency": "EUR",
      "estimated_days_min": 3,
      "estimated_days_max": 5
    },
    {
      "id": 2,
      "name": "Express Shipping",
      "type": "flat",
      "price_amount": 1200,
      "currency": "EUR",
      "estimated_days_min": 1,
      "estimated_days_max": 2
    }
  ],
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "shipping": 0,
    "tax": 0,
    "total": 5000,
    "currency": "EUR"
  }
}
```

**Error responses:**
- `404` - Checkout not found
- `410` - Checkout expired
- `422` - Validation errors (missing required fields, invalid country code, address not in any shipping zone)

---

#### PUT /api/storefront/v1/checkouts/{checkoutId}/shipping-method

Select a shipping method for the checkout.

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

**Request body:**

```json
{
  "shipping_method_id": 1
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `shipping_method_id` | integer | Yes | Must be one of the available shipping methods for this checkout's address. |

**Success response:** `200 OK`

Returns the full checkout object. Status transitions to `shipping_selected`. Totals are recalculated including the shipping cost and taxes (if applicable).

```json
{
  "id": 7,
  "status": "shipping_selected",
  "shipping_method_id": 1,
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "shipping": 500,
    "tax": 1045,
    "total": 6545,
    "currency": "EUR"
  },
  "tax_provider_snapshot_json": {
    "provider": "manual",
    "calculated_at": "2026-02-11T10:05:00Z",
    "lines": [
      {
        "variant_id": 101,
        "tax_amount": 950,
        "rate": 1900,
        "jurisdiction": "DE"
      }
    ],
    "shipping_tax_amount": 95,
    "shipping_tax_rate": 1900
  }
}
```

**Error responses:**
- `404` - Checkout not found
- `410` - Checkout expired
- `422` - Invalid shipping method (not available for this address/checkout)

---

#### PUT /api/storefront/v1/checkouts/{checkoutId}/payment-method

Select a payment method for the checkout. This step records the chosen method without processing payment.

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

**Request body:**

```json
{
  "payment_method": "credit_card"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `payment_method` | string | Yes | Must be one of: `credit_card`, `paypal`, `bank_transfer`. |

**Success response:** `200 OK`

Returns the full checkout object. Status transitions to `payment_selected`.

```json
{
  "id": 7,
  "status": "payment_selected",
  "payment_method": "credit_card",
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "shipping": 500,
    "tax": 1045,
    "total": 6545,
    "currency": "EUR"
  }
}
```

**Error responses:**
- `404` - Checkout not found
- `410` - Checkout expired
- `422` - Invalid payment method or checkout not in `shipping_selected` state

---

#### POST /api/storefront/v1/checkouts/{checkoutId}/apply-discount

Apply a discount code to the checkout.

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

**Request body:**

```json
{
  "code": "WELCOME10"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `code` | string | Yes | Max: 50. Must be a valid, active discount code for this store. Case-insensitivity determined by store setting. |

**Success response:** `200 OK`

```json
{
  "id": 7,


output:
Chunk ID: c64956
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1112
Output:
#### POST /api/storefront/v1/carts

Create a new cart.

- **Authentication:** None (guest) or session-based (logged-in customer)
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "currency": "EUR"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `currency` | string | No | Valid ISO 4217 code. Defaults to store's `default_currency`. |

**Success response:** `201 Created`

```json
{
  "id": 42,
  "store_id": 1,
  "customer_id": null,
  "currency": "EUR",
  "cart_version": 1,
  "status": "active",
  "lines": [],
  "totals": {
    "subtotal": 0,
    "discount": 0,
    "total": 0,
    "currency": "EUR"
  },
  "created_at": "2026-02-11T10:00:00Z",
  "updated_at": "2026-02-11T10:00:00Z"
}
```

**Error responses:**
- `422` - Validation error (invalid currency)

---

#### GET /api/storefront/v1/carts/{cartId}

Retrieve a cart with all lines and computed totals.

- **Authentication:** None (cart ID acts as token)
- **Rate limit:** `api.storefront` (120/min per IP)

**URL parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `cartId` | integer | Yes | The cart identifier |

**Success response:** `200 OK`

```json
{
  "id": 42,
  "store_id": 1,
  "customer_id": null,
  "currency": "EUR",
  "cart_version": 3,
  "status": "active",
  "lines": [
    {
      "id": 201,
      "variant_id": 101,
      "product_title": "Classic T-Shirt",
      "variant_title": "Blue / Medium",
      "sku": "TSH-BLU-M",
      "quantity": 2,
      "unit_price_amount": 2500,
      "line_subtotal_amount": 5000,
      "line_discount_amount": 0,
      "line_total_amount": 5000,
      "image_url": "https://cdn.example.com/products/tshirt-blue.jpg",
      "requires_shipping": true,
      "available_quantity": 50
    }
  ],
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "total": 5000,
    "currency": "EUR",
    "line_count": 1,
    "item_count": 2
  },
  "created_at": "2026-02-11T10:00:00Z",
  "updated_at": "2026-02-11T10:05:00Z"
}
```

**Error responses:**
- `404` - Cart not found

---

#### POST /api/storefront/v1/carts/{cartId}/lines

Add a line item to the cart.

- **Authentication:** None
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "variant_id": 101,
  "quantity": 2
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `variant_id` | integer | Yes | Must exist, belong to an active product in the current store |
| `quantity` | integer | Yes | Min: 1, max: 9999. Must not exceed available inventory when policy is `deny`. |

**Success response:** `201 Created`

Returns the full cart object (same schema as GET /carts/{cartId}).

**Error responses:**
- `404` - Cart not found
- `409` - Cart version conflict (concurrent modification, retry with fresh cart_version)
- `422` - Validation error

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "variant_id": ["The selected variant is out of stock."]
  }
}
```

---

#### PUT /api/storefront/v1/carts/{cartId}/lines/{lineId}

Update the quantity of a cart line.

- **Authentication:** None
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "quantity": 3,
  "cart_version": 3
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `quantity` | integer | Yes | Min: 1, max: 9999. Must not exceed available inventory when policy is `deny`. |
| `cart_version` | integer | Yes | Must match current cart version for optimistic concurrency. |

**Success response:** `200 OK`

Returns the full cart object with updated totals.

**Error responses:**
- `404` - Cart or line not found
- `409` - Version conflict
- `422` - Validation error (e.g., exceeds inventory)

---

#### DELETE /api/storefront/v1/carts/{cartId}/lines/{lineId}

Remove a line item from the cart.

- **Authentication:** None
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "cart_version": 3
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `cart_version` | integer | Yes | Must match current cart version. |

**Success response:** `200 OK`

Returns the full cart object.

**Error responses:**
- `404` - Cart or line not found
- `409` - Version conflict

---

### 2.2 Checkout Endpoints

#### Checkout Flow Sequence Diagram

```mermaid
sequenceDiagram


output:
Chunk ID: b48a8a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2004
Output:
#### POST /api/storefront/v1/checkouts

Create a checkout from a cart.

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

**Request body:**

```json
{
  "cart_id": 42,
  "email": "customer@example.com"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `cart_id` | integer | Yes | Must reference an active cart with at least one line. |
| `email` | string | Yes | Valid email address. |

**Success response:** `201 Created`

```json
{
  "id": 7,
  "store_id": 1,
  "cart_id": 42,
  "customer_id": null,
  "status": "started",
  "email": "customer@example.com",
  "shipping_address_json": null,
  "billing_address_json": null,
  "shipping_method_id": null,
  "discount_code": null,
  "lines": [
    {
      "variant_id": 101,
      "product_title": "Classic T-Shirt",
      "variant_title": "Blue / Medium",
      "sku": "TSH-BLU-M",
      "quantity": 2,
      "unit_price_amount": 2500,
      "line_total_amount": 5000
    }
  ],
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "shipping": 0,
    "tax": 0,
    "total": 5000,
    "currency": "EUR"
  },
  "available_shipping_methods": [],
  "expires_at": "2026-02-12T10:00:00Z",
  "created_at": "2026-02-11T10:00:00Z"
}
```

**Error responses:**
- `404` - Cart not found
- `422` - Validation errors (empty cart, invalid email)

---

#### GET /api/storefront/v1/checkouts/{checkoutId}

Retrieve the current checkout state and computed totals.

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

**Success response:** `200 OK`

Returns the full checkout object (same schema as POST response, with updated fields reflecting the current state).

**Error responses:**
- `404` - Checkout not found
- `410` - Checkout expired

---

#### PUT /api/storefront/v1/checkouts/{checkoutId}/address

Set the shipping and/or billing address on a checkout.

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

**Request body:**

```json
{
  "shipping_address": {
    "first_name": "Jane",
    "last_name": "Doe",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Berlin",
    "province": "Berlin",
    "province_code": "BE",
    "country": "DE",
    "country_code": "DE",
    "postal_code": "10115",
    "phone": "+49301234567"
  },
  "billing_address": null,
  "use_shipping_as_billing": true
}
```

**Shipping address fields:**

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `shipping_address` | object | Yes (if any line requires shipping) | See fields below |
| `shipping_address.first_name` | string | Yes | Max: 255 |
| `shipping_address.last_name` | string | Yes | Max: 255 |
| `shipping_address.address1` | string | Yes | Max: 500 |
| `shipping_address.address2` | string | No | Max: 500 |
| `shipping_address.city` | string | Yes | Max: 255 |
| `shipping_address.province` | string | No | Max: 255 |
| `shipping_address.province_code` | string | No | Max: 10 |
| `shipping_address.country` | string | Yes | Max: 255 |
| `shipping_address.country_code` | string | Yes | Valid ISO 3166-1 alpha-2 |
| `shipping_address.postal_code` | string | Yes | Max: 20 |
| `shipping_address.phone` | string | No | Max: 50 |
| `billing_address` | object | No | Same fields as shipping_address |
| `use_shipping_as_billing` | boolean | No | Default: true. If true, billing_address is ignored and copied from shipping. |

**Success response:** `200 OK`

Returns the full checkout object. Status transitions to `addressed`. The response includes `available_shipping_methods` populated based on the shipping address and store's shipping zones.

```json
{
  "id": 7,
  "status": "addressed",
  "shipping_address_json": {
    "first_name": "Jane",
    "last_name": "Doe",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Berlin",
    "province": "Berlin",
    "province_code": "BE",
    "country": "DE",
    "country_code": "DE",
    "postal_code": "10115",
    "phone": "+49301234567"
  },
  "billing_address_json": {
    "first_name": "Jane",
    "last_name": "Doe",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Berlin",
    "province": "Berlin",
    "province_code": "BE",
    "country": "DE",
    "country_code": "DE",
    "postal_code": "10115",
    "phone": "+49301234567"
  },
  "available_shipping_methods": [
    {
      "id": 1,
      "name": "Standard Shipping",
      "type": "flat",
      "price_amount": 500,
      "currency": "EUR",
      "estimated_days_min": 3,
      "estimated_days_max": 5
    },
    {
      "id": 2,
      "name": "Express Shipping",
      "type": "flat",
      "price_amount": 1200,
      "currency": "EUR",
      "estimated_days_min": 1,
      "estimated_days_max": 2
    }
  ],
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "shipping": 0,
    "tax": 0,
    "total": 5000,
    "currency": "EUR"
  }
}
```

**Error responses:**
- `404` - Checkout not found
- `410` - Checkout expired
- `422` - Validation errors (missing required fields, invalid country code, address not in any shipping zone)

---

#### PUT /api/storefront/v1/checkouts/{checkoutId}/shipping-method

Select a shipping method for the checkout.

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

**Request body:**

```json
{
  "shipping_method_id": 1
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `shipping_method_id` | integer | Yes | Must be one of the available shipping methods for this checkout's address. |

**Success response:** `200 OK`

Returns the full checkout object. Status transitions to `shipping_selected`. Totals are recalculated including the shipping cost and taxes (if applicable).

```json
{
  "id": 7,
  "status": "shipping_selected",
  "shipping_method_id": 1,
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "shipping": 500,
    "tax": 1045,
    "total": 6545,
    "currency": "EUR"
  },
  "tax_provider_snapshot_json": {
    "provider": "manual",
    "calculated_at": "2026-02-11T10:05:00Z",
    "lines": [
      {
        "variant_id": 101,
        "tax_amount": 950,
        "rate": 1900,
        "jurisdiction": "DE"
      }
    ],
    "shipping_tax_amount": 95,
    "shipping_tax_rate": 1900
  }
}
```

**Error responses:**
- `404` - Checkout not found
- `410` - Checkout expired
- `422` - Invalid shipping method (not available for this address/checkout)

---

#### PUT /api/storefront/v1/checkouts/{checkoutId}/payment-method

Select a payment method for the checkout. This step records the chosen method without processing payment.

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

**Request body:**

```json
{
  "payment_method": "credit_card"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `payment_method` | string | Yes | Must be one of: `credit_card`, `paypal`, `bank_transfer`. |

**Success response:** `200 OK`

Returns the full checkout object. Status transitions to `payment_selected`.

```json
{
  "id": 7,
  "status": "payment_selected",
  "payment_method": "credit_card",
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "shipping": 500,
    "tax": 1045,
    "total": 6545,
    "currency": "EUR"
  }
}
```

**Error responses:**
- `404` - Checkout not found
- `410` - Checkout expired
- `422` - Invalid payment method or checkout not in `shipping_selected` state

---

#### POST /api/storefront/v1/checkouts/{checkoutId}/apply-discount

Apply a discount code to the checkout.

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

**Request body:**

```json
{
  "code": "WELCOME10"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `code` | string | Yes | Max: 50. Must be a valid, active discount code for this store. Case-insensitivity determined by store setting. |

**Success response:** `200 OK`

```json
{
  "id": 7,


output:
Chunk ID: 4e95ed
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 420
Output:
  "id": 7,
  "status": "shipping_selected",
  "discount_code": "WELCOME10",
  "totals": {
    "subtotal": 5000,
    "discount": 500,
    "shipping": 500,
    "tax": 950,
    "total": 5950,
    "currency": "EUR"
  },
  "applied_discounts": [
    {
      "code": "WELCOME10",
      "type": "percent",
      "value_amount": 10,
      "applied_amount": 500,
      "description": "10% off your first order"
    }
  ]
}
```

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

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

---

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

Remove the applied discount code from the checkout.

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

**Success response:** `200 OK`

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

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

---

### 2.3 Payment Endpoint

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

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

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

**Request body (Credit Card):**

```json
{
  "payment_method": "credit_card",
  "card_number": "4242424242424242",


cmd: sed -n '1,240p' app/Models/ShippingRate.php


cmd: sed -n '1,220p' database/seeders/DiscountSeeder.php && sed -n '1,220p' database/seeders/ShippingRateSeeder.php


command:
/bin/zsh
-lc
sed -n '1,240p' app/Models/Cart.php

Cart.php

cmd: sed -n '1,240p' app/Models/Cart.php

aggregated_output:
<?php

namespace App\Models;

use App\Enums\CartStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Cart extends Model
{
    /** @use HasFactory<\Database\Factories\CartFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'customer_id',
        'currency',
        'cart_version',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'currency' => 'USD',
        'cart_version' => 1,
        'status' => 'active',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return BelongsTo<Customer, $this>
     */
    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class);
    }

    /**
     * @return HasMany<CartLine, $this>
     */
    public function lines(): HasMany
    {
        return $this->hasMany(CartLine::class);
    }

    /**
     * @return HasMany<Checkout, $this>
     */
    public function checkouts(): HasMany
    {
        return $this->hasMany(Checkout::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'cart_version' => 'integer',
            'status' => CartStatus::class,
        ];
    }
}


command: sed -n '1,240p' app/Models/ShippingRate.php

aggregated_output:
<?php

namespace App\Models;

use App\Enums\ShippingRateType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ShippingRate extends Model
{
    /** @use HasFactory<\Database\Factories\ShippingRateFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'zone_id',
        'name',
        'type',
        'config_json',
        'is_active',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'type' => 'flat',
        'config_json' => '{}',
        'is_active' => true,
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('zone', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

    /**
     * @return BelongsTo<ShippingZone, $this>
     */
    public function zone(): BelongsTo
    {
        return $this->belongsTo(ShippingZone::class, 'zone_id');
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => ShippingRateType::class,
            'config_json' => 'array',
            'is_active' => 'bool',
        ];
    }
}


command:
/bin/zsh
-lc
sed -n '1,260p' app/Models/Checkout.php

Checkout.php

cmd: sed -n '1,260p' app/Models/Checkout.php

aggregated_output:
<?php

namespace App\Models;

use App\Enums\CheckoutStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Checkout extends Model
{
    /** @use HasFactory<\Database\Factories\CheckoutFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'cart_id',
        'customer_id',
        'status',
        'payment_method',
        'email',
        'shipping_address_json',
        'billing_address_json',
        'shipping_method_id',
        'discount_code',
        'tax_provider_snapshot_json',
        'totals_json',
        'expires_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'started',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return BelongsTo<Cart, $this>
     */
    public function cart(): BelongsTo
    {
        return $this->belongsTo(Cart::class);
    }

    /**
     * @return BelongsTo<Customer, $this>
     */
    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class);
    }

    /**
     * @return BelongsTo<ShippingRate, $this>
     */
    public function shippingRate(): BelongsTo
    {
        return $this->belongsTo(ShippingRate::class, 'shipping_method_id');
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => CheckoutStatus::class,
            'shipping_address_json' => 'array',
            'billing_address_json' => 'array',
            'shipping_method_id' => 'integer',
            'tax_provider_snapshot_json' => 'array',
            'totals_json' => 'array',
            'expires_at' => 'datetime',
        ];
    }
}


output:
Chunk ID: b2246c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 506
Output:
<?php

namespace App\Models;

use App\Enums\CheckoutStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Checkout extends Model
{
    /** @use HasFactory<\Database\Factories\CheckoutFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'cart_id',
        'customer_id',
        'status',
        'payment_method',
        'email',
        'shipping_address_json',
        'billing_address_json',
        'shipping_method_id',
        'discount_code',
        'tax_provider_snapshot_json',
        'totals_json',
        'expires_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'started',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return BelongsTo<Cart, $this>
     */
    public function cart(): BelongsTo
    {
        return $this->belongsTo(Cart::class);
    }

    /**
     * @return BelongsTo<Customer, $this>
     */
    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class);
    }

    /**
     * @return BelongsTo<ShippingRate, $this>
     */
    public function shippingRate(): BelongsTo
    {
        return $this->belongsTo(ShippingRate::class, 'shipping_method_id');
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => CheckoutStatus::class,
            'shipping_address_json' => 'array',
            'billing_address_json' => 'array',
            'shipping_method_id' => 'integer',
            'tax_provider_snapshot_json' => 'array',
            'totals_json' => 'array',
            'expires_at' => 'datetime',
        ];
    }
}


output:
Chunk ID: f85453
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 407
Output:
<?php

namespace App\Models;

use App\Enums\CartStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Cart extends Model
{
    /** @use HasFactory<\Database\Factories\CartFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'customer_id',
        'currency',
        'cart_version',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'currency' => 'USD',
        'cart_version' => 1,
        'status' => 'active',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return BelongsTo<Customer, $this>
     */
    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class);
    }

    /**
     * @return HasMany<CartLine, $this>
     */
    public function lines(): HasMany
    {
        return $this->hasMany(CartLine::class);
    }

    /**
     * @return HasMany<Checkout, $this>
     */
    public function checkouts(): HasMany
    {
        return $this->hasMany(Checkout::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'cart_version' => 'integer',
            'status' => CartStatus::class,
        ];
    }
}


output:
Chunk ID: 9c8c3a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 447
Output:
<?php

namespace App\Models;

use App\Enums\ShippingRateType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ShippingRate extends Model
{
    /** @use HasFactory<\Database\Factories\ShippingRateFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'zone_id',
        'name',
        'type',
        'config_json',
        'is_active',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'type' => 'flat',
        'config_json' => '{}',
        'is_active' => true,
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('zone', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

    /**
     * @return BelongsTo<ShippingZone, $this>
     */
    public function zone(): BelongsTo
    {
        return $this->belongsTo(ShippingZone::class, 'zone_id');
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => ShippingRateType::class,
            'config_json' => 'array',
            'is_active' => 'bool',
        ];
    }
}


command: sed -n '1,220p' database/seeders/DiscountSeeder.php && sed -n '1,220p' database/seeders/ShippingRateSeeder.php

ShippingRateSeeder.php

aggregated_output:
<?php

namespace Database\Seeders;

use App\Models\Discount;
use App\Models\Store;
use Illuminate\Database\Seeder;

class DiscountSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Store::query()->get()->each(function (Store $store): void {
            foreach ($this->discounts() as $discount) {
                Discount::withoutGlobalScopes()->updateOrCreate(
                    [
                        'store_id' => $store->getKey(),
                        'code' => $discount['code'],
                    ],
                    [
                        'type' => 'code',
                        'value_type' => $discount['value_type'],
                        'value_amount' => $discount['value_amount'],
                        'starts_at' => now()->subDay(),
                        'ends_at' => now()->addMonth(),
                        'usage_limit' => null,
                        'usage_count' => 0,
                        'rules_json' => $discount['rules_json'],
                        'status' => 'active',
                    ],
                );
            }
        });
    }

    /**
     * @return array<int, array{code: string, value_type: string, value_amount: int, rules_json: array<string, mixed>}>
     */
    private function discounts(): array
    {
        return [
            ['code' => 'SAVE10', 'value_type' => 'percent', 'value_amount' => 10, 'rules_json' => ['customer_eligibility' => 'all']],
            ['code' => '5OFF', 'value_type' => 'fixed', 'value_amount' => 500, 'rules_json' => ['min_purchase_amount' => 2500, 'customer_eligibility' => 'all']],
            ['code' => 'FREESHIP', 'value_type' => 'free_shipping', 'value_amount' => 0, 'rules_json' => ['customer_eligibility' => 'all']],
        ];
    }
}
<?php

namespace Database\Seeders;

use App\Models\ShippingRate;
use App\Models\ShippingZone;
use Illuminate\Database\Seeder;

class ShippingRateSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        ShippingZone::withoutGlobalScopes()->get()->each(function (ShippingZone $zone): void {
            foreach ($this->rates() as $rate) {
                ShippingRate::withoutGlobalScopes()->updateOrCreate(
                    [
                        'zone_id' => $zone->getKey(),
                        'name' => $rate['name'],
                    ],
                    [
                        'type' => $rate['type'],
                        'config_json' => $rate['config_json'],
                        'is_active' => true,
                    ],
                );
            }
        });
    }

    /**
     * @return array<int, array{name: string, type: string, config_json: array<string, mixed>}>
     */
    private function rates(): array
    {
        return [
            ['name' => 'Standard Shipping', 'type' => 'flat', 'config_json' => ['amount' => 799]],
            ['name' => 'Express Shipping', 'type' => 'flat', 'config_json' => ['amount' => 1499]],
            [
                'name' => 'Free Shipping Over 75',
                'type' => 'price',
                'config_json' => [
                    'ranges' => [
                        ['min_amount' => 0, 'max_amount' => 7499, 'amount' => 799],
                        ['min_amount' => 7500, 'amount' => 0],
                    ],
                ],
            ],
        ];
    }
}


output:
Chunk ID: 1ef50c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 864
Output:
<?php

namespace Database\Seeders;

use App\Models\Discount;
use App\Models\Store;
use Illuminate\Database\Seeder;

class DiscountSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Store::query()->get()->each(function (Store $store): void {
            foreach ($this->discounts() as $discount) {
                Discount::withoutGlobalScopes()->updateOrCreate(
                    [
                        'store_id' => $store->getKey(),
                        'code' => $discount['code'],
                    ],
                    [
                        'type' => 'code',
                        'value_type' => $discount['value_type'],
                        'value_amount' => $discount['value_amount'],
                        'starts_at' => now()->subDay(),
                        'ends_at' => now()->addMonth(),
                        'usage_limit' => null,
                        'usage_count' => 0,
                        'rules_json' => $discount['rules_json'],
                        'status' => 'active',
                    ],
                );
            }
        });
    }

    /**
     * @return array<int, array{code: string, value_type: string, value_amount: int, rules_json: array<string, mixed>}>
     */
    private function discounts(): array
    {
        return [
            ['code' => 'SAVE10', 'value_type' => 'percent', 'value_amount' => 10, 'rules_json' => ['customer_eligibility' => 'all']],
            ['code' => '5OFF', 'value_type' => 'fixed', 'value_amount' => 500, 'rules_json' => ['min_purchase_amount' => 2500, 'customer_eligibility' => 'all']],
            ['code' => 'FREESHIP', 'value_type' => 'free_shipping', 'value_amount' => 0, 'rules_json' => ['customer_eligibility' => 'all']],
        ];
    }
}
<?php

namespace Database\Seeders;

use App\Models\ShippingRate;
use App\Models\ShippingZone;
use Illuminate\Database\Seeder;

class ShippingRateSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        ShippingZone::withoutGlobalScopes()->get()->each(function (ShippingZone $zone): void {
            foreach ($this->rates() as $rate) {
                ShippingRate::withoutGlobalScopes()->updateOrCreate(
                    [
                        'zone_id' => $zone->getKey(),
                        'name' => $rate['name'],
                    ],
                    [
                        'type' => $rate['type'],
                        'config_json' => $rate['config_json'],
                        'is_active' => true,
                    ],
                );
            }
        });
    }

    /**
     * @return array<int, array{name: string, type: string, config_json: array<string, mixed>}>
     */
    private function rates(): array
    {
        return [
            ['name' => 'Standard Shipping', 'type' => 'flat', 'config_json' => ['amount' => 799]],
            ['name' => 'Express Shipping', 'type' => 'flat', 'config_json' => ['amount' => 1499]],
            [
                'name' => 'Free Shipping Over 75',
                'type' => 'price',
                'config_json' => [
                    'ranges' => [
                        ['min_amount' => 0, 'max_amount' => 7499, 'amount' => 799],
                        ['min_amount' => 7500, 'amount' => 0],
                    ],
                ],
            ],
        ];
    }
}


cmd: rg -n "RateLimiter|api.storefront|checkout|store.resolve|ResolveStore" app bootstrap routes config -g '*.php'


cmd: sed -n '1,260p' app/Providers/AppServiceProvider.php


cmd: sed -n '1,240p' app/Http/Middleware/ResolveStore.php


command: sed -n '1,240p' app/Http/Middleware/ResolveStore.php

aggregated_output:
<?php

namespace App\Http\Middleware;

use App\Enums\StoreStatus;
use App\Models\Store;
use App\Models\StoreDomain;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;

class ResolveStore
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $store = $request->is('admin*')
            ? $this->resolveAdminStore($request)
            : $this->resolveStorefrontStore($request);

        abort_unless($store, 404);

        app()->instance('current_store', $store);

        if (! $request->is('admin*') && $store->status === StoreStatus::Suspended) {
            abort(503);
        }

        if ($request->is('admin*') && $store->status === StoreStatus::Suspended && ! $request->isMethodSafe()) {
            abort(403);
        }

        return $next($request);
    }

    private function resolveStorefrontStore(Request $request): ?Store
    {
        $hostname = $request->getHost();
        $cacheKey = "store_domain:{$hostname}";

        $storeId = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($hostname): ?int {
            return StoreDomain::query()
                ->where('hostname', $hostname)
                ->value('store_id');
        });

        return $storeId ? Store::query()->find($storeId) : null;
    }

    private function resolveAdminStore(Request $request): ?Store
    {
        $user = $request->user();

        if (! $user) {
            return null;
        }

        $storeId = $request->session()->get('current_store_id');

        if (! $storeId) {
            $storeId = $user->stores()->oldest('stores.id')->value('stores.id');

            if ($storeId) {
                $request->session()->put('current_store_id', $storeId);
            }
        }

        if (! $storeId || ! $user->stores()->whereKey($storeId)->exists()) {
            return null;
        }

        return Store::query()->find($storeId);
    }
}


command: sed -n '1,260p' app/Providers/AppServiceProvider.php

aggregated_output:
<?php

namespace App\Providers;

use App\Auth\CustomerUserProvider;
use App\Models\Store;
use App\Services\NavigationService;
use App\Services\ThemeSettingsService;
use Carbon\CarbonImmutable;
use Illuminate\Auth\Middleware\Authenticate;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
use Illuminate\View\View as ViewInstance;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        Auth::provider('store_scoped_eloquent', function ($app, array $config): CustomerUserProvider {
            return new CustomerUserProvider($app['hash'], $config['model']);
        });

        $this->app->singleton(ThemeSettingsService::class);
        $this->app->singleton(NavigationService::class);
    }

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

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

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

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

        RateLimiter::for('api.storefront', function (Request $request): Limit {
            return Limit::perMinute(120)->by($request->ip());
        });

        RateLimiter::for('checkout', function (Request $request): Limit {
            return Limit::perMinute(10)->by($request->session()->getId() ?: $request->ip());
        });

        Authenticate::redirectUsing(function (Request $request): string {
            if ($request->is('admin*')) {
                return route('admin.login');
            }

            if ($request->is('account*')) {
                return route('account.login');
            }

            return route('login');
        });
    }

    protected function configureStorefrontViewData(): void
    {
        View::composer('layouts.storefront', function (ViewInstance $view): void {
            $store = app()->bound('current_store') ? app('current_store') : null;

            if (! $store instanceof Store) {
                $view->with([
                    'themeSettings' => [],
                    'mainNavigation' => [],
                    'footerNavigation' => [],
                ]);

                return;
            }

            $themeSettings = app(ThemeSettingsService::class)->forStore($store);
            $navigation = app(NavigationService::class);

            $view->with([
                'themeSettings' => $themeSettings,
                'mainNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'header.main_menu', 'main-menu')),
                'footerNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'footer.menu', 'footer-menu')),
            ]);
        });
    }
}


command: rg -n "RateLimiter|api.storefront|checkout|store.resolve|ResolveStore" app bootstrap routes config -g '*.php'

type: search

cmd: rg -n 'RateLimiter|api.storefront|checkout|store.resolve|ResolveStore' app bootstrap routes config -g '*.php'

query: RateLimiter|api.storefront|checkout|store.resolve|ResolveStore

path: app

aggregated_output:
bootstrap/app.php:4:use App\Http\Middleware\ResolveStore;
bootstrap/app.php:19:            'store.resolve' => ResolveStore::class,
bootstrap/app.php:23:            ResolveStore::class,
bootstrap/app.php:27:            ResolveStore::class,
app/Jobs/CleanupAbandonedCarts.php:20:    public function handle(CheckoutService $checkouts): void
app/Jobs/CleanupAbandonedCarts.php:27:            ->each(function (Cart $cart) use ($checkouts): void {
app/Jobs/CleanupAbandonedCarts.php:32:                    ->each(fn (Checkout $checkout): Checkout => $checkouts->expireCheckout($checkout));
app/Providers/FortifyServiceProvider.php:9:use Illuminate\Support\Facades\RateLimiter;
app/Providers/FortifyServiceProvider.php:61:        RateLimiter::for('two-factor', function (Request $request) {
app/Providers/FortifyServiceProvider.php:65:        RateLimiter::for('login', function (Request $request) {
routes/api.php:8:Route::middleware(['store.resolve', 'throttle:api.storefront'])
routes/api.php:10:    ->name('api.storefront.v1.')
routes/api.php:18:        Route::post('checkouts', [CheckoutController::class, 'store'])->name('checkouts.store');
routes/api.php:19:        Route::get('checkouts/{checkout}', [CheckoutController::class, 'show'])->name('checkouts.show');
routes/api.php:20:        Route::put('checkouts/{checkout}/address', [CheckoutController::class, 'address'])->name('checkouts.address');
routes/api.php:21:        Route::put('checkouts/{checkout}/shipping-method', [CheckoutController::class, 'shippingMethod'])->name('checkouts.shipping-method');
routes/api.php:22:        Route::post('checkouts/{checkout}/apply-discount', [CheckoutController::class, 'applyDiscount'])->name('checkouts.apply-discount');
routes/api.php:23:        Route::put('checkouts/{checkout}/payment-method', [CheckoutController::class, 'paymentMethod'])->name('checkouts.payment-method');
routes/web.php:28:    Route::livewire('checkout', StorefrontCheckoutShow::class)->name('checkout.show');
app/Providers/AppServiceProvider.php:16:use Illuminate\Support\Facades\RateLimiter;
app/Providers/AppServiceProvider.php:67:        RateLimiter::for('api.storefront', function (Request $request): Limit {
app/Providers/AppServiceProvider.php:71:        RateLimiter::for('checkout', function (Request $request): Limit {
app/Jobs/ExpireAbandonedCheckouts.php:18:    public function handle(CheckoutService $checkouts): void
app/Jobs/ExpireAbandonedCheckouts.php:33:            ->each(fn (Checkout $checkout): Checkout => $checkouts->expireCheckout($checkout));
app/Http/Middleware/ResolveStore.php:13:class ResolveStore
bootstrap/cache/services.php:85:    'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider',
app/Models/Customer.php:62:    public function checkouts(): HasMany
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:27:    public function store(StoreCheckoutRequest $request, CheckoutService $checkouts): CheckoutResource|JsonResponse
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:33:                $checkouts->createFromCart($cart),
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:40:    public function show(Checkout $checkout): CheckoutResource
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:42:        return CheckoutResource::make($this->loadCheckout($checkout));
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:45:    public function address(SetCheckoutAddressRequest $request, Checkout $checkout, CheckoutService $checkouts): CheckoutResource|JsonResponse
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:48:            return CheckoutResource::make($this->loadCheckout($checkouts->setAddress($checkout, $request->validated())));
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:54:    public function shippingMethod(SetCheckoutShippingRequest $request, Checkout $checkout, CheckoutService $checkouts): CheckoutResource|JsonResponse
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:57:            return CheckoutResource::make($this->loadCheckout($checkouts->setShippingMethod(
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:58:                $checkout,
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:66:    public function applyDiscount(ApplyCheckoutDiscountRequest $request, Checkout $checkout, PricingEngine $pricing): CheckoutResource|JsonResponse
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:68:        $checkout->forceFill([
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:73:            $pricing->calculate($checkout);
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:75:            return CheckoutResource::make($this->loadCheckout($checkout->refresh()));
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:77:            $checkout->forceFill(['discount_code' => null])->save();
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:78:            $pricing->calculate(…8708 chars truncated…:31:                    'seconds' => RateLimiter::availableIn($key),
app/Livewire/Admin/Auth/Login.php:41:            RateLimiter::hit($key, 60);
app/Livewire/Admin/Auth/Login.php:48:        RateLimiter::clear($key);
app/Services/ThemeSettingsService.php:63:                'tagline' => 'A self-contained demo storefront with scoped catalog data and checkout-ready products.',
app/Models/Cart.php:64:    public function checkouts(): HasMany
app/Livewire/Storefront/CartDrawer.php:63:    public function checkout(): void
app/Livewire/Storefront/CartDrawer.php:69:        $this->redirectRoute('checkout.show', navigate: true);
app/Livewire/Storefront/Cart/Show.php:63:    public function checkout(): void
app/Livewire/Storefront/Cart/Show.php:69:        $this->redirectRoute('checkout.show', navigate: true);
app/Livewire/Storefront/Checkout/Show.php:73:        $this->fillFromCheckout($this->checkout());
app/Livewire/Storefront/Checkout/Show.php:89:        $checkout = $this->checkout();
app/Livewire/Storefront/Checkout/Show.php:91:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:96:            $checkout = app(CheckoutService::class)->setAddress($checkout, [
app/Livewire/Storefront/Checkout/Show.php:105:                app(CheckoutService::class)->setShippingMethod($checkout, null);
app/Livewire/Storefront/Checkout/Show.php:121:        $checkout = $this->checkout();
app/Livewire/Storefront/Checkout/Show.php:123:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:134:            app(CheckoutService::class)->setShippingMethod($checkout, $this->selectedShippingRateId);
app/Livewire/Storefront/Checkout/Show.php:145:        $checkout = $this->checkout();
app/Livewire/Storefront/Checkout/Show.php:147:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:151:        $checkout->forceFill([
app/Livewire/Storefront/Checkout/Show.php:156:            app(PricingEngine::class)->calculate($checkout);
app/Livewire/Storefront/Checkout/Show.php:159:            $checkout->forceFill(['discount_code' => null])->save();
app/Livewire/Storefront/Checkout/Show.php:161:            app(PricingEngine::class)->calculate($checkout);
app/Livewire/Storefront/Checkout/Show.php:175:        $checkout = $this->checkout();
app/Livewire/Storefront/Checkout/Show.php:177:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:182:            app(CheckoutService::class)->selectPaymentMethod($checkout, $this->paymentMethod);
app/Livewire/Storefront/Checkout/Show.php:217:    public function checkout(): ?Checkout
app/Livewire/Storefront/Checkout/Show.php:225:        $checkout = Checkout::withoutGlobalScopes()
app/Livewire/Storefront/Checkout/Show.php:231:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:232:            $checkout = app(CheckoutService::class)->createFromCart($cart, $this->customer());
app/Livewire/Storefront/Checkout/Show.php:235:        return $checkout->load(['cart.lines.variant.product', 'cart.lines.variant.optionValues.option']);
app/Livewire/Storefront/Checkout/Show.php:300:        return $this->checkout()?->totals_json ?? [
app/Livewire/Storefront/Checkout/Show.php:312:        return view('livewire.storefront.checkout.show', [
app/Livewire/Storefront/Checkout/Show.php:314:            'checkout' => $this->checkout(),
app/Livewire/Storefront/Checkout/Show.php:333:    private function fillFromCheckout(?Checkout $checkout): void
app/Livewire/Storefront/Checkout/Show.php:335:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:339:        $this->email = $checkout->email ?: $this->email;
app/Livewire/Storefront/Checkout/Show.php:340:        $this->shippingAddress = array_replace($this->shippingAddress, $checkout->shipping_address_json ?? []);
app/Livewire/Storefront/Checkout/Show.php:341:        $this->billingAddress = array_replace($this->billingAddress, $checkout->billing_address_json ?? []);
app/Livewire/Storefront/Checkout/Show.php:342:        $this->selectedShippingRateId = $checkout->shipping_method_id;
app/Livewire/Storefront/Checkout/Show.php:343:        $this->discountCode = (string) ($checkout->discount_code ?? '');
app/Livewire/Storefront/Checkout/Show.php:344:        $this->paymentMethod = (string) ($checkout->payment_method ?? $this->paymentMethod);
app/Livewire/Storefront/Checkout/Show.php:345:        $this->step = match ($checkout->status) {
app/Livewire/Storefront/Account/Auth/Login.php:9:use Illuminate\Support\Facades\RateLimiter;
app/Livewire/Storefront/Account/Auth/Login.php:42:        if (RateLimiter::tooManyAttempts($key, 5)) {
app/Livewire/Storefront/Account/Auth/Login.php:45:                    'seconds' => RateLimiter::availableIn($key),
app/Livewire/Storefront/Account/Auth/Login.php:56:            RateLimiter::hit($key, 60);
app/Livewire/Storefront/Account/Auth/Login.php:63:        RateLimiter::clear($key);