feat(WHE-29): add v1 accounting schema migration and seed fixtures

This commit is contained in:
2026-03-05 04:13:00 +04:00
parent 27205bc90b
commit b3ae1a51e4
10 changed files with 1841 additions and 2 deletions

View File

@@ -0,0 +1,123 @@
CREATE TABLE "billing_cycles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"period" text NOT NULL,
"currency" text NOT NULL,
"started_at" timestamp with time zone DEFAULT now() NOT NULL,
"closed_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "presence_overrides" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cycle_id" uuid NOT NULL,
"member_id" uuid NOT NULL,
"utility_days" integer NOT NULL,
"reason" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "processed_bot_messages" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"source" text NOT NULL,
"source_message_key" text NOT NULL,
"payload_hash" text,
"processed_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "purchase_entries" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"cycle_id" uuid,
"payer_member_id" uuid NOT NULL,
"amount_minor" bigint NOT NULL,
"currency" text NOT NULL,
"raw_text" text NOT NULL,
"normalized_text" text,
"parser_mode" text NOT NULL,
"parser_confidence" integer NOT NULL,
"telegram_chat_id" text,
"telegram_message_id" text,
"telegram_thread_id" text,
"message_sent_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "rent_rules" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"amount_minor" bigint NOT NULL,
"currency" text NOT NULL,
"effective_from_period" text NOT NULL,
"effective_to_period" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "settlement_lines" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"settlement_id" uuid NOT NULL,
"member_id" uuid NOT NULL,
"rent_share_minor" bigint NOT NULL,
"utility_share_minor" bigint NOT NULL,
"purchase_offset_minor" bigint NOT NULL,
"net_due_minor" bigint NOT NULL,
"explanations" jsonb DEFAULT '[]'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "settlements" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"cycle_id" uuid NOT NULL,
"input_hash" text NOT NULL,
"total_due_minor" bigint NOT NULL,
"currency" text NOT NULL,
"computed_at" timestamp with time zone DEFAULT now() NOT NULL,
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL
);
--> statement-breakpoint
CREATE TABLE "utility_bills" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"household_id" uuid NOT NULL,
"cycle_id" uuid NOT NULL,
"bill_name" text NOT NULL,
"amount_minor" bigint NOT NULL,
"currency" text NOT NULL,
"due_date" date,
"source" text DEFAULT 'manual' NOT NULL,
"created_by_member_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "billing_cycles" ADD CONSTRAINT "billing_cycles_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "presence_overrides" ADD CONSTRAINT "presence_overrides_cycle_id_billing_cycles_id_fk" FOREIGN KEY ("cycle_id") REFERENCES "public"."billing_cycles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "presence_overrides" ADD CONSTRAINT "presence_overrides_member_id_members_id_fk" FOREIGN KEY ("member_id") REFERENCES "public"."members"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "processed_bot_messages" ADD CONSTRAINT "processed_bot_messages_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "purchase_entries" ADD CONSTRAINT "purchase_entries_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "purchase_entries" ADD CONSTRAINT "purchase_entries_cycle_id_billing_cycles_id_fk" FOREIGN KEY ("cycle_id") REFERENCES "public"."billing_cycles"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "purchase_entries" ADD CONSTRAINT "purchase_entries_payer_member_id_members_id_fk" FOREIGN KEY ("payer_member_id") REFERENCES "public"."members"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "rent_rules" ADD CONSTRAINT "rent_rules_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "settlement_lines" ADD CONSTRAINT "settlement_lines_settlement_id_settlements_id_fk" FOREIGN KEY ("settlement_id") REFERENCES "public"."settlements"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "settlement_lines" ADD CONSTRAINT "settlement_lines_member_id_members_id_fk" FOREIGN KEY ("member_id") REFERENCES "public"."members"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "settlements" ADD CONSTRAINT "settlements_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "settlements" ADD CONSTRAINT "settlements_cycle_id_billing_cycles_id_fk" FOREIGN KEY ("cycle_id") REFERENCES "public"."billing_cycles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "utility_bills" ADD CONSTRAINT "utility_bills_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "utility_bills" ADD CONSTRAINT "utility_bills_cycle_id_billing_cycles_id_fk" FOREIGN KEY ("cycle_id") REFERENCES "public"."billing_cycles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "utility_bills" ADD CONSTRAINT "utility_bills_created_by_member_id_members_id_fk" FOREIGN KEY ("created_by_member_id") REFERENCES "public"."members"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "billing_cycles_household_period_unique" ON "billing_cycles" USING btree ("household_id","period");--> statement-breakpoint
CREATE INDEX "billing_cycles_household_period_idx" ON "billing_cycles" USING btree ("household_id","period");--> statement-breakpoint
CREATE UNIQUE INDEX "presence_overrides_cycle_member_unique" ON "presence_overrides" USING btree ("cycle_id","member_id");--> statement-breakpoint
CREATE INDEX "presence_overrides_cycle_idx" ON "presence_overrides" USING btree ("cycle_id");--> statement-breakpoint
CREATE UNIQUE INDEX "processed_bot_messages_source_message_unique" ON "processed_bot_messages" USING btree ("household_id","source","source_message_key");--> statement-breakpoint
CREATE INDEX "purchase_entries_household_cycle_idx" ON "purchase_entries" USING btree ("household_id","cycle_id");--> statement-breakpoint
CREATE INDEX "purchase_entries_payer_idx" ON "purchase_entries" USING btree ("payer_member_id");--> statement-breakpoint
CREATE UNIQUE INDEX "purchase_entries_household_tg_message_unique" ON "purchase_entries" USING btree ("household_id","telegram_chat_id","telegram_message_id");--> statement-breakpoint
CREATE UNIQUE INDEX "rent_rules_household_from_period_unique" ON "rent_rules" USING btree ("household_id","effective_from_period");--> statement-breakpoint
CREATE INDEX "rent_rules_household_from_period_idx" ON "rent_rules" USING btree ("household_id","effective_from_period");--> statement-breakpoint
CREATE UNIQUE INDEX "settlement_lines_settlement_member_unique" ON "settlement_lines" USING btree ("settlement_id","member_id");--> statement-breakpoint
CREATE INDEX "settlement_lines_settlement_idx" ON "settlement_lines" USING btree ("settlement_id");--> statement-breakpoint
CREATE UNIQUE INDEX "settlements_cycle_unique" ON "settlements" USING btree ("cycle_id");--> statement-breakpoint
CREATE INDEX "settlements_household_computed_idx" ON "settlements" USING btree ("household_id","computed_at");--> statement-breakpoint
CREATE INDEX "utility_bills_cycle_idx" ON "utility_bills" USING btree ("cycle_id");--> statement-breakpoint
CREATE INDEX "utility_bills_household_cycle_idx" ON "utility_bills" USING btree ("household_id","cycle_id");

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,13 @@
"when": 1772663250726,
"tag": "0000_modern_centennial",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1772669239939,
"tag": "0001_spicy_sersi",
"breakpoints": true
}
]
}

View File

@@ -6,7 +6,8 @@
"build": "bun build src/index.ts --outdir dist --target bun",
"typecheck": "tsgo --project tsconfig.json --noEmit",
"test": "bun test --pass-with-no-tests",
"lint": "oxlint \"src\""
"lint": "oxlint \"src\"",
"seed": "bun run src/seed.ts"
},
"dependencies": {
"@household/config": "workspace:*",

View File

@@ -1,4 +1,16 @@
import { index, integer, pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'
import {
bigint,
date,
index,
integer,
jsonb,
pgTable,
text,
timestamp,
uniqueIndex,
uuid
} from 'drizzle-orm/pg-core'
export const households = pgTable('households', {
id: uuid('id').defaultRandom().primaryKey(),
@@ -26,3 +38,227 @@ export const members = pgTable(
)
})
)
export const billingCycles = pgTable(
'billing_cycles',
{
id: uuid('id').defaultRandom().primaryKey(),
householdId: uuid('household_id')
.notNull()
.references(() => households.id, { onDelete: 'cascade' }),
period: text('period').notNull(),
currency: text('currency').notNull(),
startedAt: timestamp('started_at', { withTimezone: true }).defaultNow().notNull(),
closedAt: timestamp('closed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
householdPeriodUnique: uniqueIndex('billing_cycles_household_period_unique').on(
table.householdId,
table.period
),
householdPeriodIdx: index('billing_cycles_household_period_idx').on(
table.householdId,
table.period
)
})
)
export const rentRules = pgTable(
'rent_rules',
{
id: uuid('id').defaultRandom().primaryKey(),
householdId: uuid('household_id')
.notNull()
.references(() => households.id, { onDelete: 'cascade' }),
amountMinor: bigint('amount_minor', { mode: 'bigint' }).notNull(),
currency: text('currency').notNull(),
effectiveFromPeriod: text('effective_from_period').notNull(),
effectiveToPeriod: text('effective_to_period'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
householdFromPeriodUnique: uniqueIndex('rent_rules_household_from_period_unique').on(
table.householdId,
table.effectiveFromPeriod
),
householdFromPeriodIdx: index('rent_rules_household_from_period_idx').on(
table.householdId,
table.effectiveFromPeriod
)
})
)
export const utilityBills = pgTable(
'utility_bills',
{
id: uuid('id').defaultRandom().primaryKey(),
householdId: uuid('household_id')
.notNull()
.references(() => households.id, { onDelete: 'cascade' }),
cycleId: uuid('cycle_id')
.notNull()
.references(() => billingCycles.id, { onDelete: 'cascade' }),
billName: text('bill_name').notNull(),
amountMinor: bigint('amount_minor', { mode: 'bigint' }).notNull(),
currency: text('currency').notNull(),
dueDate: date('due_date'),
source: text('source').default('manual').notNull(),
createdByMemberId: uuid('created_by_member_id').references(() => members.id, {
onDelete: 'set null'
}),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
cycleIdx: index('utility_bills_cycle_idx').on(table.cycleId),
householdCycleIdx: index('utility_bills_household_cycle_idx').on(
table.householdId,
table.cycleId
)
})
)
export const presenceOverrides = pgTable(
'presence_overrides',
{
id: uuid('id').defaultRandom().primaryKey(),
cycleId: uuid('cycle_id')
.notNull()
.references(() => billingCycles.id, { onDelete: 'cascade' }),
memberId: uuid('member_id')
.notNull()
.references(() => members.id, { onDelete: 'cascade' }),
utilityDays: integer('utility_days').notNull(),
reason: text('reason'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
cycleMemberUnique: uniqueIndex('presence_overrides_cycle_member_unique').on(
table.cycleId,
table.memberId
),
cycleIdx: index('presence_overrides_cycle_idx').on(table.cycleId)
})
)
export const purchaseEntries = pgTable(
'purchase_entries',
{
id: uuid('id').defaultRandom().primaryKey(),
householdId: uuid('household_id')
.notNull()
.references(() => households.id, { onDelete: 'cascade' }),
cycleId: uuid('cycle_id').references(() => billingCycles.id, {
onDelete: 'set null'
}),
payerMemberId: uuid('payer_member_id')
.notNull()
.references(() => members.id, { onDelete: 'restrict' }),
amountMinor: bigint('amount_minor', { mode: 'bigint' }).notNull(),
currency: text('currency').notNull(),
rawText: text('raw_text').notNull(),
normalizedText: text('normalized_text'),
parserMode: text('parser_mode').notNull(),
parserConfidence: integer('parser_confidence').notNull(),
telegramChatId: text('telegram_chat_id'),
telegramMessageId: text('telegram_message_id'),
telegramThreadId: text('telegram_thread_id'),
messageSentAt: timestamp('message_sent_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
householdCycleIdx: index('purchase_entries_household_cycle_idx').on(
table.householdId,
table.cycleId
),
payerIdx: index('purchase_entries_payer_idx').on(table.payerMemberId),
tgMessageUnique: uniqueIndex('purchase_entries_household_tg_message_unique').on(
table.householdId,
table.telegramChatId,
table.telegramMessageId
)
})
)
export const processedBotMessages = pgTable(
'processed_bot_messages',
{
id: uuid('id').defaultRandom().primaryKey(),
householdId: uuid('household_id')
.notNull()
.references(() => households.id, { onDelete: 'cascade' }),
source: text('source').notNull(),
sourceMessageKey: text('source_message_key').notNull(),
payloadHash: text('payload_hash'),
processedAt: timestamp('processed_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
sourceMessageUnique: uniqueIndex('processed_bot_messages_source_message_unique').on(
table.householdId,
table.source,
table.sourceMessageKey
)
})
)
export const settlements = pgTable(
'settlements',
{
id: uuid('id').defaultRandom().primaryKey(),
householdId: uuid('household_id')
.notNull()
.references(() => households.id, { onDelete: 'cascade' }),
cycleId: uuid('cycle_id')
.notNull()
.references(() => billingCycles.id, { onDelete: 'cascade' }),
inputHash: text('input_hash').notNull(),
totalDueMinor: bigint('total_due_minor', { mode: 'bigint' }).notNull(),
currency: text('currency').notNull(),
computedAt: timestamp('computed_at', { withTimezone: true }).defaultNow().notNull(),
metadata: jsonb('metadata')
.default(sql`'{}'::jsonb`)
.notNull()
},
(table) => ({
cycleUnique: uniqueIndex('settlements_cycle_unique').on(table.cycleId),
householdComputedIdx: index('settlements_household_computed_idx').on(
table.householdId,
table.computedAt
)
})
)
export const settlementLines = pgTable(
'settlement_lines',
{
id: uuid('id').defaultRandom().primaryKey(),
settlementId: uuid('settlement_id')
.notNull()
.references(() => settlements.id, { onDelete: 'cascade' }),
memberId: uuid('member_id')
.notNull()
.references(() => members.id, { onDelete: 'restrict' }),
rentShareMinor: bigint('rent_share_minor', { mode: 'bigint' }).notNull(),
utilityShareMinor: bigint('utility_share_minor', { mode: 'bigint' }).notNull(),
purchaseOffsetMinor: bigint('purchase_offset_minor', { mode: 'bigint' }).notNull(),
netDueMinor: bigint('net_due_minor', { mode: 'bigint' }).notNull(),
explanations: jsonb('explanations')
.default(sql`'[]'::jsonb`)
.notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => ({
settlementMemberUnique: uniqueIndex('settlement_lines_settlement_member_unique').on(
table.settlementId,
table.memberId
),
settlementIdx: index('settlement_lines_settlement_idx').on(table.settlementId)
})
)
export type Household = typeof households.$inferSelect
export type Member = typeof members.$inferSelect
export type BillingCycle = typeof billingCycles.$inferSelect
export type UtilityBill = typeof utilityBills.$inferSelect
export type PurchaseEntry = typeof purchaseEntries.$inferSelect
export type Settlement = typeof settlements.$inferSelect

226
packages/db/src/seed.ts Normal file
View File

@@ -0,0 +1,226 @@
import { and, eq } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import {
billingCycles,
households,
members,
presenceOverrides,
processedBotMessages,
purchaseEntries,
rentRules,
settlementLines,
settlements,
utilityBills
} from './schema'
const databaseUrl = process.env.DATABASE_URL
if (!databaseUrl) {
throw new Error('DATABASE_URL is required for db seed')
}
const queryClient = postgres(databaseUrl, {
prepare: false,
max: 2
})
const db = drizzle(queryClient)
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'
} as const
async function seed(): Promise<void> {
await db
.insert(households)
.values({
id: FIXTURE_IDS.household,
name: 'Kojori Demo Household'
})
.onConflictDoNothing()
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(billingCycles)
.values({
id: FIXTURE_IDS.cycle,
householdId: FIXTURE_IDS.household,
period: '2026-03',
currency: 'USD'
})
.onConflictDoNothing()
await db
.insert(rentRules)
.values({
householdId: FIXTURE_IDS.household,
amountMinor: 70000n,
currency: 'USD',
effectiveFromPeriod: '2026-03'
})
.onConflictDoNothing()
await db
.insert(utilityBills)
.values({
householdId: FIXTURE_IDS.household,
cycleId: FIXTURE_IDS.cycle,
billName: 'Electricity',
amountMinor: 12000n,
currency: 'USD',
source: 'manual',
createdByMemberId: FIXTURE_IDS.memberAlice
})
.onConflictDoNothing()
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,
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,
cycleId: FIXTURE_IDS.cycle,
inputHash: 'demo-settlement-hash',
totalDueMinor: 82000n,
currency: 'USD'
})
.onConflictDoNothing()
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()
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)
)
)
.limit(1)
if (seededCycle.length === 0) {
throw new Error('Seed verification failed: billing cycle not found')
}
}
try {
await seed()
console.log('Seed completed')
} finally {
await queryClient.end({ timeout: 5 })
}