import { relations, sql } from 'drizzle-orm'; import { pgTable, pgEnum, text, boolean, timestamp, uniqueIndex, uuid, numeric, date, index, primaryKey, integer, varchar } from 'drizzle-orm/pg-core'; // ── Enums ────────────────────────────────────────────── export const companyRoleEnum = pgEnum('company_role', ['admin', 'manager', 'user', 'viewer', 'hr', 'accountant']); export const expenseStatusEnum = pgEnum('expense_status', ['pending', 'approved', 'rejected']); // ── Users ────────────────────────────────────────────── export const users = pgTable( 'users', { id: text('id').primaryKey(), email: text('email').notNull().unique(), username: text('username').unique(), displayName: text('display_name'), passwordHash: text('password_hash'), oidcProvider: text('oidc_provider'), oidcSubject: text('oidc_subject'), isSystemAdmin: boolean('is_system_admin').notNull().default(false), disabledAt: timestamp('disabled_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ uniqueIndex('users_oidc_idx') .on(table.oidcProvider, table.oidcSubject) .where(sql`${table.oidcProvider} IS NOT NULL AND ${table.oidcSubject} IS NOT NULL`) ] ); // ── Sessions ─────────────────────────────────────────── export const sessions = pgTable('sessions', { id: text('id').primaryKey(), userId: text('user_id') .notNull() .references(() => users.id, { onDelete: 'cascade' }), expiresAt: timestamp('expires_at', { withTimezone: true }).notNull() }); // ── Companies ────────────────────────────────────────── export const companies = pgTable('companies', { id: uuid('id').primaryKey().defaultRandom(), name: text('name').notNull(), description: text('description'), totalBudget: numeric('total_budget', { precision: 15, scale: 2 }).notNull().default('0'), currency: text('currency').notNull().default('THB'), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }); // ── Company Members ──────────────────────────────────── export const companyMembers = pgTable( 'company_members', { id: uuid('id').primaryKey().defaultRandom(), userId: text('user_id') .notNull() .references(() => users.id, { onDelete: 'cascade' }), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), roles: companyRoleEnum('roles').array().notNull(), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [uniqueIndex('company_members_user_company_idx').on(table.userId, table.companyId)] ); // ── Projects ─────────────────────────────────────────── export const projects = pgTable('projects', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), name: text('name').notNull(), description: text('description'), allocatedBudget: numeric('allocated_budget', { precision: 15, scale: 2 }).notNull().default('0'), isActive: boolean('is_active').notNull().default(true), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }); // ── Categories ───────────────────────────────────────── export const categories = pgTable( 'categories', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), name: text('name').notNull(), color: text('color'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [uniqueIndex('categories_company_name_idx').on(table.companyId, table.name)] ); // ── Expenses ─────────────────────────────────────────── export const expenses = pgTable( 'expenses', { id: uuid('id').primaryKey().defaultRandom(), projectId: uuid('project_id') .notNull() .references(() => projects.id, { onDelete: 'cascade' }), categoryId: uuid('category_id').references(() => categories.id, { onDelete: 'set null' }), partyId: uuid('party_id').references((): any => parties.id, { onDelete: 'set null' }), accountId: uuid('account_id').references((): any => companyAccounts.id, { onDelete: 'set null' }), invoiceId: uuid('invoice_id').references((): any => invoices.id, { onDelete: 'set null' }), submittedBy: text('submitted_by') .notNull() .references(() => users.id), approvedBy: text('approved_by').references(() => users.id), title: text('title').notNull(), description: text('description'), amount: numeric('amount', { precision: 15, scale: 2 }).notNull(), currency: text('currency').notNull(), receiptUrl: text('receipt_url'), expenseDate: date('expense_date').notNull(), status: expenseStatusEnum('status').notNull().default('pending'), reviewedAt: timestamp('reviewed_at', { withTimezone: true }), rejectionReason: text('rejection_reason'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ index('expenses_project_status_idx').on(table.projectId, table.status), index('expenses_submitted_by_idx').on(table.submittedBy), index('expenses_date_idx').on(table.expenseDate) ] ); // ── Tags ─────────────────────────────────────────────── export const tags = pgTable( 'tags', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), name: text('name').notNull() }, (table) => [uniqueIndex('tags_company_name_idx').on(table.companyId, table.name)] ); export const expenseTags = pgTable( 'expense_tags', { expenseId: uuid('expense_id') .notNull() .references(() => expenses.id, { onDelete: 'cascade' }), tagId: uuid('tag_id') .notNull() .references(() => tags.id, { onDelete: 'cascade' }) }, (table) => [primaryKey({ columns: [table.expenseId, table.tagId] })] ); // ── Budget Allocations ───────────────────────────────── export const budgetAllocations = pgTable('budget_allocations', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), projectId: uuid('project_id') .notNull() .references(() => projects.id, { onDelete: 'cascade' }), amount: numeric('amount', { precision: 15, scale: 2 }).notNull(), allocatedBy: text('allocated_by') .notNull() .references(() => users.id), note: text('note'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }); // ── Employees ────────────────────────────────────────── export const employees = pgTable('employees', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), userId: text('user_id').references(() => users.id, { onDelete: 'set null' }), firstName: text('first_name').notNull(), lastName: text('last_name').notNull(), displayName: text('display_name'), email: text('email'), phone: text('phone'), employeeCode: text('employee_code'), position: text('position'), department: text('department'), hireDate: date('hire_date').notNull(), terminationDate: date('termination_date'), nationalId: text('national_id'), taxId: text('tax_id'), bankName: text('bank_name'), bankAccount: text('bank_account'), // Personal dateOfBirth: date('date_of_birth'), gender: text('gender'), nationality: text('nationality'), maritalStatus: text('marital_status'), // Address (Thai-specific) addressLine1: text('address_line_1'), addressLine2: text('address_line_2'), subdistrict: text('subdistrict'), district: text('district'), province: text('province'), postalCode: text('postal_code'), country: text('country'), // Emergency contact emergencyContactName: text('emergency_contact_name'), emergencyContactPhone: text('emergency_contact_phone'), emergencyContactRelationship: text('emergency_contact_relationship'), isActive: boolean('is_active').notNull().default(true), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }); export const salaryHistory = pgTable('salary_history', { id: uuid('id').primaryKey().defaultRandom(), employeeId: uuid('employee_id') .notNull() .references(() => employees.id, { onDelete: 'cascade' }), effectiveFrom: date('effective_from').notNull(), grossSalary: numeric('gross_salary', { precision: 15, scale: 2 }).notNull(), currency: text('currency').notNull().default('THB'), note: text('note'), setBy: text('set_by') .notNull() .references(() => users.id), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }); // ── Public Holidays ──────────────────────────────────── export const publicHolidays = pgTable( 'public_holidays', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), date: date('date').notNull(), name: text('name').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [uniqueIndex('public_holidays_company_date_idx').on(table.companyId, table.date)] ); // ── Leave ────────────────────────────────────────────── export const leaveStatusEnum = pgEnum('leave_status', ['pending', 'approved', 'rejected']); export const leaveTypes = pgTable( 'leave_types', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), name: text('name').notNull(), defaultDaysPerYear: numeric('default_days_per_year', { precision: 5, scale: 2 }), isPaid: boolean('is_paid').notNull().default(true), color: text('color'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [uniqueIndex('leave_types_company_name_idx').on(table.companyId, table.name)] ); export const leaveBalances = pgTable( 'leave_balances', { id: uuid('id').primaryKey().defaultRandom(), employeeId: uuid('employee_id') .notNull() .references(() => employees.id, { onDelete: 'cascade' }), leaveTypeId: uuid('leave_type_id') .notNull() .references(() => leaveTypes.id, { onDelete: 'cascade' }), year: numeric('year', { precision: 4, scale: 0 }).notNull(), allocated: numeric('allocated', { precision: 5, scale: 2 }).notNull().default('0'), used: numeric('used', { precision: 5, scale: 2 }).notNull().default('0'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ uniqueIndex('leave_balances_emp_type_year_idx').on( table.employeeId, table.leaveTypeId, table.year ) ] ); export const leaveRequests = pgTable('leave_requests', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), employeeId: uuid('employee_id') .notNull() .references(() => employees.id, { onDelete: 'cascade' }), leaveTypeId: uuid('leave_type_id') .notNull() .references(() => leaveTypes.id, { onDelete: 'cascade' }), startDate: date('start_date').notNull(), endDate: date('end_date').notNull(), days: numeric('days', { precision: 5, scale: 2 }).notNull(), reason: text('reason'), status: leaveStatusEnum('status').notNull().default('pending'), reviewedBy: text('reviewed_by').references(() => users.id), reviewedAt: timestamp('reviewed_at', { withTimezone: true }), rejectionReason: text('rejection_reason'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }); // ── Payslips ─────────────────────────────────────────── export const payslipStatusEnum = pgEnum('payslip_status', ['draft', 'finalized', 'paid']); export const payslipLineTypeEnum = pgEnum('payslip_line_type', ['earning', 'deduction']); export const payslips = pgTable( 'payslips', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), employeeId: uuid('employee_id') .notNull() .references(() => employees.id, { onDelete: 'cascade' }), periodYear: numeric('period_year', { precision: 4, scale: 0 }).notNull(), periodMonth: numeric('period_month', { precision: 2, scale: 0 }).notNull(), grossSalary: numeric('gross_salary', { precision: 15, scale: 2 }).notNull(), overtime: numeric('overtime', { precision: 15, scale: 2 }).notNull().default('0'), bonus: numeric('bonus', { precision: 15, scale: 2 }).notNull().default('0'), otherEarnings: numeric('other_earnings', { precision: 15, scale: 2 }).notNull().default('0'), ssoEmployee: numeric('sso_employee', { precision: 15, scale: 2 }).notNull().default('0'), ssoEmployer: numeric('sso_employer', { precision: 15, scale: 2 }).notNull().default('0'), incomeTax: numeric('income_tax', { precision: 15, scale: 2 }).notNull().default('0'), otherDeductions: numeric('other_deductions', { precision: 15, scale: 2 }).notNull().default('0'), netPay: numeric('net_pay', { precision: 15, scale: 2 }).notNull(), currency: text('currency').notNull().default('THB'), status: payslipStatusEnum('status').notNull().default('draft'), finalizedAt: timestamp('finalized_at', { withTimezone: true }), paidAt: timestamp('paid_at', { withTimezone: true }), generatedBy: text('generated_by') .notNull() .references(() => users.id), pdfPath: text('pdf_path'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ uniqueIndex('payslips_emp_period_idx').on(table.employeeId, table.periodYear, table.periodMonth) ] ); export const payslipLineItems = pgTable('payslip_line_items', { id: uuid('id').primaryKey().defaultRandom(), payslipId: uuid('payslip_id') .notNull() .references(() => payslips.id, { onDelete: 'cascade' }), type: payslipLineTypeEnum('type').notNull(), label: text('label').notNull(), amount: numeric('amount', { precision: 15, scale: 2 }).notNull(), isStatutory: boolean('is_statutory').notNull().default(false) }); // ── Parties (Customers / Suppliers) ──────────────────── export const partyTypeEnum = pgEnum('party_type', ['customer', 'supplier', 'both']); export const parties = pgTable('parties', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), type: partyTypeEnum('type').notNull().default('customer'), name: text('name').notNull(), contactPerson: text('contact_person'), email: text('email'), phone: text('phone'), website: text('website'), taxId: text('tax_id'), addressLine1: text('address_line_1'), addressLine2: text('address_line_2'), city: text('city'), postalCode: text('postal_code'), country: text('country'), paymentTerms: text('payment_terms'), notes: text('notes'), isActive: boolean('is_active').notNull().default(true), deletedAt: timestamp('deleted_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }); // ── Invoices ─────────────────────────────────────────── export const invoiceDirectionEnum = pgEnum('invoice_direction', ['incoming', 'outgoing']); export const invoiceStatusEnum = pgEnum('invoice_status', [ 'draft', 'sent', 'paid', 'overdue', 'cancelled', 'voided' ]); export const invoices = pgTable( 'invoices', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), partyId: uuid('party_id') .notNull() .references(() => parties.id, { onDelete: 'restrict' }), direction: invoiceDirectionEnum('direction').notNull(), invoiceNumber: text('invoice_number').notNull(), issueDate: date('issue_date').notNull(), dueDate: date('due_date'), subtotal: numeric('subtotal', { precision: 15, scale: 2 }).notNull(), vat: numeric('vat', { precision: 15, scale: 2 }).notNull().default('0'), total: numeric('total', { precision: 15, scale: 2 }).notNull(), currency: text('currency').notNull().default('THB'), status: invoiceStatusEnum('status').notNull().default('draft'), expenseId: uuid('expense_id').references(() => expenses.id, { onDelete: 'set null' }), paymentAccountId: uuid('payment_account_id').references((): any => companyAccounts.id, { onDelete: 'set null' }), notes: text('notes'), pdfPath: text('pdf_path'), voidedAt: timestamp('voided_at', { withTimezone: true }), voidReason: text('void_reason'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ uniqueIndex('invoices_company_direction_number_idx').on( table.companyId, table.direction, table.invoiceNumber ) ] ); export const invoiceLineItems = pgTable('invoice_line_items', { id: uuid('id').primaryKey().defaultRandom(), invoiceId: uuid('invoice_id') .notNull() .references(() => invoices.id, { onDelete: 'cascade' }), description: text('description').notNull(), quantity: numeric('quantity', { precision: 10, scale: 2 }).notNull().default('1'), unitPrice: numeric('unit_price', { precision: 15, scale: 2 }).notNull(), total: numeric('total', { precision: 15, scale: 2 }).notNull() }); // ── External Integrations ────────────────────────────── export const integrationProviderEnum = pgEnum('integration_provider', [ 'kasikorn_kbiz', 'etherfi', 'manual' ]); export const txDirectionEnum = pgEnum('tx_direction', ['credit', 'debit']); export const externalAccounts = pgTable('external_accounts', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), provider: integrationProviderEnum('provider').notNull(), displayName: text('display_name').notNull(), accountIdentifier: text('account_identifier').notNull(), credentialsEncrypted: text('credentials_encrypted'), isActive: boolean('is_active').notNull().default(true), lastSyncedAt: timestamp('last_synced_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }); export const externalTransactions = pgTable( 'external_transactions', { id: uuid('id').primaryKey().defaultRandom(), accountId: uuid('account_id') .notNull() .references(() => externalAccounts.id, { onDelete: 'cascade' }), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), externalId: text('external_id').notNull(), occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull(), amount: numeric('amount', { precision: 15, scale: 2 }).notNull(), currency: text('currency').notNull(), direction: txDirectionEnum('direction').notNull(), description: text('description'), counterparty: text('counterparty'), matchedExpenseId: uuid('matched_expense_id').references(() => expenses.id, { onDelete: 'set null' }), rawPayload: text('raw_payload'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [uniqueIndex('external_tx_account_extid_idx').on(table.accountId, table.externalId)] ); // ── Package Tracking ─────────────────────────────────── export const packageDirectionEnum = pgEnum('package_direction', ['incoming', 'outgoing']); export const packageStatusEnum = pgEnum('package_status', [ 'pending', 'in_transit', 'out_for_delivery', 'delivered', 'exception', 'returned', 'cancelled' ]); export const carrierEnum = pgEnum('carrier', [ 'ups', 'fedex', 'dhl', 'usps', 'flash_express', 'kerry_th', 'jnt_express', 'thailand_post', 'other' ]); export const packages = pgTable( 'packages', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), direction: packageDirectionEnum('direction').notNull(), carrier: carrierEnum('carrier').notNull(), trackingNumber: text('tracking_number').notNull(), status: packageStatusEnum('status').notNull().default('pending'), currentLocation: text('current_location'), description: text('description'), recipientName: text('recipient_name'), estimatedDelivery: date('estimated_delivery'), shippedAt: timestamp('shipped_at', { withTimezone: true }), deliveredAt: timestamp('delivered_at', { withTimezone: true }), weightKg: numeric('weight_kg', { precision: 10, scale: 3 }), shippingCost: numeric('shipping_cost', { precision: 15, scale: 2 }), currency: text('currency').notNull().default('THB'), invoiceId: uuid('invoice_id').references(() => invoices.id, { onDelete: 'set null' }), customsInvoiceId: uuid('customs_invoice_id').references(() => invoices.id, { onDelete: 'set null' }), expenseId: uuid('expense_id').references(() => expenses.id, { onDelete: 'set null' }), partyId: uuid('party_id').references(() => parties.id, { onDelete: 'set null' }), notes: text('notes'), lastRefreshedAt: timestamp('last_refreshed_at', { withTimezone: true }), createdBy: text('created_by') .notNull() .references(() => users.id), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ uniqueIndex('packages_company_carrier_tracking_idx').on( table.companyId, table.carrier, table.trackingNumber ), index('packages_company_status_idx').on(table.companyId, table.status) ] ); export const packageEvents = pgTable( 'package_events', { id: uuid('id').primaryKey().defaultRandom(), packageId: uuid('package_id') .notNull() .references(() => packages.id, { onDelete: 'cascade' }), occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull(), status: packageStatusEnum('status'), location: text('location'), description: text('description'), source: text('source').notNull().default('manual'), rawPayload: text('raw_payload'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [index('package_events_package_idx').on(table.packageId, table.occurredAt)] ); export const shippingAccounts = pgTable( 'shipping_accounts', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), carrier: carrierEnum('carrier').notNull(), displayName: text('display_name'), credentialsEncrypted: text('credentials_encrypted'), isActive: boolean('is_active').notNull().default(true), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [uniqueIndex('shipping_accounts_company_carrier_idx').on(table.companyId, table.carrier)] ); // ── Feature Requests ─────────────────────────────────── export const featureRequestStatusEnum = pgEnum('feature_request_status', [ 'open', 'in_review', 'waiting_for_checks', 'in_progress', 'resolved', 'closed' ]); export const featureRequests = pgTable( 'feature_requests', { id: uuid('id').primaryKey().defaultRandom(), title: text('title').notNull(), description: text('description'), status: featureRequestStatusEnum('status').notNull().default('open'), submittedBy: text('submitted_by') .notNull() .references(() => users.id, { onDelete: 'set null' }), statusChangedBy: text('status_changed_by').references(() => users.id, { onDelete: 'set null' }), statusChangedAt: timestamp('status_changed_at', { withTimezone: true }), statusNote: text('status_note'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [index('feature_requests_status_idx').on(table.status)] ); export const featureRequestVotes = pgTable( 'feature_request_votes', { id: uuid('id').primaryKey().defaultRandom(), requestId: uuid('request_id') .notNull() .references(() => featureRequests.id, { onDelete: 'cascade' }), userId: text('user_id') .notNull() .references(() => users.id, { onDelete: 'cascade' }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [uniqueIndex('feature_request_votes_request_user_idx').on(table.requestId, table.userId)] ); // ── Company Documents ────────────────────────────────── export const companyDocumentCategoryEnum = pgEnum('company_document_category', [ 'dbd_registration', 'affidavit', 'memorandum', 'articles_of_association', 'vat_registration', 'tax_id_document', 'bank_document', 'director_id', 'director_signature_card', 'shareholder_list', 'annual_filing', 'contract', 'license', 'insurance', 'other' ]); export const companyDocuments = pgTable( 'company_documents', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), category: companyDocumentCategoryEnum('category').notNull(), customLabel: text('custom_label'), title: text('title').notNull(), description: text('description'), expiresAt: date('expires_at'), notes: text('notes'), 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('company_documents_company_category_idx').on(table.companyId, table.category)] ); export const companyDocumentVersions = pgTable( 'company_document_versions', { id: uuid('id').primaryKey().defaultRandom(), documentId: uuid('document_id') .notNull() .references(() => companyDocuments.id, { onDelete: 'cascade' }), versionNumber: integer('version_number').notNull(), fileName: text('file_name').notNull(), storedPath: text('stored_path').notNull(), mimeType: text('mime_type').notNull(), sizeBytes: integer('size_bytes').notNull(), uploadedBy: text('uploaded_by').references(() => users.id, { onDelete: 'set null' }), uploadedAt: timestamp('uploaded_at', { withTimezone: true }).notNull().defaultNow(), comment: text('comment') }, (table) => [ uniqueIndex('company_document_versions_doc_version_idx').on(table.documentId, table.versionNumber) ] ); // ── Company Links (internal tool & bookmark hub) ────── export const companyLinkCategoryEnum = pgEnum('company_link_category', [ 'internal_tool', 'communication', 'social_media', 'analytics', 'banking', 'government', 'storage', 'marketing', 'development', 'website', 'other' ]); export const companyLinks = pgTable( 'company_links', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), category: companyLinkCategoryEnum('category').notNull(), customLabel: text('custom_label'), title: text('title').notNull(), url: text('url').notNull(), description: text('description'), faviconUrl: text('favicon_url'), faviconFetchedAt: timestamp('favicon_fetched_at', { withTimezone: true }), sortOrder: integer('sort_order').notNull().default(0), 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('company_links_company_cat_sort_idx').on(table.companyId, table.category, table.sortOrder) ] ); export const userCompanyLinks = pgTable( 'user_company_links', { id: uuid('id').primaryKey().defaultRandom(), userId: text('user_id') .notNull() .references(() => users.id, { onDelete: 'cascade' }), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), category: companyLinkCategoryEnum('category').notNull(), customLabel: text('custom_label'), title: text('title').notNull(), url: text('url').notNull(), description: text('description'), faviconUrl: text('favicon_url'), faviconFetchedAt: timestamp('favicon_fetched_at', { withTimezone: true }), sortOrder: integer('sort_order').notNull().default(0), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ index('user_company_links_user_company_idx').on( table.userId, table.companyId, table.category, table.sortOrder ) ] ); // ── Company Profile (bank accounts, cards, addresses) ── export const companyAddressTypeEnum = pgEnum('company_address_type', [ 'legal', 'shipping', 'billing', 'other' ]); export const cardBrandEnum = pgEnum('card_brand', [ 'visa', 'mastercard', 'amex', 'jcb', 'unionpay', 'discover', 'other' ]); // ── Company Accounts (unified ledger) ────────────────── export const companyAccountTypeEnum = pgEnum('company_account_type', [ 'bank', 'credit_card', 'cash', 'mobile_money', 'petty_cash', 'loan', 'other' ]); export const companyAccountTxnTypeEnum = pgEnum('company_account_txn_type', [ 'opening_balance', 'expense', 'invoice_payment', 'transfer_in', 'transfer_out', 'deposit', 'adjustment', 'reconciliation' ]); export const companyAccounts = pgTable( 'company_accounts', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), accountType: companyAccountTypeEnum('account_type').notNull(), name: text('name').notNull(), currency: text('currency').notNull().default('THB'), isActive: boolean('is_active').notNull().default(true), isArchived: boolean('is_archived').notNull().default(false), notes: text('notes'), sortOrder: integer('sort_order').notNull().default(0), createdBy: text('created_by').references(() => users.id, { onDelete: 'set null' }), deletedAt: timestamp('deleted_at', { withTimezone: true }), // Bank-specific bankName: text('bank_name'), accountNumber: text('account_number'), branch: text('branch'), swiftBic: text('swift_bic'), iban: text('iban'), accountHolderName: text('account_holder_name'), // Card-specific cardBrand: cardBrandEnum('card_brand'), last4: varchar('last4', { length: 4 }), cardholderName: text('cardholder_name'), expiryMonth: integer('expiry_month'), expiryYear: integer('expiry_year'), creditLimit: numeric('credit_limit', { precision: 15, scale: 2 }), statementCloseDay: integer('statement_close_day'), paymentDueDay: integer('payment_due_day'), // Banking integration link externalAccountId: uuid('external_account_id').references(() => externalAccounts.id, { onDelete: 'set null' }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ index('company_accounts_company_type_idx').on(table.companyId, table.accountType), index('company_accounts_company_archived_idx').on(table.companyId, table.isArchived) ] ); export const companyAccountTransactions = pgTable( 'company_account_transactions', { id: uuid('id').primaryKey().defaultRandom(), accountId: uuid('account_id') .notNull() .references(() => companyAccounts.id, { onDelete: 'cascade' }), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), type: companyAccountTxnTypeEnum('type').notNull(), amount: numeric('amount', { precision: 15, scale: 2 }).notNull(), currency: text('currency').notNull(), occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull(), description: text('description'), reference: text('reference'), counterpartyAccountId: uuid('counterparty_account_id').references( (): any => companyAccounts.id, { onDelete: 'set null' } ), sourceExpenseId: uuid('source_expense_id').references(() => expenses.id, { onDelete: 'set null' }), sourceInvoiceId: uuid('source_invoice_id').references(() => invoices.id, { onDelete: 'set null' }), sourceExternalTransactionId: uuid('source_external_transaction_id').references( () => externalTransactions.id, { onDelete: 'set null' } ), fxRate: numeric('fx_rate', { precision: 18, scale: 8 }), fxAmount: numeric('fx_amount', { precision: 15, scale: 2 }), createdBy: text('created_by').references(() => users.id, { onDelete: 'set null' }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ index('company_account_txns_account_occurred_idx').on(table.accountId, table.occurredAt), index('company_account_txns_company_occurred_idx').on(table.companyId, table.occurredAt), index('company_account_txns_expense_idx').on(table.sourceExpenseId), index('company_account_txns_invoice_idx').on(table.sourceInvoiceId) ] ); // ── 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' }), serviceAccountId: uuid('service_account_id').references( (): any => companyServiceAccounts.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) ] ); // ── Service Accounts ─────────────────────────────────── export const serviceAccountTypeEnum = pgEnum('service_account_type', [ 'electricity', 'water', 'gas', 'internet', 'phone', 'shipping', 'insurance', 'tax_registration', 'social_security', 'customs', 'other' ]); export const companyServiceAccounts = pgTable( 'company_service_accounts', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), type: serviceAccountTypeEnum('type').notNull(), providerName: text('provider_name').notNull(), accountNumber: text('account_number').notNull(), customLabel: text('custom_label'), contactPhone: text('contact_phone'), websiteUrl: text('website_url'), notes: text('notes'), isActive: boolean('is_active').notNull().default(true), 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('company_service_accounts_company_type_idx').on(table.companyId, table.type) ] ); // ── Procedures & Checklists ──────────────────────────── export const procedureInstanceStatusEnum = pgEnum('procedure_instance_status', [ 'in_progress', 'completed', 'cancelled' ]); export const procedureTemplates = pgTable( 'procedure_templates', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), title: text('title').notNull(), description: text('description'), category: text('category'), isPublished: boolean('is_published').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('procedure_templates_company_idx').on(table.companyId)] ); export const procedureSteps = pgTable( 'procedure_steps', { id: uuid('id').primaryKey().defaultRandom(), templateId: uuid('template_id') .notNull() .references(() => procedureTemplates.id, { onDelete: 'cascade' }), stepNumber: integer('step_number').notNull(), title: text('title').notNull(), description: text('description'), assigneeRole: text('assignee_role'), estimatedMinutes: integer('estimated_minutes'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ uniqueIndex('procedure_steps_template_step_idx').on(table.templateId, table.stepNumber) ] ); export const procedureInstances = pgTable( 'procedure_instances', { id: uuid('id').primaryKey().defaultRandom(), templateId: uuid('template_id') .notNull() .references(() => procedureTemplates.id, { onDelete: 'restrict' }), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), title: text('title').notNull(), status: procedureInstanceStatusEnum('status').notNull().default('in_progress'), startedBy: text('started_by').references(() => users.id, { onDelete: 'set null' }), completedAt: timestamp('completed_at', { withTimezone: true }), cancelledAt: timestamp('cancelled_at', { withTimezone: true }), notes: text('notes'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ index('procedure_instances_company_status_idx').on(table.companyId, table.status) ] ); export const procedureInstanceSteps = pgTable( 'procedure_instance_steps', { id: uuid('id').primaryKey().defaultRandom(), instanceId: uuid('instance_id') .notNull() .references(() => procedureInstances.id, { onDelete: 'cascade' }), stepId: uuid('step_id') .notNull() .references(() => procedureSteps.id, { onDelete: 'restrict' }), stepNumber: integer('step_number').notNull(), title: text('title').notNull(), description: text('description'), isCompleted: boolean('is_completed').notNull().default(false), completedBy: text('completed_by').references(() => users.id, { onDelete: 'set null' }), completedAt: timestamp('completed_at', { withTimezone: true }), notes: text('notes') }, (table) => [ index('procedure_instance_steps_instance_idx').on(table.instanceId, table.stepNumber) ] ); export const companyAddresses = pgTable( 'company_addresses', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), type: companyAddressTypeEnum('type').notNull(), label: text('label'), recipient: text('recipient'), addressLine1: text('address_line_1'), addressLine2: text('address_line_2'), subdistrict: text('subdistrict'), district: text('district'), province: text('province'), postalCode: text('postal_code'), country: text('country').notNull().default('Thailand'), contactPerson: text('contact_person'), contactPhone: text('contact_phone'), isDefault: boolean('is_default').notNull().default(false), notes: text('notes'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [index('company_addresses_company_type_idx').on(table.companyId, table.type)] ); // ── Company Log (Audit Trail) ────────────────────────── export const companyLogEventEnum = pgEnum('company_log_event', [ 'company_created', 'company_updated', 'budget_initial', 'budget_added', 'budget_allocated', 'budget_deallocated', 'project_created', 'project_updated', 'member_added', 'member_removed', 'member_role_changed', 'expense_submitted', 'expense_approved', 'expense_rejected', 'category_created', 'import_completed', 'employee_created', 'employee_updated', 'employee_terminated', 'salary_changed', 'holiday_added', 'leave_type_created', 'leave_submitted', 'leave_approved', 'leave_rejected', 'payslip_generated', 'payslip_finalized', 'payslip_paid', 'party_created', 'invoice_created', 'invoice_sent', 'invoice_paid', 'invoice_voided', 'integration_connected', 'integration_disconnected', 'transaction_matched', 'package_created', 'package_updated', 'package_delivered', 'package_status_refreshed', 'shipping_account_added', 'shipping_account_removed', 'financial_exported', 'bank_account_added', 'bank_account_updated', 'bank_account_removed', 'card_added', 'card_removed', 'address_added', 'address_updated', 'address_removed', 'document_uploaded', 'document_version_added', 'document_metadata_updated', 'document_deleted', 'link_added', 'link_updated', 'link_deleted', 'account_created', 'account_updated', 'account_archived', 'account_deleted', 'account_transaction_added', 'account_transfer_posted', 'account_reconciled', 'recurring_bill_created', 'recurring_bill_updated', 'recurring_bill_deleted', 'recurring_bill_paused', 'recurring_bill_resumed', 'recurring_bill_skipped', 'recurring_bill_posted', 'service_account_created', 'service_account_updated', 'service_account_deleted', 'procedure_template_created', 'procedure_template_updated', 'procedure_template_deleted', 'procedure_instance_started', 'procedure_step_completed', 'procedure_instance_completed', 'procedure_instance_cancelled' ]); export const companyLog = pgTable( 'company_log', { id: uuid('id').primaryKey().defaultRandom(), companyId: uuid('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), userId: text('user_id').references(() => users.id), event: companyLogEventEnum('event').notNull(), description: text('description').notNull(), metadata: text('metadata'), // JSON string for extra context (amounts, names, etc.) createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [index('company_log_company_idx').on(table.companyId, table.createdAt)] ); // ── Relations ────────────────────────────────────────── export const usersRelations = relations(users, ({ many }) => ({ sessions: many(sessions), companyMemberships: many(companyMembers), submittedExpenses: many(expenses, { relationName: 'submittedExpenses' }), approvedExpenses: many(expenses, { relationName: 'approvedExpenses' }) })); export const sessionsRelations = relations(sessions, ({ one }) => ({ user: one(users, { fields: [sessions.userId], references: [users.id] }) })); export const companiesRelations = relations(companies, ({ many }) => ({ members: many(companyMembers), projects: many(projects), categories: many(categories), tags: many(tags), budgetAllocations: many(budgetAllocations), logs: many(companyLog) })); export const companyMembersRelations = relations(companyMembers, ({ one }) => ({ user: one(users, { fields: [companyMembers.userId], references: [users.id] }), company: one(companies, { fields: [companyMembers.companyId], references: [companies.id] }) })); export const projectsRelations = relations(projects, ({ one, many }) => ({ company: one(companies, { fields: [projects.companyId], references: [companies.id] }), expenses: many(expenses), budgetAllocations: many(budgetAllocations) })); export const categoriesRelations = relations(categories, ({ one, many }) => ({ company: one(companies, { fields: [categories.companyId], references: [companies.id] }), expenses: many(expenses) })); export const expensesRelations = relations(expenses, ({ one, many }) => ({ project: one(projects, { fields: [expenses.projectId], references: [projects.id] }), category: one(categories, { fields: [expenses.categoryId], references: [categories.id] }), submitter: one(users, { fields: [expenses.submittedBy], references: [users.id], relationName: 'submittedExpenses' }), approver: one(users, { fields: [expenses.approvedBy], references: [users.id], relationName: 'approvedExpenses' }), expenseTags: many(expenseTags) })); export const tagsRelations = relations(tags, ({ one, many }) => ({ company: one(companies, { fields: [tags.companyId], references: [companies.id] }), expenseTags: many(expenseTags) })); export const expenseTagsRelations = relations(expenseTags, ({ one }) => ({ expense: one(expenses, { fields: [expenseTags.expenseId], references: [expenses.id] }), tag: one(tags, { fields: [expenseTags.tagId], references: [tags.id] }) })); export const companyLogRelations = relations(companyLog, ({ one }) => ({ company: one(companies, { fields: [companyLog.companyId], references: [companies.id] }), user: one(users, { fields: [companyLog.userId], references: [users.id] }) })); export const budgetAllocationsRelations = relations(budgetAllocations, ({ one }) => ({ company: one(companies, { fields: [budgetAllocations.companyId], references: [companies.id] }), project: one(projects, { fields: [budgetAllocations.projectId], references: [projects.id] }), allocator: one(users, { fields: [budgetAllocations.allocatedBy], references: [users.id] }) }));