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)` |


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


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


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


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

<?php

namespace App\Services;

use App\Enums\InventoryPolicy;
use App\Exceptions\InsufficientInventoryException;
use App\Models\InventoryItem;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

class InventoryService
{
    public function checkAvailability(InventoryItem $item, int $quantity): bool
    {
        $this->assertPositiveQuantity($quantity);

        if ($item->policy === InventoryPolicy::Continue) {
            return true;
        }

        return $this->available($item) >= $quantity;
    }

    public function reserve(InventoryItem $item, int $quantity): void
    {
        $this->assertPositiveQuantity($quantity);

        DB::transaction(function () use ($item, $quantity): void {
            $locked = $this->freshItem($item);

            if (! $this->checkAvailability($locked, $quantity)) {
                throw InsufficientInventoryException::forQuantity($this->available($locked), $quantity);
            }

            $locked->forceFill([
                'quantity_reserved' => $locked->quantity_reserved + $quantity,
            ])->save();
        });
    }

    public function release(InventoryItem $item, int $quantity): void
    {
        $this->assertPositiveQuantity($quantity);

        DB::transaction(function () use ($item, $quantity): void {
            $locked = $this->freshItem($item);

            if ($locked->quantity_reserved < $quantity) {
                throw new InvalidArgumentException('Cannot release more inventory than is reserved.');
            }

            $locked->forceFill([
                'quantity_reserved' => $locked->quantity_reserved - $quantity,
            ])->save();
        });
    }

    public function commit(InventoryItem $item, int $quantity): void
    {
        $this->assertPositiveQuantity($quantity);

        DB::transaction(function () use ($item, $quantity): void {
            $locked = $this->freshItem($item);

            if ($locked->quantity_reserved < $quantity) {
                throw new InvalidArgumentException('Cannot commit more inventory than is reserved.');
            }

            $locked->forceFill([
                'quantity_on_hand' => $locked->quantity_on_hand - $quantity,
                'quantity_reserved' => $locked->quantity_reserved - $quantity,
            ])->save();
        });
    }

    public function restock(InventoryItem $item, int $quantity): void
    {
        $this->assertPositiveQuantity($quantity);

        DB::transaction(function () use ($item, $quantity): void {
            $locked = $this->freshItem($item);

            $locked->forceFill([
                'quantity_on_hand' => $locked->quantity_on_hand + $quantity,
            ])->save();
        });
    }

    private function available(InventoryItem $item): int
    {
        return $item->quantity_on_hand - $item->quantity_reserved;
    }

    private function freshItem(InventoryItem $item): InventoryItem
    {
        return InventoryItem::query()
            ->whereKey($item->getKey())
            ->lockForUpdate()
            ->firstOrFail();
    }

    private function assertPositiveQuantity(int $quantity): void
    {
        if ($quantity <= 0) {
            throw new InvalidArgumentException('Inventory quantity must be greater than zero.');
        }
    }
}


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

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('checkouts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('cart_id')->constrained()->cascadeOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->enum('status', ['started', 'addressed', 'shipping_selected', 'payment_selected', 'completed', 'expired'])->default('started');
            $table->enum('payment_method', ['credit_card', 'paypal', 'bank_transfer'])->nullable();
            $table->string('email')->nullable();
            $table->text('shipping_address_json')->nullable();
            $table->text('billing_address_json')->nullable();
            $table->unsignedBigInteger('shipping_method_id')->nullable();
            $table->string('discount_code')->nullable();
            $table->text('tax_provider_snapshot_json')->nullable();
            $table->text('totals_json')->nullable();
            $table->timestamp('expires_at')->nullable();
            $table->timestamps();

            $table->index('store_id');
            $table->index('cart_id');
            $table->index('customer_id');
            $table->index(['store_id', 'status']);
            $table->index('expires_at');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('checkouts');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('discounts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->enum('type', ['code', 'automatic'])->default('code');
            $table->string('code')->nullable();
            $table->enum('value_type', ['fixed', 'percent', 'free_shipping']);
            $table->integer('value_amount')->default(0);
            $table->timestamp('starts_at');
            $table->timestamp('ends_at')->nullable();
            $table->unsignedInteger('usage_limit')->nullable();
            $table->unsignedInteger('usage_count')->default(0);
            $table->text('rules_json')->default('{}');
            $table->enum('status', ['draft', 'active', 'expired', 'disabled'])->default('active');
            $table->timestamps();

            $table->unique(['store_id', 'code']);
            $table->index('store_id');
            $table->index(['store_id', 'status']);
            $table->index(['store_id', 'type']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('discounts');
    }
};


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

CartStatus.php
CheckoutStatus.php
CollectionStatus.php
CollectionType.php
DiscountStatus.php
DiscountType.php
DiscountValueType.php
InventoryPolicy.php
MediaStatus.php
MediaType.php
NavigationItemType.php
PageStatus.php
ProductStatus.php
ShippingRateType.php
StoreDomainType.php
StoreStatus.php
StoreUserRole.php
TaxMode.php
ThemeStatus.php
VariantStatus.php
app/Enums/CartStatus.php
<?php

namespace App\Enums;

enum CartStatus: string
{
    case Active = 'active';
    case Converted = 'converted';
    case Abandoned = 'abandoned';
}
app/Enums/CheckoutStatus.php
<?php

namespace App\Enums;

enum CheckoutStatus: string
{
    case Started = 'started';
    case Addressed = 'addressed';
    case ShippingSelected = 'shipping_selected';
    case PaymentSelected = 'payment_selected';
    case Completed = 'completed';
    case Expired = 'expired';
}
app/Enums/CollectionStatus.php
<?php

namespace App\Enums;

enum CollectionStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Archived = 'archived';
}
app/Enums/CollectionType.php
<?php

namespace App\Enums;

enum CollectionType: string
{
    case Manual = 'manual';
    case Automated = 'automated';
}
app/Enums/DiscountStatus.php
<?php

namespace App\Enums;

enum DiscountStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Expired = 'expired';
    case Disabled = 'disabled';
}
app/Enums/DiscountType.php
<?php

namespace App\Enums;

enum DiscountType: string
{
    case Code = 'code';
    case Automatic = 'automatic';
}
app/Enums/DiscountValueType.php
<?php

namespace App\Enums;

enum DiscountValueType: string
{
    case Percent = 'percent';
    case Fixed = 'fixed';
    case FreeShipping = 'free_shipping';
}
app/Enums/InventoryPolicy.php
<?php

namespace App\Enums;

enum InventoryPolicy: string
{
    case Deny = 'deny';
    case Continue = 'continue';
}
app/Enums/MediaStatus.php
<?php

namespace App\Enums;

enum MediaStatus: string
{
    case Processing = 'processing';
    case Ready = 'ready';
    case Failed = 'failed';
}
app/Enums/MediaType.php
<?php

namespace App\Enums;

enum MediaType: string
{
    case Image = 'image';
    case Video = 'video';
}
app/Enums/NavigationItemType.php
<?php

namespace App\Enums;

enum NavigationItemType: string
{
    case Link = 'link';
    case Page = 'page';
    case Collection = 'collection';
    case Product = 'product';
}
app/Enums/PageStatus.php
<?php

namespace App\Enums;

enum PageStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
}
app/Enums/ProductStatus.php
<?php

namespace App\Enums;

enum ProductStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Archived = 'archived';
}
app/Enums/ShippingRateType.php
<?php

namespace App\Enums;

enum ShippingRateType: string
{
    case Flat = 'flat';
    case Weight = 'weight';
    case Price = 'price';
    case Carrier = 'carrier';
}
app/Enums/StoreDomainType.php
<?php

namespace App\Enums;

enum StoreDomainType: string
{
    case Storefront = 'storefront';
    case Admin = 'admin';
    case Api = 'api';
}
app/Enums/StoreStatus.php
<?php

namespace App\Enums;

enum StoreStatus: string
{
    case Active = 'active';
    case Suspended = 'suspended';
}
app/Enums/StoreUserRole.php
<?php

namespace App\Enums;

enum StoreUserRole: string
{
    case Owner = 'owner';
    case Admin = 'admin';
    case Staff = 'staff';
    case Support = 'support';
}
app/Enums/TaxMode.php
<?php

namespace App\Enums;

enum TaxMode: string
{
    case Manual = 'manual';
    case Provider = 'provider';
}
app/Enums/ThemeStatus.php
<?php

namespace App\Enums;

enum ThemeStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
}
app/Enums/VariantStatus.php
<?php

namespace App\Enums;

enum VariantStatus: string
{
    case Active = 'active';
    case Archived = 'archived';
}


Output:
Chunk ID: 5252fb
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 972
Output:
CartStatus.php
CheckoutStatus.php
CollectionStatus.php
CollectionType.php
DiscountStatus.php
DiscountType.php
DiscountValueType.php
InventoryPolicy.php
MediaStatus.php
MediaType.php
NavigationItemType.php
PageStatus.php
ProductStatus.php
ShippingRateType.php
StoreDomainType.php
StoreStatus.php
StoreUserRole.php
TaxMode.php
ThemeStatus.php
VariantStatus.php
app/Enums/CartStatus.php
<?php

namespace App\Enums;

enum CartStatus: string
{
    case Active = 'active';
    case Converted = 'converted';
    case Abandoned = 'abandoned';
}
app/Enums/CheckoutStatus.php
<?php

namespace App\Enums;

enum CheckoutStatus: string
{
    case Started = 'started';
    case Addressed = 'addressed';
    case ShippingSelected = 'shipping_selected';
    case PaymentSelected = 'payment_selected';
    case Completed = 'completed';
    case Expired = 'expired';
}
app/Enums/CollectionStatus.php
<?php

namespace App\Enums;

enum CollectionStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Archived = 'archived';
}
app/Enums/CollectionType.php
<?php

namespace App\Enums;

enum CollectionType: string
{
    case Manual = 'manual';
    case Automated = 'automated';
}
app/Enums/DiscountStatus.php
<?php

namespace App\Enums;

enum DiscountStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Expired = 'expired';
    case Disabled = 'disabled';
}
app/Enums/DiscountType.php
<?php

namespace App\Enums;

enum DiscountType: string
{
    case Code = 'code';
    case Automatic = 'automatic';
}
app/Enums/DiscountValueType.php
<?php

namespace App\Enums;

enum DiscountValueType: string
{
    case Percent = 'percent';
    case Fixed = 'fixed';
    case FreeShipping = 'free_shipping';
}
app/Enums/InventoryPolicy.php
<?php

namespace App\Enums;

enum InventoryPolicy: string
{
    case Deny = 'deny';
    case Continue = 'continue';
}
app/Enums/MediaStatus.php
<?php

namespace App\Enums;

enum MediaStatus: string
{
    case Processing = 'processing';
    case Ready = 'ready';
    case Failed = 'failed';
}
app/Enums/MediaType.php
<?php

namespace App\Enums;

enum MediaType: string
{
    case Image = 'image';
    case Video = 'video';
}
app/Enums/NavigationItemType.php
<?php

namespace App\Enums;

enum NavigationItemType: string
{
    case Link = 'link';
    case Page = 'page';
    case Collection = 'collection';
    case Product = 'product';
}
app/Enums/PageStatus.php
<?php

namespace App\Enums;

enum PageStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
}
app/Enums/ProductStatus.php
<?php

namespace App\Enums;

enum ProductStatus: string
{
    case Draft = 'draft';
    case Active = 'active';
    case Archived = 'archived';
}
app/Enums/ShippingRateType.php
<?php

namespace App\Enums;

enum ShippingRateType: string
{
    case Flat = 'flat';
    case Weight = 'weight';
    case Price = 'price';
    case Carrier = 'carrier';
}
app/Enums/StoreDomainType.php
<?php

namespace App\Enums;

enum StoreDomainType: string
{
    case Storefront = 'storefront';
    case Admin = 'admin';
    case Api = 'api';
}
app/Enums/StoreStatus.php
<?php

namespace App\Enums;

enum StoreStatus: string
{
    case Active = 'active';
    case Suspended = 'suspended';
}
app/Enums/StoreUserRole.php
<?php

namespace App\Enums;

enum StoreUserRole: string
{
    case Owner = 'owner';
    case Admin = 'admin';
    case Staff = 'staff';
    case Support = 'support';
}
app/Enums/TaxMode.php
<?php

namespace App\Enums;

enum TaxMode: string
{
    case Manual = 'manual';
    case Provider = 'provider';
}
app/Enums/ThemeStatus.php
<?php

namespace App\Enums;

enum ThemeStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
}
app/Enums/VariantStatus.php
<?php

namespace App\Enums;

enum VariantStatus: string
{
    case Active = 'active';
    case Archived = 'archived';
}


Output:
Chunk ID: 0d4f1e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 824
Output:
<?php

namespace App\Services;

use App\Enums\InventoryPolicy;
use App\Exceptions\InsufficientInventoryException;
use App\Models\InventoryItem;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

class InventoryService
{
    public function checkAvailability(InventoryItem $item, int $quantity): bool
    {
        $this->assertPositiveQuantity($quantity);

        if ($item->policy === InventoryPolicy::Continue) {
            return true;
        }

        return $this->available($item) >= $quantity;
    }

    public function reserve(InventoryItem $item, int $quantity): void
    {
        $this->assertPositiveQuantity($quantity);

        DB::transaction(function () use ($item, $quantity): void {
            $locked = $this->freshItem($item);

            if (! $this->checkAvailability($locked, $quantity)) {
                throw InsufficientInventoryException::forQuantity($this->available($locked), $quantity);
            }

            $locked->forceFill([
                'quantity_reserved' => $locked->quantity_reserved + $quantity,
            ])->save();
        });
    }

    public function release(InventoryItem $item, int $quantity): void
    {
        $this->assertPositiveQuantity($quantity);

        DB::transaction(function () use ($item, $quantity): void {
            $locked = $this->freshItem($item);

            if ($locked->quantity_reserved < $quantity) {
                throw new InvalidArgumentException('Cannot release more inventory than is reserved.');
            }

            $locked->forceFill([
                'quantity_reserved' => $locked->quantity_reserved - $quantity,
            ])->save();
        });
    }

    public function commit(InventoryItem $item, int $quantity): void
    {
        $this->assertPositiveQuantity($quantity);

        DB::transaction(function () use ($item, $quantity): void {
            $locked = $this->freshItem($item);

            if ($locked->quantity_reserved < $quantity) {
                throw new InvalidArgumentException('Cannot commit more inventory than is reserved.');
            }

            $locked->forceFill([
                'quantity_on_hand' => $locked->quantity_on_hand - $quantity,
                'quantity_reserved' => $locked->quantity_reserved - $quantity,
            ])->save();
        });
    }

    public function restock(InventoryItem $item, int $quantity): void
    {
        $this->assertPositiveQuantity($quantity);

        DB::transaction(function () use ($item, $quantity): void {
            $locked = $this->freshItem($item);

            $locked->forceFill([
                'quantity_on_hand' => $locked->quantity_on_hand + $quantity,
            ])->save();
        });
    }

    private function available(InventoryItem $item): int
    {
        return $item->quantity_on_hand - $item->quantity_reserved;
    }

    private function freshItem(InventoryItem $item): InventoryItem
    {
        return InventoryItem::query()
            ->whereKey($item->getKey())
            ->lockForUpdate()
            ->firstOrFail();
    }

    private function assertPositiveQuantity(int $quantity): void
    {
        if ($quantity <= 0) {
            throw new InvalidArgumentException('Inventory quantity must be greater than zero.');
        }
    }
}


Output:
Chunk ID: 8f66b7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 790
Output:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('checkouts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('cart_id')->constrained()->cascadeOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->enum('status', ['started', 'addressed', 'shipping_selected', 'payment_selected', 'completed', 'expired'])->default('started');
            $table->enum('payment_method', ['credit_card', 'paypal', 'bank_transfer'])->nullable();
            $table->string('email')->nullable();
            $table->text('shipping_address_json')->nullable();
            $table->text('billing_address_json')->nullable();
            $table->unsignedBigInteger('shipping_method_id')->nullable();
            $table->string('discount_code')->nullable();
            $table->text('tax_provider_snapshot_json')->nullable();
            $table->text('totals_json')->nullable();
            $table->timestamp('expires_at')->nullable();
            $table->timestamps();

            $table->index('store_id');
            $table->index('cart_id');
            $table->index('customer_id');
            $table->index(['store_id', 'status']);
            $table->index('expires_at');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('checkouts');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('discounts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->enum('type', ['code', 'automatic'])->default('code');
            $table->string('code')->nullable();
            $table->enum('value_type', ['fixed', 'percent', 'free_shipping']);
            $table->integer('value_amount')->default(0);
            $table->timestamp('starts_at');
            $table->timestamp('ends_at')->nullable();
            $table->unsignedInteger('usage_limit')->nullable();
            $table->unsignedInteger('usage_count')->default(0);
            $table->text('rules_json')->default('{}');
            $table->enum('status', ['draft', 'active', 'expired', 'disabled'])->default('active');
            $table->timestamps();

            $table->unique(['store_id', 'code']);
            $table->index('store_id');
            $table->index(['store_id', 'status']);
            $table->index(['store_id', 'type']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('discounts');
    }
};


Phase 5 is too large for one clean patch, so I’m taking the same vertical-slice approach: first the order/payment data layer plus mock payment and order completion services, then refunds/fulfillment, then UI/API surfaces. This commit will target checkout completion into orders with mock PSP coverage.


Create Phase 5 order/payment/refund/fulfillment schema, enums, models, factories

Implement mock payment provider and checkout completion/order service

Add focused payment/order tests and update progress

Run formatting, focused tests, full suite, migrate/seed, then commit


Output: Plan updated


php artisan make:model CustomerAddress --migration --factory --seed --no-interaction


php artisan make:model Order --migration --factory --seed --no-interaction


php artisan make:model OrderLine --migration --factory --seed --no-interaction


php artisan make:model Payment --migration --factory --seed --no-interaction


Output:
Chunk ID: 876a56
Wall time: 0.1825 seconds
Process exited with code 0
Original token count: 93
Output:

   INFO  Model [app/Models/CustomerAddress.php] created successfully.

   INFO  Factory [database/factories/CustomerAddressFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000408_create_customer_addresses_table.php] created successfully.

   INFO  Seeder [database/seeders/CustomerAddressSeeder.php] created successfully.


Output:
Chunk ID: 6d4e86
Wall time: 0.1826 seconds
Process exited with code 0
Original token count: 82
Output:

   INFO  Model [app/Models/Order.php] created successfully.

   INFO  Factory [database/factories/OrderFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000408_create_orders_table.php] created successfully.

   INFO  Seeder [database/seeders/OrderSeeder.php] created successfully.


Output:
Chunk ID: c82fba
Wall time: 0.1821 seconds
Process exited with code 0
Original token count: 87
Output:

   INFO  Model [app/Models/OrderLine.php] created successfully.

   INFO  Factory [database/factories/OrderLineFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000408_create_order_lines_table.php] created successfully.

   INFO  Seeder [database/seeders/OrderLineSeeder.php] created successfully.


Output:
Chunk ID: 5b682d
Wall time: 0.1822 seconds
Process exited with code 0
Original token count: 84
Output:

   INFO  Model [app/Models/Payment.php] created successfully.

   INFO  Factory [database/factories/PaymentFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000408_create_payments_table.php] created successfully.

   INFO  Seeder [database/seeders/PaymentSeeder.php] created successfully.


php artisan make:model Order --migration --factory --seed --no-interaction

   INFO  Model [app/Models/Order.php] created successfully.

   INFO  Factory [database/factories/OrderFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000408_create_orders_table.php] created successfully.

   INFO  Seeder [database/seeders/OrderSeeder.php] created successfully.


php artisan make:model Payment --migration --factory --seed --no-interaction

   INFO  Model [app/Models/Payment.php] created successfully.

   INFO  Factory [database/factories/PaymentFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000408_create_payments_table.php] created successfully.

   INFO  Seeder [database/seeders/PaymentSeeder.php] created successfully.


php artisan make:model OrderLine --migration --factory --seed --no-interaction

   INFO  Model [app/Models/OrderLine.php] created successfully.

   INFO  Factory [database/factories/OrderLineFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000408_create_order_lines_table.php] created successfully.

   INFO  Seeder [database/seeders/OrderLineSeeder.php] created successfully.


php artisan make:model CustomerAddress --migration --factory --seed --no-interaction

   INFO  Model [app/Models/CustomerAddress.php] created successfully.

   INFO  Factory [database/factories/CustomerAddressFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000408_create_customer_addresses_table.php] created successfully.

   INFO  Seeder [database/seeders/CustomerAddressSeeder.php] created successfully.


php artisan make:model Refund --migration --factory --seed --no-interaction


php artisan make:model Fulfillment --migration --factory --seed --no-interaction


php artisan make:model FulfillmentLine --migration --factory --seed --no-interaction


Output:
Chunk ID: ede2f8
Wall time: 0.0245 seconds
Process exited with code 0
Original token count: 83
Output:

   INFO  Model [app/Models/Refund.php] created successfully.

   INFO  Factory [database/factories/RefundFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000415_create_refunds_table.php] created successfully.

   INFO  Seeder [database/seeders/RefundSeeder.php] created successfully.


Output:
Chunk ID: b091ab
Wall time: 0.0249 seconds
Process exited with code 0
Original token count: 88
Output:

   INFO  Model [app/Models/Fulfillment.php] created successfully.

   INFO  Factory [database/factories/FulfillmentFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000415_create_fulfillments_table.php] created successfully.

   INFO  Seeder [database/seeders/FulfillmentSeeder.php] created successfully.


Output:
Chunk ID: e0ef0e
Wall time: 0.0239 seconds
Process exited with code 0
Original token count: 93
Output:

   INFO  Model [app/Models/FulfillmentLine.php] created successfully.

   INFO  Factory [database/factories/FulfillmentLineFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000415_create_fulfillment_lines_table.php] created successfully.

   INFO  Seeder [database/seeders/FulfillmentLineSeeder.php] created successfully.


php artisan make:model FulfillmentLine --migration --factory --seed --no-interaction

   INFO  Model [app/Models/FulfillmentLine.php] created successfully.

   INFO  Factory [database/factories/FulfillmentLineFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000415_create_fulfillment_lines_table.php] created successfully.

   INFO  Seeder [database/seeders/FulfillmentLineSeeder.php] created successfully.


php artisan make:model Refund --migration --factory --seed --no-interaction

   INFO  Model [app/Models/Refund.php] created successfully.

   INFO  Factory [database/factories/RefundFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000415_create_refunds_table.php] created successfully.

   INFO  Seeder [database/seeders/RefundSeeder.php] created successfully.


php artisan make:model Fulfillment --migration --factory --seed --no-interaction

   INFO  Model [app/Models/Fulfillment.php] created successfully.

   INFO  Factory [database/factories/FulfillmentFactory.php] created successfully.

   INFO  Migration [database/migrations/2026_05_04_000415_create_fulfillments_table.php] created successfully.

   INFO  Seeder [database/seeders/FulfillmentSeeder.php] created successfully.


ls database/migrations/*customer_addresses* database/migrations/*orders* database/migrations/*order_lines* database/migrations/*payments* database/migrations/*refunds* database/migrations/*fulfillments* database/migrations/*fulfillment_lines*


ls database/migrations/*customer_addresses* database/migrations/*orders* database/migrations/*order_lines* database/migrations/*payments* database/migrations/*refunds* database/migrations/*fulfillments* database/migrations/*fulfillment_lines*

ls 'database/migrations/*customer_addresses*' 'database/migrations/*orders*' 'database/migrations/*order_lines*' 'database/migrations/*payments*' 'database/migrations/*refunds*' 'database/migrations/*fulfillments*' 'database/migrations/*fulfillment_lines*'


Output:
Chunk ID: 582aaa
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 118
Output:
database/migrations/2026_05_04_000408_create_customer_addresses_table.php
database/migrations/2026_05_04_000408_create_order_lines_table.php
database/migrations/2026_05_04_000408_create_orders_table.php
database/migrations/2026_05_04_000408_create_payments_table.php
database/migrations/2026_05_04_000415_create_fulfillment_lines_table.php
database/migrations/2026_05_04_000415_create_fulfillments_table.php
database/migrations/2026_05_04_000415_create_refunds_table.php


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


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


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


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


Output:
Chunk ID: 38adc7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


for f in database/migrations/2026_05_04_000408_create_customer_addresses_table.php database/migrations/2026_05_04_000408_create_orders_table.php database/migrations/2026_05_04_000410_create_order_lines_table.php database/migrations/2026_05_04_000411_create_payments_table.php database/migrations/2026_05_04_000412_create_refunds_table.php database/migrations/2026_05_04_000413_create_fulfillments_table.php database/migrations/2026_05_04_000414_create_fulfillment_lines_table.php; do printf '%s\n' "$f"; sed -n '1,160p' "$f"; done


for f in app/Models/CustomerAddress.php app/Models/Order.php app/Models/OrderLine.php app/Models/Payment.php app/Models/Refund.php app/Models/Fulfillment.php app/Models/FulfillmentLine.php; do printf '%s\n' "$f"; sed -n '1,140p' "$f"; done


sed -n '1,140p' app/Models/Customer.php && sed -n '1,140p' app/Models/Store.php


sed -n '1,140p' app/Models/Customer.php && sed -n '1,140p' app/Models/Store.php

<?php

namespace App\Models;

use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;

class Customer extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\CustomerFactory> */
    use BelongsToStore, HasFactory, Notifiable;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'email',
        'password',
        'name',
        'marketing_opt_in',
    ];

    /**
     * @var list<string>
     */
    protected $hidden = [
        'password_hash',
        'remember_token',
    ];

    public function getAuthPassword(): ?string
    {
        return $this->password_hash;
    }

    public function setPasswordAttribute(?string $value): void
    {
        $this->attributes['password_hash'] = $value && Hash::needsRehash($value) ? Hash::make($value) : $value;
    }

    public function getPasswordAttribute(): ?string
    {
        return $this->password_hash;
    }

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

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

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'marketing_opt_in' => 'bool',
        ];
    }
}
<?php

namespace App\Models;

use App\Enums\StoreStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class Store extends Model
{
    /** @use HasFactory<\Database\Factories\StoreFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'organization_id',
        'name',
        'handle',
        'status',
        'default_currency',
        'default_locale',
        'timezone',
    ];

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

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

    /**
     * @return BelongsToMany<User, $this>
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'store_users')
            ->using(StoreUser::class)
            ->withPivot('role');
    }

    /**
     * @return HasOne<StoreSettings, $this>
     */
    public function settings(): HasOne
    {
        return $this->hasOne(StoreSettings::class);
    }

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

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

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

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

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

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

    /**
     * @return HasOne<TaxSettings, $this>
     */
    public function taxSettings(): HasOne
    {
        return $this->hasOne(TaxSettings::class);
    }

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

    public function isSuspended(): bool
    {
        return $this->status === StoreStatus::Suspended;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => StoreStatus::class,


for f in app/Models/CustomerAddress.php app/Models/Order.php app/Models/OrderLine.php app/Models/Payment.php app/Models/Refund.php app/Models/Fulfillment.php app/Models/FulfillmentLine.php; do printf '%s\n' "$f"; sed -n '1,140p' "$f"; done

app/Models/CustomerAddress.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class CustomerAddress extends Model
{
    /** @use HasFactory<\Database\Factories\CustomerAddressFactory> */
    use HasFactory;
}
app/Models/Order.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    /** @use HasFactory<\Database\Factories\OrderFactory> */
    use HasFactory;
}
app/Models/OrderLine.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class OrderLine extends Model
{
    /** @use HasFactory<\Database\Factories\OrderLineFactory> */
    use HasFactory;
}
app/Models/Payment.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Payment extends Model
{
    /** @use HasFactory<\Database\Factories\PaymentFactory> */
    use HasFactory;
}
app/Models/Refund.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Refund extends Model
{
    /** @use HasFactory<\Database\Factories\RefundFactory> */
    use HasFactory;
}
app/Models/Fulfillment.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Fulfillment extends Model
{
    /** @use HasFactory<\Database\Factories\FulfillmentFactory> */
    use HasFactory;
}
app/Models/FulfillmentLine.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class FulfillmentLine extends Model
{
    /** @use HasFactory<\Database\Factories\FulfillmentLineFactory> */
    use HasFactory;
}


for f in database/migrations/2026_05_04_000408_create_customer_addresses_table.php database/migrations/2026_05_04_000408_create_orders_table.php database/migrations/2026_05_04_000410_create_order_lines_table.php database/migrations/2026_05_04_000411_create_payments_table.php database/migrations/2026_05_04_000412_create_refunds_table.php database/migrations/2026_05_04_000413_create_fulfillments_table.php database/migrations/2026_05_04_000414_create_fulfillment_lines_table.php; do printf '%s\n' "$f"; sed -n '1,160p' "$f"; done

database/migrations/2026_05_04_000408_create_customer_addresses_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('customer_addresses', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('customer_addresses');
    }
};
database/migrations/2026_05_04_000408_create_orders_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('orders');
    }
};
database/migrations/2026_05_04_000410_create_order_lines_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('order_lines', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('order_lines');
    }
};
database/migrations/2026_05_04_000411_create_payments_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('payments', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('payments');
    }
};
database/migrations/2026_05_04_000412_create_refunds_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('refunds', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('refunds');
    }
};
database/migrations/2026_05_04_000413_create_fulfillments_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('fulfillments', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('fulfillments');
    }
};
database/migrations/2026_05_04_000414_create_fulfillment_lines_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('fulfillment_lines', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('fulfillment_lines');
    }
};


Output:
Chunk ID: afff02
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1062
Output:
database/migrations/2026_05_04_000408_create_customer_addresses_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('customer_addresses', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('customer_addresses');
    }
};
database/migrations/2026_05_04_000408_create_orders_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('orders');
    }
};
database/migrations/2026_05_04_000410_create_order_lines_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('order_lines', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('order_lines');
    }
};
database/migrations/2026_05_04_000411_create_payments_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('payments', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('payments');
    }
};
database/migrations/2026_05_04_000412_create_refunds_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('refunds', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('refunds');
    }
};
database/migrations/2026_05_04_000413_create_fulfillments_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('fulfillments', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('fulfillments');
    }
};
database/migrations/2026_05_04_000414_create_fulfillment_lines_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('fulfillment_lines', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('fulfillment_lines');
    }
};


Output:
Chunk ID: ba4690
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 477
Output:
app/Models/CustomerAddress.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class CustomerAddress extends Model
{
    /** @use HasFactory<\Database\Factories\CustomerAddressFactory> */
    use HasFactory;
}
app/Models/Order.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    /** @use HasFactory<\Database\Factories\OrderFactory> */
    use HasFactory;
}
app/Models/OrderLine.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class OrderLine extends Model
{
    /** @use HasFactory<\Database\Factories\OrderLineFactory> */
    use HasFactory;
}
app/Models/Payment.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Payment extends Model
{
    /** @use HasFactory<\Database\Factories\PaymentFactory> */
    use HasFactory;
}
app/Models/Refund.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Refund extends Model
{
    /** @use HasFactory<\Database\Factories\RefundFactory> */
    use HasFactory;
}
app/Models/Fulfillment.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Fulfillment extends Model
{
    /** @use HasFactory<\Database\Factories\FulfillmentFactory> */
    use HasFactory;
}
app/Models/FulfillmentLine.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class FulfillmentLine extends Model
{
    /** @use HasFactory<\Database\Factories\FulfillmentLineFactory> */
    use HasFactory;
}


Output:
Chunk ID: c991b5
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1157
Output:
<?php

namespace App\Models;

use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;

class Customer extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\CustomerFactory> */
    use BelongsToStore, HasFactory, Notifiable;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'email',
        'password',
        'name',
        'marketing_opt_in',
    ];

    /**
     * @var list<string>
     */
    protected $hidden = [
        'password_hash',
        'remember_token',
    ];

    public function getAuthPassword(): ?string
    {
        return $this->password_hash;
    }

    public function setPasswordAttribute(?string $value): void
    {
        $this->attributes['password_hash'] = $value && Hash::needsRehash($value) ? Hash::make($value) : $value;
    }

    public function getPasswordAttribute(): ?string
    {
        return $this->password_hash;
    }

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

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

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'marketing_opt_in' => 'bool',
        ];
    }
}
<?php

namespace App\Models;

use App\Enums\StoreStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class Store extends Model
{
    /** @use HasFactory<\Database\Factories\StoreFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'organization_id',
        'name',
        'handle',
        'status',
        'default_currency',
        'default_locale',
        'timezone',
    ];

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

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

    /**
     * @return BelongsToMany<User, $this>
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'store_users')
            ->using(StoreUser::class)
            ->withPivot('role');
    }

    /**
     * @return HasOne<StoreSettings, $this>
     */
    public function settings(): HasOne
    {
        return $this->hasOne(StoreSettings::class);
    }

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

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

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

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

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

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

    /**
     * @return HasOne<TaxSettings, $this>
     */
    public function taxSettings(): HasOne
    {
        return $this->hasOne(TaxSettings::class);
    }

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

    public function isSuspended(): bool
    {
        return $this->status === StoreStatus::Suspended;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => StoreStatus::class,


/bin/zsh
-lc
sed -n '140,260p' app/Models/Store.php

            'status' => StoreStatus::class,
        ];
    }
}


Output:
Chunk ID: 4c3687
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 16
Output:
            'status' => StoreStatus::class,
        ];
    }
}


sed -n '708,750p' specs/01-DATABASE-SCHEMA.md && sed -n '380,460p' specs/07-SEEDERS-AND-TEST-DATA.md


sed -n '708,750p' specs/01-DATABASE-SCHEMA.md && sed -n '380,460p' specs/07-SEEDERS-AND-TEST-DATA.md

Storefront customer accounts, scoped to a store.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| email | TEXT | No | - | UNIQUE per (store_id, email) | Customer email |
| password_hash | TEXT | Yes | NULL | - | Nullable for guest checkout customers |
| name | TEXT | Yes | NULL | - | Display name |
| marketing_opt_in | INTEGER | No | 0 | - | Boolean: marketing consent |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_customers_store_email` UNIQUE on (`store_id`, `email`) - Unique email per store
- `idx_customers_store_id` on (`store_id`) - FK lookup, listing customers per store

**Notes:** `password_hash` is nullable to allow guest checkout customers who may later claim their account. Email is unique per store, not globally.

---

### customer_addresses

Saved addresses for a customer.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| customer_id | INTEGER | No | - | FK -> customers(id) ON DELETE CASCADE | Owning customer |
| label | TEXT | Yes | NULL | - | User label, e.g. "Home", "Work" |
| address_json | TEXT | No | '{}' | - | JSON address object (see notes) |
| is_default | INTEGER | No | 0 | - | Boolean: default address flag |

**Indexes:**

- `idx_customer_addresses_customer_id` on (`customer_id`) - FK lookup, listing addresses per customer
- `idx_customer_addresses_default` on (`customer_id`, `is_default`) - Quick lookup of default address

**Notes:** `address_json` structure: `{"first_name": "", "last_name": "", "company": "", "address1": "", "address2": "", "city": "", "province": "", "province_code": "", "country": "", "country_code": "", "zip": "", "phone": ""}`.

---

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending", financial_status = "pending" |
| `pendingBankTransfer` | status = "pending", financial_status = "pending", payment_method = "bank_transfer" |
| `fulfilled` | status = "fulfilled", fulfillment_status = "fulfilled" |
| `cancelled` | status = "cancelled", financial_status = "refunded" |
| `refunded` | status = "refunded", financial_status = "refunded" |
| `partiallyFulfilled` | fulfillment_status = "partial" |
| `creditCard` | payment_method = "credit_card" |
| `paypal` | payment_method = "paypal" |
| `bankTransfer` | payment_method = "bank_transfer" |

---

### 2.13 OrderLineFactory

**Model:** OrderLine

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| product_id | Creates a new Product via its factory |
| variant_id | Creates a new ProductVariant via its factory |
| title_snapshot | Faker 3 words |
| sku_snapshot | Faker pattern "SKU-####-???" |
| quantity | Random 1-3 |
| unit_price_amount | Random 999-19999 |
| total_amount | Computed: unit_price_amount * quantity |
| tax_lines_json | Empty JSON array |
| discount_allocations_json | Empty JSON array |

**States:** None.

---

### 2.14 PaymentFactory

**Model:** Payment

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| provider | "mock" |
| method | "credit_card" |
| provider_payment_id | "mock_" + Faker 20 random alphanumeric characters |
| status | "captured" |
| amount | Random 999-99999 |
| currency | "EUR" |
| raw_json_encrypted | null |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending" |
| `failed` | status = "failed" |
| `refunded` | status = "refunded" |
| `creditCard` | method = "credit_card" |
| `paypal` | method = "paypal" |
| `bankTransfer` | method = "bank_transfer" |

---

### 2.15 RefundFactory

**Model:** Refund

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| payment_id | Creates a new Payment via its factory |
| amount | Random 999-19999 |
| reason | Faker sentence |
| status | "processed" |
| provider_refund_id | "mock_re_" + Faker 20 random alphanumeric characters |


Output:
Chunk ID: 08063d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1084
Output:
Storefront customer accounts, scoped to a store.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| email | TEXT | No | - | UNIQUE per (store_id, email) | Customer email |
| password_hash | TEXT | Yes | NULL | - | Nullable for guest checkout customers |
| name | TEXT | Yes | NULL | - | Display name |
| marketing_opt_in | INTEGER | No | 0 | - | Boolean: marketing consent |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_customers_store_email` UNIQUE on (`store_id`, `email`) - Unique email per store
- `idx_customers_store_id` on (`store_id`) - FK lookup, listing customers per store

**Notes:** `password_hash` is nullable to allow guest checkout customers who may later claim their account. Email is unique per store, not globally.

---

### customer_addresses

Saved addresses for a customer.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| customer_id | INTEGER | No | - | FK -> customers(id) ON DELETE CASCADE | Owning customer |
| label | TEXT | Yes | NULL | - | User label, e.g. "Home", "Work" |
| address_json | TEXT | No | '{}' | - | JSON address object (see notes) |
| is_default | INTEGER | No | 0 | - | Boolean: default address flag |

**Indexes:**

- `idx_customer_addresses_customer_id` on (`customer_id`) - FK lookup, listing addresses per customer
- `idx_customer_addresses_default` on (`customer_id`, `is_default`) - Quick lookup of default address

**Notes:** `address_json` structure: `{"first_name": "", "last_name": "", "company": "", "address1": "", "address2": "", "city": "", "province": "", "province_code": "", "country": "", "country_code": "", "zip": "", "phone": ""}`.

---

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending", financial_status = "pending" |
| `pendingBankTransfer` | status = "pending", financial_status = "pending", payment_method = "bank_transfer" |
| `fulfilled` | status = "fulfilled", fulfillment_status = "fulfilled" |
| `cancelled` | status = "cancelled", financial_status = "refunded" |
| `refunded` | status = "refunded", financial_status = "refunded" |
| `partiallyFulfilled` | fulfillment_status = "partial" |
| `creditCard` | payment_method = "credit_card" |
| `paypal` | payment_method = "paypal" |
| `bankTransfer` | payment_method = "bank_transfer" |

---

### 2.13 OrderLineFactory

**Model:** OrderLine

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| product_id | Creates a new Product via its factory |
| variant_id | Creates a new ProductVariant via its factory |
| title_snapshot | Faker 3 words |
| sku_snapshot | Faker pattern "SKU-####-???" |
| quantity | Random 1-3 |
| unit_price_amount | Random 999-19999 |
| total_amount | Computed: unit_price_amount * quantity |
| tax_lines_json | Empty JSON array |
| discount_allocations_json | Empty JSON array |

**States:** None.

---

### 2.14 PaymentFactory

**Model:** Payment

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| provider | "mock" |
| method | "credit_card" |
| provider_payment_id | "mock_" + Faker 20 random alphanumeric characters |
| status | "captured" |
| amount | Random 999-99999 |
| currency | "EUR" |
| raw_json_encrypted | null |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending" |
| `failed` | status = "failed" |
| `refunded` | status = "refunded" |
| `creditCard` | method = "credit_card" |
| `paypal` | method = "paypal" |
| `bankTransfer` | method = "bank_transfer" |

---

### 2.15 RefundFactory

**Model:** Refund

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| payment_id | Creates a new Payment via its factory |
| amount | Random 999-19999 |
| reason | Faker sentence |
| status | "processed" |
| provider_refund_id | "mock_re_" + Faker 20 random alphanumeric characters |


sed -n '340,420p' specs/07-SEEDERS-AND-TEST-DATA.md && sed -n '460,540p' specs/07-SEEDERS-AND-TEST-DATA.md


sed -n '340,420p' specs/07-SEEDERS-AND-TEST-DATA.md && sed -n '460,540p' specs/07-SEEDERS-AND-TEST-DATA.md

**States:**

| State Name | Overrides |
|-----------|-----------|
| `completed` | status = "completed" |
| `expired` | status = "expired", expires_at = 1 hour ago |
| `withCreditCard` | payment_method = "credit_card" |
| `withPaypal` | payment_method = "paypal" |
| `withBankTransfer` | payment_method = "bank_transfer" |

---

### 2.12 OrderFactory

**Model:** Order

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| customer_id | Creates a new Customer via its factory |
| order_number | "#" + Faker unique integer 1001-9999 |
| payment_method | "credit_card" |
| status | "paid" |
| financial_status | "paid" |
| fulfillment_status | "unfulfilled" |
| currency | "EUR" |
| subtotal_amount | 4998 |
| discount_amount | 0 |
| shipping_amount | 499 |
| tax_amount | 798 |
| total_amount | 5497 |
| email | Faker safe email |
| billing_address_json | JSON address object with German Faker data |
| shipping_address_json | JSON address object with German Faker data |
| placed_at | Current timestamp |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending", financial_status = "pending" |
| `pendingBankTransfer` | status = "pending", financial_status = "pending", payment_method = "bank_transfer" |
| `fulfilled` | status = "fulfilled", fulfillment_status = "fulfilled" |
| `cancelled` | status = "cancelled", financial_status = "refunded" |
| `refunded` | status = "refunded", financial_status = "refunded" |
| `partiallyFulfilled` | fulfillment_status = "partial" |
| `creditCard` | payment_method = "credit_card" |
| `paypal` | payment_method = "paypal" |
| `bankTransfer` | payment_method = "bank_transfer" |

---

### 2.13 OrderLineFactory

**Model:** OrderLine

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| product_id | Creates a new Product via its factory |
| variant_id | Creates a new ProductVariant via its factory |
| title_snapshot | Faker 3 words |
| sku_snapshot | Faker pattern "SKU-####-???" |
| quantity | Random 1-3 |
| unit_price_amount | Random 999-19999 |
| total_amount | Computed: unit_price_amount * quantity |
| tax_lines_json | Empty JSON array |
| discount_allocations_json | Empty JSON array |

**States:** None.

---

### 2.14 PaymentFactory

**Model:** Payment

| provider_refund_id | "mock_re_" + Faker 20 random alphanumeric characters |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending", provider_refund_id = null |
| `failed` | status = "failed" |

---

### 2.16 FulfillmentFactory

**Model:** Fulfillment

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| status | "shipped" |
| tracking_company | Random from: DHL, UPS, FedEx, DPD |
| tracking_number | Faker pattern "??########" (2 letters + 8 digits) |
| tracking_url | "https://tracking.example.com/" + tracking_number |
| shipped_at | Current timestamp |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending", tracking_number = null, tracking_url = null, shipped_at = null |
| `delivered` | status = "delivered" |

---

### 2.16b FulfillmentLineFactory

**Model:** FulfillmentLine

**Default definition:**

| Field | Rule | Notes |
|-------|------|-------|
| fulfillment_id | FK | Parent fulfillment |
| order_line_id | FK | The order line being fulfilled |
| quantity | integer, 1..max | Quantity fulfilled for this line (must not exceed order line quantity) |

**States:** None.

---

### 2.17 CustomerFactory

**Model:** Customer

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| email | Faker unique safe email |
| password_hash | bcrypt of "password" |
| name | Faker full name |
| marketing_opt_in | 30% chance of 1, otherwise 0 |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `guest` | password_hash = null |
| `optedInMarketing` | marketing_opt_in = 1 |

---

### 2.18 CustomerAddressFactory

**Model:** CustomerAddress

**Default definition:**

| Field | Generation Rule |


Output:
Chunk ID: 835f87
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1069
Output:
**States:**

| State Name | Overrides |
|-----------|-----------|
| `completed` | status = "completed" |
| `expired` | status = "expired", expires_at = 1 hour ago |
| `withCreditCard` | payment_method = "credit_card" |
| `withPaypal` | payment_method = "paypal" |
| `withBankTransfer` | payment_method = "bank_transfer" |

---

### 2.12 OrderFactory

**Model:** Order

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| customer_id | Creates a new Customer via its factory |
| order_number | "#" + Faker unique integer 1001-9999 |
| payment_method | "credit_card" |
| status | "paid" |
| financial_status | "paid" |
| fulfillment_status | "unfulfilled" |
| currency | "EUR" |
| subtotal_amount | 4998 |
| discount_amount | 0 |
| shipping_amount | 499 |
| tax_amount | 798 |
| total_amount | 5497 |
| email | Faker safe email |
| billing_address_json | JSON address object with German Faker data |
| shipping_address_json | JSON address object with German Faker data |
| placed_at | Current timestamp |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending", financial_status = "pending" |
| `pendingBankTransfer` | status = "pending", financial_status = "pending", payment_method = "bank_transfer" |
| `fulfilled` | status = "fulfilled", fulfillment_status = "fulfilled" |
| `cancelled` | status = "cancelled", financial_status = "refunded" |
| `refunded` | status = "refunded", financial_status = "refunded" |
| `partiallyFulfilled` | fulfillment_status = "partial" |
| `creditCard` | payment_method = "credit_card" |
| `paypal` | payment_method = "paypal" |
| `bankTransfer` | payment_method = "bank_transfer" |

---

### 2.13 OrderLineFactory

**Model:** OrderLine

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| product_id | Creates a new Product via its factory |
| variant_id | Creates a new ProductVariant via its factory |
| title_snapshot | Faker 3 words |
| sku_snapshot | Faker pattern "SKU-####-???" |
| quantity | Random 1-3 |
| unit_price_amount | Random 999-19999 |
| total_amount | Computed: unit_price_amount * quantity |
| tax_lines_json | Empty JSON array |
| discount_allocations_json | Empty JSON array |

**States:** None.

---

### 2.14 PaymentFactory

**Model:** Payment

| provider_refund_id | "mock_re_" + Faker 20 random alphanumeric characters |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending", provider_refund_id = null |
| `failed` | status = "failed" |

---

### 2.16 FulfillmentFactory

**Model:** Fulfillment

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| status | "shipped" |
| tracking_company | Random from: DHL, UPS, FedEx, DPD |
| tracking_number | Faker pattern "??########" (2 letters + 8 digits) |
| tracking_url | "https://tracking.example.com/" + tracking_number |
| shipped_at | Current timestamp |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending", tracking_number = null, tracking_url = null, shipped_at = null |
| `delivered` | status = "delivered" |

---

### 2.16b FulfillmentLineFactory

**Model:** FulfillmentLine

**Default definition:**

| Field | Rule | Notes |
|-------|------|-------|
| fulfillment_id | FK | Parent fulfillment |
| order_line_id | FK | The order line being fulfilled |
| quantity | integer, 1..max | Quantity fulfilled for this line (must not exceed order line quantity) |

**States:** None.

---

### 2.17 CustomerFactory

**Model:** Customer

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| email | Faker unique safe email |
| password_hash | bcrypt of "password" |
| name | Faker full name |
| marketing_opt_in | 30% chance of 1, otherwise 0 |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `guest` | password_hash = null |
| `optedInMarketing` | marketing_opt_in = 1 |

---

### 2.18 CustomerAddressFactory

**Model:** CustomerAddress

**Default definition:**

| Field | Generation Rule |


sed -n '540,590p' specs/07-SEEDERS-AND-TEST-DATA.md


sed -n '540,590p' specs/07-SEEDERS-AND-TEST-DATA.md

| Field | Generation Rule |
|-------|----------------|
| customer_id | Creates a new Customer via its factory |
| label | Random from: Home, Work, Other |
| address_json | JSON object (see Address JSON Structure in Section 6) |
| is_default | 0 |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `default` | is_default = 1 |

---

### 2.19 DiscountFactory

**Model:** Discount

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| type | "code" |
| code | Faker unique uppercase pattern "????##" |
| value_type | "percent" |
| value_amount | 10 |
| starts_at | 1 month ago |
| ends_at | 1 year from now |
| usage_limit | null |
| usage_count | 0 |
| rules_json | Empty JSON object |
| status | "active" |

**Note on value_amount for percent type:** Stored as whole number percentages. 10 = 10%, 20 = 20%.

**States:**

| State Name | Overrides |
|-----------|-----------|
| `fixed(amountCents)` | value_type = "fixed", value_amount = amountCents |
| `freeShipping` | value_type = "free_shipping", value_amount = 0 |
| `expired` | starts_at = 1 year ago, ends_at = yesterday, status = "expired" |
| `maxedOut` | usage_limit = 5, usage_count = 5 |
| `automatic` | type = "automatic", code = null |
| `draft` | status = "draft" |
| `disabled` | status = "disabled", starts_at = past date |

---


Output:
Chunk ID: c717ff
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 349
Output:
| Field | Generation Rule |
|-------|----------------|
| customer_id | Creates a new Customer via its factory |
| label | Random from: Home, Work, Other |
| address_json | JSON object (see Address JSON Structure in Section 6) |
| is_default | 0 |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `default` | is_default = 1 |

---

### 2.19 DiscountFactory

**Model:** Discount

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| type | "code" |
| code | Faker unique uppercase pattern "????##" |
| value_type | "percent" |
| value_amount | 10 |
| starts_at | 1 month ago |
| ends_at | 1 year from now |
| usage_limit | null |
| usage_count | 0 |
| rules_json | Empty JSON object |
| status | "active" |

**Note on value_amount for percent type:** Stored as whole number percentages. 10 = 10%, 20 = 20%.

**States:**

| State Name | Overrides |
|-----------|-----------|
| `fixed(amountCents)` | value_type = "fixed", value_amount = amountCents |
| `freeShipping` | value_type = "free_shipping", value_amount = 0 |
| `expired` | starts_at = 1 year ago, ends_at = yesterday, status = "expired" |
| `maxedOut` | usage_limit = 5, usage_count = 5 |
| `automatic` | type = "automatic", code = null |
| `draft` | status = "draft" |
| `disabled` | status = "disabled", starts_at = past date |

---


I’m adding the Phase 5 backend foundation now: schema and models first, including a `checkout_id` link on orders so checkout completion can be idempotent.


Input:
*** Begin Patch
*** Update File: database/migrations/2026_05_04_000408_create_customer_addresses_table.php
@@
         Schema::create('customer_addresses', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->foreignId('customer_id')->constrained()->cascadeOnDelete();
+            $table->string('label')->nullable();
+            $table->text('address_json')->default('{}');
+            $table->boolean('is_default')->default(false);
+
+            $table->index('customer_id');
+            $table->index(['customer_id', 'is_default']);
         });
*** Update File: database/migrations/2026_05_04_000408_create_orders_table.php
@@
         Schema::create('orders', function (Blueprint $table) {
             $table->id();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('checkout_id')->nullable()->constrained()->nullOnDelete();
+            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
+            $table->string('order_number');
+            $table->enum('payment_method', ['credit_card', 'paypal', 'bank_transfer']);
+            $table->enum('status', ['pending', 'paid', 'fulfilled', 'cancelled', 'refunded'])->default('pending');
+            $table->enum('financial_status', ['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])->default('pending');
+            $table->enum('fulfillment_status', ['unfulfilled', 'partial', 'fulfilled'])->default('unfulfilled');
+            $table->string('currency', 3)->default('USD');
+            $table->integer('subtotal_amount')->default(0);
+            $table->integer('discount_amount')->default(0);
+            $table->integer('shipping_amount')->default(0);
+            $table->integer('tax_amount')->default(0);
+            $table->integer('total_amount')->default(0);
+            $table->string('email')->nullable();
+            $table->text('billing_address_json')->nullable();
+            $table->text('shipping_address_json')->nullable();
+            $table->timestamp('placed_at')->nullable();
             $table->timestamps();
+
+            $table->unique(['store_id', 'order_number']);
+            $table->unique('checkout_id');
+            $table->index('store_id');
+            $table->index('customer_id');
+            $table->index(['store_id', 'status']);
+            $table->index(['store_id', 'financial_status']);
+            $table->index(['store_id', 'fulfillment_status']);
+            $table->index(['store_id', 'placed_at']);
         });
*** Update File: database/migrations/2026_05_04_000410_create_order_lines_table.php
@@
         Schema::create('order_lines', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('product_id')->nullable()->constrained()->nullOnDelete();
+            $table->foreignId('variant_id')->nullable()->constrained('product_variants')->nullOnDelete();
+            $table->string('title_snapshot');
+            $table->string('sku_snapshot')->nullable();
+            $table->unsignedInteger('quantity')->default(1);
+            $table->integer('unit_price_amount')->default(0);
+            $table->integer('total_amount')->default(0);
+            $table->text('tax_lines_json')->default('[]');
+            $table->text('discount_allocations_json')->default('[]');
+
+            $table->index('order_id');
+            $table->index('product_id');
+            $table->index('variant_id');
         });
*** Update File: database/migrations/2026_05_04_000411_create_payments_table.php
@@
         Schema::create('payments', function (Blueprint $table) {
             $table->id();
+            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
+            $table->enum('provider', ['mock'])->default('mock');
+            $table->enum('method', ['credit_card', 'paypal', 'bank_transfer']);
+            $table->string('provider_payment_id')->nullable();
+            $table->enum('status', ['pending', 'captured', 'failed', 'refunded'])->default('pending');
+            $table->integer('amount')->default(0);
+            $table->string('currency', 3)->default('USD');
+            $table->text('raw_json_encrypted')->nullable();
             $table->timestamps();
+
+            $table->index('order_id');
+            $table->index(['provider', 'provider_payment_id']);
+            $table->index('method');
+            $table->index('status');
         });
*** Update File: database/migrations/2026_05_04_000412_create_refunds_table.php
@@
         Schema::create('refunds', function (Blueprint $table) {
             $table->id();
+            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('payment_id')->constrained()->cascadeOnDelete();
+            $table->integer('amount')->default(0);
+            $table->string('reason')->nullable();
+            $table->enum('status', ['pending', 'processed', 'failed'])->default('pending');
+            $table->string('provider_refund_id')->nullable();
             $table->timestamps();
+
+            $table->index('order_id');
+            $table->index('payment_id');
+            $table->index('status');
         });
*** Update File: database/migrations/2026_05_04_000413_create_fulfillments_table.php
@@
         Schema::create('fulfillments', function (Blueprint $table) {
             $table->id();
+            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
+            $table->enum('status', ['pending', 'shipped', 'delivered'])->default('pending');
+            $table->string('tracking_company')->nullable();
+            $table->string('tracking_number')->nullable();
+            $table->string('tracking_url')->nullable();
+            $table->timestamp('shipped_at')->nullable();
+            $table->timestamp('delivered_at')->nullable();
             $table->timestamps();
+
+            $table->index('order_id');
+            $table->index('status');
+            $table->index(['tracking_company', 'tracking_number']);
         });
*** Update File: database/migrations/2026_05_04_000414_create_fulfillment_lines_table.php
@@
         Schema::create('fulfillment_lines', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->foreignId('fulfillment_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('order_line_id')->constrained()->cascadeOnDelete();
+            $table->unsignedInteger('quantity')->default(1);
+
+            $table->index('fulfillment_id');
+            $table->unique(['fulfillment_id', 'order_line_id']);
         });
*** End Patch


Success. Updated the following files:
M database/migrations/2026_05_04_000408_create_customer_addresses_table.php
M database/migrations/2026_05_04_000408_create_orders_table.php
M database/migrations/2026_05_04_000410_create_order_lines_table.php
M database/migrations/2026_05_04_000411_create_payments_table.php
M database/migrations/2026_05_04_000412_create_refunds_table.php
M database/migrations/2026_05_04_000413_create_fulfillments_table.php
M database/migrations/2026_05_04_000414_create_fulfillment_lines_table.php

@@ -15,3 +15,9 @@
             $table->id();
-            $table->timestamps();
+            $table->foreignId('customer_id')->constrained()->cascadeOnDelete();
+            $table->string('label')->nullable();
+            $table->text('address_json')->default('{}');
+            $table->boolean('is_default')->default(false);
+
+            $table->index('customer_id');
+            $table->index(['customer_id', 'is_default']);
         });

@@ -15,3 +15,13 @@
             $table->id();
+            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('payment_id')->constrained()->cascadeOnDelete();
+            $table->integer('amount')->default(0);
+            $table->string('reason')->nullable();
+            $table->enum('status', ['pending', 'processed', 'failed'])->default('pending');
+            $table->string('provider_refund_id')->nullable();
             $table->timestamps();
+
+            $table->index('order_id');
+            $table->index('payment_id');
+            $table->index('status');
         });

@@ -15,3 +15,14 @@
             $table->id();
+            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
+            $table->enum('status', ['pending', 'shipped', 'delivered'])->default('pending');
+            $table->string('tracking_company')->nullable();
+            $table->string('tracking_number')->nullable();
+            $table->string('tracking_url')->nullable();
+            $table->timestamp('shipped_at')->nullable();
+            $table->timestamp('delivered_at')->nullable();
             $table->timestamps();
+
+            $table->index('order_id');
+            $table->index('status');
+            $table->index(['tracking_company', 'tracking_number']);
         });

@@ -15,3 +15,8 @@
             $table->id();
-            $table->timestamps();
+            $table->foreignId('fulfillment_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('order_line_id')->constrained()->cascadeOnDelete();
+            $table->unsignedInteger('quantity')->default(1);
+
+            $table->index('fulfillment_id');
+            $table->unique(['fulfillment_id', 'order_line_id']);
         });

@@ -15,3 +15,30 @@
             $table->id();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('checkout_id')->nullable()->constrained()->nullOnDelete();
+            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
+            $table->string('order_number');
+            $table->enum('payment_method', ['credit_card', 'paypal', 'bank_transfer']);
+            $table->enum('status', ['pending', 'paid', 'fulfilled', 'cancelled', 'refunded'])->default('pending');
+            $table->enum('financial_status', ['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])->default('pending');
+            $table->enum('fulfillment_status', ['unfulfilled', 'partial', 'fulfilled'])->default('unfulfilled');
+            $table->string('currency', 3)->default('USD');
+            $table->integer('subtotal_amount')->default(0);
+            $table->integer('discount_amount')->default(0);
+            $table->integer('shipping_amount')->default(0);
+            $table->integer('tax_amount')->default(0);
+            $table->integer('total_amount')->default(0);
+            $table->string('email')->nullable();
+            $table->text('billing_address_json')->nullable();
+            $table->text('shipping_address_json')->nullable();
+            $table->timestamp('placed_at')->nullable();
             $table->timestamps();
+
+            $table->unique(['store_id', 'order_number']);
+            $table->unique('checkout_id');
+            $table->index('store_id');
+            $table->index('customer_id');
+            $table->index(['store_id', 'status']);
+            $table->index(['store_id', 'financial_status']);
+            $table->index(['store_id', 'fulfillment_status']);
+            $table->index(['store_id', 'placed_at']);
         });

@@ -15,3 +15,16 @@
             $table->id();
-            $table->timestamps();
+            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('product_id')->nullable()->constrained()->nullOnDelete();
+            $table->foreignId('variant_id')->nullable()->constrained('product_variants')->nullOnDelete();
+            $table->string('title_snapshot');
+            $table->string('sku_snapshot')->nullable();
+            $table->unsignedInteger('quantity')->default(1);
+            $table->integer('unit_price_amount')->default(0);
+            $table->integer('total_amount')->default(0);
+            $table->text('tax_lines_json')->default('[]');
+            $table->text('discount_allocations_json')->default('[]');
+
+            $table->index('order_id');
+            $table->index('product_id');
+            $table->index('variant_id');
         });

@@ -15,3 +15,16 @@
             $table->id();
+            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
+            $table->enum('provider', ['mock'])->default('mock');
+            $table->enum('method', ['credit_card', 'paypal', 'bank_transfer']);
+            $table->string('provider_payment_id')->nullable();
+            $table->enum('status', ['pending', 'captured', 'failed', 'refunded'])->default('pending');
+            $table->integer('amount')->default(0);
+            $table->string('currency', 3)->default('USD');
+            $table->text('raw_json_encrypted')->nullable();
             $table->timestamps();
+
+            $table->index('order_id');
+            $table->index(['provider', 'provider_payment_id']);
+            $table->index('method');
+            $table->index('status');
         });


Output: {"output":"Success. Updated the following files:\nM database/migrations/2026_05_04_000408_create_customer_addresses_table.php\nM database/migrations/2026_05_04_000408_create_orders_table.php\nM database/migrations/2026_05_04_000410_create_order_lines_table.php\nM database/migrations/2026_05_04_000411_create_payments_table.php\nM database/migrations/2026_05_04_000412_create_refunds_table.php\nM database/migrations/2026_05_04_000413_create_fulfillments_table.php\nM database/migrations/2026_05_04_000414_create_fulfillment_lines_table.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}