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/apps/hq/test/send-reminder.test.ts

60 lines
3.7 KiB
TypeScript

// apps/hq/test/send-reminder.test.ts
import { describe, it, expect } from 'vitest'
import { openDb } from '../src/db'
import { seedIfEmpty } from '../src/seed'
import { createClient } from '../src/repos-clients'
import { createModule, setPrice } from '../src/repos-modules'
import { createDraft, issueDocument } from '../src/repos-documents'
import { saveAccount } from '../src/repos-email'
import { encrypt } from '../src/crypto'
import { upsertReminder, getReminder } from '../src/repos-reminders'
import { sendReminder, type SendReminderDeps } from '../src/send-reminder'
const KEY = '11'.repeat(32)
const fakePdf = async () => Buffer.from('%PDF-fake')
const company = () => ({ 'company.name': 'Tecnostac' })
function deps(f: typeof fetch): SendReminderDeps {
return { gmail: { f, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, renderPdf: fakePdf, company, now: () => '2026-07-10T09:00:00Z' }
}
async function invoiceSetup() {
const db = openDb(':memory:'); await seedIfEmpty(db)
await saveAccount(db, 'us@tecnostac.com', encrypt('refresh-token', KEY))
const c = await createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] })
const m = await createModule(db, 'u1', { code: 'POS', name: 'POS' })
await setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-04-01' })
const inv = await issueDocument(db, 'u1', (await createDraft(db, 'u1', { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] })).id)
return { db, c, inv }
}
describe('sendReminder', () => {
it('sends an overdue reminder with the invoice PDF and marks it sent', async () => {
const { db, c, inv } = await invoiceSetup()
const { id } = await upsertReminder(db, { ruleKind: 'invoice_overdue', subjectId: inv.id, duePeriod: '2026-07', clientId: c.id, docId: inv.id, now: '2026-07-10T00:00:00Z' })
const okFetch = (async (url: string) =>
new Response(JSON.stringify(String(url).includes('/token') ? { access_token: 'at' } : { id: 'gmsg-1' }), { status: 200 })) as typeof fetch
const out = await sendReminder(db, deps(okFetch), id, 'u1')
expect(out).toEqual({ ok: true })
expect((await getReminder(db, id))!.status).toBe('sent')
expect((await getReminder(db, id))!.sentAt).toBe('2026-07-10T09:00:00Z')
const log = await db.get<{ status: string; document_id: string }>(`SELECT status, document_id FROM email_log ORDER BY id DESC`)
expect(log).toMatchObject({ status: 'sent', document_id: inv.id })
})
it('leaves the reminder failed (not sent) on token death, and flips the account dead', async () => {
const { db, c, inv } = await invoiceSetup()
const { id } = await upsertReminder(db, { ruleKind: 'invoice_overdue', subjectId: inv.id, duePeriod: '2026-07', clientId: c.id, docId: inv.id, now: '2026-07-10T00:00:00Z' })
const deadFetch = (async () => new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400 })) as typeof fetch
const out = await sendReminder(db, deps(deadFetch), id, 'u1')
expect(out).toEqual({ ok: false, error: 'gmail-token-dead' })
expect((await getReminder(db, id))!.status).toBe('failed')
expect(await db.get(`SELECT status FROM email_account`)).toMatchObject({ status: 'dead' })
})
it('refuses to email an internal follow_up reminder', async () => {
const { db, c } = await invoiceSetup()
const { id } = await upsertReminder(db, { ruleKind: 'follow_up', subjectId: 'i1', duePeriod: '2026-07-08', clientId: c.id, now: '2026-07-10T00:00:00Z' })
const noFetch = (async () => new Response('{}', { status: 200 })) as typeof fetch
await expect(sendReminder(db, deps(noFetch), id, 'u1')).rejects.toThrow(/sendable/i)
})
})