You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
61 lines
2.5 KiB
TypeScript
61 lines
2.5 KiB
TypeScript
const GSTIN_SHAPE = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/
|
|
const B36 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
|
|
/** Standard GSTIN check digit: alternating 1/2 factors over base-36 values. */
|
|
export function gstinCheckDigit(first14: string): string {
|
|
let sum = 0
|
|
for (let i = 0; i < 14; i++) {
|
|
const v = B36.indexOf(first14[i]!)
|
|
const product = v * (i % 2 === 0 ? 1 : 2)
|
|
sum += Math.trunc(product / 36) + (product % 36)
|
|
}
|
|
return B36[(36 - (sum % 36)) % 36]!
|
|
}
|
|
|
|
export function validateGstin(gstin: string): { ok: boolean; stateCode?: string; reason?: string } {
|
|
const g = gstin.toUpperCase().trim()
|
|
if (!GSTIN_SHAPE.test(g)) return { ok: false, reason: 'shape' }
|
|
if (gstinCheckDigit(g.slice(0, 14)) !== g[14]) return { ok: false, reason: 'checksum' }
|
|
return { ok: true, stateCode: g.slice(0, 2) }
|
|
}
|
|
|
|
/** Minimal buyer identity a place-of-supply decision needs. */
|
|
export interface SupplyCustomer {
|
|
gstin?: string
|
|
/** 2-digit GST state code; when absent it is read from the GSTIN's first two digits. */
|
|
stateCode?: string
|
|
}
|
|
|
|
export interface SupplyDecision {
|
|
/** True when the buyer carries a GSTIN — a registered B2B sale. */
|
|
b2b: boolean
|
|
/** GST state code that is the place of supply (buyer's for B2B, the store's for B2C). */
|
|
placeOfSupplyStateCode: string
|
|
/** Place of supply differs from the store ⇒ the IGST path; else CGST+SGST. */
|
|
interState: boolean
|
|
/** Buyer GSTIN snapshot (upper-cased) when this is a B2B sale. */
|
|
buyerGstin?: string
|
|
}
|
|
|
|
/**
|
|
* Place-of-supply decision at the counter — the single rule the POS and the
|
|
* store server must agree on so their engine runs match to the paisa (R5).
|
|
* A buyer with a GSTIN is B2B: place of supply is the buyer's state (an explicit
|
|
* stateCode, else the GSTIN's first two digits). Everyone else is a B2C
|
|
* over-the-counter sale — place of supply is the store. Inter-state ⇒ IGST.
|
|
*/
|
|
export function deriveSupply(customer: SupplyCustomer | undefined, storeStateCode: string): SupplyDecision {
|
|
const gstin = customer?.gstin?.toUpperCase().trim()
|
|
const b2b = gstin !== undefined && gstin !== ''
|
|
const buyerState = customer?.stateCode !== undefined && customer.stateCode !== ''
|
|
? customer.stateCode
|
|
: b2b ? gstin.slice(0, 2) : undefined
|
|
const placeOfSupplyStateCode = b2b && buyerState !== undefined ? buyerState : storeStateCode
|
|
return {
|
|
b2b,
|
|
placeOfSupplyStateCode,
|
|
interState: placeOfSupplyStateCode !== storeStateCode,
|
|
...(b2b ? { buyerGstin: gstin } : {}),
|
|
}
|
|
}
|