fix: preserve legacy compliance response contract

This commit is contained in:
muhdusama committed 2026-09-21 07:17:13 +10:00
1 parent 41d47c3184
commit 24ff031f7a
9 files changed
+119 -44

No files matched your search

+17 -18
View File
@@ -5828,8 +5828,9 @@ paths:
- Medications
summary: Batched adherence read for every medication of the caller
description: Returns one compact row per non-PRN medication. A scheduled medication with no local schedule remains in
the array with applicable=false, reason NO_LOCAL_SCHEDULE, and null percentage/display fields so card and table
clients do not mistake it for 100 percent or leave it loading.
the array with applicable=false and reason NO_LOCAL_SCHEDULE. compliance7/compliance30 stay non-null as all-zero
compatibility placeholders for released clients; aware clients must ignore those percentages when
applicable=false. complianceDisplay is null.
responses:
"200":
description: One adherence row per medication.
@@ -5847,8 +5848,10 @@ paths:
- Medications
summary: Adherence read for a medication
description: Returns the cadence-aware adherence payload. A scheduled medication with no local schedule returns
applicable=false, reason NO_LOCAL_SCHEDULE, null percentage/display fields, and an empty daily grid. Medications
with a local schedule preserve the existing arithmetic, including windows in which no dose is due.
applicable=false, reason NO_LOCAL_SCHEDULE, non-null all-zero compliance7/compliance30 compatibility
placeholders, a null display block, and an empty daily grid. Aware clients must not render the placeholder
percentages. Medications with a local schedule preserve the existing arithmetic, including windows in which no
dose is due.
parameters:
- in: path
name: id
@@ -42941,13 +42944,11 @@ components:
const: NO_LOCAL_SCHEDULE
- type: "null"
compliance7:
anyOf:
- $ref: "#/components/schemas/ComplianceResult"
- type: "null"
description: Seven-day adherence summary, or an all-zero compatibility placeholder when applicable is false.
$ref: "#/components/schemas/ComplianceResult"
compliance30:
anyOf:
- $ref: "#/components/schemas/ComplianceResult"
- type: "null"
description: Thirty-day adherence summary, or an all-zero compatibility placeholder when applicable is false.
$ref: "#/components/schemas/ComplianceResult"
complianceDisplay:
anyOf:
- $ref: "#/components/schemas/ComplianceDisplay"
@@ -43203,15 +43204,13 @@ components:
description: Reason adherence is not applicable. NO_LOCAL_SCHEDULE means the medication has no HealthLog-owned
expected-dose grid.
compliance7:
anyOf:
- $ref: "#/components/schemas/ComplianceResult"
- type: "null"
description: Seven-day adherence summary, or null when applicable is false.
description: Seven-day adherence summary. When applicable is false, this remains a non-null all-zero compatibility
placeholder for released clients and must not be rendered as a percentage.
$ref: "#/components/schemas/ComplianceResult"
compliance30:
anyOf:
- $ref: "#/components/schemas/ComplianceResult"
- type: "null"
description: Thirty-day adherence summary, or null when applicable is false.
description: Thirty-day adherence summary. When applicable is false, this remains a non-null all-zero compatibility
placeholder for released clients and must not be rendered as a percentage.
$ref: "#/components/schemas/ComplianceResult"
dailyCompliance:
type: object
propertyNames:
+2
View File
@@ -1208,6 +1208,8 @@
"veryOverdue": "Mocno po terminie",
"complianceWindow": "Przestrzeganie {days} dni",
"complianceUnavailable": "Nie udało się wczytać danych o przestrzeganiu",
"complianceNotApplicable": "Przestrzeganie zaleceń nie ma zastosowania",
"complianceNotApplicableReason": "W HealthLog nie zapisano lokalnego harmonogramu.",
"complianceInfo": "Udział zarejestrowanych dawek względem oczekiwanych. Plany tygodniowe liczone są tydzień po tygodniu.",
"complianceInfoLabel": "Co oznacza przestrzeganie zaleceń?",
"lastIntake": "Ostatnie przyjęcie:",
@@ -175,8 +175,22 @@ describe("GET /api/medications/[id]/compliance - applicability", () => {
expect(data).toEqual({
applicable: false,
notApplicableReason: "NO_LOCAL_SCHEDULE",
compliance7: null,
compliance30: null,
compliance7: {
totalExpected: 0,
taken: 0,
skipped: 0,
missed: 0,
rate: 0,
streak: 0,
},
compliance30: {
totalExpected: 0,
taken: 0,
skipped: 0,
missed: 0,
rate: 0,
streak: 0,
},
dailyCompliance: {},
complianceDisplay: null,
});
@@ -59,6 +59,15 @@ import { checkRateLimit } from "@/lib/rate-limit";
const TZ = "UTC";
const LEGACY_EMPTY_COMPLIANCE = {
totalExpected: 0,
taken: 0,
skipped: 0,
missed: 0,
rate: 0,
streak: 0,
};
const SESSION_OK = {
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
user: {
@@ -165,14 +174,46 @@ describe("GET /api/medications/compliance", () => {
medicationId: "mirror-1",
applicable: false,
notApplicableReason: "NO_LOCAL_SCHEDULE",
compliance7: null,
compliance30: null,
compliance7: LEGACY_EMPTY_COMPLIANCE,
compliance30: LEGACY_EMPTY_COMPLIANCE,
complianceDisplay: null,
},
]);
expect(prisma.medicationIntakeEvent.findMany).not.toHaveBeenCalled();
});
it("keeps a mixed scheduled/mirror batch decodable by the legacy non-null contract", async () => {
vi.mocked(prisma.medication.findMany).mockResolvedValue([
medication("med-1"),
{
...medication("mirror-1"),
externalSource: "APPLE_HEALTH",
externalId: "hk-concept-1",
schedules: [],
},
] as never);
const res = await (GET as (req: Request) => Promise<Response>)(
new Request("http://localhost"),
);
expect(res.status).toBe(200);
const body = await res.json();
const items = body.data as Array<Record<string, unknown>>;
expect(items).toHaveLength(2);
expect(items[0]?.applicable).toBe(true);
expect(items[0]?.compliance7).toBeTruthy();
expect(items[0]?.compliance30).toBeTruthy();
expect(items[1]).toMatchObject({
medicationId: "mirror-1",
applicable: false,
notApplicableReason: "NO_LOCAL_SCHEDULE",
compliance7: LEGACY_EMPTY_COMPLIANCE,
compliance30: LEGACY_EMPTY_COMPLIANCE,
complianceDisplay: null,
});
});
it("reads each medication through the shared per-medication cache cell", async () => {
vi.mocked(prisma.medication.findMany).mockResolvedValue([
medication("med-1"),
@@ -382,8 +382,8 @@ export function MedicationDetailTabs({
return await apiGet<{
applicable: boolean;
notApplicableReason: "NO_LOCAL_SCHEDULE" | null;
compliance7: { rate: number; streak: number } | null;
compliance30: { rate: number } | null;
compliance7: { rate: number; streak: number };
compliance30: { rate: number };
complianceDisplay: ComplianceDisplay | null;
}>(`/api/medications/${id}/compliance`);
} catch {
+23 -4
View File
@@ -41,12 +41,31 @@ export interface CompliancePayload {
*/
applicable: boolean;
notApplicableReason: "NO_LOCAL_SCHEDULE" | null;
compliance7: ComplianceResult | null;
compliance30: ComplianceResult | null;
compliance7: ComplianceResult;
compliance30: ComplianceResult;
dailyCompliance: Record<string, DailyComplianceEntry>;
complianceDisplay: ComplianceDisplay | null;
}
/**
* Legacy-compatible placeholder for a medication whose adherence percentage
* is explicitly not applicable.
*
* Released iOS 1.0.3 requires `compliance7` and `compliance30` to decode as
* non-null `ComplianceWindowResult` objects. Keep those fields structurally
* present while `applicable=false` remains the authoritative semantic signal
* for clients that understand the newer contract. All-zero values avoid
* reviving the old vacuous 100% result.
*/
const NOT_APPLICABLE_LEGACY_COMPLIANCE: ComplianceResult = {
totalExpected: 0,
taken: 0,
skipped: 0,
missed: 0,
rate: 0,
streak: 0,
};
/** The medication slice the payload builder consumes. */
export interface ComplianceMedicationInput {
id: string;
@@ -155,8 +174,8 @@ export async function buildCompliancePayload(
return {
applicable: false,
notApplicableReason: "NO_LOCAL_SCHEDULE",
compliance7: null,
compliance30: null,
compliance7: NOT_APPLICABLE_LEGACY_COMPLIANCE,
compliance30: NOT_APPLICABLE_LEGACY_COMPLIANCE,
dailyCompliance: {},
complianceDisplay: null,
};
+2 -2
View File
@@ -1413,7 +1413,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Medications"],
summary: "Batched adherence read for every medication of the caller",
description:
"Returns one compact row per non-PRN medication. A scheduled medication with no local schedule remains in the array with applicable=false, reason NO_LOCAL_SCHEDULE, and null percentage/display fields so card and table clients do not mistake it for 100 percent or leave it loading.",
"Returns one compact row per non-PRN medication. A scheduled medication with no local schedule remains in the array with applicable=false and reason NO_LOCAL_SCHEDULE. compliance7/compliance30 stay non-null as all-zero compatibility placeholders for released clients; aware clients must ignore those percentages when applicable=false. complianceDisplay is null.",
responses: {
...recordRefusal(),
"200": {
@@ -1436,7 +1436,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Medications"],
summary: "Adherence read for a medication",
description:
"Returns the cadence-aware adherence payload. A scheduled medication with no local schedule returns applicable=false, reason NO_LOCAL_SCHEDULE, null percentage/display fields, and an empty daily grid. Medications with a local schedule preserve the existing arithmetic, including windows in which no dose is due.",
"Returns the cadence-aware adherence payload. A scheduled medication with no local schedule returns applicable=false, reason NO_LOCAL_SCHEDULE, non-null all-zero compliance7/compliance30 compatibility placeholders, a null display block, and an empty daily grid. Aware clients must not render the placeholder percentages. Medications with a local schedule preserve the existing arithmetic, including windows in which no dose is due.",
requestParams: {
path: z.object({ id: z.string() }),
},
+12 -12
View File
@@ -766,16 +766,12 @@ export const medicationComplianceResponse = z
.describe(
"Reason adherence is not applicable. NO_LOCAL_SCHEDULE means the medication has no HealthLog-owned expected-dose grid.",
),
compliance7: complianceResult
.nullable()
.describe(
"Seven-day adherence summary, or null when applicable is false.",
),
compliance30: complianceResult
.nullable()
.describe(
"Thirty-day adherence summary, or null when applicable is false.",
),
compliance7: complianceResult.describe(
"Seven-day adherence summary. When applicable is false, this remains a non-null all-zero compatibility placeholder for released clients and must not be rendered as a percentage.",
),
compliance30: complianceResult.describe(
"Thirty-day adherence summary. When applicable is false, this remains a non-null all-zero compatibility placeholder for released clients and must not be rendered as a percentage.",
),
dailyCompliance: z
.record(z.string(), dailyComplianceEntry)
.describe(
@@ -802,8 +798,12 @@ export const medicationComplianceSummaryEntry = z
"Whether this medication has a local expected-dose grid and therefore a displayable adherence percentage.",
),
notApplicableReason: z.literal("NO_LOCAL_SCHEDULE").nullable(),
compliance7: complianceResult.nullable(),
compliance30: complianceResult.nullable(),
compliance7: complianceResult.describe(
"Seven-day adherence summary, or an all-zero compatibility placeholder when applicable is false.",
),
compliance30: complianceResult.describe(
"Thirty-day adherence summary, or an all-zero compatibility placeholder when applicable is false.",
),
complianceDisplay: complianceDisplay.nullable(),
})
.meta({
@@ -17,8 +17,8 @@ export interface MedicationComplianceSummaryEntry {
medicationId: string;
applicable: boolean;
notApplicableReason: "NO_LOCAL_SCHEDULE" | null;
compliance7: { rate: number; streak: number } | null;
compliance30: { rate: number } | null;
compliance7: { rate: number; streak: number };
compliance30: { rate: number };
complianceDisplay: ComplianceDisplay | null;
}