From 54895c0bd2f286243aeffe1369366d3fbee91519 Mon Sep 17 00:00:00 2001 From: whekin Date: Tue, 10 Mar 2026 18:50:50 +0400 Subject: [PATCH] refactor(db): replace legacy fixture seed --- docs/runbooks/dev-reset.md | 30 +++ docs/runbooks/dev-setup.md | 2 + packages/db/src/seed.ts | 492 ++++++++++++++++++++++--------------- 3 files changed, 328 insertions(+), 196 deletions(-) create mode 100644 docs/runbooks/dev-reset.md diff --git a/docs/runbooks/dev-reset.md b/docs/runbooks/dev-reset.md new file mode 100644 index 0000000..53089df --- /dev/null +++ b/docs/runbooks/dev-reset.md @@ -0,0 +1,30 @@ +# Development Reset + +## Warning + +This workflow is destructive for development data. Do not use it against any database you care about. + +## Fixture refresh + +`bun run db:seed` replaces the committed fixture household with a fresh multi-household-compatible dataset. + +Use it when: + +- you want current demo data without wiping the entire database +- you only need the seeded fixture household reset + +## Full dev reset + +Use a disposable local or dev database only. + +Recommended flow: + +1. point `DATABASE_URL` at a disposable database +2. recreate the database or reset the schema using your database host tooling +3. run `bun run db:migrate` +4. run `bun run db:seed` + +## Notes + +- The committed seed reflects the current product model: multi-household setup, GEL settlement, USD-denominated rent with locked FX, topic bindings, utility categories, and payment confirmations. +- If you need historical or household-specific fixtures, create them as separate scripts instead of editing old migrations or mutating the default seed ad hoc. diff --git a/docs/runbooks/dev-setup.md b/docs/runbooks/dev-setup.md index 01f16d5..e919f2b 100644 --- a/docs/runbooks/dev-setup.md +++ b/docs/runbooks/dev-setup.md @@ -61,6 +61,7 @@ bun run review:coderabbit - Drizzle config is in `packages/db/drizzle.config.ts`. - Typed environment validation lives in `packages/config/src/env.ts`. - Copy `.env.example` to `.env` before running app/database commands. +- `bun run db:seed` refreshes the committed fixture household and is destructive for previously seeded fixture rows. - Local bot feature flags come from env presence: - finance commands require `DATABASE_URL` plus household setup in Telegram via `/setup` - purchase ingestion requires `DATABASE_URL` plus a bound purchase topic via `/bind_purchase_topic` @@ -69,6 +70,7 @@ bun run review:coderabbit and optionally use a dedicated reminders topic via `/bind_reminders_topic` - mini app CORS can be constrained with `MINI_APP_ALLOWED_ORIGINS` - Migration workflow is documented in `docs/runbooks/migrations.md`. +- Destructive dev reset guidance is documented in `docs/runbooks/dev-reset.md`. - First deploy flow is documented in `docs/runbooks/first-deploy.md`. ## CI/CD diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 53bae7c..c75c026 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -1,11 +1,16 @@ -import { and, eq } from 'drizzle-orm' +import { eq, inArray } from 'drizzle-orm' import { createDbClient } from './client' import { + billingCycleExchangeRates, billingCycles, + householdBillingSettings, householdTelegramChats, householdTopicBindings, + householdUtilityCategories, households, members, + paymentConfirmations, + paymentRecords, presenceOverrides, processedBotMessages, purchaseEntries, @@ -25,232 +30,327 @@ const { db, queryClient } = createDbClient(databaseUrl, { prepare: false }) +const LEGACY_FIXTURE_HOUSEHOLD_IDS = ['11111111-1111-4111-8111-111111111111'] as const + const FIXTURE_IDS = { - household: '11111111-1111-4111-8111-111111111111', - cycle: '22222222-2222-4222-8222-222222222222', - memberAlice: '33333333-3333-4333-8333-333333333331', - memberBob: '33333333-3333-4333-8333-333333333332', - memberCarol: '33333333-3333-4333-8333-333333333333', - settlement: '44444444-4444-4444-8444-444444444444' + household: '9f9aa111-1111-4c11-8111-111111111111', + cycle: '9f9aa222-2222-4c22-8222-222222222222', + memberDima: '9f9aa333-3333-4c33-8333-333333333331', + memberStas: '9f9aa333-3333-4c33-8333-333333333332', + memberIon: '9f9aa333-3333-4c33-8333-333333333333', + settlement: '9f9aa444-4444-4c44-8444-444444444444', + paymentConfirmation: '9f9aa555-5555-4c55-8555-555555555555' } as const async function seed(): Promise { await db - .insert(households) - .values({ - id: FIXTURE_IDS.household, - name: 'Kojori Demo Household' - }) - .onConflictDoNothing() + .delete(households) + .where(inArray(households.id, [...LEGACY_FIXTURE_HOUSEHOLD_IDS, FIXTURE_IDS.household])) - await db - .insert(members) - .values([ - { - id: FIXTURE_IDS.memberAlice, - householdId: FIXTURE_IDS.household, - telegramUserId: '10001', - displayName: 'Alice', - isAdmin: 1 - }, - { - id: FIXTURE_IDS.memberBob, - householdId: FIXTURE_IDS.household, - telegramUserId: '10002', - displayName: 'Bob', - isAdmin: 0 - }, - { - id: FIXTURE_IDS.memberCarol, - householdId: FIXTURE_IDS.household, - telegramUserId: '10003', - displayName: 'Carol', - isAdmin: 0 - } - ]) - .onConflictDoNothing() + await db.insert(households).values({ + id: FIXTURE_IDS.household, + name: 'Kojori Fixture Household', + defaultLocale: 'ru' + }) - await db - .insert(householdTelegramChats) - .values({ + await db.insert(householdBillingSettings).values({ + householdId: FIXTURE_IDS.household, + settlementCurrency: 'GEL', + rentAmountMinor: 70000n, + rentCurrency: 'USD', + rentDueDay: 20, + rentWarningDay: 17, + utilitiesDueDay: 4, + utilitiesReminderDay: 3, + timezone: 'Asia/Tbilisi' + }) + + await db.insert(members).values([ + { + id: FIXTURE_IDS.memberDima, householdId: FIXTURE_IDS.household, - telegramChatId: '-1001234567890', - telegramChatType: 'supergroup', - title: 'Kojori Demo Household' - }) - .onConflictDoNothing() - - await db - .insert(householdTopicBindings) - .values([ - { - householdId: FIXTURE_IDS.household, - role: 'purchase', - telegramThreadId: '777', - topicName: 'Общие покупки' - }, - { - householdId: FIXTURE_IDS.household, - role: 'feedback', - telegramThreadId: '778', - topicName: 'Anonymous feedback' - } - ]) - .onConflictDoNothing() - - await db - .insert(billingCycles) - .values({ - id: FIXTURE_IDS.cycle, + telegramUserId: '10001', + displayName: 'Dima', + preferredLocale: 'ru', + rentShareWeight: 1, + isAdmin: 1 + }, + { + id: FIXTURE_IDS.memberStas, householdId: FIXTURE_IDS.household, - period: '2026-03', - currency: 'USD' - }) - .onConflictDoNothing() - - await db - .insert(rentRules) - .values({ + telegramUserId: '10002', + displayName: 'Stas', + preferredLocale: 'en', + rentShareWeight: 1, + isAdmin: 0 + }, + { + id: FIXTURE_IDS.memberIon, householdId: FIXTURE_IDS.household, - amountMinor: 70000n, - currency: 'USD', - effectiveFromPeriod: '2026-03' - }) - .onConflictDoNothing() + telegramUserId: '10003', + displayName: 'Ion', + preferredLocale: 'ru', + rentShareWeight: 1, + isAdmin: 0 + } + ]) - await db - .insert(utilityBills) - .values({ + await db.insert(householdTelegramChats).values({ + householdId: FIXTURE_IDS.household, + telegramChatId: '-1003000000001', + telegramChatType: 'supergroup', + title: 'Kojori Fixture Household' + }) + + await db.insert(householdTopicBindings).values([ + { + householdId: FIXTURE_IDS.household, + role: 'purchase', + telegramThreadId: '1001', + topicName: 'Общие покупки' + }, + { + householdId: FIXTURE_IDS.household, + role: 'feedback', + telegramThreadId: '1002', + topicName: 'Анонимно' + }, + { + householdId: FIXTURE_IDS.household, + role: 'reminders', + telegramThreadId: '1003', + topicName: 'Напоминания' + }, + { + householdId: FIXTURE_IDS.household, + role: 'payments', + telegramThreadId: '1004', + topicName: 'Быт или не быт' + } + ]) + + await db.insert(householdUtilityCategories).values([ + { + householdId: FIXTURE_IDS.household, + slug: 'internet', + name: 'Internet', + sortOrder: 0, + isActive: 1 + }, + { + householdId: FIXTURE_IDS.household, + slug: 'gas-water', + name: 'Gas (water included)', + sortOrder: 1, + isActive: 1 + }, + { + householdId: FIXTURE_IDS.household, + slug: 'cleaning', + name: 'Cleaning', + sortOrder: 2, + isActive: 1 + }, + { + householdId: FIXTURE_IDS.household, + slug: 'electricity', + name: 'Electricity', + sortOrder: 3, + isActive: 1 + } + ]) + + await db.insert(billingCycles).values({ + id: FIXTURE_IDS.cycle, + householdId: FIXTURE_IDS.household, + period: '2026-03', + currency: 'GEL' + }) + + await db.insert(rentRules).values({ + householdId: FIXTURE_IDS.household, + amountMinor: 70000n, + currency: 'USD', + effectiveFromPeriod: '2026-03' + }) + + await db.insert(billingCycleExchangeRates).values({ + cycleId: FIXTURE_IDS.cycle, + sourceCurrency: 'USD', + targetCurrency: 'GEL', + rateMicros: 2760000n, + effectiveDate: '2026-03-17', + source: 'nbg' + }) + + await db.insert(utilityBills).values([ + { + householdId: FIXTURE_IDS.household, + cycleId: FIXTURE_IDS.cycle, + billName: 'Internet', + amountMinor: 3200n, + currency: 'GEL', + source: 'manual', + createdByMemberId: FIXTURE_IDS.memberDima + }, + { + householdId: FIXTURE_IDS.household, + cycleId: FIXTURE_IDS.cycle, + billName: 'Cleaning', + amountMinor: 1000n, + currency: 'GEL', + source: 'manual', + createdByMemberId: FIXTURE_IDS.memberDima + }, + { householdId: FIXTURE_IDS.household, cycleId: FIXTURE_IDS.cycle, billName: 'Electricity', - amountMinor: 12000n, - currency: 'USD', + amountMinor: 4000n, + currency: 'GEL', source: 'manual', - createdByMemberId: FIXTURE_IDS.memberAlice - }) - .onConflictDoNothing() + createdByMemberId: FIXTURE_IDS.memberDima + } + ]) - await db - .insert(presenceOverrides) - .values([ - { - cycleId: FIXTURE_IDS.cycle, - memberId: FIXTURE_IDS.memberAlice, - utilityDays: 31, - reason: 'full month' - }, - { - cycleId: FIXTURE_IDS.cycle, - memberId: FIXTURE_IDS.memberBob, - utilityDays: 31, - reason: 'full month' - }, - { - cycleId: FIXTURE_IDS.cycle, - memberId: FIXTURE_IDS.memberCarol, - utilityDays: 20, - reason: 'partial month' - } - ]) - .onConflictDoNothing() - - await db - .insert(purchaseEntries) - .values({ - householdId: FIXTURE_IDS.household, + await db.insert(presenceOverrides).values([ + { cycleId: FIXTURE_IDS.cycle, - payerMemberId: FIXTURE_IDS.memberAlice, - amountMinor: 3000n, - currency: 'USD', - rawText: 'Bought toilet paper 30 gel', - normalizedText: 'bought toilet paper 30 gel', - parserMode: 'rules', - parserConfidence: 93, - telegramChatId: '-100householdchat', - telegramMessageId: '501', - telegramThreadId: 'general-buys' - }) - .onConflictDoNothing() - - await db - .insert(processedBotMessages) - .values({ - householdId: FIXTURE_IDS.household, - source: 'telegram', - sourceMessageKey: 'chat:-100householdchat:message:501', - payloadHash: 'demo-hash' - }) - .onConflictDoNothing() - - await db - .insert(settlements) - .values({ - id: FIXTURE_IDS.settlement, - householdId: FIXTURE_IDS.household, + memberId: FIXTURE_IDS.memberDima, + utilityDays: 31, + reason: 'full month' + }, + { cycleId: FIXTURE_IDS.cycle, - inputHash: 'demo-settlement-hash', - totalDueMinor: 82000n, - currency: 'USD' - }) - .onConflictDoNothing() + memberId: FIXTURE_IDS.memberStas, + utilityDays: 31, + reason: 'full month' + }, + { + cycleId: FIXTURE_IDS.cycle, + memberId: FIXTURE_IDS.memberIon, + utilityDays: 31, + reason: 'full month' + } + ]) - await db - .insert(settlementLines) - .values([ - { - settlementId: FIXTURE_IDS.settlement, - memberId: FIXTURE_IDS.memberAlice, - rentShareMinor: 23334n, - utilityShareMinor: 4000n, - purchaseOffsetMinor: -2000n, - netDueMinor: 25334n, - explanations: ['rent_share_minor=23334', 'utility_share_minor=4000'] - }, - { - settlementId: FIXTURE_IDS.settlement, - memberId: FIXTURE_IDS.memberBob, - rentShareMinor: 23333n, - utilityShareMinor: 4000n, - purchaseOffsetMinor: 1000n, - netDueMinor: 28333n, - explanations: ['rent_share_minor=23333', 'utility_share_minor=4000'] - }, - { - settlementId: FIXTURE_IDS.settlement, - memberId: FIXTURE_IDS.memberCarol, - rentShareMinor: 23333n, - utilityShareMinor: 4000n, - purchaseOffsetMinor: 1000n, - netDueMinor: 28333n, - explanations: ['rent_share_minor=23333', 'utility_share_minor=4000'] - } - ]) - .onConflictDoNothing() + await db.insert(purchaseEntries).values({ + householdId: FIXTURE_IDS.household, + cycleId: FIXTURE_IDS.cycle, + payerMemberId: FIXTURE_IDS.memberDima, + amountMinor: 3000n, + currency: 'GEL', + rawText: 'Купил туалетную бумагу. 30 gel', + normalizedText: 'купил туалетную бумагу 30 gel', + parserMode: 'rules', + parserConfidence: 93, + telegramChatId: '-1003000000001', + telegramMessageId: '501', + telegramThreadId: '1001' + }) + + await db.insert(processedBotMessages).values({ + householdId: FIXTURE_IDS.household, + source: 'telegram', + sourceMessageKey: 'chat:-1003000000001:message:501', + payloadHash: 'fixture-purchase-hash' + }) + + await db.insert(settlements).values({ + id: FIXTURE_IDS.settlement, + householdId: FIXTURE_IDS.household, + cycleId: FIXTURE_IDS.cycle, + inputHash: 'fixture-settlement-hash', + totalDueMinor: 201400n, + currency: 'GEL', + metadata: { + fixture: true, + rentSourceCurrency: 'USD', + settlementCurrency: 'GEL' + } + }) + + await db.insert(settlementLines).values([ + { + settlementId: FIXTURE_IDS.settlement, + memberId: FIXTURE_IDS.memberDima, + rentShareMinor: 64400n, + utilityShareMinor: 2734n, + purchaseOffsetMinor: -2000n, + netDueMinor: 65134n, + explanations: ['rent_share_minor=64400', 'utility_share_minor=2734'] + }, + { + settlementId: FIXTURE_IDS.settlement, + memberId: FIXTURE_IDS.memberStas, + rentShareMinor: 64400n, + utilityShareMinor: 2733n, + purchaseOffsetMinor: 1000n, + netDueMinor: 68133n, + explanations: ['rent_share_minor=64400', 'utility_share_minor=2733'] + }, + { + settlementId: FIXTURE_IDS.settlement, + memberId: FIXTURE_IDS.memberIon, + rentShareMinor: 64400n, + utilityShareMinor: 2733n, + purchaseOffsetMinor: 1000n, + netDueMinor: 68133n, + explanations: ['rent_share_minor=64400', 'utility_share_minor=2733'] + } + ]) + + await db.insert(paymentConfirmations).values({ + id: FIXTURE_IDS.paymentConfirmation, + householdId: FIXTURE_IDS.household, + cycleId: FIXTURE_IDS.cycle, + memberId: FIXTURE_IDS.memberStas, + senderTelegramUserId: '10002', + rawText: 'за жилье закинул', + normalizedText: 'за жилье закинул', + detectedKind: 'rent', + explicitAmountMinor: null, + explicitCurrency: null, + resolvedAmountMinor: 68133n, + resolvedCurrency: 'GEL', + status: 'recorded', + reviewReason: null, + attachmentCount: 1, + telegramChatId: '-1003000000001', + telegramMessageId: '601', + telegramThreadId: '1004', + telegramUpdateId: '9001' + }) + + await db.insert(paymentRecords).values({ + householdId: FIXTURE_IDS.household, + cycleId: FIXTURE_IDS.cycle, + memberId: FIXTURE_IDS.memberStas, + kind: 'rent', + amountMinor: 68133n, + currency: 'GEL', + confirmationId: FIXTURE_IDS.paymentConfirmation, + recordedAt: new Date('2026-03-19T10:00:00.000Z') + }) const seededCycle = await db .select({ period: billingCycles.period, currency: billingCycles.currency }) .from(billingCycles) - .where( - and( - eq(billingCycles.id, FIXTURE_IDS.cycle), - eq(billingCycles.householdId, FIXTURE_IDS.household) - ) - ) + .where(eq(billingCycles.id, FIXTURE_IDS.cycle)) .limit(1) if (seededCycle.length === 0) { throw new Error('Seed verification failed: billing cycle not found') } - const seededChat = await db - .select({ telegramChatId: householdTelegramChats.telegramChatId }) - .from(householdTelegramChats) - .where(eq(householdTelegramChats.householdId, FIXTURE_IDS.household)) + const seededSettings = await db + .select({ settlementCurrency: householdBillingSettings.settlementCurrency }) + .from(householdBillingSettings) + .where(eq(householdBillingSettings.householdId, FIXTURE_IDS.household)) .limit(1) - if (seededChat.length === 0) { - throw new Error('Seed verification failed: Telegram household chat not found') + if (seededSettings.length === 0) { + throw new Error('Seed verification failed: billing settings not found') } }