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:
commit
5a7145e0cb
4 changed files with 148 additions and 22 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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 } });
|
||||||
|
|
|
||||||
|
|
@ -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", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue