Add expense invoice fields, sales tables, and Paperless env vars

Expenses now have invoiceFileUrl, invoiceFileName, paperlessUrl,
paperlessDocumentId for supplier invoice attachment.

New expense_packages junction links expenses to multiple packages.

New sales + sale_line_items + sale_packages tables for income tracking
with per-line tax rate and per-sale withholding rate.

Added saleStatusEnum and 4 audit events: expense_invoice_uploaded,
sale_created, sale_confirmed, sale_voided.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 11:51:18 +07:00
parent 84c8beca15
commit bbfab9faaa
2 changed files with 97 additions and 0 deletions
+93
View File
@@ -146,6 +146,11 @@ export const expenses = pgTable(
status: expenseStatusEnum('status').notNull().default('pending'),
reviewedAt: timestamp('reviewed_at', { withTimezone: true }),
rejectionReason: text('rejection_reason'),
// Supplier invoice attachment
invoiceFileUrl: text('invoice_file_url'),
invoiceFileName: text('invoice_file_name'),
paperlessUrl: text('paperless_url'),
paperlessDocumentId: integer('paperless_document_id'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow()
},
@@ -156,6 +161,22 @@ export const expenses = pgTable(
]
);
// ── Expense ↔ Packages junction ────────────────────────
export const expensePackages = pgTable(
'expense_packages',
{
expenseId: uuid('expense_id')
.notNull()
.references(() => expenses.id, { onDelete: 'cascade' }),
packageId: uuid('package_id')
.notNull()
.references((): any => packages.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow()
},
(table) => [primaryKey({ columns: [table.expenseId, table.packageId] })]
);
// ── Tags ───────────────────────────────────────────────
export const tags = pgTable(
@@ -1145,6 +1166,74 @@ export const procedureInstanceSteps = pgTable(
]
);
// ── Sales / Income ─────────────────────────────────────
export const saleStatusEnum = pgEnum('sale_status', ['draft', 'confirmed', 'voided']);
export const sales = pgTable(
'sales',
{
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id, { onDelete: 'cascade' }),
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
partyId: uuid('party_id').references(() => parties.id, { onDelete: 'set null' }),
invoiceId: uuid('invoice_id').references((): any => invoices.id, { onDelete: 'set null' }),
title: text('title').notNull(),
saleDate: date('sale_date').notNull(),
currency: text('currency').notNull().default('THB'),
withholdingTaxRate: numeric('withholding_tax_rate', { precision: 5, scale: 4 })
.notNull()
.default('0'),
notes: text('notes'),
status: saleStatusEnum('status').notNull().default('draft'),
voidedAt: timestamp('voided_at', { withTimezone: true }),
voidReason: text('void_reason'),
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('sales_company_status_idx').on(table.companyId, table.status),
index('sales_project_idx').on(table.projectId),
index('sales_date_idx').on(table.saleDate)
]
);
export const saleLineItems = pgTable(
'sale_line_items',
{
id: uuid('id').primaryKey().defaultRandom(),
saleId: uuid('sale_id')
.notNull()
.references(() => sales.id, { onDelete: 'cascade' }),
productName: text('product_name').notNull(),
description: text('description'),
quantity: numeric('quantity', { precision: 15, scale: 4 }).notNull().default('1'),
unitPrice: numeric('unit_price', { precision: 15, scale: 2 }).notNull(),
taxRate: numeric('tax_rate', { precision: 5, scale: 4 }).notNull().default('0'),
sortOrder: integer('sort_order').notNull().default(0),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow()
},
(table) => [index('sale_line_items_sale_idx').on(table.saleId, table.sortOrder)]
);
export const salePackages = pgTable(
'sale_packages',
{
saleId: uuid('sale_id')
.notNull()
.references(() => sales.id, { onDelete: 'cascade' }),
packageId: uuid('package_id')
.notNull()
.references(() => packages.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow()
},
(table) => [primaryKey({ columns: [table.saleId, table.packageId] })]
);
export const companyAddresses = pgTable(
'company_addresses',
{
@@ -1208,6 +1297,10 @@ export const companyLogEventEnum = pgEnum('company_log_event', [
'invoice_sent',
'invoice_paid',
'invoice_voided',
'expense_invoice_uploaded',
'sale_created',
'sale_confirmed',
'sale_voided',
'integration_connected',
'integration_disconnected',
'transaction_matched',