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/printing/test/printing.test.ts

84 lines
3.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { EscPos, hr, twoCol, renderReceipt } from '@sims/printing'
import type { BillLine, BillTotals } from '@sims/domain'
describe('twoCol', () => {
it('pads to exact width and truncates long names', () => {
expect(twoCol('Atta 5kg', '270.00', 32)).toHaveLength(32)
const long = twoCol('A very very long item name indeed', '1,431.00', 32)
expect(long).toHaveLength(32)
expect(long.endsWith('1,431.00')).toBe(true)
})
it('always keeps one space between columns', () => {
expect(twoCol('X'.repeat(40), '99.00', 32)).toContain(' 99.00')
})
})
describe('EscPos builder', () => {
it('starts with init and encodes control sequences', () => {
const b = new EscPos().init().bold(true).text('HI').cut().build()
expect([...b.slice(0, 2)]).toEqual([0x1b, 0x40])
expect([...b]).toContain(0x48) // 'H'
expect([...b.slice(-4)]).toEqual([0x1d, 0x56, 66, 3])
})
it('emits the standard drawer-kick pulse', () => {
const b = new EscPos().drawerKick().build()
expect([...b]).toEqual([0x1b, 0x70, 0, 25, 250])
})
it('collapses non-ASCII to ? in text mode (raster path comes later)', () => {
const b = new EscPos().text('चाय').build()
expect([...b]).toEqual([0x3f, 0x3f, 0x3f])
})
})
describe('renderReceipt', () => {
const line: BillLine = {
itemId: 'i1', name: 'Aashirvaad Atta 5kg', hsn: '1101', qty: 1, unitCode: 'PCS',
unitPricePaise: 27_000, mrpPaise: 28_500, grossPaise: 27_000, discountPaise: 0,
taxablePaise: 22_881, taxRateBp: 1800, cgstPaise: 2_059, sgstPaise: 2_060,
igstPaise: 0, cessPaise: 0, lineTotalPaise: 27_000,
}
const totals: BillTotals = {
grossPaise: 27_000, discountPaise: 0, taxablePaise: 22_881, cgstPaise: 2_059,
sgstPaise: 2_060, igstPaise: 0, cessPaise: 0, roundOffPaise: 0,
payablePaise: 27_000, savingsVsMrpPaise: 1_500,
}
const doc = {
storeName: 'MegaMart', docNo: 'ST1C2/26-000123', businessDate: '2026-07-09',
counterCode: 'C2', cashierName: 'Ramesh', lines: [line], totals, paymentLabel: 'CASH',
}
it('renders a complete receipt with cut at the end', () => {
const bytes = renderReceipt(doc, { width: 42, kickDrawer: false })
const text = new TextDecoder('ascii').decode(bytes)
expect(text).toContain('PAYABLE')
expect(text).toContain('ST1C2/26-000123')
expect(text).toContain('You saved 15.00 vs MRP')
expect([...bytes.slice(-4)]).toEqual([0x1d, 0x56, 66, 3])
})
it('kicks the drawer before printing only when asked', () => {
const withKick = renderReceipt(doc, { width: 42, kickDrawer: true })
expect([...withKick.slice(2, 7)]).toEqual([0x1b, 0x70, 0, 25, 250])
const noKick = renderReceipt(doc, { width: 42, kickDrawer: false })
expect([...noKick.slice(2, 7)]).not.toEqual([0x1b, 0x70, 0, 25, 250])
})
it(`shows IGST instead of the split for inter-state bills`, () => {
const inter = renderReceipt(
{ ...doc, totals: { ...totals, cgstPaise: 0, sgstPaise: 0, igstPaise: 4_119 } },
{ width: 42, kickDrawer: false },
)
const text = new TextDecoder('ascii').decode(inter)
expect(text).toContain('IGST')
expect(text).not.toContain('CGST')
})
it('prints the buyer GSTIN line for a B2B receipt', () => {
const b2b = renderReceipt(
{ ...doc, buyerName: 'Anand Traders', buyerGstin: '27ABCDE1234F1Z0' },
{ width: 42, kickDrawer: false },
)
const text = new TextDecoder('ascii').decode(b2b)
expect(text).toContain('Buyer: Anand Traders')
expect(text).toContain('Buyer GSTIN: 27ABCDE1234F1Z0')
})
})