From 2c91dc7b755a03153958730985474fd6a89debda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 1 Jun 2026 23:42:44 +0200 Subject: [PATCH] refactor(medications): share the wizard cadence-line builder `summariseCadence` and `summariseScheduleDraft` each re-spelled the cadence phrase, its detail clause, and the interpolation count. Extract a single `{mode, cadence, subControls}`-shaped helper both call so the medication-wide and per-schedule summaries can't drift apart, and drop the now-redundant `summaryKeyForCadence` wrapper. Behaviour is identical; add coverage pinning the per-schedule summary to the medication-wide summary's leading cadence segment. --- .../wizard/__tests__/wizard-payload.test.ts | 44 +++++++ .../medications/wizard/wizard-payload.ts | 108 +++++++++--------- 2 files changed, 98 insertions(+), 54 deletions(-) diff --git a/src/components/medications/wizard/__tests__/wizard-payload.test.ts b/src/components/medications/wizard/__tests__/wizard-payload.test.ts index 24f920607..4dcef09f1 100644 --- a/src/components/medications/wizard/__tests__/wizard-payload.test.ts +++ b/src/components/medications/wizard/__tests__/wizard-payload.test.ts @@ -14,6 +14,7 @@ import { rowFromTreatment, setActiveSchedule, summariseCadence, + summariseScheduleDraft, validateStep, type WizardPayload, WIZARD_TREATMENT_MAPPING, @@ -559,6 +560,49 @@ describe("summariseCadence", () => { }); }); +describe("summariseScheduleDraft — shared cadence-line helper", () => { + const t = makeStubT(); + + // The per-schedule summary and the medication-wide summary build their + // cadence segment from the same extracted helper, so for an equivalent + // cadence shape the draft summary equals the leading segment of the + // full summary (which trails the times / course-window phrases). + it("matches summariseCadence's leading cadence segment", () => { + const cases: WizardPayload[] = [ + withCadence("daily"), + withCadence("everyNWeeks", { intervalWeeks: 2, weekdays: ["MO"] }), + withCadence("rolling", { rollingDays: 14 }), + withCadence("weekdays", { weekdays: ["MO", "WE", "FR"] }), + withCadence("monthly", { dayOfMonth: 15 }), + ]; + for (const base of cases) { + // `withCadence` only sets the flat mirror fields; commit them onto + // the active schedule draft so the draft carries the same cadence. + const p = commitActiveDraft(base); + const draft = p.schedules[p.activeScheduleIndex]; + const draftLine = summariseScheduleDraft(draft, t); + const fullSummary = summariseCadence(p, t); + // The draft line's cadence segment is the leading " · " segment of + // the full summary (which trails times / course-window phrases). + const firstSegment = fullSummary.split(" · ")[0]; + expect(draftLine.split(" · ")[0]).toBe(firstSegment); + } + }); + + it("threads the everyNWeeks interval into the cadence key", () => { + const p = commitActiveDraft(withCadence("everyNWeeks", { intervalWeeks: 3 })); + const out = summariseScheduleDraft(p.schedules[p.activeScheduleIndex], t); + expect(out).toMatch(/cadence\.everyNWeeks[^|]*"n":3/); + }); + + it("omits the cadence detail for a one-shot draft", () => { + const p = commitActiveDraft(withCadence("oneShot")); + const out = summariseScheduleDraft(p.schedules[p.activeScheduleIndex], t); + expect(out).not.toContain("weekdaysDetail"); + expect(out).not.toContain("dayOfMonthDetail"); + }); +}); + describe("hydrateWizardPayload", () => { function makeInitial( overrides: Partial = {}, diff --git a/src/components/medications/wizard/wizard-payload.ts b/src/components/medications/wizard/wizard-payload.ts index 16ec63843..21e7f27ce 100644 --- a/src/components/medications/wizard/wizard-payload.ts +++ b/src/components/medications/wizard/wizard-payload.ts @@ -712,10 +712,6 @@ function summaryKeyForDraft( } } -function summaryKeyForCadence(payload: WizardPayload): string { - return summaryKeyForDraft(payload.mode, payload.cadence, payload.subControls); -} - function weekdaysDetail( tokens: readonly string[], t: (key: string, params?: Record) => string, @@ -732,25 +728,71 @@ function weekdaysDetail( }); } +/** + * The cadence-bearing slice both {@link WizardPayload} and + * {@link ScheduleDraft} share. Summary helpers only ever read these three + * fields, so the cadence-line builder below stays draft/payload-agnostic. + */ +type CadenceShape = Pick; + +/** + * The interpolation count the cadence summary keys read. `everyNWeeks` + * surfaces the week interval; `rolling` the day interval; everything else + * leaves it at zero. Extracted so the medication-wide and per-schedule + * summaries can't drift apart. + */ +function cadenceInterpolationN(shape: CadenceShape): number { + if (shape.cadence.kind === "everyNWeeks") return shape.subControls.intervalWeeks; + if (shape.cadence.kind === "rolling") + return shape.cadence.rollingIntervalDays ?? 0; + return 0; +} + +/** + * The trailing detail clause appended to a cadence phrase (the weekday + * list or the day-of-month). Empty for one-shot doses and for cadences + * that carry no extra detail. + */ function cadenceDetailPhrase( - payload: WizardPayload, + shape: CadenceShape, t: (key: string, params?: Record) => string, ): string { - if (payload.mode === "oneShot") return ""; - switch (payload.cadence.kind) { + if (shape.mode === "oneShot") return ""; + switch (shape.cadence.kind) { case "weekdays": - return weekdaysDetail(payload.subControls.weekdays, t); + return weekdaysDetail(shape.subControls.weekdays, t); case "everyNWeeks": - return weekdaysDetail(payload.subControls.weekdays, t); + return weekdaysDetail(shape.subControls.weekdays, t); case "monthly": return t("medications.wizard.summary.dayOfMonthDetail", { - day: payload.subControls.dayOfMonth, + day: shape.subControls.dayOfMonth, }); default: return ""; } } +/** + * The full cadence line — phrase (`every N weeks`, `daily`, …) plus its + * detail clause. Both {@link summariseCadence} and + * {@link summariseScheduleDraft} build their first segment from this so + * the two summaries render byte-identical cadence text. + */ +function cadenceLineFor( + shape: CadenceShape, + t: (key: string, params?: Record) => string, +): string { + const cadenceKey = summaryKeyForDraft( + shape.mode, + shape.cadence, + shape.subControls, + ); + const cadencePhrase = t(`medications.wizard.summary.cadence.${cadenceKey}`, { + n: cadenceInterpolationN(shape), + }); + return cadencePhrase + cadenceDetailPhrase(shape, t); +} + /** * Plain-language summary for Step 8. Mirrors v1.5.3's helper shape so * the test surface stays familiar. The `t` callback is passed in so @@ -760,20 +802,7 @@ export function summariseCadence( payload: WizardPayload, t: (key: string, params?: Record) => string, ): string { - const cadenceKey = summaryKeyForCadence(payload); - const cadencePhrase = t( - `medications.wizard.summary.cadence.${cadenceKey}`, - { - n: - payload.cadence.kind === "everyNWeeks" - ? payload.subControls.intervalWeeks - : payload.cadence.kind === "rolling" - ? (payload.cadence.rollingIntervalDays ?? 0) - : 0, - }, - ); - const cadenceDetail = cadenceDetailPhrase(payload, t); - const cadenceLine = cadencePhrase + cadenceDetail; + const cadenceLine = cadenceLineFor(payload, t); const timesPhrase = payload.mode !== "oneShot" && payload.timesOfDay.length > 0 ? t("medications.wizard.summary.times", { @@ -807,36 +836,7 @@ export function summariseScheduleDraft( draft: ScheduleDraft, t: (key: string, params?: Record) => string, ): string { - const cadenceKey = summaryKeyForDraft( - draft.mode, - draft.cadence, - draft.subControls, - ); - const cadencePhrase = t( - `medications.wizard.summary.cadence.${cadenceKey}`, - { - n: - draft.cadence.kind === "everyNWeeks" - ? draft.subControls.intervalWeeks - : draft.cadence.kind === "rolling" - ? (draft.cadence.rollingIntervalDays ?? 0) - : 0, - }, - ); - let cadenceDetail = ""; - if (draft.mode !== "oneShot") { - if ( - draft.cadence.kind === "weekdays" || - draft.cadence.kind === "everyNWeeks" - ) { - cadenceDetail = weekdaysDetail(draft.subControls.weekdays, t); - } else if (draft.cadence.kind === "monthly") { - cadenceDetail = t("medications.wizard.summary.dayOfMonthDetail", { - day: draft.subControls.dayOfMonth, - }); - } - } - const cadenceLine = cadencePhrase + cadenceDetail; + const cadenceLine = cadenceLineFor(draft, t); const timesPhrase = draft.mode !== "oneShot" && draft.timesOfDay.length > 0 ? t("medications.wizard.summary.times", {