Add recurring bills schema and cycle math helper

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 15:13:38 +07:00
parent 70bb5954a0
commit bd87cd09f5
2 changed files with 157 additions and 1 deletions
+63 -1
View File
@@ -944,6 +944,61 @@ export const companyAccountTransactions = pgTable(
]
);
// ── Recurring Bills ────────────────────────────────────
export const recurringBillCycleEnum = pgEnum('recurring_bill_cycle', [
'weekly',
'monthly',
'quarterly',
'yearly'
]);
export const recurringBillStatusEnum = pgEnum('recurring_bill_status', [
'active',
'paused',
'ended'
]);
export const recurringBills = pgTable(
'recurring_bills',
{
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id, { onDelete: 'cascade' }),
projectId: uuid('project_id')
.notNull()
.references(() => projects.id, { onDelete: 'restrict' }),
accountId: uuid('account_id')
.notNull()
.references(() => companyAccounts.id, { onDelete: 'restrict' }),
categoryId: uuid('category_id').references(() => categories.id, { onDelete: 'set null' }),
partyId: uuid('party_id').references(() => parties.id, { onDelete: 'set null' }),
name: text('name').notNull(),
description: text('description'),
cycle: recurringBillCycleEnum('cycle').notNull(),
defaultAmount: numeric('default_amount', { precision: 15, scale: 2 }).notNull(),
nextCycleAmount: numeric('next_cycle_amount', { precision: 15, scale: 2 }),
currency: text('currency').notNull().default('THB'),
dayOfCycle: integer('day_of_cycle'),
startDate: date('start_date').notNull(),
endDate: date('end_date'),
nextDueDate: date('next_due_date').notNull(),
lastPostedDate: date('last_posted_date'),
status: recurringBillStatusEnum('status').notNull().default('active'),
pausedAt: timestamp('paused_at', { withTimezone: true }),
skipNext: boolean('skip_next').notNull().default(false),
createdBy: text('created_by').references(() => users.id, { onDelete: 'set null' }),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow()
},
(table) => [
index('recurring_bills_company_next_due_idx').on(table.companyId, table.nextDueDate),
index('recurring_bills_company_status_idx').on(table.companyId, table.status)
]
);
export const companyAddresses = pgTable(
'company_addresses',
{
@@ -1037,7 +1092,14 @@ export const companyLogEventEnum = pgEnum('company_log_event', [
'account_deleted',
'account_transaction_added',
'account_transfer_posted',
'account_reconciled'
'account_reconciled',
'recurring_bill_created',
'recurring_bill_updated',
'recurring_bill_deleted',
'recurring_bill_paused',
'recurring_bill_resumed',
'recurring_bill_skipped',
'recurring_bill_posted'
]);
export const companyLog = pgTable(