backend: prescription start/end dates + dispense ledger

Add optional startDate/endDate columns to prescriptions (schema,
validation, types, service) — when set, endDate drives expiry.

Add a new append-only `dispenses` resource (schema, types, validation,
service, route at /api/dispenses) recording who received which medication,
gated on the existing inventory RBAC statement. Migration 0020.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-16 20:44:47 +03:00
parent 15fc7fdf26
commit f0fe501d62
15 changed files with 3749 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
import { desc, eq } from "drizzle-orm";
import { db } from "../db/index.js";
import { dispenses } from "../db/schema/dispenses.js";
import type { DispenseInput } from "../lib/dispense-validation.js";
import type { Dispense } from "../types/dispense.js";
type DispenseRow = typeof dispenses.$inferSelect;
function toDispense(row: DispenseRow): Dispense {
return {
id: row.id,
fileNumber: row.patientFileNumber,
name: row.patientName,
initials: row.patientInitials,
medication: row.medication,
dose: row.dose,
quantity: row.quantity,
unit: row.unit,
prescriptionId: row.prescriptionId,
dispensedBy: row.dispensedBy,
dispensedByName: row.dispensedByName,
dispensedAt: row.dispensedAt.toISOString(),
notes: row.notes,
createdAt: row.createdAt.toISOString(),
};
}
export async function listDispenses(orgId: string): Promise<Dispense[]> {
const rows = await db
.select()
.from(dispenses)
.where(eq(dispenses.organizationId, orgId))
.orderBy(desc(dispenses.dispensedAt));
return rows.map(toDispense);
}
export async function createDispense(
orgId: string,
dispenser: { id: string; name: string },
input: DispenseInput,
): Promise<Dispense> {
const [row] = await db
.insert(dispenses)
.values({
organizationId: orgId,
patientFileNumber: input.fileNumber,
patientName: input.name,
patientInitials: input.initials,
medication: input.medication,
dose: input.dose,
quantity: input.quantity,
unit: input.unit,
prescriptionId: input.prescriptionId ?? null,
dispensedBy: dispenser.id,
dispensedByName: dispenser.name,
notes: input.notes ?? null,
})
.returning();
return toDispense(row!);
}
+4
View File
@@ -22,6 +22,8 @@ function toPrescription(row: PrescriptionRow): Prescription {
frequency: row.frequency,
prescriber: row.prescriber,
prescribedAt: row.prescribedAt,
startDate: row.startDate,
endDate: row.endDate,
status: row.status,
duration: row.duration,
notes: row.notes,
@@ -45,6 +47,8 @@ function columns(orgId: string, input: PrescriptionInput, createdBy?: string) {
duration: input.duration ?? null,
notes: input.notes ?? null,
source: input.source,
startDate: input.startDate ?? null,
endDate: input.endDate ?? null,
// Only set prescribedAt when supplied; otherwise the column default (today).
...(input.prescribedAt ? { prescribedAt: input.prescribedAt } : {}),
...(createdBy ? { createdBy } : {}),