text:
Perfect! I have now read the entire business logic specification file. Let me provide you with a comprehensive summary of all business logic rules.

## Comprehensive Business Logic Summary for Shop E-Commerce Platform

### CONVENTIONS & DATA STANDARDS
- **Monetary amounts:** Stored as INTEGER in minor currency units (cents). All arithmetic uses integers - never floating point.
- **Percentage storage:** Basis points (e.g., 1900 = 19.00%) or whole-number percentage where noted
- **Rounding:** Each line rounded independently, then summed (prevents rounding drift)
- **Currency:** Single currency per store (`stores.default_currency`). No multi-currency conversion.
- **Timestamps:** ISO-8601 UTC via Laravel `created_at` / `updated_at`

---

## 1. MULTI-TENANCY & STORE ISOLATION

**Middleware:** `App\Http\Middleware\ResolveStore` - registers in `storefront` and `admin` groups

**Storefront Resolution:**
- Extract hostname, lookup in `store_domains` table
- Cache hostname-to-store_id mapping (5-minute TTL, key: `store_domain:{hostname}`)
- Return HTTP 404 if hostname not found
- Return HTTP 503 if store status = 'suspended'

**Admin Resolution:**
- Read store_id from session (`current_store_id`)
- Verify user has `store_users` record for that store
- Return HTTP 403 if store suspended (admin can view but cannot mutate storefront data)

**Global Scope:** `App\Models\Scopes\StoreScope` + `BelongsToStore` trait on all tenant models
- Auto-set `store_id` on model create event
- All queries filtered by `WHERE store_id = <current_store_id>`
- Models: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries

**Role-Based Access Control:** `App\Enums\StoreUserRole`
- Roles: Owner, Admin, Staff, Support
- Permission matrix: Owner/Admin full access; Staff manage products/orders/discounts; Support read-only orders
- Implement via Laravel policies (e.g., `ProductPolicy`, `OrderPolicy`)
- Helper: `User.roleForStore(store)` returns user's role or null

---

## 2. PRODUCT LIFECYCLE

**Status Enum:** `ProductStatus` - draft, active, archived

**State Machine Transitions:**
- Draft → Active: Requires variant with `price_amount > 0` AND non-empty title. Sets `published_at` timestamp.
- Draft → Archived: No preconditions
- Active → Archived: Soft transition (hidden from storefront, preserved in order history)
- Archived → Active: Same preconditions as Draft → Active
- Active/Archived → Draft: BLOCKED if any `order_lines` reference this product's variants
- **Deletion:** Only draft products with zero order references may be hard-deleted

**Service:** `App\Services\ProductService`
- `transitionStatus(product, newStatus)` - validates state, checks preconditions, throws `InvalidProductTransitionException`
- Dispatches `ProductStatusChanged` event

**Variant & Option Matrix:**
- Product has 0-3 options (Size, Color, Material) stored in `product_options`
- Each option has 1+ values in `product_option_values`
- Variants = cartesian product of option values (or manually curated subset)
- Single "Default" variant auto-created when no options exist (`is_default = 1`)

**Matrix Rebuild Logic** (when options change):
```
PROCEDURE rebuildMatrix(product):
  desired_combos = cartesianProduct(option_values)
  existing_variants = load all variants with option value sets

  FOR EACH combo IN desired_combos:
    IF matches existing variant: SKIP (preserve prices, SKUs, inventory)
    ELSE: CREATE new variant with default pricing

  FOR EACH existing_variant NOT matching desired combo:
    IF variant has order_line references: ARCHIVE
    ELSE: DELETE
```

**Service:** `App\Services\VariantMatrixService`

**SKU Uniqueness:**
- Combination of `store_id` + `sku` must be unique
- Null/empty SKUs exempt from uniqueness check (multiple variants may have no SKU)
- Validation at application level before save

**Handle/Slug Generation:** `App\Support\HandleGenerator`
- Auto-generate from title (lowercase, hyphens for spaces, strip special chars)
- Uniqueness per store via unique index on `(store_id, handle)`
- On collision: append incrementing suffix (`my-product`, `my-product-1`, `my-product-2`)
- Same logic for collections and pages

---

## 3. INVENTORY MANAGEMENT

**Tracking Model:**
Each `product_variant` has one `inventory_item` record (auto-created with variant):
- `quantity_on_hand` - Total physical stock
- `quantity_reserved` - Units held for active checkouts/pending orders
- **Available** (computed) - `quantity_on_hand - quantity_reserved`

**Inventory Policy** (per-variant in `inventory_items.policy`):
- `deny` - Block add-to-cart when available <= 0. Display "Out of stock".
- `continue` - Allow add-to-cart regardless. Display "Available on backorder" when available <= 0.

**Operations:** `App\Services\InventoryService` - all within database transaction

| Operation | Trigger | Effect on `quantity_on_hand` | Effect on `quantity_reserved` | Pre-check |
|-----------|---------|------|---------|-----------|
| **Reserve** | Checkout enters `payment_selected` | No change | +quantity | If policy=deny: verify available >= quantity. Throw `InsufficientInventoryException` if not. |
| **Release** | Checkout expires/abandoned | No change | -quantity | None |
| **Commit** | Payment confirmed, order created | -quantity | -quantity | None (reservation already validated) |
| **Restock** | Refund processed with restock flag | +quantity | No change | None |

**Concurrency:** SQLite single-writer mode with WAL journaling provides sufficient isolation. No optimistic locking needed.

---

## 4. CART SERVICE

**Service:** `App\Services\CartService`

**Status Lifecycle:**
- active → converted (checkout completed)
- active → abandoned (14-day cleanup job)

**Cart Identification:**
- Guest users: cart ID stored in session cookie (key: `cart_id`)
- Authenticated customers: linked via `customer_id` foreign key

**Cart Merge on Login:**
```
PROCEDURE mergeGuestCart(guestCart, customerCart):
  FOR EACH line IN guestCart.lines:
    existingLine = customerCart.findLineByVariant(line.variant_id)
    IF existingLine EXISTS:
      existingLine.quantity = MAX(existingLine.quantity, line.quantity)
      RECALCULATE existingLine amounts
    ELSE:
      MOVE line to customerCart
  MARK guestCart as abandoned
  RECALCULATE all line amounts on customerCart
```

**Create Cart:**
- Accepts: store, optional customer
- Sets: store_id, currency from store's default_currency, initial cart_version = 1

**Add Line Validations** (in order):
1. Variant exists in current store (through product)
2. Product status = `active`
3. Variant status = `active`
4. Inventory policy check: if policy=deny, verify available >= quantity
5. If variant already in cart: increment existing line qty instead of duplicate
6. Set `unit_price_amount` from variant's `price_amount`
7. Calculate `line_subtotal_amount = unit_price_amount * quantity`
8. Set `line_discount_amount = 0` (recalculated by discount engine)
9. Calculate `line_total_amount = line_subtotal_amount - line_discount_amount`
10. Increment `cart_version`

**Update Quantity:**
- Validate new qty > 0. If 0, remove line.
- Re-check inventory policy
- Recalculate line amounts
- Increment `cart_version`

**Remove Line:**
- Delete `cart_lines` record
- Increment `cart_version`

**Cart Version (Optimistic Concurrency):**
- Every mutation increments `cart_version` by 1
- API clients: send `expected_version` with each request
- If mismatch: return HTTP 409 Conflict with current cart state
- Prevents race conditions across multiple devices/tabs

**Cart Expiration:** `App\Jobs\CleanupAbandonedCarts` - daily
- Find carts where status=active AND updated_at < (now - 14 days)
- Mark as `abandoned`
- Release any reserved inventory for active checkouts on these carts
- 14-day threshold configurable via `store_settings`

---

## 5. PRICING ENGINE

**Service:** `App\Services\PricingEngine` - deterministic pipeline

**Calculation Order:**
```
Step 1: Line Subtotals
  FOR EACH line: subtotal = unit_price * quantity

Step 2: Cart Subtotal
  SUM of all line subtotals

Step 3: Discount
  Apply discount rules (see Section 7)
  Allocate proportionally across qualifying lines

Step 4: Discounted Subtotal
  subtotal - total_discount

Step 5: Shipping
  Based on selected method and rate rules

Step 6: Tax
  Calculated on discounted line amounts + taxable shipping

Step 7: Total
  discounted_subtotal + shipping + tax_total
```

**Output Structure:** `App\ValueObjects\PricingResult`
- `subtotal` - Sum of all line subtotals (cents)
- `discount` - Total discount amount (cents)
- `shipping` - Shipping cost (cents)
- `taxLines` - Array of TaxLine objects
- `taxTotal` - Sum of all tax amounts (cents)
- `total` - Final amount: discounted_subtotal + shipping + taxTotal (cents)
- `currency` - ISO 4217 code

**TaxLine Value Object:**
- `name` - Tax label (e.g., "State Tax")
- `rate` - Tax rate in basis points (e.g., 800 = 8.00%)
- `amount` - Calculated tax amount (cents)

**Rounding Strategy:**
- Each line's discount, tax, total rounded independently
- Totals computed by summing already-rounded line amounts
- Prevents fractional cent accumulation
- Remainder from proportional allocation added to last qualifying line (largest-remainder method)

**Determinism & Snapshotting:**
- Store full result in `checkouts.totals_json` as JSON snapshot
- Recalculate on: address change (zone/tax changes), shipping method change, discount code change
- Same inputs always produce same output

---

## 6. CHECKOUT STATE MACHINE

**Service:** `App\Services\CheckoutService`

**Status Enum:** `CheckoutStatus` - started, addressed, shipping_selected, payment_selected, completed, expired

**State Transitions:**

### Transition: started → addressed
**Method:** `setAddress(checkout, addressData)`

**Required data:**
- `email` - valid email address
- `shipping_address`:
  - first_name (required)
  - last_name (required)
  - address1 (required)
  - address2 (optional)
  - company (optional)
  - city (required)
  - province (optional)
  - province_code (optional, for zone matching)
  - country (required, 2-letter ISO 3166-1)
  - postal_code (required)
  - phone (optional)

**Actions:**
1. Validate using `SetCheckoutAddressRequest` form request
2. Save email and shipping_address_json
3. Copy shipping address to billing_address_json (can be overridden later)
4. Recalculate pricing (zones/tax may change)
5. Transition to `addressed`
6. Dispatch `CheckoutAddressed` event

### Transition: addressed → shipping_selected
**Method:** `setShippingMethod(checkout, shippingRateId)`

**Skip Condition:** If no cart lines have `requires_shipping=true`, skip shipping step. Set status to `shipping_selected` with `shipping_method_id=null`, `shipping_amount=0`.

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

### Transition: shipping_selected → payment_selected
**Method:** `selectPaymentMethod(checkout, paymentMethod)`

**Requires:**
- All prior data valid (address, shipping)
- Totals calculated and stored
- `paymentMethod` = `credit_card`, `paypal`, or `bank_transfer`

**Actions:**
1. Store `payment_method` on checkout
2. Reserve inventory for all cart lines (call `InventoryService.reserve` per line)
3. Set `expires_at = now + 24 hours`
4. Transition to `payment_selected`

### Transition: payment_selected → completed
**Method:** `completeCheckout(checkout, paymentMethodData)` - returns Order

**CRITICAL: IDEMPOTENT** - If called multiple times (double-click), must not create duplicate orders. Always check if order already exists.

**Actions (within single transaction):**
```
PROCEDURE completeCheckout(checkout, paymentMethodData):
  BEGIN TRANSACTION

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

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

  3. Generate next sequential order number for store

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

  5. CREATE order record with all checkout data + statuses

  6. FOR EACH cart line:
     CREATE order_line with snapshot data:
       - title_snapshot = product title + variant option labels
       - sku_snapshot = variant SKU
       - unit_price_amount, quantity, total_amount
       - tax_lines_json, discount_allocations_json

  7. CREATE payment record:
     - Link to order, provider='mock'
     - method = checkout.payment_method
     - provider_payment_id = mock reference
     - status = paymentStatus
     - amount = order total

  8. IF inventoryAction = 'commit':
     FOR EACH cart line:
       CALL InventoryService.commit(variant, quantity)
       // For bank_transfer: inventory stays reserved

  9. IF checkout has discount code:
     INCREMENT discount.usage_count by 1

  10. SET cart.status = 'converted'

  11. SET checkout.status = 'completed'

  12. IF inventoryAction='commit' AND all order lines are digital:
      Auto-create fulfillment (see Section 11.7)

  13. DISPATCH OrderCreated event

  COMMIT TRANSACTION
  RETURN order
```

### Transition: Any active state → expired
**Triggered by:** `App\Jobs\ExpireAbandonedCheckouts` (every 15 minutes)

**Conditions:** Checkouts where status NOT IN (completed, expired) AND updated_at < (now - 24 hours)

**Actions:**
1. If inventory was reserved (status was payment_selected): release via `InventoryService.release`
2. Transition to `expired`
3. Dispatch `CheckoutExpired` event

**Events:**
- `CheckoutAddressed` - Address saved
- `CheckoutShippingSelected` - Shipping method selected
- `CheckoutCompleted` - Payment confirmed and order created
- `CheckoutExpired` - Checkout expired due to inactivity

---

## 7. DISCOUNT ENGINE

**Service:** `App\Services\DiscountService`

**Discount Types:** `DiscountType` - code, automatic

| Type | Behavior |
|------|----------|
| `code` | Customer enters at checkout. Matched case-insensitively. |
| `automatic` | Applied automatically when rule conditions met. Evaluated on every pricing calculation. |

**Stacking Rules:**
- Only ONE code discount per checkout
- Multiple automatic discounts can stack (applied sequentially to remaining amount)

**Status Lifecycle:**
| Status | Description | Transition |
|--------|-------------|-----------|
| `draft` | Not active, not visible to customers | Admin creates → draft. Admin activates → active. |
| `active` | Live and applicable (subject to date/usage limits) | Auto-transition when starts_at reached OR admin activates. |
| `expired` | Past ends_at date | Auto-transition when ends_at passes. Irreversible. |
| `disabled` | Manually deactivated | Admin disables active → disabled. Admin re-enables → active. |

**Allowed Transitions:**
- draft → active
- active → disabled
- active → expired (automatic)
- disabled → active

**Value Types:** `DiscountValueType` - percent, fixed, free_shipping

| Type | Calculation | Notes |
|------|-------------|-------|
| `percent` | `discount = applicable_subtotal * value_amount / 100` | value_amount = whole percentage (15 = 15%) |
| `fixed` | `discount = value_amount` (cents) | Cannot exceed applicable subtotal |
| `free_shipping` | Override shipping to 0 | No item discount. value_amount ignored. |

**Rules** (stored in `discounts.rules_json`):
- `min_purchase_amount` (integer cents or null) - Cart subtotal must be >= this
- `applicable_product_ids` (array or null) - Discount applies only to these products
- `applicable_collection_ids` (array or null) - Discount applies to products in these collections
- `customer_eligibility` (string) - `"all"` for all customers. Future: `"returning"`, `"specific"`.

**Applicability Logic:**
- When both product_ids AND collection_ids set: product qualifies if matches EITHER (union, not intersection)
- When both null/empty: applies to entire order

**Validation Flow:** `validate(code, cart)` → DiscountValidationResult

Check conditions in order:
1. Code exists in store? (case-insensitive) → error: `discount_not_found`
2. Status = active? → error: `discount_expired`
3. starts_at <= now? → error: `discount_not_yet_active`
4. ends_at is null OR ends_at >= now? → error: `discount_expired`
5. usage_limit set AND usage_count >= usage_limit? → error: `discount_usage_limit_reached`
6. min_purchase_amount set AND subtotal < minimum? → error: `discount_min_purchase_not_met`
7. Product/collection restrictions set AND no qualifying lines? → error: `discount_not_applicable`
8. If all pass: Valid - return discount

**Error Codes:**
- `discount_not_found` - Code doesn't exist in store
- `discount_expired` - Past ends_at or status not active
- `discount_not_yet_active` - Before starts_at
- `discount_usage_limit_reached` - usage_count >= usage_limit
- `discount_min_purchase_not_met` - Subtotal < minimum
- `discount_not_applicable` - No qualifying products in cart

**Discount Allocation** (for percent/fixed with product restrictions):
```
PROCEDURE allocateDiscount(discount, cartLines):
  qualifyingLines = FILTER cartLines where product matches applicable_product_ids
                    OR product IN applicable_collection_ids
  qualifyingSubtotal = SUM(qualifyingLines.line_subtotal_amount)

  IF discount.value_type = 'percent':
    totalDiscount = qualifyingSubtotal * discount.value_amount / 100
  ELSE IF discount.value_type = 'fixed':
    totalDiscount = MIN(discount.value_amount, qualifyingSubtotal)

  remainingDiscount = totalDiscount

  FOR EACH qualifyingLine (except last):
    lineDiscount = ROUND(totalDiscount * lineSubtotal / qualifyingSubtotal)
    remainingDiscount -= lineDiscount
    SET line.line_discount_amount = lineDiscount

  // Last line gets remainder (largest-remainder method)
  SET lastLine.line_discount_amount = remainingDiscount

  RECALCULATE line_total_amount for each affected line
```

**Storage of Allocations:**
- During checkout: update `cart_lines.line_discount_amount`
- On order creation: store in `order_lines.discount_allocations_json` as array of `{discount_id, amount}`

**Usage Tracking:**
- When order created with discount code: INCREMENT `discounts.usage_count` by 1
- Happens within order creation transaction (Section 6.2, step 9)

---

## 8. TAX CALCULATION

**Interface:** `App\Contracts\TaxProvider` - implement `calculate(request)` → TaxCalculationResult

**Service:** `App\Services\TaxCalculator` - orchestrates provider selection

**Implementations:**
- `App\Services\Tax\ManualTaxProvider` - default, manual rates
- `App\Services\Tax\StripeTaxProvider` - stub for Stripe Tax API integration

### Manual Tax Mode

**Rate Resolution:**
1. Match shipping address against shipping zones (same algorithm as shipping - see Section 9.1)
2. Use tax rate from `tax_settings.config_json` for matched zone, or global default
3. Rate stored as integer basis points (e.g., 1900 = 19.00%)

**Calculation per Line:**
```
line_tax = ROUND(line_total_after_discount * rate / 10000)
```
- Round each line independently, then sum for total tax

### Prices Include Tax

Controlled by `tax_settings.prices_include_tax` (boolean):

**Tax-inclusive pricing** (prices_include_tax = true):
- Displayed prices are gross (include tax)
- Tax "extracted" from gross amount:
  ```
  net_amount = intdiv(gross_amount * 10000, 10000 + rate_bps)
  tax = gross_amount - net_amount
  ```
- Uses PHP's `intdiv()` (truncation toward zero) for determinism

Example: Price=1190 cents, rate=1900 (19%)
- net = intdiv(1190 * 10000, 11900) = intdiv(11900000, 11900) = 1000
- tax = 1190 - 1000 = 190

**Tax-exclusive pricing** (prices_include_tax = false):
- Displayed prices are net (excluding tax)
- Tax added on top:
  ```
  tax = ROUND(net_amount * rate / 10000)
  total = net_amount + tax
  ```

Example: Price=1000 cents, rate=1900
- tax = ROUND(1000 * 1900 / 10000) = 190
- total = 1190

### Stripe Tax Provider (Stub)

**Implementation:** `App\Services\Tax\StripeTaxProvider`

- Sends line items and address to Stripe Tax API
- Stores full response in `checkouts.tax_provider_snapshot_json` for audit
- Fallback behavior (configurable per store):
  - `"block"` - Block checkout if API errors
  - `"allow"` - Allow checkout without tax (log warning)

### Discount-Before-Tax Semantics

Discounts applied BEFORE tax. Tax calculated on discounted amount.

For tax-inclusive pricing: Discount subtracted from gross subtotal. Tax extracted from post-discount gross: `tax = post_discount_gross * rate / (1 + rate)`. Tax portion decreases proportionally when discount applied.

**Calculation order within pricing pipeline:**
1. Calculate line subtotals
2. Apply discount allocations to lines
3. Calculate tax on discounted line amounts
4. Sum for total

---

## 9. SHIPPING CALCULATION

**Service:** `App\Services\ShippingCalculator`

### Zone Matching Algorithm

```
PROCEDURE getMatchingZone(address, store):
  zones = load all shipping zones for store

  bestMatch = null
  bestSpecificity = -1

  FOR EACH zone IN zones:
    countryMatch = address.country_code IN zone.countries_json
    regionMatch = address.province_code IN zone.regions_json

    IF countryMatch AND regionMatch:
      specificity = 2  // Most specific
    ELSE IF countryMatch:
      specificity = 1  // Country-only
    ELSE:
      CONTINUE  // No match

    IF specificity > bestSpecificity:
      bestMatch = zone
      bestSpecificity = specificity
    ELSE IF specificity = bestSpecificity AND zone.id < bestMatch.id:
      bestMatch = zone  // Tie-break: lowest ID wins

  RETURN bestMatch  // null if no match
```

**When no zone matches:** Address unserviceable. Block checkout with error "Cannot ship to this address".

### Rate Types

For matched zone, return all active rates (`is_active=1`). Calculate cost for each:

**Flat Rate:**
- Config: `{"amount": 500}`
- Cost = fixed amount regardless of contents

**Weight-Based Rate:**
- Config: `{"ranges": [{"min_g": 0, "max_g": 1000, "amount": 500}, {"min_g": 1001, "max_g": 5000, "amount": 1000}]}`

```
PROCEDURE calculateWeightRate(config, cart):
  totalWeight = 0
  FOR EACH line IN cart.lines:
    IF line.variant.requires_shipping = true:
      totalWeight += line.variant.weight_g * line.quantity

  FOR EACH range IN config.ranges:
    IF range.min_g <= totalWeight AND totalWeight <= range.max_g:
      RETURN range.amount

  RETURN null  // Rate not available
```

**Price-Based Rate:**
- Config: `{"ranges": [{"min_amount": 0, "max_amount": 5000, "amount": 500}, {"min_amount": 5001, "amount": 0}]}`

```
PROCEDURE calculatePriceRate(config, cartSubtotal):
  FOR EACH range IN config.ranges:
    IF range.min_amount <= cartSubtotal:
      IF range.max_amount NOT SET OR cartSubtotal <= range.max_amount:
        RETURN range.amount

  RETURN null  // Rate not available
```

Range without `max_amount` matches all subtotals >= `min_amount` (useful for "free shipping over X").

**Carrier-Calculated Rate (Stub):**
- Config: `{"carrier": "ups", "service": "ground"}`
- Placeholder for future carrier API integration
- Returns hardcoded rate or error until implemented

### Requires Shipping Check

- If no line has `requires_shipping=true`: skip shipping step entirely
- shipping_amount = 0
- Checkout transitions directly past shipping_selected (set status with shipping_method_id=null)

**Mixed Carts** (physical + digital):
- Shipping calculated only for physical items' weight
- Digital items excluded from weight calculations

### Free Shipping Discount Override

If validated discount has `value_type='free_shipping'`:
- Override calculated shipping to 0
- Shipping method still selected (for fulfillment routing)
- Customer not charged

---

## 10. PAYMENT PROCESSING (MOCK PSP)

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

Must implement:
- `charge(checkout, paymentMethodData)` → PaymentResult
- `refund(payment, amount)` → RefundResult

**Service:** `App\Services\PaymentService` - orchestrates

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

### Supported Payment Methods

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

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

### Magic Card Numbers (Credit Card Testing)

When payment_method=`credit_card`, mock provider inspects card number:

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

Mock provider does NOT validate card expiry, CVC, cardholder name. Fields exist for UI realism but ignored by backend.

### Mock Payment Flow

```
Customer → Storefront → Backend → MockPSP

Customer selects payment method + enters details
Storefront → Backend: POST /checkouts/{id}/pay

Backend: Reserve inventory (if not already reserved)
Backend → MockPSP: charge(checkout, paymentMethodData)

IF Credit Card / PayPal (instant capture):
  MockPSP → Backend: PaymentResult (success/failure)
  IF Success:
    Backend: Create order (financial_status=paid)
    Backend: Commit inventory, mark cart converted
    Backend → Storefront: 200 OK (order confirmation)
  ELSE (decline / insufficient funds):
    Backend: Release reserved inventory
    Backend → Storefront: 422 (error code + message)

IF Bank Transfer (deferred):
  MockPSP → Backend: PaymentResult (success=true, status=pending)
  Backend: Create order (financial_status=pending)
  Backend: Inventory remains reserved (not committed)
  Backend → Storefront: 200 OK (order confirmation + bank details)
```

### Payment State Machine

**Enum:** `PaymentStatus` - pending, captured, failed, refunded

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

### Bank Transfer Instructions

When payment_method=`bank_transfer`, order confirmation displays mock details:

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

Static mock values. No real bank interaction.

### Admin: Confirm Payment (Bank Transfer)

Admin can confirm bank transfer payment received via "Confirm Payment" button (visible when payment_method=`bank_transfer` AND financial_status=`pending`).

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

### Auto-Cancel Unpaid Bank Transfer Orders

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

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

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

---

## 11. ORDER MANAGEMENT

### Order Creation (from Completed Checkout)

See Section 6.2 "Transition: payment_selected → completed" for full procedure.

**Key Requirements:**
- Snapshot all checkout data (addresses, email, totals) at order creation
- Order lines contain snapshot data (`title_snapshot`, `sku_snapshot`) so history survives product changes
- Mock payment response encrypted before storage
- Entire operation atomic (single transaction)
- `payment_method` from checkout copied to order

**Payment-Method-Specific Order Creation:**

| Payment Method | `orders.status` | `orders.financial_status` | Inventory Action | Payment Record Status |
|---|---|---|---|---|
| `credit_card` | `paid` | `paid` | Commit | `captured` |
| `paypal` | `paid` | `paid` | Commit | `captured` |
| `bank_transfer` | `pending` | `pending` | Reserve only | `pending` |

### Order Numbering

- Sequential per store, starting at 1001
- Format: configurable prefix (default: `#`) from `store_settings.settings_json` key `order_number_prefix`
- Generation: within order creation transaction, find max existing order number and increment
- SQLite transaction prevents duplicate numbers

### Order Statuses

Three independent dimensions:

**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 confirmed |
| `authorized` | Payment authorized but not captured |
| `paid` | Payment captured |
| `partially_refunded` | Some items or partial amount refunded |
| `refunded` | Fully refunded |
| `voided` | Authorization voided (no capture) |

**Fulfillment Status** (`orders.fulfillment_status`):
| Status | Description |
|--------|-------------|
| `unfulfilled` | No items shipped |
| `partial` | Some items shipped, others pending |
| `fulfilled` | All items shipped |

### Refund Processing

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

**Input (RefundRequest):**
- `amount` (integer cents, optional) - Custom refund amount
- `lines` (map of order_line_id → quantity, optional) - Which lines/quantities to refund
- `reason` (string, optional) - Text reason
- `restock` (boolean, optional) - Whether to restock inventory

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

  2. IF request.amount set:
       VALIDATE request.amount <= refundable
     ELSE IF request.lines set:
       CALCULATE amount from specified lines/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:
     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/quantity:
       CALL InventoryService.restock(variant, quantity)

  8. DISPATCH OrderRefunded event
```

### Fulfillment

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

#### Fulfillment Guard (CRITICAL)

**Fulfillment CANNOT be created unless order's financial_status is `paid` or `partially_refunded`.** This prevents shipping before payment confirmed.

| `financial_status` | Fulfillment Allowed? | Reason |
|---|---|---|
| `pending` | **No** | Payment not 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 doesn't block shipment |
| `refunded` | **No** | Fully refunded, nothing to ship |
| `voided` | **No** | Payment voided/cancelled |

When blocked: Admin UI shows callout "Fulfillment cannot be created until payment is confirmed." Button disabled.

#### Creating a Fulfillment

**Input:**
- `lines` - Map of order_line_id → quantity specifying which items in 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 + tracking data

  4. CREATE fulfillment_lines for each order line/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)

Triggered by explicit admin button clicks. No automatic transitions for physical products.

```
[*] → pending (Fulfillment created - admin clicks "Create fulfillment")
pending → shipped (Admin clicks "Mark as shipped")
shipped → delivered (Admin clicks "Mark as delivered")
```

**Admin buttons on each fulfillment card:**

| Button | Shown When | Action |
|--------|------------|--------|
| "Mark as shipped" | fulfillment.status=pending | Transition → shipped, set shipped_at, dispatch FulfillmentShipped, send tracking email |
| "Mark as delivered" | fulfillment.status=shipped | Transition → delivered, dispatch FulfillmentDelivered |

**When transitioning → shipped:**
- Set `shipped_at` timestamp
- Dispatch `FulfillmentShipped` event
- Send email to customer with tracking info

**When transitioning → delivered:**
- Dispatch `FulfillmentDelivered` event

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

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

For bank transfer: inventory held in reserved state until admin confirms payment. If auto-cancelled, reserved inventory released.

#### Auto-Fulfillment for Digital Products

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

1. Check if ALL order lines reference variants with `requires_shipping=false` (digital)
2. If yes: auto-create fulfillment covering all lines:
   - status=`delivered` (skip pending/shipped)
   - No tracking_company/number/url
   - shipped_at=now
   - order.fulfillment_status=`fulfilled`
   - order.status=`fulfilled`
3. If mix of physical + digital: do NOT auto-fulfill. Admin must create manually (can fulfill digital immediately, physical when shipped).

Ensures digital orders immediately marked complete after payment.

### Order Export

**Endpoint:** `GET /api/admin/v1/orders/export` - Returns CSV

**CSV Columns:** order_number, created_at, status, financial_status, fulfillment_status, customer_email, customer_name, subtotal_amount, discount_amount, shipping_amount, tax_amount, total_amount, currency, shipping_method, tracking_number

- Monetary values in minor units (cents)
- Dates in ISO-8601 format
- Content-Type: `text/csv`
- Content-Disposition: `attachment; filename="orders-{date}.csv"`

---

## 12. CUSTOMER ACCOUNTS

### Registration

**Service:** `App\Services\CustomerService`

**Required Fields:**

| Field | Validation |
|-------|-----------|
| `name` | Required, string |
| `email` | Required, valid email, unique per store |
| `password` | Required, minimum 8 characters |
| `marketing_opt_in` | Optional, boolean, default false |

- Email uniqueness: database unique index on `(store_id, email)`
- Password hashing: Laravel Hash facade (bcrypt by default)

### Authentication

**Separate auth guard:** `customer`

Requirements:
- Configure dedicated `customer` guard in auth config, session driver
- Configure dedicated `customers` provider using Customer model
- Login: email + password, scoped to current store
- Password reset: standard Laravel flow adapted for customer guard/broker

**Store Isolation:** Customer accounts per-store. Same email can exist in different stores. Auth always scopes query to current store.

### Data Access

- Customers view only own orders (scoped by customer_id)
- Customers manage only own addresses (scoped by customer_id)
- All customer queries include StoreScope global scope

### Guest Cart to Customer Linking

```
PROCEDURE linkGuestToCustomer(checkoutEmail, storeId, order):
  existingCustomer = find customer by email in store
  IF existingCustomer:
    LINK order to existingCustomer
  ELSE:
    CREATE new customer with:
      - email = checkoutEmail
      - password_hash = null (guest customer)
      - store_id = storeId
    LINK order to new customer

  // Guest customer can later "claim" account
  // by setting password via registration/account-claim flow
```

---

## 13. WEBHOOK DELIVERY SYSTEM

### Subscription Model

Stores subscribe to event types via `webhook_subscriptions`.

**Supported Event Types:**

| Event Type | Triggered By |
|---|---|
| `order.created` | OrderCreated event |
| `order.paid` | Payment captured successfully |
| `order.fulfilled` | All order lines fulfilled |
| `order.refunded` | Refund processed |
| `product.created` | New product saved |
| `product.updated` | Product modified |
| `product.deleted` | Product deleted/archived |
| `checkout.completed` | Checkout successfully completed |

### Delivery Process

**Listener:** `App\Listeners\DispatchWebhooks` - registered to listen for domain events

**Job:** `App\Jobs\DeliverWebhook`

```
DomainEvent → Listener → Query webhook_subscriptions
             FOR EACH matching subscription:
               Dispatch DeliverWebhook job

Job: Serialize payload as JSON
     Generate HMAC-SHA256 signature
     HTTP POST with headers + payload
     Record result in webhook_deliveries
```

**Required HTTP Headers:**

| Header | Value |
|--------|-------|
| `X-Platform-Signature` | HMAC-SHA256 hex digest of payload |
| `X-Platform-Event` | Event type (e.g., `order.created`) |
| `X-Platform-Delivery-Id` | Unique UUID for delivery attempt |
| `X-Platform-Timestamp` | Unix timestamp of event |
| `Content-Type` | `application/json` |

### Retry Logic

On non-2xx response or timeout:

| Attempt | Backoff Delay |
|---------|---------------|
| 1 (initial) | Immediate |
| 2 | 1 minute (60s) |
| 3 | 5 minutes (300s) |
| 4 | 30 minutes (1800s) |
| 5 | 2 hours (7200s) |
| 6 (final) | 12 hours (43200s) |

After 5 retries (6 total): mark delivery as `failed` (dead letter).

Use Laravel's built-in job retry with backoff: 60, 300, 1800, 7200, 43200 seconds.

### Circuit Breaker

- Track consecutive failures per subscription
- If endpoint fails 5 consecutive times: auto-set `webhook_subscriptions.status='paused'`
- Log warning (future: send admin notification)
- Manual re-enable required through admin UI
- On each successful delivery: reset consecutive failure counter to 0

**Service:** `App\Services\WebhookService`

---

## 14. ANALYTICS

### Event Ingestion

**Endpoint:** `POST /api/storefront/v1/analytics/events`

**Request Structure:**
```json
{
  "events": [
    {
      "type": "page_view",
      "session_id": "...",
      "client_event_id": "...",
      "occurred_at": "2025-01-15T10:30:00Z",
      "properties": { ... }
    }
  ]
}
```

**Event Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | One of: page_view, product_view, add_to_cart, checkout_started, checkout_completed |
| `session_id` | string | Yes | Client session identifier |
| `client_event_id` | string | Yes | Client-generated unique ID for deduplication |
| `occurred_at` | string (ISO-8601) | Yes | When event occurred on client |
| `properties` | object | No | Event-specific data (product_id, variant_id, etc.) |

**Deduplication:** Events deduplicated by `client_event_id` within store scope. Enforced by unique index on `(store_id, client_event_id)`. Duplicate events silently dropped.

**Rate Limiting:** 60 requests per minute per IP address, using Laravel's throttle middleware.

### Aggregation

**Job:** `App\Jobs\AggregateAnalytics` - Daily at 01:00 UTC

Aggregates previous day's raw `analytics_events` into `analytics_daily`.

**Aggregated Metrics:**

| Metric | Calculation |
|--------|-------------|
| `orders_count` | Count of checkout_completed events for day |
| `revenue_amount` | Sum of order totals from checkout_completed event properties |
| `aov_amount` | revenue_amount / orders_count (integer division, 0 if no orders) |
| `visits_count` | Count of distinct session_id values with page_view events |
| `add_to_cart_count` | Count of add_to_cart events |
| `checkout_started_count` | Count of checkout_started events |

---

## 15. MEDIA PROCESSING

### Upload Flow

```
Admin uploads file via Livewire
→ Store original in storage/app/public/media/originals/
→ Create product_media record with status=processing
→ Dispatch ProcessMediaUpload job to queue
→ Processing succeeds?
   YES: Store resized versions, set status=ready
   NO: Set status=failed, log error, keep original
```

### Processing

**Job:** `App\Jobs\ProcessMediaUpload`

**Resize Targets:**

| Key | Max Dimensions | Use Case |
|-----|---|---|
| `thumbnail` | 150x150 px | Admin grid, small previews |
| `small` | 300x300 px | Product cards, listings |
| `medium` | 600x600 px | Product detail page |
| `large` | 1200x1200 px | Full-size product image, zoom |

**Processing Rules:**
- Maintain aspect ratio (contain within target, no cropping)
- Generate WebP version if server supports
- Store all sizes with predictable path keys: `media/{product_id}/{media_id}/{size}.{ext}`
- Update `product_media` record with: width, height, mime_type, byte_size (from original), status=`ready`
- On failure: set status=`failed`, log error, keep original
- Maximum 3 retry attempts before permanent failure

### Cleanup

When `product_media` record deleted: model event listener removes all associated files from storage (all sizes + original).

---

## 16. SEARCH (SQLite FTS5)

### Index Setup

Create FTS5 virtual table named `products_fts`:

| Column | Indexed | Description |
|--------|---------|-------------|
| `store_id` | No (UNINDEXED) | Used for filtering, not searching |
| `product_id` | No (UNINDEXED) | Foreign key reference |
| `title` | Yes | Product title, fully searchable |
| `description` | Yes | Plain-text version of description_html (HTML stripped) |
| `vendor` | Yes | Vendor name |
| `product_type` | Yes | Product type |
| `tags` | Yes | Space-separated tag values |

### Index Synchronization

**Observer:** `App\Observers\ProductObserver`

| Model Event | FTS Action |
|---|---|
| `created` | Insert row into products_fts |
| `updated` | Delete old row, insert updated row (FTS5 no UPDATE) |
| `deleted` | Delete row from products_fts |

**Data Preparation Before Insert:**
- Strip HTML tags from `description_html`
- Convert JSON tags array to space-separated string
- Use empty string for null vendor/product_type

### Search Query

**Service:** `App\Services\SearchService`

**Algorithm:**
```
PROCEDURE search(store, queryString, limit=20):
  1. Sanitize queryString: escape FTS5 special characters
  2. Append '*' to last token for prefix matching (autocomplete)
     Example: "running sh" → "running sh*"
  3. Execute FTS5 MATCH query:
     - Filter by store_id
     - Order by FTS5 relevance rank
     - Limit results
  4. Load matching Product models
  5. Exclude products with status != 'active' or published_at IS NULL
  6. RETURN ordered collection
```

### Search Logging

Each search logged to `search_queries` with: store_id, query text, results_count, created_at. Data feeds analytics and can inform autocomplete suggestions.

---

## 17. EVENT & LISTENER REGISTRY

| Event | Listeners / Side Effects |
|-------|--------------------------|
| `OrderCreated` | Send confirmation email, dispatch webhooks (order.created), log analytics |
| `OrderRefunded` | Send refund notification email, dispatch webhooks (order.refunded), optionally restock |
| `CheckoutAddressed` | Recalculate pricing |
| `CheckoutShippingSelected` | Recalculate pricing |
| `CheckoutCompleted` | Dispatch webhooks (checkout.completed) |
| `CheckoutExpired` | Release reserved inventory |
| `ProductStatusChanged` | Sync FTS5 index if transitioning to/from active |
| `ProductCreated` | Sync FTS5 index, dispatch webhooks (product.created) |
| `ProductUpdated` | Sync FTS5 index, dispatch webhooks (product.updated) |
| `ProductDeleted` | Remove from FTS5 index, dispatch webhooks (product.deleted) |
| `OrderPaid` | Update order status, commit inventory (bank transfer), auto-fulfill digital |
| `FulfillmentCreated` | Update order fulfillment status |
| `FulfillmentShipped` | Send shipping notification + tracking, dispatch webhooks (order.fulfilled if complete) |
| `FulfillmentDelivered` | Update fulfillment record, dispatch webhooks |
| `OrderCancelled` | Release reserved inventory, send cancellation email |

All events: `App\Events`. All listeners: `App\Listeners`.

---

## 18. SCHEDULED JOBS REGISTRY

| Job Class | Schedule | Description |
|-----------|----------|-------------|
| `App\Jobs\CleanupAbandonedCarts` | Daily | Mark stale carts as abandoned (14-day threshold) |
| `App\Jobs\ExpireAbandonedCheckouts` | Every 15 minutes | Expire checkouts idle 24+ hours, release inventory |
| `App\Jobs\AggregateAnalytics` | Daily at 01:00 UTC | Roll up raw analytics into daily aggregates |
| `App\Jobs\CancelUnpaidBankTransferOrders` | Daily | Cancel unpaid bank transfer orders (7-day default) |

**Queued Jobs (on-demand):**

| Job Class | Description |
|-----------|-------------|
| `App\Jobs\ProcessMediaUpload` | Resize uploaded images to standard sizes |
| `App\Jobs\DeliverWebhook` | POST webhook payload to subscriber endpoint |

Schedule registration: `routes/console.php`

---

## 19. SERVICE CLASS REGISTRY

| Service | Namespace | Responsibility |
|---------|-----------|-----------------|
| ProductService | `App\Services` | Product CRUD, status transitions |
| VariantMatrixService | `App\Services` | Option/variant matrix management |
| InventoryService | `App\Services` | Reserve, release, commit, restock operations |
| CartService | `App\Services` | Cart CRUD, line management, merge on login |
| PricingEngine | `App\Services` | Deterministic total calculation pipeline |
| CheckoutService | `App\Services` | Checkout state machine transitions |
| DiscountService | `App\Services` | Discount validation and application |
| TaxCalculator | `App\Services` | Tax calculation orchestration |
| ManualTaxProvider | `App\Services\Tax` | Manual tax rate calculation |
| StripeTaxProvider | `App\Services\Tax` | Stripe Tax API integration (stub) |
| ShippingCalculator | `App\Services` | Zone matching, rate calculation |
| PaymentService | `App\Services` | Payment processing orchestration |
| MockPaymentProvider | `App\Services\Payment` | Mock PSP - simulates card/PayPal/bank transfer |
| RefundService | `App\Services` | Refund processing and inventory restock |
| FulfillmentService | `App\Services` | Fulfillment creation and status management |
| CustomerService | `App\Services` | Customer registration, account management |
| WebhookService | `App\Services` | Webhook delivery, circuit breaker |
| SearchService | `App\Services` | FTS5 search queries |
| HandleGenerator | `App\Support` | Unique handle/slug generation |

---

## 20. CONTRACTS (INTERFACES)

| Interface | Namespace | Implementing Classes |
|-----------|-----------|----------------------|
| TaxProvider | `App\Contracts` | ManualTaxProvider, StripeTaxProvider |
| PaymentProvider | `App\Contracts` | MockPaymentProvider |

---

## 21. ENUM REGISTRY

All in `App\Enums`, string-backed. Keys in TitleCase, values lowercase:

| Enum | Values |
|------|--------|
| StoreUserRole | Owner (owner), Admin (admin), Staff (staff), Support (support) |
| ProductStatus | Draft (draft), Active (active), Archived (archived) |
| VariantStatus | Active (active), Archived (archived) |
| DiscountType | Code (code), Automatic (automatic) |
| DiscountValueType | Percent (percent), Fixed (fixed), FreeShipping (free_shipping) |
| CartStatus | Active (active), Converted (converted), Abandoned (abandoned) |
| CheckoutStatus | Started (started), Addressed (addressed), ShippingSelected (shipping_selected), PaymentSelected (payment_selected), Completed (completed), Expired (expired) |
| PaymentMethod | CreditCard (credit_card), Paypal (paypal), BankTransfer (bank_transfer) |
| PaymentStatus | Pending (pending), Captured (captured), Failed (failed), Refunded (refunded) |
| OrderStatus | Pending (pending), Paid (paid), Fulfilled (fulfilled), Cancelled (cancelled), Refunded (refunded) |
| FinancialStatus | Pending (pending), Authorized (authorized), Paid (paid), PartiallyRefunded (partially_refunded), Refunded (refunded), Voided (voided) |
| FulfillmentOrderStatus | Unfulfilled (unfulfilled), Partial (partial), Fulfilled (fulfilled) |
| FulfillmentShipmentStatus | Pending (pending), Shipped (shipped), Delivered (delivered) |
| ShippingRateType | Flat (flat), Weight (weight), Price (price), Carrier (carrier) |
| MediaStatus | Processing (processing), Ready (ready), Failed (failed) |
| WebhookSubscriptionStatus | Active (active), Paused (paused), Disabled (disabled) |

---

## 22. VALUE OBJECT REGISTRY

All in `App\ValueObjects`.

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

---

This comprehensive summary covers all business logic specifications for implementation. Every detail from validations, state machines, calculations, and workflows has been captured for reference during development.

text:
agentId: ada37a8 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 67491
tool_uses: 5
duration_ms: 158826</usage>

status: completed

prompt: Read the file /Users/fabianwesner/Herd/shop/specs/05-BUSINESS-LOGIC.md in chunks (500 lines at a time) and provide a comprehensive summary of ALL business logic rules including: services, state machines, validation rules, pricing calculations, inventory management, payment processing, and fulfillment rules. I need every detail for implementation.


userType: external

slug: iterative-seeking-pine

type: message

role: assistant

type: text

service_tier: standard

type: assistant


type: thinking

thinking:
Now I have a comprehensive understanding of the entire project. This is a massive undertaking - approximately 320-380 files need to be created. Let me organize this into a team-based approach.

Let me create the team, set up tasks, and start implementing. Given the sequential dependencies, I'll need to:

1. First implement Phase 1 (Foundation) - everything depends on this
2. Then parallelize Phases 2 and 3
3. Then Phase 4 (depends on 2 and 3)
4. Then Phase 5 (depends on 4)
5. Then parallelize Phases 6, 7, 8, 9, 10

6. Phase 11 (Polish)
7. Phase 12 (Full Test Suite)

I'll create a specialized team with agents focused on different domains - one handling the foundation layer with migrations and auth, another managing the product catalog and inventory, a third building the storefront experience, and a fourth tackling commerce operations and beyond.

Since these phases have strict dependencies, I need to be strategic about what can run in parallel. I'll start Phase 1 myself since everything builds on it, then spawn agents for the subsequent phases once that foundation is solid.

Let me set up the team structure and progress tracking first, then kick off the implementation.


text: I now have a complete understanding of all specifications. This is a massive project (~320-380 files). Let me create the team and start implementing.


Write

content:
# Implementation Progress

## Overview
- **Start:** 2026-02-13
- **Phases:** 12 total
- **Estimated Files:** ~320-380

## Phase Status

| Phase | Name | Status | Progress |
|-------|------|--------|----------|
| 1 | Foundation (Migrations, Models, Middleware, Auth) | In Progress | 0% |
| 2 | Catalog (Products, Variants, Inventory, Collections, Media) | Pending | 0% |
| 3 | Themes, Pages, Navigation, Storefront Layout | Pending | 0% |
| 4 | Cart, Checkout, Discounts, Shipping, Taxes | Pending | 0% |
| 5 | Payments, Orders, Fulfillment | Pending | 0% |
| 6 | Customer Accounts | Pending | 0% |
| 7 | Admin Panel | Pending | 0% |
| 8 | Search | Pending | 0% |
| 9 | Analytics | Pending | 0% |
| 10 | Apps and Webhooks | Pending | 0% |
| 11 | Polish | Pending | 0% |
| 12 | Full Test Suite | Pending | 0% |

## Detailed Log

### Phase 1: Foundation
- [ ] Step 1.1: Environment and Config
- [ ] Step 1.2: Core Migrations (Batch 1-2)
- [ ] Step 1.3: Core Models
- [ ] Step 1.4: Enums
- [ ] Step 1.5: Tenant Resolution Middleware
- [ ] Step 1.6: BelongsToStore Trait and Global Scope
- [ ] Step 1.7: Authentication
- [ ] Step 1.8: Authorization
- [ ] Tests written and passing

### Phase 2: Catalog
- [ ] Step 2.1: Migrations (Batch 3-5)
- [ ] Step 2.2: Models with Relationships
- [ ] Step 2.3: Product Service
- [ ] Step 2.4: Inventory Service
- [ ] Step 2.5: Media Upload
- [ ] Tests written and passing

### Phase 3: Themes and Storefront
- [ ] Step 3.1: Migrations (Batch 3)
- [ ] Step 3.2: Models
- [ ] Step 3.3: Storefront Blade Layout
- [ ] Step 3.4: Storefront Livewire Components
- [ ] Step 3.5: Navigation Service
- [ ] Tests written and passing

### Phase 4: Cart, Checkout, Discounts, Shipping, Taxes
- [ ] Step 4.1-4.2: Migrations and Models
- [ ] Step 4.3: Cart Service
- [ ] Step 4.4: Discount Service
- [ ] Step 4.5: Shipping Calculator
- [ ] Step 4.6: Tax Calculator
- [ ] Step 4.7: Pricing Engine
- [ ] Step 4.8: Checkout State Machine
- [ ] Step 4.9: Storefront Cart/Checkout UI
- [ ] Tests written and passing

### Phase 5: Payments, Orders, Fulfillment
- [ ] Step 5.1-5.2: Migrations and Models
- [ ] Step 5.3: Payment Service (Mock PSP)
- [ ] Step 5.4: Order Service
- [ ] Step 5.5: Refund Service
- [ ] Step 5.6: Fulfillment Service
- [ ] Tests written and passing

### Phase 6: Customer Accounts
- [ ] Customer Auth
- [ ] Customer Account Pages
- [ ] Tests written and passing

### Phase 7: Admin Panel
- [ ] Admin Layout
- [ ] Dashboard
- [ ] Product Management
- [ ] Order Management
- [ ] Other Admin Sections
- [ ] Tests written and passing

### Phase 8-10: Search, Analytics, Apps/Webhooks
- [ ] Search (FTS5)
- [ ] Analytics
- [ ] Apps and Webhooks
- [ ] Tests written and passing

### Phase 11: Polish
- [ ] Accessibility
- [ ] Responsive
- [ ] Dark mode
- [ ] Error pages
- [ ] Seed data
- [ ] Tests written and passing

### Phase 12: Full Test Suite
- [ ] All unit tests passing
- [ ] All feature tests passing
- [ ] All browser tests passing
- [ ] Code style check passing
- [ ] Fresh migration + seeding verified


type: progress

hookEvent: PostToolUse

command: callback


type: user

role: user

content: File created successfully at: /Users/fabianwesner/Herd/shop/specs/progress.md

type: create

content:
# Implementation Progress

## Overview
- **Start:** 2026-02-13
- **Phases:** 12 total
- **Estimated Files:** ~320-380

## Phase Status

| Phase | Name | Status | Progress |
|-------|------|--------|----------|
| 1 | Foundation (Migrations, Models, Middleware, Auth) | In Progress | 0% |
| 2 | Catalog (Products, Variants, Inventory, Collections, Media) | Pending | 0% |
| 3 | Themes, Pages, Navigation, Storefront Layout | Pending | 0% |
| 4 | Cart, Checkout, Discounts, Shipping, Taxes | Pending | 0% |
| 5 | Payments, Orders, Fulfillment | Pending | 0% |
| 6 | Customer Accounts | Pending | 0% |
| 7 | Admin Panel | Pending | 0% |
| 8 | Search | Pending | 0% |
| 9 | Analytics | Pending | 0% |
| 10 | Apps and Webhooks | Pending | 0% |
| 11 | Polish | Pending | 0% |
| 12 | Full Test Suite | Pending | 0% |

## Detailed Log

### Phase 1: Foundation
- [ ] Step 1.1: Environment and Config
- [ ] Step 1.2: Core Migrations (Batch 1-2)
- [ ] Step 1.3: Core Models
- [ ] Step 1.4: Enums
- [ ] Step 1.5: Tenant Resolution Middleware
- [ ] Step 1.6: BelongsToStore Trait and Global Scope
- [ ] Step 1.7: Authentication
- [ ] Step 1.8: Authorization
- [ ] Tests written and passing

### Phase 2: Catalog
- [ ] Step 2.1: Migrations (Batch 3-5)
- [ ] Step 2.2: Models with Relationships
- [ ] Step 2.3: Product Service
- [ ] Step 2.4: Inventory Service
- [ ] Step 2.5: Media Upload
- [ ] Tests written and passing

### Phase 3: Themes and Storefront
- [ ] Step 3.1: Migrations (Batch 3)
- [ ] Step 3.2: Models
- [ ] Step 3.3: Storefront Blade Layout
- [ ] Step 3.4: Storefront Livewire Components
- [ ] Step 3.5: Navigation Service
- [ ] Tests written and passing

### Phase 4: Cart, Checkout, Discounts, Shipping, Taxes
- [ ] Step 4.1-4.2: Migrations and Models
- [ ] Step 4.3: Cart Service
- [ ] Step 4.4: Discount Service
- [ ] Step 4.5: Shipping Calculator
- [ ] Step 4.6: Tax Calculator
- [ ] Step 4.7: Pricing Engine
- [ ] Step 4.8: Checkout State Machine
- [ ] Step 4.9: Storefront Cart/Checkout UI
- [ ] Tests written and passing

### Phase 5: Payments, Orders, Fulfillment
- [ ] Step 5.1-5.2: Migrations and Models
- [ ] Step 5.3: Payment Service (Mock PSP)
- [ ] Step 5.4: Order Service
- [ ] Step 5.5: Refund Service
- [ ] Step 5.6: Fulfillment Service
- [ ] Tests written and passing

### Phase 6: Customer Accounts
- [ ] Customer Auth
- [ ] Customer Account Pages
- [ ] Tests written and passing

### Phase 7: Admin Panel
- [ ] Admin Layout
- [ ] Dashboard
- [ ] Product Management
- [ ] Order Management
- [ ] Other Admin Sections
- [ ] Tests written and passing

### Phase 8-10: Search, Analytics, Apps/Webhooks
- [ ] Search (FTS5)
- [ ] Analytics
- [ ] Apps and Webhooks
- [ ] Tests written and passing

### Phase 11: Polish
- [ ] Accessibility
- [ ] Responsive
- [ ] Dark mode
- [ ] Error pages
- [ ] Seed data
- [ ] Tests written and passing

### Phase 12: Full Test Suite
- [ ] All unit tests passing
- [ ] All feature tests passing
- [ ] All browser tests passing
- [ ] Code style check passing
- [ ] Fresh migration + seeding verified


text: Now let me create the team and start implementing Phase 1 (Foundation) - the critical path everything depends on.


teamName: wild-sleeping-shell

TeamCreate

description: Building the complete e-commerce shop system from specs


team_name: wild-sleeping-shell


TaskCreate

subject: Phase 1: Foundation - Config, Migrations, Models, Enums, Middleware, Auth

description: Implement all Phase 1 items: environment config, core migrations (organizations, stores, store_domains, users modifications, store_users, store_settings), core models with relationships/factories/seeders, enums (StoreStatus, StoreUserRole, StoreDomainType), ResolveStore middleware, BelongsToStore trait + StoreScope, admin auth (Login/Logout), customer auth (guard, provider, Login/Register), rate limiting, authorization policies, and all Phase 1 tests.


content: Task #1 created successfully: Phase 1: Foundation - Config, Migrations, Models, Enums, Middleware, Auth


subject: Phase 2: Catalog - Products, Variants, Inventory, Collections, Media

description: Implement catalog system: migrations for products, product_options, product_option_values, product_variants, variant_option_values, inventory_items, collections, collection_products, product_media. Models with relationships/factories. ProductService, VariantMatrixService, InventoryService, HandleGenerator, ProcessMediaUpload job. All catalog enums and tests.


content: Task #2 created successfully: Phase 2: Catalog - Products, Variants, Inventory, Collections, Media


subject: Phase 3: Themes, Pages, Navigation, Storefront Layout

description: Implement themes/storefront: migrations for themes, theme_files, theme_settings, pages, navigation_menus, navigation_items. Models with relationships. Storefront Blade layout, all storefront Livewire components (Home, Collections, Products, Cart, Search, Pages), NavigationService. Tests.


content: Task #3 created successfully: Phase 3: Themes, Pages, Navigation, Storefront Layout


subject: Phase 4: Cart, Checkout, Discounts, Shipping, Taxes

description: Implement shopping flow: migrations for carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts. Models. CartService, DiscountService, ShippingCalculator, TaxCalculator, PricingEngine, CheckoutService. Value objects (PricingResult, TaxLine). Cart/Checkout UI. All tests.


content: Task #4 created successfully: Phase 4: Cart, Checkout, Discounts, Shipping, Taxes


subject: Phase 5: Payments, Orders, Fulfillment

description: Implement order flow: migrations for customers, customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines. Models. MockPaymentProvider, OrderService, RefundService, FulfillmentService. Events (OrderCreated, OrderPaid, etc.). CancelUnpaidBankTransferOrders job. All tests.


content: Task #5 created successfully: Phase 5: Payments, Orders, Fulfillment

subject: Phase 5: Payments, Orders, Fulfillment


subject: Phase 6+7: Customer Accounts + Admin Panel

description: Implement customer account pages (Dashboard, Orders, Addresses) and full admin panel (Layout, Dashboard, Products, Orders, Collections, Customers, Discounts, Settings, Themes, Pages, Navigation, Analytics, Search, Apps, Developers). All Livewire components and tests.

activeForm: Implementing Customer Accounts and Admin Panel


content: Task #6 created successfully: Phase 6+7: Customer Accounts + Admin Panel


subject: Phase 8-10: Search, Analytics, Apps/Webhooks

description: Implement search (FTS5, SearchService, ProductObserver, search UI), analytics (events, daily aggregation, AnalyticsService), and apps/webhooks (WebhookService, DeliverWebhook job, circuit breaker). All migrations, models, and tests.

activeForm: Implementing Search, Analytics, Webhooks


content: Task #7 created successfully: Phase 8-10: Search, Analytics, Apps/Webhooks


subject: Phase 11-12: Polish + Full Test Suite + Review

description: Polish (accessibility, responsive, dark mode, error pages, seed data), run full test suite, Playwright E2E tests, PHPStan compliance, code style, fresh migration verification. Independent code review by fresh agent.


content: Task #8 created successfully: Phase 11-12: Polish + Full Test Suite + Review


content: Updated task #2 blockedBy


content: Updated task #3 blockedBy


content: Updated task #4 blockedBy


content: Updated task #5 blockedBy


content: Updated task #6 blockedBy


content: Updated task #7 blockedBy


content: Updated task #8 blockedBy


content: Updated task #1 owner, status

updatedFields: owner status

from: pending


text: Starting Phase 1: Foundation. Let me begin by reading current config files and setting up the environment.


content:
     1→<?php
     2→
     3→return [
     4→
     5→    /*
     6→    |--------------------------------------------------------------------------
     7→    | Authentication Defaults
     8→    |--------------------------------------------------------------------------
     9→    |
    10→    | This option defines the default authentication "guard" and password
    11→    | reset "broker" for your application. You may change these values
    12→    | as required, but they're a perfect start for most applications.
    13→    |
    14→    */
    15→
    16→    'defaults' => [
    17→        'guard' => env('AUTH_GUARD', 'web'),
    18→        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    19→    ],
    20→
    21→    /*
    22→    |--------------------------------------------------------------------------
    23→    | Authentication Guards
    24→    |--------------------------------------------------------------------------
    25→    |
    26→    | Next, you may define every authentication guard for your application.
    27→    | Of course, a great default configuration has been defined for you
    28→    | which utilizes session storage plus the Eloquent user provider.
    29→    |
    30→    | All authentication guards have a user provider, which defines how the
    31→    | users are actually retrieved out of your database or other storage
    32→    | system used by the application. Typically, Eloquent is utilized.
    33→    |
    34→    | Supported: "session"
    35→    |
    36→    */
    37→
    38→    'guards' => [
    39→        'web' => [
    40→            'driver' => 'session',
    41→            'provider' => 'users',
    42→        ],
    43→    ],
    44→
    45→    /*
    46→    |--------------------------------------------------------------------------
    47→    | User Providers
    48→    |--------------------------------------------------------------------------
    49→    |
    50→    | All authentication guards have a user provider, which defines how the
    51→    | users are actually retrieved out of your database or other storage
    52→    | system used by the application. Typically, Eloquent is utilized.
    53→    |
    54→    | If you have multiple user tables or models you may configure multiple
    55→    | providers to represent the model / table. These providers may then
    56→    | be assigned to any extra authentication guards you have defined.
    57→    |
    58→    | Supported: "database", "eloquent"
    59→    |
    60→    */
    61→
    62→    'providers' => [
    63→        'users' => [
    64→            'driver' => 'eloquent',
    65→            'model' => env('AUTH_MODEL', App\Models\User::class),
    66→        ],
    67→
    68→        // 'users' => [
    69→        //     'driver' => 'database',
    70→        //     'table' => 'users',
    71→        // ],
    72→    ],
    73→
    74→    /*
    75→    |--------------------------------------------------------------------------
    76→    | Resetting Passwords
    77→    |--------------------------------------------------------------------------
    78→    |
    79→    | These configuration options specify the behavior of Laravel's password
    80→    | reset functionality, including the table utilized for token storage
    81→    | and the user provider that is invoked to actually retrieve users.
    82→    |
    83→    | The expiry time is the number of minutes that each reset token will be
    84→    | considered valid. This security feature keeps tokens short-lived so
    85→    | they have less time to be guessed. You may change this as needed.
    86→    |
    87→    | The throttle setting is the number of seconds a user must wait before
    88→    | generating more password reset tokens. This prevents the user from
    89→    | quickly generating a very large amount of password reset tokens.
    90→    |
    91→    */
    92→
    93→    'passwords' => [
    94→        'users' => [
    95→            'provider' => 'users',
    96→            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
    97→            'expire' => 60,
    98→            'throttle' => 60,
    99→        ],
   100→    ],
   101→
   102→    /*
   103→    |--------------------------------------------------------------------------
   104→    | Password Confirmation Timeout
   105→    |--------------------------------------------------------------------------
   106→    |
   107→    | Here you may define the number of seconds before a password confirmation
   108→    | window expires and users are asked to re-enter their password via the
   109→    | confirmation screen. By default, the timeout lasts for three hours.
   110→    |
   111→    */
   112→
   113→    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
   114→
   115→];
   116→

content:
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option defines the default authentication "guard" and password
    | reset "broker" for your application. You may change these values
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | which utilizes session storage plus the Eloquent user provider.
    |
    | All authentication guards have a user provider, which defines how the
    | users are actually retrieved out of your database or other storage
    | system used by the application. Typically, Eloquent is utilized.
    |
    | Supported: "session"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication guards have a user provider, which defines how the
    | users are actually retrieved out of your database or other storage
    | system used by the application. Typically, Eloquent is utilized.
    |
    | If you have multiple user tables or models you may configure multiple
    | providers to represent the model / table. These providers may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\User::class),
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | These configuration options specify the behavior of Laravel's password
    | reset functionality, including the table utilized for token storage
    | and the user provider that is invoked to actually retrieve users.
    |
    | The expiry time is the number of minutes that each reset token will be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    | The throttle setting is the number of seconds a user must wait before
    | generating more password reset tokens. This prevents the user from
    | quickly generating a very large amount of password reset tokens.
    |
    */

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Password Confirmation Timeout
    |--------------------------------------------------------------------------
    |
    | Here you may define the number of seconds before a password confirmation
    | window expires and users are asked to re-enter their password via the
    | confirmation screen. By default, the timeout lasts for three hours.
    |
    */

    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),

];


content:
     1→<?php
     2→
     3→use Illuminate\Support\Str;
     4→
     5→return [
     6→
     7→    /*
     8→    |--------------------------------------------------------------------------
     9→    | Default Database Connection Name
    10→    |--------------------------------------------------------------------------
    11→    |
    12→    | Here you may specify which of the database connections below you wish
    13→    | to use as your default connection for database operations. This is
    14→    | the connection which will be utilized unless another connection
    15→    | is explicitly specified when you execute a query / statement.
    16→    |
    17→    */
    18→
    19→    'default' => env('DB_CONNECTION', 'sqlite'),
    20→
    21→    /*
    22→    |--------------------------------------------------------------------------
    23→    | Database Connections
    24→    |--------------------------------------------------------------------------
    25→    |
    26→    | Below are all of the database connections defined for your application.
    27→    | An example configuration is provided for each database system which
    28→    | is supported by Laravel. You're free to add / remove connections.
    29→    |
    30→    */
    31→
    32→    'connections' => [
    33→
    34→        'sqlite' => [
    35→            'driver' => 'sqlite',
    36→            'url' => env('DB_URL'),
    37→            'database' => env('DB_DATABASE', database_path('database.sqlite')),
    38→            'prefix' => '',
    39→            'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
    40→            'busy_timeout' => null,
    41→            'journal_mode' => null,
    42→            'synchronous' => null,
    43→            'transaction_mode' => 'DEFERRED',
    44→        ],
    45→
    46→        'mysql' => [
    47→            'driver' => 'mysql',
    48→            'url' => env('DB_URL'),
    49→            'host' => env('DB_HOST', '127.0.0.1'),
    50→            'port' => env('DB_PORT', '3306'),
    51→            'database' => env('DB_DATABASE', 'laravel'),
    52→            'username' => env('DB_USERNAME', 'root'),
    53→            'password' => env('DB_PASSWORD', ''),
    54→            'unix_socket' => env('DB_SOCKET', ''),
    55→            'charset' => env('DB_CHARSET', 'utf8mb4'),
    56→            'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
    57→            'prefix' => '',
    58→            'prefix_indexes' => true,
    59→            'strict' => true,
    60→            'engine' => null,
    61→            'options' => extension_loaded('pdo_mysql') ? array_filter([
    62→                (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
    63→            ]) : [],
    64→        ],
    65→
    66→        'mariadb' => [
    67→            'driver' => 'mariadb',
    68→            'url' => env('DB_URL'),
    69→            'host' => env('DB_HOST', '127.0.0.1'),
    70→            'port' => env('DB_PORT', '3306'),
    71→            'database' => env('DB_DATABASE', 'laravel'),
    72→            'username' => env('DB_USERNAME', 'root'),
    73→            'password' => env('DB_PASSWORD', ''),
    74→            'unix_socket' => env('DB_SOCKET', ''),
    75→            'charset' => env('DB_CHARSET', 'utf8mb4'),
    76→            'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
    77→            'prefix' => '',
    78→            'prefix_indexes' => true,
    79→            'strict' => true,
    80→            'engine' => null,
    81→            'options' => extension_loaded('pdo_mysql') ? array_filter([
    82→                (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
    83→            ]) : [],
    84→        ],
    85→
    86→        'pgsql' => [
    87→            'driver' => 'pgsql',
    88→            'url' => env('DB_URL'),
    89→            'host' => env('DB_HOST', '127.0.0.1'),
    90→            'port' => env('DB_PORT', '5432'),
    91→            'database' => env('DB_DATABASE', 'laravel'),
    92→            'username' => env('DB_USERNAME', 'root'),
    93→            'password' => env('DB_PASSWORD', ''),
    94→            'charset' => env('DB_CHARSET', 'utf8'),
    95→            'prefix' => '',
    96→            'prefix_indexes' => true,
    97→            'search_path' => 'public',
    98→            'sslmode' => env('DB_SSLMODE', 'prefer'),
    99→        ],
   100→
   101→        'sqlsrv' => [
   102→            'driver' => 'sqlsrv',
   103→            'url' => env('DB_URL'),
   104→            'host' => env('DB_HOST', 'localhost'),
   105→            'port' => env('DB_PORT', '1433'),
   106→            'database' => env('DB_DATABASE', 'laravel'),
   107→            'username' => env('DB_USERNAME', 'root'),
   108→            'password' => env('DB_PASSWORD', ''),
   109→            'charset' => env('DB_CHARSET', 'utf8'),
   110→            'prefix' => '',
   111→            'prefix_indexes' => true,
   112→            // 'encrypt' => env('DB_ENCRYPT', 'yes'),
   113→            // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
   114→        ],
   115→
   116→    ],
   117→
   118→    /*
   119→    |--------------------------------------------------------------------------
   120→    | Migration Repository Table
   121→    |--------------------------------------------------------------------------
   122→    |
   123→    | This table keeps track of all the migrations that have already run for
   124→    | your application. Using this information, we can determine which of
   125→    | the migrations on disk haven't actually been run on the database.
   126→    |
   127→    */
   128→
   129→    'migrations' => [
   130→        'table' => 'migrations',
   131→        'update_date_on_publish' => true,
   132→    ],
   133→
   134→    /*
   135→    |--------------------------------------------------------------------------
   136→    | Redis Databases
   137→    |--------------------------------------------------------------------------
   138→    |
   139→    | Redis is an open source, fast, and advanced key-value store that also
   140→    | provides a richer body of commands than a typical key-value system
   141→    | such as Memcached. You may define your connection settings here.
   142→    |
   143→    */
   144→
   145→    'redis' => [
   146→
   147→        'client' => env('REDIS_CLIENT', 'phpredis'),
   148→
   149→        'options' => [
   150→            'cluster' => env('REDIS_CLUSTER', 'redis'),
   151→            'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
   152→            'persistent' => env('REDIS_PERSISTENT', false),
   153→        ],
   154→
   155→        'default' => [
   156→            'url' => env('REDIS_URL'),
   157→            'host' => env('REDIS_HOST', '127.0.0.1'),
   158→            'username' => env('REDIS_USERNAME'),
   159→            'password' => env('REDIS_PASSWORD'),
   160→            'port' => env('REDIS_PORT', '6379'),
   161→            'database' => env('REDIS_DB', '0'),
   162→            'max_retries' => env('REDIS_MAX_RETRIES', 3),
   163→            'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
   164→            'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
   165→            'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
   166→        ],
   167→
   168→        'cache' => [
   169→            'url' => env('REDIS_URL'),
   170→            'host' => env('REDIS_HOST', '127.0.0.1'),
   171→            'username' => env('REDIS_USERNAME'),
   172→            'password' => env('REDIS_PASSWORD'),
   173→            'port' => env('REDIS_PORT', '6379'),
   174→            'database' => env('REDIS_CACHE_DB', '1'),
   175→            'max_retries' => env('REDIS_MAX_RETRIES', 3),
   176→            'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
   177→            'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
   178→            'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
   179→        ],
   180→
   181→    ],
   182→
   183→];
   184→

content:
<?php

use Illuminate\Support\Str;

return [

    /*
    |--------------------------------------------------------------------------
    | Default Database Connection Name
    |--------------------------------------------------------------------------
    |
    | Here you may specify which of the database connections below you wish
    | to use as your default connection for database operations. This is
    | the connection which will be utilized unless another connection
    | is explicitly specified when you execute a query / statement.
    |
    */

    'default' => env('DB_CONNECTION', 'sqlite'),

    /*
    |--------------------------------------------------------------------------
    | Database Connections
    |--------------------------------------------------------------------------
    |
    | Below are all of the database connections defined for your application.
    | An example configuration is provided for each database system which
    | is supported by Laravel. You're free to add / remove connections.
    |
    */

    'connections' => [

        'sqlite' => [
            'driver' => 'sqlite',
            'url' => env('DB_URL'),
            'database' => env('DB_DATABASE', database_path('database.sqlite')),
            'prefix' => '',
            'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
            'busy_timeout' => null,
            'journal_mode' => null,
            'synchronous' => null,
            'transaction_mode' => 'DEFERRED',
        ],

        'mysql' => [
            'driver' => 'mysql',
            'url' => env('DB_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'laravel'),
            'username' => env('DB_USERNAME', 'root'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => env('DB_CHARSET', 'utf8mb4'),
            'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

        'mariadb' => [
            'driver' => 'mariadb',
            'url' => env('DB_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'laravel'),
            'username' => env('DB_USERNAME', 'root'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => env('DB_CHARSET', 'utf8mb4'),
            'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

        'pgsql' => [
            'driver' => 'pgsql',
            'url' => env('DB_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '5432'),
            'database' => env('DB_DATABASE', 'laravel'),
            'username' => env('DB_USERNAME', 'root'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => env('DB_CHARSET', 'utf8'),
            'prefix' => '',
            'prefix_indexes' => true,
            'search_path' => 'public',
            'sslmode' => env('DB_SSLMODE', 'prefer'),
        ],

        'sqlsrv' => [
            'driver' => 'sqlsrv',
            'url' => env('DB_URL'),
            'host' => env('DB_HOST', 'localhost'),
            'port' => env('DB_PORT', '1433'),
            'database' => env('DB_DATABASE', 'laravel'),
            'username' => env('DB_USERNAME', 'root'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => env('DB_CHARSET', 'utf8'),
            'prefix' => '',
            'prefix_indexes' => true,
            // 'encrypt' => env('DB_ENCRYPT', 'yes'),
            // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
        ],

    ],

    /*
    |--------------------------------------------------------------------------
    | Migration Repository Table
    |--------------------------------------------------------------------------
    |
    | This table keeps track of all the migrations that have already run for
    | your application. Using this information, we can determine which of
    | the migrations on disk haven't actually been run on the database.
    |
    */

    'migrations' => [
        'table' => 'migrations',
        'update_date_on_publish' => true,
    ],

    /*
    |--------------------------------------------------------------------------
    | Redis Databases
    |--------------------------------------------------------------------------
    |
    | Redis is an open source, fast, and advanced key-value store that also
    | provides a richer body of commands than a typical key-value system
    | such as Memcached. You may define your connection settings here.
    |
    */

    'redis' => [

        'client' => env('REDIS_CLIENT', 'phpredis'),

        'options' => [
            'cluster' => env('REDIS_CLUSTER', 'redis'),
            'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
            'persistent' => env('REDIS_PERSISTENT', false),
        ],

        'default' => [
            'url' => env('REDIS_URL'),
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'username' => env('REDIS_USERNAME'),
            'password' => env('REDIS_PASSWORD'),
            'port' => env('REDIS_PORT', '6379'),
            'database' => env('REDIS_DB', '0'),
            'max_retries' => env('REDIS_MAX_RETRIES', 3),
            'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
            'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
            'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
        ],

        'cache' => [
            'url' => env('REDIS_URL'),
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'username' => env('REDIS_USERNAME'),
            'password' => env('REDIS_PASSWORD'),
            'port' => env('REDIS_PORT', '6379'),
            'database' => env('REDIS_CACHE_DB', '1'),
            'max_retries' => env('REDIS_MAX_RETRIES', 3),
            'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
            'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
            'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
        ],

    ],

];


content:
     1→<?php
     2→
     3→use Illuminate\Foundation\Application;
     4→use Illuminate\Foundation\Configuration\Exceptions;
     5→use Illuminate\Foundation\Configuration\Middleware;
     6→
     7→return Application::configure(basePath: dirname(__DIR__))
     8→    ->withRouting(
     9→        web: __DIR__.'/../routes/web.php',
    10→        commands: __DIR__.'/../routes/console.php',
    11→        health: '/up',
    12→    )
    13→    ->withMiddleware(function (Middleware $middleware): void {
    14→        //
    15→    })
    16→    ->withExceptions(function (Exceptions $exceptions): void {
    17→        //
    18→    })->create();
    19→

content:
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();


content:
     1→APP_NAME=Laravel
     2→APP_ENV=local
     3→APP_KEY=base64:nW7zTogbucXWMRU2+3CxeYXYni4FTNdm8nIUeG7tOzQ=
     4→APP_DEBUG=true
     5→APP_URL=http://shop.test
     6→
     7→APP_LOCALE=en
     8→APP_FALLBACK_LOCALE=en
     9→APP_FAKER_LOCALE=en_US
    10→
    11→APP_MAINTENANCE_DRIVER=file
    12→# APP_MAINTENANCE_STORE=database
    13→
    14→# PHP_CLI_SERVER_WORKERS=4
    15→
    16→BCRYPT_ROUNDS=12
    17→
    18→LOG_CHANNEL=stack
    19→LOG_STACK=single
    20→LOG_DEPRECATIONS_CHANNEL=null
    21→LOG_LEVEL=debug
    22→
    23→DB_CONNECTION=sqlite
    24→# DB_HOST=127.0.0.1
    25→# DB_PORT=3306
    26→# DB_DATABASE=laravel
    27→# DB_USERNAME=root
    28→# DB_PASSWORD=
    29→
    30→SESSION_DRIVER=file
    31→SESSION_LIFETIME=120
    32→SESSION_ENCRYPT=false
    33→SESSION_PATH=/
    34→SESSION_DOMAIN=null
    35→
    36→BROADCAST_CONNECTION=log
    37→FILESYSTEM_DISK=local
    38→QUEUE_CONNECTION=sync
    39→
    40→CACHE_STORE=file
    41→# CACHE_PREFIX=
    42→
    43→MEMCACHED_HOST=127.0.0.1
    44→
    45→REDIS_CLIENT=phpredis
    46→REDIS_HOST=127.0.0.1
    47→REDIS_PASSWORD=null
    48→REDIS_PORT=6379
    49→
    50→MAIL_MAILER=log
    51→MAIL_SCHEME=null
    52→MAIL_HOST=127.0.0.1
    53→MAIL_PORT=2525
    54→MAIL_USERNAME=null
    55→MAIL_PASSWORD=null
    56→MAIL_FROM_ADDRESS="hello@example.com"
    57→MAIL_FROM_NAME="${APP_NAME}"
    58→
    59→AWS_ACCESS_KEY_ID=
    60→AWS_SECRET_ACCESS_KEY=
    61→AWS_DEFAULT_REGION=us-east-1
    62→AWS_BUCKET=
    63→AWS_USE_PATH_STYLE_ENDPOINT=false
    64→
    65→VITE_APP_NAME="${APP_NAME}"
    66→FORGE_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZTJlODczZTViNzQxOWIwMjk0MDA2MjI4Y2RlNzliMmM5ZDNhNjEzYWJkM2Q3NmMxNWYzZTI3OGE1NTRiZDgwNjk3ZDQ3ZWU1YWM1MmViNmMiLCJpYXQiOjE3NjkxODU2NTUuMDc1NjE0LCJuYmYiOjE3NjkxODU2NTUuMDc1NjE3LCJleHAiOjIwODQ3MTg0NTUuMDcyNzExLCJzdWIiOiIyNDE3NDgiLCJzY29wZXMiOlsic2VydmVyOnZpZXciLCJzZXJ2ZXI6Y3JlYXRlIiwic2VydmVyOmRlbGV0ZSIsInNlcnZlcjphcmNoaXZlIiwic2VydmVyOnRyYW5zZmVyIiwic2VydmVyOm1hbmFnZS1tZXRhIiwic2VydmVyOm1hbmFnZS1wYWNrYWdlcyIsInNlcnZlcjptYW5hZ2UtcGhwIiwic2VydmVyOm1hbmFnZS1sb2dzIiwic2VydmVyOm1hbmFnZS1uZXR3b3JrIiwic2VydmVyOm1hbmFnZS1uZ2lueC10ZW1wbGF0ZXMiLCJzZXJ2ZXI6bWFuYWdlLXNlcnZpY2VzIiwic2VydmVyOm1hbmFnZS1wYXNzd29yZCIsInNlcnZlcjpjcmVhdGUta2V5cyIsInNlcnZlcjpkZWxldGUta2V5cyIsInNlcnZlcjpjcmVhdGUtbW9uaXRvcnMiLCJzZXJ2ZXI6ZGVsZXRlLW1vbml0b3JzIiwic2VydmVyOmNyZWF0ZS1kYXRhYmFzZXMiLCJzZXJ2ZXI6ZGVsZXRlLWRhdGFiYXNlcyIsInNlcnZlcjpjcmVhdGUtYmFja3VwcyIsInNlcnZlcjpkZWxldGUtYmFja3VwcyIsInNlcnZlcjpjcmVhdGUtZGFlbW9ucyIsInNlcnZlcjpkZWxldGUtZGFlbW9ucyIsInNlcnZlcjpjcmVhdGUtc2NoZWR1bGVycyIsInNlcnZlcjpkZWxldGUtc2NoZWR1bGVycyIsInNlcnZlcjp3ZWItdGVybWluYWwiLCJzaXRlOmNyZWF0ZSIsInNpdGU6ZGVsZXRlIiwic2l0ZTptZXRhIiwic2l0ZTptYW5hZ2UtY29tbWFuZHMiLCJzaXRlOm1hbmFnZS1kZXBsb3lzIiwic2l0ZTptYW5hZ2UtbmdpbngiLCJzaXRlOm1hbmFnZS1wcm9qZWN0Iiwic2l0ZTptYW5hZ2UtZW52aXJvbm1lbnQiLCJzaXRlOm1hbmFnZS1ub3RpZmljYXRpb25zIiwic2l0ZTptYW5hZ2UtcXVldWVzIiwic2l0ZTptYW5hZ2UtcmVkaXJlY3RzIiwic2l0ZTptYW5hZ2Utc2VjdXJpdHkiLCJzaXRlOm1hbmFnZS1zc2wiLCJzaXRlOm1hbmFnZS1pbnRlZ3JhdGlvbnMiLCJzaXRlOm1hbmFnZS1oZWFydGJlYXRzIiwic3RvcmFnZTptYW5hZ2UiXX0.jnUjxC3lIQJLCTJ1AbgVDQ-JhExT7gQASOPMdtCWAW8HQbFEz4uvOERS1xGvwCFiUe0dJPt1O1G_Npi9SdjJaKiFJE-MrUoyKTkEOO41Ay6hftxx73p3QlyGfXZkx0IREaX8bgn6JwPFy5CLiOXhTzvBLj3JL0QVR2gUxPtGBgatO5KLQUyA8UCpA_mikRP2itUTshf0t2xF-fwKK6PK2iOIsG4TCaMpt3qPE_CJWataxQKOKFFa1qsek_LLwTwePfnEboys27wGht0G4-sMRTs_QzsGwwhrhpRm3f_16c7uH_mCNDhMLccJrl_N82yaJVPkgcpIag8hhx2sOtjNA6bl8ghyKA4-tdM1CwmFXeK-Lexd1pjyza9J8lG3cezK5wTlcNfrkTkhUNdE51h0IqCAVSnGxlfJIlzBJX9xOZl_NNdl4RItdoxUccfbbsZ7V-OjWoFEnZG2PTeupgnOGm10eEqd4nZkXD3TX_ecqX54pgmEakir3p_as7KVN1EygJlGGrv9FsZrdnyseah127Zz1EG1Mrh8LMVZv9SxQtvsE4Qw77N_rNmNznSgBVRgp7CBkglkolWijB8cGN2ktvVOGSeOJp77Oe1wfnH5E1VDochsDUW2R7ouFx6bJwS6_SV__wLOSjLYRmqHu9rscTKFPAo3A-ZsgJQGyfK7TIw"
    67→
    68→DB_DATABASE=database/shop.sqlite
    69→

content:
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:nW7zTogbucXWMRU2+3CxeYXYni4FTNdm8nIUeG7tOzQ=
APP_DEBUG=true
APP_URL=http://shop.test

APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US

APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database

# PHP_CLI_SERVER_WORKERS=4

BCRYPT_ROUNDS=12

LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync

CACHE_STORE=file
# CACHE_PREFIX=

MEMCACHED_HOST=127.0.0.1

REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

VITE_APP_NAME="${APP_NAME}"
FORGE_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZTJlODczZTViNzQxOWIwMjk0MDA2MjI4Y2RlNzliMmM5ZDNhNjEzYWJkM2Q3NmMxNWYzZTI3OGE1NTRiZDgwNjk3ZDQ3ZWU1YWM1MmViNmMiLCJpYXQiOjE3NjkxODU2NTUuMDc1NjE0LCJuYmYiOjE3NjkxODU2NTUuMDc1NjE3LCJleHAiOjIwODQ3MTg0NTUuMDcyNzExLCJzdWIiOiIyNDE3NDgiLCJzY29wZXMiOlsic2VydmVyOnZpZXciLCJzZXJ2ZXI6Y3JlYXRlIiwic2VydmVyOmRlbGV0ZSIsInNlcnZlcjphcmNoaXZlIiwic2VydmVyOnRyYW5zZmVyIiwic2VydmVyOm1hbmFnZS1tZXRhIiwic2VydmVyOm1hbmFnZS1wYWNrYWdlcyIsInNlcnZlcjptYW5hZ2UtcGhwIiwic2VydmVyOm1hbmFnZS1sb2dzIiwic2VydmVyOm1hbmFnZS1uZXR3b3JrIiwic2VydmVyOm1hbmFnZS1uZ2lueC10ZW1wbGF0ZXMiLCJzZXJ2ZXI6bWFuYWdlLXNlcnZpY2VzIiwic2VydmVyOm1hbmFnZS1wYXNzd29yZCIsInNlcnZlcjpjcmVhdGUta2V5cyIsInNlcnZlcjpkZWxldGUta2V5cyIsInNlcnZlcjpjcmVhdGUtbW9uaXRvcnMiLCJzZXJ2ZXI6ZGVsZXRlLW1vbml0b3JzIiwic2VydmVyOmNyZWF0ZS1kYXRhYmFzZXMiLCJzZXJ2ZXI6ZGVsZXRlLWRhdGFiYXNlcyIsInNlcnZlcjpjcmVhdGUtYmFja3VwcyIsInNlcnZlcjpkZWxldGUtYmFja3VwcyIsInNlcnZlcjpjcmVhdGUtZGFlbW9ucyIsInNlcnZlcjpkZWxldGUtZGFlbW9ucyIsInNlcnZlcjpjcmVhdGUtc2NoZWR1bGVycyIsInNlcnZlcjpkZWxldGUtc2NoZWR1bGVycyIsInNlcnZlcjp3ZWItdGVybWluYWwiLCJzaXRlOmNyZWF0ZSIsInNpdGU6ZGVsZXRlIiwic2l0ZTptZXRhIiwic2l0ZTptYW5hZ2UtY29tbWFuZHMiLCJzaXRlOm1hbmFnZS1kZXBsb3lzIiwic2l0ZTptYW5hZ2UtbmdpbngiLCJzaXRlOm1hbmFnZS1wcm9qZWN0Iiwic2l0ZTptYW5hZ2UtZW52aXJvbm1lbnQiLCJzaXRlOm1hbmFnZS1ub3RpZmljYXRpb25zIiwic2l0ZTptYW5hZ2UtcXVldWVzIiwic2l0ZTptYW5hZ2UtcmVkaXJlY3RzIiwic2l0ZTptYW5hZ2Utc2VjdXJpdHkiLCJzaXRlOm1hbmFnZS1zc2wiLCJzaXRlOm1hbmFnZS1pbnRlZ3JhdGlvbnMiLCJzaXRlOm1hbmFnZS1oZWFydGJlYXRzIiwic3RvcmFnZTptYW5hZ2UiXX0.jnUjxC3lIQJLCTJ1AbgVDQ-JhExT7gQASOPMdtCWAW8HQbFEz4uvOERS1xGvwCFiUe0dJPt1O1G_Npi9SdjJaKiFJE-MrUoyKTkEOO41Ay6hftxx73p3QlyGfXZkx0IREaX8bgn6JwPFy5CLiOXhTzvBLj3JL0QVR2gUxPtGBgatO5KLQUyA8UCpA_mikRP2itUTshf0t2xF-fwKK6PK2iOIsG4TCaMpt3qPE_CJWataxQKOKFFa1qsek_LLwTwePfnEboys27wGht0G4-sMRTs_QzsGwwhrhpRm3f_16c7uH_mCNDhMLccJrl_N82yaJVPkgcpIag8hhx2sOtjNA6bl8ghyKA4-tdM1CwmFXeK-Lexd1pjyza9J8lG3cezK5wTlcNfrkTkhUNdE51h0IqCAVSnGxlfJIlzBJX9xOZl_NNdl4RItdoxUccfbbsZ7V-OjWoFEnZG2PTeupgnOGm10eEqd4nZkXD3TX_ecqX54pgmEakir3p_as7KVN1EygJlGGrv9FsZrdnyseah127Zz1EG1Mrh8LMVZv9SxQtvsE4Qw77N_rNmNznSgBVRgp7CBkglkolWijB8cGN2ktvVOGSeOJp77Oe1wfnH5E1VDochsDUW2R7ouFx6bJwS6_SV__wLOSjLYRmqHu9rscTKFPAo3A-ZsgJQGyfK7TIw"

DB_DATABASE=database/shop.sqlite