# Q8 — Legacy Project Commission Stack: Retirement Plan (plan only, nothing removed)

**Status:** plan only. No code, route, permission, table or test has been changed.
**Owner answers (23 Sep 2026):** nobody uses the legacy feature yet, and the branch is not deployed
to staging or production. §7's gate is therefore satisfied by statement, and §§2, 6, 9 collapse —
see §0. Awaiting one explicit go-ahead before anything is deleted.
**Related:** `CONTRACT_CENTRIC_COMMISSION_ARCHITECTURE_PLAN.md` Q8, §13

---

## 0. Collapsed plan for the pre-release case

Given no users and no deployment, the observation period, deprecation headers, dual-run window,
data archival and retention period all fall away: there is no traffic to observe and no data to
preserve. The staged R1–R9 sequence in §9 exists for the case where this is discovered late; it is
**not** what should be executed now.

**What to execute instead — two commits, one release**

**Commit 1 — remove the feature**

| Layer | Files |
|---|---|
| Routes | the `commission-types`, `commission-rules`, `commission-earning-records`, `commission-earnings` blocks in `Modules/Project/routes/api.php` |
| Controllers | `CommissionRuleController`, `CommissionEarningRecordController`, `CommissionTypeController` |
| Requests | `StoreCommissionRuleRequest`, `UpdateCommissionRuleRequest`, `StoreCommissionEarningRecordRequest` |
| Services | `CommissionRuleService`, `CommissionEarningRecordService` + both interfaces |
| Repository | `CommissionRuleRepository` + interface |
| Models | `CommissionRule`, `CommissionEarningRecord` |
| Provider | the matching bindings in `ProjectServiceProvider` |
| Tests | `CommissionRuleApiTest`, `CommissionEarningRecordTest`, `CommissionTypeApiTest` (36 tests, deleted with the code they cover) |
| Frontend | `pages/CommissionEarningsPage.tsx`, `components/commission-earning/*` (4 files), `api/commissionEarningApi.ts`, `types/commissionEarning.ts`, plus the route entry in `project/index.tsx` and the tab in `pages/ProjectReportsPage.tsx` / `components/ProjectReportsNav.tsx` |

This also clears the 21 pre-existing oxlint warnings, all of which live in
`project/components/commission-earning/*`.

**Commit 2 — drop the tables**

`commission_rules` and `commission_earning_records`, via a normal `down()`-able migration. Safe to do
immediately here because both are empty in dev and the feature was never deployed, so no environment
holds rows. (Confirm with `SELECT COUNT(*)` on each environment that does have the tables before
running it.)

### Blocker found during inspection: the permissions are shared

`project.commission-rules-view` and `project.commission-rules-manage` are **not** exclusive to the
legacy feature. They are also used by the current Commissioning module:

- `frontend/src/modules/core/config/navigation.ts` — the `commissioning-config` nav item
- `frontend/src/modules/commissioning/index.tsx` — route guard
- `frontend/src/modules/commissioning/pages/CommissioningConfigPage.tsx`
- `backend/Modules/Project/routes/api.php` — non-legacy routes in the same file
- `backend/Modules/Project/tests/Feature/ProjectPermissionSeedSmokeTest.php`

Therefore, in `backend/config/actions.php`:

- **keep** the `Commission Rules` group (`commission-rules-view`, `commission-rules-manage`) until those Commissioning references are repointed to `commissioning.*` or `project.contracts-*`
- `earnings-view` / `earnings-manage` appear to be legacy-only — verify with a grep after commit 1, and only then remove that group and re-run the permission sync

Deleting a permission that a role still references can break role editing, so permission removal
stays a separate, later commit either way (§3).

---

## 1. What "legacy" means here

Two overlapping things live in the Project module and predate the Commissioning module:

| Concern | Project (legacy) | Commissioning (current) |
|---|---|---|
| Rule authoring | `commission_rules` table, `CommissionRule` model | `commissioning_rules` (templates) + `contract_commission_configurations` (contract-owned copies) |
| Earnings | `commission_earning_records` table, manual create/finalize/cancel | `commissioning_runs` + `commissioning_run_items` + ledger/funds, calculated by the engine |
| Commission types | `commission-types` endpoint (lookup) | `CommissionType` enum in Commissioning |

The current program never touched any of it, which is why both stacks exist today.

### Inventory (verified by inspection)

**Backend — `backend/Modules/Project/`**

- Models: `CommissionRule`, `CommissionEarningRecord`
- Repositories: `CommissionRuleRepository` + interface
- Services: `CommissionRuleService`, `CommissionEarningRecordService` (+ interfaces)
- Controllers: `CommissionRuleController`, `CommissionEarningRecordController`, `CommissionTypeController`
- Requests: `StoreCommissionRuleRequest`, `UpdateCommissionRuleRequest`, `StoreCommissionEarningRecordRequest`
- Migrations: `2026_09_14_000006_create_commission_rules_table`, `2026_09_14_000018_create_commission_earning_records_table`

**Routes — `Modules/Project/routes/api.php`** (prefix `api/`, so `/api/project/...`)

| Prefix | Methods | Permissions |
|---|---|---|
| `commission-types` | `GET /`, `GET /{code}` | `project.commission-rules-view\|...-manage\|project.projects-manage\|project.projects-view` |
| `commission-rules` | `GET /`, `GET /{id}`, `POST /`, `PUT /{id}`, `DELETE /{id}` | view group as above; writes need `project.commission-rules-manage\|project.projects-manage` |
| `commission-earning-records` | `GET /`, `GET /{id}`, `POST /`, `POST /{id}/finalize`, `POST /{id}/cancel` | `project.earnings-view` / `project.earnings-manage` (+ commission-rules / projects fallbacks) |
| `commission-earnings` | same controller, alias prefix | as above |

**Frontend — `frontend/src/modules/project/`**

- `api/commissionEarningApi.ts`, `types/commissionEarning.ts`
- `components/commission-earning/`: `CommissionEarningList.tsx`, `CommissionEarningForm.tsx`, `CommissionEarningDetailDrawer.tsx`
- `pages/CommissionEarningsPage.tsx`, referenced from `pages/ProjectReportsPage.tsx` and `index.tsx` (routing)

**Tests — 36 passing**: `CommissionRuleApiTest`, `CommissionEarningRecordTest`, `CommissionTypeApiTest`

**Data (local dev only):** `commission_rules` 0 rows; `commission_earning_records` table not present in the dev database. Dev proves nothing about staging/production.

---

## 2. Route / API deprecation

Do not delete endpoints in the first release.

1. **Announce**: mark all four prefixes deprecated in the API docs and in the controller docblocks, naming the replacement (`/api/v1/commissioning/contract-commissions/...` for configuration, `/api/v1/commissioning/runs/...` for earnings).
2. **Observe**: add a deprecation log line (`Log::info('deprecated endpoint', [...])` or the project's existing telemetry) on every legacy endpoint hit, including company id, user id and route name. Keep it for at least one full billing/commission cycle.
3. **Warn**: respond with a `Deprecation` / `Sunset` header on those routes so any internal client surfaces it.
4. **Close writes first**: once §7 shows no writes, remove `POST`/`PUT`/`DELETE` and `finalize`/`cancel`, keeping `GET` for historical reads.
5. **Remove reads last**, together with the frontend removal in the same release.

`commission-types` is a pure lookup; check separately whether anything outside the legacy pages calls it before removing.

## 3. Permission handling

Permissions involved: `project.commission-rules-view`, `project.commission-rules-manage`, `project.earnings-view`, `project.earnings-manage`.

1. Do **not** delete permission rows in the same release as the code. Roles referencing a deleted permission can break role editing screens.
2. Order: remove routes → remove the actions from the module action config so they stop being synced → run the permission sync command → only then delete the orphaned permission rows, in a separate release.
3. Check for reuse first: `project.commission-rules-view` also guards parts of the Commissioning navigation entry (`commissioning-config` item in `navigation.ts`) and some Commissioning routes. It must stay until those references are repointed.
4. Roles that only carry these permissions should be reported to the customer before the permissions disappear.

## 4. Frontend removal order

1. Remove the nav entry / links first (`ProjectReportsPage.tsx` tab, `index.tsx` route) so the page becomes unreachable while the code still exists — easy revert.
2. Next release: delete `pages/CommissionEarningsPage.tsx`, `components/commission-earning/*`, `api/commissionEarningApi.ts`, `types/commissionEarning.ts`.
3. Check `frontend/src/modules/commissioning/pages/CommissionEarningsPage.tsx` — a similarly named page exists in the Commissioning module; confirm which one the sidebar uses before deleting either.
4. This step also clears the 21 pre-existing oxlint warnings, which all live in `project/components/commission-earning/*` and are not worth fixing separately if the files are going away.

## 5. Backend retirement order

Per release, smallest reversible step first:

1. Routes removed (controllers still present).
2. Controllers + Requests deleted.
3. Services + interfaces + provider bindings deleted.
4. Repositories + interfaces deleted.
5. Models deleted last (other code may type-hint them).

Grep gate before each step: no references outside the files being deleted.

## 6. Data / table handling

1. **Never drop a table in the same release as the code.** Keep `commission_rules` and `commission_earning_records` for at least one full retention period after the API is gone.
2. Before any drop, export both tables (`mysqldump` of the two tables per company) and store the dump with the release notes.
3. Prefer rename-then-drop: `commission_rules` → `legacy_commission_rules_YYYYMM` in one release, drop in a later one. A rename is instantly reversible; a drop is not.
4. Check FKs pointing at these tables before renaming (`commission_earning_records` may reference `commission_rules`, contracts and employees).
5. Commissioning data is independent: `commissioning_*` and `contract_commission_configurations` have no FK to these tables. Dropping them cannot affect posted runs, `rule_snapshot`, ledgers or funds.

## 7. Production usage verification (the gate)

Retirement may not start until all of these are answered from real environments:

```sql
-- per environment, per company
SELECT COUNT(*) FROM commission_rules;
SELECT COUNT(*) FROM commission_earning_records;
SELECT status, COUNT(*) FROM commission_earning_records GROUP BY status;
SELECT MAX(created_at), MAX(updated_at) FROM commission_earning_records;
SELECT MAX(created_at), MAX(updated_at) FROM commission_rules;
```

Plus, from the deprecation logging in §2.2, over one full cycle: which companies/users hit the legacy endpoints, read vs write.

Decision rule:

| Finding | Action |
|---|---|
| 0 rows everywhere, no endpoint hits | proceed with §2–§6 |
| rows exist but no recent writes, no endpoint hits | archive data (§6.2), then proceed |
| recent writes or endpoint hits | **stop.** Someone is using it. Needs a migration path from `commission_earning_records` to Commissioning runs, or the feature stays |

## 8. Rollback strategy

| Step | Rollback |
|---|---|
| Nav/link removal | revert the commit |
| Route removal | revert the commit; controllers untouched |
| Code deletion | revert the commit (git is the rollback; no data involved) |
| Permission row deletion | re-run the permission sync command, re-assign to roles from the pre-release export |
| Table rename | rename back |
| Table drop | restore from the §6.2 dump — slow, and the reason drop comes last |

Each release must be independently revertible; never combine a code deletion with a data change.

## 9. Recommended deployment sequence

```text
R1  docs + deprecation headers + endpoint-hit logging          (no behaviour change)
R2  observe one full commission cycle                          (no deploy)
R3  frontend nav/link removal                                  (page unreachable)
R4  legacy write endpoints removed (POST/PUT/DELETE/finalize/cancel)
R5  frontend files deleted; read endpoints removed; controllers/requests deleted
R6  services/repositories/models deleted; module actions cleaned; permission sync
R7  permission rows deleted
R8  tables renamed to legacy_*
R9  tables dropped (after retention period, dump in hand)
```

## 10. Tests and replacement coverage

- The 36 tests (`CommissionRuleApiTest`, `CommissionEarningRecordTest`, `CommissionTypeApiTest`) are deleted with the code they cover, in the same commit — not before, and not weakened to keep them green.
- No replacement tests are needed: they test the legacy stack, not shared behaviour. Contract commission configuration and earnings calculation are already covered by the Commissioning suites (`ContractCommissionConfiguration*`, `ContractCommissionRuleApiTest`, `CommissioningRunApiTest`, `RuleResolverTest`, `CommissionEnginesTest`).
- Before each release, run the full Project and Commissioning suites; the expected diff is only the removed tests.
- If §7 finds real usage and a data migration is built instead, that migration needs its own tests and this plan is replaced.

---

## Open questions for the product owner

1. Is Project → Commission Earnings in use by any customer today?
2. If rows exist in production, must they be migrated into Commissioning runs/ledger, or is read-only archival acceptable?
3. Retention period before dropping the tables?
