Merge pull request 'fix: Add items to inventory on PO approval, not on closure' (#20) from claude/issue-7 into master

Reviewed-on: https://git.pelagiamarine.com/shad0w/pelagia-portal/pulls/20
This commit is contained in:
shad0w 2026-06-18 21:53:20 +00:00
commit 5a7145e0cb
4 changed files with 148 additions and 22 deletions

View file

@ -8,7 +8,7 @@ import { revalidatePath } from "next/cache";
type ActionResult = { ok: true } | { error: string }; type ActionResult = { ok: true } | { error: string };
export async function approvepo({ export async function approvePo({
poId, poId,
note, note,
withNote = false, withNote = false,
@ -22,7 +22,7 @@ export async function approvepo({
const po = await db.purchaseOrder.findUnique({ const po = await db.purchaseOrder.findUnique({
where: { id: poId }, where: { id: poId },
include: { submitter: true }, include: { submitter: true, lineItems: true },
}); });
if (!po) return { error: "PO not found" }; if (!po) return { error: "PO not found" };
@ -51,6 +51,20 @@ export async function approvepo({
}, },
}); });
// Add line items to site inventory immediately on approval (not on closure)
const siteId = po.siteId ?? null;
if (siteId) {
for (const li of po.lineItems) {
if (!li.productId) continue;
await db.itemInventory.upsert({
where: { productId_siteId: { productId: li.productId, siteId } },
update: { quantity: { increment: Number(li.quantity) } },
create: { productId: li.productId, siteId, quantity: Number(li.quantity) },
});
}
revalidatePath(`/admin/sites/${siteId}`);
}
const accounts = await db.user.findMany({ where: { role: "ACCOUNTS", isActive: true } }); const accounts = await db.user.findMany({ where: { role: "ACCOUNTS", isActive: true } });
await notify({ await notify({
event: withNote ? "PO_APPROVED_WITH_NOTE" : "PO_APPROVED", event: withNote ? "PO_APPROVED_WITH_NOTE" : "PO_APPROVED",

View file

@ -2,7 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { approvepo, rejectPo, requestEdits, requestVendorId } from "./actions"; import { approvePo, rejectPo, requestEdits, requestVendorId } from "./actions";
import type { POStatus } from "@prisma/client"; import type { POStatus } from "@prisma/client";
export function ApprovalActions({ export function ApprovalActions({
@ -26,8 +26,8 @@ export function ApprovalActions({
setPending(action); setPending(action);
setError(""); setError("");
let result: { ok: true } | { error: string } | undefined; let result: { ok: true } | { error: string } | undefined;
if (action === "approve") result = await approvepo({ poId, note }); if (action === "approve") result = await approvePo({ poId, note });
else if (action === "approve_note") result = await approvepo({ poId, note, withNote: true }); else if (action === "approve_note") result = await approvePo({ poId, note, withNote: true });
else if (action === "reject") result = await rejectPo({ poId, note }); else if (action === "reject") result = await rejectPo({ poId, note });
else if (action === "request_edits") result = await requestEdits({ poId, note }); else if (action === "request_edits") result = await requestEdits({ poId, note });
else if (action === "request_vendor_id") result = await requestVendorId({ poId }); else if (action === "request_vendor_id") result = await requestVendorId({ poId });

View file

@ -131,23 +131,6 @@ export async function confirmReceipt({
}, },
}); });
// Auto-update inventory for delivered quantities
const siteId =
(po as typeof po & { siteId?: string | null }).siteId ??
null;
if (siteId) {
for (const u of lineUpdates) {
if (!u.productId || u.nowDelivered <= 0) continue;
await db.itemInventory.upsert({
where: { productId_siteId: { productId: u.productId, siteId } },
update: { quantity: { increment: u.nowDelivered } },
create: { productId: u.productId, siteId, quantity: u.nowDelivered },
});
}
revalidatePath(`/admin/sites/${siteId}`);
}
// Closing a PO auto-verifies its vendor (proof of a real, completed transaction). // Closing a PO auto-verifies its vendor (proof of a real, completed transaction).
if (newStatus === "CLOSED" && po.vendorId) { if (newStatus === "CLOSED" && po.vendorId) {
await db.vendor.update({ where: { id: po.vendorId }, data: { isVerified: true } }); await db.vendor.update({ where: { id: po.vendorId }, data: { isVerified: true } });

View file

@ -201,6 +201,135 @@ describe("S-06 — provide vendor ID", () => {
}); });
}); });
// ── Inventory update on approval ─────────────────────────────────────────────
describe("inventory — updated at MGR_APPROVED, not at closure", () => {
it("increments site inventory for line items with productId on approval", async () => {
const site = await db.site.findFirstOrThrow({ where: { code: "BOM" } });
const product = await db.product.findFirstOrThrow({ where: { code: "LUBE-EP80W90" } });
const before = await db.itemInventory.findUnique({
where: { productId_siteId: { productId: product.id, siteId: site.id } },
});
const qtyBefore = Number(before?.quantity ?? 0);
const po = await db.purchaseOrder.create({
data: {
poNumber: `INVTEST-${Date.now()}`,
title: `${PREFIX}InvApproval`,
status: "MGR_REVIEW",
totalAmount: 1000,
currency: "INR",
vesselId,
accountId,
vendorId,
siteId: site.id,
submitterId: techId,
submittedAt: new Date(),
lineItems: {
create: [{
name: "Gear Oil 80W90",
quantity: 5,
unit: "L",
unitPrice: 182,
totalPrice: 910,
gstRate: 0.18,
sortOrder: 0,
productId: product.id,
}],
},
actions: { create: { actionType: "SUBMITTED", actorId: techId } },
},
});
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
const result = await approvePo({ poId: po.id });
expect(result).toEqual({ ok: true });
const after = await db.itemInventory.findUnique({
where: { productId_siteId: { productId: product.id, siteId: site.id } },
});
expect(Number(after?.quantity)).toBe(qtyBefore + 5);
});
it("skips inventory update for line items without a productId", async () => {
const site = await db.site.findFirstOrThrow({ where: { code: "BOM" } });
const countBefore = await db.itemInventory.count({ where: { siteId: site.id } });
const po = await db.purchaseOrder.create({
data: {
poNumber: `INVTEST-NOPROD-${Date.now()}`,
title: `${PREFIX}InvNoProduct`,
status: "MGR_REVIEW",
totalAmount: 500,
currency: "INR",
vesselId,
accountId,
vendorId,
siteId: site.id,
submitterId: techId,
submittedAt: new Date(),
lineItems: {
create: [{
name: "Ad-hoc supply",
quantity: 2,
unit: "pc",
unitPrice: 100,
totalPrice: 200,
gstRate: 0.18,
sortOrder: 0,
productId: null,
}],
},
actions: { create: { actionType: "SUBMITTED", actorId: techId } },
},
});
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
await approvePo({ poId: po.id });
const countAfter = await db.itemInventory.count({ where: { siteId: site.id } });
expect(countAfter).toBe(countBefore);
});
it("does not touch inventory for vessel-only POs (no siteId)", async () => {
const totalBefore = await db.itemInventory.count();
const po = await db.purchaseOrder.create({
data: {
poNumber: `INVTEST-VESSEL-${Date.now()}`,
title: `${PREFIX}InvVessel`,
status: "MGR_REVIEW",
totalAmount: 300,
currency: "INR",
vesselId,
accountId,
vendorId,
submitterId: techId,
submittedAt: new Date(),
lineItems: {
create: [{
name: "Vessel supply",
quantity: 3,
unit: "pc",
unitPrice: 50,
totalPrice: 150,
gstRate: 0.18,
sortOrder: 0,
}],
},
actions: { create: { actionType: "SUBMITTED", actorId: techId } },
},
});
vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER"));
await approvePo({ poId: po.id });
const totalAfter = await db.itemInventory.count();
expect(totalAfter).toBe(totalBefore);
});
});
// ── S-07: Edit and resubmit ────────────────────────────────────────────────── // ── S-07: Edit and resubmit ──────────────────────────────────────────────────
describe("S-07 — edit and resubmit after edits requested", () => { describe("S-07 — edit and resubmit after edits requested", () => {