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.
sims-hq/packages/domain/test/domain.test.ts

105 lines
4.4 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
formatINR, fyOf, fyShort, uuidv7, timestampOfUuidv7,
formatDocNo, nextDocNo, rolloverForFy, seriesPrefix, assertGstDocNo,
gstinCheckDigit, validateGstin, deriveSupply,
type DocSeries,
} from '@sims/domain'
describe('money — Indian grouping', () => {
it('groups lakh/crore style', () => {
expect(formatINR(123_456_789)).toBe('₹12,34,567.89')
expect(formatINR(100)).toBe('₹1.00')
expect(formatINR(-4_50)).toBe('-₹4.50')
expect(formatINR(99_999, { symbol: false })).toBe('999.99')
})
})
describe('financial year (Day Begin idiom)', () => {
it('splits at 1 April', () => {
expect(fyOf('2026-03-31')).toBe('2025-26')
expect(fyOf('2026-04-01')).toBe('2026-27')
expect(fyShort('2026-27')).toBe('26')
})
})
describe('uuidv7', () => {
it('is well-formed and carries its timestamp', () => {
const at = 1_750_000_000_000
const id = uuidv7(at)
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
expect(timestampOfUuidv7(id)).toBe(at)
})
it('sorts by creation time', () => {
expect(uuidv7(1000) < uuidv7(2000)).toBe(true)
})
})
describe('document series — offline per-counter numbering', () => {
const series: DocSeries = {
tenantId: 't', storeId: 's', counterId: 'c', docType: 'SALE',
fy: '2026-27', prefix: seriesPrefix('ST1', 'C2', '2026-27'), nextSeq: 123,
}
it('formats within the GST 16-char rule (4-digit FY, M9)', () => {
const { docNo, series: bumped } = nextDocNo(series)
expect(docNo).toBe('ST1C22026-000123')
expect(docNo.length).toBeLessThanOrEqual(16)
expect(bumped.nextSeq).toBe(124)
})
it('M9: the 4-digit FY start year makes centuries-apart FYs distinct (no prefix collision)', () => {
const a = seriesPrefix('ST1', 'C2', '2026-27')
const b = seriesPrefix('ST1', 'C2', '2126-27') // 100 years later — collided under the old 2-digit "26"
expect(a).toBe('ST1C22026')
expect(b).toBe('ST1C22126')
expect(a).not.toBe(b)
// both still fit the 16-char [A-Za-z0-9/-] rule at the top of the 6-digit sequence range
expect(formatDocNo(a, 999_999)).toBe('ST1C22026-999999')
expect(formatDocNo(a, 999_999).length).toBe(16)
expect(() => formatDocNo(b, 999_999)).not.toThrow()
})
it('rejects numbers that break the GST rule', () => {
expect(() => assertGstDocNo('THIS/PREFIX/IS/TOO/LONG-000001')).toThrow(/16-char/)
expect(() => formatDocNo('BAD_CHAR', 1)).toThrow(/16-char/) // underscore not allowed
})
it('FY rollover restarts the sequence', () => {
const rolled = rolloverForFy(series, '2027-28', seriesPrefix('ST1', 'C2', '2027-28'))
expect(rolled.nextSeq).toBe(1)
expect(rolloverForFy(series, '2026-27', series.prefix)).toBe(series)
})
})
describe('GSTIN validation', () => {
const body = '32ABCDE1234F1Z'
const valid = body + gstinCheckDigit(body)
it('accepts a checksum-correct GSTIN and extracts the state', () => {
expect(validateGstin(valid)).toEqual({ ok: true, stateCode: '32' })
})
it('rejects wrong checksum and wrong shape', () => {
const flipped = body + (valid[14] === 'A' ? 'B' : 'A')
expect(validateGstin(flipped).ok).toBe(false)
expect(validateGstin('not-a-gstin').ok).toBe(false)
})
})
describe('deriveSupply — place of supply at the counter', () => {
const store = '32' // MegaMart's state
const intraGstin = '32ABCDE1234F1Z' + gstinCheckDigit('32ABCDE1234F1Z')
const interGstin = '27ABCDE1234F1Z' + gstinCheckDigit('27ABCDE1234F1Z')
it('no GSTIN → B2C, place of supply is the store state (intra)', () => {
expect(deriveSupply(undefined, store)).toMatchObject({ b2b: false, placeOfSupplyStateCode: '32', interState: false })
// A walk-in with only a state on file is still B2C — place of supply stays the store.
expect(deriveSupply({ stateCode: '27' }, store)).toMatchObject({ b2b: false, placeOfSupplyStateCode: '32', interState: false })
})
it('GSTIN in the same state → B2B intra (CGST/SGST)', () => {
expect(deriveSupply({ gstin: intraGstin }, store)).toMatchObject({
b2b: true, placeOfSupplyStateCode: '32', interState: false, buyerGstin: intraGstin,
})
})
it('GSTIN in another state → B2B inter (IGST)', () => {
expect(deriveSupply({ gstin: interGstin }, store)).toMatchObject({
b2b: true, placeOfSupplyStateCode: '27', interState: true, buyerGstin: interGstin,
})
})
})