# Q5 — Physical Move of Contract Commission Configuration into the Project Module (design only)

**Status:** design only. No migration, model move, route change or frontend change has been made.
**Decision pending:** whether to do this at all (§14 argues it is optional).
**Related:** `CONTRACT_CENTRIC_COMMISSION_ARCHITECTURE_PLAN.md` Q5, §13

---

## 1. Current physical ownership

| Artefact | Lives in |
|---|---|
| Table `contract_commission_configurations` | migrations under `Modules/Commissioning/database/migrations/` (`2026_09_21_180000_create...`, `2026_09_22_120000_expand...`) |
| Model `ContractCommissionConfiguration` | `Modules\Commissioning\Models` |
| Repository + interface | `Modules\Commissioning\Repositories` |
| `ContractCommissionConfigurationService` + interface | `Modules\Commissioning\Services` |
| `EffectiveContractCommissionConfigurationService` + DTOs (`ContractRuleCopy`, `EffectiveContractCommissionConfiguration`) | `Modules\Commissioning` |
| Support `ContractPolicySnapshot` | `Modules\Commissioning\Support` |
| Controller, Requests, Resource | `Modules\Commissioning\Http` |
| Routes | `api/v1/commissioning/contract-commissions[...]` (`Modules/Commissioning/routes/api.php`) |
| Artisan `commissioning:backfill-policy-snapshots`, `commissioning:unbound-contracts` | `Modules\Commissioning\Console` |
| FE API client | `frontend/src/modules/commissioning/api/contractCommissionConfigurationApi.ts` |
| FE panel | `frontend/src/modules/commissioning/components/CommissionConfigurationPanel.tsx`, used by `project/components/contract/ContractDetailDrawer.tsx` and `commissioning/pages/CommissioningContractCommissionPage.tsx` |

Logical ownership is already Project/Contract (the contract drawer is the primary editor, the
Commissioning entry is framed as a shortcut). Only the physical location is in Commissioning.

### Existing coupling

- FK `contract_commission_configurations.contract_id → contracts.id` already crosses modules (the table is in Commissioning, `contracts` is Project-owned) — the move reverses, not removes, this asymmetry.
- FK `template_rule_id → commissioning_rules.id ON DELETE SET NULL` would become the cross-module FK after the move (Project table → Commissioning table).
- `RuleResolver` (Commissioning) reads the repository interface. After the move it must depend on a Project-owned contract, i.e. a port, or Commissioning keeps a read interface that Project implements.

## 2. Target ownership

```text
Project module
  ├─ contracts, contract_members, contract_commission_shares      (already)
  └─ contract_commission_configurations + model/repo/service/API   (moved)

Commissioning module
  ├─ commissioning_rules (templates), runs, items, ledger, funds, settlements
  └─ reads the contract's effective configuration through a port
```

Commissioning must not keep write access to the configuration after the move; otherwise the move
buys nothing.

## 3. Table rename vs new table

| Option | Description | Verdict |
|---|---|---|
| **A. Leave the table name, move only code** | table keeps its name; migrations move to Project | **recommended.** Zero data risk, zero downtime. The table name already reads as Project vocabulary. A migration's directory is metadata, not schema. |
| B. Rename table (`project_contract_commission_configurations`) | cosmetic prefix alignment | not worth it: rename must be coordinated with deploy, breaks any raw SQL/report, and gains nothing functional |
| C. New table + copy + drop old | full physical migration | only justified if the schema changes at the same time. Highest risk: two sources of truth during the copy window, and posted runs reference `configuration_id` in `rule_snapshot` |

**Recommendation: A.** If B/C is chosen for naming reasons, the rest of this document still applies,
plus §5.

### Moving migration files

A migration already run is recorded by filename in the `migrations` table. Moving the *file* between
modules does not re-run it, provided the filename is unchanged and the module's migration path is
still loaded. Verify per environment with `migrate:status` after deploy — a renamed or duplicated
file would attempt to re-run and fail on existing columns.

## 4. FK / index / unique constraints

Current (from the create + expand migrations):

- FKs: `company_id → companies` (cascade), `contract_id → contracts` (cascade), `template_rule_id → commissioning_rules` (set null), `created_by`/`updated_by → users` (set null)
- Indexes: `(company_id, status)`, `(company_id, contract_id, sort_order, id)`, `(company_id, contract_id, status)`
- The original 1:1 unique `(company_id, contract_id)` was **dropped** in Block 2.1 — must not come back

Under option A nothing changes. Under B/C every FK, index and the absence of that unique must be
recreated exactly; a re-introduced unique constraint silently breaks multi-rule contracts.

## 5. Migration / data-copy strategy (only if B or C)

```text
1. create new table with identical columns, indexes, FKs (no unique on company+contract)
2. copy rows with ids preserved (INSERT ... SELECT) — ids appear in posted rule_snapshot
3. deploy code reading the new table, writing both (dual-write window)
4. verify row counts and a checksum per company
5. stop writing the old table
6. rename old table to legacy_*, drop after the retention period
```
Ids must be preserved: `rule_snapshot.configuration_id` and `meta.policy.configuration_id` on posted
run items point at them, and those rows are immutable.

## 6. Rollback strategy

| Step | Rollback |
|---|---|
| Code/namespace move (option A) | revert the commit; table untouched, so rollback is pure git |
| Migration file relocation | revert; confirm `migrate:status` shows the same migration still `Ran` |
| Route change | keep the old route as an alias during the transition (§9), so rollback is a config revert |
| Table rename (B) | rename back |
| New table + copy (C) | switch reads back to the old table — only safe while dual-write is active; after step 5 rollback needs a reverse copy |

Option A is the only variant that is revertible with no data step.

## 7. Backend module / namespace changes

Move, keeping class names:

```text
Modules\Commissioning\Models\ContractCommissionConfiguration            → Modules\Project\Models\
Modules\Commissioning\Repositories\ContractCommissionConfiguration*     → Modules\Project\Repositories\
Modules\Commissioning\Services\ContractCommissionConfigurationService   → Modules\Project\Services\
Modules\Commissioning\Services\EffectiveContractCommissionConfiguration* → Modules\Project\Services\
Modules\Commissioning\DTOs\{ContractRuleCopy, EffectiveContractCommissionConfiguration} → Modules\Project\DTOs\
Modules\Commissioning\Support\ContractPolicySnapshot                    → shared (see below)
Modules\Commissioning\Http\{Controller, Requests, Resource}             → Modules\Project\Http\
Modules\Commissioning\Console\{Backfill, Unbound}Command                → Modules\Project\Console\ (rename signatures to project:*)
```

Bindings move from `CommissioningServiceProvider` to `ProjectServiceProvider`.

**`ContractPolicySnapshot` is the hard part.** It is used by both sides: Project would build
snapshots on apply, Commissioning hydrates them in `RuleResolver` and at post time. Options:

- keep it in Commissioning and let Project depend on it (weakens the boundary), or
- split: Project owns `fromTemplate`/`mergeOverrides` (writing), Commissioning owns `toRule` (reading), with the snapshot array shape as the contract between them — **preferred**, and it makes the shape an explicit interface rather than a shared class.

**New port.** Commissioning currently calls `ContractCommissionConfigurationRepositoryInterface`
directly from `RuleResolver`. After the move it should read through a port next to the existing
`ProjectContextPort`, e.g. `ProjectContextPort::getContractRuleCopiesAsOf(companyId, contractId, asOfDate)`
returning the snapshot arrays. That keeps `RuleResolver` free of Project models and lets the stub
adapter fake it in tests.

## 8. API / route changes

| Now | After |
|---|---|
| `GET /api/v1/commissioning/contract-commissions` | `GET /api/project/contracts/commission-configurations` (or keep a Commissioning read-only list for ops) |
| `GET /api/v1/commissioning/contract-commissions/{contractId}` | `GET /api/project/contracts/{contractId}/commission` |
| `PUT .../{contractId}/policy` (BC) | `PUT /api/project/contracts/{contractId}/commission/policy` |
| `POST .../{contractId}/rules` | `POST /api/project/contracts/{contractId}/commission/rules` |
| `PUT/DELETE .../{contractId}/rules/{configurationId}` | same under the Project prefix |
| `GET .../contract-commissions/unbound` | ops report — may stay in Commissioning |

Permissions: prefer the Project set (`project.contracts-manage` / `project.contracts-view`) as
primary, keeping the Commissioning permissions accepted for one release so existing roles keep working.

## 9. Compatibility / deprecation strategy

1. Release 1: new Project routes added, old Commissioning routes kept as thin forwards to the same controller. Both work.
2. Release 2: FE switched to the new routes; old routes respond with a `Deprecation` header and log hits.
3. Release 3: old routes removed after one cycle with no hits.

The `policy` (primary row) key in the response payload stays alongside `rules[]` until the FE no
longer reads it — that deprecation is already open from Block 2.3 and is independent of this move.

## 10. Frontend changes

1. Move `contractCommissionConfigurationApi.ts` to `frontend/src/modules/project/api/` and repoint URLs.
2. Move `CommissionConfigurationPanel.tsx` to `project/components/contract/`. `ContractDetailDrawer` then imports a sibling instead of reaching into the commissioning module — the boundary improvement this move is for.
3. `commissioning/pages/CommissioningContractCommissionPage.tsx` keeps working by importing the panel from Project, or is reduced to a redirect into the contract drawer.
4. Run the style-boundary script; cross-module imports may be what it checks.

## 11. Posted-run and `rule_snapshot` compatibility

Non-negotiable:

- `commissioning_run_items.rule_snapshot` and `meta.policy` are historical records containing `configuration_id` and `template_rule_id`. Nothing in this move may rewrite them.
- Those ids must keep pointing at the same rows → ids preserved (§5), and no FK is added from run items to the configuration table (there is none today; do not add one).
- A configuration row deleted after a run was posted must keep the run readable — already true, because the snapshot is self-contained.
- Post-time redistribution/BD-duration reads the item snapshot (Phase 7), so it does not depend on the configuration table at all. Verify this stays true after the port is introduced.

## 12. Deployment ordering

```text
1. code move + migration file relocation + new routes as aliases   (option A: no schema change)
2. migrate:status verified on every environment
3. FE repointed to the new routes
4. full Commissioning + Project suites, then a draft calculate on a real contract
5. old routes deprecated, then removed one cycle later
```

Single release for step 1 is acceptable only because option A touches no schema. B or C insert the
§5 sequence before step 3.

## 13. Verification plan

- `Modules/Commissioning` and `Modules/Project` suites green (baseline: 130 / 328).
- `RuleResolverTest` must be rewritten against the new port; it currently mocks the repository interface. This is the largest test change in the move.
- `ContractCommissionConfiguration*` tests move to the Project suite with namespaces updated.
- A posted run created before the move must still read identically after it (add an explicit test that posts, moves, then re-reads — or verify against a staging snapshot).
- `migrate:status` on each environment shows no pending/re-running migrations.
- `commissioning:backfill-policy-snapshots --dry-run` reports 0 rows both before and after.

## 14. Risks and recommendation

| Risk | Mitigation |
|---|---|
| Moved migration file re-runs or is reported pending | keep filenames identical; verify `migrate:status` per environment before anything else |
| `RuleResolver` test rewrite hides a behaviour change | port returns the same snapshot arrays; keep the existing assertions, change only the mock |
| Cross-module FK direction flips (Project table → `commissioning_rules`) | acceptable; it is provenance only and already `ON DELETE SET NULL` |
| `ContractPolicySnapshot` shared by both modules | split write/read halves (§7) |
| Churn with no user-visible benefit | see below |

**Recommendation: defer, or take only the frontend half.** The move's real benefit is the FE
boundary (§10.2: `ContractDetailDrawer` importing from the commissioning module) and clearer
ownership for future maintainers. The backend move costs a new port, a `RuleResolver` test rewrite
and a route deprecation cycle, while changing nothing a user can see. If the boundary is the goal,
doing §10 alone captures most of the value at a fraction of the risk.

### Deferred within this design

- Table rename (option B) and new-table migration (option C) — not recommended.
- Moving `commissioning_rules` (templates) — stays in Commissioning by Q1/Q7; not part of Q5.
- Merging the unbound/backfill commands into Project — they are ops tooling; leaving them in Commissioning is fine.
