mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 22:34:03 +00:00
feat(WHE-29): add v1 accounting schema migration and seed fixtures
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user