# Decision Note — BD/PD Commission Earnings Report

**For:** Product Owner
**Date:** 23 Sep 2026
**Decision needed:** which of three directions to take for the surviving BD/PD earnings report.
**Nothing has been implemented.** Q8 commit 1 (`dc80fc45`) removed only the legacy CRUD surface;
both legacy tables are intact and table retirement is **not** approved.

---

## Why this decision is needed

Q8 commit 2 was to drop `commission_rules` and `commission_earning_records`. Inspection showed it
cannot proceed: both tables are still read by a surviving feature — the BD/PD Commission Earnings
report. Table retirement is blocked until this report's future is decided.

## What survives today

| Piece | Detail |
|---|---|
| Routes | `GET /api/project/reports/commission-earnings/bd`, `.../pd` (permission-guarded) |
| Backend | `CommissionEarningReportController` → `CommissionEarningReportService` → `CommissionEarningReportRepository` → `CommissionEarningRecord` model |
| Response | `CommissionEarningRecordResource` — contract, employee, designation, division, `rule_id`, **`rule_name`**, base/percentage/fixed/earning amounts, period, status |
| Data | reads `commission_earning_records`; `rule_name` comes from the `rule` relation → `commission_rules` |
| Tests | `CommissionEarningReportTest` (3 tests, passing) |
| **Frontend** | **none working.** No page calls these endpoints. The Commissioning "Recorded Earnings" tab calls a path that does not exist (see §5) |

So the report is reachable by API and covered by tests, but no user can currently open it.

## Data parity with the current Commissioning source

`commissioning_run_items` already carries nearly the same shape:

| Legacy column | Commissioning equivalent |
|---|---|
| company_id, contract_id, employee_id, designation_id, division | same |
| rule_id, calculation_base_type, base_amount, percentage, fixed_amount, earning_amount | same |
| period_start, period_end, status, meta | same |
| `rule` relation → `rule_name` | `rule_snapshot['name']` — frozen at calculation, so it cannot drift |
| finalized_at | run status `posted` + `commissioning_ledgers` |
| — | additionally: run_id, eligibility_status/reason, rule_snapshot |

Rebuilding the report on Commissioning data is a like-for-like mapping, not a redesign.

---

## Option 1 — Retire the report, then retire the tables

**Affected:** the 2 routes, controller, service, repository (+ interfaces), `CommissionEarningRecordResource`, `GetCommissionEarningReportRequest`, `CommissionEarningReportTest`; then `CommissionRule` and `CommissionEarningRecord` models; then both tables. Frontend: nothing to remove (no working consumer); optionally the broken Commissioning tab in §5.

**Historical data:** lost once the tables are dropped. Any earnings recorded through the old CRUD would no longer be readable anywhere. Owner has stated nobody used the feature, so the expectation is that there is nothing to lose — **verify with a row count per environment before dropping.**

**Migration:** none, beyond the drop migration.

**Rollback:** code is revertible from git; the table drop is not, beyond a dump. Drop must come in its own release, after a dump.

**Dependency on the two tables:** removed entirely — this is the only option that actually unblocks Q8 commit 2.

**Decision required before table retirement:** confirmation that no environment holds rows, and that no one expects a BD/PD earnings report from legacy data.

**Cost:** lowest. **Risk:** losing history that may exist in an environment not yet checked.

---

## Option 2 — Keep the report, rebuild it on Commissioning run/ledger data

**Affected:** repository repointed from `CommissionEarningRecord` to `commissioning_run_items` (filtered `status = included`, joined to runs for period/posted state); resource rewritten to read `rule_snapshot['name']` instead of the `rule` relation; the report probably moves into the Commissioning module next to the other report endpoints; its 3 tests rewritten against run fixtures. Frontend: the §5 mismatch must be fixed for anyone to see it.

**Historical data:** legacy rows are *not* carried over unless they are migrated (see below). The report would show only what the engine calculated.

**Migration:** none required to ship — but if legacy rows must appear in the new report, they need a one-off import into `commissioning_run_items`, which means synthesising runs for them. That is invasive and would put non-engine rows into the run tables; **not recommended**. If legacy history matters, prefer option 3.

**Rollback:** pure code, revertible; no data change.

**Dependency on the two tables:** removed for the report. The tables could then be dropped, subject to the same history question as option 1.

**Decision required before table retirement:** whether legacy rows must remain visible after the switch.

**Cost:** medium. **Risk:** low — the data parity is close, and `rule_snapshot` is strictly better than a live `rule` join (it cannot drift after a template edit).

---

## Option 3 — Keep the report read-only on legacy data; new reporting moves to Commissioning

**Affected:** report code stays as it is, marked explicitly as historical/read-only. Write paths are already gone (commit 1). New reporting is built on Commissioning runs/ledger separately. Frontend: the historical report would need a consumer, or it stays API-only.

**Historical data:** fully preserved and readable — the only option that guarantees that.

**Migration:** none.

**Rollback:** nothing to roll back.

**Dependency on the two tables:** both stay indefinitely. `commission_rules` cannot be dropped even though its CRUD is gone, because `rule_name` resolves through it.

**Decision required before table retirement:** a retention period, after which option 1 or 2 is executed.

**Cost:** zero now. **Risk:** two reporting sources coexist, which is what this whole program set out to remove; and the legacy tables stay as a permanent exception.

---

## Recommendation

**Option 1**, conditional on a row-count check, because the owner states the feature was never used and
the report has no working frontend. If any environment does hold rows, **option 3** as a holding
position, then option 2 when reporting is next worked on — option 2 is a small, like-for-like port
thanks to the column parity above, and it removes the second source of truth for good.

Not recommended: option 2 *with* a legacy import into `commissioning_run_items` — it would put
hand-entered records into the engine's own tables.

## Question for the Product Owner

1. Does any staging/production environment hold rows in `commission_earning_records`? (`SELECT COUNT(*)`, per environment)
2. If yes — must that history stay visible, or is a database dump kept with the release notes enough?
3. Is a BD/PD earnings report still wanted at all, given Commissioning now produces runs, ledger and its own reports?

---

## §5 Separate pre-existing issue — Commissioning "Recorded Earnings" tab is broken

Not caused by Q8 commit 1, and **not fixed in this block** — recorded here so it is not lost.

```text
Frontend calls : /project/reports/bd-pd-commission-earnings
                 (frontend/src/modules/commissioning/api/commissionEarningsApi.ts)
Backend has    : /api/project/reports/commission-earnings/bd
                 /api/project/reports/commission-earnings/pd
```

The path does not exist, so the "Recorded Earnings" tab
(`ProjectReportsPage` → `commissioning/pages/CommissionEarningsPage`, nav entry `shellEarnings`)
cannot load data. Effects:

- the surviving BD/PD report has no working UI consumer today;
- whichever option is chosen, this must be fixed or the tab removed, otherwise the decision changes nothing visible to users;
- under option 1 the tab should be deleted with the report; under options 2 and 3 the path must be corrected and the two endpoints (`/bd`, `/pd`) consumed properly, since the frontend currently expects one combined endpoint.

Tracked as a separate functional issue; no code changed.

---

## §6 Commit 2 pre-flight verification — 23 Sep 2026: **BLOCKED**

Run after the owner clarified that Q8 has not been deployed to production, so history retention is
not a blocker. The five required checks were executed; **no migration was created and no table was
altered**, because check 5 fails.

| # | Check | Result |
|---|---|---|
| 1 | Application consumers of the legacy models | **FAIL — 4 files**: `CommissionEarningReportRepository` (injects `CommissionEarningRecord`, eager-loads `rule`), `CommissionEarningRecordFactory`, `CommissionEarningRecord` model, `CommissionEarningReportTest` |
| 2 | Migration / schema references | PASS — only their own create migrations (`2026_09_14_000006`, `2026_09_14_000018`) mention the tables |
| 3 | `contract_commission_configurations.template_rule_id` depends on legacy `commission_rules`? | PASS — FK targets **`commissioning_rules`**, not the legacy table |
| 4 | Template Administration uses the legacy table? | PASS — `Modules\Commissioning\Models\CommissioningRule` → `commissioning_rules`; `Modules\Project\Models\CommissionRule` → `commission_rules`. Different tables, different modules |
| 5 | No current route/service/repository/model depends on the tables after commit 1 | **FAIL** — `GET /api/project/reports/commission-earnings/bd` and `/pd` (routes/api.php:368, 372) are live, backed by `CommissionEarningReportController → Service → Repository → CommissionEarningRecord → rule → CommissionRule`; `CommissionEarningReportTest` passes 4/4 today |

**Conclusion.** The deployment clarification removes the *data* objection but not the *code* one.
Dropping the two tables now would return 500 from two live endpoints, fail 4 passing tests, and leave
a model and factory pointing at tables that no longer exist. Commit 2 cannot be executed under its
stated scope ("drop only the tables, do not touch anything else") — its own verification gate
forbids it.

**To unblock, one of:**

- **Option 1** (retire the report) — approve a preceding commit that removes the report routes, controller, service, repository + interfaces, `GetCommissionEarningReportRequest`, `CommissionEarningRecordResource`, `CommissionEarningReportTest`, the factory, and the `CommissionRule` / `CommissionEarningRecord` models. Commit 2 then becomes a clean table drop.
- **Option 2** (rebuild on Commissioning run data) — repoint the report first; the tables can be dropped afterwards.
- **Option 3** (keep read-only) — Commit 2 is cancelled; both tables stay.

No code, schema, route or test was changed by this verification.
