Add company profile schema (bank accounts, cards, addresses)

Three new per-company tables backing the upcoming Profile page:

- company_bank_accounts: bank/account name, account number, type,
  branch, SWIFT/BIC, IBAN, currency (default THB), isPrimary,
  isActive, notes
- company_cards: brand (visa/mastercard/amex/jcb/unionpay/discover/
  other), last4 (varchar(4)), cardholder, expiry month/year,
  nickname, optional FK to a bank account. Stores ONLY last 4
  digits — never the full PAN, to avoid PCI-DSS scope.
- company_addresses: type enum (legal/shipping/billing/other),
  label, recipient, full Thai address fields (subdistrict/district/
  province/postal code), country defaulting to Thailand, contact
  person + phone, isDefault, notes

Eight new audit events in companyLogEventEnum cover add/update/
remove operations on each. Page UI and export integration follow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 10:15:56 +07:00
parent 51e8cfc536
commit 92a07685b0
+106 -2
View File
@@ -10,7 +10,9 @@ import {
numeric,
date,
index,
primaryKey
primaryKey,
integer,
varchar
} from 'drizzle-orm/pg-core';
// ── Enums ──────────────────────────────────────────────
@@ -671,6 +673,100 @@ export const featureRequestVotes = pgTable(
(table) => [uniqueIndex('feature_request_votes_request_user_idx').on(table.requestId, table.userId)]
);
// ── 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'
]);
export const companyBankAccounts = pgTable(
'company_bank_accounts',
{
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id, { onDelete: 'cascade' }),
bankName: text('bank_name').notNull(),
accountName: text('account_name').notNull(),
accountNumber: text('account_number').notNull(),
accountType: text('account_type'),
branch: text('branch'),
swiftBic: text('swift_bic'),
iban: text('iban'),
currency: text('currency').notNull().default('THB'),
isPrimary: boolean('is_primary').notNull().default(false),
isActive: boolean('is_active').notNull().default(true),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow()
},
(table) => [index('company_bank_accounts_company_idx').on(table.companyId)]
);
export const companyCards = pgTable(
'company_cards',
{
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id, { onDelete: 'cascade' }),
brand: cardBrandEnum('brand').notNull(),
last4: varchar('last4', { length: 4 }).notNull(),
cardholderName: text('cardholder_name').notNull(),
expiryMonth: integer('expiry_month'),
expiryYear: integer('expiry_year'),
nickname: text('nickname'),
bankAccountId: uuid('bank_account_id').references(() => companyBankAccounts.id, {
onDelete: 'set null'
}),
isActive: boolean('is_active').notNull().default(true),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow()
},
(table) => [index('company_cards_company_idx').on(table.companyId)]
);
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', [
@@ -715,7 +811,15 @@ export const companyLogEventEnum = pgEnum('company_log_event', [
'package_status_refreshed',
'shipping_account_added',
'shipping_account_removed',
'financial_exported'
'financial_exported',
'bank_account_added',
'bank_account_updated',
'bank_account_removed',
'card_added',
'card_removed',
'address_added',
'address_updated',
'address_removed'
]);
export const companyLog = pgTable(