order-builder
SKILL.md
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',
});
Weekly Installs
6
Repository
rytass/utilsGitHub Stars
6
First Seen
Feb 5, 2026
Security Audits
Installed on
amp6
github-copilot6
replit6
codex6
kimi-cli6
gemini-cli6