Pipeline превращает AI-сгенерированный список follow-up расписаний в напоминание (email через Mailgun, WhatsApp через WABA-org), которое срабатывает в указанный момент. Используется в patient-portal. UX-pattern (per-card vs bulk vs hybrid) — отдельно, см. follow-up-reminders-ui-pattern.

Источники данных

LLM-pipeline (followup_generation Langfuse prompt) генерирует список расписаний в shape:

  • days_till_followup: целое число (REQUIRED в schema по всем версиям)
  • timeframe_description: human-readable строка («In 30 days», «In about 1 week»)
  • purpose_description: пациент-friendly мотивация
  • follow_up_tests[]: тесты этого расписания (test_name, test_purpose)

Output сохраняется в двух местах:

  • Postgres BloodTest.enrichmentDraft.followUp.followUpSchedules — все поля как есть
  • FHIR Healthcare API CarePlan.activity — преобразовано в FHIR-shape

Read path

Patient-portal preferенциально читает данные из FHIR. Только если FHIR-CarePlan отсутствует — fallback на enrichmentDraft.followUp из Postgres.

patient-portal test-data.ts
  → fhirData.followUp (preferred)
  → или draft.followUp (fallback only when fhirData is null)

На проде FHIR заполнен → читается FHIR-путь. Локально (где FHIR пустой) → читается fallback.

Создание реминдера

Server-action createReminder(testId, testName, daysFromTest, description) в patient-portal:

  1. Auth check (current patient session)
  2. Verify test ownership через Prisma
  3. Compute remindAt = test.testDate + daysFromTest * 86_400_000 (с fallback на «завтра» если результат в прошлом)
  4. Upsert в PatientReminder table — unique key (patientId, bloodTestId, testName)
  5. Send Inngest event reminder/scheduled который sleeps до remindAt, затем доставляет notification

Inngest function reminder-send после wake-up проверяет статус (cancelled? уже sent?) и шлёт через notification channel организации.

Известные дефекты

FHIR-roundtrip-loss для days_till_followup

days_till_followup (числовое количество дней) генерируется LLM, сохраняется в Postgres enrichmentDraft, но полностью теряется в FHIR-roundtrip. На проде это приводит к тому, что click «Remind me» на любом расписании не создаёт реминдер.

Причина:

  • Writer в FHIR CarePlan-builder сохраняет только текстовый timeframe_description через scheduledString, число дней нигде не записывается.
  • Reader при чтении CarePlan игнорирует extension’ы про timing, возвращает schedule без days_till_followup.

Симптом для пользователя: расчёт remindAt получает undefined * 86_400_000 = NaN, new Date(NaN) это Invalid Date, Prisma upsert валится с PrismaClientValidationError. Server-action ловит и возвращает {success: false}. На per-card UI handler не имел catch блока — failure был silent (button оставалась в исходном состоянии без feedback). На bulk UI handler имеет catch с toast.error — failure виден. То есть выбор UX случайно влияет на видимость этого defect.

Локально дефект не воспроизводится потому что FHIR пустой и читается fallback (где число есть).

Silent client-side fail на per-card UI

Per-card версия UI вызывает server-action в try/finally без catch. Любой throw из server-action (auth/CSRF/network/Prisma) глотался без UI-feedback. Bulk версия имеет catch с toast — failure виден.

Time-of-day reminder

remindAt наследует время суток от testDate (когда сдавали кровь). Не нормализуется к утру в часовом поясе пациента — реминдер может прийти, например, в 7:42 ночи если кровь сдавали ранним утром.

Unique-key collision при bulk-set

Unique-key (patientId, bloodTestId, testName) в PatientReminder. testName в bulk-варианте берётся как schedule.follow_up_tests?.[0]?.test_name — только первый тест расписания. Если у двух расписаний первый test_name совпадает (например оба «Iron Panel» с разным days_till_followup), второй upsert перетирает первый — остаётся одна запись вместо двух.

Stale timeframe_description

Текст «In about 30 days» генерируется LLM при анализе теста и не пересчитывается при просмотре через месяц. Для старого теста становится stale — ни UI, ни pipeline не флагуют это.

Связанные решения

Открытые вопросы

  • Backfill реминдеров для уже existing CarePlans без days-till-followup extension — оставить regex-fallback на timeframe_description, или прогнать одноразовый pipeline-rerun для пациентов с активными тестами?
  • Multi-language timeframe_description: AI-pipeline может вернуть строку на греческом / польском / немецком (зависит от patient locale). Regex-fallback узнаёт только английские unit-слова (day/week/month/year). Стратегия — заставить pipeline всегда возвращать английский text + локализовать на UI-стороне, или расширять regex coverage.
  • Канал доставки реминдера и fallback при недоступности (нет email-verify, нет WABA-настройки) — undocumented.

Связано

  • patient-portal — продукт, в котором живёт фича
  • fhir-careplan — FHIR-ресурс используемый для хранения расписаний
  • inngest — orchestrator реминдер-doзагрузки

Источники