mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +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
+17
-18
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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