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.
84 lines
3.5 KiB
TypeScript
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')
|
|
})
|
|
})
|