chore: migrate to KT financial system

This commit is contained in:
woshiqp465
2025-11-04 16:06:44 +08:00
parent 2c0505b73d
commit f4cd0a5f22
289 changed files with 7362 additions and 41458 deletions

View File

@@ -16,6 +16,13 @@ interface TransactionRow {
project: null | string;
memo: null | string;
created_at: string;
status: string;
status_updated_at: null | string;
reimbursement_batch: null | string;
review_notes: null | string;
submitted_by: null | string;
approved_by: null | string;
approved_at: null | string;
is_deleted: number;
deleted_at: null | string;
}
@@ -32,8 +39,22 @@ interface TransactionPayload {
memo?: null | string;
createdAt?: string;
isDeleted?: boolean;
status?: TransactionStatus;
statusUpdatedAt?: string;
reimbursementBatch?: null | string;
reviewNotes?: null | string;
submittedBy?: null | string;
approvedBy?: null | string;
approvedAt?: null | string;
}
export type TransactionStatus =
| 'draft'
| 'pending'
| 'approved'
| 'rejected'
| 'paid';
function getExchangeRateToBase(currency: string) {
if (currency === BASE_CURRENCY) {
return 1;
@@ -49,11 +70,11 @@ function mapTransaction(row: TransactionRow) {
return {
id: row.id,
userId: 1,
type: row.type as 'expense' | 'income' | 'transfer',
amount: row.amount,
type: 'expense' as const,
amount: Math.abs(row.amount),
currency: row.currency,
exchangeRateToBase: row.exchange_rate_to_base,
amountInBase: row.amount_in_base,
amountInBase: Math.abs(row.amount_in_base),
categoryId: row.category_id ?? undefined,
accountId: row.account_id ?? undefined,
transactionDate: row.transaction_date,
@@ -61,13 +82,24 @@ function mapTransaction(row: TransactionRow) {
project: row.project ?? undefined,
memo: row.memo ?? undefined,
createdAt: row.created_at,
status: row.status as TransactionStatus,
statusUpdatedAt: row.status_updated_at ?? undefined,
reimbursementBatch: row.reimbursement_batch ?? undefined,
reviewNotes: row.review_notes ?? undefined,
submittedBy: row.submitted_by ?? undefined,
approvedBy: row.approved_by ?? undefined,
approvedAt: row.approved_at ?? undefined,
isDeleted: Boolean(row.is_deleted),
deletedAt: row.deleted_at ?? undefined,
};
}
export function fetchTransactions(
options: { includeDeleted?: boolean; type?: string } = {},
options: {
includeDeleted?: boolean;
type?: string;
statuses?: TransactionStatus[];
} = {},
) {
const clauses: string[] = [];
const params: Record<string, unknown> = {};
@@ -79,11 +111,19 @@ export function fetchTransactions(
clauses.push('type = @type');
params.type = options.type;
}
if (options.statuses && options.statuses.length > 0) {
clauses.push(
`status IN (${options.statuses.map((_, index) => `@status${index}`).join(', ')})`,
);
options.statuses.forEach((status, index) => {
params[`status${index}`] = status;
});
}
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
const stmt = db.prepare<TransactionRow>(
`SELECT id, type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, is_deleted, deleted_at FROM finance_transactions ${where} ORDER BY transaction_date DESC, id DESC`,
`SELECT id, type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, status, status_updated_at, reimbursement_batch, review_notes, submitted_by, approved_by, approved_at, is_deleted, deleted_at FROM finance_transactions ${where} ORDER BY transaction_date DESC, id DESC`,
);
return stmt.all(params).map(mapTransaction);
@@ -91,7 +131,7 @@ export function fetchTransactions(
export function getTransactionById(id: number) {
const stmt = db.prepare<TransactionRow>(
`SELECT id, type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, is_deleted, deleted_at FROM finance_transactions WHERE id = ?`,
`SELECT id, type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, status, status_updated_at, reimbursement_batch, review_notes, submitted_by, approved_by, approved_at, is_deleted, deleted_at FROM finance_transactions WHERE id = ?`,
);
const row = stmt.get(id);
return row ? mapTransaction(row) : null;
@@ -104,9 +144,20 @@ export function createTransaction(payload: TransactionPayload) {
payload.createdAt && payload.createdAt.length > 0
? payload.createdAt
: new Date().toISOString();
const status: TransactionStatus = payload.status ?? 'approved';
const statusUpdatedAt =
payload.statusUpdatedAt && payload.statusUpdatedAt.length > 0
? payload.statusUpdatedAt
: createdAt;
const approvedAt =
payload.approvedAt && payload.approvedAt.length > 0
? payload.approvedAt
: status === 'approved' || status === 'paid'
? statusUpdatedAt
: null;
const stmt = db.prepare(
`INSERT INTO finance_transactions (type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, is_deleted) VALUES (@type, @amount, @currency, @exchangeRateToBase, @amountInBase, @categoryId, @accountId, @transactionDate, @description, @project, @memo, @createdAt, 0)`,
`INSERT INTO finance_transactions (type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, status, status_updated_at, reimbursement_batch, review_notes, submitted_by, approved_by, approved_at, is_deleted) VALUES (@type, @amount, @currency, @exchangeRateToBase, @amountInBase, @categoryId, @accountId, @transactionDate, @description, @project, @memo, @createdAt, @status, @statusUpdatedAt, @reimbursementBatch, @reviewNotes, @submittedBy, @approvedBy, @approvedAt, 0)`,
);
const info = stmt.run({
@@ -122,6 +173,13 @@ export function createTransaction(payload: TransactionPayload) {
project: payload.project ?? null,
memo: payload.memo ?? null,
createdAt,
status,
statusUpdatedAt,
reimbursementBatch: payload.reimbursementBatch ?? null,
reviewNotes: payload.reviewNotes ?? null,
submittedBy: payload.submittedBy ?? null,
approvedBy: payload.approvedBy ?? null,
approvedAt,
});
return getTransactionById(Number(info.lastInsertRowid));
@@ -133,6 +191,25 @@ export function updateTransaction(id: number, payload: TransactionPayload) {
return null;
}
const nextStatus = (payload.status ?? current.status ?? 'approved') as TransactionStatus;
const statusChanged = nextStatus !== current.status;
const statusUpdatedAt =
payload.statusUpdatedAt && payload.statusUpdatedAt.length > 0
? payload.statusUpdatedAt
: statusChanged
? new Date().toISOString()
: current.statusUpdatedAt ?? current.createdAt;
const approvedAt =
payload.approvedAt && payload.approvedAt.length > 0
? payload.approvedAt
: nextStatus === 'approved' || nextStatus === 'paid'
? current.approvedAt ?? (statusChanged ? statusUpdatedAt : null)
: null;
const approvedBy =
nextStatus === 'approved' || nextStatus === 'paid'
? payload.approvedBy ?? current.approvedBy ?? null
: payload.approvedBy ?? null;
const next = {
type: payload.type ?? current.type,
amount: payload.amount ?? current.amount,
@@ -144,13 +221,21 @@ export function updateTransaction(id: number, payload: TransactionPayload) {
project: payload.project ?? current.project ?? null,
memo: payload.memo ?? current.memo ?? null,
isDeleted: payload.isDeleted ?? current.isDeleted,
status: nextStatus,
statusUpdatedAt,
reimbursementBatch:
payload.reimbursementBatch ?? current.reimbursementBatch ?? null,
reviewNotes: payload.reviewNotes ?? current.reviewNotes ?? null,
submittedBy: payload.submittedBy ?? current.submittedBy ?? null,
approvedBy,
approvedAt,
};
const exchangeRate = getExchangeRateToBase(next.currency);
const amountInBase = +(next.amount * exchangeRate).toFixed(2);
const stmt = db.prepare(
`UPDATE finance_transactions SET type = @type, amount = @amount, currency = @currency, exchange_rate_to_base = @exchangeRateToBase, amount_in_base = @amountInBase, category_id = @categoryId, account_id = @accountId, transaction_date = @transactionDate, description = @description, project = @project, memo = @memo, is_deleted = @isDeleted, deleted_at = @deletedAt WHERE id = @id`,
`UPDATE finance_transactions SET type = @type, amount = @amount, currency = @currency, exchange_rate_to_base = @exchangeRateToBase, amount_in_base = @amountInBase, category_id = @categoryId, account_id = @accountId, transaction_date = @transactionDate, description = @description, project = @project, memo = @memo, status = @status, status_updated_at = @statusUpdatedAt, reimbursement_batch = @reimbursementBatch, review_notes = @reviewNotes, submitted_by = @submittedBy, approved_by = @approvedBy, approved_at = @approvedAt, is_deleted = @isDeleted, deleted_at = @deletedAt WHERE id = @id`,
);
const deletedAt = next.isDeleted ? new Date().toISOString() : null;
@@ -168,6 +253,13 @@ export function updateTransaction(id: number, payload: TransactionPayload) {
description: next.description,
project: next.project,
memo: next.memo,
status: next.status,
statusUpdatedAt: next.statusUpdatedAt,
reimbursementBatch: next.reimbursementBatch,
reviewNotes: next.reviewNotes,
submittedBy: next.submittedBy,
approvedBy: next.approvedBy,
approvedAt: next.approvedAt,
isDeleted: next.isDeleted ? 1 : 0,
deletedAt,
});
@@ -203,12 +295,20 @@ export function replaceAllTransactions(
project?: null | string;
transactionDate: string;
type: string;
status?: TransactionStatus;
statusUpdatedAt?: string;
reimbursementBatch?: null | string;
reviewNotes?: null | string;
submittedBy?: null | string;
approvedBy?: null | string;
approvedAt?: null | string;
isDeleted?: boolean;
}>,
) {
db.prepare('DELETE FROM finance_transactions').run();
const insert = db.prepare(
`INSERT INTO finance_transactions (type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, is_deleted) VALUES (@type, @amount, @currency, @exchangeRateToBase, @amountInBase, @categoryId, @accountId, @transactionDate, @description, @project, @memo, @createdAt, 0)`,
`INSERT INTO finance_transactions (type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, status, status_updated_at, reimbursement_batch, review_notes, submitted_by, approved_by, approved_at, is_deleted) VALUES (@type, @amount, @currency, @exchangeRateToBase, @amountInBase, @categoryId, @accountId, @transactionDate, @description, @project, @memo, @createdAt, @status, @statusUpdatedAt, @reimbursementBatch, @reviewNotes, @submittedBy, @approvedBy, @approvedAt, @isDeleted)`,
);
const getRate = db.prepare(
@@ -220,15 +320,36 @@ export function replaceAllTransactions(
const row = getRate.get(item.currency) as undefined | { rate: number };
const rate = row?.rate ?? 1;
const amountInBase = +(item.amount * rate).toFixed(2);
const createdAt =
item.createdAt ??
new Date(`${item.transactionDate}T00:00:00Z`).toISOString();
const status = item.status ?? 'approved';
const statusUpdatedAt =
item.statusUpdatedAt ??
new Date(
`${item.transactionDate}T00:00:00Z`,
).toISOString();
const approvedAt =
item.approvedAt ??
(status === 'approved' || status === 'paid' ? statusUpdatedAt : null);
insert.run({
...item,
exchangeRateToBase: rate,
amountInBase,
project: item.project ?? null,
memo: item.memo ?? null,
createdAt:
item.createdAt ??
new Date(`${item.transactionDate}T00:00:00Z`).toISOString(),
createdAt,
status,
statusUpdatedAt,
reimbursementBatch: item.reimbursementBatch ?? null,
reviewNotes: item.reviewNotes ?? null,
submittedBy: item.submittedBy ?? null,
approvedBy:
status === 'approved' || status === 'paid'
? item.approvedBy ?? null
: null,
approvedAt,
isDeleted: item.isDeleted ? 1 : 0,
});
}
});