# Go-live: moving a legacy iShapps tenant onto the SaaS

How to cut an agency over from the legacy app (v1/v2, Laravel 7) to the SaaS, so it feels
seamless. The one visible change: **everyone is logged out once** and signs in again with the
same email and password.

Tested end to end against a copy of Smart Healthcare's production database. Run every command
from the backend root on the server, with `php8.2` (the server also has PHP 7.4).

Set the tenant's id once per shell session; every command below uses it:

```bash
export TENANT_ID=503576dc-2cf6-4bdc-8c94-c4f27443ead6   # the tenant's UUID, no < >
```

---

## 0. Before you start

1. **Stop the legacy app and its crons.** From here the SaaS owns this database; running both
   doubles shift crons, notifications and invoices.
2. **Back up the production database** (`mysqldump`). That's your rollback.
3. **Confirm which database the tenant uses.** The central `tenants` row can override the
   database name (`data->tenancy_db_name`) — e.g. a legacy-attached tenant may point at
   `shapp` rather than `t_<schema_name>`. Run everything below against that database.

## 1. Prepare the legacy database (SQL, once)

```bash
mysql -u <user> -p <tenant_db> < database/sql/go-live-legacy-tenant.sql
```

What it does:

- Drops the legacy Passport 9 `oauth_*` tables and their migration rows. This is what logs
  everyone out.
- Sets role and permission `guard_name` from `web` to `api`.
- Rewrites `model_type` from `App\User` to `App\Models\User` in `model_has_roles` and
  `model_has_permissions`.
- Drops the obsolete `general_configurations` columns and the `trial_account_configs` table.

The column drop isn't repeatable: run the script **once**, on a database that hasn't had it.

## 2. Deploy the code

```bash
git pull
php8.2 $(which composer) install --no-dev --optimize-autoloader
php8.2 artisan migrate                                   # central: platform_settings, …
php8.2 artisan tenants:migrate --tenants=$TENANT_ID     # tenant: see below
php8.2 artisan config:cache
```

The tenant migrations recreate the Passport 13 tables and add `tenant_settings` and the
invoice GST/footer columns. They also fix legacy timestamp columns: zero-date defaults, and
`ON UPDATE CURRENT_TIMESTAMP`, which was overwriting `billing_lines.approved_at` and similar
on every update.

## 3. Make the tenant usable

```bash
# Passport personal access client — once per tenant, or nobody can log in:
php8.2 artisan tinker --execute='App\Models\Central\Tenant::find(getenv("TENANT_ID"))->run(fn () => app(Laravel\Passport\ClientRepository::class)->createPersonalAccessGrantClient("Personal Access Client", "users"));'

# Agency identity, ABN, bank details, payroll/invoicing cadence (Smart Healthcare's values):
php8.2 artisan tenants:seed --class="Database\Seeders\SmartHealthcareSettingsSeeder" --tenants=$TENANT_ID

# Register every existing user for email-first login (else login can't find their organisation):
php8.2 artisan tenants:run users:sync-directory --tenants=$TENANT_ID

# Legacy files (HCP documents, images) into the tenant's own folder — dry run, then copy:
php8.2 artisan tenants:copy-legacy-uploads $TENANT_ID
php8.2 artisan tenants:copy-legacy-uploads $TENANT_ID --run
```

To check the Passport client exists, list the tenant's `oauth_clients`. Expect one row with
`grant_types` `["personal_access"]`:

```bash
php8.2 artisan tinker --execute='App\Models\Central\Tenant::find(getenv("TENANT_ID"))->run(fn () => print_r(DB::table("oauth_clients")->get(["id","name","grant_types","revoked"])->toArray()));'
```

## 4. Background processes and environment

- **Scheduler cron:**
  `* * * * * cd /path/to/backend && php8.2 artisan schedule:run >> /dev/null 2>&1`
- **Queue worker** on PHP 8.2 under Supervisor (`php8.2 artisan queue:work`). Emails, pushes,
  invoice PDFs, exports and the shift jobs all run through it.
- **`.env`:**
  - `APP_URL` must be the public API URL (`https://api.ishapps.cloud`). Email logos and signed
    links are built from it.
  - `FRONTEND_URL` must be the SPA URL.
  - OneSignal keys are `ONESIGNAL_APP_ID` / `ONESIGNAL_REST_API_KEY`.
  - Strip Windows line endings: `sed -i 's/\r$//' .env`.
- **After any `.env` change or deploy:**
  ```bash
  php8.2 artisan config:cache
  php8.2 artisan queue:restart     # workers keep the old config until restarted
  ```

## 5. Settings and checks

- **Platform support contact** (shown in iShapps-branded emails):
  ```bash
  php8.2 artisan platform:setting support.email <address>
  php8.2 artisan platform:setting support.phone <number>
  ```
  Run `php8.2 artisan platform:setting` with no arguments to list every setting (store links,
  app versions, …).
- **Organisation settings page** (SPA → Settings → Organisation): check the logo, legal name,
  ABN, bank details, activity emails and accounting cadence.
  - **Accounting start date:** payroll and invoicing never pick up lines from before it
    (pre-SaaS history).
- **Facility configs:** every facility needs a *rostering contact email* (shift activity
  emails) and an *accounts email* (invoices). If any were pointed at test inboxes, put the
  real addresses back.
- **Dry-run the money jobs** before the first real cutoff, to see what the first pay run and
  invoices will include:
  ```bash
  php8.2 artisan tenants:run accounting:run-payroll --tenants=$TENANT_ID --option=dry-run=1
  php8.2 artisan tenants:run accounting:generate-invoices --tenants=$TENANT_ID --option=dry-run=1
  ```
  `tenants:run` passes options as `--option=name=value`. A bare `--dry-run`, or
  `--option=dry-run` without `=1`, is rejected.
- **Deploy the frontend** from `main`.

## 6. Smoke test

- Log in on the web and in the mobile app.
- Open an HCP document. Signed S3 links prove the copied uploads.
- Create a test shift and accept it. Check the shift activity email arrives (rostering contact
  plus agency activity emails on BCC).
- Send a test push from an HCP's page.
- For invoices: generate one for a single facility, then *Confirm & Send*. Check the PDF in S3
  under `t_<schema_name>/uploads/facilities/invoices/` and that the email arrives with it
  attached.

## Expected side effects

- **Everyone is logged out** and signs in again with the same email and password. If they
  belong to more than one organisation, they choose one.
- **Shifts left running** since the cutover are auto-ended on the first scheduler tick, which
  creates their payroll lines.

## Troubleshooting

| Symptom | Likely cause |
|---|---|
| Nobody can log in | No Passport personal access client (step 3) |
| Login says no organisation found | `users:sync-directory` not run (step 3) |
| Roles/permissions missing after login | SQL script not run (guards/`model_type`, step 1) |
| Emails or pushes never arrive | Queue worker not running, or not restarted after `.env` change |
| "OneSignal not configured" | Wrong env key name, or stale config cache (`config:cache` + `queue:restart`) |
| Logo/images broken in emails | Wrong `APP_URL`, or a worker started before it was fixed |
| Scheduled jobs never run | Missing cron, or `tenants:run` given a bare flag instead of `--option=…` |
