# Go-live runbook

Everything in this folder is ready to drop onto a server. Provisioning the
box itself, buying/pointing DNS, and getting a Cloudflare API token are
things I can't do from here — they need your accounts and your action.
This is the single, up-to-date checklist for getting the built system
(Phases 0–6) actually running, and for taking it from "running" to
"trusted with a real pilot company's data."

## 1. Provision the server

1. **Get a VPS.** Any provider works — DigitalOcean, Hetzner, AWS
   Lightsail. Ubuntu 22.04, 2 vCPU / 4 GB RAM is comfortable for the
   current app plus headroom for the first handful of tenant companies.
2. **Point DNS at it**, once you know the box's IP:
   - `ibi-institutes.com` → `A` record → the VPS IP
   - `*.ibi-institutes.com` → `A` record → the same IP (this is what makes
     every company's handed-out subdomain resolve without you touching DNS
     again)
3. **Get a Cloudflare API token** (or your DNS provider's equivalent)
   scoped to edit DNS on that zone — the wildcard certificate can only be
   issued via a DNS-01 challenge, which needs this. Set it as
   `CLOUDFLARE_API_TOKEN` in the environment Caddy runs under (e.g.
   `/etc/caddy/caddy.env` loaded by its systemd unit).
4. **Install PHP 8.2-FPM, MySQL, and Caddy.** Caddy needs to be built with
   the DNS provider plugin via
   [`xcaddy`](https://github.com/caddyserver/xcaddy):
   ```
   xcaddy build --with github.com/caddy-dns/cloudflare
   ```

## 2. Deploy the app

```
git clone <this repo> /var/www/abic-e-learning
cd /var/www/abic-e-learning
composer install --no-dev --optimize-autoloader
cp .env.example .env   # then fill in production values, see below
php artisan key:generate
```

**`.env` values this build specifically needs**, beyond the normal Laravel
ones (`DB_*`, `MAIL_*`):

| Key | What it's for |
|---|---|
| `APP_URL` | Must be the real root domain (`https://ibi-institutes.com`) — `tenantRootDomain()` and every subdomain a company gets are derived from this. |
| `SAAS_ROOT_ADMIN_NAME` / `_EMAIL` / `_PASSWORD` | The one platform-operator login, seeded by `CreateRootCompanySeeder`. Set real values before seeding — **rotate the password after first login**, especially since it was typed into a chat at some point during development. |

## 3. Migrate and seed, in this order

```
php artisan migrate --force
php artisan db:seed --class="Database\Seeders\PermissionSeeder" --force
php artisan db:seed --class="Database\Seeders\RoleSeeder" --force
php artisan db:seed --class="Database\Seeders\CreateRootCompanySeeder" --force
```

- `PermissionSeeder` + `RoleSeeder` **must run in that order, together,
  every time new permissions have been added** — `domain.*`,
  `company_profile.*`, and `user.reset_password` were all added after the
  original install, and `RoleSeeder` is what actually grants them to the
  `admin` role. If a company admin reports a permission-denied on a screen
  that should work, this is the first thing to check.
- `CreateRootCompanySeeder` is idempotent — safe to re-run. It backfills
  any row still missing a `company_id` into the root company and
  seeds/updates the SaaS operator account from the `.env` values above.

## 4. Wire up the reverse proxy

Copy `Caddyfile` to `/etc/caddy/Caddyfile`, adjust the `php_fastcgi`
socket path to match your actual PHP-FPM pool if it differs, then:

```
sudo systemctl restart caddy
```

What this buys you automatically, with no further action per company:

- Any `*.ibi-institutes.com` request gets the wildcard certificate.
- Any request to a hostname not sitting in the `domains` table as a
  verified custom domain gets refused a certificate and 404s — nothing
  silently serves the wrong company's data, and nothing lets a stranger
  mint a certificate through you.
- The moment a company's custom domain passes TXT verification (the
  screen at `/admin/domain/list`), its very next real visit triggers
  certificate issuance via `TenantDomainAskController`. No redeploy, no
  manual step.

## 5. Turn on the scheduler

Two commands depend on this running; without it, both only ever happen
when someone clicks the equivalent button by hand.

- `companies:sync-status` — moves a company pending → active on its
  subscription's start date, and active → expired on its end date.
  Manual suspend/reactivate in the SaaS control panel is independent of
  this and always takes precedence.
- `backups:run-scheduled` — creates a backup if automated backups are
  turned on (SaaS control panel → Backups) and enough time has passed for
  the configured frequency.

Add the standard Laravel cron entry:

```
* * * * * cd /var/www/abic-e-learning && php artisan schedule:run >> /dev/null 2>&1
```

`backups:run-scheduled` shells out to `mysqldump`/`mysql` in production
(sqlite backups are just a file copy, no extra dependency) — confirm both
are on the server's `PATH`; they normally already are on any box that also
runs a MySQL server.

## 6. Create the first real company

Log in at `https://ibi-institutes.com/saas-admin/login` with the
`SAAS_ROOT_ADMIN_*` credentials, then **"+ New company."** That one form
creates the company, its subdomain, and its first admin login together —
nothing else to provision. Optionally assign a subscription plan at the
same time (create one first under **Subscription Plans** if none exist
yet).

## Pilot rollout checklist

Once one real company is live, before trusting this with a second:

- [ ] Log into the new company's subdomain as its admin and confirm the
      dashboard, course list, and **Company Profile** (name + logo) all
      show that company's own data, not the root company's.
- [ ] Add a custom domain from `/admin/domain/list`, publish the TXT
      record at your registrar, and confirm "Check verification" actually
      flips it — this is the one step in the whole system that can't be
      verified from a dev machine, it needs real public DNS.
- [ ] Visit the custom domain once verified and confirm the certificate
      was issued automatically (browser padlock, no warning).
- [ ] From the SaaS control panel, **Suspend** the company and confirm: the
      dashboard still loads, but trying to create/edit/delete anything is
      refused with a clear message, and logging out and back in still
      works.
- [ ] Reactivate it and confirm writes work again immediately.
- [ ] Set a subscription with a start date next week and confirm the
      company shows **pending**, not active, until that date. Set one with
      an end date in the past and confirm it lands on **expired**
      immediately.
- [ ] Create a second company and confirm neither company's courses,
      students, domains, certificate branding, or payment gateway
      credentials are visible to the other's admin.
- [ ] From `/admin/payment_gateway`, connect the company's own PayPal/
      Stripe/etc. account — new companies start with all five gateways
      present but blank and inactive on purpose (see below), so this is a
      required onboarding step, not optional.
- [ ] From the SaaS control panel's Backups screen, create a manual backup
      and download it; separately, try deleting a company that's still
      active and confirm it's refused — only an **expired** company can be
      deleted, and doing so takes an automatic safety backup first.

## Known gaps — real, not yet closed

- A handful of Blade views (`user.index`, the SaaS admin subscription-plan
  screens) were verified by direct execution of their controllers and, for
  the views themselves, by compiling them through Blade's real compiler —
  not by fully rendering and eyeballing them in a browser. Worth an actual
  click-through once this is live.
- Deleting a company permanently removes every database row that belonged
  to it (verified, including everything that cascades from its courses and
  users), but does **not** remove uploaded files — logos, course
  thumbnails, certificate images — from disk/storage. They become orphaned,
  not exposed to anyone, just not cleaned up. Low priority, but real.
