feat(hq): GET /reminders/:id/preview — exact email via shared reminderContext
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>feat/client-detail-redesign
parent
48731fdb2b
commit
52e8a03c81
@ -0,0 +1,51 @@
|
|||||||
|
// apps/hq/test/reminder-preview.test.ts
|
||||||
|
import { describe, it, expect, afterAll } from 'vitest'
|
||||||
|
import express from 'express'
|
||||||
|
import { openDb } from '../src/db'
|
||||||
|
import { seedIfEmpty } from '../src/seed'
|
||||||
|
import { createStaff } from '../src/auth'
|
||||||
|
import { createClient } from '../src/repos-clients'
|
||||||
|
import { createModule, setPrice } from '../src/repos-modules'
|
||||||
|
import { createDraft, issueDocument } from '../src/repos-documents'
|
||||||
|
import { upsertReminder } from '../src/repos-reminders'
|
||||||
|
import { apiRouter } from '../src/api'
|
||||||
|
|
||||||
|
function appWith() {
|
||||||
|
const db = openDb(':memory:'); seedIfEmpty(db) // company.name='Tecnostac'
|
||||||
|
createStaff(db, { email: 'owner@test.in', displayName: 'Owner', role: 'owner', password: 'owner-password' })
|
||||||
|
const c = createClient(db, 'u1', { name: 'Acme', code: 'ACME', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] })
|
||||||
|
const m = createModule(db, 'u1', { code: 'POS', name: 'POS' })
|
||||||
|
setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-04-01' })
|
||||||
|
const inv = issueDocument(db, 'u1', createDraft(db, 'u1', { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] }).id)
|
||||||
|
const overdue = upsertReminder(db, { ruleKind: 'invoice_overdue', subjectId: inv.id, duePeriod: '2026-07', clientId: c.id, docId: inv.id })
|
||||||
|
const internal = upsertReminder(db, { ruleKind: 'follow_up', subjectId: 'i1', duePeriod: '2026-07-08', clientId: c.id })
|
||||||
|
const app = express(); app.use(express.json()); app.locals['db'] = db; app.use('/api', apiRouter(db))
|
||||||
|
const server = app.listen(0)
|
||||||
|
const base = `http://localhost:${(server.address() as { port: number }).port}/api`
|
||||||
|
return { server, base, inv, overdueId: overdue.id, internalId: internal.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GET /reminders/:id/preview', () => {
|
||||||
|
const ctx = appWith()
|
||||||
|
afterAll(() => ctx.server.close())
|
||||||
|
const login = async () =>
|
||||||
|
(await (await fetch(`${ctx.base}/auth/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: 'owner@test.in', password: 'owner-password' }) })).json() as any).token
|
||||||
|
const get = async (token: string | null, id: string) => {
|
||||||
|
const res = await fetch(`${ctx.base}/reminders/${id}/preview`, { headers: token ? { authorization: `Bearer ${token}` } : {} })
|
||||||
|
return { status: res.status, json: await res.json() as any }
|
||||||
|
}
|
||||||
|
it('requires auth', async () => { expect((await get(null, ctx.overdueId)).status).toBe(401) })
|
||||||
|
it('previews the overdue email — subject + body match what would be sent', async () => {
|
||||||
|
const token = await login()
|
||||||
|
const r = await get(token, ctx.overdueId)
|
||||||
|
expect(r.status).toBe(200)
|
||||||
|
expect(r.json.subject).toContain(ctx.inv.docNo) // 'Payment reminder — Invoice INV/26-27-0001 (Tecnostac)'
|
||||||
|
expect(r.json.subject).toContain('Tecnostac')
|
||||||
|
expect(r.json.body).toContain('Acme') // clientName
|
||||||
|
})
|
||||||
|
it('404s an unknown reminder and 400s an internal follow_up', async () => {
|
||||||
|
const token = await login()
|
||||||
|
expect((await get(token, 'nope')).status).toBe(404)
|
||||||
|
expect((await get(token, ctx.internalId)).status).toBe(400)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue