Files
makemd/server/src/services/FinanceService.ts

234 lines
7.6 KiB
TypeScript
Raw Normal View History

import db from '../config/database';
import { logger } from '../utils/logger';
import { FXHedgingService } from './FXHedgingService';
import { TaxService } from './TaxService';
export interface OrderRecord {
id: string;
tenantId: string;
shopId?: string;
taskId?: string;
traceId: string;
platform: string;
productId: string;
sellingPrice: number;
purchasePrice: number;
countryCode: string;
logisticsCost: number;
adBudget: number;
status: string;
}
/**
* [BIZ_FIN_01]
* @description
*/
export class FinanceService {
private static readonly TABLE_NAME = 'cf_orders';
private static readonly TRANSACTION_TABLE = 'cf_transactions';
/**
*
*/
static async initTable() {
const hasOrderTable = await db.schema.hasTable(this.TABLE_NAME);
if (!hasOrderTable) {
console.log(`📦 Creating ${this.TABLE_NAME} table...`);
await db.schema.createTable(this.TABLE_NAME, (table) => {
table.string('id', 64).primary();
table.string('tenant_id', 64).index();
table.string('platform', 32);
table.string('product_id', 64);
table.decimal('selling_price', 10, 2);
table.decimal('purchase_price', 10, 2);
table.string('country_code', 8);
table.decimal('logistics_cost', 10, 2);
table.decimal('ad_budget', 10, 2);
table.string('status', 32);
table.timestamps(true, true);
});
}
const hasTransactionTable = await db.schema.hasTable(this.TRANSACTION_TABLE);
if (!hasTransactionTable) {
console.log(`📦 Creating ${this.TRANSACTION_TABLE} table...`);
await db.schema.createTable(this.TRANSACTION_TABLE, (table) => {
table.increments('id').primary();
table.string('tenant_id', 64).index();
table.string('order_id', 64).nullable();
table.decimal('amount', 10, 2);
table.string('currency', 8).defaultTo('USD');
table.string('transaction_type', 32);
table.string('trace_id', 64);
table.json('metadata').nullable();
table.timestamp('created_at').defaultTo(db.fn.now());
});
}
}
/**
*
*/
static async recordOrder(order: OrderRecord): Promise<void> {
const { tenantId, shopId, taskId, traceId } = order;
// 1. 插入订单主表
await db(this.TABLE_NAME).insert({
id: order.id,
tenant_id: order.tenantId,
platform: order.platform,
product_id: order.productId,
selling_price: order.sellingPrice,
purchase_price: order.purchasePrice,
country_code: order.countryCode,
logistics_cost: order.logisticsCost,
ad_budget: order.adBudget,
status: order.status,
created_at: new Date(),
updated_at: new Date()
});
// 2. 实时税务计提 (BIZ_FIN_02)
const taxInfo = TaxService.calculateTax({
countryCode: order.countryCode,
baseAmount: order.sellingPrice,
currency: 'USD'
});
await this.accrueTax(order.id, {
tenantId,
amount: taxInfo.totalTax,
currency: 'USD',
type: taxInfo.isIOSS ? 'IOSS_VAT' : 'STANDARD_VAT',
traceId
});
// 3. 汇率自动对冲 (BIZ_FIN_03)
await FXHedgingService.autoHedge({
tenantId,
pair: 'USD/CNY',
amount: order.sellingPrice,
traceId
});
}
/**
* [BIZ_FIN_04]
*/
static async recordTransaction(params: {
tenantId: string;
amount: number;
currency?: string;
type: 'ORDER_REVENUE' | 'LOGISTICS_COST' | 'PLATFORM_FEE' | 'REFUND' | 'COMMISSION' | 'INCOME';
category?: string;
orderId?: string;
traceId?: string;
metadata?: any;
}) {
logger.info(`[Finance] Recording transaction: ${params.type} for Order ${params.orderId || 'N/A'} (${params.amount} ${params.currency || 'USD'})`);
// 实际业务中需插入 cf_transactions 表
await db('cf_transactions').insert({
tenant_id: params.tenantId,
order_id: params.orderId || null,
amount: params.amount,
currency: params.currency || 'USD',
transaction_type: params.type,
trace_id: params.traceId || 'system',
metadata: params.metadata ? JSON.stringify(params.metadata) : null,
created_at: new Date()
});
}
/**
* [BIZ_FIN_02]
* @description cf_tax_accruals
*/
private static async accrueTax(orderId: string, params: {
tenantId: string;
amount: number;
currency: string;
type: string;
traceId: string;
}): Promise<void> {
logger.info(`[Finance] Accruing tax for order ${orderId}: ${params.amount} ${params.currency} (${params.type})`);
// 实际业务中需插入 cf_tax_accruals
await db('cf_tax_accruals').insert({
order_id: orderId,
tenant_id: params.tenantId,
amount: params.amount,
currency: params.currency,
tax_type: params.type,
status: 'ACCRUED',
trace_id: params.traceId,
created_at: new Date(),
updated_at: new Date()
});
}
/**
* [BIZ_FIN_06]
* @description
*/
static async performAutoProfitRecon(tenantId: string, shopId: string): Promise<any> {
logger.info(`[Finance] Performing auto profit reconciliation for Tenant: ${tenantId}, Shop: ${shopId}`);
// 1. 获取所有已完成且未对账的订单
const orders = await db('cf_orders')
.where({ tenant_id: tenantId, shop_id: shopId, status: 'PUBLISHED' }) // 假设 PUBLISHED 状态代表已成单
.whereNull('reconciled_at');
const results = [];
for (const order of orders) {
try {
// 2. 聚合各项成本
const taxAccrual = await db('cf_tax_accruals').where({ order_id: order.id }).first();
const purchaseOrder = await db('cf_purchase_orders').where({ product_id: order.product_id, tenant_id: tenantId }).first();
const logisticsRoute = await db('cf_logistics_routes').where({ tenant_id: tenantId }).first(); // 简化取第一个
const purchaseCost = purchaseOrder ? purchaseOrder.total_amount / purchaseOrder.quantity : 0;
const taxAmount = taxAccrual ? taxAccrual.amount : 0;
const logisticsCost = logisticsRoute ? logisticsRoute.cost_per_kg * 0.5 : 5; // 假设 0.5kg
const netProfit = order.selling_price - purchaseCost - logisticsCost - taxAmount;
// 3. 记录对账结果
await db('cf_orders').where({ id: order.id }).update({
net_profit: netProfit,
reconciled_at: new Date(),
updated_at: new Date()
});
results.push({ orderId: order.id, netProfit });
} catch (err: any) {
logger.error(`[Finance] Recon failed for Order ${order.id}: ${err.message}`);
}
}
return { totalReconciled: results.length, details: results };
}
/**
*
*/
static async getBillPlayback(tenantId: string, shopId: string, orderId: string): Promise<any> {
const order = await db(this.TABLE_NAME)
.where({ id: orderId, tenant_id: tenantId })
.first();
if (!order) return null;
return {
orderId: order.id,
timeline: [
{ event: 'ORDER_CREATED', time: order.created_at, amount: order.selling_price },
{ event: 'PURCHASE_COMPLETED', time: order.updated_at, amount: -order.purchase_price },
{ event: 'LOGISTICS_DEDUCTED', time: order.updated_at, amount: -order.logistics_cost }
],
finalProfit: order.selling_price - order.purchase_price - order.logistics_cost
};
}
}