# ECStores — Super-Admin Guide
### For Denis Gallant (Platform Owner)

Last updated: 2026-05-07  
Covers: Phases 0–6 (complete).

---

## Table of Contents

1. [How the Platform Works](#1-how-the-platform-works)
2. [Tech Stack Reference](#2-tech-stack-reference)
3. [Local Development Environment](#3-local-development-environment)
4. [The Two-Database Architecture](#4-the-two-database-architecture)
5. [Subdomain Routing](#5-subdomain-routing)
6. [Your Super-Admin Panel](#6-your-super-admin-panel)
7. [Provisioning a New Tenant](#7-provisioning-a-new-tenant)
8. [Suspending and Reactivating Tenants](#8-suspending-and-reactivating-tenants)
9. [Managing Plans](#9-managing-plans)
10. [Routine Maintenance](#10-routine-maintenance)
11. [Key Artisan Commands](#11-key-artisan-commands)
12. [Troubleshooting](#12-troubleshooting)
13. [Production Deployment](#13-production-deployment-placeholder)
14. [Stripe Billing — Cashier (You Billing Merchants)](#14-stripe-billing--cashier)
15. [Stripe Connect (Merchants Billing Their Customers)](#15-stripe-connect)
16. [Branding System](#16-branding-system)

---

## 1. How the Platform Works

ECStores is a multi-tenant SaaS platform. You (Denis) run the platform. Merchants sign up and get their own online store. Each store is completely isolated from every other store.

**The three layers:**

| Layer | Who uses it | URL pattern |
|---|---|---|
| Super-Admin Panel | You (Denis) | `ecstores.ca/super` |
| Tenant Admin Panel | Each store owner | `storename.ecstores.ca/admin` |
| Public Storefront | Each store's customers | `storename.ecstores.ca` |

**The money flow (once Stripe is live):**
- You charge merchants a monthly subscription via Stripe Cashier
- Merchants charge their customers via Stripe Connect Express
- Customer payments go directly to the merchant's bank account; your platform takes nothing from those transactions
- Your revenue is entirely the monthly subscription fees

---

## 2. Tech Stack Reference

| Component | Technology | Version | Purpose |
|---|---|---|---|
| Framework | Laravel | 13 | Application foundation |
| Multi-tenancy | stancl/tenancy | v3 | Database-per-tenant isolation |
| Admin UI | Filament | v5 | Both admin panels |
| Storefront frontend | Blade + Livewire | v4 | Reactive storefront without JavaScript frameworks |
| Tenant billing | laravel/cashier | v16 | You bill merchants monthly via Stripe |
| Merchant payments | Stripe Connect Express | live | Merchants accept customer payments |
| Images | spatie/laravel-medialibrary | installed | Product image uploads |
| Mail | Resend | (Phase 6) | Transactional email |
| Dev tools | Laravel Telescope | installed | Query/mail/job inspector at `/telescope` |
| Language | PHP | 8.3 | |
| Database | MySQL | 8.4 | |
| Web server | Apache (via Laragon) | — | Local dev |

---

## 3. Local Development Environment

**Software stack:** Laragon (bundles Apache, MySQL, PHP, Composer) on Windows 11.

**Project location:** `D:\#WORK\EC_WebCraft\~PRIVATE_HTML\ecstores`

**Apache access path:** `C:\laragon\www\ecstores` (Windows junction — do not delete)

> The project path contains `#` which Apache treats as a comment. The junction at `C:\laragon\www\ecstores` gives Apache a clean path to serve from. Never rename the `#WORK` parent folder.

**Starting the environment:**
1. Launch Laragon
2. Click **Start All** — Apache and MySQL indicators go green
3. Visit `http://ecstores.test` to confirm it's running

**Stopping:** Click **Stop All** in Laragon. Always stop cleanly — don't just close the window.

**PHP and Composer in PATH:**
```
C:\laragon\bin\php\php-8.3.30-Win32-vs16-x64
C:\laragon\bin\composer
```
If you reinstall Laragon and the PHP version changes, update these PATH entries in Windows environment variables.

**Running artisan commands:** Open PowerShell, `cd` to the project directory, then run `php artisan ...`. Laragon must be running (MySQL must be up) for any database commands.

---

## 4. The Two-Database Architecture

This is the most important architectural concept in the platform.

**You have two types of databases:**

### Landlord database: `ecstores_central`

This is YOUR database. It contains:

| Table | What it stores |
|---|---|
| `tenants` | One row per merchant store (id/slug, name, email, suspended_at) |
| `domains` | Subdomain mapping — which subdomain belongs to which tenant |
| `super_admins` | Your login credentials |
| `plans` | Subscription plan definitions |
| `subscriptions` | Stripe subscription records per tenant (managed by Cashier) |
| `subscription_items` | Line items within each subscription |

**One tenant database per merchant: `tenant_{slug}`**

For example: `tenant_acme`, `tenant_beta`. Each contains:

| Table | What it stores |
|---|---|
| `admins` | The merchant's admin login |
| `users` | That store's customer accounts |
| `products` | That store's product catalogue |
| `orders` | That store's orders |
| `site_settings` | That store's branding, currency, policies |
| `shipping_methods` | That store's shipping rates |
| ... | All other store-specific data |

**Why this matters:**
- A bug can never leak Tenant A's orders to Tenant B — they're in separate databases
- You never need `WHERE tenant_id = ?` clauses scattered everywhere
- Dropping a tenant is as simple as dropping their database

**How the switch happens:**
When a request comes in to `acme.ecstores.ca`, the `InitializeTenancyBySubdomain` middleware reads the subdomain, looks up `acme` in `ecstores_central.domains`, then switches the active database connection to `tenant_acme` for the rest of that request. All Eloquent models automatically use the switched connection.

---

## 5. Subdomain Routing

**How subdomains are resolved:**

1. Browser requests `acme.ecstores.test`
2. Hosts file (Windows) maps `acme.ecstores.test` → `127.0.0.1`
3. Apache serves from `C:\laragon\www\ecstores\public` (via the `*.ecstores.test` virtual host wildcard)
4. Laravel's `InitializeTenancyBySubdomain` middleware reads `acme` from the hostname
5. Looks up `SELECT * FROM domains WHERE domain = 'acme'` in `ecstores_central`
6. Finds `tenant_id = 'acme'`, switches DB to `tenant_acme`
7. Request continues normally with the tenant's data

**The central domain (`ecstores.test`) is special:**
- It does NOT go through tenant initialization
- It's used for your super-admin panel at `ecstores.test/super`
- Accessing `ecstores.test` directly hits the landlord DB

**Adding a new subdomain locally:**
Every new tenant subdomain needs a hosts file entry. Open `C:\Windows\System32\drivers\etc\hosts` as Administrator and add:
```
127.0.0.1 newstore.ecstores.test
```

**In production:**
A wildcard DNS record (`*.ecstores.ca → your server IP`) handles all tenant subdomains automatically. No individual DNS records needed per tenant.

---

## 6. Your Super-Admin Panel

**URL:** `http://ecstores.test/super` (local) / `https://ecstores.ca/super` (production)

**Login:** Your `super_admins` credentials (separate from any store's admin account).

**What's in the panel:**

### Dashboard
Shows the **Platform Overview widget**: Total Tenants, Active Stores, Suspended count. Updates live based on the database.

### Tenants (Platform group)
- Full list of all provisioned stores
- Columns: Subdomain, Store Name, Admin Email, Domain, Status (Active/Suspended), Created date
- Row actions: **Suspend**, **Reactivate**, **Admin Panel** (opens tenant's admin in new tab)
- Header action: **Provision New Tenant** (see Section 7)

### Plans (Platform group)
- Subscription plan definitions
- Fields: Name, Price/month, Stripe Price ID (for Phase 4), Features (key-value list), Active toggle
- Standard CRUD — create, edit, delete

---

## 7. Provisioning a New Tenant

This is the full workflow for onboarding a new merchant. The provisioning button handles everything automatically.

### Step 1 — Add the subdomain to your hosts file (local dev only)

Open `C:\Windows\System32\drivers\etc\hosts` as Administrator:
```
127.0.0.1 merchantname.ecstores.test
```

In production this step is not needed (wildcard DNS handles it).

### Step 2 — Use the Provision New Tenant form

1. Visit `http://ecstores.test/super/tenants`
2. Click **Provision New Tenant**
3. Fill in the form:
   - **Store Name** — what appears in the store's header (e.g. `Acme Hardware`)
   - **Subdomain** — lowercase letters, numbers, and hyphens only (e.g. `acme`) — this becomes `acme.ecstores.ca`
   - **Owner Name** — the merchant's full name
   - **Owner Email** — the merchant's email (used to log into their admin panel)
   - **Temporary Password** — they'll change this on first login
4. Click **Provision New Tenant**

### What happens automatically:

| Step | What runs |
|---|---|
| 1 | `Tenant::create(['id' => 'acme', 'name' => '...', 'email' => '...'])` — creates tenant record + auto-creates `tenant_acme` database |
| 2 | Domain record created: `acme` → tenant `acme` |
| 3 | `php artisan tenants:migrate --tenants=acme` — runs all 11 tenant migrations, creating 25 tables |
| 4 | Tenant context initialized, switches to `tenant_acme` DB |
| 5 | `site_settings` row created with company name and `$` currency |
| 6 | Default shipping method created: Standard Shipping at $10.00 |
| 7 | Admin user created with the email and password you entered |
| 8 | Tenant context ended, switches back to landlord DB |

**Expected result:** Success toast notification. Tenant appears in list as Active.

### Step 3 — Send the merchant their login details

Give them:
- Their admin panel URL: `https://merchantname.ecstores.ca/admin`
- Their email and temporary password
- A link to the Tenant Guide

### Step 4 — They should do immediately:

1. Log in and change their password (Profile in the admin top-right)
2. Go to **Site Settings** and fill in their store name, currency, policies
3. Go to **Branding Studio** and set their colours, fonts, and upload a logo
4. Go to **Stripe Connect** and complete the Stripe Express onboarding to accept payments
5. Add their products
6. Add or adjust their shipping methods

---

## 8. Suspending and Reactivating Tenants

**When to suspend:** Non-payment, terms of service violation, merchant request.

**To suspend:**
1. Visit `http://ecstores.test/super/tenants`
2. Click **Suspend** on the tenant row
3. Confirm

**What happens:** `suspended_at` timestamp is set on the tenant record. Every request to that tenant's storefront returns a 503 "Store Temporarily Unavailable" page. The merchant's admin panel continues to work normally (so they can still log in and see their data).

**To reactivate:**
1. Click **Reactivate** on the suspended tenant row
2. Confirm

**What happens:** `suspended_at` is cleared. Storefront returns to normal immediately on the next request.

> **Note:** Suspension only affects the public storefront. The admin panel is not blocked. If you need to fully lock out a merchant's admin access, you would need to manually delete or deactivate their admin user in the tenant DB.

---

## 9. Managing Plans

Plans define what subscription tiers you offer merchants (e.g. Starter $29/month, Pro $79/month).

**To create a plan:**
1. Visit `http://ecstores.test/super/plans`
2. Click **New Plan**
3. Fill in name, price, and optional features
4. Leave Stripe Price ID blank until Phase 4

**Features field:** A key-value list displayed on your pricing page (future). Example:
- `Products: Unlimited`
- `Storage: 5 GB`
- `Support: Email`

**Stripe Price ID:** Create a recurring price in your Stripe Dashboard → Products → [plan product] → Add price, then paste the `price_xxxxx` ID here. This links the plan to Cashier billing so `Activate Subscription` in the Tenants list works.

---

## 10. Routine Maintenance

### When you update the platform code

If a new phase adds tenant migrations (new tables or columns in the tenant DB):

```powershell
php artisan tenants:migrate
```

This runs all pending tenant migrations across every tenant's database simultaneously. Safe to run at any time — it only applies new migrations, never re-runs completed ones.

If a new phase adds landlord migrations (new tables in `ecstores_central`):

```powershell
php artisan migrate
```

### Checking which migrations have run

```powershell
php artisan migrate:status
```

For tenant migrations, run with context:
```php
// In tinker:
tenancy()->initialize(App\Models\Tenant::find('acme'));
Artisan::call('migrate:status');
tenancy()->end();
```

### Resetting a tenant's password via tinker

```powershell
php artisan tinker
```
```php
tenancy()->initialize(App\Models\Tenant::find('acme'));
App\Models\Admin::where('email', 'owner@example.com')->first()->update(['password' => 'newpassword']);
tenancy()->end();
exit
```

### Viewing a tenant's data directly

```php
// In tinker:
tenancy()->initialize(App\Models\Tenant::find('acme'));
App\Models\Order::count();        // how many orders
App\Models\Product::count();      // how many products
App\Models\User::count();         // how many customer accounts
tenancy()->end();
exit
```

### Storage permissions (Windows — run if you see "Access is denied" errors)

```powershell
& icacls "d:\#WORK\EC_WebCraft\~PRIVATE_HTML\ecstores\storage" /grant "Everyone:(OI)(CI)F" /T
php artisan view:clear
```

---

## 11. Key Artisan Commands

| Command | What it does |
|---|---|
| `php artisan migrate` | Run pending landlord (central) migrations |
| `php artisan tenants:migrate` | Run pending migrations on ALL tenant databases |
| `php artisan tenants:migrate --tenants=acme` | Run pending migrations on one specific tenant |
| `php artisan migrate:status` | Show which migrations have run (landlord) |
| `php artisan optimize:clear` | Clear all caches (config, routes, views, compiled) |
| `php artisan view:clear` | Clear compiled Blade view cache |
| `php artisan config:clear` | Clear config cache |
| `php artisan route:list` | List all registered routes |
| `php artisan tinker` | Open the interactive REPL |
| `php artisan storage:link` | Create the public/storage symlink (run once per machine) |
| `php artisan serve` | Start a dev server on port 8000 (alternative to Laragon) |

---

## 12. Troubleshooting

### "Store Temporarily Unavailable" when visiting a storefront

**Cause:** That tenant is suspended.
**Fix:** Go to `ecstores.test/super/tenants` and click **Reactivate**.

### 500 error on a tenant subdomain after provisioning

**Possible causes:**
1. The subdomain isn't in your hosts file — add `127.0.0.1 newstore.ecstores.test`
2. The tenant migrations didn't run — in tinker: `Artisan::call('tenants:migrate', ['--tenants' => ['newstore']])`
3. The tenant database exists but is empty — re-run migrations as above

### "Access is denied" / `rename()` error on any page

**Cause:** Windows file permissions on the storage folder.
**Fix:**
```powershell
& icacls "d:\#WORK\EC_WebCraft\~PRIVATE_HTML\ecstores\storage" /grant "Everyone:(OI)(CI)F" /T
php artisan view:clear
```

### Admin panel login fails (tenant panel)

**Cause:** Wrong credentials, or admin user doesn't exist.
**Fix:** Reset via tinker (see Section 10).

### Provisioning fails with "Field 'id' doesn't have a default value"

**Cause:** `Tenant::getCustomColumns()` was overridden without merging the parent's `['id']`. This is already fixed in the codebase — if it reappears, check that `app/Models/Tenant.php` uses `array_merge(parent::getCustomColumns(), [...])`.

### Product images show "Loading..." in the admin panel

**Cause:** The Filament FileUpload component's `fetchFileInformation` is set incorrectly.
**Fix:** Ensure `getUploadedFileUsing()` in `ProductResource` has `->fetchFileInformation(false)` and uses a relative `/storage/` URL (not an absolute URL with the tenant subdomain).

### Super-admin panel shows 403 when accessed from a tenant subdomain

**Cause:** This is correct behaviour — `EnsureCentralDomain` middleware blocks `acme.ecstores.test/super`. Always access the super-admin from `ecstores.test/super`.

### Git remote points to Laravel framework repo

**Reminder:** The `origin` remote still points to the Laravel framework repository from when the project was created. Before pushing to your own GitHub:
```powershell
git remote set-url origin https://github.com/yourusername/ecstores.git
git push -u origin main
```

---

## 13. Production Deployment (Placeholder)

*To be completed when deploying to InMotion VPS.*

Key items that will need to be configured:
- PHP 8.3, MySQL 8.4, required PHP extensions (zip, pdo_mysql, mbstring, bcmath, gd)
- Wildcard SSL certificate for `*.ecstores.ca`
- Wildcard DNS record: `*.ecstores.ca → server IP`
- `.env` production values: `APP_ENV=production`, `APP_DEBUG=false`, correct `APP_URL`, `SESSION_DOMAIN`
- Storage: S3 or DigitalOcean Spaces for product images (instead of local disk)
- Queue worker for mail jobs
- Laravel Telescope disabled (`TELESCOPE_ENABLED=false`)
- Run `php artisan optimize` after each deployment

---

## 14. Stripe Billing — Cashier

This covers **you billing merchants** a monthly subscription fee. Implemented via `laravel/cashier`.

### How it works

- The `Tenant` model has the `Billable` trait — each tenant is a Stripe customer
- When a tenant is provisioned, a Stripe customer record is automatically created via `$tenant->createAsStripeCustomer()`
- You activate a subscription from the Tenants list in the super-admin panel
- Cashier handles subscription state, renewal, and cancellation via Stripe webhooks

### Activating a subscription for a tenant

1. Visit `http://ecstores.test/super/tenants`
2. Click the three-dot menu (⋮) on a tenant row
3. Click **Activate Subscription**
4. The tenant gets a 30-day trial — visible as a "Trial" badge in the Billing Status column
5. After the trial Stripe automatically begins charging the recurring price

### Changing a tenant's plan

1. Three-dot menu → **Change Plan**
2. Select the new plan from the dropdown
3. Cashier swaps the Stripe subscription to the new price immediately

### Viewing subscription revenue

Go to your Stripe Dashboard at `dashboard.stripe.com`. The Home tab shows gross volume and subscription MRR. For per-tenant breakdowns, go to **Customers** and find the tenant by name or email.

### Webhook (automatic subscription events)

The webhook endpoint is `POST /stripe/webhook`. Cashier handles:
- `invoice.payment_succeeded` — marks subscription active
- `invoice.payment_failed` — Stripe automatically retries and eventually cancels
- `customer.subscription.deleted` — clears subscription state on the tenant

The webhook secret is stored in `.env` as `STRIPE_WEBHOOK_SECRET`. When testing locally run:
```powershell
stripe listen --forward-to ecstores.test/stripe/webhook
```

### Suspending a non-paying tenant

Cashier webhooks don't auto-suspend — you decide the policy. When a merchant's subscription lapses:
1. Stripe marks the subscription cancelled (you'll see this in the dashboard)
2. Manually suspend the tenant in the super-admin panel (Section 8)
3. Contact them to resolve payment

---

## 15. Stripe Connect

This covers **merchants accepting customer payments**. Each merchant has their own Stripe Express account. Customer checkout money goes directly to the merchant; your platform collects a 2% application fee automatically.

### How it works

- At checkout a `PaymentIntent` is created on your platform Stripe account with `transfer_data.destination = merchant_account_id`
- Stripe automatically transfers the funds to the merchant's Express account minus your 2% fee
- You see transfers in your Stripe Dashboard → Transactions → Transfers
- The merchant sees payments in their own Stripe Express Dashboard

### Viewing your platform fee income

Stripe Dashboard → Transactions → **Collected Fees**. Each entry shows the application fee collected from that merchant's payment.

### Checking a merchant's Connect status

1. Log into the tenant's admin panel (or use **Admin Panel** row action in super-admin)
2. Go to **Settings → Stripe Connect**
3. Status shows: Account ID, Charges (Enabled/Pending), Payouts (Enabled/Pending)

**Charges Enabled** = the merchant can accept customer payments  
**Payouts Enabled** = Stripe can transfer money to their bank account

Charges can be enabled before Payouts. A merchant can take payments as soon as Charges is enabled; Payouts complete once they verify their bank account with Stripe.

### If a merchant's onboarding gets stuck

1. Log into their admin panel → Stripe Connect
2. Click **Continue Onboarding** — this generates a fresh Stripe onboarding link
3. If that fails, retrieve a new link manually via Stripe Dashboard → Connect → Accounts → find their `acct_xxxxx` → Send onboarding link

### Connect account in test mode vs live mode

In test mode (`STRIPE_SECRET=sk_test_...`), Connect payments are test transactions only. When you're ready to go live:
1. Complete Stripe's platform activation (they review your business — can take a few days)
2. Swap `.env` keys to live keys (`sk_live_...`, `pk_live_...`)
3. Merchants must re-complete Stripe onboarding on live mode (test accounts don't carry over)

---

## 16. Branding System

Each tenant store can be fully branded — colours, fonts, logo, favicon, and banner image — without touching any code.

### How it works

Every storefront page reads `site_settings` from the tenant's database and outputs CSS custom properties in the `<head>`:

```css
:root {
  --color-primary:   #3B82F6;   /* buttons, links, accents */
  --color-secondary: #1E40AF;
  --color-accent:    #F59E0B;
  --color-bg:        #FFFFFF;
  --color-text:      #111827;
  --font-heading:    'Inter', system-ui;
  --font-body:       'Inter', system-ui;
  --btn-radius:      0.5rem;    /* 9999px for pill, 0px for sharp */
}
```

Selected Google Fonts are loaded via a `<link>` tag. Logo, favicon, and banner are served from `/storage/branding/`.

### The Branding Studio (tenant admin panel)

Merchants access it at **Settings → Branding Studio**. It has:
- **Colours** — five colour pickers (primary, secondary, accent, background, text)
- **Typography** — dropdown to choose heading and body font from a curated Google Fonts list
- **Button Style** — rounded / pill / sharp radio buttons
- **Logo & Images** — file uploads for logo (replaces the store name text in the header), favicon, and homepage banner
- **Custom CSS** — a textarea for advanced overrides; output last so it wins over everything

Changes take effect immediately on the next page load — no cache to clear, no deployment needed.

### Troubleshooting branding

**Colours not changing:** Confirm the merchant clicked **Save Branding** and hard-refreshed (`Ctrl+Shift+R`). Tailwind CDN caches aggressively.

**Font not loading:** The font name must match exactly one of the curated list options. Fonts are loaded from Google Fonts — requires internet access. System fonts (Georgia, system-ui) don't need a Google Fonts load.

**Logo not showing:** Confirm `php artisan storage:link` has been run on the server. The logo is served via `/storage/branding/...`.

---

*This guide is a living document. Update it as new phases are completed.*  
*For questions about the codebase, see SETUP.md for step-by-step environment setup and TEST_PLAN.md for feature verification.*
