pelagia-portal/App/lib/validations/po.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

94 lines
3.7 KiB
TypeScript

import { z } from "zod";
export const lineItemSchema = z.object({
name: z.string().min(1, "Item name is required"),
description: z.string().optional(),
quantity: z.coerce.number().positive("Quantity must be positive"),
unit: z.string().min(1, "Unit is required"),
size: z.string().optional(),
unitPrice: z.coerce.number().nonnegative("Unit price must be non-negative"),
gstRate: z.coerce.number().min(0).max(1).default(0.18),
productId: z.string().optional(),
accountId: z.string().optional(),
});
export const TC_FIXED_LINE =
"Please quote this purchase order no. for further communications and invoices pertaining to this indent.";
export const TC_FIXED_LINE_2 =
"We encourage bulk packaging and avoid plastic. No asbestos to be used in any product or packing material.";
export const TC_DEFAULTS = {
tcDelivery: "Within 4 to 5 days",
tcDispatch: "To be transported to site address as above. Freight Supplier's A/C",
tcInspection: "NA",
tcTransitInsurance: "NA",
tcPaymentTerms: "Within 30 days from delivery.",
tcOthers: "",
};
export const createPoSchema = z.object({
title: z.string().min(1, "Title is required").max(200),
vesselId: z.string().min(1, "Cost Centre is required"),
accountId: z.string().min(1, "Accounting Code is required"),
companyId: z.string().optional(),
poDate: z.string().optional(),
projectCode: z.string().optional(),
dateRequired: z.string().optional(),
vendorId: z.string().optional(),
currency: z.string().default("INR"),
piQuotationNo: z.string().optional(),
piQuotationDate: z.string().optional(),
requisitionNo: z.string().optional(),
requisitionDate: z.string().optional(),
placeOfDelivery: z.string().optional(),
tcDelivery: z.string().optional(),
tcDispatch: z.string().optional(),
tcInspection: z.string().optional(),
tcTransitInsurance: z.string().optional(),
tcPaymentTerms: z.string().optional(),
tcOthers: z.string().optional(),
// PO-level charges, stored absolute (issue #133). Discount is applied post-GST.
tcsAmount: z.coerce.number().nonnegative("TCS cannot be negative").default(0),
discountAmount: z.coerce.number().nonnegative("Discount cannot be negative").default(0),
lineItems: z.array(lineItemSchema).min(1, "At least one line item is required"),
});
export const approvePoSchema = z.object({
note: z.string().optional(),
// Absolute advance amount the Manager wants paid first (issue #92). The UI
// slider works in whole percent of totalAmount; the resolved amount is what we
// persist. Validated against the PO total in the action. Omitted ⇒ full payment.
suggestedAdvancePayment: z.coerce
.number()
.nonnegative("Advance payment cannot be negative")
.optional(),
});
export const rejectPoSchema = z.object({
note: z.string().min(1, "A rejection reason is required"),
});
export const requestEditsSchema = z.object({
note: z.string().min(1, "Please specify what edits are needed"),
});
export const processPaymentSchema = z.object({
paymentRef: z.string().min(1, "Payment reference is required"),
paymentAmount: z.number().positive("Payment amount must be greater than 0").optional(),
paymentDate: z.coerce
.date({ required_error: "Payment date is required", invalid_type_error: "Payment date is required" })
.refine((d) => {
// Not in the future — compare against end of today (local)
const endOfToday = new Date();
endOfToday.setHours(23, 59, 59, 999);
return d.getTime() <= endOfToday.getTime();
}, "Payment date cannot be in the future"),
});
export const confirmReceiptSchema = z.object({
notes: z.string().optional(),
});
export type CreatePoInput = z.infer<typeof createPoSchema>;
export type LineItemInput = z.infer<typeof lineItemSchema>;