Add an optional PO Date field to the create and edit PO forms. Submitters can pick any date (back-dated or forward-dated). If left blank, the exported PO document falls back to the approved date, then to the creation date. Changes: - Prisma schema: add `poDate DateTime?` to PurchaseOrder - Migration 20260616000000_add_po_date: ALTER TABLE to add the column - createPoSchema: add optional `poDate` string field - new-po-form, edit-po-form: add PO Date picker in Order Information - create/edit actions: persist poDate to DB - edit action resubmit snapshot: track poDate changes for manager diff - po-detail: show PO Date in Order Details; include in resubmit diff banner - export route: use poDate ?? approvedAt ?? createdAt as the date on the exported PDF/XLSX document - validations.test: fix pre-existing costCentreRef→vesselId mismatch and add poDate test cases Fixes #4 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
194 lines
7.2 KiB
TypeScript
194 lines
7.2 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { createPoSchema, lineItemSchema, TC_DEFAULTS, TC_FIXED_LINE } from "@/lib/validations/po";
|
|
|
|
// ── lineItemSchema ────────────────────────────────────────────────────────────
|
|
|
|
describe("lineItemSchema", () => {
|
|
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);
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("defaults gstRate to 0.18 when omitted", () => {
|
|
const result = lineItemSchema.safeParse(validItem);
|
|
expect(result.success && result.data.gstRate).toBeCloseTo(0.18);
|
|
});
|
|
|
|
it("accepts gstRate of 0 (zero-rated supply)", () => {
|
|
const result = lineItemSchema.safeParse({ ...validItem, gstRate: "0" });
|
|
expect(result.success && result.data.gstRate).toBe(0);
|
|
});
|
|
|
|
it("accepts all valid GST rates: 0, 0.05, 0.12, 0.18, 0.28", () => {
|
|
for (const rate of [0, 0.05, 0.12, 0.18, 0.28]) {
|
|
const r = lineItemSchema.safeParse({ ...validItem, gstRate: String(rate) });
|
|
expect(r.success).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("rejects gstRate > 1", () => {
|
|
const result = lineItemSchema.safeParse({ ...validItem, gstRate: "1.5" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects gstRate < 0", () => {
|
|
const result = lineItemSchema.safeParse({ ...validItem, gstRate: "-0.1" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects zero or negative quantity", () => {
|
|
expect(lineItemSchema.safeParse({ ...validItem, quantity: "0" }).success).toBe(false);
|
|
expect(lineItemSchema.safeParse({ ...validItem, quantity: "-1" }).success).toBe(false);
|
|
});
|
|
|
|
it("rejects negative unit price", () => {
|
|
const result = lineItemSchema.safeParse({ ...validItem, unitPrice: "-10" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("allows zero unit price (donation/free item)", () => {
|
|
const result = lineItemSchema.safeParse({ ...validItem, unitPrice: "0" });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("accepts empty description (description is optional)", () => {
|
|
const result = lineItemSchema.safeParse({ ...validItem, description: "" });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("size is optional and omitted when empty", () => {
|
|
const withSize = lineItemSchema.safeParse({ ...validItem, size: "10mm" });
|
|
expect(withSize.success && withSize.data.size).toBe("10mm");
|
|
|
|
const noSize = lineItemSchema.safeParse(validItem);
|
|
expect(noSize.success && noSize.data.size).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ── createPoSchema ────────────────────────────────────────────────────────────
|
|
|
|
const baseValidPo = {
|
|
title: "Test Purchase Order",
|
|
vesselId: "vessel-123",
|
|
accountId: "account-456",
|
|
lineItems: [{ name: "Item A", description: "Item A", quantity: "5", unit: "pc", unitPrice: "200" }],
|
|
};
|
|
|
|
describe("createPoSchema", () => {
|
|
it("accepts a minimal valid PO", () => {
|
|
const result = createPoSchema.safeParse(baseValidPo);
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("defaults currency to INR", () => {
|
|
const result = createPoSchema.safeParse(baseValidPo);
|
|
expect(result.success && result.data.currency).toBe("INR");
|
|
});
|
|
|
|
it("rejects missing title", () => {
|
|
const result = createPoSchema.safeParse({ ...baseValidPo, title: "" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects title longer than 200 characters", () => {
|
|
const result = createPoSchema.safeParse({ ...baseValidPo, title: "x".repeat(201) });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects missing vesselId", () => {
|
|
const result = createPoSchema.safeParse({ ...baseValidPo, vesselId: "" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects empty lineItems array", () => {
|
|
const result = createPoSchema.safeParse({ ...baseValidPo, lineItems: [] });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("accepts multiple line items", () => {
|
|
const result = createPoSchema.safeParse({
|
|
...baseValidPo,
|
|
lineItems: [
|
|
{ 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);
|
|
});
|
|
|
|
it("accepts all TC structured fields as optional", () => {
|
|
const result = createPoSchema.safeParse({
|
|
...baseValidPo,
|
|
tcDelivery: "Within 3 days",
|
|
tcDispatch: "Freight on supplier",
|
|
tcInspection: "Required",
|
|
tcTransitInsurance: "Supplier's account",
|
|
tcPaymentTerms: "Net 30",
|
|
tcOthers: "No asbestos",
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.success && result.data.tcDelivery).toBe("Within 3 days");
|
|
expect(result.success && result.data.tcPaymentTerms).toBe("Net 30");
|
|
});
|
|
|
|
it("accepts PI quotation and requisition fields", () => {
|
|
const result = createPoSchema.safeParse({
|
|
...baseValidPo,
|
|
piQuotationNo: "Verbal",
|
|
requisitionNo: "REQN-2026-001",
|
|
placeOfDelivery: "Navi Mumbai",
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("accepts poDate as an optional date string", () => {
|
|
const result = createPoSchema.safeParse({ ...baseValidPo, poDate: "2026-01-15" });
|
|
expect(result.success).toBe(true);
|
|
expect(result.success && result.data.poDate).toBe("2026-01-15");
|
|
});
|
|
|
|
it("accepts back-dated poDate", () => {
|
|
const result = createPoSchema.safeParse({ ...baseValidPo, poDate: "2025-06-01" });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("accepts forward-dated poDate", () => {
|
|
const result = createPoSchema.safeParse({ ...baseValidPo, poDate: "2027-12-31" });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("leaves poDate undefined when omitted", () => {
|
|
const result = createPoSchema.safeParse(baseValidPo);
|
|
expect(result.success && result.data.poDate).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
|
|
describe("TC_FIXED_LINE", () => {
|
|
it("references purchase order number for communications", () => {
|
|
expect(TC_FIXED_LINE).toMatch(/purchase order/i);
|
|
expect(TC_FIXED_LINE).toMatch(/communications/i);
|
|
});
|
|
});
|
|
|
|
describe("TC_DEFAULTS", () => {
|
|
it("has all 6 required keys", () => {
|
|
const keys = ["tcDelivery", "tcDispatch", "tcInspection", "tcTransitInsurance", "tcPaymentTerms", "tcOthers"];
|
|
for (const k of keys) {
|
|
expect(TC_DEFAULTS).toHaveProperty(k);
|
|
// All keys must exist (tcOthers is intentionally empty string as a blank default)
|
|
expect((TC_DEFAULTS as Record<string, string>)[k]).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("default delivery mentions days", () => {
|
|
expect(TC_DEFAULTS.tcDelivery).toMatch(/day/i);
|
|
});
|
|
|
|
it("default payment terms mentions days", () => {
|
|
expect(TC_DEFAULTS.tcPaymentTerms).toMatch(/day/i);
|
|
});
|
|
});
|