Files
Voiced/src/lib/validators.ts
T
xavierk a00c816635 Add issue/cancel lifecycle, GST derivation and integer-paise totals
issue_invoice allocates the number, freezes vendor and bank details and
stores server-computed totals in one transaction. Issued invoices are
cancelled, not deleted. Tax heads derive from supplier state and place of
supply (a disagreeing choice is rejected); unregistered suppliers issue a
plain Invoice. Adds GSTIN checksum validation, canonical India Compliance
state list, server-side drafts, series validation, relative and
magic-byte-checked asset paths, and GST settings. PDF re-exports use the
frozen vendor snapshot.
2026-10-04 04:15:58 +05:30

87 lines
3.3 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.
const PAN_RE = /^[A-Z]{5}[0-9]{4}[A-Z]$/;
const IFSC_RE = /^[A-Z]{4}0[A-Z0-9]{6}$/;
const GSTIN_RE = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/;
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const PHONE_RE = /^[0-9+\-()\s]{6,16}$/;
const norm = (v: string) => v.trim().toUpperCase();
export function panError(value: string): string | undefined {
if (!value.trim()) return undefined;
return PAN_RE.test(norm(value)) ? undefined : "Enter a valid PAN, e.g. CPOPP7388K";
}
export function ifscError(value: string): string | undefined {
if (!value.trim()) return undefined;
return IFSC_RE.test(norm(value)) ? undefined : "Enter a valid IFSC, e.g. SBIN0017060";
}
export function gstinError(value: string): string | undefined {
const v = norm(value);
if (!v || v === "NA") return undefined;
return GSTIN_RE.test(v) ? undefined : "Enter a valid 15-character GSTIN";
}
const GSTIN_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
/** Standard GSTIN mod-36 check digit. Returns an error message, or undefined when valid. */
export function gstinChecksumError(value: string): string | undefined {
const v = norm(value);
if (!GSTIN_RE.test(v)) return "Enter a valid 15-character GSTIN";
let sum = 0;
for (let i = 0; i < 14; i++) {
const product = GSTIN_CHARS.indexOf(v[i]) * (i % 2 === 0 ? 1 : 2);
sum += Math.floor(product / 36) + (product % 36);
}
const expected = GSTIN_CHARS[(36 - (sum % 36)) % 36];
return v[14] === expected ? undefined : `GSTIN check digit looks wrong (expected ${expected})`;
}
/**
* Full GSTIN check: shape, check digit and, when a PAN is given, that the GSTIN
* contains that PAN. Empty (or "NA") is valid; the field is optional.
*/
export function gstinFullError(value: string, pan = ""): string | undefined {
const v = norm(value);
if (!v || v === "NA") return undefined;
const base = gstinError(v) ?? gstinChecksumError(v);
if (base) return base;
if (pan.trim() && v.slice(2, 12) !== norm(pan)) return "GSTIN does not contain the PAN";
return undefined;
}
/** Supplier GSTIN rules for a registered business; an unregistered one needs none. */
export function vendorGstinError(
registration: string,
gstin: string,
pan: string,
stateCode: string,
): string | undefined {
if (registration === "unregistered") return undefined;
const g = norm(gstin);
if (!g) return "A registered business needs a GSTIN";
const base = gstinFullError(g, pan);
if (base) return base;
if (g.slice(0, 2) !== stateCode) return `GSTIN starts with ${g.slice(0, 2)}, but your state code is ${stateCode}`;
return undefined;
}
export function emailError(value: string): string | undefined {
if (!value.trim()) return undefined;
return EMAIL_RE.test(value.trim()) ? undefined : "Enter a valid email address";
}
export function phoneError(value: string): string | undefined {
if (!value.trim()) return undefined;
return PHONE_RE.test(value.trim()) ? undefined : "Enter a valid phone number";
}
export function accountError(value: string): string | undefined {
if (!value.trim()) return undefined;
return /^[0-9]{6,20}$/.test(value.trim()) ? undefined : "Account number should be 6–20 digits";
}
export const isPan = (v: string) => PAN_RE.test(norm(v));
export const isIfsc = (v: string) => IFSC_RE.test(norm(v));
export const isGstin = (v: string) => GSTIN_RE.test(norm(v));