output:
Chunk ID: 9ec839
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 11218
Output:
Total output lines: 364

specs/06-AUTH-AND-SECURITY.md:248:| `read-orders` | List and view orders |
specs/06-AUTH-AND-SECURITY.md:249:| `write-orders` | Update orders, create fulfillments |
specs/06-AUTH-AND-SECURITY.md:311:| Staff | `'staff'` | Operational access. Can manage products, orders, collections, discounts, pages. Cannot access settings, themes, staff management, or developers. |
specs/06-AUTH-AND-SECURITY.md:312:| Support | `'support'` | Read-only access to products, orders, and customers. For customer service agents. |
specs/06-AUTH-AND-SECURITY.md:383:| OwnerAdminOrStaff | Owner, Admin, Staff | Create/update products, collections, discounts, pages, fulfillments, order updates |
specs/06-AUTH-AND-SECURITY.md:384:| AnyRole | Owner, Admin, Staff, Support | Read-only listing/viewing of products, orders, customers, collections, discounts |
specs/06-AUTH-AND-SECURITY.md:617:| Webhook routes | `api`, `throttle:webhooks` | Reserved for outbound app webhooks (no inbound payment webhooks - Mock PSP is in-process) |
specs/06-AUTH-AND-SECURITY.md:744:| `webhooks` | 100 per minute | IP address | Outbound app webhook endpoints (reserved; no inbound payment webhooks - Mock PSP is in-process) |
specs/06-AUTH-AND-SECURITY.md:768:| Payment | `raw_json` | `encrypted:array` | Full payment gateway response (may contain sensitive card/account data) |
specs/06-AUTH-AND-SECURITY.md:935:### 7.1 Inbound Payment Webhooks
specs/06-AUTH-AND-SECURITY.md:937:> **Not applicable.** All payment processing happens in-process via the Mock PSP. There are no inbound payment webhooks from external providers (no Stripe, no PayPal). This section is preserved for reference only -- if a real PSP is integrated in the future, inbound webhook verification would follow the patterns described in the outbound webhook section below (HMAC signature verification).
specs/06-AUTH-AND-SECURITY.md:1108:| 14 | Webhook signature verification | HMAC-SHA256 for outbound platform webhooks (no inbound payment webhooks - Mock PSP is in-process) | Required |
specs/09-IMPLEMENTATION-ROADMAP.md:25:| Payments | Stripe + PayPal | Mock PSP (in-process) | No external API calls. MockPaymentProvider handles credit card, PayPal, bank transfer with magic card numbers for testing. |
specs/09-IMPLEMENTATION-ROADMAP.md:45:    P5[Phase 5: Payments, Orders, Fulfillment]
specs/09-IMPLEMENTATION-ROADMAP.md:176:Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/09-IMPLEMENTATION-ROADMAP.md:232:| Manage orders | Y | Y | Y | Read-only |
specs/09-IMPLEMENTATION-ROADMAP.md:233:| Process refunds | Y | Y | N | N |
specs/09-IMPLEMENTATION-ROADMAP.md:234:| Create fulfillments | Y | Y | Y | N |
specs/09-IMPLEMENTATION-ROADMAP.md:247:**Priority:** HIGH - storefront and orders depend on this.
specs/09-IMPLEMENTATION-ROADMAP.md:404:| `account/orders/` | `index.blade.php`, `show.blade.php` |
specs/09-IMPLEMENTATION-ROADMAP.md:487:| `CheckoutStatus` | `Started`, `Addressed`, `ShippingSelected`, `PaymentPending`, `Completed`, `Expired` |
specs/09-IMPLEMENTATION-ROADMAP.md:575:| `shipping_selected` -> `payment_selected` | `selectPaymentMethod(Checkout, string)` | All data valid, reserve inventory |
specs/09-IMPLEMENTATION-ROADMAP.md:576:| `payment_selected` -> `completed` | `completeCheckout(Checkout, array)` | Payment confirmed, idempotent |
specs/09-IMPLEMENTATION-ROADMAP.md:594:| `Storefront\Checkout\Show` | Multi-step: Contact/Address -> Shipping method -> Payment. Stepper UI. |
specs/09-IMPLEMENTATION-ROADMAP.md:599:### Phase 5: Payments, Orders, Fulfillment
specs/09-IMPLEMENTATION-ROADMAP.md:607:3. `create_orders_table` - FK: `store_id`, `customer_id`
specs/09-IMPLEMENTATION-ROADMAP.md:608:4. `create_order_lines_table` - FK: `order_id`, `product_id` (nullable), `variant_id` (nullable)
specs/09-IMPLEMENTATION-ROADMAP.md:609:5. `create_payments_table` - FK: `order_id`
specs/09-IMPLEMENTATION-ROADMAP.md:610:6. `create_refunds_table` - FK: `order_id`, `payment_id`
specs/09-IMPLEMENTATION-ROADMAP.md:611:7. `create_fulfillments_table` - FK: `order_id`
specs/09-IMPLEMENTATION-ROADMAP.md:624:| Model (with migration, factory, seeder) | `Payment` | Payment record |
specs/09-IMPLEMENTATION-ROADMAP.md:635:| `Order` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(OrderLine)`, `hasMany(Payment)`, `hasMany(Refund)`, `hasMany(Fulfillment)` |
specs/09-IMPLEMENTATION-ROADMAP.md:637:| `Payment` | `belongsTo(Order)` |
specs/09-IMPLEMENTATION-ROADMAP.md:638:| `Refund` | `belongsTo(Order)`, `belongsTo(Payment)` |
specs/09-IMPLEMENTATION-ROADMAP.md:649:| `PaymentMethod` | `CreditCard`, `Paypal`, `BankTransfer` |
specs/09-IMPLEMENTATION-ROADMAP.md:650:| `PaymentStatus` | `Pending`, `Captured`, `Failed`, `Refunded` |
specs/09-IMPLEMENTATION-ROADMAP.md:654:#### Step 5.3: Payment Service (Mock PSP)
specs/09-IMPLEMENTATION-ROADMAP.md:660:| Enum | `App\Enums\PaymentMethod` | CreditCard, Paypal, BankTransfer |
specs/09-IMPLEMENTATION-ROADMAP.md:661:| Interface | `App\Contracts\PaymentProvider` | Defines charge, refund methods |
specs/09-IMPLEMENTATION-ROADMAP.md:662:| Service | `App\Services\Payments\MockPaymentProvider` | Implements PaymentProvider with in-process mock logic |
specs/09-IMPLEMENTATION-ROADMAP.md:664:**PaymentProvider interface methods:**
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:669:| `refund(Payment $payment, int $amount): RefundResult` | Issues a mock refund (always succeeds). |
specs/09-IMPLEMENTATION-ROADMAP.md:671:**MockPaymentProvider requirements:**
specs/09-IMPLEMENTATION-ROADMAP.md:674:- Credit card / PayPal: instant capture, returns PaymentResult with status=captured
specs/09-IMPLEMENTATION-ROADMAP.md:675:- Bank transfer: deferred capture, returns PaymentResult with status=pending
specs/09-IMPLEMENTATION-ROADMAP.md:678:**Registration:** Bind `PaymentProvider` interface to `MockPaymentProvider` in `AppServiceProvider`.
specs/09-IMPLEMENTATION-ROADMAP.md:680:#### Step 5.3b: Admin Payment Confirmation
specs/09-IMPLEMENTATION-ROADMAP.md:686:| Job | `App\Jobs\CancelUnpaidBankTransferOrders` | Runs daily via `routes/console.php`; finds bank_transfer orders with `financial_status = pending` and `placed_at < now() - config days`; cancels them and releases reserved inventory |
specs/09-IMPLEMENTATION-ROADMAP.md:688:**Admin "Confirm Payment" action** (in OrderService or dedicated action class):
specs/09-IMPLEMENTATION-ROADMAP.md:698:**File to create:** `App\Services\OrderService`
specs/09-IMPLEMENTATION-ROADMAP.md:714:| `create(Order $order, Payment $payment, int $amount, ?string $reason, bool $restock): Refund` | Validates amount does not exceed payment, calls provider refund API, updates financial status |
specs/09-IMPLEMENTATION-ROADMAP.md:716:- Partial refunds: `financial_status` -> `partially_refunded`
specs/09-IMPLEMENTATION-ROADMAP.md:717:- Full refunds: `financial_status` -> `refunded`
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:764:| `Storefront\Account\Dashboard` | `GET /account` | Overview with recent orders |
specs/09-IMPLEMENTATION-ROADMAP.md:765:| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
specs/09-IMPLEMENTATION-ROADMAP.md:766:| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
specs/09-IMPLEMENTATION-ROADMAP.md:798:- Recent orders table (last 10)
specs/09-IMPLEMENTATION-ROADMAP.md:837:| `Admin\Customers\Show` | Customer detail: info, orders, addresses |
specs/09-IMPLEMENTATION-ROADMAP.md:915:- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count
specs/09-IMPLEMENTATION-ROADMAP.md:977:6. Manual smoke: visit admin login, authenticate, manage products and orders
specs/09-IMPLEMENTATION-ROADMAP.md:996:| `tests/Feature/Payments/` | `MockPaymentProviderTest.php`, `PaymentServiceTest.php`, `BankTransferConfirmationTest.php` |
specs/09-IMPLEMENTATION-ROADMAP.md:1013:**Setup:** Instantiate `PricingEngine` directly. Mock dependencies or use simple value objects.
specs/09-IMPLEMENTATION-ROADMAP.md:1148:| scopes order queries to the current store | store A has 2 orders, store B has 7 orders; set current_store to store A | Order::count() = 2 | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1214:| hard deletes a draft product with no order references | draft product, no orders; delete | product removed from DB | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1215:| prevents deletion of product with order references | product with order_lines referencing its variants; attempt delete | exception thrown | validation |
specs/09-IMPLEMENTATION-ROADMAP.md:1227:| archives orphaned variants with order references | variant referenced in order_lines; remove its option value | variant status = archived, not deleted | edge case |
specs/09-IMPLEMENTATION-ROADMAP.md:1258:| reorders products within a collection | 3 products with positions 0,1,2; reorder to 2,0,1 | positions updated correctly | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1273:| reorders media positions | upload 3 images, reorder | position values updated | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1320:| prevents duplicate orders from same checkout | complete checkout once (creates order); call completeCheckout again | no duplicate order created, same order returned | edge case |
specs/09-IMPLEMENTATION-ROADMAP.md:1393:| generates sequential order numbers per store | create 3 orders in store A | order numbers are #1001, #1002, #1003 | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1394:| creates order lines with snapshots | order from checkout with 2 items | order_lines have title_snapshot, sku_snapshot | 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:1413:| only allows admin or owner to process refunds | staff user attempts refund | 403 Forbidden | authorization |
specs/09-IMPLEMENTATION-ROADMAP.md:1433:| only allows admin, owner, or staff to create fulfillments | support user attempts to create fulfillment | 403 Forbidden | authorization |
specs/09-IMPLEMENTATION-ROADMAP.md:1437:#### tests/Feature/Payments/MockPaymentProviderTest.php
specs/09-IMPLEMENTATION-ROADMAP.md:1441:| charges credit card with success card number | checkout with card 4242424242424242 | PaymentResult success=true, status=captured | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1442:| declines credit card with decline card number | checkout with card 4000000000000002 | PaymentResult success=false, errorCode=card_declined | edge case |
specs/09-IMPLEMENTATION-ROADMAP.md:1443:| returns insufficient funds for that card number | checkout with card 4000000000009995 | PaymentResult success=false, errorCode=insufficient_funds | edge case |
specs/09-IMPLEMENTATION-ROADMAP.md:1444:| charges PayPal successfully | checkout with payment_method=paypal | PaymentResult success=true, status=captured | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1445:| creates pending payment for bank transfer | checkout with payment_method=bank_transfer | PaymentResult success=true, status=pending | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1450:#### tests/Feature/Payments/PaymentServiceTest.php
specs/09-IMPLEMENTATION-ROADMAP.md:1457:| resolves MockPaymentProvider from container | resolve PaymentProvider interface | resolved provider is MockPaymentProvider | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1462:#### tests/Feature/Payments/BankTransferConfirmationTest.php
specs/09-IMPLEMENTATION-ROADMAP.md:1467:| cannot confirm payment for non-bank-transfer orders | order with payment_method=credit_card | error thrown | validation |
specs/09-IMPLEMENTATION-ROADMAP.md:1469:| auto-cancel job cancels unpaid bank transfer orders after config days | order with payment_method=bank_transfer, placed_at > 7 days ago | order cancelled, inventory released | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1470:| auto-cancel job does not cancel orders within config days | order with payment_method=bank_transfer, placed_at = 2 days ago | order remains pending | edge case |
specs/09-IMPLEMENTATION-ROADMAP.md:1480:| lists customer orders | customer with 3 orders; GET /account/orders | sees all 3 order numbers | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1481:| shows order detail | customer with order #1001; GET /account/orders/1001 | sees order items and totals | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1482:| prevents accessing another customers orders | customer A logged in; GET /account/orders/{customerB-order-number} | 404 response | authorization |
specs/09-IMPLEMENTATION-ROADMAP.md:1507:| shows KPI tiles with correct data | store has 5 orders totaling 25000 cents | sees order count 5, total revenue formatted | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1509:| filters metrics by date range | orders across multiple dates; filter to last 7 days | only matching orders included in KPIs | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1532:| lists orders with status filter | 3 pending, 2 paid orders; filter by status=paid | 2 results | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1533:| shows order detail page | GET /admin/orders/{order} | sees order number, line items, totals | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1536:| restricts order management by role | support user can view orders (read-only); support user cannot update or fulfill | appropriate 403 responses | authorization |
specs/09-IMPLEMENTATION-ROADMAP.md:1605:| calculates revenue and AOV correctly | 3 orders: 1000, 2000, 3000 cents; aggregate | revenue=6000, aov=2000 | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1682:| lists orders with authentication | GET /api/admin/v1/stores/{storeId}/orders with Bearer token | 200 response, order list | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1683:| retrieves a single order | GET /api/admin/v1/stores/{storeId}/orders/{id} | 200 response, order with lines, payments, fulfillments | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1684:| filters orders by status | GET /api/admin/v1/stores/{storeId}/orders?status=paid | only paid orders returned | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1685:| creates a fulfillment via API | POST /api/admin/v1/stores/{storeId}/orders/{id}/fulfillments | 201 response, fulfillment created | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1686:| creates a refund via API | POST /api/admin/v1/stores/{storeId}/orders/{id}/refunds | 201 response, refund created | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1687:| requires write-orders ability for mutations | token with only read-orders; POST fulfillment | 403 response | authorization |
specs/09-IMPLEMENTATION-ROADMAP.md:1726:| `Payment` | Payment record |
specs/09-IMPLEMENTATION-ROADMAP.md:1905:| `Payments/MockPaymentProviderTest` | Mock PSP charge/decline feature tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1906:| `Payments/PaymentServiceTest` | Payment service feature tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1907:| `Payments/BankTransferConfirmationTest` | Bank transfer confirmation and auto-cancel tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1951:| `routes/web.php` | Admin auth routes, admin routes (prefix `/admin`), storefront routes, checkout routes, customer account routes |
specs/09-IMPLEMENTATION-ROADMAP.md:1958:- Bind services to container (PaymentProvider, CartService, PricingEngine, etc.)
specs/09-IMPLEMENTATION-ROADMAP.md:1976:| `OrderService` | Order creation from checkout, numbering, inventory commit |
specs/09-IMPLEMENTATION-ROADMAP.md:1983:| `Payments\MockPaymentProvider` | In-process mock PSP: credit card (magic numbers), PayPal, bank transfer |
specs/09-IMPLEMENTATION-ROADMAP.md:2058:13. `OrderSeeder` - creates sample orders with lines, payments, fulfillments
specs/04-STOREFRONT-UI.md:32:| Negative amounts (refunds) | Prefix with `-` (e.g., `-12.50 EUR`) |
specs/04-STOREFRONT-UI.md:54:| Account | Customer area | login, register, dashboard, orders (list + detail), addresses |
specs/04-STOREFRONT-UI.md:196:| [Copyright] [Payment Icons]                                   |
specs/04-STOREFRONT-UI.md:206:| Payment method icons | Row of SVG icons for accepted payment methods (Visa, Mastercard, Amex, PayPal, etc.). Slightly reduced opacity |
specs/04-STOREFRONT-UI.md:232:| Borders | Light gray | Medium-dark gray |
specs/04-STOREFRONT-UI.md:966:The checkout uses a single-page design with collapsible sections. Steps: Contact, Shipping Address, Shipping Method, Payment Method & Pay.
specs/04-STOREFRONT-UI.md:1042:#### Step 4 - Payment Method & Pay
specs/04-STOREFRONT-UI.md:1044:All payment is handled onsite via the Mock PSP. No external redirects, no iframes, no third-party scripts.
specs/04-STOREFRONT-UI.md:1046:**Payment method selection (radio buttons):**
specs/04-STOREFRONT-UI.md:1112:| Error handling | On payment decline (magic card number), display an inline error callout: "Payment declined: {reason}". The customer can correct details and retry. |
specs/04-STOREFRONT-UI.md:1176:|  Shipping Address          |  Payment Method                  |
specs/04-STOREFRONT-UI.md:1204:| Payment method display | Shows "Credit Card ending in {last4}", "PayPal", or "Bank Transfer" depending on `order.payment_method` |
specs/04-STOREFRONT-UI.md:1217:|  Bank:     Mock Bank AG                                       |
specs/04-STOREFRONT-UI.md:1327:| Orders card | Order icon, "Order history", "View all your orders" |
specs/04-STOREFRONT-UI.md:1330:| Recent orders | Table showing last 5 orders. Status shown as badge component |
specs/04-STOREFRONT-UI.md:1332:### 10.4 Order History (GET /account/orders)
specs/04-STOREFRONT-UI.md:1342:### 10.5 Order Detail (GET /account/orders/{orderNumber})
specs/04-STOREFRONT-UI.md:1353:|  Shipping Address    |  Billing Address   | Payment           |
specs/04-STOREFRONT-UI.md:1377:| Fulfillment | Only shown if fulfillments exist. Shows tracking company, number, and link to tracking URL (opens in new tab safely) |
specs/04-STOREFRONT-UI.md:1819:    P --> Q[Step 4: Payment Method & Pay]
specs/04-STOREFRONT-UI.md:1899:        ShippingMethod --> Payment: Complete step 3
specs/04-STOREFRONT-UI.md:1900:        Payment --> Processing: Submit payment
specs/04-STOREFRONT-UI.md:1901:        Processing --> Confirmation: Payment success
specs/04-STOREFRONT-UI.md:1902:        Processing --> Payment: Payment failure
specs/07-SEEDERS-AND-TEST-DATA.md:26:| 12 | CustomerSeeder | Stores | Creates customer accounts with addresses |
specs/07-SEEDERS-AND-TEST-DATA.md:27:| 13 | OrderSeeder | Stores, Customers, Products, Variants, Discounts | Creates orders with lines, payments, fulfillments, refunds |
specs/07-SEEDERS-AND-TEST-DATA.md:417:### 2.14 PaymentFactory
specs/07-SEEDERS-AND-TEST-DATA.md:419:**Model:** Payment
specs/07-SEEDERS-AND-TEST-DATA.md:456:| payment_id | Creates a new Payment via its factory |
specs/07-SEEDERS-AND-TEST-DATA.md:1448:- WELCOME10: Applies 10% off to orders with subtotal >= 20.00 EUR. Already used 3 times (has capacity remaining since no usage_limit).
specs/07-SEEDERS-AND-TEST-DATA.md:1501:**Acme Fashion - 15 orders:**
…1218 tokens truncated…NESS-LOGIC.md:422:    shipping_selected --> payment_selected : Payment method selected
specs/05-BUSINESS-LOGIC.md:423:    payment_selected --> completed : Payment confirmed
specs/05-BUSINESS-LOGIC.md:489:**Method:** `selectPaymentMethod(checkout, paymentMethod)` - validates and stores the selected payment method
specs/05-BUSINESS-LOGIC.md:508:**CRITICAL: This transition must be idempotent.** If called multiple times for the same checkout (e.g., double-click), it must not create duplicate orders. Always check if an order already exists for this checkout before proceeding.
specs/05-BUSINESS-LOGIC.md:519:    2. CALL MockPaymentProvider.charge(checkout, paymentMethodData)
specs/05-BUSINESS-LOGIC.md:522:           THROW PaymentFailedException(result.errorCode)
specs/05-BUSINESS-LOGIC.md:595:| `CheckoutCompleted` | Payment confirmed and order created |
specs/05-BUSINESS-LOGIC.md:720:- On order creation: store in `order_lines.discount_allocations_json` as an array of `{discount_id, amount}`.
specs/05-BUSINESS-LOGIC.md:929:## 10. Payment Processing (Mock PSP)
specs/05-BUSINESS-LOGIC.md:931:### 10.1 Mock Payment Provider Architecture
specs/05-BUSINESS-LOGIC.md:933:The platform uses a **Mock Payment Service Provider (PSP)** instead of real payment gateways. All payment processing happens in-process with no external API calls, no webhooks, and no redirects. This enables a fully functional checkout flow for development, testing, and demonstration purposes.
specs/05-BUSINESS-LOGIC.md:935:**Interface:** `App\Contracts\PaymentProvider`
specs/05-BUSINESS-LOGIC.md:938:- `charge(checkout, paymentMethodData)` - returns a PaymentResult value object (success/failure, reference ID)
specs/05-BUSINESS-LOGIC.md:941:**Service:** `App\Services\PaymentService` - orchestrates payment processing, delegates to MockPaymentProvider.
specs/05-BUSINESS-LOGIC.md:943:**Implementation:** `App\Services\Payment\MockPaymentProvider`
specs/05-BUSINESS-LOGIC.md:945:### 10.2 Supported Payment Methods
specs/05-BUSINESS-LOGIC.md:947:**Enum:** `App\Enums\PaymentMethod` - values: `CreditCard` (`credit_card`), `Paypal` (`paypal`), `BankTransfer` (`bank_transfer`)
specs/05-BUSINESS-LOGIC.md:959:| Card Number | Outcome | PaymentResult |
specs/05-BUSINESS-LOGIC.md:968:### 10.4 Mock Payment Flow
specs/05-BUSINESS-LOGIC.md:975:    participant MockPSP
specs/05-BUSINESS-LOGIC.md:980:    Backend->>MockPSP: charge(checkout, paymentMethodData)
specs/05-BUSINESS-LOGIC.md:983:        MockPSP-->>Backend: PaymentResult (success/failure)
specs/05-BUSINESS-LOGIC.md:993:        MockPSP-->>Backend: PaymentResult (success = true, status = pending)
specs/05-BUSINESS-LOGIC.md:1000:### 10.5 Payment State Machine
specs/05-BUSINESS-LOGIC.md:1002:**Enum:** `App\Enums\PaymentStatus` - values: `pending`, `captured`, `failed`, `refunded`
specs/05-BUSINESS-LOGIC.md:1020:| Bank name | Mock Bank AG |
specs/05-BUSINESS-LOGIC.md:1028:### 10.7 Admin: Confirm Payment (Bank Transfer)
specs/05-BUSINESS-LOGIC.md:1030:Admin users can confirm that a bank transfer payment has been received via a "Confirm Payment" button on the order detail page (visible only when `payment_method = bank_transfer` AND `financial_status = pending`).
specs/05-BUSINESS-LOGIC.md:1033:1. Admin clicks "Confirm Payment" button.
specs/05-BUSINESS-LOGIC.md:1034:2. Backend updates `payments.status` from `pending` to `captured`.
specs/05-BUSINESS-LOGIC.md:1035:3. Backend updates `orders.financial_status` from `pending` to `paid`.
specs/05-BUSINESS-LOGIC.md:1036:4. Backend updates `orders.status` from `pending` to `paid`.
specs/05-BUSINESS-LOGIC.md:1046:- Finds orders where `payment_method = bank_transfer` AND `financial_status = pending` AND `placed_at < (now - configurable days)`.
specs/05-BUSINESS-LOGIC.md:1051:2. Update `orders.financial_status` to `voided`.
specs/05-BUSINESS-LOGIC.md:1052:3. Update `orders.status` to `cancelled`.
specs/05-BUSINESS-LOGIC.md:1053:4. Update `payments.status` to `failed`.
specs/05-BUSINESS-LOGIC.md:1071:**Payment-method-specific order creation:**
specs/05-BUSINESS-LOGIC.md:1073:| Payment Method | `orders.status` | `orders.financial_status` | Inventory Action | Payment Record Status |
specs/05-BUSINESS-LOGIC.md:1090:**Order status** (`orders.status`):
specs/05-BUSINESS-LOGIC.md:1095:| `paid` | Payment confirmed |
specs/05-BUSINESS-LOGIC.md:1100:**Financial status** (`orders.financial_status`):
specs/05-BUSINESS-LOGIC.md:1104:| `pending` | Payment not yet confirmed |
specs/05-BUSINESS-LOGIC.md:1105:| `authorized` | Payment authorized but not captured |
specs/05-BUSINESS-LOGIC.md:1106:| `paid` | Payment captured successfully |
specs/05-BUSINESS-LOGIC.md:1111:**Fulfillment status** (`orders.fulfillment_status`):
specs/05-BUSINESS-LOGIC.md:1128:| `lines` | map of order_line_id to quantity | No | Which lines and quantities to refund (for line-level refunds). |
specs/05-BUSINESS-LOGIC.md:1137:       refundable = order.total_amount - SUM(existing refunds for this order)
specs/05-BUSINESS-LOGIC.md:1146:    3. CALL PaymentProvider.refund(order's payment, amount)
specs/05-BUSINESS-LOGIC.md:1177:| `pending` | **No** | Payment not yet received (e.g., bank transfer awaiting confirmation) |
specs/05-BUSINESS-LOGIC.md:1178:| `authorized` | **No** | Payment authorized but not captured |
specs/05-BUSINESS-LOGIC.md:1179:| `paid` | **Yes** | Payment confirmed |
specs/05-BUSINESS-LOGIC.md:1180:| `partially_refunded` | **Yes** | Payment confirmed, partial refund does not block shipment |
specs/05-BUSINESS-LOGIC.md:1182:| `voided` | **No** | Payment voided / cancelled |
specs/05-BUSINESS-LOGIC.md:1253:### 11.6 Fulfillment Guard - Inventory Behavior per Payment Method
specs/05-BUSINESS-LOGIC.md:1257:| Payment Method | At Order Creation | At Payment Confirmation | At Fulfillment |
specs/05-BUSINESS-LOGIC.md:1262:For bank transfer orders, inventory is held in a reserved state until the admin confirms payment. If the order is auto-cancelled (see Section 10.8), the reserved inventory is released back.
specs/05-BUSINESS-LOGIC.md:1275:3. If the order contains a mix of physical and digital products: do **not** auto-fulfill. The admin must create fulfillments manually (they can fulfill digital items immediately and physical items when shipped).
specs/05-BUSINESS-LOGIC.md:1277:This ensures digital product orders are immediately marked as complete after payment.
specs/05-BUSINESS-LOGIC.md:1281:`GET /api/admin/v1/orders/export` returns a CSV file of orders matching the current filter criteria.
specs/05-BUSINESS-LOGIC.md:1285:All monetary values are exported in minor units (cents). Dates use ISO-8601 format. The response uses `Content-Type: text/csv` with `Content-Disposition: attachment; filename="orders-{date}.csv"`.
specs/05-BUSINESS-LOGIC.md:1321:- Customers can only view their own orders (scoped by customer_id match).
specs/05-BUSINESS-LOGIC.md:1356:| `order.paid` | Payment captured successfully |
specs/05-BUSINESS-LOGIC.md:1481:| `orders_count` | Count of `checkout_completed` events for the day |
specs/05-BUSINESS-LOGIC.md:1483:| `aov_amount` | `revenue_amount / orders_count` (integer division, 0 if no orders) |
specs/05-BUSINESS-LOGIC.md:1621:| `App\Jobs\CancelUnpaidBankTransferOrders` | Daily | Cancel bank transfer orders that remain unpaid after configurable days (default: 7) |
specs/05-BUSINESS-LOGIC.md:1649:| PaymentService | `App\Services` | Payment processing orchestration |
specs/05-BUSINESS-LOGIC.md:1650:| MockPaymentProvider | `App\Services\Payment` | Mock PSP - simulates card/PayPal/bank transfer payments in-process |
specs/05-BUSINESS-LOGIC.md:1665:| PaymentProvider | `App\Contracts` | MockPaymentProvider |
specs/05-BUSINESS-LOGIC.md:1681:| CheckoutStatus | Started (`started`), Addressed (`addressed`), ShippingSelected (`shipping_selected`), PaymentSelected (`payment_selected`), Completed (`completed`), Expired (`expired`) |
specs/05-BUSINESS-LOGIC.md:1682:| PaymentMethod | CreditCard (`credit_card`), Paypal (`paypal`), BankTransfer (`bank_transfer`) |
specs/05-BUSINESS-LOGIC.md:1683:| PaymentStatus | Pending (`pending`), Captured (`captured`), Failed (`failed`), Refunded (`refunded`) |
specs/05-BUSINESS-LOGIC.md:1705:| PaymentResult | success (bool), referenceId (string), status (string: pending/captured/failed), errorCode (nullable string), errorMessage (nullable string) | Mock payment processing result |
specs/01-DATABASE-SCHEMA.md:285:| quantity_reserved | INTEGER | No | 0 | - | Stock allocated to open orders |
specs/01-DATABASE-SCHEMA.md:293:**Notes:** `quantity_on_hand` is the total physical stock. `quantity_reserved` is stock allocated to open orders not yet fulfilled. Available = on_hand - reserved. Policy `deny` prevents overselling; `continue` allows selling below zero.
specs/01-DATABASE-SCHEMA.md:704:## Epic 6 - Payments, Orders, Fulfillment, and Customers
specs/01-DATABASE-SCHEMA.md:708:Storefront customer accounts, scoped to a store.
specs/01-DATABASE-SCHEMA.md:751:### orders
specs/01-DATABASE-SCHEMA.md:753:Placed orders with financial and fulfillment tracking.
specs/01-DATABASE-SCHEMA.md:759:| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for guest orders |
specs/01-DATABASE-SCHEMA.md:761:| payment_method | TEXT | No | - | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Payment method used for this order |
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:780:- `idx_orders_store_order_number` UNIQUE on (`store_id`, `order_number`) - Unique order number per store
specs/01-DATABASE-SCHEMA.md:781:- `idx_orders_store_id` on (`store_id`) - FK lookup
specs/01-DATABASE-SCHEMA.md:782:- `idx_orders_customer_id` on (`customer_id`) - Listing orders per customer
specs/01-DATABASE-SCHEMA.md:783:- `idx_orders_store_status` on (`store_id`, `status`) - Filtering by overall status
specs/01-DATABASE-SCHEMA.md:784:- `idx_orders_store_financial` on (`store_id`, `financial_status`) - Filtering by payment status
specs/01-DATABASE-SCHEMA.md:785:- `idx_orders_store_fulfillment` on (`store_id`, `fulfillment_status`) - Filtering by shipping status
specs/01-DATABASE-SCHEMA.md:786:- `idx_orders_placed_at` on (`store_id`, `placed_at`) - Date-range order queries
specs/01-DATABASE-SCHEMA.md:790:### order_lines
specs/01-DATABASE-SCHEMA.md:797:| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
specs/01-DATABASE-SCHEMA.md:810:- `idx_order_lines_order_id` on (`order_id`) - FK lookup, listing lines per order
specs/01-DATABASE-SCHEMA.md:811:- `idx_order_lines_product_id` on (`product_id`) - Finding order lines by product
specs/01-DATABASE-SCHEMA.md:812:- `idx_order_lines_variant_id` on (`variant_id`) - Finding order lines by variant
specs/01-DATABASE-SCHEMA.md:818:### payments
specs/01-DATABASE-SCHEMA.md:820:Payment attempts and captures for an order.
specs/01-DATABASE-SCHEMA.md:825:| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
specs/01-DATABASE-SCHEMA.md:826:| provider | TEXT | No | 'mock' | CHECK IN ('mock') | Payment provider (mock PSP only) |
specs/01-DATABASE-SCHEMA.md:827:| method | TEXT | No | - | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Payment method used |
specs/01-DATABASE-SCHEMA.md:828:| provider_payment_id | TEXT | Yes | NULL | - | Mock payment reference ID |
specs/01-DATABASE-SCHEMA.md:830:| amount | INTEGER | No | 0 | - | Payment amount in minor units |
specs/01-DATABASE-SCHEMA.md:837:- `idx_payments_order_id` on (`order_id`) - FK lookup, listing payments per order
specs/01-DATABASE-SCHEMA.md:838:- `idx_payments_provider_id` on (`provider`, `provider_payment_id`) - Payment reconciliation by provider ID
specs/01-DATABASE-SCHEMA.md:839:- `idx_payments_method` on (`method`) - Filtering payments by method
specs/01-DATABASE-SCHEMA.md:840:- `idx_payments_status` on (`status`) - Filtering payments by status
specs/01-DATABASE-SCHEMA.md:846:### refunds
specs/01-DATABASE-SCHEMA.md:853:| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
specs/01-DATABASE-SCHEMA.md:854:| payment_id | INTEGER | No | - | FK -> payments(id) ON DELETE CASCADE | Source payment |
specs/01-DATABASE-SCHEMA.md:863:- `idx_refunds_order_id` on (`order_id`) - FK lookup, listing refunds per order
specs/01-DATABASE-SCHEMA.md:864:- `idx_refunds_payment_id` on (`payment_id`) - Finding refunds per payment
specs/01-DATABASE-SCHEMA.md:865:- `idx_refunds_status` on (`status`) - Filtering refunds by status
specs/01-DATABASE-SCHEMA.md:869:### fulfillments
specs/01-DATABASE-SCHEMA.md:876:| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
specs/01-DATABASE-SCHEMA.md:886:- `idx_fulfillments_order_id` on (`order_id`) - FK lookup, listing fulfillments per order
specs/01-DATABASE-SCHEMA.md:887:- `idx_fulfillments_status` on (`status`) - Filtering fulfillments by status
specs/01-DATABASE-SCHEMA.md:888:- `idx_fulfillments_tracking` on (`tracking_company`, `tracking_number`) - Tracking number lookup
specs/01-DATABASE-SCHEMA.md:899:| fulfillment_id | INTEGER | No | - | FK -> fulfillments(id) ON DELETE CASCADE | Parent fulfillment |
specs/01-DATABASE-SCHEMA.md:900:| order_line_id | INTEGER | No | - | FK -> order_lines(id) ON DELETE CASCADE | Source order line |
specs/01-DATABASE-SCHEMA.md:951:| orders_count | INTEGER | No | 0 | - | Number of orders placed |
specs/01-DATABASE-SCHEMA.md:1367:### Epics 5-6 - Cart, Checkout, Orders, and Payments
specs/01-DATABASE-SCHEMA.md:1379:    stores ||--o{ orders : "has"
specs/01-DATABASE-SCHEMA.md:1380:    customers ||--o{ orders : "placed"
specs/01-DATABASE-SCHEMA.md:1381:    orders ||--o{ order_lines : "contains"
specs/01-DATABASE-SCHEMA.md:1382:    orders ||--o{ payments : "paid via"
specs/01-DATABASE-SCHEMA.md:1383:    orders ||--o{ refunds : "refunded via"
specs/01-DATABASE-SCHEMA.md:1384:    payments ||--o{ refunds : "source of"
specs/01-DATABASE-SCHEMA.md:1385:    orders ||--o{ fulfillments : "shipped via"
specs/01-DATABASE-SCHEMA.md:1386:    fulfillments ||--o{ fulfillment_lines : "contains"
specs/01-DATABASE-SCHEMA.md:1387:    order_lines ||--o{ fulfillment_lines : "fulfilled by"
specs/01-DATABASE-SCHEMA.md:1401:    orders {
specs/01-DATABASE-SCHEMA.md:1412:    payments {
specs/01-DATABASE-SCHEMA.md:1420:    fulfillments {
specs/01-DATABASE-SCHEMA.md:1452:        INTEGER orders_count
specs/01-DATABASE-SCHEMA.md:1562:        orders
specs/01-DATABASE-SCHEMA.md:1570:        order_lines
specs/01-DATABASE-SCHEMA.md:1571:        payments
specs/01-DATABASE-SCHEMA.md:1572:        fulfillments
specs/01-DATABASE-SCHEMA.md:1576:        refunds
specs/01-DATABASE-SCHEMA.md:1622:    stores --> orders
specs/01-DATABASE-SCHEMA.md:1623:    customers --> orders
specs/01-DATABASE-SCHEMA.md:1630:    orders --> order_lines
specs/01-DATABASE-SCHEMA.md:1631:    products --> order_lines
specs/01-DATABASE-SCHEMA.md:1632:    product_variants --> order_lines
specs/01-DATABASE-SCHEMA.md:1633:    orders --> payments
specs/01-DATABASE-SCHEMA.md:1634:    orders --> fulfillments
specs/01-DATABASE-SCHEMA.md:1635:    orders --> refunds
specs/01-DATABASE-SCHEMA.md:1636:    payments --> refunds
specs/01-DATABASE-SCHEMA.md:1637:    fulfillments --> fulfillment_lines
specs/01-DATABASE-SCHEMA.md:1638:    order_lines --> fulfillment_lines
specs/01-DATABASE-SCHEMA.md:1647:- **Batch 5** (depends on Batch 4): `product_option_values`, `inventory_items`, `cart_lines`, `checkouts`, `orders`, `analytics_events`, `analytics_daily`, `webhook_deliveries`
specs/01-DATABASE-SCHEMA.md:1648:- **Batch 6** (depends on Batch 5): `variant_option_values`, `order_lines`, `payments`, `fulfillments`
specs/01-DATABASE-SCHEMA.md:1649:- **Batch 7** (depends on Batch 6): `refunds`, `fulfillment_lines`
specs/01-DATABASE-SCHEMA.md:1663:5. **Date-based indexes for time series** - Tables with temporal queries (orders, analytics, search queries, webhook deliveries) include indexes on date/timestamp columns scoped by store for efficient range scans.
specs/01-DATABASE-SCHEMA.md:1678:| 6 - Payments, Orders, Fulfillment, Customers | customers, customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines | 8 |
specs/03-ADMIN-UI.md:86:  Orders                         -> admin.orders.index
specs/03-ADMIN-UI.md:218:| `ordersCount` | `int` | Number of orders in range |
specs/03-ADMIN-UI.md:222:| `ordersChange` | `float` | Percentage change vs previous period |
specs/03-ADMIN-UI.md:225:| `ordersChartData` | `array` | Array of `{date, count}` for last 30 days |
specs/03-ADMIN-UI.md:432:| Body text | "This will archive X product(s). Products with orders cannot be permanently deleted." |
specs/03-ADMIN-UI.md:665:| Body text | "This product will be archived. Products with existing orders cannot be permanently removed." |
specs/03-ADMIN-UI.md:864:**Route:** `GET /admin/orders`
specs/03-ADMIN-UI.md:868:**Blade view:** `resources/views/livewire/admin/orders/index.blade.php`
specs/03-ADMIN-UI.md:887:| `orders` | Paginated query with search, status filter, sorting. Eager loads `customer`. |
specs/03-ADMIN-UI.md:899:| Order #  | Date           | Customer  | Payment   | Fulfill | Total  |
specs/03-ADMIN-UI.md:938:**Route:** `GET /admin/orders/{order}`
specs/03-ADMIN-UI.md:942:**Blade view:** `resources/views/livewire/admin/orders/show.blade.php`
specs/03-ADMIN-UI.md:948:| `order` | `Order` | The order model (eager loaded with lines, payments, fulfillments, customer) |
specs/03-ADMIN-UI.md:960:| `confirmPayment()` | Confirms bank transfer payment received (transitions financial_status to `paid`, commits inventory, dispatches `OrderPaid`) |
specs/03-ADMIN-UI.md:982:| o  Payment received - Jan 15, 2:31 PM       | 62701              |
specs/03-ADMIN-UI.md:1003:| [Confirm Payment] button                     |                    |
specs/03-ADMIN-UI.md:1017:- "Confirm payment" button (primary, shown only when `payment_method = bank_transfer` AND `financial_status = pending`). On click, calls `confirmPayment()` which transitions the order to `paid`, commits inventory, and dispatches `OrderPaid`.
specs/03-ADMIN-UI.md:1025:> **Cannot create fulfillment.** Payment must be confirmed before items can be fulfilled. Current financial status: *{financial_status}*.
specs/03-ADMIN-UI.md:1032:- Events include: Order placed, Payment received, Fulfillment created, Shipped, Delivered, Refunded (as applicable)
specs/03-ADMIN-UI.md:1049:#### Payment Details
specs/03-ADMIN-UI.md:1051:- Payment method as text (Credit Card / PayPal / Bank Transfer), derived from `order.payment_method` enum
specs/03-ADMIN-UI.md:1054:- Mock reference ID (provider_payment_id)
specs/03-ADMIN-UI.md:1055:- **"Confirm payment" button** (primary variant): Shown only when `payment_method = bank_transfer` AND `financial_status = pending`. Clicking calls `confirmPayment()`.
specs/03-ADMIN-UI.md:1065:  - "Mark as delivered" button (shown when fulfillment status is `shipped`). On click, calls `markAsDelivered(fulfillmentId)`. Sets `delivered_at` timestamp. When all fulfillments are delivered, the order's `fulfillment_status` transitions to `fulfilled`.
specs/03-ADMIN-UI.md:1152:| `customers` | Paginated, searched customers with `withCount('orders')` and `withSum('orders', 'total_amount')` |
specs/03-ADMIN-UI.md:1180:| `mount(Customer $customer)` | Loads customer with orders and addresses |
specs/03-ADMIN-UI.md:2140:| `ordersCount` | `int` | Orders count |
specs/03-ADMIN-UI.md:2173:| [line/bar chart - daily revenue/orders]                          |
specs/03-ADMIN-UI.md:2201:**Sales chart:** Larger chart than Dashboard, line or bar chart showing daily revenue/orders.
specs/03-ADMIN-UI.md:2410:| `App\Livewire\Admin\Orders\Index` | `livewire.admin.orders.index` | `GET /admin/orders` |
specs/03-ADMIN-UI.md:2411:| `App\Livewire\Admin\Orders\Show` | `livewire.admin.orders.show` | `GET /admin/orders/{order}` |


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


cmd: sed -n '751,910p' specs/01-DATABASE-SCHEMA.md


cmd: sed -n '480,535p' specs/05-BUSINESS-LOGIC.md && sed -n '929,1090p' specs/05-BUSINESS-LOGIC.md


cmd: sed -n '1090,1278p' specs/05-BUSINESS-LOGIC.md


cmd: sed -n '599,640p' specs/09-IMPLEMENTATION-ROADMAP.md


command: sed -n '751,910p' specs/01-DATABASE-SCHEMA.md

type: read

aggregated_output:
### orders

Placed orders with financial and fulfillment tracking.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for guest orders |
| order_number | TEXT | No | - | UNIQUE per (store_id, order_number) | Human-readable order number |
| payment_method | TEXT | No | - | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Payment method used for this order |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'paid', 'fulfilled', 'cancelled', 'refunded') | Overall order status |
| financial_status | TEXT | No | 'pending' | CHECK IN ('pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided') | Payment status |
| fulfillment_status | TEXT | No | 'unfulfilled' | CHECK IN ('unfulfilled', 'partial', 'fulfilled') | Shipping status |
| currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
| subtotal_amount | INTEGER | No | 0 | - | Sum of line items in minor units |
| discount_amount | INTEGER | No | 0 | - | Total discount in minor units |
| shipping_amount | INTEGER | No | 0 | - | Shipping cost in minor units |
| tax_amount | INTEGER | No | 0 | - | Total tax in minor units |
| total_amount | INTEGER | No | 0 | - | Grand total in minor units |
| email | TEXT | Yes | NULL | - | Contact email at time of order |
| billing_address_json | TEXT | Yes | NULL | - | JSON address snapshot |
| shipping_address_json | TEXT | Yes | NULL | - | JSON address snapshot |
| placed_at | TEXT | Yes | NULL | - | ISO-8601 timestamp of placement |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_orders_store_order_number` UNIQUE on (`store_id`, `order_number`) - Unique order number per store
- `idx_orders_store_id` on (`store_id`) - FK lookup
- `idx_orders_customer_id` on (`customer_id`) - Listing orders per customer
- `idx_orders_store_status` on (`store_id`, `status`) - Filtering by overall status
- `idx_orders_store_financial` on (`store_id`, `financial_status`) - Filtering by payment status
- `idx_orders_store_fulfillment` on (`store_id`, `fulfillment_status`) - Filtering by shipping status
- `idx_orders_placed_at` on (`store_id`, `placed_at`) - Date-range order queries

---

### order_lines

Individual items within an order. Contains snapshots of product data at time of purchase.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| product_id | INTEGER | Yes | NULL | FK -> products(id) ON DELETE SET NULL | Nullable: survives product deletion |
| variant_id | INTEGER | Yes | NULL | FK -> product_variants(id) ON DELETE SET NULL | Nullable: survives variant deletion |
| title_snapshot | TEXT | No | - | - | Product title at time of purchase |
| sku_snapshot | TEXT | Yes | NULL | - | SKU at time of purchase |
| quantity | INTEGER | No | 1 | - | Number of units |
| unit_price_amount | INTEGER | No | 0 | - | Price per unit in minor units |
| total_amount | INTEGER | No | 0 | - | Line total in minor units |
| tax_lines_json | TEXT | No | '[]' | - | JSON array of tax line items |
| discount_allocations_json | TEXT | No | '[]' | - | JSON array of discount allocations |

**Indexes:**

- `idx_order_lines_order_id` on (`order_id`) - FK lookup, listing lines per order
- `idx_order_lines_product_id` on (`product_id`) - Finding order lines by product
- `idx_order_lines_variant_id` on (`variant_id`) - Finding order lines by variant

**Notes:** `product_id` and `variant_id` are nullable with SET NULL on delete so that order history survives product deletion. `tax_lines_json` example: `[{"title": "State Tax", "rate": 800, "amount": 400}]` (rate in basis points, e.g. 800 = 8%). `discount_allocations_json` example: `[{"discount_id": 5, "amount": 250}]`.

---

### payments

Payment attempts and captures for an order.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| provider | TEXT | No | 'mock' | CHECK IN ('mock') | Payment provider (mock PSP only) |
| method | TEXT | No | - | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Payment method used |
| provider_payment_id | TEXT | Yes | NULL | - | Mock payment reference ID |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'captured', 'failed', 'refunded') | |
| amount | INTEGER | No | 0 | - | Payment amount in minor units |
| currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
| raw_json_encrypted | TEXT | Yes | NULL | - | Encrypted provider response (PCI compliance) |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_payments_order_id` on (`order_id`) - FK lookup, listing payments per order
- `idx_payments_provider_id` on (`provider`, `provider_payment_id`) - Payment reconciliation by provider ID
- `idx_payments_method` on (`method`) - Filtering payments by method
- `idx_payments_status` on (`status`) - Filtering payments by status

**Notes:** `raw_json_encrypted` stores the mock payment provider response, encrypted at the application layer. `amount` is in minor units (cents). The `provider` is always `mock` since the platform uses a mock PSP (no real payment gateway integration). The `method` column records which payment method the customer selected (credit_card, paypal, or bank_transfer).

---

### refunds

Refund records linked to an order and payment.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| payment_id | INTEGER | No | - | FK -> payments(id) ON DELETE CASCADE | Source payment |
| amount | INTEGER | No | 0 | - | Refund amount in minor units |
| reason | TEXT | Yes | NULL | - | Refund reason |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'processed', 'failed') | |
| provider_refund_id | TEXT | Yes | NULL | - | External refund ID |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_refunds_order_id` on (`order_id`) - FK lookup, listing refunds per order
- `idx_refunds_payment_id` on (`payment_id`) - Finding refunds per payment
- `idx_refunds_status` on (`status`) - Filtering refunds by status

---

### fulfillments

Shipment tracking for order fulfillment.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'shipped', 'delivered') | |
| tracking_company | TEXT | Yes | NULL | - | Carrier name |
| tracking_number | TEXT | Yes | NULL | - | Tracking number |
| tracking_url | TEXT | Yes | NULL | - | Tracking page URL |
| shipped_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_fulfillments_order_id` on (`order_id`) - FK lookup, listing fulfillments per order
- `idx_fulfillments_status` on (`status`) - Filtering fulfillments by status
- `idx_fulfillments_tracking` on (`tracking_company`, `tracking_number`) - Tracking number lookup

---

### fulfillment_lines

Links specific order lines and quantities to a fulfillment.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| fulfillment_id | INTEGER | No | - | FK -> fulfillments(id) ON DELETE CASCADE | Parent fulfillment |
| order_line_id | INTEGER | No | - | FK -> order_lines(id) ON DELETE CASCADE | Source order line |
| quantity | INTEGER | No | 1 | - | Units fulfilled from this line |

**Indexes:**

- `idx_fulfillment_lines_fulfillment_id` on (`fulfillment_id`) - FK lookup, listing lines per fulfillment
- `idx_fulfillment_lines_fulfillment_order_line` UNIQUE on (`fulfillment_id`, `order_line_id`) - Prevents duplicate line entries per fulfillment

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

---

status: completed


command: sed -n '480,535p' specs/05-BUSINESS-LOGIC.md && sed -n '929,1090p' specs/05-BUSINESS-LOGIC.md

aggregated_output:
1. Resolve available shipping zones for the address (see Section 9).
2. Verify the selected rate belongs to an applicable zone.
3. Save shipping_method_id on the checkout.
4. Recalculate pricing with the selected shipping rate.
5. Transition status to `shipping_selected`.
6. Dispatch `CheckoutShippingSelected` event.

#### Transition: shipping_selected -> payment_selected

**Method:** `selectPaymentMethod(checkout, paymentMethod)` - validates and stores the selected payment method

**Requires:**
- All previous data is valid (address, shipping method).
- Totals have been calculated and stored.
- `paymentMethod` must be one of: `credit_card`, `paypal`, `bank_transfer`.

**Actions:**
1. Store `payment_method` on the checkout record.
2. Reserve inventory for all cart lines (call InventoryService.reserve for each line).
3. Set `expires_at` on the checkout to current time + 24 hours.
4. Transition status to `payment_selected`.

#### Transition: payment_selected -> completed

**Method:** `completeCheckout(checkout, paymentMethodData)` - returns an Order

**Triggered by:** Customer submitting the payment form (onsite, no external redirect).

**CRITICAL: This transition must be idempotent.** If called multiple times for the same checkout (e.g., double-click), it must not create duplicate orders. Always check if an order already exists for this checkout before proceeding.

**Actions (all within a single database transaction):**

```
PROCEDURE completeCheckout(checkout, paymentMethodData):
    BEGIN TRANSACTION

    1. IF order already exists for this checkout:
           RETURN existing order (idempotent)

    2. CALL MockPaymentProvider.charge(checkout, paymentMethodData)
       IF result.success = false:
           Release reserved inventory
           THROW PaymentFailedException(result.errorCode)

    3. Generate next sequential order number for this store

    4. DETERMINE order statuses based on payment method:
       IF checkout.payment_method IN ('credit_card', 'paypal'):
           orderStatus = 'paid'
           financialStatus = 'paid'
           paymentStatus = 'captured'
           inventoryAction = 'commit'
       ELSE IF checkout.payment_method = 'bank_transfer':
           orderStatus = 'pending'
           financialStatus = 'pending'
           paymentStatus = 'pending'
## 10. Payment Processing (Mock PSP)

### 10.1 Mock Payment Provider Architecture

The platform uses a **Mock Payment Service Provider (PSP)** instead of real payment gateways. All payment processing happens in-process with no external API calls, no webhooks, and no redirects. This enables a fully functional checkout flow for development, testing, and demonstration purposes.

**Interface:** `App\Contracts\PaymentProvider`

Must implement:
- `charge(checkout, paymentMethodData)` - returns a PaymentResult value object (success/failure, reference ID)
- `refund(payment, amount)` - initiates a mock refund, returns a RefundResult

**Service:** `App\Services\PaymentService` - orchestrates payment processing, delegates to MockPaymentProvider.

**Implementation:** `App\Services\Payment\MockPaymentProvider`

### 10.2 Supported Payment Methods

**Enum:** `App\Enums\PaymentMethod` - values: `CreditCard` (`credit_card`), `Paypal` (`paypal`), `BankTransfer` (`bank_transfer`)

| Method | Behavior | Capture Timing |
|---|---|---|
| `credit_card` | Simulates card authorization and immediate capture. Uses magic card numbers for test scenarios. | Auto-capture on checkout (instant) |
| `paypal` | Simulates PayPal approval and immediate capture. Always succeeds (no magic numbers). | Auto-capture on checkout (instant) |
| `bank_transfer` | No charge at checkout. Order created with `financial_status = pending`. Admin manually confirms payment receipt. | Deferred - admin confirms payment |

### 10.3 Magic Card Numbers (Credit Card Testing)

When payment_method is `credit_card`, the mock provider inspects the card number submitted in the checkout form to determine the outcome:

| Card Number | Outcome | PaymentResult |
|---|---|---|
| `4242 4242 4242 4242` | Success | `success = true`, status = `captured` |
| `4000 0000 0000 0002` | Decline | `success = false`, error = `card_declined` |
| `4000 0000 0000 9995` | Insufficient funds | `success = false`, error = `insufficient_funds` |
| Any other valid-looking number | Success | `success = true`, status = `captured` |

The mock provider does **not** validate card expiry, CVC, or cardholder name. These fields exist in the UI for realism but are ignored by the backend.

### 10.4 Mock Payment Flow

```mermaid
sequenceDiagram
    participant Customer
    participant Storefront
    participant Backend
    participant MockPSP

    Customer->>Storefront: Select payment method + enter details
    Storefront->>Backend: POST /checkouts/{id}/pay (payment_method, card details if applicable)
    Backend->>Backend: Reserve inventory (if not already reserved)
    Backend->>MockPSP: charge(checkout, paymentMethodData)

    alt Credit Card / PayPal (instant capture)
        MockPSP-->>Backend: PaymentResult (success/failure)
        alt Success
            Backend->>Backend: Create order (financial_status = paid)
            Backend->>Backend: Commit inventory, mark cart converted
            Backend-->>Storefront: 200 OK (order confirmation)
        else Failure (decline / insufficient funds)
            Backend->>Backend: Release reserved inventory
            Backend-->>Storefront: 422 (error code + message)
        end
    else Bank Transfer (deferred)
        MockPSP-->>Backend: PaymentResult (success = true, status = pending)
        Backend->>Backend: Create order (financial_status = pending)
        Backend->>Backend: Inventory remains reserved (not committed)
        Backend-->>Storefront: 200 OK (order confirmation with bank transfer instructions)
    end
```

### 10.5 Payment State Machine

**Enum:** `App\Enums\PaymentStatus` - values: `pending`, `captured`, `failed`, `refunded`

```mermaid
stateDiagram-v2
    [*] --> captured : Credit card / PayPal (success)
    [*] --> failed : Credit card (decline)
    [*] --> pending : Bank transfer (awaiting payment)
    pending --> captured : Admin confirms payment received
    pending --> failed : Auto-cancel job (unpaid timeout)
    captured --> refunded : Full refund processed
```

### 10.6 Bank Transfer Instructions

When `payment_method = bank_transfer`, the order confirmation page and email display mock bank transfer details:

| Field | Value |
|---|---|
| Bank name | Mock Bank AG |
| IBAN | DE89 3704 0044 0532 0130 00 |
| BIC | COBADEFFXXX |
| Reference | Order number (e.g., #1001) |
| Amount | Order total formatted in EUR |

These are static mock values. No real bank interaction occurs.

### 10.7 Admin: Confirm Payment (Bank Transfer)

Admin users can confirm that a bank transfer payment has been received via a "Confirm Payment" button on the order detail page (visible only when `payment_method = bank_transfer` AND `financial_status = pending`).

**Action flow:**
1. Admin clicks "Confirm Payment" button.
2. Backend updates `payments.status` from `pending` to `captured`.
3. Backend updates `orders.financial_status` from `pending` to `paid`.
4. Backend updates `orders.status` from `pending` to `paid`.
5. Backend calls `InventoryService.commit()` for each order line (converts reserved stock to committed).
6. If all order lines are digital (`requires_shipping = false`): auto-create fulfillment with `status = delivered` (see Section 11.7).
7. Dispatch `OrderPaid` event.

### 10.8 Auto-Cancel Unpaid Bank Transfer Orders

**Job:** `App\Jobs\CancelUnpaidBankTransferOrders`

- Schedule: daily
- Finds orders where `payment_method = bank_transfer` AND `financial_status = pending` AND `placed_at < (now - configurable days)`.
- Configurable timeout: `store_settings.settings_json` key `bank_transfer_cancel_days`, default = 7.

**Cancel action:**
1. Release reserved inventory via `InventoryService.release()`.
2. Update `orders.financial_status` to `voided`.
3. Update `orders.status` to `cancelled`.
4. Update `payments.status` to `failed`.
5. Dispatch `OrderCancelled` event.

---

## 11. Order Management

### 11.1 Order Creation (from Completed Checkout)

See Section 6.2 "Transition: payment_selected -> completed" for the full procedure. The order creation is the central action within the completeCheckout transaction.

**Key requirements for the order record:**
- Snapshot all checkout data (addresses, email, totals) at time of order creation.
- Order lines must contain snapshot data (`title_snapshot`, `sku_snapshot`) so that order history survives product changes.
- The mock payment response is encrypted before storage.
- The entire operation must be atomic (single database transaction).
- The `payment_method` from the checkout is copied to the order record.

**Payment-method-specific order creation:**

| Payment Method | `orders.status` | `orders.financial_status` | Inventory Action | Payment Record Status |
|---|---|---|---|---|
| `credit_card` | `paid` | `paid` | Commit (on_hand and reserved both decremented) | `captured` |
| `paypal` | `paid` | `paid` | Commit (on_hand and reserved both decremented) | `captured` |
| `bank_transfer` | `pending` | `pending` | Reserve only (reserved incremented, on_hand unchanged) | `pending` |

### 11.2 Order Numbering

- Sequential per store, starting at 1001.
- Format: configurable prefix (default: `#`) stored in `store_settings.settings_json` key `order_number_prefix`.
- Generation: within the order creation transaction, find the maximum existing order number for the store and increment by 1.
- Since this runs inside a SQLite transaction, there is no risk of duplicate numbers.

### 11.3 Order Statuses

Three independent status dimensions tracked on each order:

**Order status** (`orders.status`):


command: sed -n '1090,1278p' specs/05-BUSINESS-LOGIC.md

aggregated_output:
**Order status** (`orders.status`):

| Status | Description |
|---|---|
| `pending` | Order created, awaiting processing |
| `paid` | Payment confirmed |
| `fulfilled` | All items shipped |
| `cancelled` | Order cancelled before fulfillment |
| `refunded` | Fully refunded |

**Financial status** (`orders.financial_status`):

| Status | Description |
|---|---|
| `pending` | Payment not yet confirmed |
| `authorized` | Payment authorized but not captured |
| `paid` | Payment captured successfully |
| `partially_refunded` | Some items or partial amount refunded |
| `refunded` | Fully refunded |
| `voided` | Authorization voided (no capture occurred) |

**Fulfillment status** (`orders.fulfillment_status`):

| Status | Description |
|---|---|
| `unfulfilled` | No items have been shipped |
| `partial` | Some items shipped, others pending |
| `fulfilled` | All items shipped |

### 11.4 Refund Processing

**Service:** `App\Services\RefundService`

**Input (RefundRequest):**

| Field | Type | Required | Description |
|---|---|---|---|
| `amount` | integer (cents) | No | Custom refund amount. If not set, calculated from lines. |
| `lines` | map of order_line_id to quantity | No | Which lines and quantities to refund (for line-level refunds). |
| `reason` | string | No | Text reason for the refund. |
| `restock` | boolean | No | Whether to restock inventory for refunded items. |

**Process:**

```
PROCEDURE processRefund(order, request):
    1. Calculate remaining refundable amount:
       refundable = order.total_amount - SUM(existing refunds for this order)

    2. IF request.amount is set:
           VALIDATE request.amount <= refundable
       ELSE IF request.lines is set:
           CALCULATE amount from specified lines and quantities
       ELSE:
           amount = refundable (full refund)

    3. CALL PaymentProvider.refund(order's payment, amount)

    4. CREATE Refund record with status = 'pending'

    5. On provider confirmation:
       UPDATE refund status = 'processed'

    6. Calculate total refunded for this order:
       IF total refunded = order.total_amount:
           SET order.financial_status = 'refunded'
           SET order.status = 'refunded'
       ELSE:
           SET order.financial_status = 'partially_refunded'

    7. IF request.restock = true:
       FOR EACH refunded line and quantity:
           CALL InventoryService.restock(variant, quantity)

    8. DISPATCH OrderRefunded event
```

### 11.5 Fulfillment

**Service:** `App\Services\FulfillmentService`

#### Fulfillment Guard

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

| `financial_status` | Fulfillment Allowed? | Reason |
|---|---|---|
| `pending` | **No** | Payment not yet received (e.g., bank transfer awaiting confirmation) |
| `authorized` | **No** | Payment authorized but not captured |
| `paid` | **Yes** | Payment confirmed |
| `partially_refunded` | **Yes** | Payment confirmed, partial refund does not block shipment |
| `refunded` | **No** | Fully refunded, nothing to ship |
| `voided` | **No** | Payment voided / cancelled |

When fulfillment is blocked by the guard, the admin UI shows an info callout: "Fulfillment cannot be created until payment is confirmed." The "Create fulfillment" button is disabled.

#### Creating a Fulfillment

Input:
- `lines` - map of order_line_id to quantity, specifying which items are in this shipment
- `trackingData` - tracking_company, tracking_number, tracking_url

**Process:**

```
PROCEDURE createFulfillment(order, lines, trackingData):
    1. GUARD: Verify order.financial_status IN ('paid', 'partially_refunded')
       IF NOT: throw FulfillmentGuardException

    2. FOR EACH line in request:
       fulfilledSoFar = SUM(fulfillment_lines quantities for this order_line)
       unfulfilled = order_line.quantity - fulfilledSoFar
       VALIDATE requested quantity <= unfulfilled

    3. CREATE Fulfillment record with status = 'pending' and tracking data

    4. CREATE fulfillment_lines for each order line and quantity

    5. Determine new fulfillment status:
       FOR EACH order_line in order:
           totalFulfilled = SUM(all fulfillment_lines for this order_line)
           IF totalFulfilled < order_line.quantity:
               allFulfilled = false

       IF allFulfilled:
           SET order.fulfillment_status = 'fulfilled'
           SET order.status = 'fulfilled'
       ELSE:
           SET order.fulfillment_status = 'partial'

    6. DISPATCH FulfillmentCreated event
```

#### Fulfillment Status Transitions (Admin Button Actions)

Fulfillment status transitions are triggered by explicit admin button clicks. There are no automatic transitions for physical products.

```mermaid
stateDiagram-v2
    [*] --> pending : Fulfillment created (admin clicks "Create fulfillment")
    pending --> shipped : Admin clicks "Mark as shipped"
    shipped --> delivered : Admin clicks "Mark as delivered"

    note right of pending : Fulfillment record created with tracking data
    note right of shipped : Sets shipped_at timestamp, sends tracking email to customer
    note right of delivered : Sets delivered_at timestamp
```

**Admin buttons on each fulfillment card:**

| Button | Shown When | Action |
|---|---|---|
| "Mark as shipped" | `fulfillment.status = pending` | Transitions to `shipped`, sets `shipped_at`, dispatches `FulfillmentShipped` event, sends tracking email |
| "Mark as delivered" | `fulfillment.status = shipped` | Transitions to `delivered`, dispatches `FulfillmentDelivered` event |

When transitioning to `shipped`:
- Set `shipped_at` timestamp.
- Dispatch `FulfillmentShipped` event.
- Send email notification to customer with tracking information.

When transitioning to `delivered`:
- Dispatch `FulfillmentDelivered` event.

### 11.6 Fulfillment Guard - Inventory Behavior per Payment Method

The fulfillment guard interacts with inventory differently depending on the payment method:

| Payment Method | At Order Creation | At Payment Confirmation | At Fulfillment |
|---|---|---|---|
| `credit_card` / `paypal` | Inventory committed (on_hand decremented, reserved decremented) | N/A (already committed) | No inventory change |
| `bank_transfer` | Inventory reserved only (reserved incremented) | Inventory committed (on_hand decremented, reserved decremented) | No inventory change |

For bank transfer orders, inventory is held in a reserved state until the admin confirms payment. If the order is auto-cancelled (see Section 10.8), the reserved inventory is released back.

### 11.7 Auto-Fulfillment for Digital Products

When a payment is confirmed (either instant for credit_card/paypal, or via admin confirmation for bank_transfer):

1. Check if **all** order lines reference variants with `requires_shipping = false` (digital products).
2. If yes: automatically create a fulfillment record covering all order lines:
   - `status = 'delivered'` (skip pending and shipped)
   - `tracking_company = null`, `tracking_number = null`, `tracking_url = null`
   - `shipped_at = now`
   - Set `order.fulfillment_status = 'fulfilled'`
   - Set `order.status = 'fulfilled'`
3. If the order contains a mix of physical and digital products: do **not** auto-fulfill. The admin must create fulfillments manually (they can fulfill digital items immediately and physical items when shipped).

This ensures digital product orders are immediately marked as complete after payment.


command: sed -n '599,640p' specs/09-IMPLEMENTATION-ROADMAP.md

aggregated_output:
### Phase 5: Payments, Orders, Fulfillment

**Priority:** HIGH - completes the purchase flow.

#### Step 5.1: Migrations (Batch 5-7)

1. `create_customers_table` - FK: `store_id`
2. `create_customer_addresses_table` - FK: `customer_id`
3. `create_orders_table` - FK: `store_id`, `customer_id`
4. `create_order_lines_table` - FK: `order_id`, `product_id` (nullable), `variant_id` (nullable)
5. `create_payments_table` - FK: `order_id`
6. `create_refunds_table` - FK: `order_id`, `payment_id`
7. `create_fulfillments_table` - FK: `order_id`
8. `create_fulfillment_lines_table` - FK: `fulfillment_id`, `order_line_id`

#### Step 5.2: Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Customer` | Store customer entity |
| Model (with migration, factory, seeder) | `CustomerAddress` | Customer address |
| Model (with migration, factory, seeder) | `Order` | Order entity |
| Model (with migration, factory, seeder) | `OrderLine` | Order line item |
| Model (with migration, factory, seeder) | `Payment` | Payment record |
| Model (with migration, factory, seeder) | `Refund` | Refund record |
| Model (with migration, factory, seeder) | `Fulfillment` | Fulfillment record |
| Model (with migration, factory, seeder) | `FulfillmentLine` | Fulfillment line item |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Customer` | `belongsTo(Store)`, `hasMany(CustomerAddress)`, `hasMany(Order)`, `hasMany(Cart)` |
| `CustomerAddress` | `belongsTo(Customer)` |
| `Order` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(OrderLine)`, `hasMany(Payment)`, `hasMany(Refund)`, `hasMany(Fulfillment)` |
| `OrderLine` | `belongsTo(Order)`, `belongsTo(Product)` (nullable), `belongsTo(ProductVariant)` (nullable) |
| `Payment` | `belongsTo(Order)` |
| `Refund` | `belongsTo(Order)`, `belongsTo(Payment)` |
| `Fulfillment` | `belongsTo(Order)`, `hasMany(FulfillmentLine)` |
| `FulfillmentLine` | `belongsTo(Fulfillment)`, `belongsTo(OrderLine)` |


output:
Chunk ID: 8b07c7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2153
Output:
### orders

Placed orders with financial and fulfillment tracking.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for guest orders |
| order_number | TEXT | No | - | UNIQUE per (store_id, order_number) | Human-readable order number |
| payment_method | TEXT | No | - | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Payment method used for this order |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'paid', 'fulfilled', 'cancelled', 'refunded') | Overall order status |
| financial_status | TEXT | No | 'pending' | CHECK IN ('pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided') | Payment status |
| fulfillment_status | TEXT | No | 'unfulfilled' | CHECK IN ('unfulfilled', 'partial', 'fulfilled') | Shipping status |
| currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
| subtotal_amount | INTEGER | No | 0 | - | Sum of line items in minor units |
| discount_amount | INTEGER | No | 0 | - | Total discount in minor units |
| shipping_amount | INTEGER | No | 0 | - | Shipping cost in minor units |
| tax_amount | INTEGER | No | 0 | - | Total tax in minor units |
| total_amount | INTEGER | No | 0 | - | Grand total in minor units |
| email | TEXT | Yes | NULL | - | Contact email at time of order |
| billing_address_json | TEXT | Yes | NULL | - | JSON address snapshot |
| shipping_address_json | TEXT | Yes | NULL | - | JSON address snapshot |
| placed_at | TEXT | Yes | NULL | - | ISO-8601 timestamp of placement |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_orders_store_order_number` UNIQUE on (`store_id`, `order_number`) - Unique order number per store
- `idx_orders_store_id` on (`store_id`) - FK lookup
- `idx_orders_customer_id` on (`customer_id`) - Listing orders per customer
- `idx_orders_store_status` on (`store_id`, `status`) - Filtering by overall status
- `idx_orders_store_financial` on (`store_id`, `financial_status`) - Filtering by payment status
- `idx_orders_store_fulfillment` on (`store_id`, `fulfillment_status`) - Filtering by shipping status
- `idx_orders_placed_at` on (`store_id`, `placed_at`) - Date-range order queries

---

### order_lines

Individual items within an order. Contains snapshots of product data at time of purchase.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| product_id | INTEGER | Yes | NULL | FK -> products(id) ON DELETE SET NULL | Nullable: survives product deletion |
| variant_id | INTEGER | Yes | NULL | FK -> product_variants(id) ON DELETE SET NULL | Nullable: survives variant deletion |
| title_snapshot | TEXT | No | - | - | Product title at time of purchase |
| sku_snapshot | TEXT | Yes | NULL | - | SKU at time of purchase |
| quantity | INTEGER | No | 1 | - | Number of units |
| unit_price_amount | INTEGER | No | 0 | - | Price per unit in minor units |
| total_amount | INTEGER | No | 0 | - | Line total in minor units |
| tax_lines_json | TEXT | No | '[]' | - | JSON array of tax line items |
| discount_allocations_json | TEXT | No | '[]' | - | JSON array of discount allocations |

**Indexes:**

- `idx_order_lines_order_id` on (`order_id`) - FK lookup, listing lines per order
- `idx_order_lines_product_id` on (`product_id`) - Finding order lines by product
- `idx_order_lines_variant_id` on (`variant_id`) - Finding order lines by variant

**Notes:** `product_id` and `variant_id` are nullable with SET NULL on delete so that order history survives product deletion. `tax_lines_json` example: `[{"title": "State Tax", "rate": 800, "amount": 400}]` (rate in basis points, e.g. 800 = 8%). `discount_allocations_json` example: `[{"discount_id": 5, "amount": 250}]`.

---

### payments

Payment attempts and captures for an order.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| provider | TEXT | No | 'mock' | CHECK IN ('mock') | Payment provider (mock PSP only) |
| method | TEXT | No | - | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Payment method used |
| provider_payment_id | TEXT | Yes | NULL | - | Mock payment reference ID |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'captured', 'failed', 'refunded') | |
| amount | INTEGER | No | 0 | - | Payment amount in minor units |
| currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
| raw_json_encrypted | TEXT | Yes | NULL | - | Encrypted provider response (PCI compliance) |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_payments_order_id` on (`order_id`) - FK lookup, listing payments per order
- `idx_payments_provider_id` on (`provider`, `provider_payment_id`) - Payment reconciliation by provider ID
- `idx_payments_method` on (`method`) - Filtering payments by method
- `idx_payments_status` on (`status`) - Filtering payments by status

**Notes:** `raw_json_encrypted` stores the mock payment provider response, encrypted at the application layer. `amount` is in minor units (cents). The `provider` is always `mock` since the platform uses a mock PSP (no real payment gateway integration). The `method` column records which payment method the customer selected (credit_card, paypal, or bank_transfer).

---

### refunds

Refund records linked to an order and payment.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| payment_id | INTEGER | No | - | FK -> payments(id) ON DELETE CASCADE | Source payment |
| amount | INTEGER | No | 0 | - | Refund amount in minor units |
| reason | TEXT | Yes | NULL | - | Refund reason |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'processed', 'failed') | |
| provider_refund_id | TEXT | Yes | NULL | - | External refund ID |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_refunds_order_id` on (`order_id`) - FK lookup, listing refunds per order
- `idx_refunds_payment_id` on (`payment_id`) - Finding refunds per payment
- `idx_refunds_status` on (`status`) - Filtering refunds by status

---

### fulfillments

Shipment tracking for order fulfillment.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'shipped', 'delivered') | |
| tracking_company | TEXT | Yes | NULL | - | Carrier name |
| tracking_number | TEXT | Yes | NULL | - | Tracking number |
| tracking_url | TEXT | Yes | NULL | - | Tracking page URL |
| shipped_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_fulfillments_order_id` on (`order_id`) - FK lookup, listing fulfillments per order
- `idx_fulfillments_status` on (`status`) - Filtering fulfillments by status
- `idx_fulfillments_tracking` on (`tracking_company`, `tracking_number`) - Tracking number lookup

---

### fulfillment_lines

Links specific order lines and quantities to a fulfillment.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| fulfillment_id | INTEGER | No | - | FK -> fulfillments(id) ON DELETE CASCADE | Parent fulfillment |
| order_line_id | INTEGER | No | - | FK -> order_lines(id) ON DELETE CASCADE | Source order line |
| quantity | INTEGER | No | 1 | - | Units fulfilled from this line |

**Indexes:**

- `idx_fulfillment_lines_fulfillment_id` on (`fulfillment_id`) - FK lookup, listing lines per fulfillment
- `idx_fulfillment_lines_fulfillment_order_line` UNIQUE on (`fulfillment_id`, `order_line_id`) - Prevents duplicate line entries per fulfillment

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

---


output:
Chunk ID: 52b9e3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2461
Output:
1. Resolve available shipping zones for the address (see Section 9).
2. Verify the selected rate belongs to an applicable zone.
3. Save shipping_method_id on the checkout.
4. Recalculate pricing with the selected shipping rate.
5. Transition status to `shipping_selected`.
6. Dispatch `CheckoutShippingSelected` event.

#### Transition: shipping_selected -> payment_selected

**Method:** `selectPaymentMethod(checkout, paymentMethod)` - validates and stores the selected payment method

**Requires:**
- All previous data is valid (address, shipping method).
- Totals have been calculated and stored.
- `paymentMethod` must be one of: `credit_card`, `paypal`, `bank_transfer`.

**Actions:**
1. Store `payment_method` on the checkout record.
2. Reserve inventory for all cart lines (call InventoryService.reserve for each line).
3. Set `expires_at` on the checkout to current time + 24 hours.
4. Transition status to `payment_selected`.

#### Transition: payment_selected -> completed

**Method:** `completeCheckout(checkout, paymentMethodData)` - returns an Order

**Triggered by:** Customer submitting the payment form (onsite, no external redirect).

**CRITICAL: This transition must be idempotent.** If called multiple times for the same checkout (e.g., double-click), it must not create duplicate orders. Always check if an order already exists for this checkout before proceeding.

**Actions (all within a single database transaction):**

```
PROCEDURE completeCheckout(checkout, paymentMethodData):
    BEGIN TRANSACTION

    1. IF order already exists for this checkout:
           RETURN existing order (idempotent)

    2. CALL MockPaymentProvider.charge(checkout, paymentMethodData)
       IF result.success = false:
           Release reserved inventory
           THROW PaymentFailedException(result.errorCode)

    3. Generate next sequential order number for this store

    4. DETERMINE order statuses based on payment method:
       IF checkout.payment_method IN ('credit_card', 'paypal'):
           orderStatus = 'paid'
           financialStatus = 'paid'
           paymentStatus = 'captured'
           inventoryAction = 'commit'
       ELSE IF checkout.payment_method = 'bank_transfer':
           orderStatus = 'pending'
           financialStatus = 'pending'
           paymentStatus = 'pending'
## 10. Payment Processing (Mock PSP)

### 10.1 Mock Payment Provider Architecture

The platform uses a **Mock Payment Service Provider (PSP)** instead of real payment gateways. All payment processing happens in-process with no external API calls, no webhooks, and no redirects. This enables a fully functional checkout flow for development, testing, and demonstration purposes.

**Interface:** `App\Contracts\PaymentProvider`

Must implement:
- `charge(checkout, paymentMethodData)` - returns a PaymentResult value object (success/failure, reference ID)
- `refund(payment, amount)` - initiates a mock refund, returns a RefundResult

**Service:** `App\Services\PaymentService` - orchestrates payment processing, delegates to MockPaymentProvider.

**Implementation:** `App\Services\Payment\MockPaymentProvider`

### 10.2 Supported Payment Methods

**Enum:** `App\Enums\PaymentMethod` - values: `CreditCard` (`credit_card`), `Paypal` (`paypal`), `BankTransfer` (`bank_transfer`)

| Method | Behavior | Capture Timing |
|---|---|---|
| `credit_card` | Simulates card authorization and immediate capture. Uses magic card numbers for test scenarios. | Auto-capture on checkout (instant) |
| `paypal` | Simulates PayPal approval and immediate capture. Always succeeds (no magic numbers). | Auto-capture on checkout (instant) |
| `bank_transfer` | No charge at checkout. Order created with `financial_status = pending`. Admin manually confirms payment receipt. | Deferred - admin confirms payment |

### 10.3 Magic Card Numbers (Credit Card Testing)

When payment_method is `credit_card`, the mock provider inspects the card number submitted in the checkout form to determine the outcome:

| Card Number | Outcome | PaymentResult |
|---|---|---|
| `4242 4242 4242 4242` | Success | `success = true`, status = `captured` |
| `4000 0000 0000 0002` | Decline | `success = false`, error = `card_declined` |
| `4000 0000 0000 9995` | Insufficient funds | `success = false`, error = `insufficient_funds` |
| Any other valid-looking number | Success | `success = true`, status = `captured` |

The mock provider does **not** validate card expiry, CVC, or cardholder name. These fields exist in the UI for realism but are ignored by the backend.

### 10.4 Mock Payment Flow

```mermaid
sequenceDiagram
    participant Customer
    participant Storefront
    participant Backend
    participant MockPSP

    Customer->>Storefront: Select payment method + enter details
    Storefront->>Backend: POST /checkouts/{id}/pay (payment_method, card details if applicable)
    Backend->>Backend: Reserve inventory (if not already reserved)
    Backend->>MockPSP: charge(checkout, paymentMethodData)

    alt Credit Card / PayPal (instant capture)
        MockPSP-->>Backend: PaymentResult (success/failure)
        alt Success
            Backend->>Backend: Create order (financial_status = paid)
            Backend->>Backend: Commit inventory, mark cart converted
            Backend-->>Storefront: 200 OK (order confirmation)
        else Failure (decline / insufficient funds)
            Backend->>Backend: Release reserved inventory
            Backend-->>Storefront: 422 (error code + message)
        end
    else Bank Transfer (deferred)
        MockPSP-->>Backend: PaymentResult (success = true, status = pending)
        Backend->>Backend: Create order (financial_status = pending)
        Backend->>Backend: Inventory remains reserved (not committed)
        Backend-->>Storefront: 200 OK (order confirmation with bank transfer instructions)
    end
```

### 10.5 Payment State Machine

**Enum:** `App\Enums\PaymentStatus` - values: `pending`, `captured`, `failed`, `refunded`

```mermaid
stateDiagram-v2
    [*] --> captured : Credit card / PayPal (success)
    [*] --> failed : Credit card (decline)
    [*] --> pending : Bank transfer (awaiting payment)
    pending --> captured : Admin confirms payment received
    pending --> failed : Auto-cancel job (unpaid timeout)
    captured --> refunded : Full refund processed
```

### 10.6 Bank Transfer Instructions

When `payment_method = bank_transfer`, the order confirmation page and email display mock bank transfer details:

| Field | Value |
|---|---|
| Bank name | Mock Bank AG |
| IBAN | DE89 3704 0044 0532 0130 00 |
| BIC | COBADEFFXXX |
| Reference | Order number (e.g., #1001) |
| Amount | Order total formatted in EUR |

These are static mock values. No real bank interaction occurs.

### 10.7 Admin: Confirm Payment (Bank Transfer)

Admin users can confirm that a bank transfer payment has been received via a "Confirm Payment" button on the order detail page (visible only when `payment_method = bank_transfer` AND `financial_status = pending`).

**Action flow:**
1. Admin clicks "Confirm Payment" button.
2. Backend updates `payments.status` from `pending` to `captured`.
3. Backend updates `orders.financial_status` from `pending` to `paid`.
4. Backend updates `orders.status` from `pending` to `paid`.
5. Backend calls `InventoryService.commit()` for each order line (converts reserved stock to committed).
6. If all order lines are digital (`requires_shipping = false`): auto-create fulfillment with `status = delivered` (see Section 11.7).
7. Dispatch `OrderPaid` event.

### 10.8 Auto-Cancel Unpaid Bank Transfer Orders

**Job:** `App\Jobs\CancelUnpaidBankTransferOrders`

- Schedule: daily
- Finds orders where `payment_method = bank_transfer` AND `financial_status = pending` AND `placed_at < (now - configurable days)`.
- Configurable timeout: `store_settings.settings_json` key `bank_transfer_cancel_days`, default = 7.

**Cancel action:**
1. Release reserved inventory via `InventoryService.release()`.
2. Update `orders.financial_status` to `voided`.
3. Update `orders.status` to `cancelled`.
4. Update `payments.status` to `failed`.
5. Dispatch `OrderCancelled` event.

---

## 11. Order Management

### 11.1 Order Creation (from Completed Checkout)

See Section 6.2 "Transition: payment_selected -> completed" for the full procedure. The order creation is the central action within the completeCheckout transaction.

**Key requirements for the order record:**
- Snapshot all checkout data (addresses, email, totals) at time of order creation.
- Order lines must contain snapshot data (`title_snapshot`, `sku_snapshot`) so that order history survives product changes.
- The mock payment response is encrypted before storage.
- The entire operation must be atomic (single database transaction).
- The `payment_method` from the checkout is copied to the order record.

**Payment-method-specific order creation:**

| Payment Method | `orders.status` | `orders.financial_status` | Inventory Action | Payment Record Status |
|---|---|---|---|---|
| `credit_card` | `paid` | `paid` | Commit (on_hand and reserved both decremented) | `captured` |
| `paypal` | `paid` | `paid` | Commit (on_hand and reserved both decremented) | `captured` |
| `bank_transfer` | `pending` | `pending` | Reserve only (reserved incremented, on_hand unchanged) | `pending` |

### 11.2 Order Numbering

- Sequential per store, starting at 1001.
- Format: configurable prefix (default: `#`) stored in `store_settings.settings_json` key `order_number_prefix`.
- Generation: within the order creation transaction, find the maximum existing order number for the store and increment by 1.
- Since this runs inside a SQLite transaction, there is no risk of duplicate numbers.

### 11.3 Order Statuses

Three independent status dimensions tracked on each order:

**Order status** (`orders.status`):


output:
Chunk ID: 8272d5
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1883
Output:
**Order status** (`orders.status`):

| Status | Description |
|---|---|
| `pending` | Order created, awaiting processing |
| `paid` | Payment confirmed |
| `fulfilled` | All items shipped |
| `cancelled` | Order cancelled before fulfillment |
| `refunded` | Fully refunded |

**Financial status** (`orders.financial_status`):

| Status | Description |
|---|---|
| `pending` | Payment not yet confirmed |
| `authorized` | Payment authorized but not captured |
| `paid` | Payment captured successfully |
| `partially_refunded` | Some items or partial amount refunded |
| `refunded` | Fully refunded |
| `voided` | Authorization voided (no capture occurred) |

**Fulfillment status** (`orders.fulfillment_status`):

| Status | Description |
|---|---|
| `unfulfilled` | No items have been shipped |
| `partial` | Some items shipped, others pending |
| `fulfilled` | All items shipped |

### 11.4 Refund Processing

**Service:** `App\Services\RefundService`

**Input (RefundRequest):**

| Field | Type | Required | Description |
|---|---|---|---|
| `amount` | integer (cents) | No | Custom refund amount. If not set, calculated from lines. |
| `lines` | map of order_line_id to quantity | No | Which lines and quantities to refund (for line-level refunds). |
| `reason` | string | No | Text reason for the refund. |
| `restock` | boolean | No | Whether to restock inventory for refunded items. |

**Process:**

```
PROCEDURE processRefund(order, request):
    1. Calculate remaining refundable amount:
       refundable = order.total_amount - SUM(existing refunds for this order)

    2. IF request.amount is set:
           VALIDATE request.amount <= refundable
       ELSE IF request.lines is set:
           CALCULATE amount from specified lines and quantities
       ELSE:
           amount = refundable (full refund)

    3. CALL PaymentProvider.refund(order's payment, amount)

    4. CREATE Refund record with status = 'pending'

    5. On provider confirmation:
       UPDATE refund status = 'processed'

    6. Calculate total refunded for this order:
       IF total refunded = order.total_amount:
           SET order.financial_status = 'refunded'
           SET order.status = 'refunded'
       ELSE:
           SET order.financial_status = 'partially_refunded'

    7. IF request.restock = true:
       FOR EACH refunded line and quantity:
           CALL InventoryService.restock(variant, quantity)

    8. DISPATCH OrderRefunded event
```

### 11.5 Fulfillment

**Service:** `App\Services\FulfillmentService`

#### Fulfillment Guard

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

| `financial_status` | Fulfillment Allowed? | Reason |
|---|---|---|
| `pending` | **No** | Payment not yet received (e.g., bank transfer awaiting confirmation) |
| `authorized` | **No** | Payment authorized but not captured |
| `paid` | **Yes** | Payment confirmed |
| `partially_refunded` | **Yes** | Payment confirmed, partial refund does not block shipment |
| `refunded` | **No** | Fully refunded, nothing to ship |
| `voided` | **No** | Payment voided / cancelled |

When fulfillment is blocked by the guard, the admin UI shows an info callout: "Fulfillment cannot be created until payment is confirmed." The "Create fulfillment" button is disabled.

#### Creating a Fulfillment

Input:
- `lines` - map of order_line_id to quantity, specifying which items are in this shipment
- `trackingData` - tracking_company, tracking_number, tracking_url

**Process:**

```
PROCEDURE createFulfillment(order, lines, trackingData):
    1. GUARD: Verify order.financial_status IN ('paid', 'partially_refunded')
       IF NOT: throw FulfillmentGuardException

    2. FOR EACH line in request:
       fulfilledSoFar = SUM(fulfillment_lines quantities for this order_line)
       unfulfilled = order_line.quantity - fulfilledSoFar
       VALIDATE requested quantity <= unfulfilled

    3. CREATE Fulfillment record with status = 'pending' and tracking data

    4. CREATE fulfillment_lines for each order line and quantity

    5. Determine new fulfillment status:
       FOR EACH order_line in order:
           totalFulfilled = SUM(all fulfillment_lines for this order_line)
           IF totalFulfilled < order_line.quantity:
               allFulfilled = false

       IF allFulfilled:
           SET order.fulfillment_status = 'fulfilled'
           SET order.status = 'fulfilled'
       ELSE:
           SET order.fulfillment_status = 'partial'

    6. DISPATCH FulfillmentCreated event
```

#### Fulfillment Status Transitions (Admin Button Actions)

Fulfillment status transitions are triggered by explicit admin button clicks. There are no automatic transitions for physical products.

```mermaid
stateDiagram-v2
    [*] --> pending : Fulfillment created (admin clicks "Create fulfillment")
    pending --> shipped : Admin clicks "Mark as shipped"
    shipped --> delivered : Admin clicks "Mark as delivered"

    note right of pending : Fulfillment record created with tracking data
    note right of shipped : Sets shipped_at timestamp, sends tracking email to customer
    note right of delivered : Sets delivered_at timestamp
```

**Admin buttons on each fulfillment card:**

| Button | Shown When | Action |
|---|---|---|
| "Mark as shipped" | `fulfillment.status = pending` | Transitions to `shipped`, sets `shipped_at`, dispatches `FulfillmentShipped` event, sends tracking email |
| "Mark as delivered" | `fulfillment.status = shipped` | Transitions to `delivered`, dispatches `FulfillmentDelivered` event |

When transitioning to `shipped`:
- Set `shipped_at` timestamp.
- Dispatch `FulfillmentShipped` event.
- Send email notification to customer with tracking information.

When transitioning to `delivered`:
- Dispatch `FulfillmentDelivered` event.

### 11.6 Fulfillment Guard - Inventory Behavior per Payment Method

The fulfillment guard interacts with inventory differently depending on the payment method:

| Payment Method | At Order Creation | At Payment Confirmation | At Fulfillment |
|---|---|---|---|
| `credit_card` / `paypal` | Inventory committed (on_hand decremented, reserved decremented) | N/A (already committed) | No inventory change |
| `bank_transfer` | Inventory reserved only (reserved incremented) | Inventory committed (on_hand decremented, reserved decremented) | No inventory change |

For bank transfer orders, inventory is held in a reserved state until the admin confirms payment. If the order is auto-cancelled (see Section 10.8), the reserved inventory is released back.

### 11.7 Auto-Fulfillment for Digital Products

When a payment is confirmed (either instant for credit_card/paypal, or via admin confirmation for bank_transfer):

1. Check if **all** order lines reference variants with `requires_shipping = false` (digital products).
2. If yes: automatically create a fulfillment record covering all order lines:
   - `status = 'delivered'` (skip pending and shipped)
   - `tracking_company = null`, `tracking_number = null`, `tracking_url = null`
   - `shipped_at = now`
   - Set `order.fulfillment_status = 'fulfilled'`
   - Set `order.status = 'fulfilled'`
3. If the order contains a mix of physical and digital products: do **not** auto-fulfill. The admin must create fulfillments manually (they can fulfill digital items immediately and physical items when shipped).

This ensures digital product orders are immediately marked as complete after payment.


output:
Chunk ID: 8ea4b0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 514
Output:
### Phase 5: Payments, Orders, Fulfillment

**Priority:** HIGH - completes the purchase flow.

#### Step 5.1: Migrations (Batch 5-7)

1. `create_customers_table` - FK: `store_id`
2. `create_customer_addresses_table` - FK: `customer_id`
3. `create_orders_table` - FK: `store_id`, `customer_id`
4. `create_order_lines_table` - FK: `order_id`, `product_id` (nullable), `variant_id` (nullable)
5. `create_payments_table` - FK: `order_id`
6. `create_refunds_table` - FK: `order_id`, `payment_id`
7. `create_fulfillments_table` - FK: `order_id`
8. `create_fulfillment_lines_table` - FK: `fulfillment_id`, `order_line_id`

#### Step 5.2: Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Customer` | Store customer entity |
| Model (with migration, factory, seeder) | `CustomerAddress` | Customer address |
| Model (with migration, factory, seeder) | `Order` | Order entity |
| Model (with migration, factory, seeder) | `OrderLine` | Order line item |
| Model (with migration, factory, seeder) | `Payment` | Payment record |
| Model (with migration, factory, seeder) | `Refund` | Refund record |
| Model (with migration, factory, seeder) | `Fulfillment` | Fulfillment record |
| Model (with migration, factory, seeder) | `FulfillmentLine` | Fulfillment line item |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Customer` | `belongsTo(Store)`, `hasMany(CustomerAddress)`, `hasMany(Order)`, `hasMany(Cart)` |
| `CustomerAddress` | `belongsTo(Customer)` |
| `Order` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(OrderLine)`, `hasMany(Payment)`, `hasMany(Refund)`, `hasMany(Fulfillment)` |
| `OrderLine` | `belongsTo(Order)`, `belongsTo(Product)` (nullable), `belongsTo(ProductVariant)` (nullable) |
| `Payment` | `belongsTo(Order)` |
| `Refund` | `belongsTo(Order)`, `belongsTo(Payment)` |
| `Fulfillment` | `belongsTo(Order)`, `hasMany(FulfillmentLine)` |
| `FulfillmentLine` | `belongsTo(Fulfillment)`, `belongsTo(OrderLine)` |


cmd: ls app/Enums && for f in app/Enums/*.php; do printf '%s\n' "$f"; sed -n '1,120p' "$f"; done


cmd: sed -n '1,220p' app/Services/InventoryService.php


cmd: sed -n '1,220p' database/migrations/2026_05_03_225409_create_checkouts_table.php && sed -n '1,220p' database/migrations/2026_05_03_225427_create_discounts_table.php