feat(profile): capture the insurer IK number

Add an optional German insurer institution number (IKNR, 9 digits) to
the user profile, alongside the existing insurer name and KVNR. The
IKNR is a public registry identifier for the institution, not a
per-person quasi-identifier, so it is stored plaintext, matching
insurer_name rather than the encrypted KVNR column.

The profile GET echoes the field; PATCH/PUT accept it via Zod —
optional, empty string maps to null, and a non-empty value must be
exactly 9 digits (422 otherwise, no checksum). The write is built
field-by-field. /api/user/profile gains an OpenAPI registration with
the profile read/write schemas.

Migration 0102 adds the nullable column, NULL-defaulted, forward-only.
This commit is contained in:
Marc-André Bombeck committed 2026-06-01 23:24:10 +02:00
1 parent 9ecd798119
commit b99b07ad38
15 files changed
+645

No files matched your search

+1
View File
@@ -66,6 +66,7 @@ export const GET = apiHandler(async () => {
// v1.7.0 — health-record export identity fields. All optional.
fullName: user.fullName ?? null,
insurerName: user.insurerName ?? null,
insurerIkNumber: user.insurerIkNumber ?? null,
insuranceNumber,
});
});
+1
View File
@@ -33,6 +33,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
timezone: result.user.timezone,
fullName: result.user.fullName,
insurerName: result.user.insurerName,
insurerIkNumber: result.user.insurerIkNumber,
hasInsuranceNumber: result.user.hasInsuranceNumber,
});
});
@@ -81,6 +81,33 @@ describe("GET /api/user/profile", () => {
expect(body.data.heightCm).toBe(180);
expect(body.data.locale).toBe("de");
});
it("echoes the insurer IK number on GET", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.user.findUnique).mockResolvedValue({
username: "marc",
displayName: null,
email: null,
dateOfBirth: null,
gender: null,
heightCm: null,
locale: null,
timezone: "Europe/Berlin",
moodReminderEnabled: false,
fullName: null,
insurerName: "Example Insurer",
insurerIkNumber: "101234567",
insuranceNumberEncrypted: null,
} as never);
const res = await callGet(makeGetReq());
expect(res.status).toBe(200);
const body = (await res.json()) as {
data: { insurerIkNumber: string | null; insurerName: string | null };
};
expect(body.data.insurerIkNumber).toBe("101234567");
expect(body.data.insurerName).toBe("Example Insurer");
});
});
describe("PATCH /api/user/profile", () => {
@@ -132,4 +159,45 @@ describe("PATCH /api/user/profile", () => {
}),
);
});
it("accepts a valid IKNR and echoes it back", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
vi.mocked(prisma.user.update).mockResolvedValue({
id: "user-1",
username: "marc",
displayName: null,
email: null,
role: "USER",
heightCm: null,
dateOfBirth: null,
gender: null,
timezone: "Europe/Berlin",
locale: null,
moodReminderEnabled: false,
fullName: null,
insurerName: null,
insurerIkNumber: "101234567",
insuranceNumberEncrypted: null,
} as never);
const res = await PATCH(req({ insurerIkNumber: "101234567" }));
expect(res.status).toBe(200);
expect(prisma.user.update).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ insurerIkNumber: "101234567" }),
}),
);
const body = (await res.json()) as {
data: { insurerIkNumber: string | null };
};
expect(body.data.insurerIkNumber).toBe("101234567");
});
it("returns 422 for a malformed IKNR", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
const res = await PATCH(req({ insurerIkNumber: "abc" }));
expect(res.status).toBe(422);
expect(prisma.user.update).not.toHaveBeenCalled();
});
});
+3
View File
@@ -39,6 +39,7 @@ export const GET = apiHandler(async () => {
moodReminderEnabled: true,
fullName: true,
insurerName: true,
insurerIkNumber: true,
insuranceNumberEncrypted: true,
},
});
@@ -66,6 +67,7 @@ export const GET = apiHandler(async () => {
moodReminderEnabled: dbUser?.moodReminderEnabled ?? false,
fullName: dbUser?.fullName ?? null,
insurerName: dbUser?.insurerName ?? null,
insurerIkNumber: dbUser?.insurerIkNumber ?? null,
insuranceNumber,
});
});
@@ -95,6 +97,7 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
moodReminderEnabled: result.user.moodReminderEnabled,
fullName: result.user.fullName,
insurerName: result.user.insurerName,
insurerIkNumber: result.user.insurerIkNumber,
hasInsuranceNumber: result.user.hasInsuranceNumber,
});
});