| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869 |
- /**
- * core/quote.js —— 核心库(core):纯 ESM 报价计算。
- *
- * 铁律:不碰 window / document / localStorage / fetch,不 import lit。
- * 所有不确定输入(时间、id、费率)都从参数注入 → 可在 Node 里确定化单测。
- */
- /**
- * @typedef {{ sku: string, price: number, qty: number }} Item
- * @typedef {{
- * taxRate?: number, discount?: number, currency?: string,
- * now?: () => number, id?: () => string,
- * }} QuoteOptions
- */
- /**
- * 计算一张报价单。
- * @param {Item[]} items
- * @param {QuoteOptions} [opts]
- */
- export function quote(items, opts = {}) {
- const {
- taxRate = 0.06,
- discount = 0,
- currency = '¥',
- now = () => Date.now(),
- id = () => `q-${Math.random().toString(36).slice(2, 10)}`,
- } = opts;
- if (!Array.isArray(items)) throw badInput('E_ITEMS', 'items 必须是数组');
- for (const it of items) {
- if (!(it.price >= 0) || !(it.qty >= 1) || !Number.isInteger(it.qty) || !it.sku) {
- throw badInput('E_ITEM', `无效条目:${JSON.stringify(it)}`);
- }
- }
- if (discount < 0 || discount > 1) throw badInput('E_DISCOUNT', 'discount 必须在 [0, 1]');
- const subtotal = items.reduce((s, it) => s + it.price * it.qty, 0);
- const discountAmount = round2(subtotal * discount);
- const net = round2(subtotal - discountAmount);
- const tax = round2(net * taxRate);
- const total = round2(net + tax);
- return {
- id: id(),
- at: now(),
- currency,
- lines: items.map((it) => ({ sku: it.sku, qty: it.qty, amount: round2(it.price * it.qty) })),
- subtotal: round2(subtotal),
- discount: discountAmount,
- net,
- tax,
- total,
- };
- }
- /** 两位小数四舍五入(避免 0.1 + 0.2 类浮点尾巴)。 */
- export function round2(n) {
- return Math.round((n + Number.EPSILON) * 100) / 100;
- }
- /** 展示用格式化;金额格式化本身也是纯函数(金额展示与业务格式耦合,常被复用)。 */
- export function formatMoney(amount, currency = '¥') {
- return `${currency}${Number(amount).toFixed(2)}`;
- }
- function badInput(code, message) {
- return Object.assign(new Error(message), { code });
- }
|