@tantainnovative/ndpr-recipes 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @tantainnovative/ndpr-recipes
2
2
 
3
- Backend recipes for NDPA compliance with [@tantainnovative/ndpr-toolkit](https://github.com/tantainnovative/ndpr-toolkit).
3
+ Backend recipes for Nigeria NDPA 2023 / NDPC GAID 2025 compliance with [@tantainnovative/ndpr-toolkit](https://github.com/mr-tanta/ndpr-toolkit).
4
4
 
5
5
  ## What is this?
6
6
 
@@ -23,9 +23,11 @@ This package is a **reference implementation** — not a library to install. Cop
23
23
  | DSR request persistence | Prisma adapter, Drizzle adapter |
24
24
  | Breach report persistence | Prisma adapter |
25
25
  | ROPA persistence | Prisma adapter |
26
- | Next.js App Router | Consent, DSR, Breach, ROPA, Compliance route handlers |
27
- | Express | Full NDPR router with consent, DSR, breach, ROPA, compliance routes |
26
+ | Next.js App Router | Consent, DSR, Breach, ROPA, Compliance, Registration route handlers |
27
+ | Express | Full NDPR router with consent, DSR, breach, ROPA, compliance, registration routes |
28
28
  | Consent middleware | Next.js edge middleware + Express middleware |
29
+ | GAID 2025 (DCPMI + CAR) | `/registration` route — tier classification + Compliance Audit Return schedule |
30
+ | Breach Article-33 readiness | Breach detail routes return which NDPC notification fields are still missing |
29
31
 
30
32
  ---
31
33
 
@@ -41,19 +43,27 @@ This package is a **reference implementation** — not a library to install. Cop
41
43
  | `src/adapters/prisma-ropa.ts` | Prisma `StorageAdapter<RecordOfProcessingActivities>` |
42
44
  | `src/adapters/drizzle-consent.ts` | Drizzle `StorageAdapter<ConsentSettings>` |
43
45
  | `src/adapters/drizzle-dsr.ts` | Drizzle `StorageAdapter<DSRRequest[]>` |
46
+ | `src/adapters/drizzle-breach.ts` | Drizzle `StorageAdapter<BreachState>` |
47
+ | `src/adapters/drizzle-ropa.ts` | Drizzle `StorageAdapter<RecordOfProcessingActivities>` |
48
+ | `src/adapters/drizzle-dpia.ts` | Drizzle `StorageAdapter<DPIAResult[]>` |
49
+ | `src/adapters/drizzle-lawful-basis.ts` | Drizzle `StorageAdapter<ProcessingActivity[]>` |
50
+ | `src/adapters/drizzle-cross-border.ts` | Drizzle `StorageAdapter<CrossBorderTransfer[]>` |
44
51
  | `src/nextjs/app-router/api/consent/route.ts` | Next.js consent API route |
45
52
  | `src/nextjs/app-router/api/dsr/route.ts` | Next.js DSR API route |
46
53
  | `src/nextjs/app-router/api/breach/route.ts` | Next.js breach API route |
54
+ | `src/nextjs/app-router/api/breach/[id]/route.ts` | Next.js breach detail route — returns GAID 2025 Art. 33 readiness |
47
55
  | `src/nextjs/app-router/api/ropa/route.ts` | Next.js ROPA API route |
48
56
  | `src/nextjs/app-router/api/compliance/route.ts` | Next.js compliance score API route |
57
+ | `src/nextjs/app-router/api/registration/route.ts` | Next.js DCPMI tier + CAR schedule route (GAID 2025) |
49
58
  | `src/nextjs/app-router/middleware.ts` | Next.js consent gate middleware |
50
59
  | `src/nextjs/app-router/layout-example.tsx` | Full wiring example for App Router |
51
60
  | `src/express/index.ts` | Express router factory — mounts all routes |
52
61
  | `src/express/routes/consent.ts` | Express consent router |
53
62
  | `src/express/routes/dsr.ts` | Express DSR router |
54
- | `src/express/routes/breach.ts` | Express breach router |
63
+ | `src/express/routes/breach.ts` | Express breach router — `GET /:id` returns GAID 2025 Art. 33 readiness |
55
64
  | `src/express/routes/ropa.ts` | Express ROPA router |
56
65
  | `src/express/routes/compliance.ts` | Express compliance score router |
66
+ | `src/express/routes/registration.ts` | Express DCPMI tier + CAR schedule router (GAID 2025) |
57
67
  | `src/express/middleware/consent-check.ts` | Express consent gate middleware |
58
68
 
59
69
  ---
@@ -307,6 +317,7 @@ This mounts:
307
317
  | `GET/POST/PATCH /api/ndpr/breach` | Breach notification |
308
318
  | `GET/POST/PATCH /api/ndpr/ropa` | Record of Processing Activities |
309
319
  | `GET /api/ndpr/compliance` | Compliance score |
320
+ | `GET /api/ndpr/registration` | DCPMI tier + CAR schedule (GAID 2025) |
310
321
 
311
322
  ### Consent middleware (route protection)
312
323
 
@@ -373,6 +384,46 @@ The `apiAdapter` hits your `/api/consent` route handler (from `src/nextjs/app-ro
373
384
 
374
385
  ---
375
386
 
387
+ ## GAID 2025 — DCPMI registration & breach readiness
388
+
389
+ The NDPC's General Application and Implementation Directive (GAID) 2025 added
390
+ obligations the original recipes predate. Two recipes cover them, both built on
391
+ the toolkit's React-free `/server` utilities (no extra database tables needed).
392
+
393
+ ### DCPMI tier + Compliance Audit Return (`/registration`)
394
+
395
+ `classifyDCPMI` derives your registration tier and annual fee from the number of
396
+ data subjects you process in a six-month window; `generateComplianceAuditReturn`
397
+ derives the filing schedule for those that must file:
398
+
399
+ - **UHL** (> 5,000 subjects) — ₦250,000/yr, files a **CAR annually**
400
+ - **EHL** (1,000–5,000) — ₦100,000/yr, files a **CAR annually**
401
+ - **OHL** (200–999) — ₦10,000/yr, **renews registration** (no CAR)
402
+
403
+ ```ts
404
+ // GET /api/registration?dataSubjects=6200&commencementDate=2025-01-15
405
+ import { classifyDCPMI, generateComplianceAuditReturn } from '@tantainnovative/ndpr-toolkit/server';
406
+
407
+ const classification = classifyDCPMI({ dataSubjectsInSixMonths: 6200 });
408
+ const auditReturn = generateComplianceAuditReturn({
409
+ commencementDate: '2025-01-15',
410
+ tier: classification.tier, // CAR applies to UHL/EHL only
411
+ });
412
+ ```
413
+
414
+ > Thresholds, fees, and deadlines follow the NDPC GAID 2025 baseline and can
415
+ > change — verify against current NDPC guidance before relying on them.
416
+
417
+ ### Breach Article-33 readiness
418
+
419
+ The breach detail routes (`GET /api/breach/[id]` in Next.js, `GET /breach/:id`
420
+ in Express) now return an `ndpcReadiness` object via `assessBreachNotification` —
421
+ which GAID 2025 Article 33(5) notification fields are still missing and how many
422
+ hours remain on the 72-hour clock — so you know what to collect before filing.
423
+ Set `NDPR_DPO_NAME` / `NDPR_DPO_EMAIL` to record the contact point.
424
+
425
+ ---
426
+
376
427
  ## Database Schema
377
428
 
378
429
  ### Tables
@@ -380,7 +431,7 @@ The `apiAdapter` hits your `/api/consent` route handler (from `src/nextjs/app-ro
380
431
  | Table | Description | NDPA reference |
381
432
  |---|---|---|
382
433
  | `ndpr_consent_records` | Immutable consent audit trail. `revokedAt` marks withdrawal — rows are never deleted. | §25–26 |
383
- | `ndpr_dsr_requests` | Data subject rights requests. Tracks type, status, and 30-day response deadline. | Part IV §2936 |
434
+ | `ndpr_dsr_requests` | Data subject rights requests. Tracks type, status, and 30-day response deadline. | Part VI §3438 |
384
435
  | `ndpr_breach_reports` | Breach incident records with 72-hour NDPC notification tracking. | §40 |
385
436
  | `ndpr_processing_records` | Record of Processing Activities (ROPA). | Accountability principle |
386
437
  | `ndpr_audit_log` | Append-only compliance event log. | §44 |
@@ -396,7 +447,7 @@ The consent table follows an immutable-audit pattern: when a subject updates or
396
447
  | Module | NDPA provision |
397
448
  |---|---|
398
449
  | Consent | Sections 25–26 (lawful basis, consent withdrawal) |
399
- | Data Subject Rights | Part IV, Sections 2936 (access, erasure, portability, etc.) |
450
+ | Data Subject Rights | Part VI, Sections 3438 (access, erasure, portability, etc.) |
400
451
  | Breach Notification | Section 40 (72-hour notification to NDPC) |
401
452
  | ROPA | Accountability principle; Schedule 1, Part 1 |
402
453
  | Audit Log | Section 44 (accountability and record-keeping) |
package/package.json CHANGED
@@ -1,24 +1,33 @@
1
1
  {
2
2
  "name": "@tantainnovative/ndpr-recipes",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
- "description": "Backend recipes for @tantainnovative/ndpr-toolkit — Prisma schemas, API routes, and ORM adapters for NDPA compliance",
5
+ "description": "Backend recipes for @tantainnovative/ndpr-toolkit — Prisma schemas, API routes, and ORM adapters for Nigeria NDPA 2023 / NDPC GAID 2025 compliance",
6
6
  "license": "MIT",
7
7
  "author": {
8
8
  "name": "Abraham Esandayinze Tanta",
9
9
  "url": "https://linkedin.com/in/mr-tanta"
10
10
  },
11
+ "homepage": "https://ndprtoolkit.com.ng",
11
12
  "repository": {
12
13
  "type": "git",
13
14
  "url": "git+https://github.com/mr-tanta/ndpr-toolkit.git",
14
15
  "directory": "packages/ndpr-recipes"
15
16
  },
16
- "keywords": ["ndpa", "ndpr", "nigeria", "data-protection", "prisma", "nextjs", "express"],
17
+ "keywords": ["ndpa", "ndpr", "gaid-2025", "dcpmi", "compliance-audit-return", "nigeria", "data-protection", "prisma", "nextjs", "express"],
17
18
  "files": [
18
19
  "src/**/*",
19
20
  "prisma/**/*",
20
21
  "README.md"
21
22
  ],
23
+ "peerDependencies": {
24
+ "@tantainnovative/ndpr-toolkit": "^5.5.1"
25
+ },
26
+ "peerDependenciesMeta": {
27
+ "@tantainnovative/ndpr-toolkit": {
28
+ "optional": false
29
+ }
30
+ },
22
31
  "publishConfig": {
23
32
  "access": "public"
24
33
  }
@@ -89,16 +89,71 @@ model ProcessingRecord {
89
89
  @@map("ndpr_processing_records")
90
90
  }
91
91
 
92
+ model DPIARecord {
93
+ id String @id @default(cuid())
94
+ projectName String
95
+ description String
96
+ dpiaData Json
97
+ overallRisk String
98
+ score Int
99
+ status String @default("draft")
100
+ conductedBy String
101
+ approvedBy String?
102
+ createdAt DateTime @default(now())
103
+ updatedAt DateTime @updatedAt
104
+
105
+ @@index([status])
106
+ @@index([conductedBy])
107
+ @@map("ndpr_dpia_records")
108
+ }
109
+
110
+ model LawfulBasisRecord {
111
+ id String @id @default(cuid())
112
+ activityName String
113
+ lawfulBasis String
114
+ justification String
115
+ dataCategories Json
116
+ purposes Json
117
+ assessedBy String
118
+ assessedAt DateTime @default(now())
119
+ reviewDate DateTime?
120
+ createdAt DateTime @default(now())
121
+ updatedAt DateTime @updatedAt
122
+
123
+ @@index([lawfulBasis])
124
+ @@index([assessedBy])
125
+ @@map("ndpr_lawful_basis_records")
126
+ }
127
+
128
+ model CrossBorderTransferRecord {
129
+ id String @id @default(cuid())
130
+ destinationCountry String
131
+ recipientName String
132
+ transferMechanism String
133
+ safeguards String
134
+ dataCategories Json
135
+ adequacyStatus String
136
+ ndpcApprovalRequired Boolean @default(false)
137
+ ndpcApprovalReference String?
138
+ riskLevel String
139
+ createdAt DateTime @default(now())
140
+ updatedAt DateTime @updatedAt
141
+
142
+ @@index([destinationCountry])
143
+ @@index([riskLevel])
144
+ @@map("ndpr_cross_border_transfer_records")
145
+ }
146
+
92
147
  model ComplianceAuditLog {
93
148
  id String @id @default(cuid())
94
- module String
95
149
  action String
96
- entityId String
97
- entityType String
98
- changes Json?
150
+ module String
151
+ details Json?
99
152
  performedBy String?
153
+ ipAddress String?
100
154
  createdAt DateTime @default(now())
101
155
 
102
- @@index([module, entityId])
156
+ @@index([module])
157
+ @@index([performedBy])
103
158
  @@map("ndpr_audit_log")
104
159
  }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Drizzle adapter for the Breach Notification module.
3
+ *
4
+ * Implements StorageAdapter<BreachState> backed by the `ndpr_breach_reports`
5
+ * Drizzle table, where BreachState is the shape managed by the useBreach() hook:
6
+ *
7
+ * {
8
+ * reports: BreachReport[];
9
+ * assessments: RiskAssessment[];
10
+ * notifications: RegulatoryNotification[];
11
+ * }
12
+ *
13
+ * This adapter persists and loads BreachReport records. RiskAssessments and
14
+ * RegulatoryNotifications are stored as JSON on the breach row — extend the
15
+ * schema with additional columns if you need full relational queries.
16
+ *
17
+ * Behaviour
18
+ * ---------
19
+ * - LOAD → loads all breach reports from the database, ordered newest first.
20
+ * - SAVE → upserts each report by ID using Drizzle's `onConflictDoUpdate`.
21
+ * - REMOVE → marks all ongoing reports as 'resolved' (no hard deletes;
22
+ * NDPA Section 40 audit trail is preserved).
23
+ *
24
+ * Usage
25
+ * -----
26
+ * Copy this file into your project alongside your Drizzle client, then wire it
27
+ * into the toolkit hook:
28
+ *
29
+ * import { drizzle } from 'drizzle-orm/node-postgres';
30
+ * import { Pool } from 'pg';
31
+ * import { useBreach } from '@tantainnovative/ndpr-toolkit';
32
+ * import { drizzleBreachAdapter } from './adapters/drizzle-breach';
33
+ * import * as schema from './drizzle/schema';
34
+ *
35
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
36
+ * const db = drizzle(pool, { schema });
37
+ *
38
+ * function BreachPage() {
39
+ * const adapter = drizzleBreachAdapter(db);
40
+ * const { reports, submitReport } = useBreach({ adapter });
41
+ * // ...
42
+ * }
43
+ *
44
+ * Prerequisites
45
+ * -------------
46
+ * - The `ndpr_breach_reports` table must exist (run your Drizzle migration).
47
+ * - `drizzle-orm` must be installed in your project.
48
+ * - `@tantainnovative/ndpr-toolkit` must be installed in your project.
49
+ *
50
+ * @module adapters/drizzle-breach
51
+ */
52
+
53
+ import { eq, desc } from 'drizzle-orm';
54
+ import type { StorageAdapter } from '@tantainnovative/ndpr-toolkit';
55
+ import type { BreachReport, RiskAssessment, RegulatoryNotification } from '@tantainnovative/ndpr-toolkit';
56
+ import { breachReports } from '../drizzle/schema';
57
+
58
+ /** The state shape managed by the useBreach() hook */
59
+ export interface BreachState {
60
+ reports: BreachReport[];
61
+ assessments: RiskAssessment[];
62
+ notifications: RegulatoryNotification[];
63
+ }
64
+
65
+ /**
66
+ * Creates a Drizzle-backed StorageAdapter for the breach module's state.
67
+ *
68
+ * @param db - Your Drizzle database instance (any driver — pg, neon, libsql, etc.)
69
+ * @returns A StorageAdapter<BreachState> ready to pass to useBreach().
70
+ */
71
+ export function drizzleBreachAdapter(db: any): StorageAdapter<BreachState> {
72
+ return {
73
+ /**
74
+ * Load all breach reports from the database, ordered newest first.
75
+ * Assessments and notifications are returned as empty arrays — extend
76
+ * the schema if you need to persist them.
77
+ */
78
+ async load(): Promise<BreachState | null> {
79
+ const rows = await db
80
+ .select()
81
+ .from(breachReports)
82
+ .orderBy(desc(breachReports.reportedAt));
83
+
84
+ if (rows.length === 0) return null;
85
+
86
+ return {
87
+ reports: rows.map(mapRowToBreachReport),
88
+ assessments: [],
89
+ notifications: [],
90
+ };
91
+ },
92
+
93
+ /**
94
+ * Persist the current breach state.
95
+ * Each report is upserted by ID using Drizzle's `onConflictDoUpdate`.
96
+ * Assessments and notifications are ignored unless you extend the schema.
97
+ */
98
+ async save(state: BreachState): Promise<void> {
99
+ if (state.reports.length === 0) return;
100
+
101
+ await Promise.all(
102
+ state.reports.map((report) => {
103
+ const row = mapBreachReportToRow(report);
104
+ return db
105
+ .insert(breachReports)
106
+ .values({ id: report.id, ...row })
107
+ .onConflictDoUpdate({
108
+ target: breachReports.id,
109
+ set: row,
110
+ });
111
+ }),
112
+ );
113
+ },
114
+
115
+ /**
116
+ * Soft-close all ongoing breach reports by setting status to 'resolved'.
117
+ * Hard deletes are never performed to preserve the NDPA Section 40
118
+ * compliance audit trail.
119
+ */
120
+ async remove(): Promise<void> {
121
+ await db
122
+ .update(breachReports)
123
+ .set({ status: 'resolved' })
124
+ .where(eq(breachReports.status, 'ongoing'));
125
+ },
126
+ };
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Mapping helpers
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /**
134
+ * Map a raw Drizzle row (from ndpr_breach_reports) to the toolkit's BreachReport type.
135
+ */
136
+ function mapRowToBreachReport(row: any): BreachReport {
137
+ return {
138
+ id: row.id,
139
+ title: row.title,
140
+ description: row.description,
141
+ category: row.category,
142
+ discoveredAt: row.discoveredAt.getTime(),
143
+ occurredAt: row.occurredAt?.getTime(),
144
+ reportedAt: row.reportedAt.getTime(),
145
+ status: row.status as BreachReport['status'],
146
+ reporter: {
147
+ name: row.reporterName,
148
+ email: row.reporterEmail,
149
+ department: row.reporterDepartment ?? '',
150
+ },
151
+ affectedSystems: (row.affectedSystems as string[]) ?? [],
152
+ dataTypes: (row.dataTypes as string[]) ?? [],
153
+ estimatedAffectedSubjects: row.estimatedAffected ?? undefined,
154
+ initialActions: row.initialActions ?? undefined,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Map a toolkit BreachReport to the Drizzle insert/update row shape.
160
+ */
161
+ function mapBreachReportToRow(report: BreachReport): Record<string, unknown> {
162
+ return {
163
+ title: report.title,
164
+ description: report.description,
165
+ category: report.category,
166
+ severity: 'medium',
167
+ status: report.status,
168
+ discoveredAt: new Date(report.discoveredAt),
169
+ occurredAt: report.occurredAt ? new Date(report.occurredAt) : null,
170
+ reportedAt: new Date(report.reportedAt),
171
+ reporterName: report.reporter.name,
172
+ reporterEmail: report.reporter.email,
173
+ reporterDepartment: report.reporter.department ?? null,
174
+ affectedSystems: report.affectedSystems,
175
+ dataTypes: report.dataTypes,
176
+ estimatedAffected: report.estimatedAffectedSubjects ?? null,
177
+ initialActions: report.initialActions ?? null,
178
+ };
179
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Drizzle adapter for the Cross-Border Data Transfer module.
3
+ *
4
+ * Implements StorageAdapter<CrossBorderTransfer[]> backed by the
5
+ * `ndpr_cross_border_transfer_records` Drizzle table.
6
+ *
7
+ * Under NDPA Part VIII (Sections 41-43), personal data may only be transferred
8
+ * outside Nigeria under specific conditions. This adapter tracks every
9
+ * cross-border transfer, the mechanism relied upon, and NDPC approval status.
10
+ *
11
+ * Behaviour
12
+ * ---------
13
+ * - LOAD → returns all cross-border transfer records, ordered newest first.
14
+ * - SAVE → upserts each CrossBorderTransfer by ID using Drizzle's
15
+ * `onConflictDoUpdate` pattern.
16
+ * - REMOVE → soft-terminates all active transfers by setting their status
17
+ * columns (no hard deletes; the audit trail is preserved).
18
+ *
19
+ * Usage
20
+ * -----
21
+ * Copy this file into your project alongside your Drizzle client, then wire it
22
+ * into the toolkit:
23
+ *
24
+ * import { drizzle } from 'drizzle-orm/node-postgres';
25
+ * import { Pool } from 'pg';
26
+ * import { drizzleCrossBorderAdapter } from './adapters/drizzle-cross-border';
27
+ * import * as schema from './drizzle/schema';
28
+ *
29
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
30
+ * const db = drizzle(pool, { schema });
31
+ *
32
+ * function CrossBorderPage() {
33
+ * const adapter = drizzleCrossBorderAdapter(db);
34
+ * // pass adapter to your cross-border hook or use it directly
35
+ * }
36
+ *
37
+ * Prerequisites
38
+ * -------------
39
+ * - The `ndpr_cross_border_transfer_records` table must exist (run your Drizzle migration).
40
+ * - `drizzle-orm` must be installed in your project.
41
+ * - `@tantainnovative/ndpr-toolkit` must be installed in your project.
42
+ *
43
+ * @module adapters/drizzle-cross-border
44
+ */
45
+
46
+ import { eq, desc } from 'drizzle-orm';
47
+ import type { StorageAdapter } from '@tantainnovative/ndpr-toolkit';
48
+ import type { CrossBorderTransfer } from '@tantainnovative/ndpr-toolkit';
49
+ import { crossBorderTransferRecords } from '../drizzle/schema';
50
+
51
+ /**
52
+ * Creates a Drizzle-backed StorageAdapter for CrossBorderTransfer[].
53
+ *
54
+ * @param db - Your Drizzle database instance (any driver — pg, neon, libsql, etc.)
55
+ * @returns A StorageAdapter<CrossBorderTransfer[]> ready to use with the cross-border module.
56
+ */
57
+ export function drizzleCrossBorderAdapter(
58
+ db: any,
59
+ ): StorageAdapter<CrossBorderTransfer[]> {
60
+ return {
61
+ /**
62
+ * Load all cross-border transfer records, ordered newest first.
63
+ * Returns null if no records exist.
64
+ */
65
+ async load(): Promise<CrossBorderTransfer[] | null> {
66
+ const rows = await db
67
+ .select()
68
+ .from(crossBorderTransferRecords)
69
+ .orderBy(desc(crossBorderTransferRecords.createdAt));
70
+
71
+ if (rows.length === 0) return null;
72
+
73
+ return rows.map(mapRowToCrossBorderTransfer);
74
+ },
75
+
76
+ /**
77
+ * Persist the current list of cross-border transfers.
78
+ * Each transfer is upserted by ID so partial updates work.
79
+ */
80
+ async save(transfers: CrossBorderTransfer[]): Promise<void> {
81
+ if (transfers.length === 0) return;
82
+
83
+ await Promise.all(
84
+ transfers.map((transfer) => {
85
+ const row = mapCrossBorderTransferToRow(transfer);
86
+ return db
87
+ .insert(crossBorderTransferRecords)
88
+ .values({ id: transfer.id, ...row })
89
+ .onConflictDoUpdate({
90
+ target: crossBorderTransferRecords.id,
91
+ set: row,
92
+ });
93
+ }),
94
+ );
95
+ },
96
+
97
+ /**
98
+ * Soft-terminate all active cross-border transfers by updating their
99
+ * timestamp. Transfers are never hard-deleted so the NDPA Part VI
100
+ * compliance audit trail is preserved.
101
+ *
102
+ * Note: This updates all records. If you need to scope removal to a
103
+ * specific status, add a `status` column to the schema and filter on it.
104
+ */
105
+ async remove(): Promise<void> {
106
+ await db
107
+ .update(crossBorderTransferRecords)
108
+ .set({ updatedAt: new Date() });
109
+ },
110
+ };
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Mapping helpers
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Map a raw Drizzle row (from ndpr_cross_border_transfer_records) to the
119
+ * toolkit's CrossBorderTransfer type. Some rich fields on the toolkit type
120
+ * default to placeholder values — extend the schema if you need full
121
+ * round-trip fidelity.
122
+ */
123
+ function mapRowToCrossBorderTransfer(row: any): CrossBorderTransfer {
124
+ return {
125
+ id: row.id,
126
+ destinationCountry: row.destinationCountry,
127
+ adequacyStatus: row.adequacyStatus as CrossBorderTransfer['adequacyStatus'],
128
+ transferMechanism: row.transferMechanism as CrossBorderTransfer['transferMechanism'],
129
+ dataCategories: (row.dataCategories as string[]) ?? [],
130
+ includesSensitiveData: false,
131
+ recipientOrganization: row.recipientName,
132
+ recipientContact: {
133
+ name: row.recipientName,
134
+ email: '',
135
+ },
136
+ purpose: '',
137
+ safeguards: row.safeguards ? [row.safeguards] : [],
138
+ riskAssessment: '',
139
+ riskLevel: row.riskLevel as CrossBorderTransfer['riskLevel'],
140
+ ndpcApproval: row.ndpcApprovalRequired
141
+ ? {
142
+ required: row.ndpcApprovalRequired,
143
+ applied: !!row.ndpcApprovalReference,
144
+ approved: !!row.ndpcApprovalReference,
145
+ referenceNumber: row.ndpcApprovalReference ?? undefined,
146
+ }
147
+ : undefined,
148
+ tiaCompleted: false,
149
+ frequency: 'continuous',
150
+ startDate: row.createdAt.getTime(),
151
+ status: 'active',
152
+ createdAt: row.createdAt.getTime(),
153
+ updatedAt: row.updatedAt.getTime(),
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Map a toolkit CrossBorderTransfer to the Drizzle insert/update row shape.
159
+ * Composite toolkit fields are flattened to match the simplified schema.
160
+ */
161
+ function mapCrossBorderTransferToRow(transfer: CrossBorderTransfer): Record<string, unknown> {
162
+ return {
163
+ destinationCountry: transfer.destinationCountry,
164
+ recipientName: transfer.recipientOrganization,
165
+ transferMechanism: transfer.transferMechanism,
166
+ safeguards: transfer.safeguards.join('; '),
167
+ dataCategories: transfer.dataCategories,
168
+ adequacyStatus: transfer.adequacyStatus,
169
+ ndpcApprovalRequired: transfer.ndpcApproval?.required ?? false,
170
+ ndpcApprovalReference: transfer.ndpcApproval?.referenceNumber ?? null,
171
+ riskLevel: transfer.riskLevel,
172
+ updatedAt: new Date(),
173
+ };
174
+ }