import { computeRucDv } from '@impuestos/rules'; import { uuidv7 } from 'uuidv7'; import type { Auth, Role } from '../auth/options'; import { writeAudit } from '../modules/audit'; import { CONSENT_TEXT_VERSION } from '../modules/pii'; import type { DbHandle } from './index'; import { seedDocuments } from './seed-documents'; export interface SeedAccount { email: string; password: string; name: string; role: Role; } /** * Accounts per CONTRACTS.md section 4. Deterministic and idempotent: running the seed * twice leaves the same rows. */ export const SEED_ACCOUNTS: readonly SeedAccount[] = [ { email: 'superadmin@demo.local', password: 'demo-superadmin-1', name: 'Super Admin', role: 'superadmin' }, { email: 'staff@demo.local', password: 'demo-staff-1', name: 'Staff Demo', role: 'staff' }, { email: 'maria@demo.local', password: 'demo-maria-1', name: 'Maria Gonzalez', role: 'user' }, { email: 'carlos@demo.local', password: 'demo-carlos-1', name: 'Carlos Benitez', role: 'user' }, ]; /** * The showcase account: an individual whose RUC is her CI plus a check digit, registered * for both IVA and IRP. Base 4123456 ends in 6, so her filing day is the 19th, which is * the example FLOWS.md uses throughout. */ export const MARIA_RUC_BASE = '4123456'; /** IVA only, to exercise the view with no IRP anywhere in it. */ export const CARLOS_RUC_BASE = '80012345'; const SEEDED_AT = '2026-01-15T12:00:00.000Z'; export interface SeedResult { created: string[]; existing: string[]; } export async function seed(handle: DbHandle, auth: Auth): Promise { const result: SeedResult = { created: [], existing: [] }; const db = handle.db; for (const account of SEED_ACCOUNTS) { const found = await db .selectFrom('user') .select('id') .where('email', '=', account.email) .executeTakeFirst(); if (found) { result.existing.push(account.email); continue; } await auth.api.signUpEmail({ body: { email: account.email, password: account.password, name: account.name }, }); // Roles and verification are set directly: the sign up endpoint always creates a // plain unverified `user`, and demo accounts need to be usable straight away. await db .updateTable('user') .set({ role: account.role, emailVerified: 1, updatedAt: SEEDED_AT }) .where('email', '=', account.email) .execute(); result.created.push(account.email); } await seedProfiles(handle); await seedDocuments(handle); await seedAuditTrail(handle); return result; } async function seedProfiles(handle: DbHandle): Promise { const db = handle.db; const maria = await userIdFor(handle, 'maria@demo.local'); const carlos = await userIdFor(handle, 'carlos@demo.local'); await upsertProfileRow(handle, { userId: maria, fullName: 'Maria Gonzalez', docType: 'ruc', ruc: MARIA_RUC_BASE, rucDv: String(computeRucDv(MARIA_RUC_BASE)), ci: MARIA_RUC_BASE, taxpayerKind: 'individual', obligations: [ { code: 'iva_120', active: true, since: '2024-01-01' }, { code: 'irp_515', active: true, since: '2024-01-01' }, ], irpGrossEstimate: 180_000_000, }); await upsertProfileRow(handle, { userId: carlos, fullName: 'Benitez y Asociados SRL', docType: 'ruc', ruc: CARLOS_RUC_BASE, rucDv: String(computeRucDv(CARLOS_RUC_BASE)), ci: null, taxpayerKind: 'company', obligations: [{ code: 'iva_120', active: true, since: '2023-06-01' }], irpGrossEstimate: null, }); const hasDependent = await db .selectFrom('dependents') .select('id') .where('user_id', '=', maria) .executeTakeFirst(); if (!hasDependent) { await db .insertInto('dependents') .values({ id: uuidv7(), user_id: maria, display_name: 'Lucas Gonzalez', relationship: 'hijo', doc_number: null, active: 1, created_at: SEEDED_AT, updated_at: SEEDED_AT, }) .execute(); } for (const userId of [maria, carlos]) { for (const kind of ['data_processing', 'notifications'] as const) { const existing = await db .selectFrom('consents') .select('id') .where('user_id', '=', userId) .where('kind', '=', kind) .executeTakeFirst(); if (existing) continue; await db .insertInto('consents') .values({ id: uuidv7(), user_id: userId, kind, granted_at: SEEDED_AT, revoked_at: null, text_version: CONSENT_TEXT_VERSION, }) .execute(); } const prefs = await db .selectFrom('notification_prefs') .select('user_id') .where('user_id', '=', userId) .executeTakeFirst(); if (!prefs) { await db .insertInto('notification_prefs') .values({ user_id: userId, push_enabled: 0, email_enabled: 1, telegram_chat_id: null, digest_hour: 9, }) .execute(); } } } /** CONTRACTS.md section 4: the audit table starts with the role assignments. */ async function seedAuditTrail(handle: DbHandle): Promise { const existing = await handle.db .selectFrom('audit_log') .select('id') .where('action', '=', 'admin.role_change') .executeTakeFirst(); if (existing) return; const superadmin = await userIdFor(handle, 'superadmin@demo.local'); for (const email of ['staff@demo.local'] as const) { await writeAudit(handle.db, { actorUserId: superadmin, actorRole: 'superadmin', subjectUserId: await userIdFor(handle, email), action: 'admin.role_change', resource: 'user', detail: { role: 'staff', seeded: true }, }); } } interface ProfileSeed { userId: string; fullName: string; docType: 'ruc' | 'ci'; ruc: string | null; rucDv: string | null; ci: string | null; taxpayerKind: 'individual' | 'company'; obligations: { code: 'iva_120' | 'irp_515'; active: boolean; since: string }[]; irpGrossEstimate: number | null; } async function upsertProfileRow(handle: DbHandle, seed: ProfileSeed): Promise { const existing = await handle.db .selectFrom('profiles') .select('user_id') .where('user_id', '=', seed.userId) .executeTakeFirst(); if (existing) return; const base = seed.ruc ?? seed.ci; if (!base) throw new Error(`seed profile for ${seed.fullName} needs a RUC or a CI`); await handle.db .insertInto('profiles') .values({ user_id: seed.userId, full_name: seed.fullName, doc_type: seed.docType, ruc: seed.ruc, ruc_dv: seed.rucDv, ci: seed.ci, taxpayer_kind: seed.taxpayerKind, deadline_digit: Number(base[base.length - 1]), obligations: JSON.stringify(seed.obligations), irp_gross_estimate: seed.irpGrossEstimate, auto_confirm_days: 7, locale: 'es', created_at: SEEDED_AT, updated_at: SEEDED_AT, }) .execute(); } async function userIdFor(handle: DbHandle, email: string): Promise { const row = await handle.db .selectFrom('user') .select('id') .where('email', '=', email) .executeTakeFirstOrThrow(); return row.id; }