# Cutover Verification Runbook — `COMMISSIONING_REQUIRE_APPLIED_CONFIG`

**Status:** superseded for the pre-release path — see §0. Nothing deployed to staging or production yet.
**Owner statement (23 Sep 2026):** the branch has not been deployed to staging or production.
**Related:** `CONTRACT_CENTRIC_COMMISSION_ARCHITECTURE_PLAN.md` §6.3, §20

---

## 0. Pre-release path (current situation)

The owner confirms the code has **not been deployed to staging or production**. There is therefore no
environment where contracts are being paid through global/runtime rules via this code, and no legacy
`contract_commission_configurations` rows exist anywhere.

That removes the risk this runbook was written to measure, and makes first deploy the cheapest
possible moment to switch the flag on: with the flag on from day one, no contract is ever silently
served by a global rule, so no habit forms that a later cutover would have to break.

**Approved approach (23 Sep 2026)**

Code default stays `false` (`config/config.php`); no code change is made to flip the default.
The deployment environment sets `COMMISSIONING_REQUIRE_APPLIED_CONFIG=true`.

### First staging / production deployment checklist

```text
1. Deploy code.
2. Run migrations.
3. Run unbound-contracts with commissioning.project_context = real:
     php artisan commissioning:unbound-contracts --company=<ID>
4. Verify every paying/active contract has an applied commission configuration
   (report must not list it).
5. Configure the missing contract rules before the first commissioning run
   (Contract -> Commission Rules; each rule is copied onto the contract).
6. Only then execute the first real commissioning run.
```

Step 3 is meaningless unless `COMMISSIONING_PROJECT_CONTEXT=real`; with `stub` the module reads
fixtures, not the tenant's contracts.

**Expected and intentional:** with the flag on, a contract with no applicable applied configuration
yields `applied_policy_required` on its run items and those members receive **no** commission —
the runtime/global fallback path is deliberately not used. This is architecture enforcement, not an
error condition, and must not be suppressed, retried or worked around in code. The fix is always to
configure the contract (step 5), never to disable the check for that run.

No further cutover mechanism is to be implemented.

**Supporting detail**

1. Keep the code default **OFF** (`config/config.php` → `false`). This keeps the 20 runtime-fallback
   test fixtures valid and leaves the transitional path available for local work.
2. Set `COMMISSIONING_REQUIRE_APPLIED_CONFIG=true` in the **staging and production env files only**.
3. Apply migrations, then run §2 once as a post-deploy check — it should report `N` = however many
   contracts exist without a rule, `M = 0` (there is no legacy data to backfill anywhere).
4. Configure a rule per paying contract (Contract → Commission Rules) until `unbound-contracts`
   reports `contract_count = 0` for contracts that should pay.
5. Only then let the first real run be calculated, approved and posted.

**Consequence to communicate before deploy:** with the flag on, a contract with no contract-owned
rule produces no commission for its members (run items show `applied_policy_required`). Commission
must be configured per contract before the first run — it is no longer possible to rely on a global
rule. This is the intended architecture (§6.3 of the plan), just enforced from the start.

§§1–5 below remain the procedure if the flag is instead left off at first deploy and cut over later.

---

## 1. What has and has not been verified

| Environment | Verified | Result |
|---|---|---|
| Test suite (sqlite, in-memory) | yes | Flag OFF: 130 passed. Flag ON: 110 passed / 20 failed — fixtures that assume runtime fallback. |
| Local dev (`erpflow-backend` → mysql `erpflow`) | yes | 1 company, 4 contracts, **0 commissioning rules, 0 configurations, 0 runs**. `unbound-contracts` → 3 contracts, all `runtime_fallback`; `backfill --dry-run` → 0 rows. |
| **Staging** | **no — no access from the working environment** | unknown |
| **Production** | **no — no access from the working environment** | unknown |

Dev is not evidence for this decision: nothing has ever been calculated there, so nothing depends on the fallback. The decision hinges on whether real contracts are currently paid through global/legacy rules.

---

## 2. Commands to run (read-only)

Run inside the application container / release directory of each environment, as the deploy user.

```bash
# 0. Pending migrations must be applied first — the report reads policy_snapshot.
php artisan migrate:status | grep -i "contract_commission_configurations"
#    expect both:
#      2026_09_21_180000_create_contract_commission_configurations_table   Ran
#      2026_09_22_120000_expand_contract_commission_configurations_...     Ran
#    if the second is Pending, stop: apply migrations before reporting.

# 1. Company ids in scope
php artisan tinker --execute="foreach (\App\Core\Companies\Models\Company::all(['id','slug']) as \$c) { echo \$c->id.' '.\$c->slug.PHP_EOL; }"

# 2. Per company: contracts still relying on live template / runtime fallback
php artisan commissioning:unbound-contracts --company=<ID>

# 3. Per company: legacy rows that would be backfilled (writes nothing)
php artisan commissioning:backfill-policy-snapshots --company=<ID> --dry-run
```

Equivalent HTTP call, if CLI access is unavailable (same data, needs a session with
`commissioning.rule-view` or `project.contracts-view`):

```
GET /api/v1/commissioning/contract-commissions/unbound
    header X-Company-Id: <company uuid>
```

### Config required

| Key | Env var | Value while verifying |
|---|---|---|
| `commissioning.require_applied_config` | `COMMISSIONING_REQUIRE_APPLIED_CONFIG` | **false** (default) — do not change yet |
| `commissioning.project_context` | `COMMISSIONING_PROJECT_CONTEXT` | must be `real` in staging/production; `stub` means the module reads fixtures, and the report is meaningless |

If config is cached, run `php artisan config:clear` (or `config:cache` again) after any env change.

---

## 3. Expected output format

```text
N contract(s) without an active snapshot rule; M legacy row(s) not backfilled.
+-------------+-----------------+--------+-----------+---------+-------------+------------------+
| contract_id | contract_number | status | has_share | members | legacy_rows | resolution       |
+-------------+-----------------+--------+-----------+---------+-------------+------------------+
| 1           | CON-ABC-SEO-001 | active | yes       | 4       | 0           | runtime_fallback |
+-------------+-----------------+--------+-----------+---------+-------------+------------------+
```

`resolution` values:

- `runtime_fallback` — no configuration row at all; today a global rule may be matching.
- `live_template` — a legacy row exists whose policy is still read from the live template.

A second table appears when `M > 0`, listing each row with the reason backfill would skip it
(`not backfilled yet`, `template not active (<status>)`, `template missing`, `orphaned binding`).

Bring back, per environment and per company: the summary line, both tables, and the
`--dry-run` summary line.

---

## 4. Decision matrix

| Result | Meaning | Next step |
|---|---|---|
| `N = 0` and `M = 0` | every paying contract already has an active contract-owned rule | flag can be enabled in that environment; still do §5 staging rehearsal first |
| `M > 0`, reasons all `not backfilled yet` | legacy rows whose templates are active | run backfill without `--dry-run`, re-run the report, expect `M = 0` |
| `M > 0` with `template not active` / `template missing` / `orphaned binding` | backfill cannot guess a policy | fix each: reactivate or replace the template, or add the rule on the contract, then re-run |
| `N > 0` with `runtime_fallback` on contracts that should pay | commission would silently drop to 0 for those members if the flag were on | **do not enable.** Configure a rule per contract (Contract → Commission Rules) or accept that they stop |
| `N > 0` but those contracts should not pay commission | no action needed for them | note them explicitly in the go/no-go, then proceed |
| `commissioning.project_context` is `stub` | the module is not reading real Project data | fix the environment first; the report says nothing useful |

---

## 5. Enabling, when the data says it is safe

Per environment, staging before production:

1. `COMMISSIONING_REQUIRE_APPLIED_CONFIG=true`, then `php artisan config:clear`.
2. Create a draft run per active contract and `calculate` — do not approve or post.
3. Inspect run items: no `eligibility_status = applied_policy_required` other than the ones
   accepted in the §4 go/no-go.
4. Production: enable, then watch the first full run cycle; `unbound-contracts` must stay at 0.

**Rollback:** set the env var back to `false` and `config:clear`. No data migration to undo —
snapshots are already authoritative for bound contracts, and posted runs, ledgers, funds and
payouts are never touched by this flag.

**Not in scope of enabling:** the 20 test fixtures that assume runtime fallback. They only matter
if the flag is ever made the default in code or in `phpunit.xml`; that is a separate decision and
is not required to enable the flag in an environment.
