backend: org-scoped pharmacy inventory model

Add an `inventory` resource mirroring the prescriptions feature end-to-end:
Drizzle table (org-scoped, indexed), domain type, zod validation, service
(list/get/create/update/delete), and an RBAC-gated CRUD router mounted at
/api/inventory. Grant the new `inventory` statement to roles — pharmacy gets
read/write, full clinicians read/write/delete, reception/lab none — in both the
backend access control and (mirrored) the frontend. Record writes in the
activity log via a new `inventory` entity type. Includes the migration and an
idempotent demo seed script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-12 19:16:28 +03:00
parent 91e4f4ed30
commit 67fdf15de4
13 changed files with 3061 additions and 0 deletions
+1
View File
@@ -3,6 +3,7 @@ export * from "./patients.js";
export * from "./notes.js";
export * from "./appointments.js";
export * from "./prescriptions.js";
export * from "./inventory.js";
export * from "./tasks.js";
export * from "./activity.js";
export * from "./messaging.js";
+44
View File
@@ -0,0 +1,44 @@
import {
date,
index,
integer,
pgTable,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
import { organization, user } from "./auth.js";
// One row per medication held in a clinic's pharmacy stock, scoped to a clinic
// (organization). Unlike prescriptions (dispensed courses), this is the standing
// inventory the pharmacy searches to check availability. Availability
// (in-stock / low / out) is derived from `stockQuantity` vs `reorderThreshold`
// at read time, not stored.
export const inventory = pgTable(
"inventory",
{
id: uuid("id").primaryKey().defaultRandom(),
organizationId: text("organization_id")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
name: text("name").notNull(),
form: text("form").notNull().default(""),
strength: text("strength").notNull().default(""),
unit: text("unit").notNull().default(""),
stockQuantity: integer("stock_quantity").notNull().default(0),
reorderThreshold: integer("reorder_threshold").notNull().default(0),
location: text("location").notNull().default(""),
expiresAt: date("expires_at"),
notes: text("notes"),
createdBy: text("created_by").references(() => user.id, {
onDelete: "set null",
}),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
},
(t) => [index("inventory_org_idx").on(t.organizationId)],
);