@tantainnovative/create-ndpr 0.4.0 → 0.5.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.
@@ -1,297 +1,283 @@
1
- /**
2
- * Next.js App Router — Lawful Basis Route
3
- * Generated by create-ndpr for: {{ORG_NAME}}
4
- *
5
- * NDPA §25 — data processing must have a lawful basis. This route
6
- * manages a register of processing activities and their lawful bases.
7
- *
8
- * Endpoints:
9
- * GET /api/lawful-basis — List lawful basis records (optional ?lawfulBasis= filter)
10
- * GET /api/lawful-basis?id=xxx — Get a single record
11
- * POST /api/lawful-basis — Create a new lawful basis record
12
- * PUT /api/lawful-basis — Update an existing record
13
- * DELETE /api/lawful-basis?id=xxx — Delete a record
14
- */
15
-
1
+ /** Next.js App Router — staff-only, tenant-scoped lawful-basis register for {{ORG_NAME_COMMENT}}. */
16
2
  import { NextRequest, NextResponse } from 'next/server';
3
+ import { getNDPRContextProblem, resolveNDPRRequestContext } from '{{NDPR_CONTEXT_IMPORT}}';
17
4
  // {{#if ORM=prisma}}
18
5
  import { PrismaClient } from '@prisma/client';
19
-
20
6
  const prisma = new PrismaClient();
21
7
  // {{/if}}
22
8
  // {{#if ORM=drizzle}}
23
- import { db } from '@/drizzle';
24
- import { lawfulBasisRecords, complianceAuditLog } from '@/drizzle/ndpr-schema';
25
- import { eq, desc } from 'drizzle-orm';
9
+ import { db } from '{{NDPR_DB_IMPORT}}';
10
+ import { complianceAuditLog, lawfulBasisRecords } from '{{NDPR_SCHEMA_IMPORT}}';
11
+ import { and, desc, eq, isNull } from 'drizzle-orm';
26
12
  // {{/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). Replace `lawfulBasisStore`/
31
- // `auditLog` with your DB / KV / API.
32
- interface LawfulBasisRecord {
33
- id: string;
13
+
14
+ const LAWFUL_BASES = ['consent', 'contract', 'legal_obligation', 'vital_interests', 'public_interest', 'legitimate_interests'] as const;
15
+ type LawfulBasis = (typeof LAWFUL_BASES)[number];
16
+ type Validation<T> = { ok: true; value: T } | { ok: false; error: string };
17
+ interface BasisExisting {
34
18
  activityName: string;
35
19
  lawfulBasis: string;
36
20
  justification: string;
21
+ dataCategories: unknown;
22
+ purposes: unknown;
23
+ reviewDate: Date | null;
24
+ }
25
+ interface BasisMutation {
26
+ activityName: string;
27
+ lawfulBasis: LawfulBasis;
28
+ justification: string;
37
29
  dataCategories: string[];
38
30
  purposes: string[];
39
31
  assessedBy: string;
32
+ assessedAt: Date;
40
33
  reviewDate: Date | null;
41
- createdAt: Date;
42
34
  updatedAt: Date;
43
35
  }
44
- const lawfulBasisStore = new Map<string, LawfulBasisRecord>();
45
- const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
46
- function newId() { return crypto.randomUUID(); }
36
+
37
+ // {{#if ORM=none}}
38
+ // DEVELOPMENT ONLY: replace both stores with one durable transactional store.
39
+ interface BasisRow extends BasisMutation { id: string; tenantId: string; createdAt: Date; removedAt: Date | null }
40
+ const lawfulBasisStore = new Map<string, BasisRow>();
41
+ const auditLog: Array<{ id: string; tenantId: string; action: string; entityId: string; performedBy: string; changes?: unknown; at: Date }> = [];
47
42
  // {{/if}}
48
43
 
49
- // GET /api/lawful-basis?lawfulBasis=consent OR /api/lawful-basis?id=xxx
44
+ function isRecord(value: unknown): value is Record<string, unknown> {
45
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
46
+ }
47
+ function hasOwn(value: Record<string, unknown>, key: string): boolean {
48
+ return Object.prototype.hasOwnProperty.call(value, key);
49
+ }
50
+ function rejectUnknownFields(value: Record<string, unknown>, allowed: readonly string[]): string | null {
51
+ const allowedSet = new Set(allowed);
52
+ const unknown = Object.keys(value).filter((key) => !allowedSet.has(key));
53
+ return unknown.length > 0 ? `Unsupported field(s): ${unknown.join(', ')}` : null;
54
+ }
55
+ function nonEmptyText(value: unknown, field: string, maxLength = 10_000): Validation<string> {
56
+ if (typeof value !== 'string' || !value.trim() || value.trim().length > maxLength) {
57
+ return { ok: false, error: `${field} must be non-empty text of at most ${maxLength} characters` };
58
+ }
59
+ return { ok: true, value: value.trim() };
60
+ }
61
+ function nonEmptyStringArray(value: unknown, field: string): Validation<string[]> {
62
+ if (!Array.isArray(value) || value.length === 0 || value.length > 100) {
63
+ return { ok: false, error: `${field} must be a non-empty array with at most 100 items` };
64
+ }
65
+ const result: string[] = [];
66
+ for (const item of value) {
67
+ if (typeof item !== 'string' || !item.trim()) return { ok: false, error: `${field} must contain only non-empty strings` };
68
+ if (!result.includes(item.trim())) result.push(item.trim());
69
+ }
70
+ return { ok: true, value: result };
71
+ }
72
+ function isLawfulBasis(value: unknown): value is LawfulBasis {
73
+ return typeof value === 'string' && (LAWFUL_BASES as readonly string[]).includes(value);
74
+ }
75
+ function parseReviewDate(value: unknown): Date | null | undefined {
76
+ if (value === null || value === '') return null;
77
+ if (value instanceof Date) return Number.isFinite(value.getTime()) ? new Date(value.getTime()) : undefined;
78
+ if (typeof value !== 'string') return undefined;
79
+ const input = value.trim();
80
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(input);
81
+ const dateTime = /^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/.exec(input);
82
+ if (!dateOnly && !dateTime) return undefined;
83
+ const timestamp = Date.parse(input);
84
+ if (!Number.isFinite(timestamp)) return undefined;
85
+ const parts = dateOnly ?? dateTime;
86
+ const check = new Date(Date.UTC(Number(parts![1]), Number(parts![2]) - 1, Number(parts![3])));
87
+ if (check.getUTCFullYear() !== Number(parts![1]) || check.getUTCMonth() + 1 !== Number(parts![2]) || check.getUTCDate() !== Number(parts![3])) return undefined;
88
+ return new Date(timestamp);
89
+ }
90
+ function buildBasisMutation(input: Record<string, unknown>, actorId: string, existing?: BasisExisting, now = new Date()): Validation<BasisMutation> {
91
+ const activityName = hasOwn(input, 'activityName') ? nonEmptyText(input.activityName, 'activityName', 500) : existing ? nonEmptyText(existing.activityName, 'stored activityName', 500) : { ok: false as const, error: 'activityName is required' };
92
+ if (!activityName.ok) return activityName;
93
+ const basisValue = hasOwn(input, 'lawfulBasis') ? input.lawfulBasis : existing?.lawfulBasis;
94
+ if (!isLawfulBasis(basisValue)) return { ok: false, error: 'lawfulBasis is not one of the six supported NDPA bases' };
95
+ const justification = hasOwn(input, 'justification') ? nonEmptyText(input.justification, 'justification') : existing ? nonEmptyText(existing.justification, 'stored justification') : { ok: false as const, error: 'justification is required' };
96
+ if (!justification.ok) return justification;
97
+ if (basisValue === 'legitimate_interests' && justification.value.length < 20) {
98
+ return { ok: false, error: 'legitimate_interests requires a detailed justification covering the interest, necessity, and balancing assessment' };
99
+ }
100
+ const categories = nonEmptyStringArray(hasOwn(input, 'dataCategories') ? input.dataCategories : existing?.dataCategories, 'dataCategories');
101
+ if (!categories.ok) return categories;
102
+ const purposes = nonEmptyStringArray(hasOwn(input, 'purposes') ? input.purposes : existing?.purposes, 'purposes');
103
+ if (!purposes.ok) return purposes;
104
+ let reviewDate: Date | null;
105
+ if (hasOwn(input, 'reviewDate')) {
106
+ const parsed = parseReviewDate(input.reviewDate);
107
+ if (parsed === undefined) return { ok: false, error: 'reviewDate must be null, YYYY-MM-DD, or an ISO timestamp with a timezone' };
108
+ if (parsed !== null && parsed.getTime() <= now.getTime()) return { ok: false, error: 'reviewDate must be in the future' };
109
+ reviewDate = parsed;
110
+ } else {
111
+ const parsed = parseReviewDate(existing?.reviewDate ?? null);
112
+ if (parsed === undefined) return { ok: false, error: 'Stored reviewDate is invalid' };
113
+ reviewDate = parsed;
114
+ }
115
+ return { ok: true, value: { activityName: activityName.value, lawfulBasis: basisValue, justification: justification.value, dataCategories: categories.value, purposes: purposes.value, assessedBy: actorId, assessedAt: now, reviewDate, updatedAt: now } };
116
+ }
117
+ function auditChanges(data: BasisMutation) {
118
+ return { activityName: data.activityName, lawfulBasis: data.lawfulBasis, assessedBy: data.assessedBy, reviewDate: data.reviewDate?.toISOString() ?? null };
119
+ }
120
+ async function staffContext(req: NextRequest) {
121
+ const context = await resolveNDPRRequestContext(req);
122
+ return { context, problem: getNDPRContextProblem(context, 'staff') };
123
+ }
124
+
50
125
  export async function GET(req: NextRequest) {
126
+ const { context, problem } = await staffContext(req);
127
+ if (problem) return NextResponse.json({ error: problem.error }, { status: problem.status });
51
128
  const id = req.nextUrl.searchParams.get('id');
52
-
53
129
  if (id) {
54
130
  // {{#if ORM=prisma}}
55
- const record = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
131
+ const record = await prisma.lawfulBasisRecord.findFirst({ where: { id, tenantId: context.tenantId, removedAt: null } });
56
132
  // {{/if}}
57
133
  // {{#if ORM=drizzle}}
58
- const [record] = await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id)).limit(1);
134
+ const [record] = await db.select().from(lawfulBasisRecords).where(and(eq(lawfulBasisRecords.id, id), eq(lawfulBasisRecords.tenantId, context.tenantId), isNull(lawfulBasisRecords.removedAt))).limit(1);
59
135
  // {{/if}}
60
136
  // {{#if ORM=none}}
61
- const record = lawfulBasisStore.get(id) ?? null;
137
+ const candidate = lawfulBasisStore.get(id);
138
+ const record = candidate?.tenantId === context.tenantId && candidate.removedAt === null ? candidate : null;
62
139
  // {{/if}}
63
-
64
- if (!record) {
65
- return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
66
- }
67
- return NextResponse.json(record);
140
+ return record ? NextResponse.json(record) : NextResponse.json({ error: 'Lawful-basis record not found' }, { status: 404 });
68
141
  }
69
-
70
- const lawfulBasis = req.nextUrl.searchParams.get('lawfulBasis');
71
-
142
+ const basisValue = req.nextUrl.searchParams.get('lawfulBasis');
143
+ if (basisValue !== null && !isLawfulBasis(basisValue)) return NextResponse.json({ error: 'lawfulBasis filter is not supported' }, { status: 400 });
144
+ const basis = basisValue ?? undefined;
72
145
  // {{#if ORM=prisma}}
73
- const records = await prisma.lawfulBasisRecord.findMany({
74
- where: lawfulBasis ? { lawfulBasis } : undefined,
75
- orderBy: { createdAt: 'desc' },
76
- });
146
+ const records = await prisma.lawfulBasisRecord.findMany({ where: { tenantId: context.tenantId, removedAt: null, ...(basis ? { lawfulBasis: basis } : {}) }, orderBy: { createdAt: 'desc' } });
77
147
  // {{/if}}
78
148
  // {{#if ORM=drizzle}}
79
- const records = lawfulBasis
80
- ? await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.lawfulBasis, lawfulBasis)).orderBy(desc(lawfulBasisRecords.createdAt))
81
- : await db.select().from(lawfulBasisRecords).orderBy(desc(lawfulBasisRecords.createdAt));
149
+ const records = basis
150
+ ? await db.select().from(lawfulBasisRecords).where(and(eq(lawfulBasisRecords.tenantId, context.tenantId), isNull(lawfulBasisRecords.removedAt), eq(lawfulBasisRecords.lawfulBasis, basis))).orderBy(desc(lawfulBasisRecords.createdAt))
151
+ : await db.select().from(lawfulBasisRecords).where(and(eq(lawfulBasisRecords.tenantId, context.tenantId), isNull(lawfulBasisRecords.removedAt))).orderBy(desc(lawfulBasisRecords.createdAt));
82
152
  // {{/if}}
83
153
  // {{#if ORM=none}}
84
- const records = [...lawfulBasisStore.values()]
85
- .filter((r) => (lawfulBasis ? r.lawfulBasis === lawfulBasis : true))
86
- .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
154
+ const records = [...lawfulBasisStore.values()].filter((row) => row.tenantId === context.tenantId && row.removedAt === null && (!basis || row.lawfulBasis === basis)).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
87
155
  // {{/if}}
88
-
89
156
  return NextResponse.json(records);
90
157
  }
91
158
 
92
- // POST /api/lawful-basis
93
159
  export async function POST(req: NextRequest) {
94
- const body = await req.json();
95
- const {
96
- activityName,
97
- lawfulBasis,
98
- justification,
99
- dataCategories,
100
- purposes,
101
- assessedBy,
102
- reviewDate,
103
- } = body;
104
-
105
- if (!activityName || !lawfulBasis || !justification || !dataCategories || !purposes || !assessedBy) {
106
- return NextResponse.json(
107
- { error: 'activityName, lawfulBasis, justification, dataCategories, purposes, and assessedBy are required' },
108
- { status: 400 },
109
- );
110
- }
111
-
112
- if (!Array.isArray(dataCategories) || !Array.isArray(purposes)) {
113
- return NextResponse.json(
114
- { error: 'dataCategories and purposes must be arrays' },
115
- { status: 400 },
116
- );
117
- }
118
-
160
+ const { context, problem } = await staffContext(req);
161
+ if (problem) return NextResponse.json({ error: problem.error }, { status: problem.status });
162
+ const actorId = context.actorId as string;
163
+ const body: unknown = await req.json().catch(() => null);
164
+ if (!isRecord(body)) return NextResponse.json({ error: 'A JSON object is required' }, { status: 400 });
165
+ const unknown = rejectUnknownFields(body, ['activityName', 'lawfulBasis', 'justification', 'dataCategories', 'purposes', 'reviewDate']);
166
+ if (unknown) return NextResponse.json({ error: unknown }, { status: 400 });
167
+ const normalized = buildBasisMutation(body, actorId);
168
+ if (!normalized.ok) return NextResponse.json({ error: normalized.error }, { status: 400 });
169
+ const data = normalized.value;
119
170
  // {{#if ORM=prisma}}
120
- const record = await prisma.lawfulBasisRecord.create({
121
- data: {
122
- activityName,
123
- lawfulBasis,
124
- justification,
125
- dataCategories,
126
- purposes,
127
- assessedBy,
128
- reviewDate: reviewDate ? new Date(reviewDate) : null,
129
- },
130
- });
131
-
132
- await prisma.complianceAuditLog.create({
133
- data: {
134
- module: 'lawful-basis',
135
- action: 'created',
136
- entityId: record.id,
137
- entityType: 'LawfulBasisRecord',
138
- changes: { activityName, lawfulBasis, assessedBy },
139
- },
171
+ const record = await prisma.$transaction(async (tx) => {
172
+ const created = await tx.lawfulBasisRecord.create({ data: { ...data, tenantId: context.tenantId, removedAt: null } });
173
+ await tx.complianceAuditLog.create({ data: { tenantId: context.tenantId, module: 'lawful-basis', action: 'created', entityId: created.id, entityType: 'LawfulBasisRecord', performedBy: actorId, changes: auditChanges(data) } });
174
+ return created;
140
175
  });
141
176
  // {{/if}}
142
177
  // {{#if ORM=drizzle}}
143
- const [record] = await db.insert(lawfulBasisRecords).values({
144
- activityName,
145
- lawfulBasis,
146
- justification,
147
- dataCategories,
148
- purposes,
149
- assessedBy,
150
- reviewDate: reviewDate ? new Date(reviewDate) : null,
151
- }).returning();
152
-
153
- await db.insert(complianceAuditLog).values({
154
- module: 'lawful-basis',
155
- action: 'created',
156
- entityId: record.id,
157
- entityType: 'LawfulBasisRecord',
158
- changes: { activityName, lawfulBasis, assessedBy },
178
+ const record = await db.transaction(async (tx) => {
179
+ const [created] = await tx.insert(lawfulBasisRecords).values({ ...data, tenantId: context.tenantId, removedAt: null }).returning();
180
+ await tx.insert(complianceAuditLog).values({ tenantId: context.tenantId, module: 'lawful-basis', action: 'created', entityId: created.id, entityType: 'LawfulBasisRecord', performedBy: actorId, changes: auditChanges(data) });
181
+ return created;
159
182
  });
160
183
  // {{/if}}
161
184
  // {{#if ORM=none}}
162
- const now = new Date();
163
- const record: LawfulBasisRecord = {
164
- id: newId(),
165
- activityName,
166
- lawfulBasis,
167
- justification,
168
- dataCategories,
169
- purposes,
170
- assessedBy,
171
- reviewDate: reviewDate ? new Date(reviewDate) : null,
172
- createdAt: now,
173
- updatedAt: now,
174
- };
185
+ const record: BasisRow = { id: crypto.randomUUID(), ...data, tenantId: context.tenantId, createdAt: data.updatedAt, removedAt: null };
175
186
  lawfulBasisStore.set(record.id, record);
176
- auditLog.push({ id: newId(), module: 'lawful-basis', action: 'created', entityId: record.id, at: new Date() });
187
+ auditLog.push({ id: crypto.randomUUID(), tenantId: context.tenantId, action: 'created', entityId: record.id, performedBy: actorId, changes: auditChanges(data), at: data.updatedAt });
177
188
  // {{/if}}
178
-
179
189
  return NextResponse.json(record, { status: 201 });
180
190
  }
181
191
 
182
- // PUT /api/lawful-basis
183
192
  export async function PUT(req: NextRequest) {
184
- const body = await req.json();
185
- const { id, ...data } = body;
186
-
187
- if (!id) {
188
- return NextResponse.json({ error: 'id is required' }, { status: 400 });
189
- }
190
-
191
- if (data.reviewDate) {
192
- data.reviewDate = new Date(data.reviewDate);
193
- }
194
-
193
+ const { context, problem } = await staffContext(req);
194
+ if (problem) return NextResponse.json({ error: problem.error }, { status: problem.status });
195
+ const actorId = context.actorId as string;
196
+ const body: unknown = await req.json().catch(() => null);
197
+ if (!isRecord(body)) return NextResponse.json({ error: 'A JSON object is required' }, { status: 400 });
198
+ const unknown = rejectUnknownFields(body, ['id', 'activityName', 'lawfulBasis', 'justification', 'dataCategories', 'purposes', 'reviewDate']);
199
+ if (unknown) return NextResponse.json({ error: unknown }, { status: 400 });
200
+ const idResult = nonEmptyText(body.id, 'id', 200);
201
+ if (!idResult.ok) return NextResponse.json({ error: idResult.error }, { status: 400 });
202
+ if (!['activityName', 'lawfulBasis', 'justification', 'dataCategories', 'purposes', 'reviewDate'].some((field) => hasOwn(body, field))) return NextResponse.json({ error: 'Provide at least one allowlisted lawful-basis field to update' }, { status: 400 });
203
+ const id = idResult.value;
204
+ let validationError: string | null = null;
195
205
  // {{#if ORM=prisma}}
196
- const existing = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
197
- if (!existing) {
198
- return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
199
- }
200
-
201
- const record = await prisma.lawfulBasisRecord.update({
202
- where: { id },
203
- data,
204
- });
205
-
206
- await prisma.complianceAuditLog.create({
207
- data: {
208
- module: 'lawful-basis',
209
- action: 'updated',
210
- entityId: record.id,
211
- entityType: 'LawfulBasisRecord',
212
- changes: data,
213
- },
206
+ const record = await prisma.$transaction(async (tx) => {
207
+ const existing = await tx.lawfulBasisRecord.findFirst({ where: { id, tenantId: context.tenantId, removedAt: null } });
208
+ if (!existing) return null;
209
+ const normalized = buildBasisMutation(body, actorId, existing);
210
+ if (!normalized.ok) { validationError = normalized.error; return null; }
211
+ const data = normalized.value;
212
+ await tx.lawfulBasisRecord.updateMany({ where: { id, tenantId: context.tenantId, removedAt: null }, data });
213
+ const updated = await tx.lawfulBasisRecord.findFirstOrThrow({ where: { id, tenantId: context.tenantId, removedAt: null } });
214
+ await tx.complianceAuditLog.create({ data: { tenantId: context.tenantId, module: 'lawful-basis', action: 'updated', entityId: id, entityType: 'LawfulBasisRecord', performedBy: actorId, changes: auditChanges(data) } });
215
+ return updated;
214
216
  });
215
217
  // {{/if}}
216
218
  // {{#if ORM=drizzle}}
217
- const [existing] = await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id)).limit(1);
218
- if (!existing) {
219
- return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
220
- }
221
-
222
- const [record] = await db.update(lawfulBasisRecords).set(data).where(eq(lawfulBasisRecords.id, id)).returning();
223
-
224
- await db.insert(complianceAuditLog).values({
225
- module: 'lawful-basis',
226
- action: 'updated',
227
- entityId: record.id,
228
- entityType: 'LawfulBasisRecord',
229
- changes: data,
219
+ const record = await db.transaction(async (tx) => {
220
+ const [existing] = await tx.select().from(lawfulBasisRecords).where(and(eq(lawfulBasisRecords.id, id), eq(lawfulBasisRecords.tenantId, context.tenantId), isNull(lawfulBasisRecords.removedAt))).limit(1);
221
+ if (!existing) return null;
222
+ const normalized = buildBasisMutation(body, actorId, existing);
223
+ if (!normalized.ok) { validationError = normalized.error; return null; }
224
+ const data = normalized.value;
225
+ const [updated] = await tx.update(lawfulBasisRecords).set(data).where(and(eq(lawfulBasisRecords.id, id), eq(lawfulBasisRecords.tenantId, context.tenantId), isNull(lawfulBasisRecords.removedAt))).returning();
226
+ await tx.insert(complianceAuditLog).values({ tenantId: context.tenantId, module: 'lawful-basis', action: 'updated', entityId: id, entityType: 'LawfulBasisRecord', performedBy: actorId, changes: auditChanges(data) });
227
+ return updated;
230
228
  });
231
229
  // {{/if}}
232
230
  // {{#if ORM=none}}
233
231
  const existing = lawfulBasisStore.get(id);
234
- if (!existing) {
235
- return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
232
+ let record: BasisRow | null = null;
233
+ if (existing?.tenantId === context.tenantId && existing.removedAt === null) {
234
+ const normalized = buildBasisMutation(body, actorId, existing);
235
+ if (!normalized.ok) validationError = normalized.error;
236
+ else {
237
+ record = { ...existing, ...normalized.value, id };
238
+ lawfulBasisStore.set(id, record);
239
+ auditLog.push({ id: crypto.randomUUID(), tenantId: context.tenantId, action: 'updated', entityId: id, performedBy: actorId, changes: auditChanges(normalized.value), at: normalized.value.updatedAt });
240
+ }
236
241
  }
237
- const record: LawfulBasisRecord = { ...existing, ...data, id, updatedAt: new Date() };
238
- lawfulBasisStore.set(id, record);
239
- auditLog.push({ id: newId(), module: 'lawful-basis', action: 'updated', entityId: record.id, at: new Date() });
240
242
  // {{/if}}
241
-
242
- return NextResponse.json(record);
243
+ if (validationError) return NextResponse.json({ error: validationError }, { status: 400 });
244
+ return record ? NextResponse.json(record) : NextResponse.json({ error: 'Lawful-basis record not found' }, { status: 404 });
243
245
  }
244
246
 
245
- // DELETE /api/lawful-basis?id=xxx
246
247
  export async function DELETE(req: NextRequest) {
248
+ const { context, problem } = await staffContext(req);
249
+ if (problem) return NextResponse.json({ error: problem.error }, { status: problem.status });
250
+ const actorId = context.actorId as string;
247
251
  const id = req.nextUrl.searchParams.get('id');
248
-
249
- if (!id) {
250
- return NextResponse.json({ error: 'id is required' }, { status: 400 });
251
- }
252
-
252
+ if (!id?.trim()) return NextResponse.json({ error: 'id is required' }, { status: 400 });
253
+ const archivedAt = new Date();
253
254
  // {{#if ORM=prisma}}
254
- const existing = await prisma.lawfulBasisRecord.findUnique({ where: { id } });
255
- if (!existing) {
256
- return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
257
- }
258
-
259
- await prisma.lawfulBasisRecord.delete({ where: { id } });
260
-
261
- await prisma.complianceAuditLog.create({
262
- data: {
263
- module: 'lawful-basis',
264
- action: 'deleted',
265
- entityId: id,
266
- entityType: 'LawfulBasisRecord',
267
- changes: { activityName: existing.activityName },
268
- },
255
+ const archived = await prisma.$transaction(async (tx) => {
256
+ const existing = await tx.lawfulBasisRecord.findFirst({ where: { id, tenantId: context.tenantId, removedAt: null } });
257
+ if (!existing) return false;
258
+ const result = await tx.lawfulBasisRecord.updateMany({ where: { id, tenantId: context.tenantId, removedAt: null }, data: { removedAt: archivedAt, updatedAt: archivedAt } });
259
+ if (result.count !== 1) return false;
260
+ await tx.complianceAuditLog.create({ data: { tenantId: context.tenantId, module: 'lawful-basis', action: 'archived', entityId: id, entityType: 'LawfulBasisRecord', performedBy: actorId, changes: { activityName: existing.activityName, removedAt: archivedAt.toISOString() } } });
261
+ return true;
269
262
  });
270
263
  // {{/if}}
271
264
  // {{#if ORM=drizzle}}
272
- const [existing] = await db.select().from(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id)).limit(1);
273
- if (!existing) {
274
- return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
275
- }
276
-
277
- await db.delete(lawfulBasisRecords).where(eq(lawfulBasisRecords.id, id));
278
-
279
- await db.insert(complianceAuditLog).values({
280
- module: 'lawful-basis',
281
- action: 'deleted',
282
- entityId: id,
283
- entityType: 'LawfulBasisRecord',
284
- changes: { activityName: existing.activityName },
265
+ const archived = await db.transaction(async (tx) => {
266
+ const [existing] = await tx.select().from(lawfulBasisRecords).where(and(eq(lawfulBasisRecords.id, id), eq(lawfulBasisRecords.tenantId, context.tenantId), isNull(lawfulBasisRecords.removedAt))).limit(1);
267
+ if (!existing) return false;
268
+ const [updated] = await tx.update(lawfulBasisRecords).set({ removedAt: archivedAt, updatedAt: archivedAt }).where(and(eq(lawfulBasisRecords.id, id), eq(lawfulBasisRecords.tenantId, context.tenantId), isNull(lawfulBasisRecords.removedAt))).returning();
269
+ if (!updated) return false;
270
+ await tx.insert(complianceAuditLog).values({ tenantId: context.tenantId, module: 'lawful-basis', action: 'archived', entityId: id, entityType: 'LawfulBasisRecord', performedBy: actorId, changes: { activityName: existing.activityName, removedAt: archivedAt.toISOString() } });
271
+ return true;
285
272
  });
286
273
  // {{/if}}
287
274
  // {{#if ORM=none}}
288
275
  const existing = lawfulBasisStore.get(id);
289
- if (!existing) {
290
- return NextResponse.json({ error: 'Lawful basis record not found' }, { status: 404 });
276
+ const archived = existing?.tenantId === context.tenantId && existing.removedAt === null;
277
+ if (archived && existing) {
278
+ lawfulBasisStore.set(id, { ...existing, removedAt: archivedAt, updatedAt: archivedAt });
279
+ auditLog.push({ id: crypto.randomUUID(), tenantId: context.tenantId, action: 'archived', entityId: id, performedBy: actorId, changes: { activityName: existing.activityName, removedAt: archivedAt.toISOString() }, at: archivedAt });
291
280
  }
292
- lawfulBasisStore.delete(id);
293
- auditLog.push({ id: newId(), module: 'lawful-basis', action: 'deleted', entityId: id, at: new Date() });
294
281
  // {{/if}}
295
-
296
- return NextResponse.json({ success: true });
282
+ return archived ? NextResponse.json({ success: true, archived: true }) : NextResponse.json({ error: 'Lawful-basis record not found' }, { status: 404 });
297
283
  }