Reconciliation link, account CSVs in export, drop legacy bank/card tables
Validate / validate (push) Successful in 31s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 14:06:53 +07:00
parent 0d4fdb6fd7
commit 77c5d72e43
7 changed files with 249 additions and 735 deletions
+70 -47
View File
@@ -19,9 +19,9 @@ import {
externalAccounts,
externalTransactions,
users,
companyBankAccounts,
companyCards,
companyAddresses,
companyAccounts,
companyAccountTransactions,
companyDocuments,
companyDocumentVersions
} from '../db/schema.js';
@@ -73,8 +73,8 @@ export async function buildFinancialExport(
``,
`Files:`,
` company.csv — company record`,
` company_bank_accounts.csv — company bank accounts`,
` company_cards.csv — company credit/debit cards (last 4 only)`,
` company_accounts.csv — unified ledger accounts (bank, card, cash, etc.)`,
` company_account_transactions.csv — ledger transactions in the selected year`,
` company_addresses.csv — legal/shipping/billing/other addresses`,
` company_documents.csv — uploaded document metadata (files not bundled)`,
` projects.csv — all projects (active + inactive)`,
@@ -119,70 +119,93 @@ export async function buildFinancialExport(
)
);
// ── company_bank_accounts.csv ──────────────────────
// ── company_accounts.csv ───────────────────────────
{
const bankRows = await db
const acctRows = await db
.select()
.from(companyBankAccounts)
.where(eq(companyBankAccounts.companyId, companyId))
.orderBy(asc(companyBankAccounts.bankName));
.from(companyAccounts)
.where(eq(companyAccounts.companyId, companyId))
.orderBy(asc(companyAccounts.accountType), asc(companyAccounts.name));
const rows: unknown[][] = [
[
'id', 'bankName', 'accountName', 'accountNumber', 'accountType', 'branch',
'swiftBic', 'iban', 'currency', 'isPrimary', 'isActive', 'notes',
'createdAt', 'updatedAt'
'id', 'accountType', 'name', 'currency', 'isActive', 'isArchived',
'bankName', 'accountNumber', 'branch', 'swiftBic', 'iban', 'accountHolderName',
'cardBrand', 'last4', 'cardholderName', 'expiryMonth', 'expiryYear',
'creditLimit', 'statementCloseDay', 'paymentDueDay',
'externalAccountId', 'notes', 'deletedAt', 'createdAt', 'updatedAt'
]
];
for (const b of bankRows) {
for (const a of acctRows) {
rows.push([
b.id, b.bankName, b.accountName, b.accountNumber, b.accountType ?? '',
b.branch ?? '', b.swiftBic ?? '', b.iban ?? '', b.currency,
b.isPrimary, b.isActive, b.notes ?? '',
b.createdAt.toISOString(), b.updatedAt.toISOString()
a.id, a.accountType, a.name, a.currency, a.isActive, a.isArchived,
a.bankName ?? '', a.accountNumber ?? '', a.branch ?? '', a.swiftBic ?? '',
a.iban ?? '', a.accountHolderName ?? '',
a.cardBrand ?? '', a.last4 ?? '', a.cardholderName ?? '',
a.expiryMonth ?? '', a.expiryYear ?? '',
a.creditLimit ?? '', a.statementCloseDay ?? '', a.paymentDueDay ?? '',
a.externalAccountId ?? '', a.notes ?? '',
a.deletedAt ? a.deletedAt.toISOString() : '',
a.createdAt.toISOString(), a.updatedAt.toISOString()
]);
}
zip.file('company_bank_accounts.csv', withBom(csvBuild(rows)));
zip.file('company_accounts.csv', withBom(csvBuild(rows)));
}
// ── company_cards.csv ──────────────────────────────
// ── company_account_transactions.csv ───────────────
{
const cardRows = await db
const yearStartDate = new Date(`${year}-01-01T00:00:00Z`);
const yearEndDate = new Date(`${year}-12-31T23:59:59.999Z`);
const txRows = await db
.select({
id: companyCards.id,
brand: companyCards.brand,
last4: companyCards.last4,
cardholderName: companyCards.cardholderName,
expiryMonth: companyCards.expiryMonth,
expiryYear: companyCards.expiryYear,
nickname: companyCards.nickname,
bankAccountId: companyCards.bankAccountId,
bankAccountName: companyBankAccounts.bankName,
isActive: companyCards.isActive,
notes: companyCards.notes,
createdAt: companyCards.createdAt,
updatedAt: companyCards.updatedAt
id: companyAccountTransactions.id,
accountId: companyAccountTransactions.accountId,
accountName: companyAccounts.name,
type: companyAccountTransactions.type,
amount: companyAccountTransactions.amount,
currency: companyAccountTransactions.currency,
occurredAt: companyAccountTransactions.occurredAt,
description: companyAccountTransactions.description,
reference: companyAccountTransactions.reference,
counterpartyAccountId: companyAccountTransactions.counterpartyAccountId,
sourceExpenseId: companyAccountTransactions.sourceExpenseId,
sourceInvoiceId: companyAccountTransactions.sourceInvoiceId,
sourceExternalTransactionId: companyAccountTransactions.sourceExternalTransactionId,
fxRate: companyAccountTransactions.fxRate,
fxAmount: companyAccountTransactions.fxAmount,
createdAt: companyAccountTransactions.createdAt
})
.from(companyCards)
.leftJoin(companyBankAccounts, eq(companyCards.bankAccountId, companyBankAccounts.id))
.where(eq(companyCards.companyId, companyId))
.orderBy(asc(companyCards.brand));
.from(companyAccountTransactions)
.innerJoin(companyAccounts, eq(companyAccountTransactions.accountId, companyAccounts.id))
.where(
and(
eq(companyAccountTransactions.companyId, companyId),
sql`${companyAccountTransactions.occurredAt} >= ${yearStartDate}`,
sql`${companyAccountTransactions.occurredAt} <= ${yearEndDate}`
)
)
.orderBy(
asc(companyAccountTransactions.occurredAt),
asc(companyAccountTransactions.createdAt)
);
const rows: unknown[][] = [
[
'id', 'brand', 'last4', 'cardholderName', 'expiryMonth', 'expiryYear',
'nickname', 'bankAccountId', 'bankAccountName', 'isActive', 'notes',
'createdAt', 'updatedAt'
'id', 'accountId', 'accountName', 'type', 'amount', 'currency',
'occurredAt', 'description', 'reference',
'counterpartyAccountId', 'sourceExpenseId', 'sourceInvoiceId',
'sourceExternalTransactionId', 'fxRate', 'fxAmount', 'createdAt'
]
];
for (const c of cardRows) {
for (const t of txRows) {
rows.push([
c.id, c.brand, c.last4, c.cardholderName,
c.expiryMonth ?? '', c.expiryYear ?? '',
c.nickname ?? '', c.bankAccountId ?? '', c.bankAccountName ?? '',
c.isActive, c.notes ?? '',
c.createdAt.toISOString(), c.updatedAt.toISOString()
t.id, t.accountId, t.accountName, t.type, t.amount, t.currency,
t.occurredAt.toISOString(), t.description ?? '', t.reference ?? '',
t.counterpartyAccountId ?? '', t.sourceExpenseId ?? '',
t.sourceInvoiceId ?? '', t.sourceExternalTransactionId ?? '',
t.fxRate ?? '', t.fxAmount ?? '',
t.createdAt.toISOString()
]);
}
zip.file('company_cards.csv', withBom(csvBuild(rows)));
zip.file('company_account_transactions.csv', withBom(csvBuild(rows)));
}
// ── company_addresses.csv ──────────────────────────