feat: accounting module — schema, migration, Zod schemas, AR balance service

Phase 1 foundation:
- Migration 0047: all accounting tables (invoice, payment_application,
  account_balance, account_code, journal_entry, billing_run), chart of
  accounts seed, nextBillingDate on enrollment, accounting module config
- Drizzle schema file with all table definitions and type exports
- Zod validation schemas (invoice, payment, billing, reports)
- AccountBalanceService: materialized AR balance per account

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-06 12:00:34 +00:00
parent 5f4a12b9c4
commit b9798f2c8c
7 changed files with 548 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
import { z } from 'zod'
// Enums
export const AccountType = z.enum(['asset', 'liability', 'revenue', 'contra_revenue', 'cogs', 'expense'])
export type AccountType = z.infer<typeof AccountType>
export const InvoiceStatus = z.enum(['draft', 'sent', 'paid', 'partial', 'overdue', 'void', 'written_off'])
export type InvoiceStatus = z.infer<typeof InvoiceStatus>
export const JournalLineType = z.enum(['debit', 'credit'])
export type JournalLineType = z.infer<typeof JournalLineType>
export const BillingRunStatus = z.enum(['pending', 'running', 'completed', 'failed'])
export type BillingRunStatus = z.infer<typeof BillingRunStatus>
// Invoice
export const InvoiceLineItemInput = z.object({
description: z.string().min(1).max(255),
qty: z.coerce.number().int().min(1).default(1),
unitPrice: z.coerce.number().min(0),
discountAmount: z.coerce.number().min(0).default(0),
taxRate: z.coerce.number().min(0).default(0),
accountCodeId: z.string().uuid().optional(),
})
export type InvoiceLineItemInput = z.infer<typeof InvoiceLineItemInput>
export const InvoiceCreateSchema = z.object({
accountId: z.string().uuid(),
locationId: z.string().uuid().optional(),
issueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
notes: z.string().optional(),
lineItems: z.array(InvoiceLineItemInput).min(1),
})
export type InvoiceCreateInput = z.infer<typeof InvoiceCreateSchema>
export const PaymentApplicationSchema = z.object({
transactionId: z.string().uuid().optional(),
amount: z.coerce.number().min(0.01),
notes: z.string().optional(),
})
export type PaymentApplicationInput = z.infer<typeof PaymentApplicationSchema>
export const InvoiceVoidSchema = z.object({
reason: z.string().min(1),
})
export type InvoiceVoidInput = z.infer<typeof InvoiceVoidSchema>
export const InvoiceWriteOffSchema = z.object({
reason: z.string().min(1),
})
export type InvoiceWriteOffInput = z.infer<typeof InvoiceWriteOffSchema>
// Journal Entry
export const JournalEntryVoidSchema = z.object({
reason: z.string().min(1),
})
export type JournalEntryVoidInput = z.infer<typeof JournalEntryVoidSchema>
// Billing
export const BillingRunSchema = z.object({
runDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
})
export type BillingRunInput = z.infer<typeof BillingRunSchema>
export const BillEnrollmentSchema = z.object({
periodStart: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
periodEnd: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
})
export type BillEnrollmentInput = z.infer<typeof BillEnrollmentSchema>
// Report filters
export const DateRangeFilterSchema = z.object({
dateFrom: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
dateTo: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
})
export type DateRangeFilter = z.infer<typeof DateRangeFilterSchema>
export const StatementFilterSchema = z.object({
from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
})
export type StatementFilter = z.infer<typeof StatementFilterSchema>

View File

@@ -200,3 +200,31 @@ export type {
export { LogLevel, AppConfigUpdateSchema } from './config.schema.js'
export type { AppConfigUpdateInput } from './config.schema.js'
export {
AccountType,
InvoiceStatus,
JournalLineType,
BillingRunStatus,
InvoiceLineItemInput,
InvoiceCreateSchema,
PaymentApplicationSchema,
InvoiceVoidSchema,
InvoiceWriteOffSchema,
JournalEntryVoidSchema,
BillingRunSchema,
BillEnrollmentSchema,
DateRangeFilterSchema,
StatementFilterSchema,
} from './accounting.schema.js'
export type {
InvoiceCreateInput,
PaymentApplicationInput,
InvoiceVoidInput,
InvoiceWriteOffInput,
JournalEntryVoidInput,
BillingRunInput,
BillEnrollmentInput,
DateRangeFilter,
StatementFilter,
} from './accounting.schema.js'