feat(vaccinations): carry the immunization log through export and restore

- a new backup section reads the dose and its document link through
  their own delegates and writes both ends in one file, the arrangement
  the visit section already uses
- the practitioner and the visit remap to the rows the same restore put
  back; the booster reminder never travels, so its reference resolves to
  null and the drop is reported rather than swallowed
- the antigen slug restores verbatim: a slug the catalogue has since
  dropped still says what a person was given
- the round trip seeds a dose with every optional column filled and
  reads each one back, so a returning row count cannot stand in for a
  returning record
This commit is contained in:
Marc-André Bombeck committed 2026-08-08 13:54:24 +02:00
1 parent 1bd97de943
commit 6926caa11d
18 files changed
+698 -3

No files matched your search

+45
View File
@@ -57,6 +57,7 @@ import {
RhythmClassification,
SecondarySymptom,
SleepStage,
VaccinationSite,
} from "@/generated/prisma/enums";
import {
DEFAULT_HEALTH_PROFILE_AI_SECTIONS,
@@ -884,6 +885,44 @@ const encounterLinkBackupSchema = z
})
.passthrough();
/**
* One administered dose.
*
* `antigenSlug` is a free string here rather than an enum of the catalogue
* this release ships, deliberately: a slug the catalogue has since dropped
* must still restore, and the renderer degrades to `vaccineName`. Validating
* it at the restore boundary would refuse a file the app itself wrote.
*/
const vaccinationBackupSchema = z
.object({
id: z.string().min(1),
occurredAt: isoDateTime,
antigenSlug: z.string().nullable().optional(),
vaccineName: z.string().nullable().optional(),
doseNumber: z.number().int().nullable().optional(),
seriesDoses: z.number().int().nullable().optional(),
lotNumber: z.string().nullable().optional(),
site: z.enum(VaccinationSite).nullable().optional(),
practitionerId: z.string().nullable().optional(),
encounterId: z.string().nullable().optional(),
// Carried so the loss is visible in the file. The restore resolves it to
// NULL and names the drop: the reminder it points at does not travel.
reminderId: z.string().nullable().optional(),
noteEncrypted: base64BytesSchema.nullable().optional(),
createdAt: isoDateTime,
updatedAt: isoDateTime,
deletedAt: isoDateTime.nullable().optional(),
})
.passthrough();
const vaccinationLinkBackupSchema = z
.object({
vaccinationId: z.string().min(1),
targetId: z.string().min(1),
createdAt: isoDateTime,
})
.passthrough();
const allergyBackupSchema = z
.object({
id: z.string().min(1),
@@ -1036,6 +1075,12 @@ export const backupPayloadSchema = z
encounterDocumentLinks: z.array(encounterLinkBackupSchema).default([]),
encounterLabLinks: z.array(encounterLinkBackupSchema).default([]),
encounterConditionLinks: z.array(encounterLinkBackupSchema).default([]),
// The immunization log and the pages it was transcribed from. Defaulted
// for the same reason as the sections above: a file written before the
// tables existed carries no key, and an account with an empty Impfpass
// writes [].
vaccinations: z.array(vaccinationBackupSchema).default([]),
vaccinationDocumentLinks: z.array(vaccinationLinkBackupSchema).default([]),
manifest: backupManifestSchema.nullable().default(null),
})
.passthrough()