pelagia-portal/App/tests/unit/po-money.test.ts
Hardik 78afcb610b
Some checks failed
PR checks / checks (pull_request) Failing after 35s
PR checks / integration (pull_request) Successful in 33s
feat(po): TCS & Discount below GST (#133)
Adds two PO-level charges shown below GST, per issue #133 ask 2.

- Stored as ABSOLUTE rupee amounts on PurchaseOrder.tcsAmount / discountAmount
  (Decimal?, default 0; null/0 on historical & imported POs). Migration added.
- Discount is applied post-GST. totalAmount folds the charges in (net payable =
  subtotal + GST + TCS − Discount), so payments / reports / advance all use the
  true amount due. lib/po-money.ts is the single source of truth.
- Forms (create + edit) render a shared TcsDiscountFields with a % control
  bidirectionally linked to the rupee value (percentage is convenience only,
  taken against the GST-inclusive total; only the absolute amount is persisted).
- createPo / updatePo store & compute; both manager-edit actions PRESERVE the
  PO's TCS/Discount when recomputing the total; import leaves them at 0.
- PO detail shows TCS / Discount / Net payable below GST; PDF + XLSX export show
  the same breakdown and a corrected grand total.

Tests: lib/po-money unit tests; po-tcs-discount integration test (create / edit /
manager-line-edit preservation). Docs: CLAUDE.md GST section + wiki Purchase
Orders (TCS/Discount + a full "what import sets vs. not" field-mapping table).

Full unit (360) + integration (305) suites green; tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:50:34 +05:30

56 lines
1.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "vitest";
import {
computePoMoney,
poNetPayable,
amountToPercent,
percentToAmount,
} from "@/lib/po-money";
const items = [
{ quantity: 10, unitPrice: 100, gstRate: 0.18 }, // taxable 1000, gst 180
{ quantity: 2, unitPrice: 50, gstRate: 0.05 }, // taxable 100, gst 5
];
describe("computePoMoney", () => {
it("breaks down taxable, GST and incl-GST", () => {
const m = computePoMoney(items);
expect(m.taxable).toBe(1100);
expect(m.gst).toBeCloseTo(185, 5);
expect(m.inclGst).toBeCloseTo(1285, 5);
expect(m.netPayable).toBeCloseTo(1285, 5); // no charges ⇒ equals inclGst
});
it("adds TCS and subtracts Discount post-GST", () => {
const m = computePoMoney(items, 50, 85);
expect(m.tcs).toBe(50);
expect(m.discount).toBe(85);
expect(m.netPayable).toBeCloseTo(1285 + 50 - 85, 5);
});
it("defaults a missing gstRate to 18%", () => {
expect(computePoMoney([{ quantity: 1, unitPrice: 100 }]).gst).toBeCloseTo(18, 5);
});
it("treats nullish charges as zero", () => {
const m = computePoMoney(items, undefined, undefined);
expect(m.netPayable).toBeCloseTo(1285, 5);
});
});
describe("poNetPayable", () => {
it("is incl-GST + TCS Discount", () => {
expect(poNetPayable(items, 100, 200)).toBeCloseTo(1285 + 100 - 200, 5);
});
});
describe("percent ↔ amount conversion", () => {
it("round-trips against a base", () => {
expect(amountToPercent(128.5, 1285)).toBeCloseTo(10, 5);
expect(percentToAmount(10, 1285)).toBeCloseTo(128.5, 5);
});
it("is zero-safe when the base is zero", () => {
expect(amountToPercent(50, 0)).toBe(0);
expect(percentToAmount(10, 0)).toBe(0);
});
});