# Adopting an existing business (`ssgplatforms_deeq`): rehearsal, adoption, validation, rollback

**Status: prepared against the `pos.zip` baseline. NOT executed. Nothing in this plan has touched production.**

Items marked **[REPORT]** need `kaabe-report-*.tar.gz`. Items marked **[COPY]** need the fresh backup copy made with `backup.sh`. Production actions wait for the exact approval **PHASE 3 APPROVED — PROCEED**.

## 0. What "migration" means here

The business is **adopted in place** (see `M7_DATA_MAPPING.md` §1). It keeps its own database and all of its rows, with the same IDs, dates and totals.

- **What Kaabe adds** (all additive and idempotent):
  - Two tables and two indexes.
  - A few settings, five role templates and one API key.
  - The central records.
- **What Kaabe never does:**
  - Copy, transform or re-key historical data.
  - Change passwords.
  - Delete anything.

That is why the main acceptance test is simple: **every business table must be byte-identical before and after** (CHECKSUM TABLE). The only exceptions are a short list of tables Kaabe adds to.

```
Production DB ──backup.sh──► Verified backup (checksums, restore test)
                                   │
                                   ▼
                      Isolated copy kb_deeq_copy (Docker demo / staging)
                                   │  php artisan kaabe:rehearse
          profile BEFORE ─► baseline perf ─► adopt (Kaabe migrations, tenant, plan)
                                   │
                first sync ─► profile AFTER ─► validation.md ─► isolation ─► perf
                                   │
             Rehearsal repeated until PASS twice ─► security review ─► sign-off
                                   │
                  PHASE 3 APPROVED — PROCEED  (you, in writing)
                                   │
                Production cut-over (§9)  ◄── rollback ready (§8)
```

The production database is **never** used as the test database. Every rehearsal tool refuses the name `ssgplatforms_deeq`. The adopt command only accepts it with `KAABE_ADOPT_PRODUCTION_APPROVED='PHASE 3 APPROVED — PROCEED'`.

## 1. Pre-migration backup

1. On InMotion, run `bash backup.sh <pos-folder>` from the verification kit. It produces:
   - `ssgplatforms_deeq_<ts>.sql.gz` (`--single-transaction`, routines, triggers, events, `--hex-blob`, default `--tz-utc`);
   - the POS files as a `.tar.gz`;
   - `SHA256SUMS`.
2. Download all three files off the server. Store two copies, one offline.
3. **Restore test:** `php artisan kaabe:rehearse` restores the dump into `kb_deeq_copy`. A backup that doesn't restore is not a backup.
4. **Just before the production cut-over,** take a second backup the same way. That backup is the rollback point (§8).

## 2. Schema preparation (additive only)

| Change | How | Reversible |
|---|---|---|
| `phppos_kaabe_meta`, `phppos_kaabe_location_meta` | `kaabe/tools/migrate.php` #1 (every location → branch) | Yes: `DROP TABLE` (only Kaabe tables) |
| Index `kaabe_sale_time` on `phppos_sales` | migrate #2 | Yes: `DROP INDEX` |
| Index `kaabe_last_modified (last_modified, sale_id)` on `phppos_sales` | migrate #3 | Yes: `DROP INDEX` |
| POS vendor migrations | Only if the live `migration_version` is below 20221115200805 **[REPORT]**. Run on the COPY first and reviewed separately. | Restore from backup |

Adding an index to `phppos_sales` rebuilds the index online (InnoDB). The time it takes on real data is measured on the copy (§7). **[COPY]**

## 3. Data migration by area

Every area below is **preserved in place**. The transformation is "none" unless stated otherwise.

| Area | Plan | Validation (§6) |
|---|---|---|
| Business | Central `tenants` row: name from `app_config.company` (confirmed by the business), sub-domain chosen by you, code `KB-xxxx` | Manual confirmation |
| Users / employees | Unchanged. The owner is **chosen by the business** (`--owner`) and recorded in `phppos_kaabe_meta` and centrally. | Counts: active, inactive, deleted |
| Passwords | §4 | Hash-type counts before = after |
| Roles / permissions | Unchanged. Five Kaabe templates added only if the names are free. Plan gates apply at runtime; no permission rows are removed. | Checksum of `permissions*` tables |
| Branches / locations | Unchanged. All become `branch` in the meta table. **Warehouses only if the business asks** (V3). | Branch count; sales by location |
| Registers | Unchanged | Count |
| Products / categories | Unchanged | Counts, checksums |
| Customers / suppliers | Unchanged, never copied centrally | Counts, balances |
| Inventory | Unchanged (`location_items` and the `inventory` history) | Stock units per location |
| Sales, sale items, taxes | Unchanged. IDs, dates, branch, register, customer, quantities, prices, discounts, taxes, status (deleted, suspended, layaway, estimate) and `last_modified` all preserved. | Counts, totals by year, location and day; checksums |
| Payments | Unchanged | Payment totals |
| Receivings, expenses, gift cards, store accounts | Unchanged | Totals and balances |
| Historical data | All of it, including deleted and suspended records | Per-table row counts |
| Tenant configuration | `kaabe:adopt`: encrypted connection, registry entry, health check | Super Admin shows health OK |
| Subscription | Professional, monthly, started on the cut-over date (price and period are yours to confirm) | Subscription history |
| Entitlements | Delivered to `app_config.kaabe_entitlements` and read back. If usage exceeds a limit, `--add-overrides` records a per-business exception at current usage. **Data is never removed.** | Entitlement version |
| API credentials | Existing business keys kept. One Kaabe platform key added, stored as SHA-1 in the business database and encrypted centrally. | Key count +1 |
| Central monitoring | First sync backfills daily aggregates from the whole history (several runs; timed) | Central totals = tenant totals |

## 4. Passwords

- **How passwords are stored now.** The `pos.zip` baseline stores `md5(password)` (32 hex characters) in `phppos_employees.password`. The report's `password_hash_types` confirms this on the live database. **[REPORT]**
- **Migration.** Hashes are **kept exactly as they are**; nobody's password is reset.
- **Upgrade at next sign-in.** After cut-over, the Milestone 1 login (`kaabe_password_check`) accepts the MD5 hash and immediately replaces it with bcrypt on the user's next successful sign-in.
- **Needing a reset.** Only active users with an empty or unrecognised hash need a temporary password. They are listed by username in `before-exceptions.md` (US1) and get one-time temporary passwords from the Super Admin after cut-over.
- **Old MD5 hashes.** Users who never sign in keep an MD5 hash. After 90 days, remaining MD5 hashes are listed for a forced reset (Milestone 8).
- **No passwords in logs.** Passwords and hashes never appear in any report: the tools select hash *types* only.

## 5. Time zones, dates and data issues

- **Critical: MySQL TIMESTAMP columns.** `sale_time`, `last_modified` and others are stored internally in UTC and **displayed in the connection's time zone**. The POS never sets a session time zone, so the server's `time_zone` decides the local times shown in the POS, in reports and in the Kaabe daily totals.
- **Rule:** the target MariaDB must use the **same effective time zone** as today. The profile records `global_tz`, `system_tz` and the UTC offset. `m7_compare.php` fails the validation if the offset differs or any day's totals move. **[REPORT]**
- **Same-server adoption** (the expected case: the database stays on the same InMotion server) does not change this.
- **Moving to another server** (for example a new VPS) requires setting `time_zone` to match before cut-over.
- **Backups:** `mysqldump` (default `--tz-utc`) and a restore on a server with the same time zone preserve the displayed values.
- **Other issues:** reported in `*-exceptions.md` and **never changed automatically**.

| Check | Examples |
|---|---|
| FK1–FK6 | Orphan foreign keys |
| DU1–DU4 | Duplicate usernames, barcodes, product IDs and account numbers |
| DT1 | Impossible dates |
| DT2 | Payment/total differences |
| ST1–ST2 | Negative stock, and stock on deleted items |
| US1–US2 | Users with no valid hash or no location |
| SY1 | Deleted sales without `last_modified` (tests the Milestone 6 sync assumption on real data) |

Each finding gets an action. Anything needing a data decision goes to the business; it is **not** fixed by a script.

## 6. Validation

**Automated:** `m7_profile.php` runs before and after (read-only, one consistent snapshot, plus `CHECKSUM TABLE`), then `m7_compare.php` writes `validation.md`.

**Comparisons:**
- **Counts:** users (active, inactive, deleted), products, categories, customers, suppliers, branches, registers, stock records, sales (all, completed, deleted, suspended), sale items, payments, receivings, expenses.
- **Totals to 4 decimals:** sales total, subtotal, tax and profit; payments; items sold; stock units; customer, supplier and gift-card balances; expenses.
- **Breakdowns:** sales by year, by location, and by day for the last 60 days; stock by location.
- **Every table:** row count and checksum. Only the listed tables may differ (EXPECTED): `app_config`, `keys`, the permission templates tables, `sessions`, and `sales` (indexes only).
- **Time zone:** UTC offset.
- **Central vs tenant:** central aggregates equal the tenant's own totals.

**Result.** Each line reads MATCH, EXPECTED or DIFFERENCE, with an explanation and the action required. **Any DIFFERENCE stops the process.** Run before and after on the same day, because the 60-day window uses today's date.

**Manual checks** (with the business, on the copy):
- Five POS reports compared with the old installation: sales summary, detailed sales, profit and loss, inventory, payments.
- 20 random sales opened and compared line by line with receipts.
- Sign-in as three real users (after the MD5 → bcrypt upgrade).
- One test sale on the copy, then the dashboard and a central sync.

## 7. Performance plan

the rehearsal's performance step runs twice on the copy: once as the **baseline** (restored copy, before any Kaabe change) and once as **migrated** (after adoption).

| Area | What is measured |
|---|---|
| Volumes | Database size; rows in employees, items, customers, sales, sale items, payments, locations, location items |
| SQL timings (median of 5) | Login lookup, product search, customer search, stock lookup, 30-day sales report, all-time report, dashboard low stock, both sync scan queries |
| HTTP | Login page |
| Sync | Full backfill duration, then one incremental run |

**Measured manually** in the browser on the copy, old code vs Kaabe (stopwatch or dev tools, 3 runs each):
- sign-in;
- the product search box;
- checkout of a 5-line sale;
- receiving;
- customer search;
- the sales summary report for last month;
- the Kaabe dashboard.

**Acceptance:** no operation more than 20% slower than the baseline. The sync backfill must finish within its batch caps without locking the POS. Metadata-only index creation time is measured with `ALTER TABLE` on the copy. **[COPY]**

## 8. Rollback

The adoption changes nothing in business rows, so rollback is quick and safe.

1. **Stop:** set the business to *suspended* in the Super Admin (the POS shows the maintenance page), or point the old URL back to the untouched old installation.
2. **Undo the Kaabe changes in the tenant database** (only Kaabe objects):
   ```sql
   DROP TABLE IF EXISTS phppos_kaabe_location_meta, phppos_kaabe_meta;
   ALTER TABLE phppos_sales DROP INDEX kaabe_sale_time, DROP INDEX kaabe_last_modified;
   DELETE FROM phppos_app_config WHERE `key` IN ('kaabe_entitlements','kaabe_status');
   DELETE FROM phppos_keys WHERE description = 'Kaabe platform – do not delete';
   -- role templates named 'Business Owner','Business Admin','Branch Manager','Cashier','Inventory Manager' only if they were added by Kaabe
   ```
3. **Passwords already upgraded** to bcrypt keep working only with the Kaabe code. If the old code must run again, restore the `phppos_employees.password` column from the pre-cut-over backup. This is the only data restore normally needed.
4. **Last resort:** restore the full pre-cut-over backup (§1.4) into the database, then verify it with `m7_profile.php` and `m7_compare.php` against the "before" profile.
5. **Time budget:** 30 minutes for steps 1–3; the full restore time is measured during the rehearsal. **[COPY]**

## 9. Production cut-over (only after approval)

1. The business agrees a maintenance window; announce it.
2. Take a fresh backup (§1.4) and verify its checksums.
3. Set the old POS to maintenance and take the final `before` profile (read-only: `--i-understand-this-is-read-only-production`).
4. Deploy the Kaabe POS code (patched) and the Super Admin on InMotion; run a health check.
5. Rotate the `ssgplatforms_deeq` database password (audit S2). The new password goes into the Super Admin (encrypted) only.
6. Run `KAABE_ADOPT_PRODUCTION_APPROVED='PHASE 3 APPROVED — PROCEED' ADOPT_DB_PASSWORD=… php artisan kaabe:adopt --slug=… --db-name=ssgplatforms_deeq --owner=… --plan=professional` **without** `--activate`.
7. Take the `after` profile, run `m7_compare.php`, and require a PASS.
8. Activate the business in the Super Admin, run the first sync, and check central totals against the tenant.
9. Business smoke test: sign-in, a sale, the receipt, a report.
10. If any step fails, roll back (§8).

## 10. Multi-tenant verification

the rehearsal's isolation step runs on the copy. It checks that:
- the copy's database user cannot read another business or the central database;
- another business and the central database user cannot read the copy;
- another business's API key is rejected by the copy;
- the copy's POS is reachable only at its own sub-domain;
- the Super Admin needs a staff login;
- the central monitoring tables contain no customer, payment, line-item or contact columns.

Super Admin management of the tenant (plan, branches, users, sync) is exercised by the rehearsal and by the Milestone 5 and 6 tests.

## 11. InMotion checks (from the report — nothing assumed)

| Requirement | Report source | Needed for |
|---|---|---|
| PHP version (web and CLI) | `environment.txt` (MultiPHP per domain, CLI `php -v`) | POS 8.2 or 8.3; Laravel 12 needs 8.2+ |
| PHP extensions | `environment.txt` extension list (mysqli, pdo_mysql, openssl, curl, mbstring, gd/imagick, bcmath, gmp, soap, zip, xml, intl, fileinfo, sodium, tokenizer, ctype, opcache) | Both applications |
| `memory_limit`, `max_execution_time`, `upload_max_filesize`, `post_max_size`, OPcache | `environment.txt` PHP settings | Provisioning, imports, sync |
| MariaDB/MySQL version, `sql_mode`, `time_zone`, `max_connections` | `report.json` → `database_server` | Schema, §5 |
| cPanel version, UAPI availability, database and user limits (`StatsBar`) | `environment.txt` | Milestone 5 cPanel provisioner |
| Database user privileges (`CREATE`, `ALTER`, `INDEX` on its own database) | Test on the copy plus the cPanel UAPI | Kaabe migrations |
| SSH / cron availability | `environment.txt` | Scheduler (every minute) |
| SSL / AutoSSL status, domains | `environment.txt` (SSL `installed_hosts`, domains) | Sub-domains |
| Disk quota and usage | `environment.txt` | Backups, growth |
| Shared vs VPS (CloudLinux, root, resources) | `environment.txt` | Sizing and cron frequency |
| Live code vs zip; live schema vs zip | `file_diff.txt`, `schema_diff.txt`, `changed_files.tar.gz` | V4, V9 |

## 12. Production gate status

| # | Gate | Status |
|---|---|---|
| 1 | InMotion verification | ⏳ Waiting for `kaabe-report-*.tar.gz` |
| 2 | Isolated database backup | ⏳ Waiting for the `backup.sh` output |
| 3 | Migration rehearsal | ⏳ Tools ready (`php artisan kaabe:rehearse`); needs item 2 |
| 4 | Data validation | ⏳ Tools ready (`m7_profile.php`, `m7_compare.php`); needs item 3 |
| 5 | Performance testing | ⏳ Tools ready (the rehearsal's performance step); needs item 3 |
| 6 | Security review | ⏳ Milestone 8 |
| 7 | Rollback plan | ✅ Documented (§8); timing needs item 3 |
| 8 | Production migration plan | ✅ Documented (§9); needs 1–6 complete |

Production stays untouched until all eight are complete and you write **PHASE 3 APPROVED — PROCEED**.


## Automated rehearsal on the server (recommended)

After the backup (`backup.sh` or `scripts/backup-all.sh`), run:

```bash
cd ~/kaabe/platform
php artisan kaabe:rehearse --dump=/home/<cpaneluser>/<path>/ssgplatforms_deeq_<ts>.sql.gz --owner=<owner username> --cleanup
```

- **What it touches.** It creates a **separate** database `<prefix>rehearsal` through the provisioner and restores the backup into it. It never opens `ssgplatforms_deeq`.
- **Steps, in order:**
  1. BEFORE profile and baseline timings.
  2. Adoption (plan limits never block; exceeded limits are recorded).
  3. First sync.
  4. AFTER profile, comparison, and central-vs-tenant totals.
  5. Isolation checks and timings after adoption.
  6. **Rollback test** with its own comparison.
- **Result.** It writes `platform/storage/app/rehearsal/<timestamp>/ACCEPTANCE.md`, where every criterion is marked PASS, FAIL, EXPECTED DIFFERENCE or REQUIRES ACTION.
- **Clean-up.** With `--cleanup`, the rehearsal database is dropped at the end.

## Production adoption (only after PHASE 3 APPROVED — PROCEED)

```bash
KAABE_APPROVAL='PHASE 3 APPROVED — PROCEED' ADOPT_DB_PASSWORD='<new password>' \
  bash ~/kaabe/scripts/adopt-existing.sh --db-name=ssgplatforms_deeq --db-user=<user> --owner=<username> --slug=<subdomain>
```

The script:
1. makes a fresh backup;
2. takes a read-only BEFORE profile;
3. runs `kaabe:adopt` **without** activation;
4. takes an AFTER profile;
5. runs the comparison.

On any difference it stops and points to `BACKUP-RESTORE.md` §5. Activation is then a separate click in the Super Admin.
