# Roadmap: Single Supply Chain — Foundation Media

**Date:** 2026-05-27
**Author:** Michael Rojas
**Status:** Approved — implementing Option B (see [DECISION_LOG.md](DECISION_LOG.md) D24)

---

## Epic Structure

Three separate ACC epics, one per milestone (per DECISION_LOG D29). Cleanup is concentrated in M3 so every intermediate state remains fully functional.

| Epic | Theme | Tickets | Engineering Effort |
|------|-------|---------|--------------------|
| **ACC — Single Supply Chain M1: Backend Decoupling + Manual Workflow** | DDL + backfill + dual-path FF + read-only UI | 23 | 2–3 weeks |
| **ACC — Single Supply Chain M2: Self-Service UI** | Admin CRUD + cascading dropdown + Foundation Media data | 10 | 2–3 weeks |
| **ACC — Single Supply Chain M3: Contract Moves + Cleanup** | Contract moves + P&L + FF removal + DROP legacy column | 13 | 2 weeks |
| **Total** | | **46** | **~6–8 weeks** |

---

## Timeline (Aggressive — Recommended)

```
2026
May         Jun (wk1)   Jun (wk3)   Jul (wk1)   Jul (wk3)
 |              |           |           |           |
 |  M1: DDL +   |  M1 DONE  |           |           |
 |  backfill +  |           |           |           |
 |  FF rollout  |  M2: UI   |  M2 DONE  |           |
 |              |  starts   |           |           |
 |              |           |  M3: Moves|  M3 DONE  |
 |              |           |  + Cleanup|  ALL DONE |
```

M2 backend work (API endpoints, GraphQL types) can start before all of M1 ships — it only needs the junction table to exist (M1.1). M3 starts after the FF has soaked in prod for at least one full accounting cycle.

---

## M1 — Backend Decoupling + Manual Workflow

**Target:** 2–3 weeks. No business blockers.

### DDL (additive only)

| # | Ticket | Depends On |
|---|--------|------------|
| 1.1 | DDL: `CREATE TABLE signing_entity_profit_center` (PK + 2 NOT NULL FKs + composite UNIQUE + soft-delete) | — |
| 1.2 | DDL: `ALTER reference_sap_profit_center ADD COLUMN display_name VARCHAR(255) NULL` + `ADD UNIQUE` on `profit_center` | — |
| 1.3 | DDL: `ALTER contract ADD COLUMN reference_sap_profit_center_id` (nullable + FK + index) | — |
| 1.4 | DDL: `ALTER contract_history ADD COLUMN reference_sap_profit_center_id` (nullable + FK + index) | — |
| 1.5 | DDL: Drop and recreate `after_contract_update` trigger including `reference_sap_profit_center_id` | 1.3, 1.4 |
| 1.6 | DDL: GRANT permissions on `signing_entity_profit_center` to `ows-abacus-contract`, `ows-royalties`, `ows-moneyhub` | 1.1 |

### Feature Flag + python-abacus-models

| # | Ticket | Depends On |
|---|--------|------------|
| 1.7 | Create Harness Feature Flag `single_supply_chain_company_codes` — default OFF | — |
| 1.8 | Regenerate `python-abacus-models` with new `SigningEntityProfitCenter` model + altered `Contract`/`ContractHistory`/`ReferenceSapProfitCenter` (bumpversion) | 1.1, 1.2, 1.3, 1.4 |

### Consumer code refactor (FF-gated, additive — old path still works)

| # | Ticket | Depends On |
|---|--------|------------|
| 1.9 | ows-royalties: refactor `Contract.get_sap_profit_center_by_contract_id()` to branch on FF — OFF reads SE→PC; ON reads `contract.reference_sap_profit_center_id` | 1.7, 1.8 |
| 1.10 | ows-royalties: update tests + fixtures to cover both FF branches | 1.9 |
| 1.11 | `lambda-abacus/sync_contract_sap`: FF-gated dual `Prctr` read path | 1.7, 1.8 |

### Backfill

| # | Ticket | Depends On |
|---|--------|------------|
| 1.12 | Backfill `signing_entity_profit_center` from current `reference_signing_entity` (one row per active SE) | 1.1 |
| 1.13 | Backfill `reference_sap_profit_center.display_name` with placeholder `<legal_name> -- <profit_center>` | 1.2 |
| 1.14 | Verify zero NULLs in `display_name`, then `ALTER MODIFY COLUMN display_name NOT NULL` | 1.13 |
| 1.15 | Backfill `contract.reference_sap_profit_center_id` via `contract → SE → SE.reference_sap_profit_center_id` | 1.3 |
| 1.16 | Backfill `contract_history.reference_sap_profit_center_id` (same join, NULLs allowed for soft-deleted SE history) | 1.4 |
| 1.17 | Verify zero NULLs on `contract.reference_sap_profit_center_id` + spot-check correctness (50-row sample where legacy = new) | 1.15 |
| 1.18 | `ALTER contract MODIFY COLUMN reference_sap_profit_center_id NOT NULL` | 1.17 |

### Production rollout (FF flip)

| # | Ticket | Depends On |
|---|--------|------------|
| 1.19 | Enable FF in staging → smoke test → enable in prod (one accounting cycle soak) | 1.9, 1.11, 1.18 |

### Downstream non-FF switches

| # | Ticket | Depends On |
|---|--------|------------|
| 1.20 | Snowflake `dim_abacus_contract` view: switch to `contract.reference_sap_profit_center_id` join path + verify `signing_entity_profit_center` is replicated | 1.18, 1.19 |
| 1.21 | LookML view updates in `abacus-looker` (10 views) — surface `display_name`; remove SE→PC join references | 1.20 |
| 1.22 | Frontend: display `display_name` + SAP code on contract detail view (read-only, FF-gated) | 1.8 |

### QA

| # | Ticket | Depends On |
|---|--------|------------|
| 1.23 | QA: settlement feed regression test (byte-for-byte vs pre-migration baseline) + FF rollout verification | 1.20 |

### M1 dependency graph

```
1.1 (junction) ─┬─> 1.6 (grants)
                ├─> 1.8 (models) ──┬─> 1.9 (royalties) ──┐
                ├─> 1.12 (backfill SE↔PC)               ├─> 1.19 (FF on)
                └─> 1.20 (view)*                         │   |
                                                        1.11 (lambda) ──┤
1.2 (display_name) ──> 1.13 (backfill) ──> 1.14 (NOT NULL)   |          │
                                                            1.22 (frontend) ──┤
1.3 (contract.PC) ──> 1.15 (backfill) ──> 1.17 (verify) ──> 1.18 (NOT NULL) ──> 1.19
1.4 (history.PC)  ──> 1.16 (backfill)
1.3,1.4 ──> 1.5 (trigger)
1.7 (FF created) ──> 1.9, 1.11, 1.22, 1.19

1.18,1.19 ──> 1.20 (view) ──> 1.21 (LookML) ──> 1.23 (QA) ──> M1 DONE
```

---

## M2 — Self-Service UI

**Target:** 2–3 weeks. Can start once 1.1 (junction table) and 1.8 (models) are done — does not need full M1 sign-off.

| # | Ticket | Depends On |
|---|--------|------------|
| 2.1 | API: `GET /signing-entities/:id/profit-centers` — list active mappings via junction | 1.1, 1.8 |
| 2.2 | API: `POST /signing-entity-profit-centers` (admin) — create mapping with validation against `reference_sap_profit_center` | 1.1, 1.8 |
| 2.3 | API: `PATCH /signing-entity-profit-centers/:id` (admin) — update / soft-delete mapping | 1.1, 1.8 |
| 2.4 | API: `PATCH /reference-sap-profit-centers/:id/display-name` (admin) — update name; reject empty | 1.2, 1.8 |
| 2.5 | GraphQL: `SapProfitCenter` + `SigningEntityProfitCenter` types; extend `AbacusContract` and `SigningEntity` | 2.1 |
| 2.6 | Frontend: cascading dropdown on contract creation (SE → authorized PCs from junction) | 2.5 |
| 2.7 | Frontend: auto-select when SE has exactly one mapping | 2.6 |
| 2.8 | Frontend: admin screen for SE↔PC mapping CRUD + PC display_name edit | 2.2, 2.3, 2.4 |
| 2.9 | Insert Foundation Media / Santa Anna / OVO mappings (manual one-time via admin UI once SAP team confirms new PC codes) | 2.8 |
| 2.10 | Looker report: mappings by SE, contracts by PC, orphan PCs, display_name placeholders | 1.20 |

---

## M3 — Contract Moves + Cleanup

**Target:** 2 weeks. Cannot start until M1.19 has soaked in prod for at least one full accounting cycle and M2 admin UI is live.

### Contract move features (additive)

| # | Ticket | Depends On |
|---|--------|------------|
| 3.1 | API: `PATCH /contracts/:id` accepts `reference_sap_profit_center_id` change; validates (SE, PC) pair exists in junction | 2.8 |
| 3.2 | Confirm `after_contract_update` trigger captures PC change in `contract_history` (added in 1.5) | 1.5, 3.1 |
| 3.3 | When SE changes on contract, force re-selection of PC mapping | 3.1 |
| 3.4 | P&L reconciliation notification — extend existing SE-change flow (D14) to include PC changes | 3.1 |
| 3.5 | Frontend: contract edit with PC change + confirmation modal | 3.1 |
| 3.6 | Audit trail report for PC changes (Looker or admin screen) | 3.2 |

### Cleanup (destructive — all here)

| # | Ticket | Depends On |
|---|--------|------------|
| 3.7 | Remove FF checks from all callsites: ows-royalties, lambda-abacus/sync_contract_sap, frontend-royalties — code uses new path only | M1 done + soak |
| 3.8 | Delete Harness Feature Flag `single_supply_chain_company_codes` | 3.7 |
| 3.9 | DDL: Drop `reference_signing_entity.reference_sap_profit_center_id` column + FK | 3.7 (all consumers off old path) |
| 3.10 | DDL: Update `after_contract_update` trigger to remove any reference to legacy column (if applicable after 3.9) | 3.9 |
| 3.11 | ows-royalties: drop `ReferenceSigningEntity.reference_sap_profit_center` relationship from model; remove old-path code | 3.9 |
| 3.12 | Regenerate `python-abacus-models` after column drop (bumpversion) | 3.9, 3.11 |
| 3.13 | Signing entity dedup (D8 carries D7) — reparent contracts from duplicate SE IDs to canonical | 3.9, M2 admin |

### M3 dependency graph

```
M1 done + soak ──> 3.7 (remove FF checks) ──> 3.8 (delete FF)
                                          ├─> 3.9 (DROP col) ──> 3.10 (trigger) ──┐
                                          └─> 3.11 (royalties cleanup) ──────────┤
                                                                                 ├─> 3.12 (models)
M2 admin UI live ──> 3.1 (PATCH contract) ──> 3.2/3.3/3.4/3.5/3.6 (move features)
                                                                                 │
3.9 ──> 3.13 (SE dedup) ─────────────────────────────────────────────────────────┘
```

---

## Business Follow-ups (Non-blocking)

None of these gate engineering work. All can be resolved with a data update (no code change, no deploy).

| Item | Impact If Delayed | Resolution |
|------|-------------------|------------|
| Display names for ~100 existing PC rows | Placeholder names shown in UI (`legal_name -- profit_center`) | `UPDATE reference_sap_profit_center SET display_name = '...' WHERE ...` |
| Foundation Media / Santa Anna / OVO PC codes | M2.9 ticket blocked until SAP team provides codes | One-time admin entry once codes are known |
| QC process for Finance | Admin screen ships without approval workflow | Add approval step in a follow-up if needed |
| Looker report refinements | Default columns ship; refine based on feedback | Dashboard update only |

---

## Key Milestones & Success Criteria

| Date | Milestone | Success Criteria |
|------|-----------|------------------|
| **~June 6** | M1 complete | All contracts have `reference_sap_profit_center_id` populated and NOT NULL; FF enabled in prod; settlement feed regression clean; `display_name` shown on contract detail view |
| **~June 20** | M2 complete | Contract admins self-service PC selection via cascading dropdown; admin screen live; Foundation Media data inserted; Looker report live |
| **~July 4** | M3 complete | Contracts can be moved between PCs; P&L notifications working; FF deleted; legacy `reference_signing_entity.reference_sap_profit_center_id` column dropped; SE dedup complete |

---

## Acceptance Criteria per Epic (for Jira)

### M1 epic acceptance

- [ ] All DDL applied in prod
- [ ] Backfill complete; zero NULLs on `contract.reference_sap_profit_center_id` and `reference_sap_profit_center.display_name`
- [ ] Spot-check verifies legacy = new for sampled contracts
- [ ] Harness FF created (default OFF)
- [ ] FF enabled in prod for at least 24 hours with no settlement-feed regression
- [ ] Snowflake view switched + LookML views updated
- [ ] Read-only `display_name` visible on contract detail UI

### M2 epic acceptance

- [ ] Admins can create/edit/soft-delete SE↔PC mappings via UI
- [ ] Contract creation cascading dropdown working in prod
- [ ] Foundation Media / Santa Anna / OVO mappings inserted and validated
- [ ] Admins can edit `display_name` (non-empty enforced)
- [ ] Looker report deployed

### M3 epic acceptance

- [ ] Contract PC changes work end-to-end with `contract_history` audit trail
- [ ] P&L notification fires on PC change
- [ ] FF removed from all callsites
- [ ] Harness FF deleted
- [ ] Legacy `reference_signing_entity.reference_sap_profit_center_id` column dropped
- [ ] SE dedup complete; no orphaned contracts
