diff --git a/apps/hq/test/templates.test.ts b/apps/hq/test/templates.test.ts
index bd4f450..feadf4b 100644
--- a/apps/hq/test/templates.test.ts
+++ b/apps/hq/test/templates.test.ts
@@ -52,3 +52,43 @@ describe('rupees in words', () => {
expect(rupeesInWords(50)).toBe('Fifty Paise Only')
})
})
+
+describe('quotation sections (D18 WS-H)', () => {
+ const line = (n: number) => ({
+ itemId: `m${n}`, name: `Module ${n}`, hsn: '998313', qty: 1, unitCode: 'NOS',
+ unitPricePaise: 1_000_00, grossPaise: 1_000_00, discountPaise: 0, taxablePaise: 1_000_00,
+ taxRateBp: 1800, cgstPaise: 90_00, sgstPaise: 90_00, igstPaise: 0, cessPaise: 0,
+ lineTotalPaise: 1_180_00,
+ })
+ const fiveModuleQuote = {
+ ...(doc as object), docType: 'QUOTATION', docNo: 'QT/26-27-0002',
+ payload: {
+ lines: [1, 2, 3, 4, 5].map(line),
+ totals: {},
+ lineContents: [['Core ledgers', 'Day-end'], ['RTGS/NEFT'], [], ['App for members'], ['SMS 50k pack']],
+ },
+ } as never
+
+ it('a 5-module quotation renders 5 titled sections, the summary line, and NO billing grid', () => {
+ const html = documentHtml(fiveModuleQuote, client, company)
+ expect(html.match(/class="qsection/g)).toHaveLength(5)
+ expect(html.match(/class="qsection qbreak/g)).toHaveLength(4) // fresh page from the 2nd on (print)
+ expect(html).toContain('This proposal covers:')
+ expect(html).toContain('Module 1')
+ expect(html).toContain('Core ledgers') // per-module contents land in their section
+ expect(html).toContain('SMS 50k pack')
+ expect(html).not.toContain('
') // proposals are not billing grids
+ expect(html).toContain('Grand Total') // totals block still follows
+ })
+
+ it('a module without content prints a tight section (no empty included-block)', () => {
+ const html = documentHtml(fiveModuleQuote, client, company)
+ expect(html.match(/What's included/g)).toHaveLength(4) // section 3 has no contents
+ })
+
+ it('INVOICE keeps the compact billing grid — layout untouched by WS-H', () => {
+ const html = documentHtml(doc, client, company)
+ expect(html).toContain('')
+ expect(html).not.toContain('class="qsection')
+ })
+})