# ECStores — Deployment & Server Reference

> Server: inMotion Hosting (shared)
> SSH user: n777909
> ECStores root: `/home/n777909/ecstores/`
> Production URL: `*.ecstores.ca` (addon domain on the `eastcoastwebcraft.ca` cPanel account; old `widget.eastcoastweb.ca` test domain retired)

---

> **Primary deploy method:** ongoing releases ship via **Git → GitHub → cPanel** (`.cpanel.yml` deployment). The manual FTP/SSH **upload + cache-clear steps in this document are the fallback / troubleshooting reference** (and for the initial deploy) — not the day-to-day path.
>
> The old WinSCP push (`deploy.bat` / `deploy.winscp`, untracked local files) is **LEGACY** — both files carry a warning header. Use it only as a fallback, and check `.cpanel.yml` for the current steps first.

## What `.cpanel.yml` automates (updated 2026-07-02)

Every cPanel Git deploy runs, in order:

1. **rsync** repo → `/home/n777909/ecstores/`, excluding `.git`, `.gitignore`, `.github`, `.cpanel.yml`, `node_modules`, `.env`, `storage/`, `tests/`, `phpunit.xml`, `*.log`, `database/*.sqlite`. (Gitignored files — `documents/`, `CLAUDE.md`, `_setup/`, `deploy.*` — never enter the git checkout, so they can't ship.)
2. `composer install --no-dev --optimize-autoloader` (server-side; regenerates the classmap, so new class files need no manual `dump-autoload`)
3. `php artisan migrate --force` — **central** migrations
4. `php artisan tenants:migrate --force` — **tenant** migrations, all tenant DBs
5. `php artisan filament:clear-cached-components`
6. `php artisan config:cache`, `route:cache`, `view:cache`

**Still manual after every deploy that changes PHP files:** the OPcache reset (see "edited PHP files aren't taking effect" below — the `opc.php` one-time script). CLI cannot clear the web server's OPcache.

**Verify after deploy:** `tail` the deploy log in cPanel → Git Version Control → Manage → Pull or Deploy; confirm the tenant migrations ran (step 4 lists each tenant).

### One-time setup for the storefront Connect webhook (Stage R1b — audit 2.2)

The new `POST /stripe/connect/webhook` (order reconciliation for direct charges) needs a
**separate** signing secret from the Cashier `/stripe/webhook`:

1. Stripe **test** dashboard → Developers → Webhooks → **Add endpoint** → *Connect* endpoint
   (listening to events from connected accounts) → URL `https://ecstores.ca/stripe/connect/webhook`,
   events **`payment_intent.succeeded`** + **`account.updated`**.
2. Copy the endpoint's **Signing secret** into the server `.env` as
   `STRIPE_CONNECT_WEBHOOK_SECRET=whsec_…` (do NOT reuse `STRIPE_WEBHOOK_SECRET`).
3. New route → delete `bootstrap/cache/routes-v7.php` (or `route:cache`) per the routes checklist below.
4. The `checkout_intents` (2026_07_16_000001) and `coupons.customer_*` (2026_07_16_000002)
   tenant tables are created automatically by step 4 of `.cpanel.yml` (`tenants:migrate`).

Until `STRIPE_CONNECT_WEBHOOK_SECRET` is set the endpoint returns 500 and logs a clear error;
the checkout itself (Livewire path) is unaffected — the webhook is only the reconciliation backstop.

### Stage P-2 — Express → Standard (Connect merchant consumption)

Onboarding moved from the ECStores admin to the ECW client portal; merchants now use their own
**Standard** connected account. **No schema change** (`site_settings` already carries the stripe
columns). What the deploy needs:

1. **New API route** `POST /api/v1/tenant/{slug}/stripe-account` (ECW backfill seam) → delete
   `bootstrap/cache/routes-v7.php` (or `route:cache`) per the routes checklist below.
2. **New class files** (`app/Services/StripeAccountPropagator.php`, changed controllers/pages) →
   `composer install --no-dev` regenerates the classmap on deploy; clear OPcache after.
3. **Filament component cache** — the super-admin Tenants resource gained a "Sync Stripe Account"
   action and the tenant page is renamed "Payments": run `php artisan filament:clear-cached-components`.
4. **Optional env** — `ECW_PORTAL_URL` (defaults to `https://eastcoastwebcraft.ca/portal/`); set it
   only if the portal path differs. It's the link shown on the read-only Payments page.
5. **Live-mode reminder:** enable **Connect** on the platform's LIVE Stripe account (one-time) so
   merchants can connect Standard accounts; the deleted Express onboarding routes are gone.

### T0.5 — Tenant media isolation (one-time server-side file move)

Uploads are now tenant-scoped (`product-images/{tenant-id}/`, `branding/{tenant-id}/` —
DESIGN_DOCUMENT §13.6). **`.cpanel.yml` rsync EXCLUDES `storage/`, so the code deploy does
NOT touch existing uploads — the move script below is the ONLY mechanism on production.**
New uploads land tenant-scoped automatically once the code is deployed; existing files must
be moved once, ON THE SERVER, in this order:

```bash
PHP=/opt/cpanel/ea-php83/root/usr/bin/php

# 1. Preview — shows every planned copy + DB rewrite, changes nothing
$PHP ~/ecstores/artisan tenants:isolate-media --dry-run

# 2. Run it — COPIES files into per-tenant dirs + rewrites tenant-DB paths (idempotent, safe to re-run)
$PHP ~/ecstores/artisan tenants:isolate-media

# 3. Spot-check every store's images BEFORE cleanup:
#    each storefront home + a product page + logo/favicon, and an admin Products list.

# 4. Preview the cleanup — lists root-level originals no tenant references anymore
$PHP ~/ecstores/artisan tenants:isolate-media --cleanup --dry-run

# 5. Delete the leftovers
$PHP ~/ecstores/artisan tenants:isolate-media --cleanup
```

Notes:
- Phase 1 **copies** (never moves) because seeded/test stores can reference the same physical
  file from multiple tenants; the `--cleanup` pass only deletes root-level files that NO
  tenant references (checked across all tenant DBs on every run).
- A `referenced file not found on disk` warning means a DB row points at a file that doesn't
  exist (pre-existing damage) — the path is left unchanged; investigate before `--cleanup`.
- New class files ship with this change (`App\Support\TenantMedia`, `App\Services\MediaIsolator`,
  the `tenants:isolate-media` command) — the standard deploy's `composer install` regenerates
  the classmap; clear OPcache as usual.

---

## After Every Upload — Checklist

### If you uploaded anything in `routes/`
Laravel caches compiled routes in `bootstrap/cache/routes-v7.php`. The production
server's default PHP (8.1) can't run `php artisan route:clear`, so you must delete
the cache file manually after every routes upload or the new route will 404.

**Via FTP (FileZilla):**
Navigate to `/home/n777909/ecstores/bootstrap/cache/` and delete `routes-v7.php`.

**Via SSH:**
```bash
rm ~/ecstores/bootstrap/cache/routes-v7.php
```

### If you uploaded anything in `config/`
Same issue — delete `config.php` from the cache folder:
```bash
rm ~/ecstores/bootstrap/cache/config.php
```

### If you uploaded any compiled views / Blade templates
Cached Blade views live in `storage/framework/views/`. Laravel auto-recompiles on
next request so this rarely needs manual clearing, but if you see stale output:
```bash
rm ~/ecstores/storage/framework/views/*.php
```

### If Filament widgets, resources, or pages aren't appearing or are stale
Filament caches its discovered components in `bootstrap/cache/filament/`. This
overrides runtime discovery and persists across all requests until cleared.

```bash
/opt/cpanel/ea-php83/root/usr/bin/php ~/ecstores/artisan filament:clear-cached-components
# or if that command isn't available:
rm -rf ~/ecstores/bootstrap/cache/filament/
```

### If you uploaded a NEW PHP class file (not just editing an existing one)
The server uses an optimized Composer classmap. New classes won't be found until
the classmap is regenerated — PHP will silently fail to load the class.

**Composer path on this server:** `/opt/cpanel/composer/bin/composer`

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

This also clears route cache, config cache, compiled views, and republishes
Filament assets automatically — it's safe to run any time.

### If edited PHP files aren't taking effect (changes invisible after upload)
PHP OPcache has the old compiled version in memory. Create a one-time reset script,
hit it in the browser, then delete it immediately:
```bash
echo '<?php opcache_reset(); echo "OPcache cleared."; ?>' > ~/ecstores/public/opc.php
# Visit https://ecstores.ca/opc.php in browser
rm -f ~/ecstores/public/opc.php
```

**Rule of thumb:**
- Edited existing file, change not showing → clear OPcache
- Added a new class file → run `composer dump-autoload` (also clears OPcache effect)

---

## One-Time Server Setup (run once after first deployment)

```bash
# Create the storage symlink so /storage/ URLs resolve (replaces php artisan storage:link)
ln -s ~/ecstores/storage/app/public ~/ecstores/public/storage

# Ensure required storage directories exist
mkdir -p ~/ecstores/storage/app/public/product-images
mkdir -p ~/ecstores/storage/app/livewire-tmp
chmod 775 ~/ecstores/storage/app/public/product-images
chmod 775 ~/ecstores/storage/app/livewire-tmp
```

Without the symlink, all product images, logos, banners, and favicons will 404
even though files upload successfully — `php artisan storage:link` can't be run
directly due to the PHP version mismatch, so the manual `ln -s` is the fix.

### Enable database-backed sessions (security — Task 4.7 follow-up)

Sessions move from the shared filesystem (`SESSION_DRIVER=file`) to the central
database so tenant admin sessions can't collide or leak across subdomains. The
`sessions` table is **central-only** and `config/session.php` pins the session
store to the central connection (defaults to `DB_CONNECTION`), so tenant requests
still read/write sessions on central even though their default DB connection is
the tenant DB. Run once, in this order:

```bash
# 1. Create the central sessions table (migration ships in database/migrations/)
/opt/cpanel/ea-php83/root/usr/bin/php ~/ecstores/artisan migrate --force

# 2. Edit ~/ecstores/.env:  SESSION_DRIVER=file  ->  SESSION_DRIVER=database
#    (leave SESSION_CONNECTION unset — it defaults to the central connection)

# 3. Clear config + OPcache
/opt/cpanel/ea-php83/root/usr/bin/php ~/ecstores/artisan config:clear
pkill -f lsphp || true
```

Verify a tenant storefront/admin still loads (a 500 would mean the central pin
failed). Everyone gets signed out once (old file sessions are discarded) — expected.

---

## Running Artisan on the Server

The default `php` binary is PHP 8.1 but ECStores requires 8.3+. Direct `php artisan`
calls will show a Composer platform warning and may fail.

Find the PHP 8.3 binary path (run once, save the output):
```bash
which php83
# or
ls /opt/cpanel/ea-php83/root/usr/bin/php
```

Then use the full path:
```bash
/opt/cpanel/ea-php83/root/usr/bin/php ~/ecstores/artisan route:clear
/opt/cpanel/ea-php83/root/usr/bin/php ~/ecstores/artisan cache:clear
/opt/cpanel/ea-php83/root/usr/bin/php ~/ecstores/artisan migrate --force
```

---

## Server Paths

| What | Path |
|---|---|
| ECStores root | `/home/n777909/ecstores/` |
| ECStores public web root | `/home/n777909/ecstores/public/` |
| ECW root | `/home/n777909/public_html/` |
| ECStores `.env` | `/home/n777909/ecstores/.env` |
| ECW secrets | `/home/n777909/public_html/includes/_secrets.php` |
| Route cache | `/home/n777909/ecstores/bootstrap/cache/routes-v7.php` |
| Config cache | `/home/n777909/ecstores/bootstrap/cache/config.php` |
| Laravel log | `/home/n777909/ecstores/storage/logs/laravel.log` |

---

## Checking the Production Error Log

```bash
tail -50 ~/ecstores/storage/logs/laravel.log
```

---

## open_basedir Restriction

inMotion's shared hosting enforces `open_basedir`. PHP running under Apache **cannot
read files outside the web directory tree**, even if file permissions allow it.
This is why ECW secrets live in `includes/_secrets.php` (a PHP file inside the web
root) rather than in `~/.env_ecw` above `public_html`.

---

## What to Upload vs. What NOT to Upload

**Safe to bulk-upload:**
- `app/` — PHP class files only
- `resources/` — views, CSS, JS source
- `routes/` — route files (remember to clear route cache after)
- `config/` — config files (remember to clear config cache after)
- `database/migrations/` — migration files (then run `php artisan migrate`)

**Never upload (server-managed):**
- `vendor/` — managed by Composer on the server
- `.env` — contains live secrets, edit directly on server via SSH
- `storage/` — runtime files, logs, uploaded images
- `bootstrap/cache/` — auto-generated cache files
- `node_modules/` — not present on server (Vite build runs locally)
- `public/build/` — upload after running `npm run build` locally

---

## ECW ↔ ECStores API Integration

ECW communicates with ECStores via HMAC-SHA256 signed cURL requests. The API route
prefix is `/api/v1/platform/` (was `/super/` — renamed to avoid ModSecurity CRS rules).

### ModSecurity — IMPORTANT

inMotion's nginx ModSecurity layer blocks all requests to `/api/v1/` by default with
HTTP 406 before the request reaches PHP. The `.htaccess` `SecRuleEngine Off` does NOT
fix this — it only covers Apache mod_security2, not nginx modsec3.

**Fix (already applied):** ModSecurity is disabled for `ecstores.ca.eastcoastwebcraft.ca`
(the addon-domain internal vhost — was `eastcoastweb.ca.eastcoastwebcraft.ca` before the
domain migration) in cPanel → Security → ModSecurity. This must remain Off.

If the ECStores API ever returns 406 again, check this toggle first.

### ECW `_secrets.php` — NEVER UPLOAD

`/home/n777909/public_html/includes/_secrets.php` must NEVER be overwritten by uploading
from your local machine. The local file has test credentials (empty DB password, ecw_test DB)
that will immediately break the production site.

**Rule: always upload specific files, never the entire `includes/` folder.**

If the file is accidentally overwritten, fix it via SSH:
```bash
sed -i "/'DB_NAME'/d; /'DB_USER'/d; /'ECSTORES_API_URL'/d; s|'ECSTORES_API_SECRET'.*|'ECSTORES_API_SECRET'   => '<CURRENT_ECSTORES_API_SECRET>',|" /home/n777909/public_html/includes/_secrets.php
# ^ Replace <CURRENT_ECSTORES_API_SECRET> with the live value — it MUST match
#   ECStores .env ECW_API_SECRET. Do NOT paste an old/rotated secret here.
```
Then update `DB_PASS` with the real password via `nano`.

---

## Go-Live Checklist (when ecstores.ca domain is ready)

> ⚠ **SUPERSEDED 2026-07-31 (cross-repo audit). This list is NOT the maintained one.**
>
> The current, maintained go-live checklist is **`eastcoastwebcraft.ca/documents/before_golive.md`**
> (last rewritten 2026-07-29), which covers both products, carries the ordered indexing sequence, and
> is the list Denis actually works from. This copy is a **superseded duplicate** kept for its
> deployment context above; do not tick items here and do not treat unticked rows as open work.
>
> Two concrete ways it is stale, both verified:
> - The first four rows (`APP_URL`, `SESSION_DOMAIN`, `central_domains`, wildcard SSL) were **done at
>   the 0.4.0 cutover** — see `CHANGELOG.md:186` — but are still shown unticked here.
> - It predates the entire tier build (T0→T5, L, N1–N3) and the Phase-8 regression, so it has none of
>   those gates: purging test plans **7 and 8**, the `[TESTDATA]` tenant wipe, the **Connect** platform
>   webhook with a *distinct* `STRIPE_CONNECT_WEBHOOK_SECRET` (open **defect #8**), or removing the
>   `X-Robots-Tag: noindex` header from `public/.htaccess:16`.
>
> ⚠ Note also that the Stripe row below names only **one** webhook. Go-live needs **two**: the
> Cashier/platform endpoint AND the Connect platform endpoint, with different secrets.
>
> **One home per doc — use `before_golive.md`.**

- [ ] Update `APP_URL` in `~/ecstores/.env` → `https://ecstores.ca`
- [ ] Update `SESSION_DOMAIN` in `~/ecstores/.env` → `.ecstores.ca`
- [ ] Add `ecstores.ca` to `central_domains` in `config/tenancy.php`
- [ ] Set up wildcard SSL for `*.ecstores.ca` in cPanel
- [ ] Switch Stripe keys to live mode in `~/ecstores/.env` (`STRIPE_KEY`, `STRIPE_SECRET`, `STRIPE_WEBHOOK_SECRET`)
- [ ] Register live Stripe webhook: `https://ecstores.ca/stripe/webhook`
- [ ] Update `ECSTORES_API_URL` in ECW `includes/_secrets.php` → `https://ecstores.ca/api/v1`
- [ ] Switch ECW Stripe keys to live in `includes/_secrets.php`
- [ ] Add Plans (Starter/Growth/Pro) in ECStores super-admin *(plan definitions / feature limits only — billing is ECW-side; Cashier is inert, no Stripe Price IDs needed)*
- [ ] Confirm ModSecurity is **Off** for `ecstores.ca.eastcoastwebcraft.ca` in cPanel (the addon-domain internal vhost — required for the ECW↔ECStores API to work)
