137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
'use server';
|
|
|
|
import { db } from '@/db';
|
|
import { transactions, transactionTypeEnum, exchangeRates, assets as assetsTable } from '@/db/schema';
|
|
import { z } from 'zod';
|
|
import { eq, desc, and } from 'drizzle-orm';
|
|
|
|
const createTransactionSchema = z.object({
|
|
assetId: z.string().uuid(),
|
|
txType: z.enum(['BUY', 'SELL', 'DIVIDEND', 'AIRDROP', 'FEE']),
|
|
quantity: z.string().regex(/^-?\d+(\.\d+)?$/, '数量必须是数字字符串'),
|
|
price: z.string().regex(/^-?\d+(\.\d+)?$/, '价格必须是数字字符串'),
|
|
fee: z.string().regex(/^-?\d+(\.\d+)?$/, '手续费必须是数字字符串').default('0'),
|
|
txCurrency: z.string().min(1),
|
|
exchangeRate: z.string().regex(/^-?\d+(\.\d+)?$/, '汇率必须是数字字符串').default('1'),
|
|
executedAt: z.coerce.date(),
|
|
});
|
|
|
|
const exchangeToCurrencyMap: Record<string, string> = {
|
|
'US': 'USD',
|
|
'HKEX': 'HKD',
|
|
'SSE': 'CNY',
|
|
'SZSE': 'CNY',
|
|
};
|
|
|
|
function getCurrencyFromExchange(exchange: string): string {
|
|
return exchangeToCurrencyMap[exchange] || 'USD';
|
|
}
|
|
|
|
export async function createTransaction(params: z.infer<typeof createTransactionSchema>) {
|
|
const validation = createTransactionSchema.safeParse(params);
|
|
if (!validation.success) {
|
|
return { success: false, error: validation.error.issues[0].message };
|
|
}
|
|
|
|
try {
|
|
const asset = await db
|
|
.select({ exchange: assetsTable.exchange })
|
|
.from(assetsTable)
|
|
.where(eq(assetsTable.id, validation.data.assetId))
|
|
.limit(1);
|
|
|
|
let data = { ...validation.data };
|
|
|
|
if (asset.length > 0 && (!data.txCurrency || data.txCurrency.length === 0)) {
|
|
data.txCurrency = getCurrencyFromExchange(asset[0].exchange);
|
|
}
|
|
|
|
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 (
|
|
error &&
|
|
typeof error === 'object' &&
|
|
'code' in error &&
|
|
(error as { code: string }).code === '23503'
|
|
) {
|
|
return { success: false, error: '资产不存在' };
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const updateTransactionSchema = z.object({
|
|
id: z.string().uuid(),
|
|
txType: z.enum(['BUY', 'SELL', 'DIVIDEND', 'AIRDROP', 'FEE']).optional(),
|
|
quantity: z.string().regex(/^-?\d+(\.\d+)?$/, '数量必须是数字字符串').optional(),
|
|
price: z.string().regex(/^-?\d+(\.\d+)?$/, '价格必须是数字字符串').optional(),
|
|
fee: z.string().regex(/^-?\d+(\.\d+)?$/, '手续费必须是数字字符串').optional(),
|
|
txCurrency: z.string().min(1).optional(),
|
|
exchangeRate: z.string().regex(/^-?\d+(\.\d+)?$/, '汇率必须是数字字符串').optional(),
|
|
executedAt: z.coerce.date().optional(),
|
|
});
|
|
|
|
export async function updateTransaction(params: z.infer<typeof updateTransactionSchema>) {
|
|
const validation = updateTransactionSchema.safeParse(params);
|
|
if (!validation.success) {
|
|
return { success: false, error: validation.error.issues[0].message };
|
|
}
|
|
|
|
const { id, ...updates } = validation.data;
|
|
const filteredUpdates = Object.fromEntries(
|
|
Object.entries(updates).filter(([, v]) => v !== undefined)
|
|
);
|
|
|
|
if (Object.keys(filteredUpdates).length === 0) {
|
|
return { success: false, error: 'No fields to update' };
|
|
}
|
|
|
|
try {
|
|
await db.update(transactions).set(filteredUpdates).where(eq(transactions.id, id));
|
|
return { success: true };
|
|
} catch (error: unknown) {
|
|
if (
|
|
error &&
|
|
typeof error === 'object' &&
|
|
'code' in error &&
|
|
(error as { code: string }).code === '23503'
|
|
) {
|
|
return { success: false, error: '资产不存在' };
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function deleteTransaction(id: string) {
|
|
try {
|
|
await db.delete(transactions).where(eq(transactions.id, id));
|
|
return { success: true };
|
|
} catch (error: unknown) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getTransactions(assetId?: string) {
|
|
if (assetId) {
|
|
return db
|
|
.select()
|
|
.from(transactions)
|
|
.where(eq(transactions.assetId, assetId))
|
|
.orderBy(desc(transactions.executedAt));
|
|
}
|
|
return db.select().from(transactions).orderBy(desc(transactions.executedAt));
|
|
} |