# ECStores — Platform Design & Requirements Document

**Document Version:** 1.0  
**Prepared By:** EastCoast WebCraft (Internal)  
**Date:** May 18, 2026  
**Status:** Draft — Pending Client Sign-Off  

---

## Table of Contents

1. [Platform Overview](#1-platform-overview)
2. [Goals & Success Criteria](#2-goals--success-criteria)
3. [Audience Definitions](#3-audience-definitions)
4. [System Architecture](#4-system-architecture)
5. [Data Models](#5-data-models)
6. [Storefront — Customer-Facing Pages](#6-storefront--customer-facing-pages)
7. [Checkout Flow](#7-checkout-flow)
8. [Customer Account Pages](#8-customer-account-pages)
9. [Authentication Pages](#9-authentication-pages)
10. [Merchant Admin Dashboard](#10-merchant-admin-dashboard)
11. [Super Admin Dashboard](#11-super-admin-dashboard)
12. [Global Storefront Design System](#12-global-storefront-design-system)
13. [Branding Studio](#13-branding-studio)
14. [Multi-Tenancy & Provisioning](#14-multi-tenancy--provisioning)
15. [API Endpoints](#15-api-endpoints)
16. [Integrations & External Services](#16-integrations--external-services)
17. [Session & State Management](#17-session--state-management)
18. [Security Requirements](#18-security-requirements)
19. [Validation Rules](#19-validation-rules)
20. [Error Handling & User Feedback](#20-error-handling--user-feedback)
21. [Email Notifications](#21-email-notifications)
22. [Future Phases (Phase 4)](#22-future-phases-phase-4)
23. [Out of Scope](#23-out-of-scope)
24. [Sign-Off](#24-sign-off)

---

## 1. Platform Overview

### 1.1 Product Name

**ECStores**  
**Store URL Pattern:** `yourbrand.ecstores.ca`  
**Platform Operator:** EastCoast WebCraft  
**Platform URL (Central Domain):** ecstores.ca  
**Support Contact:** info@eastcoastwebcraft.ca  

### 1.2 Product Description

ECStores is a fully managed, multi-tenant Software-as-a-Service (SaaS) e-commerce platform operated by EastCoast WebCraft. Each merchant (tenant) receives their own isolated online store hosted on a branded subdomain (e.g., `acmebakery.ecstores.ca`). The platform handles all infrastructure, security, and updates; merchants focus exclusively on managing their products, orders, and customers through a clean admin dashboard.

Payments flow directly from customers to the merchant via **Stripe Connect** — EastCoast WebCraft does not hold or touch merchant funds.

### 1.3 Platform Positioning

> "Your Own Online Store. Up and Running Today."
> Zero technical knowledge required. 100% Canadian-hosted and supported.

### 1.4 Key Platform Differentiators

| Differentiator | Description |
|----------------|-------------|
| **Subdomain per merchant** | Each store lives at a unique branded subdomain |
| **Fully managed** | EastCoast WebCraft handles hosting, security, updates |
| **Direct Stripe payouts** | Merchants connect their own Stripe account; funds go directly to them (0% platform take) |
| **Branding Studio** | Per-store color palette, fonts, button styles, logo, banner, favicon |
| **No technical knowledge required** | Merchant admin is designed for non-technical users |
| **100% Canadian-hosted** | All data stored on Canadian infrastructure |
| **Multi-tenant isolation** | Each merchant's data is fully isolated in its own database |

### 1.5 Subscription Plans

| Plan | Price | Product Limit | Key Additions |
|------|-------|--------------|---------------|
| **Starter** | $49/mo | Up to 50 products | Unlimited orders, Stripe payments, mobile storefront, branding studio |
| **Growth** *(Most Popular)* | $99/mo | Up to 250 products | + Wishlist & customer accounts, priority support |
| **Pro** | $199/mo | Unlimited products | + Supplier management, dedicated support |

---

## 2. Goals & Success Criteria

### 2.1 Platform Goals

1. **Merchant Acquisition** — Provision new merchant stores quickly and reliably from the ECW agency site.
2. **Merchant Retention** — Provide a complete, easy-to-use admin panel so merchants stay on the platform.
3. **Customer Conversion** — Deliver a clean, mobile-first storefront that converts shoppers into buyers.
4. **Operational Automation** — Automate tenant provisioning, billing, and suspension with minimal manual intervention.
5. **Revenue Generation** — Collect monthly subscription fees from all active tenants.

### 2.2 Success Metrics

- Time to provision a new store from API call to live (target: < 60 seconds)
- Monthly Recurring Revenue (MRR) across all active tenants
- Monthly active merchants (dashboard logins)
- Storefront checkout completion rate
- Page load time ≤ 2 seconds on storefront (LCP)
- Zero cross-tenant data leakage

---

## 3. Audience Definitions

### 3.1 Platform Operator (Super Admin)

EastCoast WebCraft staff. Has access to the central-domain Super Admin dashboard. Manages all tenants, creates and edits subscription plans, and can suspend or reactivate any store.

### 3.2 Merchants (Tenant Admins)

Small business owners, nonprofits, and personal brand operators who have subscribed to ECStores. They access the per-tenant Filament admin panel to manage their store's products, orders, settings, and branding. They are expected to have **no technical background**.

### 3.3 Customers (Storefront Shoppers)

The public end-users who visit a merchant's storefront subdomain, browse products, and complete purchases. They may shop as guests or create accounts. They interact exclusively with the tenant storefront — they never see the admin panel or any ECStores branding unless the merchant has not customized it.

---

## 4. System Architecture

### 4.1 Technology Stack

| Layer | Technology | Version |
|-------|-----------|---------|
| Server-side language | PHP | 8.3+ |
| Application framework | Laravel | 13.7 |
| Admin panel framework | Filament | 5.6 |
| Real-time UI components | Livewire | 4.3 |
| Frontend JavaScript | Alpine.js | (bundled with Livewire) |
| CSS framework | Tailwind CSS | CDN (storefront) |
| Multi-tenancy | Stancl Tenancy | — |
| Payment processing | Stripe (Laravel Cashier) | Cashier 16.5 |
| Media management | Spatie MediaLibrary | 11.22 |
| PDF generation | *(future — not in current scope)* | — |

### 4.2 Multi-Tenancy Model

- **Tenancy driver:** Subdomain-based (`InitializeTenancyBySubdomain` middleware)
- **Tenant identification:** Every request on a subdomain URL (e.g., `acme.ecstores.ca`) resolves the tenant from the subdomain prefix
- **Database isolation:** Each tenant has its own isolated database; no cross-tenant queries are possible
- **Central domain:** The root domain (`ecstores.ca`) and API routes exist outside tenancy; the Super Admin panel is also central-domain-only
- **Tenant middleware stack:**
  - `InitializeTenancyBySubdomain` — resolves tenant from subdomain
  - `CheckTenantNotSuspended` — redirects to suspended page if `suspended_at` is set
  - `CheckTenantSubscriptionActive` — redirects to subscription-required page if no active plan or trial

### 4.3 Route Groups

| Route File | Domain | Purpose |
|-----------|--------|---------|
| `routes/web.php` | Central domain | Super Admin panel, API proxy routes |
| `routes/tenant.php` | `{tenant}.ecstores.ca` | Storefront, merchant auth, merchant admin, customer auth |
| `routes/api.php` | Central domain | HMAC-protected provisioning and management API |

### 4.4 Admin Panel Architecture

| Panel | Framework | Path | Access |
|-------|-----------|------|--------|
| Merchant Admin | Filament (`AdminPanelProvider`) | `/admin` (on subdomain) | Admin role only (per tenant) |
| Super Admin | Filament (`SuperAdminPanelProvider`) | `/super-admin` (central domain) | SuperAdmin role only |

---

## 5. Data Models

### 5.1 Product

The central catalog entity for a merchant's store.

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `name` | string (255) | Product display name |
| `slug` | string (255) | URL-friendly unique identifier |
| `description` | text, nullable | Full product description |
| `sku` | string (100), nullable | Stock Keeping Unit |
| `price` | decimal (10,2) | Base retail price |
| `sale_price` | decimal (10,2), nullable | Sale/discounted price (must be < price) |
| `weight` | decimal (8,2), nullable | Weight in kilograms |
| `units_in_stock` | integer, default 0 | Current inventory count |
| `track_inventory` | boolean, default false | Enable inventory deduction on purchase |
| `allow_exceed_inventory` | boolean, default false | Allow orders when out of stock |
| `track_variant_inventory` | boolean, default false | Track inventory per variant combination |
| `track_variant_price` | boolean, default false | Allow variants to modify price |
| `is_featured` | boolean, default false | Show in Featured section on homepage |
| `is_discontinued` | boolean, default false | Hide from storefront |
| `is_on_sale` | boolean, default false | Enable sale price display |
| `additional_info_title` | string (255), nullable | Label for additional info section |
| `additional_info_body` | text, nullable | Content for additional info section |
| `sort_order` | integer, default 0 | Manual ordering in catalog |
| `images` | array/JSON, nullable | *(legacy; primary via Spatie MediaLibrary)* |
| `category_id` | foreign key | Belongs to Category |
| `supplier_id` | foreign key, nullable | Belongs to Supplier |

**Relationships:**
- `hasMany(ProductVariantGroup)`
- `hasMany(ProductVariantCombination)`
- `belongsTo(Category)`
- `belongsTo(Supplier)` (nullable)

**Media Collections (Spatie):**
- `images` — Full product images (multiple, reorderable, max 10)
- Conversions: `thumb`, `card`

### 5.2 Category

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `name` | string | Category display name |
| `slug` | string | URL-friendly unique identifier |
| `sort_order` | integer, default 0 | Manual ordering |

**Relationships:** `hasMany(Product)`

### 5.3 Supplier

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `name` | string | Supplier company name |
| `contact_info` | text, nullable | Contact details (email, phone, address, person) |
| `payment_terms` | text, nullable | Payment terms description |

**Relationships:** `hasMany(Product)`

### 5.4 ProductVariantGroup

Represents a dimension of variation on a product (e.g., "Size", "Colour").

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `product_id` | foreign key | Parent product |
| `name` | string (100) | Group label (e.g., "Size") |
| `is_required` | boolean, default true | Customer must select an option |
| `affects_price` | boolean | Options in this group can alter price |
| `affects_inventory` | boolean | Options in this group use variant inventory |
| `sort_order` | integer, default 0 | Display order |

**Relationships:** `belongsTo(Product)`, `hasMany(ProductVariantOption)`

### 5.5 ProductVariantOption

A single selectable value within a variant group (e.g., "Small", "Red", "XL").

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `variant_group_id` | foreign key | Parent variant group |
| `name` | string (100) | Option label |
| `sort_order` | integer, default 0 | Display order within group |

**Relationships:** `belongsTo(ProductVariantGroup)`, `belongsToMany(ProductVariantCombination)`

### 5.6 ProductVariantCombination

Tracks inventory (and optionally price) for a specific combination of selected variant options.

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `product_id` | foreign key | Parent product |
| `units_in_stock` | integer | Inventory for this specific combination |

**Relationships:** `belongsTo(Product)`, `belongsToMany(ProductVariantOption)`

### 5.7 Order

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `user_id` | foreign key, nullable | Authenticated customer (null if guest checkout) |
| `status` | enum | `pending`, `processing`, `shipped`, `cancelled`, `refunded` |
| `contact_name` | string | Customer's full name |
| `contact_email` | string | Customer's email |
| `contact_phone` | string, nullable | Customer's phone |
| `note_to_seller` | text, nullable | Customer note at checkout |
| `note_to_buyer` | text, nullable | Merchant note visible in order history |
| `address1` | string | Billing street address |
| `address2` | string, nullable | Billing suite/apartment |
| `city` | string | Billing city |
| `province` | string | Billing province/state |
| `postal_code` | string | Billing postal/ZIP code |
| `country` | string | Billing country |
| `ship_name` | string | Shipping recipient name |
| `ship_address1` | string | Shipping street address |
| `ship_address2` | string, nullable | Shipping suite/apartment |
| `ship_city` | string | Shipping city |
| `ship_province` | string | Shipping province/state |
| `ship_postal_code` | string | Shipping postal/ZIP code |
| `ship_country` | string | Shipping country |
| `shipping_method` | string | Name of shipping method chosen |
| `freight` | decimal (10,2) | Shipping cost charged |
| `subtotal` | decimal (10,2) | Sum of all line items before tax and shipping |
| `tax_charged` | decimal (10,2) | Tax amount collected |
| `total` | decimal (10,2) | Final total (subtotal + freight + tax) |
| `stripe_payment_intent_id` | string, nullable | Stripe PaymentIntent ID |
| `stripe_charge_id` | string, nullable | Stripe Charge ID |
| `amount_refunded` | decimal (10,2), default 0 | Total amount refunded |
| `required_date` | date, nullable | Customer-requested delivery date |
| `shipped_date` | date, nullable | Date merchant marked as shipped |
| `ordered_at` | timestamp | Order placement timestamp |

**Relationships:** `belongsTo(User)`, `hasMany(OrderItem)`

### 5.8 OrderItem

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `order_id` | foreign key | Parent order |
| `product_name` | string | Snapshot of product name at time of order |
| `variant_label` | string, nullable | Human-readable variant description (e.g., "Size: L, Colour: Red") |
| `sku` | string, nullable | Snapshot of SKU at time of order |
| `unit_price` | decimal (10,2) | Price per unit at time of order |
| `quantity` | integer | Quantity ordered |
| `subtotal` | decimal (10,2) | `unit_price × quantity` |

**Relationships:** `belongsTo(Order)`

### 5.9 User (Customer)

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `name` | string | Full name |
| `email` | string, unique | Email address (login identifier) |
| `password` | string | Bcrypt-hashed password |
| `remember_token` | string, nullable | For "remember me" sessions |
| `email_verified_at` | timestamp, nullable | Email verification timestamp |

**Relationships:** `hasOne(UserProfile)`, `hasMany(Order)`, `hasMany(Wishlist)`

### 5.10 UserProfile

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `user_id` | foreign key | Belongs to User |
| `phone` | string, nullable | Phone number |
| `address_line1` | string, nullable | Default street address |
| `address_line2` | string, nullable | Default suite/apartment |
| `city` | string, nullable | Default city |
| `province` | string, nullable | Default province/state |
| `postal_code` | string, nullable | Default postal/ZIP code |
| `country` | string, default "Canada" | Default country |

**Relationships:** `belongsTo(User)`

### 5.11 Admin (Merchant User)

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `tenant_id` | string | Tenant this admin belongs to |
| `email` | string | Login email |
| `password` | string | Bcrypt-hashed password |

Used exclusively for the Filament merchant admin panel. Separate from the storefront `User` model.

### 5.12 SuperAdmin

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `email` | string | Login email |
| `password` | string | Bcrypt-hashed password |

Used exclusively for the Filament Super Admin panel on the central domain.

### 5.13 Tenant

| Field | Type | Description |
|-------|------|-------------|
| `id` | string (slug) | Subdomain prefix (e.g., `acmebakery`) |
| `suspended_at` | timestamp, nullable | If set, store is suspended and displays suspended page |
| `manually_billed` | boolean | If true, billing is handled outside the platform |
| `plan_id` | foreign key, nullable | Active subscription plan |
| `stripe_id` | string, nullable | Stripe Customer ID |
| `pm_type` | string, nullable | Payment method type |
| `pm_last_four` | string, nullable | Last 4 digits of payment method |
| `trial_ends_at` | timestamp, nullable | Trial expiry date |

**Methods:** `isSuspended()`, `suspend()`, `reactivate()`  
**Relationships:** `belongsTo(Plan)`

### 5.14 Plan

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `name` | string (255) | Plan display name (e.g., "Starter", "Growth", "Pro") |
| `price` | decimal (10,2) | Monthly fee |
| `stripe_price_id` | string (255), nullable | Stripe Price ID (used for automated billing — Phase 4) |
| `features` | array/JSON | Key-value list of plan features and their values |
| `is_active` | boolean, default true | Whether this plan is currently offered |

**Relationships:** `hasMany(Tenant)`

### 5.15 SiteSettings

One record per tenant. Controls all store-wide configuration.

| Field Group | Field | Type | Description |
|-------------|-------|------|-------------|
| **Store Info** | `company_name` | string (255) | Displayed in header if no logo |
| | `company_slogan` | string (500), nullable | Displayed in header below logo |
| | `contact_email` | string (255), nullable | Store contact email |
| **Commerce** | `currency` | enum: `cad`, `usd`, `eur`, `gbp` | Store currency |
| | `currency_symbol` | string (5) | Symbol displayed (e.g., `CA$`, `US$`, `£`, `€`) |
| | `tax_rate` | decimal (5,2) | Tax percentage (e.g., `15.00` for 15% HST) |
| | `require_account` | boolean | If true, customers must be logged in to check out |
| **Policies** | `shipping_policy` | text, nullable | Displayed on storefront |
| | `return_policy` | text, nullable | Displayed on storefront |
| **Stripe** | `stripe_connect_account_id` | string, nullable | Merchant's Stripe Connect account ID |
| | `stripe_charges_enabled` | boolean | Whether Stripe charges are active |
| | `stripe_payouts_enabled` | boolean | Whether Stripe payouts are active |
| **Branding** | `primary_color` | string (7) | Hex color (default `#3B82F6`) |
| | `secondary_color` | string (7) | Hex color (default `#1E40AF`) |
| | `accent_color` | string (7) | Hex color (default `#F59E0B`) |
| | `background_color` | string (7) | Hex color (default `#FFFFFF`) |
| | `text_color` | string (7) | Hex color (default `#111827`) |
| | `font_heading` | string | Google Font name (default `Inter`) |
| | `font_body` | string | Google Font name (default `Inter`) |
| | `button_style` | enum: `rounded`, `pill`, `sharp` | Border-radius style for buttons |
| | `custom_css` | text, nullable | Injected verbatim into `<style>` tag |
| **Media** | `logo_path` | string, nullable | Path to uploaded logo image |
| | `favicon_path` | string, nullable | Path to uploaded favicon |
| | `banner_path` | string, nullable | Path to uploaded homepage banner |

### 5.16 ShippingMethod

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `tenant_id` | string | Owning tenant |
| `name` | string | Method label (e.g., "Standard Shipping") |
| `description` | string, nullable | Short description displayed at checkout |
| `price` | decimal (10,2) | Shipping cost |

### 5.17 Wishlist

| Field | Type | Description |
|-------|------|-------------|
| `id` | bigint | Auto-increment primary key |
| `user_id` | foreign key | Authenticated customer |
| `product_id` | foreign key | Saved product |

---

## 6. Storefront — Customer-Facing Pages

All storefront pages are served on the tenant subdomain. They inherit branding from `SiteSettings`. The master layout is `resources/views/storefront/layout.blade.php`.

### 6.1 Homepage / Product Listing (`/`)

**Route:** `storefront.index`  
**Controller:** `ProductController@index`  
**Template:** `storefront/index.blade.php`

#### URL Parameters

| Parameter | Type | Effect |
|-----------|------|--------|
| `search` | string | Filters products by name (LIKE query) |
| `category` | string (slug) | Filters products to the specified category |

#### Page Sections

**Hero Banner**  
Displayed only on homepage (not on filtered/search results).  
Full-width image sourced from `SiteSettings.banner_path`. Hidden if no banner is set.

**Category Filter Bar**  
Horizontal row of category buttons. Clicking a category appends `?category=slug` to the URL. Includes a "Clear" button when a filter is active.

**Search Bar**  
Text input that submits `?search=term` on form submit.

**Featured Products Section**  
Displayed only when no search or category filter is active.  
Heading: "Featured Products" (or equivalent).  
Products with `is_featured = true` displayed in a card grid.

**All Products Grid**  
4 columns on desktop, 2 on tablet, 1 on mobile.  
Paginated: 12 items per page.  
Heading: "All Products" (or filtered/search heading when applicable).

**Product Card Contents**

| Element | Description |
|---------|-------------|
| Product image | Thumbnail from MediaLibrary `card` conversion. Camera emoji placeholder if no image |
| Category name | Small-caps label above product name |
| Product name | Clickable link to product detail page |
| Price | Regular price displayed. If `is_on_sale` and `sale_price` is set: sale price (colored) + original price with strikethrough |
| "Out of Stock" badge | Red badge displayed when `track_inventory = true` and `units_in_stock ≤ 0` |
| Wishlist toggle | Heart icon (Livewire `WishlistToggle` component); filled if in wishlist, outlined if not; requires authentication to add |

---

### 6.2 Product Detail Page (`/products/{slug}`)

**Route:** `storefront.product`  
**Controller:** `ProductController@show`  
**Template:** `storefront/product.blade.php`

#### Page Layout

Two-column layout on desktop (left: images; right: details). Single column on mobile.

#### Breadcrumb Navigation

`Products > [Category Name] > [Product Name]`  
Each segment is a clickable link.

#### Left Column — Images

- Main product image (large)
- Thumbnail strip below main image for additional images
- Clicking a thumbnail updates the main image
- Camera emoji placeholder if no images exist

#### Right Column — Product Details

| Element | Description |
|---------|-------------|
| Category label | Small-caps link to category filter |
| Product name | H1 heading |
| Price display | Regular price; or sale price + strikethrough original if on sale |
| Product description | Formatted text (line breaks preserved) |
| Variant Selector | Livewire component (see Section 6.2.1) |
| Quantity selector | Numeric input with `+` and `−` buttons |
| Add to Cart button | Adds item (with selected variant combination) to session cart |
| Wishlist toggle | Heart button; requires authentication |
| SKU display | Displayed below variant selector if `sku` is set |
| Additional Info section | Displayed if `additional_info_title` and `additional_info_body` are set; shows as a collapsible or separate section with the merchant-defined title and body |

#### "Back to products" Link

Returns to the homepage product listing.

#### 6.2.1 Variant Selector (Livewire: `ProductVariantSelector`)

Displayed only when a product has variant groups defined.

**Behavior:**
- Each variant group is displayed as a labeled row of selectable option buttons
- Options rendered as pill/button style buttons
- Selected option receives `.variant-option-selected` CSS class (blue border, blue text, bold, light blue background)
- If a group has `is_required = true`, the customer must select an option before adding to cart
- If `affects_price = true` on the group and the combination has a custom price: displayed price updates dynamically
- If `affects_inventory = true` on the group: stock count updates per combination; "Out of Stock" shown if the combination's `units_in_stock ≤ 0` and overselling is not allowed

---

### 6.3 Cart Page (`/cart`)

**Route:** `storefront.cart`  
**Template:** `storefront/cart.blade.php` (wrapper)  
**Livewire Component:** `CartPage`

#### Empty State

Cart emoji + "Your cart is empty." text + "Continue shopping" link to homepage.

#### Cart Items List

Each row contains:

| Element | Description |
|---------|-------------|
| Product thumbnail | Small image (or placeholder) |
| Product name | Bold text |
| Variant label | If applicable (e.g., "Size: L, Colour: Red") |
| SKU | If present |
| Unit price | Currency symbol from settings |
| Quantity control | `−` button / quantity number / `+` button. Updates via Livewire |
| Line subtotal | `unit_price × quantity` |
| Remove button | Red text "Remove" link. Removes item from cart via Livewire |

#### Cart Summary

| Element | Description |
|---------|-------------|
| Subtotal | Sum of all line item subtotals |
| Shipping note | "Shipping calculated at checkout" |
| "Proceed to Checkout" button | Primary button → `/checkout` |
| "Continue shopping" link | → homepage |

---

## 7. Checkout Flow

**Route:** `storefront.checkout`  
**Template:** `storefront/checkout.blade.php` (wrapper)  
**Livewire Component:** `CheckoutWizard`  
**Middleware:** If `SiteSettings.require_account = true`, unauthenticated users are redirected to login before checkout.

The checkout is a 4-step wizard. A progress bar at the top of the page shows the current step (Steps 1–4 highlighted progressively). The right sidebar displays the order summary at all steps.

---

### 7.1 Order Summary Sidebar (All Steps — Sticky)

| Element | Description |
|---------|-------------|
| Heading | "Order Summary" |
| Item list | Each cart item with thumbnail, name, variant label, and quantity |
| Subtotal | Sum of line items |
| Shipping | Cost of selected shipping method (or "—" until Step 2 is completed) |
| Tax | Tax amount (displayed with tax rate percentage label) if `tax_rate > 0` |
| **Total** | Bold, final total |

---

### 7.2 Step 1: Contact & Billing

**Heading:** "Contact & Billing"

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| Full Name | Text | Yes | Required |
| Email | Email | Yes | Required, valid email |
| Phone | Tel | No | Optional |
| **Billing Address:** | | | |
| Address (street) | Text | Yes | Required |
| Apartment / Suite | Text | No | Optional |
| City | Text | Yes | Required |
| Province / State | Text | Yes | Required |
| Postal Code / ZIP | Text | Yes | Required |
| Country | Text | Yes | Required |

**Navigation:** "Continue to Shipping →" button. Advances to Step 2. Validates all required fields before advancing; errors displayed inline below each field in red text.

---

### 7.3 Step 2: Shipping

**Heading:** "Shipping"

**Shipping Method Selection**

| Element | Description |
|---------|-------------|
| Method list | Radio button cards, one per configured shipping method |
| Each card shows | Method name (bold), description, price |
| Selected state | Blue border, blue background on card; radio input checked |
| Hover state | Lighter highlight for unselected options |
| Selection required | A method must be selected to advance |

**Alternate Shipping Address**

| Element | Description |
|---------|-------------|
| "Ship to same address as billing" checkbox | Checked by default |
| If unchecked | Alternate shipping address form appears (all fields below required) |

Alternate shipping address fields (shown only if above checkbox is unchecked):

| Field | Type | Required |
|-------|------|----------|
| Recipient Name | Text | Yes |
| Address (street) | Text | Yes |
| City | Text | Yes |
| Province / State | Text | Yes |
| Postal Code / ZIP | Text | Yes |
| Country | Text | Yes |

**Navigation:** "← Back" (returns to Step 1) and "Review Order →" (advances to Step 3, validates shipping method selection and alternate address fields if applicable).

---

### 7.4 Step 3: Review

**Heading:** "Review Your Order"

| Section | Content |
|---------|---------|
| Contact summary | Name, email, phone |
| Shipping summary | Full shipping address (or "Same as billing address") |
| Error display | Red alert box if a server-side error is returned |

**Navigation:** "← Back" (returns to Step 2) and "Continue to Payment →" (advances to Step 4; triggers server-side PaymentIntent creation via Stripe API).

---

### 7.5 Step 4: Payment

**Heading:** "Payment"

| Element | Description |
|---------|-------------|
| Stripe Payment Element | PCI-compliant Stripe-hosted payment form embedded via Stripe.js v3 |
| Error display | Red text (Alpine.js reactive); displays Stripe error message if payment fails |
| Loading state | "Processing..." text displayed on button while awaiting Stripe confirmation |
| "← Back" button | Returns to Step 3 |
| "Pay & Place Order" button | Triggers Stripe `confirmPayment()`. On success, calls Livewire `placeOrder()` method server-side |

**Payment Flow:**
1. On advancing to Step 4, Livewire creates a Stripe `PaymentIntent` server-side with the order total
2. The Stripe Payment Element is initialized with the client secret from the PaymentIntent
3. Customer enters card details in the Stripe Payment Element
4. On button click, client-side `stripe.confirmPayment()` is called
5. On Stripe success, Livewire `placeOrder()` is called, which:
   - Creates the `Order` record in the database
   - Creates `OrderItem` records
   - Deducts inventory (if tracking enabled)
   - Clears the session cart
   - Stores `last_order_id` in session
   - Redirects to the order confirmation page
6. On Stripe failure, the error message is displayed inline; the customer may retry

---

### 7.6 Order Confirmation Page (`/orders/{id}/confirmation`)

**Route:** `storefront.order.confirmation`  
**Controller:** `OrderController@confirmation`  
**Template:** `storefront/order-confirmation.blade.php`

**Security:** The page checks that `session('last_order_id')` matches the `{id}` in the URL. If they do not match (e.g., a user attempts to access another customer's confirmation), the request is redirected to the homepage.

| Section | Content |
|---------|---------|
| Success header | "✅ Order Confirmed!" + customer name + Order ID |
| Items ordered | Table: product name, variant label, quantity × unit price, subtotal |
| Cost breakdown | Subtotal, shipping method name + cost, total (bold) |
| Shipping address | Recipient name, full address block |
| Payment status | Green "Payment Received" box if `stripe_payment_intent_id` is set + confirmation email sent note |
| Payment status (fallback) | Amber "Payment Pending" box if no Stripe ID + note that the store will contact to arrange payment |
| "Continue Shopping" button | Returns to homepage |

---

## 8. Customer Account Pages

All account pages require authentication. Unauthenticated users are redirected to `/login`.

All three account pages share the same **tab navigation**:

| Tab | URL |
|-----|-----|
| Profile | `/account` |
| Order History | `/account/orders` |
| Wishlist | `/account/wishlist` |

---

### 8.1 Profile Page (`/account`)

**Route:** `account.profile`  
**Controller:** `AccountController@profile`  
**Handler:** `PATCH /account` → `AccountController@updateProfile`  
**Template:** `storefront/account/profile.blade.php`

#### Account Information Section

| Field | Type | Required | Source |
|-------|------|----------|--------|
| Full Name | Text | Yes | `User.name` |
| Email | Email | Yes | `User.email` |

#### Default Address Section

| Field | Type | Required | Source |
|-------|------|----------|--------|
| Phone | Tel | No | `UserProfile.phone` |
| Address | Text | No | `UserProfile.address_line1` |
| Apartment / Suite | Text | No | `UserProfile.address_line2` |
| City | Text | No | `UserProfile.city` |
| Province | Text | No | `UserProfile.province` |
| Postal Code | Text | No | `UserProfile.postal_code` |
| Country | Text | No | `UserProfile.country` (default: "Canada") |

#### Change Password Section

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| New Password | Password | No | Leave blank to keep current password |
| Confirm New Password | Password | Required if password filled | Must match |

**"Save Changes" button:** Submits the form via PATCH. On success, displays a green success alert on the same page.

---

### 8.2 Order History Page (`/account/orders`)

**Route:** `account.orders`  
**Controller:** `AccountController@orders`  
**Template:** `storefront/account/orders.blade.php`

**Empty State:** Package emoji + "You haven't placed any orders yet." + "Start Shopping" button → homepage.

**Order List:** Paginated list of orders belonging to the authenticated user. Each order is displayed as a card.

**Order Card Contents:**

| Element | Description |
|---------|-------------|
| Order ID | Bold, in a header row |
| Placed date | Formatted timestamp |
| Shipped date | Shown in green if `shipped_date` is set |
| Status badge | Color-coded by status (see below) |
| Line items | Each order item: product name, variant label, quantity, subtotal |
| Note to buyer | Blue info box shown if `note_to_buyer` is set |
| Cost breakdown | Subtotal, shipping, tax (if applicable) |
| **Total** | Bold |

**Order Status Badge Colors:**

| Status | Badge Color |
|--------|------------|
| `pending` | Yellow / Amber |
| `processing` | Blue |
| `shipped` | Green |
| `cancelled` | Red |
| `refunded` | Gray |

---

### 8.3 Wishlist Page (`/account/wishlist`)

**Route:** `wishlist.index`  
**Controller:** `WishlistController@index`  
**Template:** `storefront/wishlist.blade.php`

**Empty State:** Heart icon + "Your wishlist is empty." + "Browse Products" button → homepage.

**Wishlist Grid:** 2 columns on tablet, 1 on mobile. Paginated.

Each wishlist item card shows:

| Element | Description |
|---------|-------------|
| Product thumbnail | Image or placeholder icon |
| Product name | Clickable link to product detail page |
| Category | Category name |
| Price | Current price (sale price if on sale) |
| "View Product" button | Links to product detail page |
| Wishlist toggle button | Heart icon. Clicking removes the product from the wishlist; the card is removed from the DOM via a JavaScript `wishlist-removed` event listener |

---

## 9. Authentication Pages

All authentication pages are served under the tenant subdomain. They use a minimal centered-card layout with the store logo.

### 9.1 Login (`/login`)

**Route:** `login`  
**Controller:** `LoginController@showForm` (GET), `LoginController@login` (POST)  
**Template:** `auth/login.blade.php`

| Element | Specification |
|---------|--------------|
| Heading | "Sign In" |
| Email field | Email type, required, autofocus |
| Password field | Password type, required |
| "Remember me" checkbox | Optional |
| "Forgot password?" link | → `/forgot-password` |
| Submit button | "Sign In" (primary brand button) |
| Register link | "Don't have an account? Create one" → `/register` |
| Error messages | Red text below the relevant fields |

---

### 9.2 Register (`/register`)

**Route:** `register`  
**Controller:** `RegisterController@showForm` (GET), `RegisterController@register` (POST)  
**Template:** `auth/register.blade.php`

| Element | Specification |
|---------|--------------|
| Heading | "Create Account" |
| Full Name field | Text type, required, autofocus |
| Email field | Email type, required |
| Password field | Password type, required |
| Confirm Password field | Password type, required |
| Submit button | "Create Account" (primary brand button) |
| Login link | "Already have an account? Sign in" → `/login` |
| Error messages | Red text below the relevant fields |

---

### 9.3 Forgot Password (`/forgot-password`)

**Route:** `password.request`  
**Controller:** `PasswordResetController@showRequest` (GET), `PasswordResetController@sendResetLink` (POST)  
**Template:** `auth/forgot-password.blade.php`

| Element | Specification |
|---------|--------------|
| Heading | "Forgot Password" (or equivalent) |
| Email field | Email type, required |
| Submit button | "Send Reset Link" (primary brand button) |
| Status message | Success or error feedback after submission |

---

### 9.4 Reset Password (`/reset-password/{token}`)

**Route:** `password.reset`  
**Controller:** `PasswordResetController@showReset` (GET), `PasswordResetController@resetPassword` (POST)  
**Template:** `auth/reset-password.blade.php`

| Element | Specification |
|---------|--------------|
| Heading | "Reset Password" (or equivalent) |
| Email field | Pre-filled from the reset link |
| New Password field | Required |
| Confirm New Password field | Required |
| Submit button | "Reset Password" (primary brand button) |
| Error messages | Red text below the relevant fields |

---

### 9.5 Status / Error Pages

| Page | Trigger | Template |
|------|---------|---------|
| Store Suspended | `Tenant.suspended_at` is not null | `storefront/suspended.blade.php` |
| Subscription Required | Tenant has no active subscription or trial | `storefront/subscription-required.blade.php` |

Both pages display a branded message (using the store's name) and contact information. They do not allow navigation to the rest of the storefront.

---

## 10. Merchant Admin Dashboard

**Framework:** Filament 5.6  
**URL:** `/admin` on the tenant subdomain (e.g., `acmebakery.ecstores.ca/admin`)  
**Access:** Authenticated `Admin` model users only (per-tenant admin accounts)  
**Magic Login:** Platform operators can access any merchant admin via a signed magic link at `/ecw-login` (bypasses tenant suspension check)

### 10.1 Navigation Groups

| Group | Resource / Page | Icon | Sort |
|-------|----------------|------|------|
| **Catalog** | Products | `shopping-bag` | 1 |
| | Categories | `folder` | — |
| | Suppliers | `truck` | — |
| **Shipping** | Shipping Methods | — | — |
| **Orders** | Orders | `clipboard` | — |
| **Settings** | Site Settings | `cog-6-tooth` | — |
| | Branding Studio | — | — |
| | Manage Stripe Connect | — | — |

---

### 10.2 Products Resource

**CRUD:** Full create, read, update, delete.

#### Product Form Sections

**Section 1: Images**

| Element | Specification |
|---------|--------------|
| Component | FileUpload (Filament) |
| Multiple | Yes |
| Reorderable | Yes (drag handles) |
| Maximum files | 10 |
| Accepted types | Images only |
| Storage | `public` disk, `/storage/product-images` |

**Section 2: Product Details**

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| Name | Text | Yes | Triggers auto-generation of slug on blur |
| Slug | Text | Yes | Unique; auto-generated from name; editable |
| Category | Searchable select | No | Lists all tenant categories |
| Supplier | Searchable select | No | Lists all tenant suppliers |
| SKU | Text | No | Max 100 characters |
| Description | Textarea (4 rows) | No | — |

**Section 3: Pricing**

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| Regular Price | Decimal (prefix: $) | Yes | Min 0 |
| Sale Price | Decimal (prefix: $) | No | Must be less than Regular Price (validated) |
| Is On Sale | Toggle | No | Enables sale price display on storefront |

**Section 4: Inventory**

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| Units in Stock | Integer | No | Min 0, default 0 |
| Weight | Decimal (suffix: kg) | No | Min 0 |
| Track Inventory | Toggle | No | If on, inventory is decremented on purchase |
| Allow Overselling | Toggle | No | If on, orders can proceed even if `units_in_stock ≤ 0` |
| Track Per-Variant Inventory | Toggle | No | If on, each variant combination has its own stock count |
| Per-Variant Pricing | Toggle | No | If on, variant combinations can have individual prices |

**Section 5: Visibility & Status**

| Field | Type | Notes |
|-------|------|-------|
| Featured | Toggle | If on, product appears in Featured section on homepage |
| Discontinued | Toggle | If on, product is hidden from the storefront |
| Sort Order | Integer (min 0, default 0) | Controls ordering in product grid |

**Section 6: Variants** (collapsed by default)

A Repeater component for creating variant groups. Each variant group contains:

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| Group Name | Text | Yes | e.g., "Size", "Colour" |
| Is Required | Toggle | — | Default: true |
| Affects Price | Toggle | — | Whether options in this group alter the product price |
| Affects Inventory | Toggle | — | Whether options use per-combination inventory |

Within each group, a nested Repeater for options:

| Field | Type | Required |
|-------|------|----------|
| Option Name | Text | Yes |

Nested repeater is reorderable and collapsible. Labels: "Add option", "Add variant group".

**Section 7: Additional Information** (collapsed by default)

| Field | Type | Notes |
|-------|------|-------|
| Title | Text (max 255) | Section heading displayed on product detail page |
| Body | Textarea (4 rows) | Section content |

#### Products Table

| Column | Sortable | Searchable | Notes |
|--------|----------|------------|-------|
| Name | — | Yes | — |
| Category | — | — | — |
| SKU | — | — | — |
| Price | — | — | Formatted as currency |
| Status | — | — | Badge: Featured, Discontinued, On Sale |

**Filters:** Category, Status (Featured, Discontinued, On Sale)  
**Bulk Actions:** Delete  
**Pagination:** 10 per page

#### Combinations Relation Manager

Accessible from the edit view of a product. Displays variant combinations (the cross-product of all variant options) with their inventory counts. Allows the merchant to set per-combination `units_in_stock`.

---

### 10.3 Categories Resource

**CRUD:** Full.

#### Category Form

| Field | Type | Required |
|-------|------|----------|
| Name | Text | Yes |
| Slug | Text | Yes (unique, auto-generated from name) |
| Description | Textarea | No |
| Sort Order | Integer | No |

#### Categories Table

| Column | Searchable |
|--------|------------|
| Name | Yes |
| Description | — |
| Sort Order | — |

---

### 10.4 Suppliers Resource

**CRUD:** Full.  
**Availability:** Pro plan only (Phase 4 plan enforcement; currently accessible to all tenants).

#### Supplier Form

| Field | Type | Required |
|-------|------|----------|
| Name | Text | Yes |
| Contact Email | Email | No |
| Contact Phone | Tel | No |
| Address | Text | No |
| Contact Person | Text | No |
| Payment Terms | Textarea | No |

#### Suppliers Table

| Column | Searchable |
|--------|------------|
| Name | Yes |
| Email | — |
| Phone | — |

---

### 10.5 Shipping Methods Resource

**CRUD:** Full.

#### Shipping Method Form

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| Name | Text | Yes | e.g., "Standard Shipping", "Local Pickup" |
| Description | Text | No | Shown during checkout |
| Price | Decimal (prefix: $) | Yes | Min 0 |
| Estimated Delivery Days | Integer | No | Informational only |

#### Shipping Methods Table

| Column | Description |
|--------|-------------|
| Name | Method label |
| Price | Formatted currency |
| Estimated Delivery | Days |

---

### 10.6 Orders Resource

**CRUD:** Read and update only (no create or delete from admin).

#### Orders Table

| Column | Sortable | Searchable | Notes |
|--------|----------|------------|-------|
| Order ID | — | Yes | — |
| Customer Name | — | — | `contact_name` |
| Status | — | — | Badge (color-coded) |
| Total | — | — | Formatted currency |
| Order Date | Yes | — | `ordered_at` |

**Filters:** Status, Date range  
**Pagination:** 25 per page

#### Edit Order Page

| Element | Description |
|---------|-------------|
| Status | Dropdown (pending, processing, shipped, cancelled, refunded) |
| Note to Buyer | Textarea — merchant message visible to customer in order history |
| Customer contact info | Read-only display |
| Shipping address | Read-only display |
| Payment details | Stripe Payment Intent ID, charge ID, amount refunded |

**Order Items Relation Manager:** Lists all `OrderItem` records for the order. Read-only.  
Columns: Product Name, Variant Label, SKU, Unit Price, Quantity, Subtotal.

---

### 10.7 Site Settings Page

**Type:** Filament Page (no standard CRUD resource)  
**URL:** `/admin/site-settings`

#### Store Information Section

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| Store Name | Text (max 255) | Yes | Used in header and emails |
| Contact Email | Email (max 255) | No | — |
| Slogan | Text (max 500) | No | Displayed in header |

#### Store Behaviour Section

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| Currency | Select (cad, usd, eur, gbp) | Yes | Changing currency auto-populates the currency symbol |
| Currency Symbol | Text (max 5) | Yes | e.g., `CA$`, `US$`, `£`, `€` |
| Tax Rate (%) | Decimal (0–99.99, step 0.01) | No | e.g., `15` for 15% HST |
| Require Account to Checkout | Toggle | No | Forces login before checkout |

#### Policies Section (collapsed by default)

| Field | Type | Notes |
|-------|------|-------|
| Shipping Policy | Textarea (5 rows) | Displayed to customers on storefront |
| Return Policy | Textarea (5 rows) | Displayed to customers on storefront |

**"Save Settings" button:** Persists all changes to the `SiteSettings` record.

---

### 10.8 Branding Studio Page

**Type:** Filament Page  
**URL:** `/admin/branding-studio`

#### Color Pickers

| Field | Default |
|-------|---------|
| Primary Color | `#3B82F6` |
| Secondary Color | `#1E40AF` |
| Accent Color | `#F59E0B` |
| Background Color | `#FFFFFF` |
| Text Color | `#111827` |

#### Typography

| Field | Type | Default |
|-------|------|---------|
| Heading Font | Text / Dropdown | `Inter` |
| Body Font | Text / Dropdown | `Inter` |

Fonts are loaded from Google Fonts at runtime. The storefront CSS variables `--font-heading` and `--font-body` are set accordingly.

#### Button Style

Radio selection:

| Option | Effect |
|--------|--------|
| Rounded (default) | `border-radius: 0.5rem` |
| Pill | `border-radius: 9999px` |
| Sharp | `border-radius: 0px` |

#### Custom CSS

Textarea. Content is injected verbatim into a `<style>` tag in the storefront `<head>`. Merchant-controlled; no sanitization beyond what the template renders.

#### Media Uploads

| Upload | Purpose | Displayed |
|--------|---------|-----------|
| Logo | Store logo in header | Header; replaces company name text if uploaded |
| Favicon | Browser tab icon | `<link rel="icon">` |
| Banner | Homepage hero image | Full-width banner above product grid |

---

### 10.9 Manage Stripe Connect Page

**Type:** Filament Page  
**URL:** `/admin/stripe-connect`

This page guides the merchant through connecting their Stripe account to receive payments directly.

| Element | Description |
|---------|-------------|
| Connection status | Shows whether Stripe Connect is active, charges enabled, payouts enabled |
| "Connect to Stripe" button | Initiates Stripe Connect OAuth flow. Redirects to Stripe's onboarding |
| Return handling | After Stripe onboarding, merchant is returned to `/stripe/connect/return` |
| Refresh handling | Stripe may redirect to `/stripe/connect/refresh` if the link expires |
| Dashboard link | Link to merchant's Stripe Express dashboard |
| Disconnect button | Removes the connection (with confirmation) |

**Stripe Connect Routes:**

| Route | Purpose |
|-------|---------|
| `GET /stripe/connect/start` | Initiates OAuth flow |
| `GET /stripe/connect/return` | Handles return from Stripe after onboarding |
| `GET /stripe/connect/refresh` | Refreshes the onboarding link if expired |
| `GET /stripe/connect/dashboard` | Redirects to Stripe Express dashboard |

---

## 11. Super Admin Dashboard

**Framework:** Filament 5.6 (`SuperAdminPanelProvider`)  
**URL:** `/super-admin` on the **central domain** only  
**Middleware:** `EnsureCentralDomain` — this panel is inaccessible from any tenant subdomain  
**Access:** `SuperAdmin` model users only

### 11.1 Navigation

| Group | Resource / Widget | Icon | Sort |
|-------|------------------|------|------|
| **Platform** | Tenants | `building-2` | 1 |
| | Plans | `credit-card` | 2 |
| **Dashboard** | Platform Overview Widget | — | — |

---

### 11.2 Plans Resource

**CRUD:** Full.

#### Plan Form

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| Name | Text (max 255) | Yes | e.g., "Starter", "Growth", "Pro" |
| Price | Decimal (prefix: $, step: 0.01) | Yes | Monthly fee; helper text: "Monthly fee" |
| Stripe Price ID | Text (max 255) | No | From Stripe Dashboard; placeholder `price_xxxxx`. Helper: "Leave blank until Phase 4" |
| Is Active | Toggle | — | Default: true. Inactive plans are not offered to new tenants |
| Features | KeyValue repeater | No | Key: feature name; Value: description (e.g., "Included" or "5 GB"). Reorderable. |

#### Plans Table

| Column | Sortable | Searchable | Hidden by Default |
|--------|----------|------------|-------------------|
| Name | Yes | Yes | No |
| Price / month | Yes | — | No |
| Stripe Price ID | — | — | Yes (toggleable) |
| Is Active | — | — | No (badge) |

**Bulk Actions:** Delete  
**Pagination:** Default

---

### 11.3 Tenants Resource

**CRUD:** Create and update. Soft delete / deactivation via Suspend.

#### Tenant Table Columns

| Column | Description |
|--------|-------------|
| Tenant ID (slug) | Searchable; the subdomain prefix |
| Plan | Assigned plan name |
| Status | Active / Suspended badge |
| Trial Ends | Trial expiry date |
| Stripe ID | Stripe Customer ID (if connected) |

**Row Actions:**
- View tenant store (link to subdomain)
- Edit tenant
- Suspend (sets `suspended_at` to now)
- Reactivate (clears `suspended_at`)
- Generate magic link (calls API to generate a magic login URL for the merchant admin)

#### Tenant Form / Edit

| Field | Type | Notes |
|-------|------|-------|
| Tenant ID | Text | Slug; the subdomain prefix |
| Plan | Select | All active plans |
| Manually Billed | Toggle | If on, automated billing is skipped |
| Suspended | Toggle / action | Sets or clears `suspended_at` |

---

### 11.4 Platform Overview Widget

Dashboard widget displayed on the Super Admin home screen.

| Metric | Description |
|--------|-------------|
| Total tenants | Count of all tenant records |
| Active tenants | Count where `suspended_at` is null |
| Suspended tenants | Count where `suspended_at` is not null |
| MRR | Sum of plan prices for all active, non-manually-billed tenants |

---

## 12. Global Storefront Design System

### 12.1 CSS Variable System

The storefront master layout (`storefront/layout.blade.php`) injects a `<style>` block into the `<head>` that defines CSS custom properties derived from `SiteSettings`:

| CSS Variable | Source | Default Value |
|-------------|--------|--------------|
| `--color-primary` | `primary_color` | `#3B82F6` |
| `--color-secondary` | `secondary_color` | `#1E40AF` |
| `--color-accent` | `accent_color` | `#F59E0B` |
| `--color-bg` | `background_color` | `#FFFFFF` |
| `--color-text` | `text_color` | `#111827` |
| `--font-heading` | `font_heading` | `Inter` |
| `--font-body` | `font_body` | `Inter` |
| `--btn-radius` | `button_style` | `0.5rem` (rounded) |

**Button style mapping:**

| `button_style` value | `--btn-radius` value |
|---------------------|---------------------|
| `rounded` (default) | `0.5rem` |
| `pill` | `9999px` |
| `sharp` | `0px` |

### 12.2 Global CSS Classes

These classes are defined in the layout and available site-wide:

| Class | Description |
|-------|-------------|
| `.btn-brand` | Primary action button: `background: var(--color-primary)`, white text, `border-radius: var(--btn-radius)`, hover darkens by 12% |
| `.btn-brand-outline` | Outline button: border + text in `var(--color-primary)`, hover fills |
| `.text-brand` | Text color: `var(--color-primary)` |
| `.bg-brand` | Background: `var(--color-primary)` |
| `.border-brand` | Border: `var(--color-primary)` |
| `.ring-brand` | Focus ring: `var(--color-primary)` |
| `.variant-option-selected` | Variant button selected state: blue border, blue text, bold, light blue background |

### 12.3 Default Color Palette (Tailwind)

The storefront uses Tailwind CSS (CDN build). Colors in use across templates:

| Category | Shades Used |
|----------|------------|
| Blue | `blue-50`, `blue-100`, `blue-200`, `blue-500`, `blue-600`, `blue-700` |
| Gray | `gray-50` through `gray-900` |
| Red | `red-100`, `red-400`, `red-500`, `red-600`, `red-700` |
| Green | `green-50`, `green-100`, `green-700` |
| Amber / Yellow | `yellow-100`, `yellow-700`, `amber-50`, `amber-200` |

### 12.4 Typography

**Default Font:** Inter (Google Fonts)  
**Weights loaded:** 400, 500, 600, 700  
**Google Fonts URL:** Dynamically constructed from `font_heading` and `font_body` settings values.

If `font_heading = font_body`, a single Google Fonts import is used; otherwise both fonts are loaded.

### 12.5 Storefront Layout Header

| Element | Condition | Description |
|---------|-----------|-------------|
| Logo image | If `logo_path` is set | Renders `<img>` from media path |
| Company name (text) | If no `logo_path` | Company name in `var(--color-primary)` |
| Company slogan | If `company_slogan` is set | Small text below logo; hidden on mobile |
| Auth nav (logged in) | User authenticated | "My Account", "Wishlist", "Log out" button |
| Auth nav (guest) | User not authenticated | "Sign in" link, "Register" button |
| Cart icon | Always | Shopping bag icon with a count badge; count is Alpine.js reactive, updates on `@cart-updated.window` custom event |
| Favicon | If `favicon_path` is set | `<link rel="icon">` in `<head>` |

### 12.6 Storefront Layout Footer

| Element | Description |
|---------|-------------|
| Copyright | `© [current year] [company_name]` |

*(Footer is intentionally minimal; merchants may customize further via Custom CSS.)*

---

## 13. Branding Studio

The Branding Studio allows merchants to fully customize the visual appearance of their storefront without writing code. All changes are stored in `SiteSettings` and applied at render time via CSS variables.

### 13.1 Live Preview

The Branding Studio page in the admin displays a live preview panel that updates as the merchant adjusts colors, fonts, and button styles.

### 13.2 Color System

Five color slots, each with a color picker control:

| Slot | Role |
|------|------|
| Primary Color | Main brand color — buttons, links, badges |
| Secondary Color | Supporting color — hover states, secondary elements |
| Accent Color | Highlight color — sale badges, featured labels |
| Background Color | Page background |
| Text Color | Default body text |

### 13.3 Fonts

Two font selectors (one for headings, one for body text). The merchant types or selects a Google Font name. The platform validates that the font name is a real Google Font before saving *(validation behavior TBD — may be free-text in current implementation)*.

### 13.4 Button Styles

Three radio options with visual previews in the admin:

| Label | Preview |
|-------|---------|
| Rounded | Button with rounded corners |
| Pill | Button with full-oval corners |
| Sharp | Button with square corners |

### 13.5 Custom CSS

A textarea for raw CSS. Content is appended after all theme CSS in a `<style>` tag. Allows advanced merchants to override any styling.

### 13.6 Media Uploads

Three upload zones:

| Upload | Format | Usage |
|--------|--------|-------|
| Logo | Image (JPEG, PNG, WebP) | Header branding |
| Favicon | Image (ICO, PNG, WebP) | Browser tab icon |
| Banner | Image (JPEG, PNG, WebP) | Homepage hero section |

---

## 14. Multi-Tenancy & Provisioning

### 14.1 Tenant Lifecycle

```
New Tenant Request (via ECW agency site / API)
        ↓
API: POST /api/provision
        ↓
TenantProvisioningService:
  1. Create Tenant record (id = slug)
  2. Create isolated tenant database
  3. Run tenant migrations
  4. Create default SiteSettings record
  5. Create Admin account for the merchant
  6. Send StoreWelcomeMail to merchant
        ↓
Tenant is live at {slug}.ecstores.ca
```

### 14.2 Tenant Suspension

When a tenant is suspended (via Super Admin action or API):
- `Tenant.suspended_at` is set to the current timestamp
- All storefront and admin requests are intercepted by `CheckTenantNotSuspended`
- The suspended storefront page is displayed to all visitors
- The merchant admin displays an appropriate message
- The magic link route (`/ecw-login`) bypasses suspension (allows platform operator access)

### 14.3 Reactivation

When a tenant is reactivated:
- `Tenant.suspended_at` is set to `null`
- Normal storefront and admin access resumes immediately

### 14.4 Welcome Email

A `StoreWelcomeMail` is dispatched to the merchant's email address upon successful provisioning. It includes:
- Store name and subdomain URL
- Admin login credentials (or a link to set the password)
- Getting-started instructions

---

## 15. API Endpoints

All API endpoints are on the **central domain** only. Protected by:
- `EnsureCentralDomain` middleware (rejects requests from tenant subdomains)
- `EcwHmacMiddleware` (validates an HMAC-signed request signature; used by the ECW agency site to authenticate API calls)

| Method | Endpoint | Controller | Description |
|--------|----------|-----------|-------------|
| `POST` | `/api/provision` | `ProvisionController@store` | Create a new tenant store |
| `GET` | `/api/tenant/{slug}/status` | `TenantStatusController@show` | Get tenant status and subscription info |
| `POST` | `/api/tenant/{slug}/suspend` | `TenantStatusController@suspend` | Suspend a tenant |
| `POST` | `/api/tenant/{slug}/reactivate` | `TenantStatusController@reactivate` | Reactivate a suspended tenant |
| `POST` | `/api/magic-link` | `MagicLinkController@generate` | Generate a one-time magic login link for a merchant admin |

### 15.1 POST `/api/provision`

**Request body (JSON):**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `slug` | string | Yes | Subdomain prefix (e.g., `acmebakery`); must be lowercase alphanumeric with hyphens |
| `store_name` | string | Yes | Merchant's business name |
| `admin_email` | string | Yes | Merchant's admin login email |
| `plan_id` | integer | No | Assigned plan ID (if not supplied, tenant starts in trial) |

**Response (201 Created):**

| Field | Description |
|-------|-------------|
| `store_url` | Full store URL (e.g., `https://acmebakery.ecstores.ca`) |
| `admin_url` | Admin panel URL |
| `status` | `active` or `trial` |

---

## 16. Integrations & External Services

| Service | SDK / Package | Purpose |
|---------|--------------|---------|
| **Stripe** | Laravel Cashier 16.5 + Stripe PHP SDK | Payment processing (PaymentIntents), Connect OAuth for merchant payouts |
| **Stripe.js** | CDN `https://js.stripe.com/v3/` | Client-side PCI-compliant Payment Element |
| **Tailwind CSS** | CDN `https://cdn.tailwindcss.com` | Storefront utility-first CSS |
| **Google Fonts** | `https://fonts.googleapis.com` | Dynamic font loading based on SiteSettings |
| **Spatie MediaLibrary** | Composer package (v11.22) | Product image uploads, media conversions (thumb, card) |
| **Stancl Tenancy** | Composer package | Multi-tenant database isolation and subdomain routing |
| **Livewire** | Composer package (v4.3) | Cart, Checkout Wizard, Wishlist Toggle, Variant Selector — real-time UI without full page reloads |
| **Alpine.js** | Bundled with Livewire | Client-side reactivity (cart count badge, payment form state) |

### 16.1 Stripe Connect Flow

1. Merchant clicks "Connect to Stripe" in the admin
2. Redirected to `GET /stripe/connect/start`
3. Stripe OAuth redirect to Stripe's onboarding
4. After onboarding, Stripe redirects to `GET /stripe/connect/return` with authorization code
5. Platform exchanges code for the merchant's Stripe Connect account ID
6. `SiteSettings.stripe_connect_account_id`, `stripe_charges_enabled`, and `stripe_payouts_enabled` are updated

All customer charges are created on the merchant's connected Stripe account. Funds flow directly to the merchant.

### 16.2 Stripe Webhooks

**Route:** `POST /stripe/webhook` (handled by Laravel Cashier)

Used for subscription lifecycle events (Phase 4). Current scope: payment confirmation for orders.

---

## 17. Session & State Management

### 17.1 Cart (Session-Based)

The cart is stored in the PHP session under the key `cart`.

**Cart item structure:**

| Key | Type | Description |
|-----|------|-------------|
| `product_id` | integer | Product ID |
| `name` | string | Product name (snapshot) |
| `sku` | string | SKU (snapshot) |
| `image` | string | Image URL (snapshot) |
| `variant_label` | string, nullable | Human-readable variant description |
| `unit_price` | decimal | Price at time of add-to-cart |
| `quantity` | integer | Quantity in cart |

**CartService methods:** `add()`, `remove()`, `clear()`, `count()`, `items()`

When the cart is modified, a custom browser event `@cart-updated.window` is dispatched (Alpine.js). The header cart icon listens to this event and updates its count badge reactively.

### 17.2 Order Confirmation Security

After a successful order, the Order ID is stored in the session under `last_order_id`. The confirmation page at `/orders/{id}/confirmation` validates that the session value matches the URL parameter before displaying the page. Mismatch → redirect to homepage.

### 17.3 Authentication Sessions

| Guard | Model | Used For |
|-------|-------|---------|
| `web` | `User` | Customer storefront authentication |
| `admin` | `Admin` | Merchant admin panel authentication (Filament) |
| `super-admin` | `SuperAdmin` | Platform Super Admin authentication (Filament, central domain) |

---

## 18. Security Requirements

| Requirement | Implementation |
|-------------|---------------|
| Multi-tenant isolation | Each tenant has its own database; no cross-tenant queries |
| Admin panel access control | Merchant admin only accessible by `Admin` model; Super Admin only by `SuperAdmin` model |
| Suspension enforcement | `CheckTenantNotSuspended` middleware on all tenant routes |
| CSRF protection | Laravel's default CSRF token on all forms |
| Password hashing | Bcrypt (Laravel default) |
| Payment PCI compliance | Stripe Payment Element (card data never touches the platform server) |
| API authentication | HMAC signature verification (`EcwHmacMiddleware`) on all provisioning API routes |
| Order confirmation security | Session-based `last_order_id` check prevents enumeration of other customers' confirmations |
| Central domain enforcement | Super Admin and API routes reject requests from tenant subdomains (`EnsureCentralDomain`) |
| Magic link security | Magic login links are time-limited and signed; used only by platform operators |
| SSL/TLS | All subdomains and the central domain must be served over HTTPS |
| File upload validation | Product images validated as image type; stored on the server filesystem via Spatie MediaLibrary |

---

## 19. Validation Rules

### 19.1 Checkout (Livewire `CheckoutWizard`)

| Field | Rule |
|-------|------|
| `contact_name` | Required |
| `contact_email` | Required, valid email |
| `bill_address1` | Required |
| `bill_city` | Required |
| `bill_province` | Required |
| `bill_postal_code` | Required |
| `bill_country` | Required |
| `shipping_method_id` | Required |
| `ship_name` | Required if `ship_same_as_billing = false` |
| `ship_address1` | Required if `ship_same_as_billing = false` |
| `ship_city` | Required if `ship_same_as_billing = false` |
| `ship_province` | Required if `ship_same_as_billing = false` |
| `ship_postal_code` | Required if `ship_same_as_billing = false` |
| `ship_country` | Required if `ship_same_as_billing = false` |

### 19.2 Product Form (Filament)

| Field | Rule |
|-------|------|
| `name` | Required, max 255 |
| `slug` | Required, max 255, unique (scoped to tenant, ignoring current record on edit) |
| `price` | Required, numeric, min 0 |
| `sale_price` | Nullable, numeric, min 0, must be strictly less than `price` |
| `units_in_stock` | Nullable, integer, min 0 |
| `weight` | Nullable, numeric, min 0 |
| `variantGroups[].name` | Required, max 100 |
| `variantGroups[].options[].name` | Required, max 100 |

### 19.3 Plan Form (Super Admin)

| Field | Rule |
|-------|------|
| `name` | Required, max 255 |
| `price` | Required, numeric, min 0, step 0.01 |
| `stripe_price_id` | Nullable, max 255 |

### 19.4 Customer Auth

| Form | Field | Rule |
|------|-------|------|
| Login | `email` | Required, email |
| Login | `password` | Required |
| Register | `name` | Required |
| Register | `email` | Required, email, unique in `users` table |
| Register | `password` | Required, confirmed (must match `password_confirmation`) |
| Profile Update | `name` | Required |
| Profile Update | `email` | Required, email |
| Profile Update | `password` | Nullable; if present, must be confirmed |

---

## 20. Error Handling & User Feedback

### 20.1 Alert / Notification Styles

| Type | CSS Classes | When Used |
|------|------------|-----------|
| Success | `bg-green-50 border border-green-200 text-green-700` | Profile saved, order confirmed |
| Error | `bg-red-50 border border-red-200 text-red-700` | Validation failures, payment errors |
| Warning | `bg-amber-50 border border-amber-200 text-yellow-700` | Payment pending, low stock |
| Info | `bg-blue-50 border border-blue-200 text-blue-700` | Note to buyer, informational messages |

### 20.2 Inline Validation Errors

Form fields display errors directly below the input in red text using Laravel's `@error('field')` directive. Fields with errors have their border changed to `border-red-400`.

### 20.3 Focus States

All interactive inputs use `focus:ring-2 focus:ring-blue-500` for keyboard accessibility and visible focus indication.

---

## 21. Email Notifications

### 21.1 Store Welcome Email

**Class:** `StoreWelcomeMail`  
**Template:** `resources/views/emails/store-welcome.blade.php`  
**Trigger:** Sent to the merchant admin email immediately after successful tenant provisioning.

**Contents:**
- Store name
- Store URL (`{slug}.ecstores.ca`)
- Admin panel URL
- Admin login credentials or password-setup link
- Getting-started instructions / next steps

---

## 22. Future Phases (Phase 4)

The following features are referenced in code comments and architecture but are **not in the current scope**. They are documented here to inform future planning.

| Feature | Description |
|---------|-------------|
| **Automated subscription billing** | Stripe Cashier subscriptions using `Plan.stripe_price_id`; monthly automated charges to merchants |
| **Merchant payment history** | Dashboard page showing merchant billing transactions |
| **Analytics dashboard** | Revenue reports, product sales analytics, customer statistics for merchants |
| **Order refund processing** | Merchant-initiated Stripe refunds from the Orders admin |
| **Plan enforcement** | Enforce product limits per plan (e.g., Starter: 50 products max); gating Supplier access to Pro plan |
| **Multi-language / i18n** | Storefront and admin in multiple languages |
| **Advanced reporting** | Super Admin revenue analytics across all tenants |

---

## 23. Out of Scope

The following items are explicitly **not included** in the current platform unless separately agreed upon:

- Native mobile applications (iOS / Android)
- A merchant-facing analytics dashboard beyond order listing *(Phase 4)*
- Automated recurring subscription billing via Stripe *(Phase 4)*
- Marketplace features (multiple merchants on one storefront)
- Physical point-of-sale (POS) integration
- Tax calculation service integration (e.g., Avalara, TaxJar) — tax rate is a single flat percentage configured per store
- Product import / export (CSV or otherwise)
- Abandoned cart recovery emails
- Discount codes / coupon system
- Loyalty / rewards program
- Digital / downloadable products
- Subscription-based products (recurring orders for customers)
- A/B testing
- Live chat widget
- Multi-language storefront *(Phase 4)*
- Custom domain mapping (e.g., `shop.acme.com` → store) — stores are subdomain-only in current scope
- Bulk order management / CSV export
- Integration with third-party shipping carriers (UPS, Purolator, Canada Post rates API)

---

## 24. Sign-Off

This document represents the agreed scope, architecture, data model, and functional requirements for the ECStores SaaS platform. Any changes requested after sign-off that fall outside of the scope defined herein will be treated as change requests and may affect the project timeline and cost.

---

**Client / Owner Sign-Off**

By signing below, the undersigned confirms that all requirements described in this document accurately represent the intended platform, and authorizes the project to proceed on this basis.

|  |  |
|--|--|
| **Authorized Signatory** | _________________________ |
| **Name (Print)** | _________________________ |
| **Title** | _________________________ |
| **Date** | _________________________ |

---

**Agency Sign-Off**

|  |  |
|--|--|
| **Prepared By** | Denis Gallant, EastCoast WebCraft |
| **Date** | May 18, 2026 |
| **Version** | 1.0 |

---

*EastCoast WebCraft — info@eastcoastwebcraft.ca — Nova Scotia, Canada*
