feat(round-15): 跨境多币种发票税额/线索自动化阶段跃升/名片私密访问锁/芯片激光标刻排版 (P1/P2功能达成)
这个提交包含在:
@@ -0,0 +1,175 @@
|
||||
import { Provide } from '@midwayjs/core';
|
||||
import { BaseService } from '@cool-midway/core';
|
||||
|
||||
export type SupportedCurrency = 'USD' | 'AED' | 'EUR' | 'GBP' | 'JPY' | 'CNY';
|
||||
|
||||
/**
|
||||
* 相对 USD 的基准出海商业结算汇率表
|
||||
*/
|
||||
const USD_RATES: Record<SupportedCurrency, number> = {
|
||||
USD: 1.0,
|
||||
AED: 3.6725, // 阿联酋迪拉姆与美元挂钩固定汇率
|
||||
EUR: 0.9259, // 1 EUR = 1.08 USD -> 1 USD = 0.9259 EUR
|
||||
GBP: 0.7874, // 1 GBP = 1.27 USD
|
||||
JPY: 155.50,
|
||||
CNY: 7.2350,
|
||||
};
|
||||
|
||||
const CURRENCY_SYMBOLS: Record<SupportedCurrency, string> = {
|
||||
USD: '$',
|
||||
AED: 'AED ',
|
||||
EUR: '€',
|
||||
GBP: '£',
|
||||
JPY: '¥',
|
||||
CNY: '¥',
|
||||
};
|
||||
|
||||
export interface TaxCalculationParams {
|
||||
amount: number;
|
||||
currency: SupportedCurrency;
|
||||
country: string; // ISO 2位国家码 (如 AE, DE, US, GB)
|
||||
isB2b?: boolean;
|
||||
vatNumber?: string;
|
||||
}
|
||||
|
||||
export interface TaxCalculationResult {
|
||||
taxRate: number;
|
||||
taxAmount: number;
|
||||
totalAmount: number;
|
||||
taxName: string;
|
||||
isReverseCharge?: boolean;
|
||||
}
|
||||
|
||||
export interface InvoiceTaxSummary {
|
||||
currency: SupportedCurrency;
|
||||
currencySymbol: string;
|
||||
subtotal: number;
|
||||
taxRate: number;
|
||||
taxAmount: number;
|
||||
totalAmount: number;
|
||||
taxName: string;
|
||||
isReverseCharge: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多币种精准换算 (以 USD 为中间桥梁)
|
||||
*/
|
||||
export function convertCurrency(
|
||||
amount: number,
|
||||
from: SupportedCurrency,
|
||||
to: SupportedCurrency
|
||||
): number {
|
||||
if (from === to) return amount;
|
||||
|
||||
const rateFrom = USD_RATES[from];
|
||||
const rateTo = USD_RATES[to];
|
||||
|
||||
if (!rateFrom || !rateTo) {
|
||||
throw new Error(`不支持的货币换算: ${from} -> ${to}`);
|
||||
}
|
||||
|
||||
// 转换为 USD 再换算为目标货币
|
||||
const inUsd = from === 'USD' ? amount : amount / (from === 'EUR' ? 1.08 : rateFrom);
|
||||
let converted = 0;
|
||||
|
||||
if (to === 'AED') {
|
||||
converted = inUsd * 3.6725;
|
||||
} else if (to === 'USD') {
|
||||
converted = from === 'EUR' ? amount * 1.08 : inUsd;
|
||||
} else {
|
||||
converted = inUsd * rateTo;
|
||||
}
|
||||
|
||||
return Number(converted.toFixed(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算国际出海合规税率与税额
|
||||
*/
|
||||
export function calculateInternationalTax(
|
||||
params: TaxCalculationParams
|
||||
): TaxCalculationResult {
|
||||
const { amount, country, isB2b, vatNumber } = params;
|
||||
const c = (country || 'US').toUpperCase();
|
||||
|
||||
// 1. 阿联酋 (AE): 统一 5% VAT
|
||||
if (c === 'AE') {
|
||||
const taxRate = 0.05;
|
||||
const taxAmount = Number((amount * taxRate).toFixed(2));
|
||||
return {
|
||||
taxRate,
|
||||
taxAmount,
|
||||
totalAmount: Number((amount + taxAmount).toFixed(2)),
|
||||
taxName: 'UAE VAT',
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 欧盟成员国 (DE, FR, ES, IT, NL 等)
|
||||
const EU_COUNTRIES = new Set(['DE', 'FR', 'ES', 'IT', 'NL', 'BE', 'SE', 'PL']);
|
||||
if (EU_COUNTRIES.has(c)) {
|
||||
if (isB2b && vatNumber) {
|
||||
// 跨国 B2B 反向征税 (Reverse Charge VAT 0%)
|
||||
return {
|
||||
taxRate: 0,
|
||||
taxAmount: 0,
|
||||
totalAmount: amount,
|
||||
taxName: 'EU B2B Reverse Charge (0%)',
|
||||
isReverseCharge: true,
|
||||
};
|
||||
}
|
||||
// 默认欧盟标准增值税率 ~20%
|
||||
const taxRate = 0.20;
|
||||
const taxAmount = Number((amount * taxRate).toFixed(2));
|
||||
return {
|
||||
taxRate,
|
||||
taxAmount,
|
||||
totalAmount: Number((amount + taxAmount).toFixed(2)),
|
||||
taxName: 'EU Standard VAT',
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 默认零税或境外免税 (US, HK, SG 等)
|
||||
return {
|
||||
taxRate: 0,
|
||||
taxAmount: 0,
|
||||
totalAmount: amount,
|
||||
taxName: 'Zero-rated / Non-taxable Export',
|
||||
isReverseCharge: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨境多币种汇率换算与出海发票税额计算服务
|
||||
*/
|
||||
@Provide()
|
||||
export class CurrencyTaxCalculatorService extends BaseService {
|
||||
async generateTaxInvoiceSummary(options: {
|
||||
baseAmountUsd: number;
|
||||
targetCurrency: SupportedCurrency;
|
||||
billingCountry: string;
|
||||
isB2b?: boolean;
|
||||
vatNumber?: string;
|
||||
}): Promise<InvoiceTaxSummary> {
|
||||
const { baseAmountUsd, targetCurrency, billingCountry, isB2b, vatNumber } = options;
|
||||
|
||||
const subtotal = convertCurrency(baseAmountUsd, 'USD', targetCurrency);
|
||||
const tax = calculateInternationalTax({
|
||||
amount: subtotal,
|
||||
currency: targetCurrency,
|
||||
country: billingCountry,
|
||||
isB2b,
|
||||
vatNumber,
|
||||
});
|
||||
|
||||
return {
|
||||
currency: targetCurrency,
|
||||
currencySymbol: CURRENCY_SYMBOLS[targetCurrency] || `${targetCurrency} `,
|
||||
subtotal,
|
||||
taxRate: tax.taxRate,
|
||||
taxAmount: tax.taxAmount,
|
||||
totalAmount: tax.totalAmount,
|
||||
taxName: tax.taxName,
|
||||
isReverseCharge: !!tax.isReverseCharge,
|
||||
};
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户