From b43d44b59afa8a5c08f00d0040425521ee20d084 Mon Sep 17 00:00:00 2001 From: Hardik Date: Sat, 30 May 2026 18:50:23 +0530 Subject: [PATCH] fix(seed+tests): correct vessel/cost-centre names, update unit tests seed-prod.ts: - Correct vessel list: Head Office, PMS Kochi, CSD H&R 1/3/4, CSD Champion, CSD Hanuman, Kavaratti, Laccadives, Thinnakara, Thillaakam, GD3000 - Add System Admin user (admin@pelagia.local / admin1234) via bcrypt Unit tests: - po-import-parser: assert on line item .name rather than .description - po-line-items-editor: fix placeholder text assertions, add .name to LineItemInput fixtures, add two new GST 0% calculation tests - validations: add .name to line item fixtures; update createPoSchema assertions to reference costCentreRef; mark description as optional Co-Authored-By: Claude Sonnet 4.6 --- App/prisma/seed-prod.ts | 31 ++++++++++------ App/tests/unit/po-import-parser.test.ts | 8 ++--- App/tests/unit/po-line-items-editor.test.tsx | 38 ++++++++++++++++---- App/tests/unit/validations.test.ts | 31 ++++++++++------ 4 files changed, 77 insertions(+), 31 deletions(-) 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(); } });