# Communication Template Configuration — Implementation Tasks

Companion to [COMMUNICATION_TEMPLATE_CONFIG_SPEC.md](./COMMUNICATION_TEMPLATE_CONFIG_SPEC.md) (rev 9) and [COMMUNICATION_TEMPLATE_CONFIG_UI_SPEC.md](./COMMUNICATION_TEMPLATE_CONFIG_UI_SPEC.md) (rev 4). 6 sequential tasks. Work in order — each depends on the previous.

**Total estimate:** ~29 hours (≈ 3.5–4 working days at 8h/day).

---

## Task 001 — All schema migrations

**Estimate:** 5h
**Spec refs:** §3.1, §3.2, §3.3, §6

**Files (new):**

- `db/migrations/<ts>_add-master-config-columns.sql`
- `db/migrations/<ts>_add-clone-columns.sql`
- `db/migrations/<ts>_create-merge-field-catalog.sql`
- `db/migrations/<ts>_seed-merge-field-catalog.sql`
- `db/migrations/<ts>_backfill-step-role-master-id.sql`

**Goal:** Land every schema change + back-fill + seed in one go.

**Acceptance:**

- `communication_templates_master` gains `step VARCHAR(64) NULL`, `target_role VARCHAR(64) NULL`, `target_audience_scope VARCHAR(16) NOT NULL DEFAULT 'ASSIGNED'`, `subject VARCHAR(500) NULL`, `body TEXT NULL`, `is_default BOOLEAN NOT NULL DEFAULT true`.
- `hdb_communication_templates` gains `is_enabled BOOLEAN NOT NULL DEFAULT true`, `master_template_id BIGINT NULL REFERENCES communication_templates_master(id) ON DELETE SET NULL`, `step VARCHAR(64) NULL`, `target_role VARCHAR(64) NULL`, `target_audience_scope VARCHAR(16) NOT NULL DEFAULT 'ASSIGNED'`.
- Drop any older uniqueness on `(program_type_id, template_access_key, template_type)` that omits `target_audience_scope`; rebuild as unique partial index `uq_ctm_pt_ak_type_scope` over `(program_type_id, template_access_key, template_type, target_audience_scope) WHERE is_active = true`.
- Indexes on master: `idx_ctm_step_role_channel` over `(program_type_id, step, target_role, target_audience_scope, template_type) WHERE is_active = true`; `idx_ctm_program_type_default` over `(program_type_id, is_default) WHERE is_active = true`.
- Indexes on clones: `idx_hct_program_step_enabled` over `(program_id, step, is_enabled) WHERE is_enabled = true`; `idx_hct_program_master` over `(program_id, master_template_id)`.
- New table `merge_field_catalog` with all columns from §3.2 + `idx_mfc_active`, `idx_mfc_data_type`.
- Back-fill on `communication_templates_master`: parse `template_access_key` to populate `step` + `target_role` per §3.1 table. Legacy keys keep `step` / `target_role` NULL. All rows back-fill `target_audience_scope = 'ASSIGNED'` and `is_default = true`.
- Back-fill on `hdb_communication_templates`:
  - `step` + `target_role`: same parsing rules as master.
  - `target_audience_scope = 'ASSIGNED'`; `is_enabled` stays `true` for every existing row.
  - `master_template_id`: populate via the join query in §3.3 of the BE spec — match by `(program_type_id from program, template_access_key, template_type, target_audience_scope='ASSIGNED')` on the active master. Rows where no master match exists stay NULL (legacy orphan clones).
- Seed `merge_field_catalog` with all ~55 entries from §6 (initial seed + FE additions); `is_per_recipient = true` for `rm.name`, `rm.email`, `rm.mobile`, `user.phone`; `ON CONFLICT (catalog_key) DO NOTHING`.
- All migrations idempotent on re-run.
- **No new join table.** Defaults live on `master.is_default`.

---

## Task 002 — Enums, constants, entities

**Estimate:** 3h
**Spec refs:** §3.1, §3.2, §3.3, §3.4

**Files:**

- `src/common/enum/communication-step.enum.ts` (new)
- `src/common/enum/communication-audience-scope.enum.ts` (new)
- `src/common/constants/strings-constants.ts` (extend — add `COMMUNICATION_TARGET_ROLES`)
- `src/common/entities/communication-templates-master.entity.ts` (modify)
- `src/common/entities/communication-templates.entity.ts` (modify)
- `src/common/entities/merge-field-catalog.entity.ts` (new)

**Goal:** TS-side mirror of the new schema.

**Acceptance:**

- `CommunicationStepEnum` exports all 31 values from §3.4.
- `CommunicationAudienceScopeEnum` exports `ASSIGNED`, `ALL`.
- `COMMUNICATION_TARGET_ROLES` added next to `ROLE_KEYS`: `['Seeker', ROLE_KEYS.RM, ROLE_KEYS.RELATIONAL_MANAGER, ROLE_KEYS.FINANCE_MANAGER, ROLE_KEYS.SHOBA, ROLE_KEYS.ADMIN, ROLE_KEYS.MAHATRIA] as const`. **No new role enum.**
- `CommunicationTemplatesMaster` entity gains: `step`, `targetRole`, `targetAudienceScope`, `subject`, `body`, `isDefault`.
- `CommunicationTemplates` entity gains: `isEnabled`, `masterTemplateId` (with `@ManyToOne` to `CommunicationTemplatesMaster`), `step`, `targetRole`, `targetAudienceScope`.
- `MergeFieldCatalog` entity created for `merge_field_catalog`; audit fields follow the project pattern.
- App boots clean — no TypeORM sync errors.

---

## Task 003 — Repository methods

**Estimate:** 3h
**Spec refs:** §4.3, §5.2, §5.3

**Files:**

- `src/communication/repositories/communication-templates.repository.ts` (extend)

**Goal:** Data-access layer for the read endpoints + save integration + send-time.

**Acceptance:**

- New methods on `CommunicationTemplatesRepository`:
  - `findByProgramGrouped(programId)` — returns the §4.3 nested `{ steps: [{ value, label, templates: [...] }] }` shape for an existing program. Empty steps filtered out server-side. Sorted by `step ASC, templateAccessKey ASC`.
  - `findByProgramTypeGrouped(programTypeId)` — same shape, but reads from `communication_templates_master` (no clone exists yet) with `isEnabled` projected from `master.is_default`.
  - `applyEnabledSet(programId, enabledMasterTemplateIds, userId)` — runs the §5.3 update: `UPDATE hdb_communication_templates SET is_enabled = (master_template_id = ANY(:ids)) WHERE program_id = :programId AND master_template_id IS NOT NULL`. Idempotent. No-op when no clones match.
- Update existing send-time finder(s) (whatever the current method name on `CommunicationService` resolves) to add `is_enabled = true` to its WHERE clause. Do not break other callers — if a finder is shared by admin / debug listing paths, introduce a new send-time-only method instead of changing the shared one.
- All methods use `handleKnownErrors`.
- The clone code in T005 reads `master.is_default` (or the optional `enabledMasterTemplateIds` override) directly. **No separate defaults repository.**

---

## Task 004 — Catalog computed functions

**Estimate:** 5h
**Spec refs:** §6 FE additions

**Files:**

- `src/communication/service/communication-merge-data.service.ts` (extend)

**Goal:** Add the new computed functions referenced by the catalog seed.

**Acceptance:**

- New functions registered in `computedFunctionRegistry`:
  - `getUserFirstName`, `getUserLastName`, `getUserGender`, `getUserDOB` — resolve via `users` JOIN through registration FK. `getUserDOB` formats `DD-MM-YYYY`.
  - `getRmContactEmail`, `getRmContactMobile` — mirror `getRmContactUserName` lookup path.
  - `getOrgName`, `getOrgHelplineNumber`, `getOrgSupportEmail` — read from program-config block (see [addProgramFormConfig.json:2272](../../src/config/addProgramFormConfig.json#L2272) for keys).
  - `generateApproveLink`, `generateRejectLink`, `generateViewRegistrationLink` — follow `generatePaymentLink` URL pattern (env-var base URL + querystring with `registrationId` / `programId` / signature).
- All functions return empty string on missing data; never throw.
- Existing `getProgramField:*` and `getAllocatedProgramField:*` already cover `program.code`, `program.venue_name`, etc. — no new code needed for those.

---

## Task 005 — Read endpoints + Add Program save integration + clone updates + send-time filter + consumer audit

**Estimate:** 9h
**Spec refs:** §4, §5.2, §5.3

**Files:**

- `src/communication/config/service/communication-config-read.service.ts` (new — handles both read endpoints)
- `src/communication/config/communication-config.controller.ts` (new — exposes the two GETs)
- `src/communication/service/communication-templates-master.service.ts` (modify — clone signature accepts optional `enabledMasterTemplateIds`; copies new master columns + populates `master_template_id`)
- `src/communication/communication.service.ts` (modify — send-time filter on `is_enabled = true`)
- `src/communication/communication.module.ts` (modify — register new providers + entities)
- Add Program controller + service (locate via grep — likely `src/program/program.controller.ts` + `src/program/program.service.ts`):
  - Extend create + update DTOs with optional `communicationConfig`.
  - On create: pass `enabledMasterTemplateIds` to `cloneMultipleMasterTemplatesToProgramByProgramType`.
  - On update: call `applyEnabledSet(programId, enabledMasterTemplateIds)`.
  - Validation: every id must reference an active master with matching `program_type_id` → else 400 `INVALID_MASTER_TEMPLATE_IDS` with the offending ids in `details`.
- Consumer audit (read-only review + selective patches): `src/registration/registration.service.ts`, `src/registration-approval/`, `src/payment/`, `src/messages/`, `src/scheduler/`, `src/communication/communication.controller.ts`.

**Goal:** Read endpoints + Add Program submit integration + clone metadata + send-time filter + consumer audit. **No per-toggle endpoints — save lives on the existing Add Program create/update endpoints.**

**Acceptance:**

- `CommunicationConfigReadService`:
  - `getByProgramType(programTypeId)` — returns §4.3 payload (nested steps→templates). `isEnabled = master.is_default` per row. Empty steps filtered out.
  - `getByProgram(programId)` — same shape. `isEnabled = clone.is_enabled` per row. Includes `masterTemplateId` on every row. Empty steps filtered out.
- `CommunicationConfigController` exposes:
  - `GET /v1/communication-config/by-program-type/:programTypeId`
  - `GET /v1/programs/:programId/communication-config`

  Both behind the admin guard, both with Swagger decorators, both wrapped in `{ success, data, error }`.
- `cloneMultipleMasterTemplatesToProgramByProgramType(programId, programTypeId, enabledMasterTemplateIds?)`:
  - For each active master row matching `program_type_id`: clones into `hdb_communication_templates` with `step`, `target_role`, `target_audience_scope`, `subject`, `body` copied AND `master_template_id = master.id` set AND `is_enabled` computed as: `enabledMasterTemplateIds ? enabledMasterTemplateIds.includes(master.id) : master.is_default`.
  - Single param-shape change. Existing callers without `enabledMasterTemplateIds` keep their current behaviour (fall back to `master.is_default`).
- Add Program controller / service integration:
  - Create DTO accepts optional `communicationConfig: { enabledMasterTemplateIds: number[] }`.
  - On `POST /v1/programs`, the service runs auto-clone with the admin's selections.
  - On `PUT /v1/programs/:id`, the service calls `applyEnabledSet(programId, enabledMasterTemplateIds)`.
  - Both run in a single transaction with the rest of the program save.
  - Validation rejects any id whose master `program_type_id` mismatches the program's; 400 with the offending ids.
- `CommunicationService` send-time finder adds `is_enabled = true` to its WHERE clause. Other consumer modules audited (next bullet).
- Consumer audit: for each of the 6 other modules reading `hdb_communication_templates`, confirm whether each read is for dispatch (add `is_enabled = true` filter) or for admin/debug listing (leave as-is). Document call sites + decisions in the PR.
- Module registers new providers; `TypeOrmModule.forFeature` includes `MergeFieldCatalog`. App boots clean.

---

## Task 006 — Tests

**Estimate:** 4h
**Spec refs:** §9 tests

**Files (new / extended):**

- `src/communication/config/service/communication-config-read.service.spec.ts` (new)
- `src/communication/service/communication-templates-master.service.spec.ts` (extend — clone with `enabledMasterTemplateIds`)
- `src/communication/communication.service.spec.ts` (extend — `is_enabled` filter)
- `test/communication/template-config.e2e-spec.ts` (new — both read endpoints + Add Program save integration)
- Program test file (location follows existing program tests): extend to cover the `communicationConfig` field in create + update bodies.

**Goal:** Cover read endpoints + Add Program save integration + send-time filter + auto-clone behaviour.

**Acceptance scenarios (all must pass):**

### Read endpoints

- `GET /v1/communication-config/by-program-type/:programTypeId` returns nested steps→templates with `isEnabled = master.is_default`; empty steps filtered out.
- `GET /v1/programs/:programId/communication-config` returns the same shape with `isEnabled = clone.is_enabled`; empty steps filtered out.
- Both responses include `masterTemplateId` on every template row.
- Both responses sort templates within a step by `templateAccessKey ASC`.

### Add Program save integration

- `POST /v1/programs` with `communicationConfig.enabledMasterTemplateIds: [17, 19, 23]` creates the program AND inserts clones where `is_enabled = (master.id IN [17, 19, 23])`. Every new clone has `master_template_id` populated.
- `POST` without `communicationConfig` falls back to `is_enabled = master.is_default` per clone.
- `PUT /v1/programs/:id` with `communicationConfig.enabledMasterTemplateIds` updates `is_enabled` on existing clones to match. Clones with `master_template_id IS NULL` (legacy) are untouched.
- `PUT` without `communicationConfig` leaves existing clones untouched.
- Validation: any id whose master `program_type_id` doesn't match the program's → 400 with `INVALID_MASTER_TEMPLATE_IDS` and the offending ids in `details`.
- Save runs in a single transaction with the rest of the program update; rollback on any failure leaves no partial state.

### Send-time

- Disabled row (`is_enabled = false`) → does not fire; other rows still do.
- Zero enabled rows for a step → log + skip; surrounding business operation succeeds.
- `is_per_recipient = true` catalog entries re-resolve per recipient on `ALL`-scope dispatch.

### Auto-clone

- Creating a program with `communicationConfig.enabledMasterTemplateIds` writes clones where `is_enabled = (master.id IN that list)`.
- Creating a program without `communicationConfig` falls back to `is_enabled = master.is_default`.
- Flipping `is_default` on a master row after a program is created does NOT change that program's clone (snapshot semantics).
- Every clone has `master_template_id` populated correctly.

---

## Quick reference

| Task | Hours | What you'll see done |
| --- | --- | --- |
| 001 | 5 | All migrations land; `is_default` on master, `is_enabled` + `master_template_id` on clones; catalog seeded |
| 002 | 3 | Enums + constants + entity updates visible in code; app boots |
| 003 | 3 | New repository methods return correct shapes for read endpoints + the apply-enabled-set update |
| 004 | 5 | New computed functions resolve in merge service |
| 005 | 9 | Read endpoints reachable; Add Program create/update saves `communicationConfig`; clone populates `master_template_id` + reads selections; send-time filter live; consumer audit done |
| 006 | 4 | Test suite passes |
| **Total** | **29** | |

---

## Workflow

Say **"do task 001"** (or any T-number) and I'll:

1. Read the card here + the linked spec sections.
2. Touch only the listed files.
3. Confirm every acceptance bullet before reporting done.

Suggested day plan (3–4 days):

- **Day 1:** T001 + T002 (schema + types). T003 if time permits.
- **Day 2:** T003 (if not done Day 1) + T004 (computed functions).
- **Day 3:** T005 (read endpoints + Add Program integration + clone + send-time + consumer audit — biggest day).
- **Day 4:** T006 (tests + rough edges).

---

## Rev 10 — Per-step template attachment (additional tasks)

> Builds on the rev-9 work above (T001–T006, already implemented). Implements the
> [Rev 10 spec section](./COMMUNICATION_TEMPLATE_CONFIG_SPEC.md): each step shows **all** active
> templates for the program type, the step's **native** templates pre-ticked, and the admin may
> attach any template to any step. Save/read become **per step**; send-time becomes **step-driven**.
> Split into Phase 1 (config + storage + read/save + native gating) and Phase 2 (step-driven dispatch).

**Phase total:** ~30h. Phase 1 ≈ 16h, Phase 2 ≈ 14h.

## Phase 1 — storage, read/save, native gating

### Task R10-1 — Migration: `program_step_template_map` + back-fill existing programs

**Estimate:** 3h · **Spec:** R10.2

**Files:** `database/migrations/<ts>-program-step-template-map.sql`

**Acceptance:**

- Create `program_step_template_map(id, program_id FK→program_v1 ON DELETE CASCADE, step VARCHAR(64), master_template_id FK→communication_templates_master ON DELETE CASCADE, is_enabled BOOLEAN DEFAULT true, audit cols, UNIQUE(program_id, step, master_template_id))` + `idx_pstm_program_step_enabled (program_id, step, is_enabled)`.
- **Back-fill existing programs:** for each existing program `p`, insert one enabled row per active master `m` of `p.program_type_id` where `m.step IS NOT NULL`, with `step = m.step`, `master_template_id = m.id` (i.e. reconstruct the rev-9 native defaults as per-step attachments). `ON CONFLICT DO NOTHING`.
- Idempotent on re-run. Cast enum columns to text where needed (lesson from the rev-9 back-fill).

### Task R10-2 — Entity + repository for the map

**Estimate:** 3h · **Spec:** R10.2, R10.3, R10.4

**Files:** `src/common/entities/program-step-template-map.entity.ts` (new) + barrel; `src/communication/repositories/program-step-template-map.repository.ts` (new)

**Acceptance:**

- `ProgramStepTemplateMap` entity mirrors the table; registered in `entities/index.ts` + `TypeOrmModule.forFeature`.
- Repo methods:
  - `findEnabledByProgram(programId)` → all enabled `(step, masterTemplateId)` rows.
  - `findEnabledMasterIdsByProgramStep(programId, step)` → ids enabled for one step (Phase 2 dispatch + native gate).
  - `seedDefaultsForProgram(programId, programTypeId, userId?)` → insert native defaults (mirror the migration back-fill, used at program create).
  - `replaceForProgram(programId, perStep: {step, enabledMasterTemplateIds}[], userId?)` → upsert enabled rows, disable/delete pairs no longer present, in one transaction.

### Task R10-3 — Read endpoints return all-templates-per-step

**Estimate:** 4h · **Spec:** R10.3

**Files:** `src/communication/config/service/communication-config-read.service.ts` (rewrite grouping)

**Acceptance:**

- For each step of the program type, return **every** active master template of that type, with:
  - `isNativeToStep = (master.step === step.value)`.
  - `by-program-type`: `isEnabled = isNativeToStep`.
  - `by-program`: `isEnabled` = enabled row exists in `program_step_template_map` for `(programId, step, masterTemplateId)`.
- Steps come from the distinct non-null `step` values of the type's active masters (ordered). Templates within a step sorted `templateAccessKey ASC`; native ones may sort first (optional).
- Response adds `isNativeToStep` to every template row (§R10.3).

### Task R10-4 — Per-step save integration + validation

**Estimate:** 4h · **Spec:** R10.4

**Files:** `src/program/dto/create-program.dto.ts` (change `CommunicationConfigDto`), program create/update service, master service helpers

**Acceptance:**

- `CommunicationConfigDto` becomes `{ steps: { step: string; enabledMasterTemplateIds: number[] }[] }` (validated; `step` must belong to the type).
- On **create**: `seedDefaultsForProgram(...)` then `replaceForProgram(...)` with the payload (payload wins where present).
- On **update**: `replaceForProgram(programId, payload.steps)` inside the program transaction.
- Validation: invalid step or invalid master id for the type → `400 INVALID_MASTER_TEMPLATE_IDS` (offenders in `details`). Reuses the existing error code.
- The rev-9 flat `enabledMasterTemplateIds` path is removed from create/update (replaced by per-step).

### Task R10-5 — Native send gating via the map (Phase 1 send behaviour)

**Estimate:** 2h · **Spec:** R10.5 (Phase 1)

**Files:** `src/communication/service/communication-merge-data.service.ts` (`getTemplateWithMergeInfo`)

**Acceptance:**

- Replace the rev-9 `template.isEnabled === false` gate with: resolve the template's master + native `step`, then check `program_step_template_map` for an enabled `(programId, nativeStep, masterTemplateId)` row; if absent → return `null` (skip), exactly as today. Consumers already treat `null` as skip.
- Native templates therefore fire iff still attached+enabled to their own step. Foreign attachments are stored but **not** dispatched yet (Phase 2).

### Task R10-6 — Phase 1 tests

**Estimate:** 4h

**Acceptance:**

- Read: each step lists all type templates; `isEnabled`/`isNativeToStep` correct for both endpoints; empty type → empty steps.
- Save: create seeds defaults then applies payload; update replaces per-step map; invalid id/step → 400.
- Native gate: disabling a native attachment makes its send return `null`; enabling fires.
- Migration back-fill reconstructs native defaults for existing programs.

## Phase 2 — step-driven dispatch (foreign attachments actually fire)

### Task R10-7 — `dispatchTemplatesForStep` service

**Estimate:** 5h · **Spec:** R10.5 (Phase 2)

**Files:** new `src/communication/service/step-dispatch.service.ts`

**Acceptance:**

- `dispatchTemplatesForStep(programId, step, context)`:
  - loads enabled attachments for `(programId, step)` joined to master,
  - for each, resolves recipients from `target_role` + `target_audience_scope` (`ASSIGNED` = the specific person in context; `ALL` = every active user of the role for the program),
  - resolves merge data (reuse `CommunicationMergeDataService`), sends per channel (Email/WhatsApp/SMS),
  - per-template failures are logged and skipped; never throws into the business flow.
- Reuses existing provider send paths; no new provider code.

### Task R10-8 — Wire dispatch into lifecycle events + de-dup native sends

**Estimate:** 6h · **Spec:** R10.5 (Phase 2)

**Files:** registration, registration-approval, payment, invoice, scheduler, messages services (the call sites audited in T005)

**Acceptance:**

- At each lifecycle event, call `dispatchTemplatesForStep(programId, <step>, context)` once.
- Remove/disable the now-redundant hard-coded per-access-key sends for that step so a native template isn't sent twice. (Native gating from R10-5 is superseded by the dispatch path.)
- Behaviour parity for existing (native-only) configs: same templates, same recipients, same content as before this change.

### Task R10-9 — ALL-scope recipient resolution + per-recipient merge

**Estimate:** 3h · **Spec:** §2.2, R10.5

**Files:** `step-dispatch.service.ts`, merge service

**Acceptance:**

- `ALL` scope fans out to every active user holding `target_role` for the program; `is_per_recipient` catalog fields (`rm.*`, `user.phone`) re-resolve per recipient.
- `ASSIGNED` resolves the single contextual recipient.

### Task R10-10 — Phase 2 tests + regression

**Estimate:** 4h

**Acceptance:**

- Foreign template attached to a step fires on that step's event; unattached does not.
- Native-only program: no double-sends; output identical to pre-Phase-2 (regression guard).
- `ALL`-scope dispatch sends to every role holder with per-recipient fields resolved.
- Per-template failure doesn't abort the surrounding business operation.
