1 Commits

Author SHA1 Message Date
ryan
b9798f2c8c 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>
2026-04-06 12:00:34 +00:00
7 changed files with 548 additions and 0 deletions

View File

@@ -4,3 +4,4 @@ export * from './schema/accounts.js'
export * from './schema/inventory.js' export * from './schema/inventory.js'
export * from './schema/pos.js' export * from './schema/pos.js'
export * from './schema/settings.js' export * from './schema/settings.js'
export * from './schema/accounting.js'

View File

@@ -0,0 +1,189 @@
-- Accounting module tables
-- Enums
DO $$ BEGIN
CREATE TYPE invoice_status AS ENUM ('draft','sent','paid','partial','overdue','void','written_off');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE account_type AS ENUM ('asset','liability','revenue','contra_revenue','cogs','expense');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE normal_balance AS ENUM ('debit','credit');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE journal_line_type AS ENUM ('debit','credit');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE billing_run_status AS ENUM ('pending','running','completed','failed');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- Core: Invoice
CREATE TABLE IF NOT EXISTS "invoice" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"invoice_number" varchar(50) UNIQUE NOT NULL,
"account_id" uuid NOT NULL REFERENCES "account"("id"),
"location_id" uuid REFERENCES "location"("id"),
"status" invoice_status NOT NULL DEFAULT 'draft',
"issue_date" date NOT NULL DEFAULT CURRENT_DATE,
"due_date" date NOT NULL DEFAULT CURRENT_DATE,
"source_type" varchar(50),
"source_id" uuid,
"subtotal" numeric(10,2) NOT NULL DEFAULT 0,
"discount_total" numeric(10,2) NOT NULL DEFAULT 0,
"tax_total" numeric(10,2) NOT NULL DEFAULT 0,
"total" numeric(10,2) NOT NULL DEFAULT 0,
"amount_paid" numeric(10,2) NOT NULL DEFAULT 0,
"balance" numeric(10,2) NOT NULL DEFAULT 0,
"refund_of_invoice_id" uuid REFERENCES "invoice"("id"),
"notes" text,
"created_by" uuid REFERENCES "user"("id"),
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS "invoice_account_id" ON "invoice" ("account_id");
CREATE INDEX IF NOT EXISTS "invoice_status" ON "invoice" ("status");
CREATE INDEX IF NOT EXISTS "invoice_source" ON "invoice" ("source_type", "source_id");
-- Core: Invoice Line Item
CREATE TABLE IF NOT EXISTS "invoice_line_item" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"invoice_id" uuid NOT NULL REFERENCES "invoice"("id") ON DELETE CASCADE,
"description" varchar(255) NOT NULL,
"qty" integer NOT NULL DEFAULT 1,
"unit_price" numeric(10,2) NOT NULL,
"discount_amount" numeric(10,2) NOT NULL DEFAULT 0,
"tax_rate" numeric(5,4) NOT NULL DEFAULT 0,
"tax_amount" numeric(10,2) NOT NULL DEFAULT 0,
"line_total" numeric(10,2) NOT NULL DEFAULT 0,
"account_code_id" uuid,
"source_type" varchar(50),
"source_id" uuid,
"created_at" timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS "invoice_line_item_invoice_id" ON "invoice_line_item" ("invoice_id");
-- Core: Payment Application
CREATE TABLE IF NOT EXISTS "payment_application" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"invoice_id" uuid NOT NULL REFERENCES "invoice"("id"),
"transaction_id" uuid REFERENCES "transaction"("id"),
"amount" numeric(10,2) NOT NULL,
"applied_at" timestamptz NOT NULL DEFAULT now(),
"applied_by" uuid NOT NULL REFERENCES "user"("id"),
"notes" text,
"created_at" timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS "payment_application_invoice_id" ON "payment_application" ("invoice_id");
-- Core: Account Balance (materialized AR)
CREATE TABLE IF NOT EXISTS "account_balance" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"account_id" uuid NOT NULL UNIQUE REFERENCES "account"("id"),
"current_balance" numeric(10,2) NOT NULL DEFAULT 0,
"last_invoice_date" date,
"last_payment_date" date,
"updated_at" timestamptz NOT NULL DEFAULT now()
);
-- Accounting module: Chart of Accounts
CREATE TABLE IF NOT EXISTS "account_code" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"code" varchar(10) UNIQUE NOT NULL,
"name" varchar(255) NOT NULL,
"account_type" account_type NOT NULL,
"normal_balance" normal_balance NOT NULL,
"is_system" boolean NOT NULL DEFAULT true,
"is_active" boolean NOT NULL DEFAULT true,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
);
-- Accounting module: Journal Entry
CREATE TABLE IF NOT EXISTS "journal_entry" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"entry_number" varchar(20) UNIQUE NOT NULL,
"entry_date" date NOT NULL DEFAULT CURRENT_DATE,
"entry_type" varchar(50) NOT NULL,
"source_type" varchar(50),
"source_id" uuid,
"description" text NOT NULL,
"total_debits" numeric(10,2) NOT NULL,
"total_credits" numeric(10,2) NOT NULL,
"is_void" boolean NOT NULL DEFAULT false,
"void_reason" text,
"voided_by" uuid REFERENCES "user"("id"),
"voided_at" timestamptz,
"reversal_of_id" uuid REFERENCES "journal_entry"("id"),
"created_by" uuid NOT NULL REFERENCES "user"("id"),
"created_at" timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS "journal_entry_date" ON "journal_entry" ("entry_date");
CREATE INDEX IF NOT EXISTS "journal_entry_type" ON "journal_entry" ("entry_type");
-- Accounting module: Journal Entry Line
CREATE TABLE IF NOT EXISTS "journal_entry_line" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"journal_entry_id" uuid NOT NULL REFERENCES "journal_entry"("id") ON DELETE CASCADE,
"account_code_id" uuid NOT NULL REFERENCES "account_code"("id"),
"line_type" journal_line_type NOT NULL,
"amount" numeric(10,2) NOT NULL,
"description" text,
"entity_type" varchar(50),
"entity_id" uuid,
"created_at" timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS "journal_entry_line_entry_id" ON "journal_entry_line" ("journal_entry_id");
CREATE INDEX IF NOT EXISTS "journal_entry_line_account" ON "journal_entry_line" ("account_code_id");
-- Lessons module: Billing Run
CREATE TABLE IF NOT EXISTS "billing_run" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"run_date" date NOT NULL,
"status" billing_run_status NOT NULL DEFAULT 'pending',
"enrollments_processed" integer NOT NULL DEFAULT 0,
"invoices_generated" integer NOT NULL DEFAULT 0,
"total_amount" numeric(10,2) NOT NULL DEFAULT 0,
"errors" jsonb,
"started_at" timestamptz,
"completed_at" timestamptz,
"created_by" uuid REFERENCES "user"("id"),
"created_at" timestamptz NOT NULL DEFAULT now()
);
-- Add nextBillingDate to enrollment
ALTER TABLE "enrollment" ADD COLUMN IF NOT EXISTS "next_billing_date" date;
-- Add accounting to module_config
INSERT INTO "module_config" ("slug", "name", "description", "licensed", "enabled")
VALUES ('accounting', 'Accounting', 'Chart of accounts, journal entries, and financial reports', true, false)
ON CONFLICT ("slug") DO NOTHING;
-- Seed chart of accounts
INSERT INTO "account_code" ("code", "name", "account_type", "normal_balance", "is_system") VALUES
('1000', 'Cash - Register Drawer', 'asset', 'debit', true),
('1100', 'Accounts Receivable', 'asset', 'debit', true),
('1200', 'Payment Clearing', 'asset', 'debit', true),
('1300', 'Inventory - Sale Stock', 'asset', 'debit', true),
('1320', 'Inventory - Parts & Supplies', 'asset', 'debit', true),
('2000', 'Sales Tax Payable', 'liability', 'credit', true),
('2110', 'Deferred Revenue - Lessons', 'liability', 'credit', true),
('4000', 'Sales Revenue', 'revenue', 'credit', true),
('4200', 'Lesson Revenue', 'revenue', 'credit', true),
('4300', 'Repair Revenue - Labor', 'revenue', 'credit', true),
('4310', 'Repair Revenue - Parts', 'revenue', 'credit', true),
('4900', 'Sales Discounts', 'contra_revenue', 'debit', true),
('4910', 'Sales Returns & Refunds', 'contra_revenue', 'debit', true),
('5000', 'Cost of Goods Sold', 'cogs', 'debit', true),
('5100', 'Repair Parts Cost', 'cogs', 'debit', true),
('6000', 'Cash Over / Short', 'expense', 'debit', true),
('6200', 'Bad Debt Expense', 'expense', 'debit', true)
ON CONFLICT ("code") DO NOTHING;

View File

@@ -330,6 +330,13 @@
"when": 1775860000000, "when": 1775860000000,
"tag": "0046_auto-employee-number", "tag": "0046_auto-employee-number",
"breakpoints": true "breakpoints": true
},
{
"idx": 47,
"version": "7",
"when": 1775950000000,
"tag": "0047_accounting",
"breakpoints": true
} }
] ]
} }

View File

@@ -0,0 +1,147 @@
import { pgTable, uuid, varchar, text, numeric, integer, boolean, date, timestamp, jsonb, pgEnum } from 'drizzle-orm/pg-core'
import { accounts } from './accounts.js'
import { locations } from './stores.js'
import { users } from './users.js'
import { transactions } from './pos.js'
// Enums
export const invoiceStatusEnum = pgEnum('invoice_status', ['draft', 'sent', 'paid', 'partial', 'overdue', 'void', 'written_off'])
export const accountTypeEnum = pgEnum('account_type', ['asset', 'liability', 'revenue', 'contra_revenue', 'cogs', 'expense'])
export const normalBalanceEnum = pgEnum('normal_balance', ['debit', 'credit'])
export const journalLineTypeEnum = pgEnum('journal_line_type', ['debit', 'credit'])
export const billingRunStatusEnum = pgEnum('billing_run_status', ['pending', 'running', 'completed', 'failed'])
// Core: Invoice
export const invoices = pgTable('invoice', {
id: uuid('id').primaryKey().defaultRandom(),
invoiceNumber: varchar('invoice_number', { length: 50 }).notNull().unique(),
accountId: uuid('account_id').notNull().references(() => accounts.id),
locationId: uuid('location_id').references(() => locations.id),
status: invoiceStatusEnum('status').notNull().default('draft'),
issueDate: date('issue_date').notNull().defaultNow(),
dueDate: date('due_date').notNull().defaultNow(),
sourceType: varchar('source_type', { length: 50 }),
sourceId: uuid('source_id'),
subtotal: numeric('subtotal', { precision: 10, scale: 2 }).notNull().default('0'),
discountTotal: numeric('discount_total', { precision: 10, scale: 2 }).notNull().default('0'),
taxTotal: numeric('tax_total', { precision: 10, scale: 2 }).notNull().default('0'),
total: numeric('total', { precision: 10, scale: 2 }).notNull().default('0'),
amountPaid: numeric('amount_paid', { precision: 10, scale: 2 }).notNull().default('0'),
balance: numeric('balance', { precision: 10, scale: 2 }).notNull().default('0'),
refundOfInvoiceId: uuid('refund_of_invoice_id'),
notes: text('notes'),
createdBy: uuid('created_by').references(() => users.id),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
// Core: Invoice Line Item
export const invoiceLineItems = pgTable('invoice_line_item', {
id: uuid('id').primaryKey().defaultRandom(),
invoiceId: uuid('invoice_id').notNull().references(() => invoices.id, { onDelete: 'cascade' }),
description: varchar('description', { length: 255 }).notNull(),
qty: integer('qty').notNull().default(1),
unitPrice: numeric('unit_price', { precision: 10, scale: 2 }).notNull(),
discountAmount: numeric('discount_amount', { precision: 10, scale: 2 }).notNull().default('0'),
taxRate: numeric('tax_rate', { precision: 5, scale: 4 }).notNull().default('0'),
taxAmount: numeric('tax_amount', { precision: 10, scale: 2 }).notNull().default('0'),
lineTotal: numeric('line_total', { precision: 10, scale: 2 }).notNull().default('0'),
accountCodeId: uuid('account_code_id'),
sourceType: varchar('source_type', { length: 50 }),
sourceId: uuid('source_id'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
// Core: Payment Application
export const paymentApplications = pgTable('payment_application', {
id: uuid('id').primaryKey().defaultRandom(),
invoiceId: uuid('invoice_id').notNull().references(() => invoices.id),
transactionId: uuid('transaction_id').references(() => transactions.id),
amount: numeric('amount', { precision: 10, scale: 2 }).notNull(),
appliedAt: timestamp('applied_at', { withTimezone: true }).notNull().defaultNow(),
appliedBy: uuid('applied_by').notNull().references(() => users.id),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
// Core: Account Balance (materialized AR)
export const accountBalances = pgTable('account_balance', {
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id').notNull().unique().references(() => accounts.id),
currentBalance: numeric('current_balance', { precision: 10, scale: 2 }).notNull().default('0'),
lastInvoiceDate: date('last_invoice_date'),
lastPaymentDate: date('last_payment_date'),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
// Accounting module: Chart of Accounts
export const accountCodes = pgTable('account_code', {
id: uuid('id').primaryKey().defaultRandom(),
code: varchar('code', { length: 10 }).notNull().unique(),
name: varchar('name', { length: 255 }).notNull(),
accountType: accountTypeEnum('account_type').notNull(),
normalBalance: normalBalanceEnum('normal_balance').notNull(),
isSystem: boolean('is_system').notNull().default(true),
isActive: boolean('is_active').notNull().default(true),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
// Accounting module: Journal Entry
export const journalEntries = pgTable('journal_entry', {
id: uuid('id').primaryKey().defaultRandom(),
entryNumber: varchar('entry_number', { length: 20 }).notNull().unique(),
entryDate: date('entry_date').notNull().defaultNow(),
entryType: varchar('entry_type', { length: 50 }).notNull(),
sourceType: varchar('source_type', { length: 50 }),
sourceId: uuid('source_id'),
description: text('description').notNull(),
totalDebits: numeric('total_debits', { precision: 10, scale: 2 }).notNull(),
totalCredits: numeric('total_credits', { precision: 10, scale: 2 }).notNull(),
isVoid: boolean('is_void').notNull().default(false),
voidReason: text('void_reason'),
voidedBy: uuid('voided_by').references(() => users.id),
voidedAt: timestamp('voided_at', { withTimezone: true }),
reversalOfId: uuid('reversal_of_id'),
createdBy: uuid('created_by').notNull().references(() => users.id),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
// Accounting module: Journal Entry Line
export const journalEntryLines = pgTable('journal_entry_line', {
id: uuid('id').primaryKey().defaultRandom(),
journalEntryId: uuid('journal_entry_id').notNull().references(() => journalEntries.id, { onDelete: 'cascade' }),
accountCodeId: uuid('account_code_id').notNull().references(() => accountCodes.id),
lineType: journalLineTypeEnum('line_type').notNull(),
amount: numeric('amount', { precision: 10, scale: 2 }).notNull(),
description: text('description'),
entityType: varchar('entity_type', { length: 50 }),
entityId: uuid('entity_id'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
// Lessons module: Billing Run
export const billingRuns = pgTable('billing_run', {
id: uuid('id').primaryKey().defaultRandom(),
runDate: date('run_date').notNull(),
status: billingRunStatusEnum('status').notNull().default('pending'),
enrollmentsProcessed: integer('enrollments_processed').notNull().default(0),
invoicesGenerated: integer('invoices_generated').notNull().default(0),
totalAmount: numeric('total_amount', { precision: 10, scale: 2 }).notNull().default('0'),
errors: jsonb('errors'),
startedAt: timestamp('started_at', { withTimezone: true }),
completedAt: timestamp('completed_at', { withTimezone: true }),
createdBy: uuid('created_by').references(() => users.id),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
// Type exports
export type Invoice = typeof invoices.$inferSelect
export type InvoiceInsert = typeof invoices.$inferInsert
export type InvoiceLineItem = typeof invoiceLineItems.$inferSelect
export type PaymentApplication = typeof paymentApplications.$inferSelect
export type AccountBalance = typeof accountBalances.$inferSelect
export type AccountCode = typeof accountCodes.$inferSelect
export type JournalEntry = typeof journalEntries.$inferSelect
export type JournalEntryLine = typeof journalEntryLines.$inferSelect
export type BillingRun = typeof billingRuns.$inferSelect

View File

@@ -0,0 +1,93 @@
import { eq, desc, gt, count } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { accountBalances } from '../db/schema/accounting.js'
import { accounts } from '../db/schema/accounts.js'
import type { PaginationInput } from '@lunarfront/shared/schemas'
export const AccountBalanceService = {
async getBalance(db: PostgresJsDatabase<any>, accountId: string) {
const [existing] = await db.select().from(accountBalances).where(eq(accountBalances.accountId, accountId)).limit(1)
if (existing) return existing
// Create balance record if it doesn't exist
const [created] = await db.insert(accountBalances).values({ accountId }).returning()
return created
},
async adjustBalance(
db: PostgresJsDatabase<any>,
accountId: string,
adjustment: number,
reason: 'invoice' | 'payment' | 'void' | 'write_off',
) {
const balance = await this.getBalance(db, accountId)
const newBalance = parseFloat(balance.currentBalance) + adjustment
const updates: Record<string, unknown> = {
currentBalance: newBalance.toFixed(2),
updatedAt: new Date(),
}
if (reason === 'invoice' && adjustment > 0) {
updates.lastInvoiceDate = new Date().toISOString().slice(0, 10)
}
if (reason === 'payment' && adjustment < 0) {
updates.lastPaymentDate = new Date().toISOString().slice(0, 10)
}
await db.update(accountBalances).set(updates).where(eq(accountBalances.accountId, accountId))
},
async getOutstandingAccounts(db: PostgresJsDatabase<any>, params: PaginationInput) {
const where = gt(accountBalances.currentBalance, '0')
const offset = ((params.page ?? 1) - 1) * (params.limit ?? 25)
const [data, [{ total }]] = await Promise.all([
db.select({
accountId: accountBalances.accountId,
currentBalance: accountBalances.currentBalance,
lastInvoiceDate: accountBalances.lastInvoiceDate,
lastPaymentDate: accountBalances.lastPaymentDate,
accountName: accounts.name,
accountEmail: accounts.email,
})
.from(accountBalances)
.innerJoin(accounts, eq(accountBalances.accountId, accounts.id))
.where(where)
.orderBy(desc(accountBalances.currentBalance))
.limit(params.limit ?? 25)
.offset(offset),
db.select({ total: count() })
.from(accountBalances)
.where(where),
])
return {
data,
pagination: {
page: params.page ?? 1,
limit: params.limit ?? 25,
total,
totalPages: Math.ceil(total / (params.limit ?? 25)),
},
}
},
async recalculateFromInvoices(db: PostgresJsDatabase<any>, accountId: string) {
// Safety valve: recalculate balance from all invoices
const { invoices } = await import('../db/schema/accounting.js')
const rows = await db
.select({ balance: invoices.balance, status: invoices.status })
.from(invoices)
.where(eq(invoices.accountId, accountId))
const totalOutstanding = rows
.filter(r => ['sent', 'partial', 'overdue'].includes(r.status))
.reduce((sum, r) => sum + parseFloat(r.balance), 0)
await db.update(accountBalances).set({
currentBalance: totalOutstanding.toFixed(2),
updatedAt: new Date(),
}).where(eq(accountBalances.accountId, accountId))
},
}

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 { LogLevel, AppConfigUpdateSchema } from './config.schema.js'
export type { AppConfigUpdateInput } 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'