mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
fix: preserve legacy compliance response contract
This commit is contained in:
1 parent
41d47c3184
commit
24ff031f7a
9 files changed
+119
-44
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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() }),
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user