quote.js 2.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. /**
  2. * core/quote.js —— 核心库(core):纯 ESM 报价计算。
  3. *
  4. * 铁律:不碰 window / document / localStorage / fetch,不 import lit。
  5. * 所有不确定输入(时间、id、费率)都从参数注入 → 可在 Node 里确定化单测。
  6. */
  7. /**
  8. * @typedef {{ sku: string, price: number, qty: number }} Item
  9. * @typedef {{
  10. * taxRate?: number, discount?: number, currency?: string,
  11. * now?: () => number, id?: () => string,
  12. * }} QuoteOptions
  13. */
  14. /**
  15. * 计算一张报价单。
  16. * @param {Item[]} items
  17. * @param {QuoteOptions} [opts]
  18. */
  19. export function quote(items, opts = {}) {
  20. const {
  21. taxRate = 0.06,
  22. discount = 0,
  23. currency = '¥',
  24. now = () => Date.now(),
  25. id = () => `q-${Math.random().toString(36).slice(2, 10)}`,
  26. } = opts;
  27. if (!Array.isArray(items)) throw badInput('E_ITEMS', 'items 必须是数组');
  28. for (const it of items) {
  29. if (!(it.price >= 0) || !(it.qty >= 1) || !Number.isInteger(it.qty) || !it.sku) {
  30. throw badInput('E_ITEM', `无效条目:${JSON.stringify(it)}`);
  31. }
  32. }
  33. if (discount < 0 || discount > 1) throw badInput('E_DISCOUNT', 'discount 必须在 [0, 1]');
  34. const subtotal = items.reduce((s, it) => s + it.price * it.qty, 0);
  35. const discountAmount = round2(subtotal * discount);
  36. const net = round2(subtotal - discountAmount);
  37. const tax = round2(net * taxRate);
  38. const total = round2(net + tax);
  39. return {
  40. id: id(),
  41. at: now(),
  42. currency,
  43. lines: items.map((it) => ({ sku: it.sku, qty: it.qty, amount: round2(it.price * it.qty) })),
  44. subtotal: round2(subtotal),
  45. discount: discountAmount,
  46. net,
  47. tax,
  48. total,
  49. };
  50. }
  51. /** 两位小数四舍五入(避免 0.1 + 0.2 类浮点尾巴)。 */
  52. export function round2(n) {
  53. return Math.round((n + Number.EPSILON) * 100) / 100;
  54. }
  55. /** 展示用格式化;金额格式化本身也是纯函数(金额展示与业务格式耦合,常被复用)。 */
  56. export function formatMoney(amount, currency = '¥') {
  57. return `${currency}${Number(amount).toFixed(2)}`;
  58. }
  59. function badInput(code, message) {
  60. return Object.assign(new Error(message), { code });
  61. }