diff --git a/.env.example b/.env.example index 7cc0373..0cec794 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,7 @@ BODY_SIZE_LIMIT=26214400 # Company Links favicon fetching (set false to disable outbound fetches in offline dev) FAVICON_FETCH_ENABLED=true + +# Paperless-ngx integration (optional — leave blank to disable) +PAPERLESS_URL= +PAPERLESS_TOKEN= diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 47a4198..ec3e62c 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -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',