Commit Graph
8 Commits
Author SHA1 Message Date
Marc-André Bombeck 5619783648 Let a checkup be skipped or pushed back, and remember every outcome
Refs #223. A due Vorsorge item could only be done, deleted or ignored;
Telegram's postpone moved the date with no trace. Both decisions are
first-class now, and both are honest:

- POST /{id}/skip (no body): the interval restarts from the skip instant
  through the recurrence engine, lastSkippedAt is stamped, skipCount
  increments, any snooze clears. lastSatisfiedAt is NEVER touched — done
  and skipped stay distinguishable forever, enforced in the primitive and
  pinned by tests.
- POST /{id}/snooze ({until: YYYY-MM-DD}): the server resolves the day to
  the notifyHour in the profile timezone and moves snoozedUntil and
  nextDueAt to the same instant, so every due-state consumer follows
  without knowing snooze exists. At least tomorrow, at most five years.
- GET /{id}/history: the new append-only completion ledger
  (MeasurementReminderEvent), one row per satisfy from every engine path
  and per skip, onTime derived at write time against the due instant that
  was current when the event landed.

Two anchor fixes close the backdated-event gaps: the auto-resolve floor is
now the later of last satisfy and last skip (an old reading synced late no
longer resolves the cycle the user just skipped), and a backdated real
result re-anchors the next due at the later of its own instant and the
skip decision.

Screenings stay skippable and snoozable — the satisfy-side 409 guards
claimed fulfilment; a skip claims the opposite. Appointment reminders 404
on all three routes, in the lookup, like every by-id sibling. /checkups
gains the postpone sheet (quick chips, date field, the skip zone with its
own confirm) in both view branches, with muted meta lines and no card
colour change. The MCP preventive-care tool carries the resolved fields;
the OpenAPI contract publishes all three paths and the widened DTO.
2026-08-14 09:23:58 +02:00
Marc-André Bombeck 958f3bc50a test(vaccinations): reconcile the structural guards with the new surfaces
- freeze the booster-mint and upload-suggestion routes into the sharing
  surface guard with their why sentences; delegable-write and mutation
  inventories move by the two routes they add
- inventory the two new routes as module-exempt data routes (SURFACE-gated,
  like the medications posture)
- give the booster-plan audit action its own owner-facing verb in six locales
- register the dose delete as a destructive control; route the vaccination
  success toasts through the outcome module
- move the report leaf counts (structured 15→16, all 92→93) and the history
  group count in the panel; regenerate the OpenAPI selection bound
- seed the immunization rows in the export-route mock so the aggregator no
  longer throws for a fully-selected report
2026-08-08 21:43:28 +02:00
Marc-André Bombeck 9b58394724 feat(vaccinations): link documents to doses, with the upload suggestion
- from the record: a document link picker on the capture form, gated on the
  documents module (blanked when off, never post-filtered)
- from the upload: a VACCINATION scan offers to file against a dose recorded
  within the shared ±7-day window; a single candidate pre-selects, two offer a
  picker with nothing pre-selected (WR-8 seen red on the pre-select rule,
  reverted by content hash), zero shows nothing, and it never blocks the upload
- the suggestion rule reuses the visit moment's window constant rather than
  forking it; it registers with the shared filing module when that lands
- suggestion and link copy in six locales
2026-08-08 21:11:21 +02:00
Marc-André Bombeck b5ce44628a feat(vaccinations): mint the booster reminder a logged dose suggests
- rung 2: a prefilled, user-confirmed, editable, declinable prompt writes an
  ordinary VORSORGE reminder keyed on the dose's primary antigen; the
  published origin enum and reminder DTO are unchanged
- idempotent by antigen — a second confirmation re-anchors the existing
  reminder rather than minting a second (WR-9 seen red on the reminder count,
  reverted by content hash)
- rolling day cadence so logging the next dose re-anchors the due date through
  the Phase 1 satisfy matcher; the whole loop proven through real routes
- booster prompt copy in six locales, plural-aware
2026-08-08 21:01:25 +02:00
Marc-André Bombeck 82982038cb feat(vaccinations): let a logged dose clear the boosters it answers
- the dose is expanded to its component antigens first, so one Tdap
  entry settles a tetanus booster and a pertussis booster at once;
  matching the preparation's own slug would leave both standing
- it runs inside the create transaction and nowhere else: no cron, no
  ingest hook, and an edit never re-runs it, so correcting a typo in a
  date cannot move a reminder
- forward-only, which is what makes transcribing an old record safe: a
  page from decades ago never clears a booster that is due now
- a free-text-only dose matches nothing; a name string is not evidence
  about what was in the syringe
- an ordinary preventive-care reminder carries no antigen key and is
  structurally out of reach
2026-08-08 14:36:08 +02:00
Marc-André Bombeck 4dc3b1aa27 feat(vaccinations): add the wire schemas and the resolved DTO
- a dose needs a day and one way of naming what it was, and nothing
  else; a required catalogue pick would make an old Pass line unloggable
- the write path refuses a future date and a slug the catalogue does not
  offer, while storage stays tolerant so an older backup restores whole
- the DTO publishes the practitioner, the visit, the linked pages, the
  catalogue entry and the derived series already resolved, in numbers
  rather than localised text
- a linked page's name and date are withheld from a delegate whose grant
  does not reach the vault, and the withholding says so rather than
  looking like a page with no name
2026-08-08 14:09:52 +02:00
Marc-André Bombeck e44b1615e1 feat(vaccinations): derive the series position per component antigen
- one pure function resolves "dose N of M" over a person's whole
  history, so the number rides the DTO already computed and no client
  re-derives it
- a combined dose counts into every antigen it contains, at whatever
  position each of those series is at: one Tdap line is a tetanus
  booster and a first pertussis dose in the same act
- the Pass's own dose number wins over the count, and the record's own
  series length wins over the catalogue's
- a seasonal dose has no series to be part of, so nothing reads as a
  booster on a length it never had
- interval adjudication is deliberately absent; this is arithmetic on
  what a person wrote down
2026-08-08 14:00:39 +02:00
Marc-André Bombeck e190c32c77 feat(vaccinations): seed the antigen-level vaccine catalogue
- 35 entries across the childhood, standard, later-decade, indication
  and travel sets, so a Pass covering a whole life has somewhere to land
- ATC-coded from the WHO J07 subtree, component-aware so one combined
  dose counts into every antigen series it contains, and each entry
  cites the document its numbers come from
- no manufacturer column and no trade name in any shipped string; the
  one place a trade name belongs is the person's own transcription
- the resolver is the only lookup path and answers null rather than
  throwing, so a slug a later release drops still renders as what the
  person wrote
2026-08-08 13:58:17 +02:00