@tantainnovative/create-ndpr 0.1.0 → 0.4.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.
@@ -11,9 +11,45 @@
11
11
  */
12
12
 
13
13
  import { NextRequest, NextResponse } from 'next/server';
14
+ import { assessBreachNotification } from '@tantainnovative/ndpr-toolkit/server';
15
+ // {{#if ORM=prisma}}
14
16
  import { PrismaClient } from '@prisma/client';
15
17
 
16
18
  const prisma = new PrismaClient();
19
+ // {{/if}}
20
+ // {{#if ORM=drizzle}}
21
+ import { db } from '@/drizzle';
22
+ import { breachReports, complianceAuditLog } from '@/drizzle/ndpr-schema';
23
+ import { eq, desc } from 'drizzle-orm';
24
+ // {{/if}}
25
+ // {{#if ORM=none}}
26
+ // TODO ({{ORG_NAME}}): wire this to your persistent store of choice. The
27
+ // in-memory map below is enough to develop against locally but DOES NOT
28
+ // satisfy NDPA Section 44 (record-keeping) or the 72-hour NDPC notification
29
+ // requirement in Section 40. Replace `breachStore`/`auditLog` with your real
30
+ // store and connect it to your incident-response tooling.
31
+ interface BreachReport {
32
+ id: string;
33
+ title: string;
34
+ description: string;
35
+ category: string;
36
+ severity: 'critical' | 'high' | 'medium' | 'low';
37
+ status: string;
38
+ discoveredAt: Date;
39
+ occurredAt: Date | null;
40
+ reportedAt: Date;
41
+ reporterName: string;
42
+ reporterEmail: string;
43
+ reporterDepartment: string | null;
44
+ affectedSystems: string[];
45
+ dataTypes: string[];
46
+ estimatedAffected: number | null;
47
+ initialActions: string | null;
48
+ }
49
+ const breachStore = new Map<string, BreachReport>();
50
+ const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
51
+ function newId() { return crypto.randomUUID(); }
52
+ // {{/if}}
17
53
 
18
54
  function calculateSeverity(
19
55
  category: string,
@@ -32,10 +68,22 @@ function calculateSeverity(
32
68
  export async function GET(req: NextRequest) {
33
69
  const status = req.nextUrl.searchParams.get('status');
34
70
 
71
+ // {{#if ORM=prisma}}
35
72
  const reports = await prisma.breachReport.findMany({
36
73
  where: status ? { status } : undefined,
37
74
  orderBy: { reportedAt: 'desc' },
38
75
  });
76
+ // {{/if}}
77
+ // {{#if ORM=drizzle}}
78
+ const reports = status
79
+ ? await db.select().from(breachReports).where(eq(breachReports.status, status)).orderBy(desc(breachReports.reportedAt))
80
+ : await db.select().from(breachReports).orderBy(desc(breachReports.reportedAt));
81
+ // {{/if}}
82
+ // {{#if ORM=none}}
83
+ const reports = [...breachStore.values()]
84
+ .filter((r) => (status ? r.status === status : true))
85
+ .sort((a, b) => b.reportedAt.getTime() - a.reportedAt.getTime());
86
+ // {{/if}}
39
87
 
40
88
  return NextResponse.json(reports);
41
89
  }
@@ -74,6 +122,7 @@ export async function POST(req: NextRequest) {
74
122
 
75
123
  const severity = calculateSeverity(category, estimatedAffected);
76
124
 
125
+ // {{#if ORM=prisma}}
77
126
  const report = await prisma.breachReport.create({
78
127
  data: {
79
128
  title,
@@ -102,6 +151,89 @@ export async function POST(req: NextRequest) {
102
151
  changes: { title, category, severity, status: 'ongoing' },
103
152
  },
104
153
  });
154
+ // {{/if}}
155
+ // {{#if ORM=drizzle}}
156
+ const [report] = await db.insert(breachReports).values({
157
+ title,
158
+ description,
159
+ category,
160
+ severity,
161
+ status: 'ongoing',
162
+ discoveredAt: new Date(discoveredAt),
163
+ occurredAt: occurredAt ? new Date(occurredAt) : null,
164
+ reporterName,
165
+ reporterEmail,
166
+ reporterDepartment: reporterDepartment ?? null,
167
+ affectedSystems,
168
+ dataTypes,
169
+ estimatedAffected: estimatedAffected ?? null,
170
+ initialActions: initialActions ?? null,
171
+ }).returning();
105
172
 
106
- return NextResponse.json(report, { status: 201 });
173
+ await db.insert(complianceAuditLog).values({
174
+ module: 'breach',
175
+ action: 'reported',
176
+ entityId: report.id,
177
+ entityType: 'BreachReport',
178
+ changes: { title, category, severity, status: 'ongoing' },
179
+ });
180
+ // {{/if}}
181
+ // {{#if ORM=none}}
182
+ const report: BreachReport = {
183
+ id: newId(),
184
+ title,
185
+ description,
186
+ category,
187
+ severity,
188
+ status: 'ongoing',
189
+ discoveredAt: new Date(discoveredAt),
190
+ occurredAt: occurredAt ? new Date(occurredAt) : null,
191
+ reportedAt: new Date(),
192
+ reporterName,
193
+ reporterEmail,
194
+ reporterDepartment: reporterDepartment ?? null,
195
+ affectedSystems,
196
+ dataTypes,
197
+ estimatedAffected: estimatedAffected ?? null,
198
+ initialActions: initialActions ?? null,
199
+ };
200
+ breachStore.set(report.id, report);
201
+ auditLog.push({ id: newId(), module: 'breach', action: 'reported', entityId: report.id, at: new Date() });
202
+ // {{/if}}
203
+
204
+ // NDPC readiness — NDPA S.40 / GAID 2025 Art. 33(5). Advisory and
205
+ // non-blocking: you must record every incident, but this surfaces which
206
+ // mandated notification fields are still missing (and how long is left on
207
+ // the 72-hour clock) before you file with the Commission. Collect the
208
+ // missing fields — likely consequences, mitigation measures, data-subject
209
+ // categories, record count — as your investigation progresses.
210
+ const ndpcReadiness = assessBreachNotification({
211
+ id: report.id,
212
+ title,
213
+ description,
214
+ category,
215
+ discoveredAt: new Date(discoveredAt).getTime(),
216
+ occurredAt: occurredAt ? new Date(occurredAt).getTime() : undefined,
217
+ reportedAt: Date.now(),
218
+ reporter: { name: reporterName, email: reporterEmail, department: reporterDepartment ?? '' },
219
+ affectedSystems,
220
+ dataTypes,
221
+ estimatedAffectedSubjects: estimatedAffected ?? undefined,
222
+ initialActions: initialActions ?? undefined,
223
+ dpoContact: { name: '{{ORG_NAME}} DPO', email: '{{DPO_EMAIL}}' },
224
+ status: 'ongoing',
225
+ });
226
+
227
+ return NextResponse.json(
228
+ {
229
+ ...report,
230
+ ndpcReadiness: {
231
+ complete: ndpcReadiness.complete,
232
+ completeness: ndpcReadiness.completeness,
233
+ missing: ndpcReadiness.missing,
234
+ hoursRemaining: ndpcReadiness.timing.hoursRemaining,
235
+ },
236
+ },
237
+ { status: 201 },
238
+ );
107
239
  }
@@ -2,7 +2,8 @@
2
2
  * Next.js App Router — Consent API Route
3
3
  * Generated by create-ndpr for: {{ORG_NAME}}
4
4
  *
5
- * NDPA §25 (lawful basis) and §26 (right to withdraw consent).
5
+ * NDPA Section 26 (consent) and Section 25 (lawful basis). Records the
6
+ * subject's affirmative choice with the audit metadata NDPC asks for.
6
7
  *
7
8
  * Endpoints:
8
9
  * GET /api/consent?subjectId=xxx — Load the active consent record
@@ -11,9 +12,37 @@
11
12
  */
12
13
 
13
14
  import { NextRequest, NextResponse } from 'next/server';
15
+ // {{#if ORM=prisma}}
14
16
  import { PrismaClient } from '@prisma/client';
15
17
 
16
18
  const prisma = new PrismaClient();
19
+ // {{/if}}
20
+ // {{#if ORM=drizzle}}
21
+ import { db } from '@/drizzle';
22
+ import { consentRecords, complianceAuditLog } from '@/drizzle/ndpr-schema';
23
+ import { eq, and, isNull, desc } from 'drizzle-orm';
24
+ // {{/if}}
25
+ // {{#if ORM=none}}
26
+ // TODO ({{ORG_NAME}}): wire this to your persistent store of choice. The
27
+ // in-memory map below is enough to develop against locally but DOES NOT
28
+ // satisfy NDPA Section 44 (record-keeping) or the auditability NDPC asks
29
+ // about. Replace `consentStore`/`auditLog` with your DB / KV / API.
30
+ interface ConsentRecord {
31
+ id: string;
32
+ subjectId: string;
33
+ consents: Record<string, boolean>;
34
+ version: string;
35
+ method: string;
36
+ lawfulBasis: string | null;
37
+ ipAddress: string | null;
38
+ userAgent: string | null;
39
+ createdAt: Date;
40
+ revokedAt: Date | null;
41
+ }
42
+ const consentStore = new Map<string, ConsentRecord>();
43
+ const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
44
+ function newId() { return crypto.randomUUID(); }
45
+ // {{/if}}
17
46
 
18
47
  // GET /api/consent?subjectId=xxx
19
48
  export async function GET(req: NextRequest) {
@@ -23,12 +52,27 @@ export async function GET(req: NextRequest) {
23
52
  return NextResponse.json({ error: 'subjectId required' }, { status: 400 });
24
53
  }
25
54
 
55
+ // {{#if ORM=prisma}}
26
56
  const record = await prisma.consentRecord.findFirst({
27
57
  where: { subjectId, revokedAt: null },
28
58
  orderBy: { createdAt: 'desc' },
29
59
  });
30
-
31
- return NextResponse.json(record);
60
+ // {{/if}}
61
+ // {{#if ORM=drizzle}}
62
+ const [record] = await db
63
+ .select()
64
+ .from(consentRecords)
65
+ .where(and(eq(consentRecords.subjectId, subjectId), isNull(consentRecords.revokedAt)))
66
+ .orderBy(desc(consentRecords.createdAt))
67
+ .limit(1);
68
+ // {{/if}}
69
+ // {{#if ORM=none}}
70
+ const record = [...consentStore.values()]
71
+ .filter((r) => r.subjectId === subjectId && r.revokedAt === null)
72
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];
73
+ // {{/if}}
74
+
75
+ return NextResponse.json(record ?? null);
32
76
  }
33
77
 
34
78
  // POST /api/consent
@@ -43,38 +87,53 @@ export async function POST(req: NextRequest) {
43
87
  );
44
88
  }
45
89
 
46
- // Revoke any previously active consent records (immutable-audit pattern — NDPA §44).
90
+ const ipAddress = req.headers.get('x-forwarded-for');
91
+ const userAgent = req.headers.get('user-agent');
92
+
93
+ // {{#if ORM=prisma}}
94
+ // Revoke previously active records (immutable-audit pattern — Section 44).
47
95
  await prisma.consentRecord.updateMany({
48
96
  where: { subjectId, revokedAt: null },
49
97
  data: { revokedAt: new Date() },
50
98
  });
51
99
 
52
100
  const record = await prisma.consentRecord.create({
53
- data: {
54
- subjectId,
55
- consents,
56
- version,
57
- method: method ?? 'api',
58
- lawfulBasis: lawfulBasis ?? null,
59
- ipAddress: req.headers.get('x-forwarded-for'),
60
- userAgent: req.headers.get('user-agent'),
61
- },
101
+ data: { subjectId, consents, version, method: method ?? 'api', lawfulBasis: lawfulBasis ?? null, ipAddress, userAgent },
62
102
  });
63
103
 
64
104
  await prisma.complianceAuditLog.create({
65
- data: {
66
- module: 'consent',
67
- action: 'created',
68
- entityId: record.id,
69
- entityType: 'ConsentRecord',
70
- changes: { subjectId, version, consents },
71
- },
105
+ data: { module: 'consent', action: 'created', entityId: record.id, entityType: 'ConsentRecord', changes: { subjectId, version, consents } },
72
106
  });
107
+ // {{/if}}
108
+ // {{#if ORM=drizzle}}
109
+ await db.update(consentRecords).set({ revokedAt: new Date() }).where(and(eq(consentRecords.subjectId, subjectId), isNull(consentRecords.revokedAt)));
110
+
111
+ const [record] = await db.insert(consentRecords).values({
112
+ subjectId, consents, version, method: method ?? 'api', lawfulBasis: lawfulBasis ?? null, ipAddress, userAgent,
113
+ }).returning();
114
+
115
+ await db.insert(complianceAuditLog).values({
116
+ module: 'consent', action: 'created', entityId: record.id, entityType: 'ConsentRecord', changes: { subjectId, version, consents },
117
+ });
118
+ // {{/if}}
119
+ // {{#if ORM=none}}
120
+ // Revoke previously active records for this subject
121
+ for (const r of consentStore.values()) {
122
+ if (r.subjectId === subjectId && r.revokedAt === null) r.revokedAt = new Date();
123
+ }
124
+ const record: ConsentRecord = {
125
+ id: newId(), subjectId, consents, version, method: method ?? 'api',
126
+ lawfulBasis: lawfulBasis ?? null, ipAddress, userAgent,
127
+ createdAt: new Date(), revokedAt: null,
128
+ };
129
+ consentStore.set(record.id, record);
130
+ auditLog.push({ id: newId(), module: 'consent', action: 'created', entityId: record.id, at: new Date() });
131
+ // {{/if}}
73
132
 
74
133
  return NextResponse.json(record, { status: 201 });
75
134
  }
76
135
 
77
- // DELETE /api/consent?subjectId=xxx
136
+ // DELETE /api/consent?subjectId=xxx — Section 26 withdrawal
78
137
  export async function DELETE(req: NextRequest) {
79
138
  const subjectId = req.nextUrl.searchParams.get('subjectId');
80
139
 
@@ -82,19 +141,25 @@ export async function DELETE(req: NextRequest) {
82
141
  return NextResponse.json({ error: 'subjectId required' }, { status: 400 });
83
142
  }
84
143
 
144
+ // {{#if ORM=prisma}}
85
145
  await prisma.consentRecord.updateMany({
86
146
  where: { subjectId, revokedAt: null },
87
147
  data: { revokedAt: new Date() },
88
148
  });
89
-
90
149
  await prisma.complianceAuditLog.create({
91
- data: {
92
- module: 'consent',
93
- action: 'revoked',
94
- entityId: subjectId,
95
- entityType: 'ConsentRecord',
96
- },
150
+ data: { module: 'consent', action: 'revoked', entityId: subjectId, entityType: 'ConsentRecord' },
97
151
  });
152
+ // {{/if}}
153
+ // {{#if ORM=drizzle}}
154
+ await db.update(consentRecords).set({ revokedAt: new Date() }).where(and(eq(consentRecords.subjectId, subjectId), isNull(consentRecords.revokedAt)));
155
+ await db.insert(complianceAuditLog).values({ module: 'consent', action: 'revoked', entityId: subjectId, entityType: 'ConsentRecord' });
156
+ // {{/if}}
157
+ // {{#if ORM=none}}
158
+ for (const r of consentStore.values()) {
159
+ if (r.subjectId === subjectId && r.revokedAt === null) r.revokedAt = new Date();
160
+ }
161
+ auditLog.push({ id: newId(), module: 'consent', action: 'revoked', entityId: subjectId, at: new Date() });
162
+ // {{/if}}
98
163
 
99
164
  return NextResponse.json({ success: true });
100
165
  }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Next.js App Router — Cross-Border Transfer Route
3
+ * Generated by create-ndpr for: {{ORG_NAME}}
4
+ *
5
+ * NDPA §41–43 — transfers of personal data outside Nigeria require
6
+ * adequate safeguards and, in some cases, NDPC approval.
7
+ *
8
+ * Endpoints:
9
+ * GET /api/cross-border — List transfers (optional ?riskLevel= or ?destinationCountry= filter)
10
+ * GET /api/cross-border?id=xxx — Get a single transfer record
11
+ * POST /api/cross-border — Register a new cross-border transfer
12
+ * PUT /api/cross-border — Update an existing transfer record
13
+ * DELETE /api/cross-border?id=xxx — Delete a transfer record
14
+ */
15
+
16
+ import { NextRequest, NextResponse } from 'next/server';
17
+ // {{#if ORM=prisma}}
18
+ import { PrismaClient } from '@prisma/client';
19
+
20
+ const prisma = new PrismaClient();
21
+ // {{/if}}
22
+ // {{#if ORM=drizzle}}
23
+ import { db } from '@/drizzle';
24
+ import { crossBorderTransferRecords, complianceAuditLog } from '@/drizzle/ndpr-schema';
25
+ import { eq, and, desc, type SQL } from 'drizzle-orm';
26
+ // {{/if}}
27
+ // {{#if ORM=none}}
28
+ // TODO ({{ORG_NAME}}): wire this to your persistent store of choice. The
29
+ // in-memory map below is enough to develop against locally but DOES NOT
30
+ // satisfy NDPA Section 44 (record-keeping) or the cross-border register
31
+ // NDPC inspects under Sections 41–43. Replace `crossBorderStore`/`auditLog`
32
+ // with your DB / KV / API.
33
+ interface CrossBorderTransferRecord {
34
+ id: string;
35
+ destinationCountry: string;
36
+ recipientName: string;
37
+ transferMechanism: string;
38
+ safeguards: string;
39
+ dataCategories: string[];
40
+ adequacyStatus: string;
41
+ ndpcApprovalRequired: boolean;
42
+ ndpcApprovalReference: string | null;
43
+ riskLevel: string;
44
+ createdAt: Date;
45
+ updatedAt: Date;
46
+ }
47
+ const crossBorderStore = new Map<string, CrossBorderTransferRecord>();
48
+ const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
49
+ function newId() { return crypto.randomUUID(); }
50
+ // {{/if}}
51
+
52
+ // GET /api/cross-border?riskLevel=high OR /api/cross-border?id=xxx
53
+ export async function GET(req: NextRequest) {
54
+ const id = req.nextUrl.searchParams.get('id');
55
+
56
+ if (id) {
57
+ // {{#if ORM=prisma}}
58
+ const record = await prisma.crossBorderTransferRecord.findUnique({ where: { id } });
59
+ // {{/if}}
60
+ // {{#if ORM=drizzle}}
61
+ const [record] = await db.select().from(crossBorderTransferRecords).where(eq(crossBorderTransferRecords.id, id)).limit(1);
62
+ // {{/if}}
63
+ // {{#if ORM=none}}
64
+ const record = crossBorderStore.get(id) ?? null;
65
+ // {{/if}}
66
+
67
+ if (!record) {
68
+ return NextResponse.json({ error: 'Cross-border transfer record not found' }, { status: 404 });
69
+ }
70
+ return NextResponse.json(record);
71
+ }
72
+
73
+ const riskLevel = req.nextUrl.searchParams.get('riskLevel');
74
+ const destinationCountry = req.nextUrl.searchParams.get('destinationCountry');
75
+
76
+ // {{#if ORM=prisma}}
77
+ const where: Record<string, string> = {};
78
+ if (riskLevel) where.riskLevel = riskLevel;
79
+ if (destinationCountry) where.destinationCountry = destinationCountry;
80
+
81
+ const records = await prisma.crossBorderTransferRecord.findMany({
82
+ where: Object.keys(where).length > 0 ? where : undefined,
83
+ orderBy: { createdAt: 'desc' },
84
+ });
85
+ // {{/if}}
86
+ // {{#if ORM=drizzle}}
87
+ const filters: SQL[] = [];
88
+ if (riskLevel) filters.push(eq(crossBorderTransferRecords.riskLevel, riskLevel));
89
+ if (destinationCountry) filters.push(eq(crossBorderTransferRecords.destinationCountry, destinationCountry));
90
+
91
+ const records = filters.length > 0
92
+ ? await db.select().from(crossBorderTransferRecords).where(and(...filters)).orderBy(desc(crossBorderTransferRecords.createdAt))
93
+ : await db.select().from(crossBorderTransferRecords).orderBy(desc(crossBorderTransferRecords.createdAt));
94
+ // {{/if}}
95
+ // {{#if ORM=none}}
96
+ const records = [...crossBorderStore.values()]
97
+ .filter((r) => (riskLevel ? r.riskLevel === riskLevel : true))
98
+ .filter((r) => (destinationCountry ? r.destinationCountry === destinationCountry : true))
99
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
100
+ // {{/if}}
101
+
102
+ return NextResponse.json(records);
103
+ }
104
+
105
+ // POST /api/cross-border
106
+ export async function POST(req: NextRequest) {
107
+ const body = await req.json();
108
+ const {
109
+ destinationCountry,
110
+ recipientName,
111
+ transferMechanism,
112
+ safeguards,
113
+ dataCategories,
114
+ adequacyStatus,
115
+ ndpcApprovalRequired,
116
+ ndpcApprovalReference,
117
+ riskLevel,
118
+ } = body;
119
+
120
+ if (
121
+ !destinationCountry ||
122
+ !recipientName ||
123
+ !transferMechanism ||
124
+ !safeguards ||
125
+ !dataCategories ||
126
+ !adequacyStatus ||
127
+ !riskLevel
128
+ ) {
129
+ return NextResponse.json(
130
+ {
131
+ error:
132
+ 'destinationCountry, recipientName, transferMechanism, safeguards, dataCategories, adequacyStatus, and riskLevel are required',
133
+ },
134
+ { status: 400 },
135
+ );
136
+ }
137
+
138
+ if (!Array.isArray(dataCategories)) {
139
+ return NextResponse.json(
140
+ { error: 'dataCategories must be an array' },
141
+ { status: 400 },
142
+ );
143
+ }
144
+
145
+ // {{#if ORM=prisma}}
146
+ const record = await prisma.crossBorderTransferRecord.create({
147
+ data: {
148
+ destinationCountry,
149
+ recipientName,
150
+ transferMechanism,
151
+ safeguards,
152
+ dataCategories,
153
+ adequacyStatus,
154
+ ndpcApprovalRequired: ndpcApprovalRequired ?? false,
155
+ ndpcApprovalReference: ndpcApprovalReference ?? null,
156
+ riskLevel,
157
+ },
158
+ });
159
+
160
+ await prisma.complianceAuditLog.create({
161
+ data: {
162
+ module: 'cross-border',
163
+ action: 'created',
164
+ entityId: record.id,
165
+ entityType: 'CrossBorderTransferRecord',
166
+ changes: { destinationCountry, recipientName, riskLevel },
167
+ },
168
+ });
169
+ // {{/if}}
170
+ // {{#if ORM=drizzle}}
171
+ const [record] = await db.insert(crossBorderTransferRecords).values({
172
+ destinationCountry,
173
+ recipientName,
174
+ transferMechanism,
175
+ safeguards,
176
+ dataCategories,
177
+ adequacyStatus,
178
+ ndpcApprovalRequired: ndpcApprovalRequired ?? false,
179
+ ndpcApprovalReference: ndpcApprovalReference ?? null,
180
+ riskLevel,
181
+ }).returning();
182
+
183
+ await db.insert(complianceAuditLog).values({
184
+ module: 'cross-border',
185
+ action: 'created',
186
+ entityId: record.id,
187
+ entityType: 'CrossBorderTransferRecord',
188
+ changes: { destinationCountry, recipientName, riskLevel },
189
+ });
190
+ // {{/if}}
191
+ // {{#if ORM=none}}
192
+ const now = new Date();
193
+ const record: CrossBorderTransferRecord = {
194
+ id: newId(),
195
+ destinationCountry,
196
+ recipientName,
197
+ transferMechanism,
198
+ safeguards,
199
+ dataCategories,
200
+ adequacyStatus,
201
+ ndpcApprovalRequired: ndpcApprovalRequired ?? false,
202
+ ndpcApprovalReference: ndpcApprovalReference ?? null,
203
+ riskLevel,
204
+ createdAt: now,
205
+ updatedAt: now,
206
+ };
207
+ crossBorderStore.set(record.id, record);
208
+ auditLog.push({ id: newId(), module: 'cross-border', action: 'created', entityId: record.id, at: new Date() });
209
+ // {{/if}}
210
+
211
+ return NextResponse.json(record, { status: 201 });
212
+ }
213
+
214
+ // PUT /api/cross-border
215
+ export async function PUT(req: NextRequest) {
216
+ const body = await req.json();
217
+ const { id, ...data } = body;
218
+
219
+ if (!id) {
220
+ return NextResponse.json({ error: 'id is required' }, { status: 400 });
221
+ }
222
+
223
+ // {{#if ORM=prisma}}
224
+ const existing = await prisma.crossBorderTransferRecord.findUnique({ where: { id } });
225
+ if (!existing) {
226
+ return NextResponse.json({ error: 'Cross-border transfer record not found' }, { status: 404 });
227
+ }
228
+
229
+ const record = await prisma.crossBorderTransferRecord.update({
230
+ where: { id },
231
+ data,
232
+ });
233
+
234
+ await prisma.complianceAuditLog.create({
235
+ data: {
236
+ module: 'cross-border',
237
+ action: 'updated',
238
+ entityId: record.id,
239
+ entityType: 'CrossBorderTransferRecord',
240
+ changes: data,
241
+ },
242
+ });
243
+ // {{/if}}
244
+ // {{#if ORM=drizzle}}
245
+ const [existing] = await db.select().from(crossBorderTransferRecords).where(eq(crossBorderTransferRecords.id, id)).limit(1);
246
+ if (!existing) {
247
+ return NextResponse.json({ error: 'Cross-border transfer record not found' }, { status: 404 });
248
+ }
249
+
250
+ const [record] = await db.update(crossBorderTransferRecords).set(data).where(eq(crossBorderTransferRecords.id, id)).returning();
251
+
252
+ await db.insert(complianceAuditLog).values({
253
+ module: 'cross-border',
254
+ action: 'updated',
255
+ entityId: record.id,
256
+ entityType: 'CrossBorderTransferRecord',
257
+ changes: data,
258
+ });
259
+ // {{/if}}
260
+ // {{#if ORM=none}}
261
+ const existing = crossBorderStore.get(id);
262
+ if (!existing) {
263
+ return NextResponse.json({ error: 'Cross-border transfer record not found' }, { status: 404 });
264
+ }
265
+ const record: CrossBorderTransferRecord = { ...existing, ...data, id, updatedAt: new Date() };
266
+ crossBorderStore.set(id, record);
267
+ auditLog.push({ id: newId(), module: 'cross-border', action: 'updated', entityId: record.id, at: new Date() });
268
+ // {{/if}}
269
+
270
+ return NextResponse.json(record);
271
+ }
272
+
273
+ // DELETE /api/cross-border?id=xxx
274
+ export async function DELETE(req: NextRequest) {
275
+ const id = req.nextUrl.searchParams.get('id');
276
+
277
+ if (!id) {
278
+ return NextResponse.json({ error: 'id is required' }, { status: 400 });
279
+ }
280
+
281
+ // {{#if ORM=prisma}}
282
+ const existing = await prisma.crossBorderTransferRecord.findUnique({ where: { id } });
283
+ if (!existing) {
284
+ return NextResponse.json({ error: 'Cross-border transfer record not found' }, { status: 404 });
285
+ }
286
+
287
+ await prisma.crossBorderTransferRecord.delete({ where: { id } });
288
+
289
+ await prisma.complianceAuditLog.create({
290
+ data: {
291
+ module: 'cross-border',
292
+ action: 'deleted',
293
+ entityId: id,
294
+ entityType: 'CrossBorderTransferRecord',
295
+ changes: { destinationCountry: existing.destinationCountry, recipientName: existing.recipientName },
296
+ },
297
+ });
298
+ // {{/if}}
299
+ // {{#if ORM=drizzle}}
300
+ const [existing] = await db.select().from(crossBorderTransferRecords).where(eq(crossBorderTransferRecords.id, id)).limit(1);
301
+ if (!existing) {
302
+ return NextResponse.json({ error: 'Cross-border transfer record not found' }, { status: 404 });
303
+ }
304
+
305
+ await db.delete(crossBorderTransferRecords).where(eq(crossBorderTransferRecords.id, id));
306
+
307
+ await db.insert(complianceAuditLog).values({
308
+ module: 'cross-border',
309
+ action: 'deleted',
310
+ entityId: id,
311
+ entityType: 'CrossBorderTransferRecord',
312
+ changes: { destinationCountry: existing.destinationCountry, recipientName: existing.recipientName },
313
+ });
314
+ // {{/if}}
315
+ // {{#if ORM=none}}
316
+ const existing = crossBorderStore.get(id);
317
+ if (!existing) {
318
+ return NextResponse.json({ error: 'Cross-border transfer record not found' }, { status: 404 });
319
+ }
320
+ crossBorderStore.delete(id);
321
+ auditLog.push({ id: newId(), module: 'cross-border', action: 'deleted', entityId: id, at: new Date() });
322
+ // {{/if}}
323
+
324
+ return NextResponse.json({ success: true });
325
+ }