import { describe, expect, it } from 'vitest' import { amountInWordsINR, type BillLine, type BillTotals } from '@sims/domain' import { renderInvoiceHtml, type InvoiceDoc, ean13Modules, ean13Svg, normalizeEan13, renderLabelSheetHtml, packRasterRow, bitonalFromRGBA, rasterToEscPos, } from '@sims/printing' describe('amountInWordsINR — Indian numbering', () => { it('zero', () => { expect(amountInWordsINR(0)).toBe('Rupees Zero Only') }) it('paisa only', () => { expect(amountInWordsINR(50)).toBe('Fifty Paise Only') }) it('a single paisa is singular', () => { expect(amountInWordsINR(1)).toBe('One Paisa Only') }) it('one lakh rupees', () => { expect(amountInWordsINR(1_00_000 * 100)).toBe('Rupees One Lakh Only') }) it('one crore rupees', () => { expect(amountInWordsINR(1_00_00_000 * 100)).toBe('Rupees One Crore Only') }) it('mixed rupees and paise', () => { expect(amountInWordsINR(1431_50)).toBe('Rupees One Thousand Four Hundred Thirty One and Fifty Paise Only') }) }) const line: BillLine = { itemId: 'i1', name: 'Surf Excel 1kg', hsn: '3402', qty: 1, unitCode: 'PCS', unitPricePaise: 14_500, mrpPaise: 15_000, grossPaise: 14_500, discountPaise: 0, taxablePaise: 12_288, taxRateBp: 1800, cgstPaise: 1_106, sgstPaise: 1_106, igstPaise: 0, cessPaise: 0, lineTotalPaise: 14_500, } const totals: BillTotals = { grossPaise: 14_500, discountPaise: 0, taxablePaise: 12_288, cgstPaise: 1_106, sgstPaise: 1_106, igstPaise: 0, cessPaise: 0, roundOffPaise: 0, payablePaise: 14_500, savingsVsMrpPaise: 500, } const invoice: InvoiceDoc = { seller: { name: 'MegaMart T.Nagar', gstin: '32ABCDE1234F1Z5', stateCode: '32', address: 'T.Nagar, Chennai' }, buyer: { name: 'Lakshmi', phone: '9840012345' }, invoiceNo: 'ST1C2/26-000001', invoiceDate: '2026-07-10', placeOfSupplyStateCode: '32', lines: [line], totals, paymentLabel: 'CASH', } describe('renderInvoiceHtml — A4 GST invoice', () => { it('contains the key GST-invoice fields', () => { const html = renderInvoiceHtml(invoice) expect(html).toContain('Tax Invoice') expect(html).toContain('MegaMart T.Nagar') expect(html).toContain('32ABCDE1234F1Z5') expect(html).toContain('ST1C2/26-000001') expect(html).toContain('Surf Excel 1kg') expect(html).toContain('3402') // HSN expect(html).toContain('CGST') expect(html).toContain('SGST') expect(html).toContain('Place of Supply') expect(html).toContain('Kerala') // state 32 resolved }) it('renders the amount in words', () => { const html = renderInvoiceHtml(invoice) expect(html).toContain('Amount in words') expect(html).toContain(amountInWordsINR(totals.payablePaise)) // Rupees One Hundred Forty Five Only }) it('shows IGST (not CGST/SGST) for an inter-state bill', () => { const inter = renderInvoiceHtml({ ...invoice, totals: { ...totals, cgstPaise: 0, sgstPaise: 0, igstPaise: 2_212 }, lines: [{ ...line, cgstPaise: 0, sgstPaise: 0, igstPaise: 2_212 }], placeOfSupplyStateCode: '29', }) expect(inter).toContain('IGST') expect(inter).not.toContain('CGST') }) it('is a self-contained HTML document', () => { const html = renderInvoiceHtml(invoice) expect(html.startsWith('')).toBe(true) expect(html).toContain('') expect(html).toContain('window.print()') }) }) describe('ean13 encoding', () => { it('encodes the standard 95-module structure (all-zero symbol)', () => { const m = ean13Modules('000000000000') // 12 digits → check digit appended expect(m).toHaveLength(95) expect(m.startsWith('101')).toBe(true) // start guard expect(m.endsWith('101')).toBe(true) // end guard expect(m.slice(45, 50)).toBe('01010') // centre guard // first digit 0 → all L; L(0) = 0001101, so the first data element follows the start guard expect(m.startsWith('101' + '0001101')).toBe(true) }) it('computes the check digit via ean13CheckDigit for a 12-digit body', () => { expect(normalizeEan13('890106301435')).toBe('8901063014350') }) it('rejects a 13-digit code with a bad checksum (strict path)', () => { expect(() => normalizeEan13('8901063014357')).toThrow(/checksum/) }) it('still renders an SVG for legacy 13-digit data (lenient path, R18)', () => { const svg = ean13Svg('8901063014357') expect(svg.startsWith('')).toBe(true) expect(svg).toContain('8901063014357') // human-readable matches stored code }) }) describe('renderLabelSheetHtml', () => { it('lays out labels with barcode, name and MRP', () => { const html = renderLabelSheetHtml( [{ name: 'Surf Excel 1kg', mrpPaise: 15_000, code13: '8901030704833', pricePaise: 14_500 }], { cols: 3, labelWmm: 38, labelHmm: 25 }, ) expect(html).toContain('Surf Excel 1kg') expect(html).toContain('MRP') expect(html).toContain(' { const html = renderLabelSheetHtml([{ name: 'Loose Tomato', mrpPaise: 3_200 }], { cols: 2, labelWmm: 50, labelHmm: 30 }) expect(html).toContain('Loose Tomato') expect(html).not.toContain(' { it('packs a byte MSB-first', () => { expect(packRasterRow([1, 0, 1, 0, 0, 0, 0, 0])).toEqual([0xa0]) expect(packRasterRow([0, 0, 0, 0, 0, 0, 0, 1])).toEqual([0x01]) }) it('pads a partial trailing byte with zeros', () => { expect(packRasterRow([1, 1, 1, 1, 1, 1, 1, 1, 1])).toEqual([0xff, 0x80]) }) it('thresholds RGBA to bitonal (dark = printed dot)', () => { // two pixels: black then white const data = [0, 0, 0, 255, 255, 255, 255, 255] const bmp = bitonalFromRGBA(data, 2, 1) expect([...bmp.bits]).toEqual([1, 0]) }) it('emits a GS v 0 raster command with the right header', () => { const bytes = rasterToEscPos({ width: 8, height: 1, bits: Uint8Array.from([1, 0, 1, 0, 0, 0, 0, 0]) }) expect([...bytes]).toEqual([0x1d, 0x76, 0x30, 0, 1, 0, 1, 0, 0xa0]) }) })