order-builder

Order Builder (訂單計算引擎)

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "order-builder" with this command: npx skills add rytass/utils/rytass-utils-order-builder

Order Builder (訂單計算引擎)

Overview

@rytass/order-builder 提供訂單計算與優惠政策應用引擎,支援複雜折扣邏輯、多階梯優惠和精確計算(Decimal.js)。

Quick Start

安裝

npm install @rytass/order-builder

基本使用

import { OrderBuilder, ValueDiscount, PercentageDiscount } from '@rytass/order-builder';

// 建立 Builder const builder = new OrderBuilder({ policies: [ new ValueDiscount(100), // 固定折 100 元 new PercentageDiscount(0.1), // 9 折 ], });

// 建立訂單 const order = builder.build({ items: [ { id: 'A', name: 'Product A', unitPrice: 500, quantity: 2 }, { id: 'B', name: 'Product B', unitPrice: 300, quantity: 1 }, ], });

console.log(order.itemValue); // 1300 (商品總額) console.log(order.discountValue); // 折扣金額 console.log(order.price); // 最終價格

Core Concepts

Builder Pattern

const builder = new OrderBuilder() .addPolicy(new ValueDiscount(100)) .addPolicy(new PercentageDiscount(0.1));

// build() 後政策鎖定 const order = builder.build({ items: [...] });

Order Properties

order.price // 最終價格 order.discountValue // 總折扣額 order.itemValue // 商品總額 order.itemQuantity // 商品總數量 order.discounts // 折扣說明陣列 order.itemRecords // 品項記錄 order.logisticsRecord // 運費記錄(若有設定運費) order.parent // 父訂單(若為子訂單)

Discount Policies

DiscountOptions (折扣選項)

所有折扣類別都支援以下選項:

interface DiscountOptions { id?: string; // 折扣識別碼 onlyMatched?: boolean; // 只計算符合條件的品項(預設: false) excludedInCalculation?: boolean; // 此折扣不計入折扣總額(預設: false) }

注意: conditions 是作為獨立的構造函數參數傳入,而非在 options 物件中。 自訂屬性(如 name )可透過泛型類型參數 DiscountOptions<T> 傳入。

ValueDiscount (固定額折扣)

import { ValueDiscount } from '@rytass/order-builder';

// 固定折 100 元 const discount = new ValueDiscount(100, { id: 'FLAT_100', name: '滿額折 100', });

// 進階選項 const advancedDiscount = new ValueDiscount(50, { id: 'SPECIAL_50', onlyMatched: true, // 只計算符合條件品項的金額 excludedInCalculation: true, // 此折扣不計入 discountValue });

PercentageDiscount (百分比折扣)

import { PercentageDiscount } from '@rytass/order-builder';

// 9 折 (折 10%) const discount = new PercentageDiscount(0.1, { id: 'DISCOUNT_10', name: '全館 9 折', });

StepValueDiscount (累積固定額折扣)

import { StepValueDiscount } from '@rytass/order-builder';

// 每滿 1000 折 100 (累積計算) // 例如:買 2500 元 → 折 200 (2 倍) const discount = new StepValueDiscount(1000, 100, { id: 'STEP_DISCOUNT', name: '每滿千折百', });

// 計算公式: discount = value * floor(itemValue / step) // 2500 元時: 100 * floor(2500 / 1000) = 100 * 2 = 200

// 可選參數 const discountWithLimit = new StepValueDiscount(500, 50, { id: 'LIMITED_STEP', stepLimit: 3, // 最多累積 3 次 (最多折 150) stepUnit: 'price', // 'price' (金額) 或 'quantity' (數量) onlyMatched: true, // 只計算符合條件的品項 });

注意: 這是「累積折扣」模式,每達到 step 金額就累積一次折扣,不是「階梯式」選擇最高折扣。

StepPercentageDiscount (累積百分比折扣)

import { StepPercentageDiscount } from '@rytass/order-builder';

// 每買 3 件累積一次 95 折 (複利計算) // 例如:買 6 件 → 0.95^2 = 90.25 折 const discount = new StepPercentageDiscount(3, 0.95, { id: 'QUANTITY_DISCOUNT', name: '每 3 件折 5%', stepUnit: 'quantity', // 以數量計算 step });

// 計算公式: discountRate = value ^ floor(multiplier / step) // discount = itemValue * (1 - discountRate)

// 以金額累積的範例 const priceBasedDiscount = new StepPercentageDiscount(1000, 0.95, { id: 'PRICE_STEP', name: '每滿千折 5%', stepUnit: 'price', // 預設,以金額計算 step stepLimit: 5, // 最多累積 5 次 (最多 0.95^5 ≈ 77.4 折) });

注意: 這是「累積複利折扣」模式,每達到 step 就額外乘以 value 折扣率。

ItemGiveawayDiscount (贈品折扣)

import { ItemGiveawayDiscount, ItemIncluded } from '@rytass/order-builder';

// 送 1 件 (從符合條件的品項中選最低價) const discount = new ItemGiveawayDiscount(1, new ItemIncluded({ items: ['A'] }), { id: 'BUY_A_GET_FREE', });

// 多種建構方式 // 1. value + conditions 陣列 + options new ItemGiveawayDiscount(1, [new ItemIncluded({ items: ['A'] })], { id: 'GIVEAWAY_1' });

// 2. value + 單一 condition + options new ItemGiveawayDiscount(1, new ItemIncluded({ items: ['A'] }), { id: 'GIVEAWAY_2' });

// 3. value + options (無條件) new ItemGiveawayDiscount(1, { id: 'FREE_ONE', strategy: 'HIGH_PRICE_FIRST' });

// 4. 只有 value (送最低價 1 件) new ItemGiveawayDiscount(1);

// 指定贈品選擇策略 const discountWithStrategy = new ItemGiveawayDiscount(2, new ItemIncluded({ items: ['B', 'C'] }), { strategy: 'LOW_PRICE_FIRST', // 先送低價品(預設) // strategy: 'HIGH_PRICE_FIRST', // 先送高價品 id: 'GET_2_FREE', });

注意: value 是贈品數量,不是折扣金額。例如 new ItemGiveawayDiscount(2) 表示「送 2 件最低價品項」。

StepItemGiveawayDiscount (累積贈品折扣)

注意: StepItemGiveawayDiscount 目前未從 index.ts 導出,若需使用請直接從原始碼路徑 import。

// 目前無法從 @rytass/order-builder 直接 import // 需從原始碼路徑 import(若專案支援) import { StepItemGiveawayDiscount } from '@rytass/order-builder/src/policies/discount/step-item-giveaway-discount';

// 每買 3 件送 1 件(累積) // 例如:買 7 件 → 送 2 件 (floor(7/3) = 2) const discount = new StepItemGiveawayDiscount(3, 1, { id: 'BUY_3_GET_1', name: '每滿 3 件送 1 件', strategy: 'LOW_PRICE_FIRST', // 'LOW_PRICE_FIRST' | 'HIGH_PRICE_FIRST' });

// 計算公式: giveawayQuantity = value * floor(matchedQuantity / (step + value)) // stepLimit 計算: min(stepLimit, floor(matchedQuantity / (step + value)))

Conditions

PriceThreshold (金額閾值)

import { PriceThreshold } from '@rytass/order-builder';

// 滿 1000 元 const condition = new PriceThreshold(1000);

QuantityThreshold (數量閾值)

import { QuantityThreshold } from '@rytass/order-builder';

// 滿 5 件 const condition = new QuantityThreshold(5);

ItemIncluded (包含品項)

import { ItemIncluded } from '@rytass/order-builder';

// 購物車包含 A 或 B (基本用法) const condition = new ItemIncluded({ items: ['A', 'B'] });

// 指定數量閾值 const conditionWithThreshold = new ItemIncluded({ items: ['A', 'B'], threshold: 3, // 需要 A 或 B 合計至少 3 件 });

// 使用函式判斷 const conditionWithFn = new ItemIncluded({ isMatchedItem: (item) => item.category === 'special', threshold: 1, });

// 指定 scope (搜尋的屬性) const conditionWithScope = new ItemIncluded({ items: ['sku-001', 'sku-002'], scope: ['sku', 'id'], // 優先檢查 sku,沒有再檢查 id });

// 搭配子條件 const conditionWithSubConditions = new ItemIncluded({ items: ['A'], conditions: [new PriceThreshold(500)], // 包含 A 且小計需達 500 });

ItemRequired (必須包含)

import { ItemRequired } from '@rytass/order-builder';

// 購物車必須包含 A(數量至少 1) const conditionSimple = new ItemRequired('A');

// 購物車必須包含 A 且數量 >= 2(使用物件格式) const condition = new ItemRequired({ id: 'A', quantity: 2 });

// 批量需求:A 至少 2 件、B 至少 1 件 const conditionMultiple = new ItemRequired([ { id: 'A', quantity: 2 }, 'B', // 字串會自動轉為 { id: 'B', quantity: 1 } ]);

ItemExcluded (排除品項)

import { ItemExcluded } from '@rytass/order-builder';

// 排除特定品項(不參與此折扣) const condition = new ItemExcluded({ items: ['excluded-item-1', 'excluded-item-2'] });

// 使用函式判斷排除 const conditionWithFilter = new ItemExcluded({ isMatchedItem: (item) => item.category === 'special', });

// 指定 scope (搜尋的屬性) const conditionWithScope = new ItemExcluded({ items: ['sku-001'], scope: ['sku', 'id'], });

QuantityRequired (數量需求)

import { QuantityRequired } from '@rytass/order-builder';

// 指定品項總數量需達到 5 const condition = new QuantityRequired(5, ['item-a', 'item-b']);

// 不指定品項,計算全部品項數量 const conditionAll = new QuantityRequired(10);

CouponValidator (優惠券)

import { ValueDiscount, CouponValidator } from '@rytass/order-builder';

const couponDiscount = new ValueDiscount(50, { id: 'COUPON_50', conditions: [new CouponValidator('SAVE50')], });

// 使用優惠券 const order = builder.build({ items: [...], coupons: ['SAVE50'], });

Configuration Options

OrderBuilder Options

new OrderBuilder({ policies?: Policy[]; policyPickStrategy?: 'order-based' | 'item-based'; // 預設: 'item-based' discountMethod?: 'price-weighted-average' | 'quantity-weighted-average'; // 預設: 'price-weighted-average' roundStrategy?: RoundStrategyType | [RoundStrategyType, number]; // 預設: 'every-calculation' logistics?: OrderLogistics; });

Policy Pick Strategy(折扣選擇策略)

當訂單符合多個折扣政策時,決定如何選擇最佳折扣:

策略 預設 說明

item-based

✓ 針對每個品項分別選擇最佳折扣組合(最優解)

order-based

選擇整體折扣最高的單一政策

策略差異範例:

// 假設訂單有 A、B 兩品項 // 政策 1: A 折 50 元 // 政策 2: B 折 100 元

// item-based(預設): A 套用政策 1、B 套用政策 2 → 總折扣 150 // order-based: 只選擇政策 2 → 總折扣 100

new OrderBuilder({ policyPickStrategy: 'item-based', // 推薦,可獲得最大折扣 });

Discount Method(折扣分配方式)

決定如何將整體折扣分配到各品項:

方法 預設 說明

price-weighted-average

✓ 按金額加權分配折扣

quantity-weighted-average

按數量加權分配折扣

Round Strategy(四捨五入策略)

策略 預設 說明

every-calculation

✓ 每次計算都四捨五入

final-price-only

只在最終價格四捨五入

no-round

不四捨五入

精度設定:

new OrderBuilder({ roundStrategy: 'final-price-only', // 預設精度 0(整數) // 或指定精度 roundStrategy: ['final-price-only', 2], // 小數後 2 位 });

OrderLogistics(運費設定)

interface OrderLogistics { price: number; // 運費(必填) name?: string; // 物流名稱(可選) threshold?: number; // 免運門檻金額(可選) freeConditions?: Condition | Condition[]; // 免運條件(可選) }

運費範例:

import { OrderBuilder, PriceThreshold, ItemIncluded } from '@rytass/order-builder';

// 基本運費設定 new OrderBuilder({ logistics: { price: 60, name: '宅配', }, });

// 滿額免運 new OrderBuilder({ logistics: { price: 60, name: '宅配', threshold: 1000, // 滿 1000 免運 }, });

// 條件免運 new OrderBuilder({ logistics: { price: 60, name: '宅配', freeConditions: [ new PriceThreshold(1000), // 滿 1000 免運 new ItemIncluded(['vip-product']), // 或購買 VIP 商品免運 ], }, });

Order Methods

動態調整

const order = builder.build({ items: initialItems });

// 新增品項 (單一或多個) order.addItem({ id: 'C', name: 'Product C', unitPrice: 200, quantity: 1 }); order.addItem([ { id: 'D', name: 'Product D', unitPrice: 100, quantity: 2 }, { id: 'E', name: 'Product E', unitPrice: 150, quantity: 1 }, ]);

// 移除品項 - 多種方式 // 1. 依 ID 和數量移除 order.removeItem('A', 2); // 移除 A 商品 2 件

// 2. 依品項物件移除 order.removeItem({ id: 'B', name: 'Product B', unitPrice: 300, quantity: 1 });

// 3. 批次移除多個品項 order.removeItem([ { id: 'C', name: 'Product C', unitPrice: 200, quantity: 1 }, ]);

// 新增優惠券 order.addCoupon('SAVE50');

// 移除優惠券 order.removeCoupon('SAVE50');

子訂單

// 篩選特定品項的子訂單 const subOrder = order.subOrder({ subItems: ['A', 'B'], // 子訂單包含的品項 ID subCoupons: ['COUPON1'], // 子訂單使用的優惠券(可選) subPolicies: [new ValueDiscount(50)], // 子訂單額外政策(可選) itemScope: 'id', // 'id'(預設)或 'uuid',指定篩選依據 });

console.log(subOrder.price); // 只計算子訂單的價格 console.log(subOrder.parent); // 父訂單參照

Complete Example

import { OrderBuilder, ValueDiscount, PercentageDiscount, StepValueDiscount, PriceThreshold, ItemRequired, CouponValidator, } from '@rytass/order-builder';

// 定義折扣政策 const policies = [ // 全館 95 折 new PercentageDiscount(0.05, { id: 'GLOBAL_95', name: '全館 95 折', }),

// 每滿千折 50 (累積) new StepValueDiscount(1000, 50, { id: 'STEP_DISCOUNT', name: '每滿千折 50', }),

// 特定商品折扣 new ValueDiscount(100, { id: 'PRODUCT_A_DISCOUNT', name: '購買 A 商品折 100', conditions: [new ItemRequired('product-a', 1)], }),

// 優惠券折扣 new ValueDiscount(200, { id: 'COUPON_200', name: '優惠券折 200', conditions: [new CouponValidator('SAVE200')], }), ];

// 建立 Builder const builder = new OrderBuilder({ policies, discountMethod: 'price-weighted-average', roundStrategy: 'final-price-only', });

// 建立訂單 const order = builder.build({ items: [ { id: 'product-a', name: '商品 A', unitPrice: 1500, quantity: 1 }, { id: 'product-b', name: '商品 B', unitPrice: 800, quantity: 2 }, { id: 'product-c', name: '商品 C', unitPrice: 300, quantity: 3 }, ], coupons: ['SAVE200'], });

// 查看結果 console.log('商品總額:', order.itemValue); console.log('折扣金額:', order.discountValue); console.log('最終價格:', order.price); console.log('應用的折扣:'); order.discounts.forEach(d => { console.log( - ${d.name}: -${d.value}); });

// 動態調整 order.addItem({ id: 'product-d', name: '商品 D', unitPrice: 500, quantity: 1 }); console.log('新增商品後價格:', order.price);

order.removeCoupon('SAVE200'); console.log('移除優惠券後價格:', order.price);

Advanced Types

Exported Enums

// 折扣類型 enum Discount { PERCENTAGE = 'PERCENTAGE', VALUE = 'VALUE', STEP_VALUE = 'STEP_VALUE', STEP_PERCENTAGE = 'STEP_PERCENTAGE', ITEM_GIVEAWAY = 'ITEM_GIVEAWAY', STEP_ITEM_GIVEAWAY = 'STEP_ITEM_GIVEAWAY', }

// 政策前綴 enum PolicyPrefix { DISCOUNT = 'DISCOUNT', }

Core Types

// 訂單品項 interface OrderItem { id: string; name: string; unitPrice: number; quantity: number; conditionRef?: string | string[] | ((..._: unknown[]) => boolean); }

// 扁平化品項(quantity = 1,有獨立 uuid) interface FlattenOrderItem extends OrderItem { uuid: string; }

// 品項記錄(折扣結果) interface OrderItemRecord<Item extends OrderItem> { itemId: string; appliedPolicies: Policy[]; originItem: Item; initialValue: number; discountValue: number; finalPrice: number; discountRecords: ItemDiscountRecord[]; }

// 基本品項(最小化) interface BaseOrderItem { id: string; quantity: number; conditionRef?: string | string[] | ((..._: unknown[]) => boolean); }

Constants(常數)

import { ORDER_LOGISTICS_ID, ORDER_LOGISTICS_NAME } from '@rytass/order-builder';

// 運費品項的固定 ID ORDER_LOGISTICS_ID = 'LOGISTICS';

// 運費品項的預設名稱 ORDER_LOGISTICS_NAME = 'logistics';

Policy & Condition Interfaces

// 政策介面 interface Policy<T extends ObjRecord = ObjRecord> { id: string; prefix: PolicyPrefix; conditions?: Condition[]; matchedItems(order: Order): FlattenOrderItem[]; valid(order: Order): boolean; resolve<TT extends T>(order: Order, ...: unknown[]): TT[]; description(...: unknown[]): PolicyResult<T>; }

// 條件介面 type Condition<T extends ObjRecord = ObjRecord, Options extends ObjRecord = ObjRecord> = { satisfy(order: Order, ..._: unknown[]): boolean; matchedItems?: (order: Order) => FlattenOrderItem[]; readonly options?: Options; } & T;

// 折扣政策描述結果 interface PolicyDiscountDescription { id: string; type: Discount; value: number; discount: number; conditions: Condition[]; appliedItems: FlattenOrderItem[]; matchedTimes: number; policy: BaseDiscount; }

Discount Options Types

// 基本折扣選項 interface DiscountOptions { id?: string; name?: string; conditions?: Condition[]; onlyMatched?: boolean; excludedInCalculation?: boolean; }

// Step 折扣選項 interface StepDiscountOptions extends DiscountOptions { stepUnit: 'quantity' | 'price'; stepLimit?: number; }

// 贈品策略 type ItemGiveawayStrategy = 'LOW_PRICE_FIRST' | 'HIGH_PRICE_FIRST';

Dependencies

  • decimal.js ^10.6.0 (精確計算)

Troubleshooting

折扣未生效

檢查條件是否滿足:

// 確認優惠券已加入 order.addCoupon('COUPON_CODE');

// 確認品項符合條件 // ItemRequired('A', 2) 需要品項 A 數量 >= 2

金額計算不精確

使用正確的 roundStrategy:

new OrderBuilder({ roundStrategy: 'final-price-only', // 推薦 });

多重折扣衝突

預設使用 item-based 策略,會自動選擇每個品項的最佳折扣組合。

如需改為「整體擇優」模式(只選一個最高折扣政策):

new OrderBuilder({ policyPickStrategy: 'order-based', });

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

wms-module

No summary provided by upstream source.

Repository SourceNeeds Review
General

quadrats-module

No summary provided by upstream source.

Repository SourceNeeds Review
General

wms-react-components

No summary provided by upstream source.

Repository SourceNeeds Review
General

invoice-adapters

No summary provided by upstream source.

Repository SourceNeeds Review