chore: remove all 'Tecnostac' references — everything is SiMS

Founder: there is no Tecnostac in this application. Replaced the seed default
company.name ('Tecnostac' -> 'SiMS'), demo-seed data, ~22 test fixtures/emails, and
docs (Tecnostac -> SiMS, @tecnostac.com -> @sims.com). Live DB company.name also
updated to 'SiMS' (shows on dashboard, documents, letterhead, reminder emails). Owner
email was already SiMS (simssoftware13@gmail.com). Full suite 416 green.
feat/client-detail-redesign
Thomas Joise 2 days ago
parent b065332f89
commit 11d45e805c

@ -58,7 +58,7 @@ node apps/hq/dist/server.cjs # first boot on an empty data/ prints a one-ti
cd apps/hq-web && npm run dev cd apps/hq-web && npm run dev
``` ```
- Owner login on first boot: `admin@tecnostac.com` + the one-time password printed in the - Owner login on first boot: `admin@sims.com` + the one-time password printed in the
server log (change it immediately). The "Gmail disconnected" banner is expected until Gmail is connected. server log (change it immediately). The "Gmail disconnected" banner is expected until Gmail is connected.
- Env: `DATABASE_URL` (set → Postgres engine, absent → SQLite at `HQ_DATA_DIR`; D19), - Env: `DATABASE_URL` (set → Postgres engine, absent → SQLite at `HQ_DATA_DIR`; D19),
`SIMS_PIN_PEPPER` (set once, never change), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer), `SIMS_PIN_PEPPER` (set once, never change), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer),

@ -88,7 +88,7 @@ npm install # from the repo root (workspaces: packages/*, ap
npm run build --workspace @sims/hq npm run build --workspace @sims/hq
node apps/hq/dist/server.cjs # serves :5182 (API, built web UI, /share/:token) node apps/hq/dist/server.cjs # serves :5182 (API, built web UI, /share/:token)
# First boot on an empty data/ creates data/hq.db and prints a one-time owner # First boot on an empty data/ creates data/hq.db and prints a one-time owner
# password — capture it. Log in as admin@tecnostac.com, then change it. # password — capture it. Log in as admin@sims.com, then change it.
# Frontend — hq-web dev server (Vite on :5183, proxies /api and /share to :5182) # Frontend — hq-web dev server (Vite on :5183, proxies /api and /share to :5182)
cd apps/hq-web && npm run dev cd apps/hq-web && npm run dev

@ -1,5 +1,5 @@
/** /**
* Demo seed populates a fresh HQ_DATA_DIR with a realistic Tecnostac dataset so the * Demo seed populates a fresh HQ_DATA_DIR with a realistic SiMS dataset so the
* app can be shown with live screens. Run: HQ_DATA_DIR=<dir> npx tsx scripts/demo-seed.ts * app can be shown with live screens. Run: HQ_DATA_DIR=<dir> npx tsx scripts/demo-seed.ts
* Not shipped; a presentation helper. * Not shipped; a presentation helper.
*/ */
@ -17,7 +17,7 @@ void (async () => {
const db = openDb(process.env['HQ_DATA_DIR']) const db = openDb(process.env['HQ_DATA_DIR'])
// Known owner so we can log in for screenshots (before seedIfEmpty, so it skips its own). // Known owner so we can log in for screenshots (before seedIfEmpty, so it skips its own).
await createStaff(db, { email: 'owner@tecnostac.com', displayName: 'Thomas (Owner)', role: 'owner', password: 'demo-owner-2026' }) await createStaff(db, { email: 'owner@sims.com', displayName: 'Thomas (Owner)', role: 'owner', password: 'demo-owner-2026' })
await seedIfEmpty(db) await seedIfEmpty(db)
const U = 'demo' const U = 'demo'
@ -28,20 +28,20 @@ void (async () => {
// Company + template letterhead data (what the Document Template page edits). // Company + template letterhead data (what the Document Template page edits).
const settings: Record<string, string> = { const settings: Record<string, string> = {
'company.name': 'Tecnostac Solutions Pvt Ltd', 'company.name': 'SiMS Solutions Pvt Ltd',
'company.address': '2nd Floor, Cyber Park, Kakkanad, Kochi, Kerala 682030', 'company.address': '2nd Floor, Cyber Park, Kakkanad, Kochi, Kerala 682030',
'company.gstin': '32ABCDE1234F1Z9', 'company.gstin': '32ABCDE1234F1Z9',
'company.state_code': '32', 'company.state_code': '32',
'company.phone': '+91 98470 12345', 'company.phone': '+91 98470 12345',
'company.email': 'accounts@tecnostac.com', 'company.email': 'accounts@sims.com',
'company.bank': 'HDFC Bank · A/c 50200012345678 · IFSC HDFC0001234 · Kakkanad', 'company.bank': 'HDFC Bank · A/c 50200012345678 · IFSC HDFC0001234 · Kakkanad',
'template.accent': '#1e3a5f', 'template.accent': '#1e3a5f',
'template.logo': LOGO, 'template.logo': LOGO,
'template.terms': '1. Prices are valid for 15 days from the date of this quotation.\n2. 50% advance on confirmation, balance before go-live.\n3. Annual subscription renews on the anniversary of activation.', 'template.terms': '1. Prices are valid for 15 days from the date of this quotation.\n2. 50% advance on confirmation, balance before go-live.\n3. Annual subscription renews on the anniversary of activation.',
'template.declaration': 'We declare that this invoice shows the actual price of the services described and that all particulars are true and correct.', 'template.declaration': 'We declare that this invoice shows the actual price of the services described and that all particulars are true and correct.',
'template.jurisdiction': 'Subject to Ernakulam jurisdiction', 'template.jurisdiction': 'Subject to Ernakulam jurisdiction',
'template.footer_note': 'Thank you for your business. For support: support@tecnostac.com · +91 98470 12345', 'template.footer_note': 'Thank you for your business. For support: support@sims.com · +91 98470 12345',
'template.signatory_label': 'For Tecnostac Solutions Pvt Ltd', 'template.signatory_label': 'For SiMS Solutions Pvt Ltd',
} }
for (const [k, v] of Object.entries(settings)) await setSetting(db, U, k, v) for (const [k, v] of Object.entries(settings)) await setSetting(db, U, k, v)
@ -100,7 +100,7 @@ void (async () => {
const shareToken = ((await db.get<{ token: string }>(`SELECT token FROM document_share LIMIT 1`))!).token const shareToken = ((await db.get<{ token: string }>(`SELECT token FROM document_share LIMIT 1`))!).token
console.log('DEMO_READY') console.log('DEMO_READY')
console.log('login: owner@tecnostac.com / demo-owner-2026') console.log('login: owner@sims.com / demo-owner-2026')
console.log('quotation_id:', qtIssued.id, 'doc_no:', qtIssued.docNo) console.log('quotation_id:', qtIssued.id, 'doc_no:', qtIssued.docNo)
console.log('invoice_id:', inv1.id, 'doc_no:', inv1.docNo) console.log('invoice_id:', inv1.id, 'doc_no:', inv1.docNo)
console.log('share_token:', shareToken) console.log('share_token:', shareToken)

@ -15,7 +15,7 @@ import { SCHEDULE_DEFAULTS, type ScheduleRuleKind } from './repos-reminders'
const OWNER_EMAIL = process.env['HQ_OWNER_EMAIL']?.trim() || 'simssoftware13@gmail.com' const OWNER_EMAIL = process.env['HQ_OWNER_EMAIL']?.trim() || 'simssoftware13@gmail.com'
const SETTING_DEFAULTS: Record<string, string> = { const SETTING_DEFAULTS: Record<string, string> = {
'company.name': 'Tecnostac', 'company.name': 'SiMS',
'company.address': '', 'company.address': '',
'company.gstin': '', 'company.gstin': '',
'company.state_code': '32', // Kerala — founder to confirm 'company.state_code': '32', // Kerala — founder to confirm

@ -6,9 +6,9 @@ import { createStaff, login, verifySession } from '../src/auth'
describe('hq auth', () => { describe('hq auth', () => {
it('logs in with correct password, rejects wrong one', async () => { it('logs in with correct password, rejects wrong one', async () => {
const db = openDb(':memory:') const db = openDb(':memory:')
await createStaff(db, { email: 'admin@tecnostac.com', displayName: 'Owner', role: 'owner', password: 'let-me-in-9' }) await createStaff(db, { email: 'admin@sims.com', displayName: 'Owner', role: 'owner', password: 'let-me-in-9' })
expect(await login(db, 'admin@tecnostac.com', 'wrong')).toMatchObject({ ok: false, reason: 'invalid' }) expect(await login(db, 'admin@sims.com', 'wrong')).toMatchObject({ ok: false, reason: 'invalid' })
const ok = await login(db, 'admin@tecnostac.com', 'let-me-in-9') const ok = await login(db, 'admin@sims.com', 'let-me-in-9')
expect(ok.ok).toBe(true) expect(ok.ok).toBe(true)
const staff = await verifySession(db, ok.ok ? ok.token : '') const staff = await verifySession(db, ok.ok ? ok.token : '')
expect(staff).toMatchObject({ role: 'owner' }) expect(staff).toMatchObject({ role: 'owner' })

@ -26,7 +26,7 @@ function bounceFetch(recipient: string): typeof fetch {
describe('pollBounces', () => { describe('pollBounces', () => {
it('flips the sent email_log to bounced and raises an email_bounced reminder, idempotently', async () => { it('flips the sent email_log to bounced and raises an email_bounced reminder, idempotently', async () => {
const db = openDb(':memory:'); await seedIfEmpty(db) const db = openDb(':memory:'); await seedIfEmpty(db)
await saveAccount(db, 'us@tecnostac.com', encrypt('rt', KEY)) await saveAccount(db, 'us@sims.com', encrypt('rt', KEY))
await logEmail(db, { to: 'ravi@acme.in', subject: 'INV/26-27-0001', status: 'sent', gmailMessageId: 'g1' }) await logEmail(db, { to: 'ravi@acme.in', subject: 'INV/26-27-0001', status: 'sent', gmailMessageId: 'g1' })
const deps: BounceDeps = { const deps: BounceDeps = {
gmail: { f: bounceFetch('ravi@acme.in'), clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, gmail: { f: bounceFetch('ravi@acme.in'), clientId: 'cid', clientSecret: 'sec', keyHex: KEY },

@ -17,7 +17,7 @@ import { apiRouter } from '../src/api'
async function withOwner(): Promise<{ db: DB; ownerId: string }> { async function withOwner(): Promise<{ db: DB; ownerId: string }> {
const db = openDb(':memory:') const db = openDb(':memory:')
const { id } = await createStaff(db, { const { id } = await createStaff(db, {
email: 'admin@tecnostac.com', displayName: 'Owner', role: 'owner', password: 'let-me-in-9', email: 'admin@sims.com', displayName: 'Owner', role: 'owner', password: 'let-me-in-9',
}) })
return { db, ownerId: id } return { db, ownerId: id }
} }

@ -89,7 +89,7 @@ async function appWith(fetchImpl: typeof fetch) {
const db = openDb(':memory:') const db = openDb(':memory:')
const { c, m } = await base(db) const { c, m } = await base(db)
await createStaff(db, { email: 'owner@test.in', displayName: 'Owner', role: 'owner', password: 'owner-password' }) await createStaff(db, { email: 'owner@test.in', displayName: 'Owner', role: 'owner', password: 'owner-password' })
await saveAccount(db, 'us@tecnostac.com', encrypt('refresh-token', KEY)) await saveAccount(db, 'us@sims.com', encrypt('refresh-token', KEY))
const pi = await issueDocument(db, 'u1', (await createDraft(db, 'u1', { const pi = await issueDocument(db, 'u1', (await createDraft(db, 'u1', {
docType: 'PROFORMA', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }], docType: 'PROFORMA', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }],
})).id) })).id)

@ -14,7 +14,7 @@ import { runDailyScan, type ScanDeps } from '../src/scheduler'
const scanDeps: ScanDeps = { const scanDeps: ScanDeps = {
gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' }, gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' },
renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'Tecnostac' }), now: () => '2026-07-10T00:00:00Z', renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'SiMS' }), now: () => '2026-07-10T00:00:00Z',
} }
async function seeded() { async function seeded() {

@ -13,7 +13,7 @@ import { runDailyScan, type ScanDeps } from '../src/scheduler'
const deps: ScanDeps = { const deps: ScanDeps = {
gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' }, gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' },
renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'Tecnostac' }), renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'SiMS' }),
now: () => '2026-07-17T00:00:00Z', now: () => '2026-07-17T00:00:00Z',
} }
@ -122,7 +122,7 @@ describe('due dates (D18)', () => {
const inv = await issueDocument(db, 'u1', (await invoiceDraft()).id) const inv = await issueDocument(db, 'u1', (await invoiceDraft()).id)
await db.run(`UPDATE document SET due_date='2026-07-02' WHERE id=?`, inv.id) await db.run(`UPDATE document SET due_date='2026-07-02' WHERE id=?`, inv.id)
const { id } = await upsertReminder(db, { ruleKind: 'invoice_overdue', subjectId: inv.id, duePeriod: 'd15', clientId: c.id, docId: inv.id, now: '2026-07-17T00:00:00Z' }) const { id } = await upsertReminder(db, { ruleKind: 'invoice_overdue', subjectId: inv.id, duePeriod: 'd15', clientId: c.id, docId: inv.id, now: '2026-07-17T00:00:00Z' })
const { ctx } = await reminderContext(db, (await getReminder(db, id))!, 'Tecnostac', '2026-07-17') const { ctx } = await reminderContext(db, (await getReminder(db, id))!, 'SiMS', '2026-07-17')
expect(ctx.dueDate).toBe('2026-07-02') expect(ctx.dueDate).toBe('2026-07-02')
expect(ctx.daysOverdue).toBe(15) expect(ctx.daysOverdue).toBe(15)
expect(reminderEmail('invoice_overdue', ctx).bodyText).toMatch(/was due on 2026-07-02 and is now 15 day\(s\) overdue/) expect(reminderEmail('invoice_overdue', ctx).bodyText).toMatch(/was due on 2026-07-02 and is now 15 day\(s\) overdue/)
@ -133,7 +133,7 @@ describe('due dates (D18)', () => {
const inv = await issueDocument(db, 'u1', (await invoiceDraft()).id) const inv = await issueDocument(db, 'u1', (await invoiceDraft()).id)
await db.run(`UPDATE document SET doc_date='2026-07-07', due_date=NULL WHERE id=?`, inv.id) await db.run(`UPDATE document SET doc_date='2026-07-07', due_date=NULL WHERE id=?`, inv.id)
const { id } = await upsertReminder(db, { ruleKind: 'invoice_overdue', subjectId: inv.id, duePeriod: 'd7', clientId: c.id, docId: inv.id, now: '2026-07-17T00:00:00Z' }) const { id } = await upsertReminder(db, { ruleKind: 'invoice_overdue', subjectId: inv.id, duePeriod: 'd7', clientId: c.id, docId: inv.id, now: '2026-07-17T00:00:00Z' })
const { ctx } = await reminderContext(db, (await getReminder(db, id))!, 'Tecnostac', '2026-07-17') const { ctx } = await reminderContext(db, (await getReminder(db, id))!, 'SiMS', '2026-07-17')
expect(ctx.dueDate).toBeUndefined() expect(ctx.dueDate).toBeUndefined()
expect(reminderEmail('invoice_overdue', ctx).bodyText).toMatch(/is outstanding and is now 10 day\(s\) past due/) expect(reminderEmail('invoice_overdue', ctx).bodyText).toMatch(/is outstanding and is now 10 day\(s\) past due/)
}) })

@ -12,7 +12,7 @@ import { listAudit } from '../src/audit'
async function withOwner(): Promise<{ db: DB; ownerId: string }> { async function withOwner(): Promise<{ db: DB; ownerId: string }> {
const db = openDb(':memory:') const db = openDb(':memory:')
const { id } = await createStaff(db, { const { id } = await createStaff(db, {
email: 'admin@tecnostac.com', displayName: 'Owner', role: 'owner', password: 'let-me-in-9', email: 'admin@sims.com', displayName: 'Owner', role: 'owner', password: 'let-me-in-9',
}) })
return { db, ownerId: id } return { db, ownerId: id }
} }

@ -94,7 +94,7 @@ async function appWith(fetchImpl: typeof fetch) {
const { db, m, mkClient } = await world() const { db, m, mkClient } = await world()
await createStaff(db, { email: 'owner@test.in', displayName: 'Owner', role: 'owner', password: 'owner-password' }) await createStaff(db, { email: 'owner@test.in', displayName: 'Owner', role: 'owner', password: 'owner-password' })
await createStaff(db, { email: 'staff@test.in', displayName: 'Staff', role: 'staff', password: 'staff-password' }) await createStaff(db, { email: 'staff@test.in', displayName: 'Staff', role: 'staff', password: 'staff-password' })
await saveAccount(db, 'us@tecnostac.com', encrypt('refresh-token', KEY)) await saveAccount(db, 'us@sims.com', encrypt('refresh-token', KEY))
const withMail = await mkClient('Acme Bank', 'ravi@acme.in') const withMail = await mkClient('Acme Bank', 'ravi@acme.in')
const noMail = await mkClient('Beta Coop') // no contact email — must be reported, not skipped silently const noMail = await mkClient('Beta Coop') // no contact email — must be reported, not skipped silently
await assignModule(db, 'u1', { clientId: withMail.id, moduleId: m.id, kind: 'yearly' }) await assignModule(db, 'u1', { clientId: withMail.id, moduleId: m.id, kind: 'yearly' })

@ -17,7 +17,7 @@ const sampleClient = { id: 'c1', code: 'X', name: 'X', stateCode: '32', address:
describe('templates: screen paper styles + documentHtmlSample', () => { describe('templates: screen paper styles + documentHtmlSample', () => {
it('documentHtml carries screen-only paper styles (PDF unaffected — print media)', () => { it('documentHtml carries screen-only paper styles (PDF unaffected — print media)', () => {
expect(documentHtml(sampleDoc, sampleClient, { 'company.name': 'Tecnostac' })).toContain('@media screen') expect(documentHtml(sampleDoc, sampleClient, { 'company.name': 'SiMS' })).toContain('@media screen')
}) })
it('documentHtmlSample renders letterhead, bank box and given bullets through the one renderer', () => { it('documentHtmlSample renders letterhead, bank box and given bullets through the one renderer', () => {
const html = documentHtmlSample( const html = documentHtmlSample(

@ -26,13 +26,13 @@ const okFetch = (async (url: string) =>
const deps: ScanDeps & SendReminderDeps = { const deps: ScanDeps & SendReminderDeps = {
gmail: { f: okFetch, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, gmail: { f: okFetch, clientId: 'cid', clientSecret: 'sec', keyHex: KEY },
renderPdf: async () => Buffer.from('%PDF-fake'), renderPdf: async () => Buffer.from('%PDF-fake'),
company: () => ({ 'company.name': 'Tecnostac' }), company: () => ({ 'company.name': 'SiMS' }),
now: () => '2026-07-10T09:00:00Z', now: () => '2026-07-10T09:00:00Z',
} }
async function world() { async function world() {
const db = openDb(':memory:'); await seedIfEmpty(db) const db = openDb(':memory:'); await seedIfEmpty(db)
await saveAccount(db, 'us@tecnostac.com', encrypt('refresh-token', KEY)) await saveAccount(db, 'us@sims.com', encrypt('refresh-token', KEY))
await setSetting(db, 'u1', 'share.base_url', 'https://hq.example.in') // sends refuse a dead relative link without it await setSetting(db, 'u1', 'share.base_url', 'https://hq.example.in') // sends refuse a dead relative link without it
const c = await createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] }) const c = await createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] })
const m = await createModule(db, 'u1', { code: 'CBS', name: 'Core Banking' }) const m = await createModule(db, 'u1', { code: 'CBS', name: 'Core Banking' })
@ -203,9 +203,9 @@ describe('quote_followup context, preview and send', () => {
await runDailyScan(db, deps, '2026-07-04') await runDailyScan(db, deps, '2026-07-04')
const rem = (await followups(db))[0]! const rem = (await followups(db))[0]!
const auditBefore = ((await db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM audit_log`))!).n const auditBefore = ((await db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM audit_log`))!).n
const { ctx } = await reminderContext(db, rem, 'Tecnostac') const { ctx } = await reminderContext(db, rem, 'SiMS')
const mail = reminderEmail('quote_followup', ctx) const mail = reminderEmail('quote_followup', ctx)
expect(mail.subject).toContain('Tecnostac') expect(mail.subject).toContain('SiMS')
expect(await shareCount(db)).toBe(0) // resolve-only: preview NEVER mints expect(await shareCount(db)).toBe(0) // resolve-only: preview NEVER mints
expect(((await db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM audit_log`))!).n).toBe(auditBefore) expect(((await db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM audit_log`))!).n).toBe(auditBefore)
}) })
@ -213,7 +213,7 @@ describe('quote_followup context, preview and send', () => {
const { db, c, m } = await world() const { db, c, m } = await world()
const q = await sentQuote(db, c, m, '2026-07-01T09:00:00Z') // never issued: docNo is null (F10) const q = await sentQuote(db, c, m, '2026-07-01T09:00:00Z') // never issued: docNo is null (F10)
await runDailyScan(db, deps, '2026-07-04') await runDailyScan(db, deps, '2026-07-04')
const { ctx } = await reminderContext(db, (await followups(db))[0]!, 'Tecnostac') const { ctx } = await reminderContext(db, (await followups(db))[0]!, 'SiMS')
const mail = reminderEmail('quote_followup', ctx) const mail = reminderEmail('quote_followup', ctx)
expect(mail.subject).not.toContain('null') expect(mail.subject).not.toContain('null')
expect(mail.bodyText).not.toContain('null') expect(mail.bodyText).not.toContain('null')
@ -225,7 +225,7 @@ describe('quote_followup context, preview and send', () => {
const q = await sentQuote(db, c, m, '2026-07-01T09:00:00Z') const q = await sentQuote(db, c, m, '2026-07-01T09:00:00Z')
const issued = await issueDocument(db, 'u1', q.id) const issued = await issueDocument(db, 'u1', q.id)
await runDailyScan(db, deps, '2026-07-04') await runDailyScan(db, deps, '2026-07-04')
const { ctx } = await reminderContext(db, (await followups(db))[0]!, 'Tecnostac') const { ctx } = await reminderContext(db, (await followups(db))[0]!, 'SiMS')
expect(reminderEmail('quote_followup', ctx).subject).toContain(issued.docNo!) expect(reminderEmail('quote_followup', ctx).subject).toContain(issued.docNo!)
}) })
it('send resolves the dated subject/body on the injected clock, not the wall clock', async () => { it('send resolves the dated subject/body on the injected clock, not the wall clock', async () => {
@ -238,9 +238,9 @@ describe('quote_followup context, preview and send', () => {
await sentQuote(db, c, m, '2026-07-01T09:00:00Z') await sentQuote(db, c, m, '2026-07-01T09:00:00Z')
await runDailyScan(db, deps, '2026-07-04') await runDailyScan(db, deps, '2026-07-04')
const rem = (await followups(db))[0]! const rem = (await followups(db))[0]!
const before = await reminderContext(db, rem, 'Tecnostac', '2026-07-10') const before = await reminderContext(db, rem, 'SiMS', '2026-07-10')
expect(reminderEmail('quote_followup', before.ctx).subject).not.toContain('FUTURE') expect(reminderEmail('quote_followup', before.ctx).subject).not.toContain('FUTURE')
const after = await reminderContext(db, rem, 'Tecnostac', '2026-07-11') const after = await reminderContext(db, rem, 'SiMS', '2026-07-11')
expect(reminderEmail('quote_followup', after.ctx).subject).toContain('FUTURE') expect(reminderEmail('quote_followup', after.ctx).subject).toContain('FUTURE')
}) })
it('refuses to send when share.base_url is unset — loud error, NO share minted, row still queued', async () => { it('refuses to send when share.base_url is unset — loud error, NO share minted, row still queued', async () => {
@ -284,7 +284,7 @@ describe('quote_followup context, preview and send', () => {
expect(out).toEqual({ ok: true }) expect(out).toEqual({ ok: true })
expect(await shareCount(db)).toBe(1) expect(await shareCount(db)).toBe(1)
// The sent body carries a live share link. // The sent body carries a live share link.
const { ctx } = await reminderContext(db, (await getReminder(db, first.id))!, 'Tecnostac') const { ctx } = await reminderContext(db, (await getReminder(db, first.id))!, 'SiMS')
expect(reminderEmail('quote_followup', ctx).bodyText).toContain('/share/') expect(reminderEmail('quote_followup', ctx).bodyText).toContain('/share/')
await runDailyScan(db, deps, '2026-07-08') // d7 await runDailyScan(db, deps, '2026-07-08') // d7
const second = (await followups(db)).find((r) => r.duePeriod === 'd7')! const second = (await followups(db)).find((r) => r.duePeriod === 'd7')!

@ -45,7 +45,7 @@ describe('payment receipts', () => {
it('renders a receipt acknowledgment (no SAC/GST table)', async () => { it('renders a receipt acknowledgment (no SAC/GST table)', async () => {
const { db, c, inv } = await setup() const { db, c, inv } = await setup()
const out = await recordPayment(db, 'u1', { clientId: c.id, receivedOn: '2026-07-10', mode: 'bank', amountPaise: 11_800_00, allocations: [{ documentId: inv.id, amountPaise: 11_800_00 }], issueReceipt: true }) const out = await recordPayment(db, 'u1', { clientId: c.id, receivedOn: '2026-07-10', mode: 'bank', amountPaise: 11_800_00, allocations: [{ documentId: inv.id, amountPaise: 11_800_00 }], issueReceipt: true })
const html = documentHtml(out.receipt!, c, { 'company.name': 'Tecnostac' }) const html = documentHtml(out.receipt!, c, { 'company.name': 'SiMS' })
expect(html).toContain('RECEIPT') expect(html).toContain('RECEIPT')
expect(html).toContain('Received with thanks') expect(html).toContain('Received with thanks')
expect(html).not.toContain('>SAC<') // the GST line table header is absent on receipts expect(html).not.toContain('>SAC<') // the GST line table header is absent on receipts

@ -11,7 +11,7 @@ import { upsertReminder } from '../src/repos-reminders'
import { apiRouter } from '../src/api' import { apiRouter } from '../src/api'
async function appWith() { async function appWith() {
const db = openDb(':memory:'); await seedIfEmpty(db) // company.name='Tecnostac' const db = openDb(':memory:'); await seedIfEmpty(db) // company.name='SiMS'
await createStaff(db, { email: 'owner@test.in', displayName: 'Owner', role: 'owner', password: 'owner-password' }) await createStaff(db, { email: 'owner@test.in', displayName: 'Owner', role: 'owner', password: 'owner-password' })
const c = await createClient(db, 'u1', { name: 'Acme', code: 'ACME', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] }) const c = await createClient(db, 'u1', { name: 'Acme', code: 'ACME', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] })
const m = await createModule(db, 'u1', { code: 'POS', name: 'POS' }) const m = await createModule(db, 'u1', { code: 'POS', name: 'POS' })
@ -40,8 +40,8 @@ describe('GET /reminders/:id/preview', () => {
const token = await login() const token = await login()
const r = await get(token, ctx.overdueId) const r = await get(token, ctx.overdueId)
expect(r.status).toBe(200) 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(ctx.inv.docNo) // 'Payment reminder — Invoice INV/26-27-0001 (SiMS)'
expect(r.json.subject).toContain('Tecnostac') expect(r.json.subject).toContain('SiMS')
expect(r.json.body).toContain('Acme') // clientName expect(r.json.body).toContain('Acme') // clientName
}) })
it('404s an unknown reminder and 400s an internal follow_up', async () => { it('404s an unknown reminder and 400s an internal follow_up', async () => {

@ -23,13 +23,13 @@ const okFetch = (async (url: string) =>
const deps: ScanDeps & SendReminderDeps = { const deps: ScanDeps & SendReminderDeps = {
gmail: { f: okFetch, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, gmail: { f: okFetch, clientId: 'cid', clientSecret: 'sec', keyHex: KEY },
renderPdf: async () => Buffer.from('%PDF-fake'), renderPdf: async () => Buffer.from('%PDF-fake'),
company: () => ({ 'company.name': 'Tecnostac' }), company: () => ({ 'company.name': 'SiMS' }),
now: () => '2026-07-10T09:00:00Z', now: () => '2026-07-10T09:00:00Z',
} }
async function world() { async function world() {
const db = openDb(':memory:'); await seedIfEmpty(db) const db = openDb(':memory:'); await seedIfEmpty(db)
await saveAccount(db, 'us@tecnostac.com', encrypt('refresh-token', KEY)) await saveAccount(db, 'us@sims.com', encrypt('refresh-token', KEY))
await setSetting(db, 'u1', 'share.base_url', 'https://hq.example.in') await setSetting(db, 'u1', 'share.base_url', 'https://hq.example.in')
const c = await createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] }) const c = await createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] })
const m = await createModule(db, 'u1', { code: 'CBS', name: 'Core Banking' }) const m = await createModule(db, 'u1', { code: 'CBS', name: 'Core Banking' })

@ -9,7 +9,7 @@ import {
describe('reminder templates', () => { describe('reminder templates', () => {
it('renders an overdue dunning email with the amount and a polite tone', () => { it('renders an overdue dunning email with the amount and a polite tone', () => {
const mail = reminderEmail('invoice_overdue', { const mail = reminderEmail('invoice_overdue', {
clientName: 'Malabar Stores', companyName: 'Tecnostac', clientName: 'Malabar Stores', companyName: 'SiMS',
docNo: 'INV/26-27-0007', amountPaise: 11_800_00, daysOverdue: 12, docNo: 'INV/26-27-0007', amountPaise: 11_800_00, daysOverdue: 12,
}) })
expect(mail.subject).toContain('INV/26-27-0007') expect(mail.subject).toContain('INV/26-27-0007')

@ -17,12 +17,12 @@ const okFetch = (async (url: string) =>
const deadFetch = (async () => new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400 })) as typeof fetch const deadFetch = (async () => new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400 })) as typeof fetch
function deps(f: typeof fetch): ScanDeps { function deps(f: typeof fetch): ScanDeps {
return { gmail: { f, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'Tecnostac' }), now: () => '2026-07-10T00:00:00Z' } return { gmail: { f, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'SiMS' }), now: () => '2026-07-10T00:00:00Z' }
} }
async function world() { async function world() {
const db = openDb(':memory:'); await seedIfEmpty(db) const db = openDb(':memory:'); await seedIfEmpty(db)
await saveAccount(db, 'us@tecnostac.com', encrypt('rt', KEY)) await saveAccount(db, 'us@sims.com', encrypt('rt', KEY))
const c = await createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'R', email: 'r@acme.in' }] }) const c = await createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'R', email: 'r@acme.in' }] })
const m = await createModule(db, 'u1', { code: 'CLOUD', name: 'Cloud', allowedKinds: ['monthly'] }) const m = await createModule(db, 'u1', { code: 'CLOUD', name: 'Cloud', allowedKinds: ['monthly'] })
await setPrice(db, 'u1', { moduleId: m.id, kind: 'monthly', pricePaise: 2_000_00, effectiveFrom: '2026-01-01' }) await setPrice(db, 'u1', { moduleId: m.id, kind: 'monthly', pricePaise: 2_000_00, effectiveFrom: '2026-01-01' })

@ -14,7 +14,7 @@ import { runDailyScan, type ScanDeps } from '../src/scheduler'
const deps: ScanDeps = { const deps: ScanDeps = {
gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' }, gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' },
renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'Tecnostac' }), renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'SiMS' }),
now: () => '2026-07-10T00:00:00Z', now: () => '2026-07-10T00:00:00Z',
} }
@ -112,7 +112,7 @@ describe('invoice_overdue — dN milestone escalation (Phase 9, spec §8)', () =
it('the email renders "N day(s) past due" from ctx.daysOverdue', async () => { it('the email renders "N day(s) past due" from ctx.daysOverdue', async () => {
const { db, c, inv } = await agedInvoice('2026-06-25') // 15 days before the 10th const { db, c, inv } = await agedInvoice('2026-06-25') // 15 days before the 10th
const { id } = await upsertReminder(db, { ruleKind: 'invoice_overdue', subjectId: inv.id, duePeriod: 'd15', clientId: c.id, docId: inv.id, now: '2026-07-10T00:00:00Z' }) const { id } = await upsertReminder(db, { ruleKind: 'invoice_overdue', subjectId: inv.id, duePeriod: 'd15', clientId: c.id, docId: inv.id, now: '2026-07-10T00:00:00Z' })
const { ctx } = await reminderContext(db, (await getReminder(db, id))!, 'Tecnostac', '2026-07-10') const { ctx } = await reminderContext(db, (await getReminder(db, id))!, 'SiMS', '2026-07-10')
expect(ctx.daysOverdue).toBe(15) expect(ctx.daysOverdue).toBe(15)
expect(reminderEmail('invoice_overdue', ctx).bodyText).toMatch(/15 day\(s\) past due/) expect(reminderEmail('invoice_overdue', ctx).bodyText).toMatch(/15 day\(s\) past due/)
}) })

@ -12,7 +12,7 @@ import { sendReminder, type SendReminderDeps } from '../src/send-reminder'
const KEY = '11'.repeat(32) const KEY = '11'.repeat(32)
const fakePdf = async () => Buffer.from('%PDF-fake') const fakePdf = async () => Buffer.from('%PDF-fake')
const company = () => ({ 'company.name': 'Tecnostac' }) const company = () => ({ 'company.name': 'SiMS' })
function deps(f: typeof fetch): SendReminderDeps { function deps(f: typeof fetch): SendReminderDeps {
return { gmail: { f, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, renderPdf: fakePdf, company, now: () => '2026-07-10T09:00:00Z' } return { gmail: { f, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, renderPdf: fakePdf, company, now: () => '2026-07-10T09:00:00Z' }
@ -20,7 +20,7 @@ function deps(f: typeof fetch): SendReminderDeps {
async function invoiceSetup() { async function invoiceSetup() {
const db = openDb(':memory:'); await seedIfEmpty(db) const db = openDb(':memory:'); await seedIfEmpty(db)
await saveAccount(db, 'us@tecnostac.com', encrypt('refresh-token', KEY)) await saveAccount(db, 'us@sims.com', encrypt('refresh-token', KEY))
const c = await createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] }) 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' }) 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' }) await setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-04-01' })

@ -30,10 +30,10 @@ describe('company profile settings', () => {
it('owner reads and updates company.* and it is audited', async () => { it('owner reads and updates company.* and it is audited', async () => {
const token = await login('owner@test.in', 'owner-password') const token = await login('owner@test.in', 'owner-password')
const before = await call(token, 'GET', '/settings/company') const before = await call(token, 'GET', '/settings/company')
expect(before.json.company['company.name']).toBe('Tecnostac') expect(before.json.company['company.name']).toBe('SiMS')
const put = await call(token, 'PUT', '/settings/company', { name: 'Tecnostac Pvt Ltd', phone: '0484-1234567', stateCode: '32' }) const put = await call(token, 'PUT', '/settings/company', { name: 'SiMS Pvt Ltd', phone: '0484-1234567', stateCode: '32' })
expect(put.status).toBe(200) expect(put.status).toBe(200)
expect(put.json.company['company.name']).toBe('Tecnostac Pvt Ltd') expect(put.json.company['company.name']).toBe('SiMS Pvt Ltd')
expect(put.json.company['company.phone']).toBe('0484-1234567') expect(put.json.company['company.phone']).toBe('0484-1234567')
const audits = (await ctx.db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM audit_log WHERE entity='setting' AND entity_id LIKE 'company.%'`))! const audits = (await ctx.db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM audit_log WHERE entity='setting' AND entity_id LIKE 'company.%'`))!
expect(audits.n).toBeGreaterThanOrEqual(3) expect(audits.n).toBeGreaterThanOrEqual(3)

@ -31,7 +31,7 @@ describe('template settings', () => {
it('owner GET returns company.* + template.*; PUT writes template fields + a title, each audited', async () => { it('owner GET returns company.* + template.*; PUT writes template fields + a title, each audited', async () => {
const token = await login('owner@test.in', 'owner-password') const token = await login('owner@test.in', 'owner-password')
const before = await call(token, 'GET', '/settings/template') const before = await call(token, 'GET', '/settings/template')
expect(before.json.settings['company.name']).toBe('Tecnostac') expect(before.json.settings['company.name']).toBe('SiMS')
const put = await call(token, 'PUT', '/settings/template', { const put = await call(token, 'PUT', '/settings/template', {
template: { footerNote: 'Thank you!', declaration: 'We declare…', signatoryLabel: 'Proprietor' }, template: { footerNote: 'Thank you!', declaration: 'We declare…', signatoryLabel: 'Proprietor' },
titles: { INVOICE: 'GST INVOICE' }, titles: { INVOICE: 'GST INVOICE' },

@ -14,7 +14,7 @@ const client = { id: 'c1', code: 'CL1', name: 'Acme', stateCode: '32', address:
describe('template.accent (validated hex, single source of truth)', () => { describe('template.accent (validated hex, single source of truth)', () => {
it('defaults to slate-indigo #334155 when unset', () => { it('defaults to slate-indigo #334155 when unset', () => {
const html = documentHtml(base, client, { 'company.name': 'Tecnostac' }) const html = documentHtml(base, client, { 'company.name': 'SiMS' })
expect(html).toContain('--accent:#334155') expect(html).toContain('--accent:#334155')
}) })
it('applies a valid hex accent verbatim (6- and 3-digit)', () => { it('applies a valid hex accent verbatim (6- and 3-digit)', () => {

@ -14,7 +14,7 @@ const client = { id: 'c1', code: 'CL1', name: 'Acme', stateCode: '32', address:
describe('template.* rendering (default-fallback)', () => { describe('template.* rendering (default-fallback)', () => {
it('renders built-in defaults when template.* is unset (nothing breaks pre-config)', () => { it('renders built-in defaults when template.* is unset (nothing breaks pre-config)', () => {
const html = documentHtml(invoice, client, { 'company.name': 'Tecnostac' }) const html = documentHtml(invoice, client, { 'company.name': 'SiMS' })
expect(html).toContain('TAX INVOICE') // built-in title expect(html).toContain('TAX INVOICE') // built-in title
expect(html).toContain('Authorised Signatory') // built-in signatory label expect(html).toContain('Authorised Signatory') // built-in signatory label
expect(html).not.toContain('class="declaration"') expect(html).not.toContain('class="declaration"')
@ -24,7 +24,7 @@ describe('template.* rendering (default-fallback)', () => {
}) })
it('applies each template.* override where set', () => { it('applies each template.* override where set', () => {
const html = documentHtml(invoice, client, { const html = documentHtml(invoice, client, {
'company.name': 'Tecnostac', 'company.name': 'SiMS',
'template.title_INVOICE': 'GST INVOICE', 'template.title_INVOICE': 'GST INVOICE',
'template.declaration': 'We declare the particulars are true.', 'template.declaration': 'We declare the particulars are true.',
'template.jurisdiction': 'Subject to Kochi jurisdiction', 'template.jurisdiction': 'Subject to Kochi jurisdiction',

@ -12,7 +12,7 @@ const doc = {
} as never } as never
const client = { id: 'c1', code: 'CL0001', name: 'Acme', stateCode: '32', address: 'Kochi', const client = { id: 'c1', code: 'CL0001', name: 'Acme', stateCode: '32', address: 'Kochi',
contacts: [], status: 'active', notes: '' } as never contacts: [], status: 'active', notes: '' } as never
const company = { 'company.name': 'Tecnostac', 'company.gstin': '', 'company.address': '', const company = { 'company.name': 'SiMS', 'company.gstin': '', 'company.address': '',
'company.phone': '', 'company.email': '', 'company.bank': 'HDFC ****1234' } 'company.phone': '', 'company.email': '', 'company.bank': 'HDFC ****1234' }
describe('document html', () => { describe('document html', () => {

@ -59,7 +59,7 @@ npm i -g pm2
env $(grep -v '^#' apps/hq/.env | xargs) pm2 start apps/hq/dist/server.cjs --name sims-hq env $(grep -v '^#' apps/hq/.env | xargs) pm2 start apps/hq/dist/server.cjs --name sims-hq
pm2 save && pm2 startup pm2 save && pm2 startup
``` ```
Owner login on first boot is `admin@tecnostac.com` + the printed password (change it Owner login on first boot is `admin@sims.com` + the printed password (change it
immediately). The Gmail-disconnected banner is expected until step 6. immediately). The Gmail-disconnected banner is expected until step 6.
## 4. Put HTTPS in front (never expose Node directly) ## 4. Put HTTPS in front (never expose Node directly)

@ -411,9 +411,9 @@ import { createStaff, login, verifySession } from '../src/auth'
describe('hq auth', () => { describe('hq auth', () => {
it('logs in with correct password, rejects wrong one', () => { it('logs in with correct password, rejects wrong one', () => {
const db = openDb(':memory:') const db = openDb(':memory:')
createStaff(db, { email: 'admin@tecnostac.com', displayName: 'Owner', role: 'owner', password: 'let-me-in-9' }) createStaff(db, { email: 'admin@sims.com', displayName: 'Owner', role: 'owner', password: 'let-me-in-9' })
expect(login(db, 'admin@tecnostac.com', 'wrong')).toBeNull() expect(login(db, 'admin@sims.com', 'wrong')).toBeNull()
const ok = login(db, 'admin@tecnostac.com', 'let-me-in-9') const ok = login(db, 'admin@sims.com', 'let-me-in-9')
expect(ok).not.toBeNull() expect(ok).not.toBeNull()
const staff = verifySession(db, ok!.token) const staff = verifySession(db, ok!.token)
expect(staff).toMatchObject({ role: 'owner' }) expect(staff).toMatchObject({ role: 'owner' })
@ -912,7 +912,7 @@ const doc = {
} as never } as never
const client = { id: 'c1', code: 'CL0001', name: 'Acme', stateCode: '32', address: 'Kochi', const client = { id: 'c1', code: 'CL0001', name: 'Acme', stateCode: '32', address: 'Kochi',
contacts: [], status: 'active', notes: '' } as never contacts: [], status: 'active', notes: '' } as never
const company = { 'company.name': 'Tecnostac', 'company.gstin': '', 'company.address': '', const company = { 'company.name': 'SiMS', 'company.gstin': '', 'company.address': '',
'company.phone': '', 'company.email': '', 'company.bank': 'HDFC ****1234' } 'company.phone': '', 'company.email': '', 'company.bank': 'HDFC ****1234' }
describe('document html', () => { describe('document html', () => {
@ -1031,7 +1031,7 @@ describe('gmail connect', () => {
**Interfaces:** **Interfaces:**
- Produces: - Produces:
- `seedIfEmpty(db): void` — if no `staff_user`: owner `admin@tecnostac.com` with a random 12-char password **printed to console once**; settings `company.*` placeholders + `company.state_code='32'`; `tax_class` row `('GST18', 1800, '2017-07-01')`. - `seedIfEmpty(db): void` — if no `staff_user`: owner `admin@sims.com` with a random 12-char password **printed to console once**; settings `company.*` placeholders + `company.state_code='32'`; `tax_class` row `('GST18', 1800, '2017-07-01')`.
- `stageCsv(db, kind: 'clients'|'invoices', csvText: string): { staged: number }` — tiny CSV parser (header row, comma, double-quote escaping — ~20 lines, no dependency); per-row problems collected into `problems` JSON: missing name, GSTIN checksum failure, bad date, duplicate code/doc_no. - `stageCsv(db, kind: 'clients'|'invoices', csvText: string): { staged: number }` — tiny CSV parser (header row, comma, double-quote escaping — ~20 lines, no dependency); per-row problems collected into `problems` JSON: missing name, GSTIN checksum failure, bad date, duplicate code/doc_no.
- `verificationReport(db): { clients: { staged: number; problems: number }; invoices: { staged: number; problems: number; totalPaise: number }; samples: { firstClients: string[]; lastInvoices: string[] } }` - `verificationReport(db): { clients: { staged: number; problems: number }; invoices: { staged: number; problems: number; totalPaise: number }; samples: { firstClients: string[]; lastInvoices: string[] } }`
- `commitImport(db, userId): { clients: number; invoices: number; seeded: { fy: string; lastSeq: number } | null }` — inserts clients (`source='apex'`), invoices as issued documents (`source='apex'`, status `paid` when `paid=1` else `sent`, payload lines empty — history-only), then **seeds the INVOICE series**: parse the max numeric tail of current-FY staged doc numbers → `seedSeries(db,'INVOICE', fy, lastSeq)`. Refuses to run when any staged row has problems. - `commitImport(db, userId): { clients: number; invoices: number; seeded: { fy: string; lastSeq: number } | null }` — inserts clients (`source='apex'`), invoices as issued documents (`source='apex'`, status `paid` when `paid=1` else `sent`, payload lines empty — history-only), then **seeds the INVOICE series**: parse the max numeric tail of current-FY staged doc numbers → `seedSeries(db,'INVOICE', fy, lastSeq)`. Refuses to run when any staged row has problems.

@ -1001,7 +1001,7 @@ import {
describe('reminder templates', () => { describe('reminder templates', () => {
it('renders an overdue dunning email with the amount and a polite tone', () => { it('renders an overdue dunning email with the amount and a polite tone', () => {
const mail = reminderEmail('invoice_overdue', { const mail = reminderEmail('invoice_overdue', {
clientName: 'Malabar Stores', companyName: 'Tecnostac', clientName: 'Malabar Stores', companyName: 'SiMS',
docNo: 'INV/26-27-0007', amountPaise: 11_800_00, daysOverdue: 12, docNo: 'INV/26-27-0007', amountPaise: 11_800_00, daysOverdue: 12,
}) })
expect(mail.subject).toContain('INV/26-27-0007') expect(mail.subject).toContain('INV/26-27-0007')
@ -1302,7 +1302,7 @@ import { sendReminder, type SendReminderDeps } from '../src/send-reminder'
const KEY = '11'.repeat(32) const KEY = '11'.repeat(32)
const fakePdf = async () => Buffer.from('%PDF-fake') const fakePdf = async () => Buffer.from('%PDF-fake')
const company = () => ({ 'company.name': 'Tecnostac' }) const company = () => ({ 'company.name': 'SiMS' })
function deps(f: typeof fetch): SendReminderDeps { function deps(f: typeof fetch): SendReminderDeps {
return { gmail: { f, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, renderPdf: fakePdf, company, now: () => '2026-07-10T09:00:00Z' } return { gmail: { f, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, renderPdf: fakePdf, company, now: () => '2026-07-10T09:00:00Z' }
@ -1310,7 +1310,7 @@ function deps(f: typeof fetch): SendReminderDeps {
function invoiceSetup() { function invoiceSetup() {
const db = openDb(':memory:'); seedIfEmpty(db) const db = openDb(':memory:'); seedIfEmpty(db)
saveAccount(db, 'us@tecnostac.com', encrypt('refresh-token', KEY)) saveAccount(db, 'us@sims.com', encrypt('refresh-token', KEY))
const c = createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] }) const c = createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'Ravi', email: 'ravi@acme.in' }] })
const m = createModule(db, 'u1', { code: 'POS', name: 'POS' }) 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' }) setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-04-01' })
@ -1587,7 +1587,7 @@ import { runDailyScan, type ScanDeps } from '../src/scheduler'
const deps: ScanDeps = { const deps: ScanDeps = {
gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' }, gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' },
renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'Tecnostac' }), renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'SiMS' }),
now: () => '2026-07-10T00:00:00Z', now: () => '2026-07-10T00:00:00Z',
} }
@ -1789,12 +1789,12 @@ const okFetch = (async (url: string) =>
const deadFetch = (async () => new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400 })) as typeof fetch const deadFetch = (async () => new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400 })) as typeof fetch
function deps(f: typeof fetch): ScanDeps { function deps(f: typeof fetch): ScanDeps {
return { gmail: { f, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'Tecnostac' }), now: () => '2026-07-10T00:00:00Z' } return { gmail: { f, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'SiMS' }), now: () => '2026-07-10T00:00:00Z' }
} }
function world() { function world() {
const db = openDb(':memory:'); seedIfEmpty(db) const db = openDb(':memory:'); seedIfEmpty(db)
saveAccount(db, 'us@tecnostac.com', encrypt('rt', KEY)) saveAccount(db, 'us@sims.com', encrypt('rt', KEY))
const c = createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'R', email: 'r@acme.in' }] }) const c = createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'R', email: 'r@acme.in' }] })
const m = createModule(db, 'u1', { code: 'CLOUD', name: 'Cloud', allowedKinds: ['monthly'] }) const m = createModule(db, 'u1', { code: 'CLOUD', name: 'Cloud', allowedKinds: ['monthly'] })
setPrice(db, 'u1', { moduleId: m.id, kind: 'monthly', pricePaise: 2_000_00, effectiveFrom: '2026-01-01' }) setPrice(db, 'u1', { moduleId: m.id, kind: 'monthly', pricePaise: 2_000_00, effectiveFrom: '2026-01-01' })
@ -2010,7 +2010,7 @@ function bounceFetch(recipient: string): typeof fetch {
describe('pollBounces', () => { describe('pollBounces', () => {
it('flips the sent email_log to bounced and raises an email_bounced reminder, idempotently', async () => { it('flips the sent email_log to bounced and raises an email_bounced reminder, idempotently', async () => {
const db = openDb(':memory:'); seedIfEmpty(db) const db = openDb(':memory:'); seedIfEmpty(db)
saveAccount(db, 'us@tecnostac.com', encrypt('rt', KEY)) saveAccount(db, 'us@sims.com', encrypt('rt', KEY))
logEmail(db, { to: 'ravi@acme.in', subject: 'INV/26-27-0001', status: 'sent', gmailMessageId: 'g1' }) logEmail(db, { to: 'ravi@acme.in', subject: 'INV/26-27-0001', status: 'sent', gmailMessageId: 'g1' })
const deps: BounceDeps = { const deps: BounceDeps = {
gmail: { f: bounceFetch('ravi@acme.in'), clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, gmail: { f: bounceFetch('ravi@acme.in'), clientId: 'cid', clientSecret: 'sec', keyHex: KEY },
@ -2198,7 +2198,7 @@ import { runDailyScan, type ScanDeps } from '../src/scheduler'
const scanDeps: ScanDeps = { const scanDeps: ScanDeps = {
gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' }, gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' },
renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'Tecnostac' }), now: () => '2026-07-10T00:00:00Z', renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'SiMS' }), now: () => '2026-07-10T00:00:00Z',
} }
function seeded() { function seeded() {

@ -945,7 +945,7 @@ describe('payment receipts', () => {
it('renders a receipt acknowledgment (no SAC/GST table)', () => { it('renders a receipt acknowledgment (no SAC/GST table)', () => {
const { db, c, inv } = setup() const { db, c, inv } = setup()
const out = recordPayment(db, 'u1', { clientId: c.id, receivedOn: '2026-07-10', mode: 'bank', amountPaise: 11_800_00, allocations: [{ documentId: inv.id, amountPaise: 11_800_00 }], issueReceipt: true }) const out = recordPayment(db, 'u1', { clientId: c.id, receivedOn: '2026-07-10', mode: 'bank', amountPaise: 11_800_00, allocations: [{ documentId: inv.id, amountPaise: 11_800_00 }], issueReceipt: true })
const html = documentHtml(out.receipt!, c, { 'company.name': 'Tecnostac' }) const html = documentHtml(out.receipt!, c, { 'company.name': 'SiMS' })
expect(html).toContain('RECEIPT') expect(html).toContain('RECEIPT')
expect(html).toContain('Received with thanks') expect(html).toContain('Received with thanks')
expect(html).not.toContain('>SAC<') // the GST line table header is absent on receipts expect(html).not.toContain('>SAC<') // the GST line table header is absent on receipts
@ -1225,10 +1225,10 @@ describe('company profile settings', () => {
it('owner reads and updates company.* and it is audited', async () => { it('owner reads and updates company.* and it is audited', async () => {
const token = await login('owner@test.in', 'owner-password') const token = await login('owner@test.in', 'owner-password')
const before = await call(token, 'GET', '/settings/company') const before = await call(token, 'GET', '/settings/company')
expect(before.json.company['company.name']).toBe('Tecnostac') expect(before.json.company['company.name']).toBe('SiMS')
const put = await call(token, 'PUT', '/settings/company', { name: 'Tecnostac Pvt Ltd', phone: '0484-1234567', stateCode: '32' }) const put = await call(token, 'PUT', '/settings/company', { name: 'SiMS Pvt Ltd', phone: '0484-1234567', stateCode: '32' })
expect(put.status).toBe(200) expect(put.status).toBe(200)
expect(put.json.company['company.name']).toBe('Tecnostac Pvt Ltd') expect(put.json.company['company.name']).toBe('SiMS Pvt Ltd')
expect(put.json.company['company.phone']).toBe('0484-1234567') expect(put.json.company['company.phone']).toBe('0484-1234567')
const audits = db.prepare(`SELECT COUNT(*) AS n FROM audit_log WHERE entity='setting' AND entity_id LIKE 'company.%'`).get() as { n: number } const audits = db.prepare(`SELECT COUNT(*) AS n FROM audit_log WHERE entity='setting' AND entity_id LIKE 'company.%'`).get() as { n: number }
expect(audits.n).toBeGreaterThanOrEqual(3) expect(audits.n).toBeGreaterThanOrEqual(3)

@ -562,7 +562,7 @@ const client = { id: 'c1', code: 'CL1', name: 'Acme', stateCode: '32', address:
describe('template.* rendering (default-fallback)', () => { describe('template.* rendering (default-fallback)', () => {
it('renders built-in defaults when template.* is unset (nothing breaks pre-config)', () => { it('renders built-in defaults when template.* is unset (nothing breaks pre-config)', () => {
const html = documentHtml(invoice, client, { 'company.name': 'Tecnostac' }) const html = documentHtml(invoice, client, { 'company.name': 'SiMS' })
expect(html).toContain('TAX INVOICE') // built-in title expect(html).toContain('TAX INVOICE') // built-in title
expect(html).toContain('Authorised Signatory') // built-in signatory label expect(html).toContain('Authorised Signatory') // built-in signatory label
expect(html).not.toContain('class="declaration"') expect(html).not.toContain('class="declaration"')
@ -572,7 +572,7 @@ describe('template.* rendering (default-fallback)', () => {
}) })
it('applies each template.* override where set', () => { it('applies each template.* override where set', () => {
const html = documentHtml(invoice, client, { const html = documentHtml(invoice, client, {
'company.name': 'Tecnostac', 'company.name': 'SiMS',
'template.title_INVOICE': 'GST INVOICE', 'template.title_INVOICE': 'GST INVOICE',
'template.declaration': 'We declare the particulars are true.', 'template.declaration': 'We declare the particulars are true.',
'template.jurisdiction': 'Subject to Kochi jurisdiction', 'template.jurisdiction': 'Subject to Kochi jurisdiction',
@ -719,7 +719,7 @@ const sampleClient = { id: 'c1', code: 'X', name: 'X', stateCode: '32', address:
describe('templates: screen paper styles + documentHtmlSample', () => { describe('templates: screen paper styles + documentHtmlSample', () => {
it('documentHtml carries screen-only paper styles (PDF unaffected — print media)', () => { it('documentHtml carries screen-only paper styles (PDF unaffected — print media)', () => {
expect(documentHtml(sampleDoc, sampleClient, { 'company.name': 'Tecnostac' })).toContain('@media screen') expect(documentHtml(sampleDoc, sampleClient, { 'company.name': 'SiMS' })).toContain('@media screen')
}) })
it('documentHtmlSample renders letterhead, bank box and given bullets through the one renderer', () => { it('documentHtmlSample renders letterhead, bank box and given bullets through the one renderer', () => {
const html = documentHtmlSample( const html = documentHtmlSample(
@ -950,7 +950,7 @@ describe('template settings', () => {
it('owner GET returns company.* + template.*; PUT writes template fields + a title, each audited', async () => { it('owner GET returns company.* + template.*; PUT writes template fields + a title, each audited', async () => {
const token = await login('owner@test.in', 'owner-password') const token = await login('owner@test.in', 'owner-password')
const before = await call(token, 'GET', '/settings/template') const before = await call(token, 'GET', '/settings/template')
expect(before.json.settings['company.name']).toBe('Tecnostac') expect(before.json.settings['company.name']).toBe('SiMS')
const put = await call(token, 'PUT', '/settings/template', { const put = await call(token, 'PUT', '/settings/template', {
template: { footerNote: 'Thank you!', declaration: 'We declare…', signatoryLabel: 'Proprietor' }, template: { footerNote: 'Thank you!', declaration: 'We declare…', signatoryLabel: 'Proprietor' },
titles: { INVOICE: 'GST INVOICE' }, titles: { INVOICE: 'GST INVOICE' },
@ -1086,7 +1086,7 @@ import { upsertReminder } from '../src/repos-reminders'
import { apiRouter } from '../src/api' import { apiRouter } from '../src/api'
function appWith() { function appWith() {
const db = openDb(':memory:'); seedIfEmpty(db) // company.name='Tecnostac' const db = openDb(':memory:'); seedIfEmpty(db) // company.name='SiMS'
createStaff(db, { email: 'owner@test.in', displayName: 'Owner', role: 'owner', password: 'owner-password' }) 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 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' }) const m = createModule(db, 'u1', { code: 'POS', name: 'POS' })
@ -1114,8 +1114,8 @@ describe('GET /reminders/:id/preview', () => {
const token = await login() const token = await login()
const r = await get(token, ctx.overdueId) const r = await get(token, ctx.overdueId)
expect(r.status).toBe(200) 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(ctx.inv.docNo) // 'Payment reminder — Invoice INV/26-27-0001 (SiMS)'
expect(r.json.subject).toContain('Tecnostac') expect(r.json.subject).toContain('SiMS')
expect(r.json.body).toContain('Acme') // clientName expect(r.json.body).toContain('Acme') // clientName
}) })
it('404s an unknown reminder and 400s an internal follow_up', async () => { it('404s an unknown reminder and 400s an internal follow_up', async () => {
@ -2121,7 +2121,7 @@ function QueueActions(props: { rem: Reminder & { docNo: string | null }; onDone:
``` ```
- [ ] **Step 2: Browser-verify** (owner login → **Dashboard**, with at least one sendable reminder in the queue — an overdue invoice reminder is easiest to seed): - [ ] **Step 2: Browser-verify** (owner login → **Dashboard**, with at least one sendable reminder in the queue — an overdue invoice reminder is easiest to seed):
- Click **Preview** on an overdue reminder → the exact subject (e.g. `Payment reminder — Invoice INV/… (Tecnostac)`) and body appear inline, matching what Send would email; **Hide** collapses it. - Click **Preview** on an overdue reminder → the exact subject (e.g. `Payment reminder — Invoice INV/… (SiMS)`) and body appear inline, matching what Send would email; **Hide** collapses it.
- Internal-only rows (follow-up / bounced) show no Preview/Send button (only Dismiss), unchanged. - Internal-only rows (follow-up / bounced) show no Preview/Send button (only Dismiss), unchanged.
- [ ] **Step 3: Typecheck + build + commit**`npm run typecheck` + `npm run build -w @sims/hq-web` clean. - [ ] **Step 3: Typecheck + build + commit**`npm run typecheck` + `npm run build -w @sims/hq-web` clean.

@ -26,7 +26,7 @@
**Verify commands** (run from repo root `C:\SiMS\hq`): **Verify commands** (run from repo root `C:\SiMS\hq`):
- Typecheck: `npm run typecheck` → exits 0 - Typecheck: `npm run typecheck` → exits 0
- Tests: `npm test` → all pass - Tests: `npm test` → all pass
- Dev app: terminal A `cd apps/hq && npm start` (API :5182), terminal B `cd apps/hq-web && npm run dev` (Vite :5183). Login `admin@tecnostac.com` (owner password known to the user; ask them if a login is needed for visual checks). - Dev app: terminal A `cd apps/hq && npm start` (API :5182), terminal B `cd apps/hq-web && npm run dev` (Vite :5183). Login `admin@sims.com` (owner password known to the user; ask them if a login is needed for visual checks).
--- ---

@ -7,7 +7,7 @@
*Approved direction: "system upgrade" — Direction A v2's system discipline wearing H-04's *Approved direction: "system upgrade" — Direction A v2's system discipline wearing H-04's
warm palette. Sources reviewed: `admin_design_v2.html` (a Claude Design handoff tar; the warm palette. Sources reviewed: `admin_design_v2.html` (a Claude Design handoff tar; the
chosen "Direction A v2" system), `H-04 Client 360 (timeline ribbon).html`, `Tecnostac chosen "Direction A v2" system), `H-04 Client 360 (timeline ribbon).html`, `SiMS
Unified Workspace` promo, the small APEX card/loan-detail mockups, and the current Unified Workspace` promo, the small APEX card/loan-detail mockups, and the current
`@sims/ui` tokens. Companion docs: `2026-07-17-apex-82963-ux-reference.md` (UX checklist `@sims/ui` tokens. Companion docs: `2026-07-17-apex-82963-ux-reference.md` (UX checklist
honoured throughout), `2026-07-17-quote-to-close-funnel-design.md` (pages this shell must honoured throughout), `2026-07-17-quote-to-close-funnel-design.md` (pages this shell must

@ -108,7 +108,7 @@ rebuildTable(db, table, newDDL, guardToken):
Employees [ + Add employee ] Employees [ + Add employee ]
──────────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────────────
Name Email Role Active Actions Name Email Role Active Actions
Asha Rao asha@tecnostac.com owner ● Edit · Reset pw Asha Rao asha@sims.com owner ● Edit · Reset pw
Vikram Shah vikram@… manager ● Edit · Reset pw · Deactivate Vikram Shah vikram@… manager ● Edit · Reset pw · Deactivate
Priya Nair priya@… staff ○ Edit · Reset pw · Reactivate Priya Nair priya@… staff ○ Edit · Reset pw · Reactivate
──────────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────────────

Loading…
Cancel
Save