# Commissioning Module — Implementation Plan

**Status:** C0–C5 complete on stub; switch `COMMISSIONING_PROJECT_CONTEXT=real` when Project emits payments  
**Epic:** `PMS-COMM-EPIC` (Phase 2)  
**Module:** Standalone `Commissioning` (backend + frontend)  
**Created:** 16 Sep 2026  

---

## 1. Source of truth

| Document | Role |
|---|---|
| [Commissioning Management System — SRS & Dynamic Rule Matrix](../../projects/Commissioning%20Management%20System%20%E2%80%94%20SRS%20%26%20Dynamic%20Rule%20Matrix.md) | Product SRS, rule matrix, engine flow, acceptance |
| [PMS_Commission_Business_Logic_EN.md](./PMS_Commission_Business_Logic_EN.md) | Confirmed PMS business formulas / unresolved items |
| [08-commission-integration.md](../wbs/08-commission-integration.md) | Phase 1 shell boundary vs Commissioning ownership |
| [07-commission-share.md](../wbs/07-commission-share.md) | Team/BD/PD share inputs (Project-owned) |
| [phase1-database-schema.md](../wbs/phase1-database-schema.md) | Project tables Commissioning will consume |
| [11-api.md](../wbs/11-api.md) | Payment cleared event payload sample |
| `.cursor/skills/coding-standard/SKILL.md` | Service–Repository, thin controllers, API envelope |

**Philosophy (SRS §59):**

> Don't hardcode the commission policy; build an engine that executes the policy.

Project provides **facts**. Commissioning executes **policy**.

---

## 2. Strategy: Stub-first → Real connect later

### 2.1 Decision

Project Phase 1 schema is treated as **ready**. Contract/Share/Member **app APIs** and `PaymentCleared` may still be incomplete.

**We do not wait.** Build Commissioning against a **Project Context Port**. Ship with a **Stub adapter**. When Project spine is live, swap to **Real adapter** without rewriting engines.

```text
Commissioning Engines (rules, eligibility, calc, …)
                    │
                    ▼
        ProjectContextPort (interface)
                    │
         ┌──────────┴──────────┐
         ▼                     ▼
 StubProjectContext      RealProjectContext
 (now — fixtures)        (later — Eloquent / APIs)
```

### 2.2 Non-negotiable rules

1. Engines **never** import `Modules\Project\Models\*` directly for business context.
2. All Project reads go through `ProjectContextPort` (or nested ports).
3. Stub DTOs match the **target real payload shape** (especially payment event).
4. Config flag / binding: `commissioning.project_context = stub|real`.
5. Unit/feature tests default to **stub**. Integration suite later uses **real**.

### 2.3 What stub provides (minimum)

| Context slice | Stub content |
|---|---|
| Contract | id, company_id, project_id, service_id, contract_value, currency, start/end, status |
| Share (as-of date) | team/bd/pd %, pool amounts, currency, effective range |
| Members (as-of date) | employee_id, designation_id, division, is_act_as, status, assigned window |
| Payment cleared | company_id, project_id, contract_id, payment_date, amount, applicable_net_base, reference |
| Optional | office expense rule snapshot, project/team labels for reports |

Canonical SRS example fixture:

```text
Contract Value = 100
Team Share = 30% → Team Pool = 30
BD = 40% → 12 | PD = 60% → 18
```

---

## 3. Module boundary

### 3.1 Stays in Project (consume only)

| Asset | Use in Commissioning |
|---|---|
| `contracts` | Value, dates, status, service link |
| `contract_commission_shares` | Pool **inputs** (not earnings) |
| `contract_members` + histories | Who + Act-As role + BD/PD |
| `contract_team_assignments` | Team/division on contract |
| `projects`, `project_service_links`, `pms_services` | Scope + reporting |
| `service_teams`, `team_divisions`, `pms_contract_role_maps` | Scope / validation |
| `office_expense_rules`, PF config tables | Soft inputs (Phase C2+) |
| Phase 1 `commission_rules` shell | **Migrate or supersede** — see §6 |
| Phase 1 `commission_earning_records` shell | **Own in Commissioning** — see §6 |
| `PaymentCleared` emit | Trigger only |

**Do not** use `service_team_members` as commission eligibility (SRS §7). Permanent team ≠ contract role.

### 3.2 Owns in Commissioning

| Capability |
|---|
| Dynamic rule matrix (full SRS dimensions) |
| Rule resolution (scope + priority + effective date) |
| Rule snapshot on generate |
| Eligibility / duration / calculation engines |
| Redistribution (BD release → PD/Company/…) |
| Salary vs Fund allocation |
| Fund unlock + commission fund ledger |
| Commission Run lifecycle (draft → posted) |
| Approval / post / adjustment / reversal |
| Exit settlement on termination |
| Commissioning APIs + FE module |
| Permissions `commission.*` |

### 3.3 Out of scope (do not build here)

- Full payroll / salary engine  
- Statutory PF calculation as payroll product  
- Invoice / client payment collection UI  
- Full accounting GL replacement  

Export posted salary lines / fund balances to Payroll/Finance later via events.

---

## 4. Architecture

### 4.1 Backend layout

```text
backend/Modules/Commissioning/
├── app/
│   ├── Http/Controllers/
│   ├── Http/Requests/
│   ├── Http/Resources/
│   ├── Models/
│   ├── Enums/
│   ├── DTOs/                          # Context + run payloads
│   ├── Repositories/ + Contracts/
│   ├── Services/ + Contracts/
│   │   ├── Rule/
│   │   ├── Engine/
│   │   │   ├── RuleResolver
│   │   │   ├── EligibilityEvaluator
│   │   │   ├── DurationResolver
│   │   │   ├── CalculationEngine
│   │   │   ├── RedistributionEngine
│   │   │   ├── AllocationEngine
│   │   │   └── FundUnlockEngine
│   │   ├── Run/CommissionRunService
│   │   ├── Ledger/LedgerService
│   │   ├── Fund/CommissioningFundService
│   │   └── Settlement/SettlementService
│   ├── Integration/
│   │   ├── Contracts/ProjectContextPort.php
│   │   ├── Stub/StubProjectContext.php
│   │   └── Project/RealProjectContext.php   # later
│   ├── Events/ + Listeners/
│   └── Providers/
├── database/migrations/
├── routes/api.php
└── tests/
```

Follow ERPFlow coding standard:

- Controller → Form Request → **Service Interface** → **Repository Interface**
- Thin controllers, try/catch, standardized API responses
- Business logic only in services/engines
- Tenant: always `company_id`

### 4.2 Engine pipeline (SRS §53)

```text
Commission Request / PaymentCleared / Manual Run
       ↓
Load Context          ← ProjectContextPort
       ↓
Resolve Rules         ← scope + priority + effective date
       ↓
Create Rule Snapshot  ← immutable copy
       ↓
Check Eligibility
       ↓
Determine Period      ← duration from payment/start condition
       ↓
Determine Calc Base   ← contract / team / bd / pd / net / fixed / custom
       ↓
Calculate Gross
       ↓
Apply Redistribution
       ↓
Apply Allocation      ← salary % / fund %
       ↓
Generate Transactions + Ledger
       ↓
Approval → Posting    ← posted = immutable
```

### 4.3 Frontend layout

```text
frontend/src/modules/commissioning/
├── api/
├── types/
├── pages/
│   ├── rules/           # Rule builder + list
│   ├── runs/            # Commission runs
│   ├── earnings/        # Earning detail + audit
│   ├── funds/           # Employee fund + unlock schedule
│   ├── settlements/     # Exit settlement
│   └── reports/
├── components/
└── index.tsx            # routes + nav registration
```

Permissions: `commission.view`, `commission.rule.*`, `commission.calculate`, `commission.approve`, `commission.post`, `commission.adjust`, `commission.settlement.*`, `commission.report.view` (SRS §51).

Project FE keeps share/assignment config. Commissioning FE owns engine screens. Deep-links between modules OK.

---

## 5. Data model (Commissioning-owned)

Conceptual tables (names may be refined in migration design pass):

| Table | Purpose |
|---|---|
| `commissioning_rules` | Full dynamic rule (or evolve Project `commission_rules`) |
| `commissioning_rule_conditions` / JSON conditions | Eligibility & custom predicates |
| `commissioning_rule_snapshots` | Frozen rule at earn time |
| `commission_runs` | Period batch header |
| `commission_run_items` | Per employee/contract line in a run |
| `commission_earnings` | Earning records (from/replace shell) |
| `commission_allocations` | Salary vs fund split lines |
| `commission_redistributions` | Released pool movements |
| `commissioning_funds` | Employee fund balance header |
| `commissioning_fund_unlocks` | Unlock schedule lines (locked → unlocked over time) |
| `commissioning_settlements` | Exit / termination settlements |
| `commission_adjustments` | Reversal / correction (no in-place edit) |

**Immutability:** Posted/finalized rows are not updated in place. Corrections = adjustment/reversal (SRS §50).

**Idempotency:** Unique key intent = `company_id + employee_id + contract_id + period (+ run_id)` (SRS §42).

---

## 6. Decisions to lock during C0–C1

| # | Decision | Options | Recommendation |
|---|---|---|---|
| D1 | Rule table ownership | Expand Project `commission_rules` vs new Commissioning tables | **New Commissioning tables**; keep Project shell read-only or deprecate after migrate |
| D2 | Earning table ownership | Keep in Project vs move | **Commissioning owns**; migrate shell when ready |
| D3 | Employee FK | `employees` vs `employee_personal_infos` | Align with Employee module canonical ID before first earning migration |
| D4 | Office expense | Fixed `amount` vs `% of net` | Confirm with business; stub both until locked |
| D5 | “3 months” boundary | Calendar vs exact day | Pluggable `DurationStrategy`; default documented in C2 |
| D6 | Calc trigger | PaymentCleared only vs + manual run | **Both**: event + manual Commission Run |
| D7 | Stub binding | env / config | `COMMISSIONING_PROJECT_CONTEXT=stub` default in local/CI |

Unresolved business items in `PMS_Commission_Business_Logic_EN.md` must not be assumed — mark `UNRESOLVED` and stub-skip or feature-flag.

---

## 7. Build phases

### Phase C0 — Module skeleton + port ✅ DONE (16 Sep 2026)

**Goal:** Installable module; stub context; permissions; empty engines wired.

| Workstream | Deliverables | Status |
|---|---|---|
| BE | Module `Modules/Commissioning` (manual scaffold; `make:module` unavailable) | ✅ |
| BE | `ProjectContextPort` + `StubProjectContext` + `RealProjectContext` placeholder + DTOs | ✅ |
| BE | Config `commissioning.project_context` (`COMMISSIONING_PROJECT_CONTEXT`) | ✅ |
| BE | Permissions via `config/actions.php` + `CommissioningModuleSeeder` | ✅ |
| BE | `GET /api/v1/commissioning/health` · `GET /api/v1/commissioning/context/preview` | ✅ |
| FE | `frontend/src/modules/commissioning` + nav + `registerModules` | ✅ |
| Test | Stub SRS $100→$30→$12/$18 + health/preview API — **6 passed** | ✅ |

**Exit:** Module boots; health/list ping works; no Project hard dependency. **Met.**

**Key paths**
- Port: `Modules/Commissioning/app/Integration/Contracts/ProjectContextPort.php`
- Stub: `Modules/Commissioning/app/Integration/Stub/StubProjectContext.php`
- Plan continues at **C1** (dynamic rule engine)

---

### Phase C1 — Dynamic Rule Engine (config) ✅ DONE (16 Sep 2026)

**Goal:** Company can configure full rule matrix without hardcoding.

| Workstream | Deliverables | Status |
|---|---|---|
| BE | `commissioning_rules` migration + rich dimensions + JSON configs | ✅ |
| BE | Rule CRUD + activate/deactivate + meta | ✅ |
| BE | `RuleResolver` (Employee > Contract > Team > Service > Company) | ✅ |
| BE | `POST /rules/preview-match` against stub context | ✅ |
| FE | `/commissioning/rules` list + drawer builder + preview | ✅ |
| Test | Priority override, effective date, % XOR fixed — **12 module tests green** | ✅ |

**Exit:** SRS §47 matrix configurable; §58 items 1–2. **Met (config + resolve).**

**APIs**
- `GET/POST /api/v1/commissioning/rules`
- `GET/PUT/DELETE /api/v1/commissioning/rules/{id}`
- `POST …/activate` · `POST …/deactivate`
- `GET …/meta` · `POST …/preview-match`

**Next:** Phase **C2** — Eligibility + Duration + Calculation + Run

---

### Phase C2 — Eligibility + Duration + Calculation + Run (draft/calculate) ✅ DONE (16 Sep 2026)

**Goal:** End-to-end calc on stub payment/context.

| Workstream | Deliverables | Status |
|---|---|---|
| BE | EligibilityEvaluator (probation/confirmation/tenure/division/role) | ✅ |
| BE | DurationResolver (one-time / months / lifetime / contract) | ✅ |
| BE | CalculationEngine (contract/team/bd/pd/collected/fixed) | ✅ |
| BE | Commission Run create → calculate + rule snapshot | ✅ |
| BE | `EmployeeLifecyclePort` stub (probation until 2026-04-01) | ✅ |
| FE | `/commissioning/runs` list/detail + Calculate | ✅ |
| Test | $1.20 BD calc · probation skip · duration cutoff · idempotent recalc — **18 tests green** | ✅ |

**Exit:** SRS §58 items 3–6 (calc path). **Met (stub).**

**Next:** Phase **C3** — Redistribution + Allocation + Approval + Ledger

---

### Phase C3 — Redistribution + Allocation + Approval + Ledger ✅ DONE (16 Sep 2026)

| Workstream | Deliverables | Status |
|---|---|---|
| BE | RedistributionEngine (BD release → Company / PD / split) | ✅ |
| BE | AllocationEngine (salary % / fund %) | ✅ |
| BE | Approve → Post; ledger entries; transaction boundary | ✅ |
| BE | Posted immutability + adjustment API skeleton | ✅ |
| FE | Approve/Post UI; allocation breakdown; ledger view | ✅ |
| Test | BD release options; salary 60 / fund 40; post then rule change does not mutate — **24 tests green** | ✅ |

**Exit:** SRS §58 items 7–8, 11–14 (core). **Met (stub).**

**Next:** Phase **C4** — Vesting + Fund + Settlement

---

### Phase C4 — Fund unlock + Employee fund + Exit settlement ✅ DONE (16 Sep 2026)

| Workstream | Deliverables | Status |
|---|---|---|
| BE | Employee fund balance + fund movement ledger | ✅ |
| BE | Fund unlock schedule (e.g. 30% → 60% → 100% over time) | ✅ |
| BE | Exit settlement (pay unlocked; forfeit or pay still-locked per rule) | ✅ |
| FE | Employee fund page; exit settlement wizard | ✅ |
| Test | Unlock progress; exit settlement scenarios — **27 tests green** | ✅ |

**Exit:** SRS §58 items 9–10. **Met (stub).**

**Language:** UI/API use **fund unlock** / **unlocked** / **still locked** (not “vesting”).

**Next:** Phase **C5** — Real Project connect + reports + exports

---

### Phase C5 — Real Project connect + reports + exports ✅ DONE (16 Sep 2026)

| Workstream | Deliverables | Status |
|---|---|---|
| BE | `RealProjectContext` reading Project tables | ✅ |
| BE | Listener on `PaymentCleared` (+ simulate artisan) | ✅ |
| BE | Switch config `stub → real`; keep stub for tests | ✅ |
| BE | Reports: contract / employee / team / period | ✅ |
| BE | Events out: `CommissionPostedToSalary`, `CommissionFundUpdated` | ✅ |
| FE | Reports screen; stub banner only when driver=stub | ✅ |
| Test | Dual-adapter contract + PaymentCleared + reports — **30 tests green** | ✅ |

**Exit:** Live Project data path ready; default remains stub until Project payment spine emits. **Met (adapter + reports).**

**Switch:** `COMMISSIONING_PROJECT_CONTEXT=real` after Project contracts/shares/members (and payment emit) are populated.

---

## 8. Integration contracts (freeze early)

### 8.1 ProjectContextPort (read)

```text
getContract(companyId, contractId): ContractContext
getShareAsOf(companyId, contractId, date): ShareContext
getMembersAsOf(companyId, contractId, date): MemberContext[]
getServiceForContract(...): ServiceContext
# later
getOfficeExpenseAsOf(...)
```

### 8.2 PaymentCleared (inbound event)

Align with `11-api.md` sample:

```json
{
  "company_id": 1,
  "project_id": 100,
  "contract_id": 900,
  "payment_reference": "PAY-2026-0001",
  "payment_date": "2026-03-01",
  "payment_status": "cleared",
  "payment_amount": "100.00",
  "applicable_net_base": "93.00"
}
```

Phase C2–C4: stub dispatcher / artisan command to fake this event.  
Phase C5: real listener.

### 8.3 Outbound (later)

| Event | Consumer |
|---|---|
| `CommissionPosted` | Audit / reports |
| `CommissionSalaryAllocated` | Payroll |
| `CommissionFundUpdated` | HR/Finance views |

---

## 9. Testing strategy

| Layer | Focus |
|---|---|
| Unit | Pool math, eligibility, duration, redistribution, allocation |
| Feature (stub) | Rule CRUD, run calculate/post, snapshot immutability, idempotency |
| Contract | Stub DTO === Real adapter DTO shape (shared fixture schema) |
| Integration (C5) | Real Project rows + PaymentCleared |

Regression anchors (must stay green):

1. `$100 → team 30 → BD40/PD60 → $12/$18`  
2. Rule change after finalize does not change earning  
3. Duplicate post same period rejected  
4. Probation employee not eligible until confirmation window  

---

## 10. Permissions & tenancy

- Every query scoped by `company_id`
- No cross-tenant contract/member reads
- Authorize via `commission.*` (and existing Project perms only when deep-linking Project UI)
- Never bypass policy for stub convenience in non-local env

---

## 11. Risks & mitigations

| Risk | Mitigation |
|---|---|
| Stub drifts from real Project shape | Shared DTO + contract tests; update stub when Project API locks |
| Slim Phase 1 `commission_rules` insufficient | Commissioning owns rich rules from C1; don’t bolt calc onto shell |
| Employee FK mismatch | Lock D3 before earning migrations |
| Unresolved business rules | Feature-flag; do not invent |
| Premature Payroll coupling | Allocation lines only; Payroll consumes events later |

---

## 12. Suggested execution order (immediate)

1. Write this plan ✅ (this file)  
2. **C0** scaffold module + `ProjectContextPort` + stub fixture  
3. **C1** rule model + resolver + FE builder  
4. **C2** engines + runs on stub  
5. Parallel: Project team finishes Contract/Share/Member APIs + PaymentCleared emit  
6. **C3** redistribution / allocation / approve-post / ledger ✅  
7. **C4** fund unlock / employee fund / exit settlement ✅  
8. **C5** Real adapter swap + reports ✅  

---

## 13. Acceptance gate (epic done)

From SRS §58 — module complete when:

1. Dynamic rules creatable  
2. Scope levels + priority work  
3. BD/PD allocation works  
4. Duration configurable  
5. Probation/confirmation eligibility configurable  
6. Recurring runs while contract active  
7. BD release redistribution configurable  
8. Salary/Fund split works  
9. Vesting configurable  
10. Termination settlement configurable  
11. Historical earnings immutable under old rules  
12. No duplicate generate  
13. Full audit/traceability  
14. Posted not directly editable  
15. New policy without core rewrite  

Plus: **stub→real switch without engine rewrite**.

---

## 14. Related WBS note

Phase 1 cards (`PMS-1.4`, `PMS-2.5`, `PMS-5.1`, `PMS-5.2`) remain Project-owned shells/hooks.

This plan is the execution guide for **`PMS-COMM-EPIC`**. Do not implement full engine inside `Modules/Project`.

---

## 15. Change log

| Date | Change |
|---|---|
| 16 Sep 2026 | Initial stub-first implementation plan |
| 16 Sep 2026 | **C0 complete** — Commissioning module scaffold, ProjectContextPort + Stub, health/preview APIs, FE shell, 6 tests green |
| 16 Sep 2026 | **C1 complete** — `commissioning_rules`, RuleResolver, CRUD + preview-match APIs, Rules UI, 12 tests green |
| 16 Sep 2026 | **C2 complete** — Eligibility/Duration/Calculation engines, Commission Runs + snapshot, Runs UI, 18 tests green |
| 16 Sep 2026 | **C3 complete** — Allocation/Redistribution engines, approve→post+ledger, FE approve/post views, 24 tests green |
| 16 Sep 2026 | **C4 complete** — Fund unlock (plain language), employee funds, exit settlement, Funds UI, 27 tests green |
| 16 Sep 2026 | **C5 complete** — RealProjectContext, PaymentCleared listener, reports, outbound events, 30 tests green |
| 16 Sep 2026 | **Post-C5** — tenant `company_id` for Project reads; real-mode smoke test; PR #299; Payroll `HandleCommissionPostedToSalary` skeleton |
| 16 Sep 2026 | **Payout mode** — rule option `with_salary` \| `separate` for immediate cash; separate payout queue + mark-paid API; salary event only for with_salary |
