# ECStores — AI Reference Guide

**Before working on this project, read the master reference document:**

> `../eastcoastwebcraft.ca/documents/ECWC_CLAUDE_NOTES.md`

That document contains: all working rules (including Rules 1–14), shared environment facts, ECW ↔ ECStores integration details, database schemas, deployment process, staging environment, Git workflow, SFTP overwrite warnings, and the known issues log. The ECStores-specific content below is in addition to — not instead of — that document.

---

This is a living reference document for AI assistants working on this project with Denis Gallant (East Coast WebCraft). Update it whenever you discover new gotchas, complete major features, or change the infrastructure.

---

## About Denis

- Raw PHP developer who is new to Laravel. Explain Laravel concepts clearly; don't assume Eloquent/Artisan familiarity.
- Prefers clean, working implementations over temporary patches.
- Has SSH access to the production server (inMotion shared hosting). He runs commands himself when given clear copy-paste instructions.
- Works in VS Code on Windows 11. The local project is at `d:\#WORK\EC_WebCraft\~PRIVATE_HTML\ecstores\`.
- Ask Denis directly when uncertain about server state, past tasks, or intent — he is available as a live resource during sessions.

---

## What ECStores Is

ECStores is a **multi-tenant SaaS e-commerce platform** that Denis sells through his agency, EastCoast WebCraft (ECW). Each tenant gets their own subdomain (`{slug}.ecstores.ca`) with a fully isolated database and a white-labelled storefront + merchant admin panel.

Denis bills clients through ECW's existing invoice + Stripe system. ECStores does **not** run its own subscription billing for Denis's clients — tenants provisioned via ECW have `manually_billed = true`, which bypasses the Stripe subscription gate; Denis suspends the store manually if a client doesn't pay.

The companion ECW codebase is at `d:\#WORK\EC_WebCraft\~PRIVATE_HTML\eastcoastwebcraft.ca\`.

---

## Tech Stack

| Layer | Package | Version |
|---|---|---|
| Framework | Laravel | ^13.7 |
| Admin UI | Filament | ^5.6 |
| Reactive components | Livewire | ^4.3 |
| Multi-tenancy | stancl/tenancy | ^3.10 |
| Payments | laravel/cashier (Stripe) | ^16.5 |
| Media | spatie/laravel-medialibrary | ^11.22 |
| PHP requirement | PHP | ^8.3 |

---

## Project Structure — Key Files

```
ecstores/
├── app/
│   ├── Filament/
│   │   ├── Pages/                        # Custom Filament pages (tenant admin)
│   │   │   ├── BrandingStudio.php        # Logo, colours, fonts
│   │   │   ├── ManageSiteSettings.php    # Store name, contact, currency, tax, shipping
│   │   │   └── ManageStripeConnect.php   # Stripe Connect onboarding UI
│   │   ├── Resources/                    # Filament CRUD resources (tenant admin)
│   │   │   ├── Categories/
│   │   │   ├── Orders/
│   │   │   │   ├── Schemas/OrderForm.php         # Order edit form (status, shipped date, refund)
│   │   │   │   └── Tables/OrdersTable.php        # Orders list + refund action
│   │   │   ├── Products/
│   │   │   │   ├── Schemas/ProductForm.php
│   │   │   │   └── Tables/ProductsTable.php      # Bulk actions, supplier/category filters
│   │   │   ├── ShippingMethods/
│   │   │   └── Suppliers/
│   │   ├── SuperAdmin/                   # Filament panel for Denis (ecstores.ca/super)
│   │   │   ├── Resources/Plans/          # Subscription plans
│   │   │   └── Resources/Tenants/        # Tenant list + suspend/reactivate
│   │   └── Widgets/
│   │       └── StoreOverviewWidget.php   # Dashboard: revenue, orders, awaiting, customers
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Api/                      # ECW → ECStores API (HMAC-authenticated)
│   │   │   │   ├── MagicLinkController.php
│   │   │   │   ├── ProvisionController.php
│   │   │   │   └── TenantStatusController.php
│   │   │   ├── Auth/
│   │   │   │   ├── ExternalLoginController.php   # Magic link token → session
│   │   │   │   ├── LoginController.php
│   │   │   │   ├── PasswordResetController.php   # Forgot/reset password flow
│   │   │   │   └── RegisterController.php
│   │   │   └── Storefront/
│   │   │       ├── AccountController.php
│   │   │       ├── OrderController.php
│   │   │       ├── ProductController.php
│   │   │       ├── StripeConnectController.php
│   │   │       └── WishlistController.php
│   │   └── Middleware/
│   │       ├── CheckTenantNotSuspended.php
│   │       ├── CheckTenantSubscriptionActive.php  # Bypassed when manually_billed = true
│   │       ├── EcwHmacMiddleware.php              # Validates X-ECW-Signature header
│   │       ├── EnsureCentralDomain.php
│   │       └── InitializeTenancyBySubdomainIfNotCentral.php
│   ├── Livewire/
│   │   ├── Cart/CartPage.php
│   │   ├── Checkout/CheckoutWizard.php   # Multi-step checkout (address → shipping → payment)
│   │   ├── ProductVariantSelector.php
│   │   └── WishlistToggle.php            # Heart icon; dispatches 'wishlist-removed' browser event
│   ├── Models/
│   │   ├── Admin.php          # Tenant admin user (admins guard)
│   │   ├── Category.php
│   │   ├── Order.php
│   │   ├── OrderItem.php
│   │   ├── Plan.php           # Central DB — subscription plans
│   │   ├── Product.php
│   │   ├── ProductVariantCombination.php
│   │   ├── ProductVariantGroup.php
│   │   ├── ProductVariantOption.php
│   │   ├── ShippingMethod.php
│   │   ├── SiteSettings.php   # Tenant settings (logo, stripe, currency, tax, etc.)
│   │   ├── SuperAdmin.php     # Denis's super-admin account
│   │   ├── Supplier.php
│   │   ├── Tenant.php         # stancl/tenancy Tenant model; has manually_billed bool
│   │   ├── User.php           # Storefront customer
│   │   ├── UserProfile.php
│   │   └── Wishlist.php
│   ├── Providers/
│   │   ├── Filament/AdminPanelProvider.php       # Tenant admin panel (/admin)
│   │   ├── Filament/SuperAdminPanelProvider.php  # Denis's panel (/super)
│   │   └── TenancyServiceProvider.php
│   ├── Mail/
│   │   └── StoreWelcomeMail.php           # Sent on provisioning; contains store URL + temp credentials
│   └── Services/
│       ├── CartService.php
│       └── TenantProvisioningService.php   # Creates tenant, runs migrations, seeds defaults, sends welcome email
├── documents/                # Project documents (user stories, test cases)
│   ├── user-stories.csv
│   └── test-cases.csv
├── resources/views/
│   ├── auth/                 # Guest auth views (login, register, password reset)
│   │   ├── login.blade.php
│   │   ├── register.blade.php
│   │   ├── forgot-password.blade.php
│   │   └── reset-password.blade.php
│   ├── emails/
│   │   └── store-welcome.blade.php        # Welcome email sent on tenant provisioning
│   ├── storefront/           # Customer-facing storefront views
│   │   ├── layout.blade.php  # IMPORTANT: has no @stack('scripts') — inline JS only
│   │   ├── wishlist.blade.php
│   │   └── account/orders.blade.php
│   └── ...
└── CLAUDE.md                 # This file
```

---

## Database Architecture

**Two database tiers (stancl/tenancy v3):**

- **Central DB** (`ecstores_central`): Tenants, Plans, SuperAdmin, domains. Managed by central migrations in `database/migrations/`.
- **Tenant DBs** (one per store, e.g., `ecstores_tenant_{slug}`): Products, Orders, Customers, SiteSettings, etc. Managed by tenant migrations in `database/migrations/tenant/` and run via `php artisan tenants:migrate`.

When writing code:
- Central models live in `app/Models/` and use the `central` database connection (default).
- Tenant models also live in `app/Models/` but run against whichever tenant is currently initialized.
- Never query tenant models from a central context without first calling `tenancy()->initialize($tenant)`.

---

## Authentication

| Guard | Model | Who |
|---|---|---|
| `web` (default) | `User` | Storefront customers |
| `admins` | `Admin` | Tenant store owners (Filament `/admin` panel) |
| `super-admin` | `SuperAdmin` | Denis (Filament `/super` panel) |

The `admins` guard is configured in `config/auth.php`. Filament's `AdminPanelProvider` uses `->authGuard('admins')`.

---

## Server & Hosting

**Host:** inMotion Shared Hosting
**Server path:** `~/ecstores/` (i.e., `/home/{username}/ecstores/`)
**Web root:** `~/ecstores/public/` — this IS the web root; `/public/` is NOT part of the URL.
**Current testing domain:** `widget.eastcoastweb.ca` (points to ecstores/public/)
**Production domain (not yet live):** `ecstores.ca` (wildcard `*.ecstores.ca` needed for subdomains)

**SSH access:** Denis has SSH access. He copies and runs commands you provide.

**PHP on the server:**
- Default CLI PHP: 8.1 (too old — do NOT use for artisan commands)
- PHP 8.3 CLI path: `/opt/cpanel/ea-php83/root/usr/bin/php`
- Always prefix artisan commands with the full PHP path: `/opt/cpanel/ea-php83/root/usr/bin/php artisan ...`

**Composer on the server:**
- Path: `/opt/cpanel/composer/bin/composer`
- Full dump-autoload command: `cd ~/ecstores && /opt/cpanel/ea-php83/root/usr/bin/php /opt/cpanel/composer/bin/composer dump-autoload`

---

## Deployment Workflow

Denis uploads changed files via FTP/SFTP (FileZilla or cPanel File Manager). He does **not** use Git deploy on the server.

### After uploading any PHP file (modified existing class):

Clear OPcache so the web server picks up the new file:

```bash
# Create a one-time OPcache clear script
echo '<?php opcache_reset(); echo "OPcache cleared.";' > ~/ecstores/public/opc.php
# Visit https://widget.eastcoastweb.ca/opc.php in a browser
# Then delete it immediately:
rm -f ~/ecstores/public/opc.php
```

> **Important:** The web server (PHP-FPM) and CLI PHP have **separate** OPcache instances. Clearing one does NOT affect the other. The web script clears the web server's OPcache.

### After uploading a NEW PHP class file:

The production autoloader uses an optimized classmap (`--classmap-authoritative`). New class files are invisible until the classmap is regenerated:

```bash
cd ~/ecstores && /opt/cpanel/ea-php83/root/usr/bin/php /opt/cpanel/composer/bin/composer dump-autoload
```

### After adding/changing Filament widgets, resources, or pages:

Filament caches discovered components in `bootstrap/cache/filament/`. This cache overrides runtime discovery and will show stale or missing components even after OPcache is cleared.

```bash
cd ~/ecstores && /opt/cpanel/ea-php83/root/usr/bin/php artisan filament:clear-cached-components
# Or directly:
rm -rf ~/ecstores/bootstrap/cache/filament/
```

### After schema migrations:

```bash
cd ~/ecstores && /opt/cpanel/ea-php83/root/usr/bin/php artisan migrate
# For tenant migrations:
cd ~/ecstores && /opt/cpanel/ea-php83/root/usr/bin/php artisan tenants:migrate
```

### After config/route changes:

```bash
cd ~/ecstores && /opt/cpanel/ea-php83/root/usr/bin/php artisan config:clear
cd ~/ecstores && /opt/cpanel/ea-php83/root/usr/bin/php artisan route:clear
cd ~/ecstores && /opt/cpanel/ea-php83/root/usr/bin/php artisan cache:clear
```

### Rule of thumb:
- Edited an existing file → clear OPcache
- Added a new class file → `composer dump-autoload`
- Added/changed a Filament resource, widget, or page → clear Filament component cache

---

## Known Gotchas & Issues We've Encountered

### Filament v5 specific

**`->form([])` is deprecated — use `->schema([])`**
Actions (including `BulkAction`) use `->schema([])`, not `->form([])`. Using `->form([])` will work but triggers a deprecation notice.

**`Get` and `Set` imports changed in Filament v5**
Import from: `Filament\Schemas\Components\Utilities\Get` and `Filament\Schemas\Components\Utilities\Set`
NOT from `Filament\Forms\Components\...`.

**`BooleanColumn` is deprecated**
Use `IconColumn::make('field')->boolean()` instead.

**Resource file structure**
Filament v5 resources split across multiple files:
- `{Resource}Resource.php` — the resource class (navigation, model binding)
- `Schemas/{Name}Form.php` — form schema
- `Tables/{Name}Table.php` — table columns/filters/actions
- `Pages/` — List, Create, Edit pages

**Filament component cache causes stale UI**
Symptoms: new widget/resource/page doesn't appear after upload, or old deleted content still shows.
Fix: `php artisan filament:clear-cached-components` (see Deployment section).

**`discoverWidgets()` requires cache clear on first use**
Even though `AdminPanelProvider` uses `->discoverWidgets()`, the discovered classes are cached. After adding `StoreOverviewWidget.php`, had to clear `bootstrap/cache/filament/` before it appeared.

### Livewire 3

**Browser events for cross-component communication**
Use `$this->dispatch('event-name', key: value)` in the Livewire component.
Listen with `window.addEventListener('event-name', e => { ... e.detail.key ... })` in JS.

**`@push('scripts')` does not work on storefront pages**
The storefront layout (`resources/views/storefront/layout.blade.php`) has no `@stack('scripts')`.
Write inline JS directly inside `@section('content')` instead.

### Multi-tenancy (stancl/tenancy v3)

**Tenant DB vs Central DB**
Always be aware of which DB context you're in. Calling a tenant model from a central route without initializing tenancy will hit the wrong DB.

**FileUpload in subdomain context**
For file uploads in the Filament admin on a tenant subdomain, use:
```php
->fetchFileInformation(false)
->getUploadedFileUsing(fn (...) => /* relative URL */)
```
Without this, the Livewire file component gets stuck on "Loading / Waiting for size".

### Production server

**Wrong PHP version**
If you run `php artisan` without the full path, it uses PHP 8.1 which is too old for this project. Always use `/opt/cpanel/ea-php83/root/usr/bin/php artisan`.

**`rm` prompts for confirmation**
Use `rm -f` to skip the confirmation prompt. Without `-f`, pasting a follow-up command as the answer to the prompt will run it as a shell command, not as the intended action.

**OPcache URL confusion**
The web root IS `public/` — the URL is `https://widget.eastcoastweb.ca/opc.php`, not `.../public/opc.php`.

**Composer not at `/usr/local/bin/composer`**
Found at `/opt/cpanel/composer/bin/composer`.

### Currency Architecture

`SiteSettings` has two separate currency fields:
- `currency` — lowercase ISO code (`'cad'`, `'usd'`, `'eur'`, `'gbp'`). Used by Stripe (`strtolower($settings->currency)`). Never display this to customers.
- `currency_symbol` — display symbol (`'$'`, `'€'`, `'£'`). Used in all storefront views and Filament money columns.

**Filament money columns** use: `->money(fn () => strtoupper(\App\Models\SiteSettings::current()->currency ?? 'cad'))`
**Storefront views** use: `$settings->currency_symbol ?? '$'`

Do NOT conflate the two. The three-way mismatch (symbol seeded, ISO saved by settings form, ISO needed by Stripe) was a confirmed bug that was fixed 2026-05-14.

### View Composer Coverage

`AppServiceProvider` shares `$settings` via `View::composer(['storefront.*', 'auth.*'], ...)`.
This covers both storefront AND auth views (login, forgot-password, reset-password).
If you add views outside these namespaces that need `$settings`, either extend the composer or pass it explicitly from the controller.

### Stripe

**Refund flow**
Refunds issued from the platform account (no `stripe_account` header needed for Connect destination charges). The `amount_refunded` field on orders accumulates — partial refunds are supported. Status automatically becomes 'refunded' when `amount_refunded >= total`.

**Stripe Connect**
Stripe calls in `StripeConnectController` (`accounts->create`, `accountLinks->create`, `accounts->retrieve`) must be wrapped in try-catch for `\Stripe\Exception\ApiErrorException`. Unhandled, they return 500s with no user feedback.

---

## ECW ↔ ECStores API Integration

The ECW portal (plain PHP) calls ECStores via a REST API authenticated with HMAC-SHA256.

**Shared secret:** Stored as `ECW_API_SECRET` in ECStores `.env` and `ECSTORES_API_SECRET` in ECW `includes/config.php`.

**HMAC pattern:**
```
token = HMAC-SHA256(method + "|" + path + "|" + timestamp + "|" + body, secret)
Headers: X-ECW-Timestamp, X-ECW-Signature
Reject if timestamp > 5 minutes old
```

**ECStores API endpoints** (all under `/api/v1`, middleware `ecw.hmac`):
- `POST /provision` — create a new tenant
- `GET /tenant/{slug}/status` — get tenant status
- `POST /tenant/{slug}/suspend` — suspend a tenant
- `POST /tenant/{slug}/reactivate` — reactivate a tenant
- `POST /magic-link` — generate a signed login URL for the tenant admin

**Magic link flow:**
1. ECW calls `/api/v1/magic-link` with `{ slug, email }`
2. ECStores returns a signed Laravel URL pointing to `GET /admin/external-login?token=...` on the tenant subdomain
3. Tenant's `ExternalLoginController` validates the signed URL, looks up `Admin` by email, creates a session, redirects to `/admin`

---

## Environment Variables (production `.env` — key ones)

```
APP_URL=https://ecstores.ca
SESSION_DOMAIN=.ecstores.ca          # Leading dot covers all subdomains
DB_CONNECTION=mysql
DB_DATABASE=ecstores_central
STRIPE_KEY=pk_live_...
STRIPE_SECRET=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
ECW_API_SECRET=<64-char hex shared with ECW>
```

Tenancy config: `config/tenancy.php` → `central_domains` must include `ecstores.ca`.

---

## Pending Tasks (as of 2026-05-14)

- [ ] Switch Stripe from test keys to live in ECStores `.env` and ECW `includes/config.php`
- [ ] Update `APP_URL` and `SESSION_DOMAIN` in ECStores `.env` to `ecstores.ca`
- [ ] Add `ecstores.ca` to `central_domains` in `config/tenancy.php`
- [ ] Set up wildcard SSL for `*.ecstores.ca`
- [ ] Add Plans (Starter/Growth/Pro) in ECStores super-admin with live Stripe Price IDs
- [ ] Register live Stripe webhook endpoint (`https://ecstores.ca/stripe/webhook`)
- [ ] Re-enable ModSecurity on ECW in cPanel after testing
- [ ] Update `ECSTORES_API_URL` in ECW config from `widget.eastcoastweb.ca` to `ecstores.ca` when domain ready

---

## Current Development Status

**Admin panel (merchant):** Fully functional. Products, categories, suppliers, orders, shipping methods, site settings, branding, Stripe Connect, refunds, dashboard stats. Includes: drag-to-reorder products/categories/shipping methods, out-of-stock filter, refund max validation, sale price validation, tax rate configuration, admin profile/password change page.

**Storefront (customer):** Fully functional. Product browsing (featured section, out-of-stock badges), product detail, variant selection, wishlist (header nav link), cart, checkout (Stripe + tax calculation), order confirmation, order history, account management, password reset flow.

**Multi-tenancy:** Working locally and on shared hosting. Tenant provisioning via API and via super-admin both work. `manually_billed` flag correctly bypasses subscription check. Welcome email sent on provisioning.

**Features completed 2026-05-14 (audit fix pass):**
- Currency architecture: `currency` (ISO/Stripe) vs `currency_symbol` (display) separated
- Tax rate: configurable in admin, calculated and stored at checkout, shown in order summary
- Password reset: full forgot/reset flow with email delivery
- Featured products: dedicated homepage section (hidden when filter active)
- Out-of-stock badges on product listing grid
- Wishlist link in storefront header nav
- Admin password change via Filament profile page
- Welcome email on store provisioning
- Drag-to-reorder: products, categories, shipping methods
- Out-of-stock filter in products admin table
- Refund amount server-side max validation
- Sale price < regular price validation
- Soft deletes on products
- Category bulk-delete warning about uncategorized products
- ECW `_setup/install.php` and `_setup/upgrade.php` deleted from server

**ECW integration:** Architecture designed and partially implemented. ECW portal changes (My Store card, magic link, provisioning on payment) are in the plan but not yet implemented in ECW code.

**Go-live blocker:** ECStores domain (`ecstores.ca`) not yet pointed to the server. Still running under `widget.eastcoastweb.ca` for testing.

---

## Related Projects

- **EastCoast WebCraft site:** `d:\#WORK\EC_WebCraft\~PRIVATE_HTML\eastcoastwebcraft.ca\` — plain PHP + Bootstrap 5 agency site that will integrate with ECStores for client billing and portal.
- **Integration plan:** See the plan file at `C:\Users\Alpha\.claude\plans\this-is-my-business-serene-spring.md` for the full phased ECW ↔ ECStores integration plan including error hardening tasks.
