|
| 1 | +# NHS Notify Supplier API Event Schemas |
| 2 | + |
| 3 | +This internal package defines CloudEvents-compatible schemas (with Zod) for the Supplier API domain – currently focusing on Letter Status Change events. It provides: |
| 4 | + |
| 5 | +* A reusable CloudEvents envelope profile (`$EnvelopeProfile`) |
| 6 | +* Domain model schemas for letter status transitions (`$LetterStatus`, `$LetterStatusChange`) |
| 7 | +* Concrete per-status event schemas with strict `type`, `dataschema` URI and semantic version validation |
| 8 | +* Utilities to programmatically access all status change event schemas (`statusChangeEvents`) |
| 9 | + |
| 10 | +> NOTE: This package is private and published only to the internal GitHub Packages registry. Do not reference it externally; consume it within this mono‑repo or via internal pipelines. |
| 11 | +
|
| 12 | +--- |
| 13 | + |
| 14 | +## Directory Structure |
| 15 | + |
| 16 | +```text |
| 17 | +src/ |
| 18 | + domain/ |
| 19 | + letter-status-change.ts # Domain model and status enum |
| 20 | + events/ |
| 21 | + envelope-profile.ts # CloudEvents base envelope extensions & constraints |
| 22 | + letter-status-change-events.ts # Per status event schema generation |
| 23 | + cli/ # CLI scripts for bundling / codegen |
| 24 | + index.ts # (re-)exports (not shown above if generated later) |
| 25 | +``` |
| 26 | + |
| 27 | +--- |
| 28 | + |
| 29 | +## Concepts |
| 30 | + |
| 31 | +### 1. Envelope Profile (`$EnvelopeProfile`) |
| 32 | + |
| 33 | +> NB: this will be replaced with a common schema in future published in the nhs-notify-standards repo |
| 34 | +
|
| 35 | +Defines the constrained CloudEvents 1.0 envelope used across Notify. It enforces: |
| 36 | + |
| 37 | +* `specversion` fixed to `1.0` |
| 38 | +* Reverse‑DNS `type` pattern starting `uk.nhs.notify.` with prohibited ambiguous verbs |
| 39 | +* Structured `source` and `subject` path formats (with additional subject shape rules for `/data-plane` sources) |
| 40 | +* Trace context (`traceparent`, optional `tracestate`) |
| 41 | +* Optional classification / regulation tags |
| 42 | +* Consistency rules (e.g. severity text ↔ number mapping) |
| 43 | + |
| 44 | +### 2. Letter Status Domain |
| 45 | + |
| 46 | +`letter-status-change.ts` introduces: |
| 47 | + |
| 48 | +* `$LetterStatus` enumeration covering lifecycle states: |
| 49 | + `PENDING | ACCEPTED | REJECTED | PRINTED | ENCLOSED | CANCELLED | DISPATCHED | FAILED | RETURNED | DESTROYED | FORWARDED | DELIVERED` |
| 50 | +* `$LetterStatusChange` domain object, extending a `DomainBase('LetterStatusChange')` (see helpers package) with: |
| 51 | + * `domainId` (branded identifier) |
| 52 | + * `sourceSubject` – original resource subject |
| 53 | + * `status` – one of `$LetterStatus` |
| 54 | + * Optional `reasonCode`, `reasonText` |
| 55 | + |
| 56 | +### 3. Per‑Status Event Schemas |
| 57 | + |
| 58 | +`letter-status-change-events.ts` programmatically creates a schema per status by extending `$EnvelopeProfile` and replacing `data` with the domain payload. Each schema enforces: |
| 59 | + |
| 60 | +* `type = uk.nhs.notify.supplier-api.letter-status.<STATUS>.v1` |
| 61 | +* `dataschema` matches: `https://notify.nhs.uk/events/supplier-api/letter-status/<STATUS>/1.<minor>.<patch>.json` |
| 62 | +* `dataschemaversion` uses semantic version with major fixed to `1` (`1.x.y`) |
| 63 | +* `data.status` literal‑locked to the matching status |
| 64 | + |
| 65 | +The export `statusChangeEvents` is a dictionary keyed by `letter-status.<STATUS>`. |
| 66 | + |
| 67 | +--- |
| 68 | + |
| 69 | +## Installation (Internal) |
| 70 | + |
| 71 | +Inside this mono‑repo other internal packages should depend on it by name: |
| 72 | + |
| 73 | +```jsonc |
| 74 | +// package.json |
| 75 | +"dependencies": { |
| 76 | + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*" |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +External `npm install` instructions are intentionally omitted (private package). |
| 81 | + |
| 82 | +--- |
| 83 | + |
| 84 | +## Usage Examples |
| 85 | + |
| 86 | +### Validating a Raw Event |
| 87 | + |
| 88 | +```typescript |
| 89 | +import { statusChangeEvents } from '@nhsdigital/nhs-notify-event-schemas-supplier-api'; |
| 90 | + |
| 91 | +const schema = statusChangeEvents['letter-status.PRINTED']; |
| 92 | +const parsed = schema.safeParse(incomingEventJson); |
| 93 | +if (!parsed.success) { |
| 94 | + // handle validation failure (log / DLQ) |
| 95 | + console.error(parsed.error.format()); |
| 96 | +} else { |
| 97 | + const evt = parsed.data; // strongly typed |
| 98 | + console.log(`Letter ${evt.data.domainId} moved to ${evt.data.status}`); |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +### Validating a generic letter-status.* event |
| 103 | + |
| 104 | +```typescript |
| 105 | +```typescript |
| 106 | +import { $LetterStatusChangeEvent } from '@nhsdigital/nhs-notify-event-schemas-supplier-api'; |
| 107 | +
|
| 108 | +function validateLetterStatusEvent(e: unknown) { |
| 109 | + const result = $LetterStatusChangeEvent.safeParse(e); |
| 110 | + if (!result.success) { |
| 111 | + // handle validation failure (log / DLQ) |
| 112 | + return { ok: false as const, error: result.error }; |
| 113 | + } |
| 114 | + // event is strongly typed |
| 115 | + return { ok: true as const, event: result.data }; |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +### Creating a New Letter Status Event Instance |
| 120 | + |
| 121 | +```typescript |
| 122 | +import { statusChangeEvents } from '@nhsdigital/nhs-notify-event-schemas-supplier-api'; |
| 123 | +import { randomUUID } from 'crypto'; |
| 124 | +
|
| 125 | +const status = 'ACCEPTED' as const; |
| 126 | +const schema = statusChangeEvents[`letter-status.${status}`]; |
| 127 | +
|
| 128 | +const event = { |
| 129 | + specversion: '1.0', |
| 130 | + id: randomUUID(), |
| 131 | + source: '/data-plane/supplier-api', |
| 132 | + subject: 'customer/1b20f918-bb05-4c78-a4aa-5f6a3b8e0c91/letter/4a5a9cb5-1440-4a12-bd72-baa7cfecd111', |
| 133 | + type: 'uk.nhs.notify.supplier-api.letter-status.ACCEPTED.v1', |
| 134 | + time: new Date().toISOString(), |
| 135 | + dataschema: 'https://notify.nhs.uk/events/supplier-api/letter-status/ACCEPTED/1.0.0.json', |
| 136 | + dataschemaversion: '1.0.0', |
| 137 | + data: { |
| 138 | + domainId: 'abc123', |
| 139 | + sourceSubject: 'customer/.../letter/...', |
| 140 | + status: 'ACCEPTED', |
| 141 | + }, |
| 142 | + traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', |
| 143 | + recordedtime: new Date().toISOString(), |
| 144 | + severitytext: 'INFO', |
| 145 | + severitynumber: 2, |
| 146 | +}; |
| 147 | +
|
| 148 | +schema.parse(event); // throws if invalid |
| 149 | +``` |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## Versioning & Dataschema |
| 154 | + |
| 155 | +* Major version locked at `1` for current lineage. |
| 156 | +* Minor + patch increments should reflect additive / backwards compatible changes to the data payload. |
| 157 | +* `dataschema` URIs must be updated in lockstep with `dataschemaversion` when publishing new schema variants. |
| 158 | + |
| 159 | +Automated generation tasks (below) assist bundling and JSON Schema emission. |
| 160 | + |
| 161 | +--- |
| 162 | + |
| 163 | +## Scripts |
| 164 | + |
| 165 | +| Script | Purpose | |
| 166 | +|--------|---------| |
| 167 | +| `npm run build` | TypeScript compile to `dist/` | |
| 168 | +| `npm test` / `npm run test:unit` | Run Jest unit tests | |
| 169 | +| `npm run gen:asyncapi` | Bundle AsyncAPI sources into `dist/asyncapi` | |
| 170 | +| `npm run gen:jsonschema` | Emit JSON Schemas derived from Zod definitions | |
| 171 | +| `npm run lint` | Lint code + schema (Spectral) | |
| 172 | +| `npm run lint:fix` | Auto-fix lint issues | |
| 173 | + |
| 174 | +Execution order helpers: |
| 175 | + |
| 176 | +* `prebuild` ensures a clean `dist` and generates asyncapi bundle |
| 177 | +* `prelint:schema` generates JSON prior to Spectral validation |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## Adding New Event Types (Future) |
| 182 | + |
| 183 | +1. Extend the domain model under `src/domain/` |
| 184 | +2. Add a generator similar to `letter-status-change-events.ts` |
| 185 | +3. Ensure `type` naming: `uk.nhs.notify.supplier-api.<area>.<action>.v1` |
| 186 | +4. Provide deterministic `dataschema` pattern with semantic versioning |
| 187 | +5. Export via `src/index.ts` |
| 188 | +6. Add unit tests & update documentation |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## Validation Philosophy |
| 193 | + |
| 194 | +Rules aim for early rejection of: |
| 195 | + |
| 196 | +* Ambiguous or post-hoc semantic verbs ("completed", "updated", etc.) in event `type` |
| 197 | +* Poorly structured routing metadata (`source`, `subject`) |
| 198 | +* Inconsistent severity pairings |
| 199 | +* Non-conformant trace context |
| 200 | + |
| 201 | +This reduces downstream consumer ambiguity and improves observability correlation. |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## License |
| 206 | + |
| 207 | +MIT (internal usage only) |
| 208 | + |
| 209 | +--- |
| 210 | + |
| 211 | +## Support |
| 212 | + |
| 213 | +Raise questions via the repository discussions or internal channel referencing the `events` package. |
0 commit comments