fix(api): 實裝 portfolio 匯率兜底邏輯,並在 transaction 錄入層自動攔截寫入歷史匯率

This commit is contained in:
kennethcheng 2026-04-28 01:54:54 +08:00
parent 67ceb63b08
commit effa84fe14
2 changed files with 43 additions and 14 deletions

View File

@ -73,12 +73,13 @@ function calculateCnyValueFromPrice(
}
export async function getPortfolioPositions(): Promise<Position[]> {
const allTransactions = await db
const allTransactions = await db
.select({
txType: transactions.txType,
quantity: transactions.quantity,
price: transactions.price,
exchangeRate: transactions.exchangeRate,
txCurrency: transactions.txCurrency,
assetId: transactions.assetId,
assetSymbol: assets.symbol,
assetType: assets.type,
@ -89,6 +90,14 @@ export async function getPortfolioPositions(): Promise<Position[]> {
.leftJoin(assets, eq(assets.id, transactions.assetId))
.orderBy(desc(transactions.executedAt));
const rates = await db.select({
fromCurrency: exchangeRates.fromCurrency,
toCurrency: exchangeRates.toCurrency,
rate: exchangeRates.rate,
}).from(exchangeRates);
const rateMap = buildRateMap(rates);
const holdings = new Map<string, {
assetId: string;
symbol: string;
@ -123,13 +132,27 @@ export async function getPortfolioPositions(): Promise<Position[]> {
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
const costPerUnit = new Big(tx.quantity).times(new Big(tx.price));
holding.totalCostNative = holding.totalCostNative.plus(costPerUnit);
const costCny = costPerUnit.times(new Big(tx.exchangeRate || '1'));
let appliedRate = tx.exchangeRate;
if ((!appliedRate || appliedRate === '1' || appliedRate === '1.00000000') && tx.txCurrency !== 'CNY') {
const fallbackRate = getRate(rateMap, tx.txCurrency, 'CNY');
if (fallbackRate) {
appliedRate = fallbackRate;
}
}
const costCny = costPerUnit.times(new Big(appliedRate || '1'));
holding.totalCostCny = holding.totalCostCny.plus(costCny);
} else if (tx.txType === 'SELL') {
holding.quantity = holding.quantity.minus(new Big(tx.quantity));
const sellCostPerUnit = new Big(tx.quantity).times(new Big(tx.price));
holding.totalCostNative = holding.totalCostNative.minus(sellCostPerUnit);
const sellCostCny = sellCostPerUnit.times(new Big(tx.exchangeRate || '1'));
let appliedRate = tx.exchangeRate;
if ((!appliedRate || appliedRate === '1' || appliedRate === '1.00000000') && tx.txCurrency !== 'CNY') {
const fallbackRate = getRate(rateMap, tx.txCurrency, 'CNY');
if (fallbackRate) {
appliedRate = fallbackRate;
}
}
const sellCostCny = sellCostPerUnit.times(new Big(appliedRate || '1'));
holding.totalCostCny = holding.totalCostCny.minus(sellCostCny);
} else if (tx.txType === 'AIRDROP') {
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
@ -142,14 +165,6 @@ export async function getPortfolioPositions(): Promise<Position[]> {
}
}
const rates = await db.select({
fromCurrency: exchangeRates.fromCurrency,
toCurrency: exchangeRates.toCurrency,
rate: exchangeRates.rate,
}).from(exchangeRates);
const rateMap = buildRateMap(rates);
const result: Position[] = [];
let totalCnyValue = new Big('0');
let totalPnlCny = new Big('0');

View File

@ -1,9 +1,9 @@
'use server';
import { db } from '@/db';
import { transactions, transactionTypeEnum } from '@/db/schema';
import { transactions, transactionTypeEnum, exchangeRates } from '@/db/schema';
import { z } from 'zod';
import { eq, desc } from 'drizzle-orm';
import { eq, desc, and } from 'drizzle-orm';
const createTransactionSchema = z.object({
assetId: z.string().uuid(),
@ -23,7 +23,21 @@ export async function createTransaction(params: z.infer<typeof createTransaction
}
try {
const [transaction] = await db.insert(transactions).values(validation.data).returning();
const data = { ...validation.data };
if (data.txCurrency !== 'CNY' && (!data.exchangeRate || data.exchangeRate === '1' || data.exchangeRate === '1.00000000')) {
const [latestRate] = await db
.select({ rate: exchangeRates.rate })
.from(exchangeRates)
.where(and(
eq(exchangeRates.fromCurrency, data.txCurrency),
eq(exchangeRates.toCurrency, 'CNY')
))
.limit(1);
if (latestRate) {
data.exchangeRate = latestRate.rate;
}
}
const [transaction] = await db.insert(transactions).values(data).returning();
return { success: true, data: transaction };
} catch (error: unknown) {
if (