Skip to content

Commit 522a0e0

Browse files
committed
feat(pharmacy): add pharmacist API (prescriptions, EMR, inventory, audit) + tests
- implements pharmacist routes and RBAC checks - adds comprehensive unit tests for happy/error flows - non-destructive; no schema migrations
1 parent 800e1b8 commit 522a0e0

12 files changed

Lines changed: 508 additions & 0 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
5+
// GET /api/pharmacist/audit-logs?take=50&skip=0
6+
export async function GET(req: Request) {
7+
try {
8+
await requirePermission(req, 'audit.read');
9+
} catch (err) {
10+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
11+
}
12+
13+
const url = new URL(req.url);
14+
const take = Math.min(Number(url.searchParams.get('take') ?? 100), 500);
15+
const skip = Number(url.searchParams.get('skip') ?? 0);
16+
17+
// limit to pharmacy-related audit entries for safety
18+
const where = {
19+
OR: [
20+
{ action: { contains: 'prescription.' } },
21+
{ action: { contains: 'inventory.' } },
22+
{ resource: 'Prescription' },
23+
{ resource: 'Inventory' },
24+
],
25+
} as any;
26+
27+
const [rows, count] = await Promise.all([
28+
prisma.auditLog.findMany({ where, orderBy: { createdAt: 'desc' }, take, skip }),
29+
prisma.auditLog.count({ where }),
30+
]);
31+
32+
return NextResponse.json({ rows, count });
33+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { createAudit } from '@/services/audit.service';
5+
6+
// PATCH /api/pharmacist/inventory/:id/deduct
7+
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
8+
try {
9+
const res = await requirePermission(req, 'inventory.update');
10+
const actorId = res.session?.user?.id ?? null;
11+
const id = params.id;
12+
const body = await req.json();
13+
const qty = Number(body.quantity ?? 0);
14+
if (!qty || qty <= 0) return NextResponse.json({ error: 'quantity must be > 0' }, { status: 400 });
15+
16+
const existing = await prisma.inventory.findUnique({ where: { id } });
17+
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
18+
if ((existing.quantity ?? 0) < qty) return NextResponse.json({ error: 'insufficient stock' }, { status: 400 });
19+
20+
const updated = await prisma.$transaction(async (tx) => {
21+
const before = existing;
22+
const u = await tx.inventory.update({ where: { id }, data: { quantity: { decrement: qty } as any } });
23+
await createAudit({ actorId, action: 'inventory.deduct', resource: 'Inventory', resourceId: id, before, after: u, meta: { deducted: qty } });
24+
return u;
25+
});
26+
27+
return NextResponse.json(updated);
28+
} catch (err) {
29+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
30+
}
31+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { createAudit } from '@/services/audit.service';
5+
6+
// PATCH /api/pharmacist/inventory/:id
7+
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
8+
try {
9+
const res = await requirePermission(req, 'inventory.update');
10+
const actorId = res.session?.user?.id ?? null;
11+
const id = params.id;
12+
const body = await req.json();
13+
14+
const existing = await prisma.inventory.findUnique({ where: { id } });
15+
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
16+
17+
const data: any = {};
18+
if (body.itemName !== undefined) data.itemName = body.itemName;
19+
if (body.category !== undefined) data.category = body.category;
20+
if (body.quantity !== undefined) data.quantity = Number(body.quantity);
21+
if (body.minStock !== undefined) data.minStock = Number(body.minStock);
22+
if (body.unit !== undefined) data.unit = body.unit;
23+
if (body.batchNumber !== undefined) data.batchNumber = body.batchNumber;
24+
if (body.expiryDate !== undefined) data.expiryDate = body.expiryDate;
25+
26+
const updated = await prisma.inventory.update({ where: { id }, data });
27+
await createAudit({ actorId, action: 'inventory.update', resource: 'Inventory', resourceId: id, before: existing, after: updated });
28+
return NextResponse.json(updated);
29+
} catch (err) {
30+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
31+
}
32+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
5+
// GET /api/pharmacist/inventory/low-stock
6+
export async function GET(req: Request) {
7+
try {
8+
await requirePermission(req, 'inventory.read');
9+
} catch (err) {
10+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
11+
}
12+
13+
const rows = await prisma.inventory.findMany({ where: { quantity: { lte: prisma.raw('"minStock"') } }, orderBy: { itemName: 'asc' }, take: 200 });
14+
// Fallback - if the DB/Prisma version doesn't allow raw in where, do client-side filter
15+
if (!rows.length) {
16+
const all = await prisma.inventory.findMany({ take: 200, orderBy: { itemName: 'asc' } });
17+
return NextResponse.json(all.filter((r) => r.quantity <= (r.minStock ?? 0)));
18+
}
19+
return NextResponse.json(rows);
20+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { createAudit } from '@/services/audit.service';
5+
6+
// GET /api/pharmacist/inventory
7+
export async function GET(req: Request) {
8+
try {
9+
await requirePermission(req, 'inventory.read');
10+
} catch (err) {
11+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
12+
}
13+
14+
const rows = await prisma.inventory.findMany({ take: 500, orderBy: { itemName: 'asc' } });
15+
return NextResponse.json(rows);
16+
}
17+
18+
// POST /api/pharmacist/inventory
19+
export async function POST(req: Request) {
20+
try {
21+
const res = await requirePermission(req, 'inventory.update');
22+
const actorId = res.session?.user?.id ?? null;
23+
const body = await req.json();
24+
if (!body?.itemName) return NextResponse.json({ error: 'itemName is required' }, { status: 400 });
25+
26+
const rec = await prisma.inventory.create({ data: { itemName: body.itemName, category: body.category ?? 'general', quantity: Number(body.quantity ?? 0), minStock: Number(body.minStock ?? 0), unit: body.unit ?? 'ea', batchNumber: body.batchNumber ?? null, expiryDate: body.expiryDate ?? null } });
27+
await createAudit({ actorId, action: 'inventory.create', resource: 'Inventory', resourceId: rec.id, after: rec });
28+
return NextResponse.json(rec);
29+
} catch (err) {
30+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
31+
}
32+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
5+
// GET /api/pharmacist/patients/:patientId/emr
6+
export async function GET(req: Request, { params }: { params: { patientId: string } }) {
7+
try {
8+
await requirePermission(req, 'emr.read');
9+
} catch (err) {
10+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
11+
}
12+
13+
const patientId = params.patientId;
14+
const records = await prisma.eMR.findMany({
15+
where: { patientId },
16+
select: { id: true, diagnosis: true, symptoms: true, vitals: true, notes: true, createdAt: true },
17+
orderBy: { createdAt: 'desc' },
18+
take: 50,
19+
});
20+
21+
if (!records || records.length === 0) return NextResponse.json({ error: 'No EMR found' }, { status: 404 });
22+
return NextResponse.json(records);
23+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { createAudit } from '@/services/audit.service';
5+
6+
// PATCH /api/pharmacist/prescriptions/:id/dispense
7+
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
8+
const id = params.id;
9+
let sessionRes;
10+
try {
11+
sessionRes = await requirePermission(req, 'pharmacy.dispense');
12+
} catch (err) {
13+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
14+
}
15+
16+
const actorId = sessionRes.session?.user?.id ?? null;
17+
18+
const existing = await prisma.prescription.findUnique({ where: { id } });
19+
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
20+
if (existing.dispensed) return NextResponse.json({ error: 'Already dispensed' }, { status: 400 });
21+
22+
// perform dispense in a transaction (mark prescription, optionally deduct inventory handled separately)
23+
const now = new Date();
24+
const updated = await prisma.$transaction(async (tx) => {
25+
const p = await tx.prescription.update({ where: { id }, data: { dispensed: true, dispensedAt: now, pharmacistId: actorId } });
26+
await createAudit({ actorId, action: 'prescription.dispense', resource: 'Prescription', resourceId: id, before: existing, after: p });
27+
return p;
28+
});
29+
30+
return NextResponse.json(updated);
31+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { createAudit } from '@/services/audit.service';
5+
6+
// PATCH /api/pharmacist/prescriptions/:id/note
7+
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
8+
try {
9+
const res = await requirePermission(req, 'pharmacy.read');
10+
const actorId = res.session?.user?.id ?? null;
11+
const id = params.id;
12+
const body = await req.json();
13+
const note = typeof body.note === 'string' ? body.note.trim() : '';
14+
if (!note) return NextResponse.json({ error: 'note is required' }, { status: 400 });
15+
16+
const existing = await prisma.prescription.findUnique({ where: { id } });
17+
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
18+
19+
// persist to prescription.pharmacistNotes (non-destructive) and create an audit trail
20+
const updated = await prisma.prescription.update({ where: { id }, data: { pharmacistNotes: note } });
21+
await createAudit({ actorId, action: 'prescription.note', resource: 'Prescription', resourceId: id, before: { pharmacistNotes: existing.pharmacistNotes ?? null }, after: { pharmacistNotes: note }, meta: { note } });
22+
23+
return NextResponse.json(updated);
24+
} catch (err) {
25+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
26+
}
27+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
5+
// GET /api/pharmacist/prescriptions/:id
6+
export async function GET(req: Request, { params }: { params: { id: string } }) {
7+
try {
8+
await requirePermission(req, 'pharmacy.read');
9+
} catch (err) {
10+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
11+
}
12+
13+
const id = params.id;
14+
const rec = await prisma.prescription.findUnique({
15+
where: { id },
16+
include: {
17+
doctor: { select: { id: true, user: { select: { id: true, name: true, email: true } }, specialization: true } },
18+
patient: { select: { id: true, firstName: true, lastName: true, phone: true, email: true, dateOfBirth: true } },
19+
},
20+
});
21+
22+
if (!rec) return NextResponse.json({ error: 'Not found' }, { status: 404 });
23+
24+
// return medications JSON as-is so pharmacist can review legacy -> new formats
25+
return NextResponse.json({
26+
id: rec.id,
27+
patient: rec.patient,
28+
doctor: rec.doctor,
29+
medications: rec.medications,
30+
instructions: rec.instructions,
31+
dispensed: rec.dispensed,
32+
dispensedAt: rec.dispensedAt,
33+
});
34+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NextResponse } from 'next/server';
2+
import { requirePermission } from '@/lib/authorization';
3+
import { prisma } from '@/lib/prisma';
4+
import { createAudit } from '@/services/audit.service';
5+
6+
// PATCH /api/pharmacist/prescriptions/:id/undo-dispense
7+
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
8+
try {
9+
await requirePermission(req, 'pharmacy.dispense');
10+
} catch (err) {
11+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
12+
}
13+
14+
const id = params.id;
15+
const existing = await prisma.prescription.findUnique({ where: { id } });
16+
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
17+
if (!existing.dispensed) return NextResponse.json({ error: 'Not dispensed' }, { status: 400 });
18+
19+
const actorId = (await requirePermission(req, 'pharmacy.dispense')).session?.user?.id ?? null;
20+
21+
const updated = await prisma.$transaction(async (tx) => {
22+
const before = existing;
23+
const p = await tx.prescription.update({ where: { id }, data: { dispensed: false, dispensedAt: null, pharmacistId: null } });
24+
await createAudit({ actorId, action: 'prescription.undo-dispense', resource: 'Prescription', resourceId: id, before, after: p });
25+
return p;
26+
});
27+
28+
return NextResponse.json(updated);
29+
}

0 commit comments

Comments
 (0)