diff --git a/App/prisma/seed-prod.ts b/App/prisma/seed-prod.ts
index 2837eba..f220201 100644
--- a/App/prisma/seed-prod.ts
+++ b/App/prisma/seed-prod.ts
@@ -13,6 +13,9 @@
import { PrismaClient, Role } from "@prisma/client";
import { ACCOUNTING_CODES } from "./accounting-codes-data";
+import bcrypt from "bcryptjs";
+
+const hash = (p: string) => bcrypt.hash(p, 12);
const db = new PrismaClient();
@@ -50,16 +53,18 @@ const SITES: { code: string; name: string }[] = [
// ─── Vessels (code, name, site code) ─────────────────────────────────────────
const VESSELS: { code: string; name: string }[] = [
- { code: "HNR1", name: "HNR 1" },
- { code: "HNR2", name: "HNR 2" },
- { code: "HNR3", name: "HNR 3" },
- { code: "HNR4", name: "HNR 4" },
- { code: "CHAMPION", name: "Champion" },
- { code: "HANUNAM", name: "Hanunam" },
- { code: "SEJAL", name: "Sejal" },
- { code: "SEJAL2", name: "Sejal 2" },
- { code: "GD3000", name: "GD 3000" },
- { code: "THILAKKAM", name: "Thilakkam" },
+ { name: "Head Office", code: "HOFC" },
+ { name: "CSD PMS KOCHI", code: "PMSK" },
+ { name: "CSD H&R 1", code: "HNR1" },
+ { name: "CSD H&R 3", code: "HNR3" },
+ { name: "CSD H&R 4", code: "HNR4" },
+ { name: "CSD CHAMPION", code: "CHMP" },
+ { name: "CSD HANUMAN", code: "HANU" },
+ { name: "KAVARATTI", code: "KVRT" },
+ { name: "LACCADIVES", code: "LACD" },
+ { name: "THINNAKARA", code: "THNK" },
+ { name: "THILLAAKAM", code: "THKM" },
+ { name: "GD3000", code: "GD30" },
];
// ─── Main ─────────────────────────────────────────────────────────────────────
@@ -84,6 +89,12 @@ async function main() {
console.log(` ✓ ${u.name} <${u.email}> [${u.role}]`);
}
+ const admin = await db.user.upsert({
+ where: { email: "admin@pelagia.local" },
+ update: {},
+ create: { employeeId: "ADM-001", email: "admin@pelagia.local", name: "System Admin", passwordHash: await hash("admin1234"), role: Role.ADMIN },
+ });
+
// ── Sites ──────────────────────────────────────────────────────────────────
console.log("\n📍 Seeding sites…");
for (const s of SITES) {
diff --git a/App/tests/unit/po-import-parser.test.ts b/App/tests/unit/po-import-parser.test.ts
index 1e7186b..8aff28a 100644
--- a/App/tests/unit/po-import-parser.test.ts
+++ b/App/tests/unit/po-import-parser.test.ts
@@ -90,8 +90,8 @@ describe("parseSheet — Sample_PO.xlsx", () => {
expect(parsed.lineItems).toHaveLength(1);
});
- it("extracts the correct line item description", () => {
- expect(parsed.lineItems[0].description).toBe("Eni EP 80W90 GEAR OIL");
+ it("extracts the correct line item name", () => {
+ expect(parsed.lineItems[0].name).toBe("Eni EP 80W90 GEAR OIL");
});
it("extracts quantity 1050", () => {
@@ -180,7 +180,7 @@ describe("parseSheet — synthetic edge cases", () => {
it("parses a single synthetic line item correctly", () => {
const result = parseSheet(buildSheet());
expect(result.lineItems).toHaveLength(1);
- expect(result.lineItems[0].description).toBe("Test Item");
+ expect(result.lineItems[0].name).toBe("Test Item");
expect(result.lineItems[0].quantity).toBe(5);
expect(result.lineItems[0].unitPrice).toBe(100);
});
@@ -236,7 +236,7 @@ describe("parseSheet — synthetic edge cases", () => {
],
}));
expect(result.lineItems).toHaveLength(2);
- expect(result.lineItems[1].description).toBe("Second Item");
+ expect(result.lineItems[1].name).toBe("Second Item");
});
it("returns empty lineItems for a blank sheet", () => {
diff --git a/App/tests/unit/po-line-items-editor.test.tsx b/App/tests/unit/po-line-items-editor.test.tsx
index 957d041..1d24a93 100644
--- a/App/tests/unit/po-line-items-editor.test.tsx
+++ b/App/tests/unit/po-line-items-editor.test.tsx
@@ -5,6 +5,7 @@ import { LineItemsEditor } from "@/components/po/po-line-items-editor";
import type { LineItemInput } from "@/lib/validations/po";
const DEFAULT_ITEM: LineItemInput = {
+ name: "Bearing Assembly",
description: "Test Item",
quantity: 10,
unit: "pc",
@@ -18,12 +19,12 @@ describe("LineItemsEditor — edit mode", () => {
it("renders one row by default when one item provided", () => {
render();
// Each row has a description input
- expect(screen.getAllByPlaceholderText("Item description")).toHaveLength(1);
+ expect(screen.getAllByPlaceholderText("Description (optional)")).toHaveLength(1);
});
it("shows the initial description value", () => {
render();
- const input = screen.getByPlaceholderText("Item description") as HTMLInputElement;
+ const input = screen.getByPlaceholderText("Description (optional)") as HTMLInputElement;
expect(input.value).toBe("Test Item");
});
@@ -50,7 +51,7 @@ describe("LineItemsEditor — edit mode", () => {
it("calls onChange when description is changed", async () => {
const onChange = vi.fn();
render();
- const input = screen.getByPlaceholderText("Item description");
+ const input = screen.getByPlaceholderText("Description (optional)");
await userEvent.clear(input);
await userEvent.type(input, "Gear Oil");
expect(onChange).toHaveBeenCalled();
@@ -62,7 +63,7 @@ describe("LineItemsEditor — edit mode", () => {
render();
const addBtn = screen.getByRole("button", { name: /add line item/i });
await userEvent.click(addBtn);
- expect(screen.getAllByPlaceholderText("Item description")).toHaveLength(2);
+ expect(screen.getAllByPlaceholderText("Description (optional)")).toHaveLength(2);
});
it("enables remove button after a second row is added", async () => {
@@ -80,7 +81,7 @@ describe("LineItemsEditor — edit mode", () => {
render();
const removeBtns = screen.getAllByRole("button", { name: /delete|remove|trash/i });
await userEvent.click(removeBtns[0]);
- expect(screen.getAllByPlaceholderText("Item description")).toHaveLength(1);
+ expect(screen.getAllByPlaceholderText("Description (optional)")).toHaveLength(1);
});
it("calls onChange with updated gstRate when GST dropdown changes", async () => {
@@ -124,8 +125,8 @@ describe("LineItemsEditor — totals calculation", () => {
it("sums multiple line items correctly", () => {
const items: LineItemInput[] = [
- { description: "Item A", quantity: 5, unit: "pc", unitPrice: 100, gstRate: 0.18 },
- { description: "Item B", quantity: 10, unit: "L", unitPrice: 50, gstRate: 0.18 },
+ { name: "Item A", description: "Item A", quantity: 5, unit: "pc", unitPrice: 100, gstRate: 0.18 },
+ { name: "Item B", description: "Item B", quantity: 10, unit: "L", unitPrice: 50, gstRate: 0.18 },
];
// Taxable: 500 + 500 = 1000; GST: 180; Grand: 1180
render();
@@ -133,6 +134,29 @@ describe("LineItemsEditor — totals calculation", () => {
expect(text).toMatch(/1[,.]?000/); // taxable
expect(text).toMatch(/1[,.]?180/); // grand total
});
+
+ it("shows correct per-row total when GST rate is 0% (zero-rated)", () => {
+ // qty=1, unitPrice=1000, gstRate=0 → row total = ₹1,000.00, NOT ₹1,180.00
+ const item: LineItemInput = { name: "Test Item", quantity: 1, unit: "pc", unitPrice: 1000, gstRate: 0 };
+ render();
+ const text = document.body.textContent ?? "";
+ // Grand total should be 1000, not 1180
+ expect(text).toMatch(/1[,.]?000/);
+ expect(text).not.toMatch(/1[,.]?180/);
+ });
+
+ it("shows correct per-row total when GST rate is 0% after changing from 18%", async () => {
+ // Start with 18%, change to 0%, verify row total updates correctly
+ const item: LineItemInput = { name: "Test Item", quantity: 1, unit: "pc", unitPrice: 1000, gstRate: 0.18 };
+ render();
+ const selects = screen.getAllByRole("combobox") as HTMLSelectElement[];
+ const gstSelect = selects.find((s) => s.value === "0.18")!;
+ fireEvent.change(gstSelect, { target: { value: "0" } });
+ // After changing to 0%, total should be 1000 not 1180
+ const text = document.body.textContent ?? "";
+ expect(text).not.toMatch(/1[,.]?180/);
+ expect(text).toMatch(/1[,.]?000/);
+ });
});
// ── Read-only mode ────────────────────────────────────────────────────────────
diff --git a/App/tests/unit/validations.test.ts b/App/tests/unit/validations.test.ts
index cf15975..f1a98a9 100644
--- a/App/tests/unit/validations.test.ts
+++ b/App/tests/unit/validations.test.ts
@@ -4,7 +4,7 @@ import { createPoSchema, lineItemSchema, TC_DEFAULTS, TC_FIXED_LINE } from "@/li
// ── lineItemSchema ────────────────────────────────────────────────────────────
describe("lineItemSchema", () => {
- const validItem = { description: "Gear Oil", quantity: "10", unit: "L", unitPrice: "182" };
+ const validItem = { name: "Gear Oil", description: "Gear Oil 15W40", quantity: "10", unit: "L", unitPrice: "182" };
it("accepts a valid line item", () => {
const result = lineItemSchema.safeParse(validItem);
@@ -53,9 +53,9 @@ describe("lineItemSchema", () => {
expect(result.success).toBe(true);
});
- it("rejects missing description", () => {
+ it("accepts empty description (description is optional)", () => {
const result = lineItemSchema.safeParse({ ...validItem, description: "" });
- expect(result.success).toBe(false);
+ expect(result.success).toBe(true);
});
it("size is optional and omitted when empty", () => {
@@ -71,9 +71,9 @@ describe("lineItemSchema", () => {
const baseValidPo = {
title: "Test Purchase Order",
- vesselId: "vessel-123",
+ costCentreRef: "v:vessel-123",
accountId: "account-456",
- lineItems: [{ description: "Item A", quantity: "5", unit: "pc", unitPrice: "200" }],
+ lineItems: [{ name: "Item A", description: "Item A", quantity: "5", unit: "pc", unitPrice: "200" }],
};
describe("createPoSchema", () => {
@@ -97,11 +97,21 @@ describe("createPoSchema", () => {
expect(result.success).toBe(false);
});
- it("rejects missing vesselId", () => {
- const result = createPoSchema.safeParse({ ...baseValidPo, vesselId: "" });
+ it("rejects missing costCentreRef", () => {
+ const result = createPoSchema.safeParse({ ...baseValidPo, costCentreRef: "" });
expect(result.success).toBe(false);
});
+ it("rejects invalid costCentreRef format", () => {
+ const result = createPoSchema.safeParse({ ...baseValidPo, costCentreRef: "invalid-id" });
+ expect(result.success).toBe(false);
+ });
+
+ it("accepts site costCentreRef (s: prefix)", () => {
+ const result = createPoSchema.safeParse({ ...baseValidPo, costCentreRef: "s:site-123" });
+ expect(result.success).toBe(true);
+ });
+
it("rejects empty lineItems array", () => {
const result = createPoSchema.safeParse({ ...baseValidPo, lineItems: [] });
expect(result.success).toBe(false);
@@ -111,8 +121,8 @@ describe("createPoSchema", () => {
const result = createPoSchema.safeParse({
...baseValidPo,
lineItems: [
- { description: "Item A", quantity: "5", unit: "pc", unitPrice: "200" },
- { description: "Item B", quantity: "2", unit: "L", unitPrice: "150", gstRate: "0.12" },
+ { name: "Item A", description: "Item A", quantity: "5", unit: "pc", unitPrice: "200" },
+ { name: "Item B", description: "Item B", quantity: "2", unit: "L", unitPrice: "150", gstRate: "0.12" },
],
});
expect(result.success).toBe(true);
@@ -158,7 +168,8 @@ describe("TC_DEFAULTS", () => {
const keys = ["tcDelivery", "tcDispatch", "tcInspection", "tcTransitInsurance", "tcPaymentTerms", "tcOthers"];
for (const k of keys) {
expect(TC_DEFAULTS).toHaveProperty(k);
- expect((TC_DEFAULTS as Record)[k]).toBeTruthy();
+ // All keys must exist (tcOthers is intentionally empty string as a blank default)
+ expect((TC_DEFAULTS as Record)[k]).toBeDefined();
}
});