0.0.21 • Published 11 months ago

@rytass/order-builder v0.0.21

Weekly downloads
-
License
MIT
Repository
github
Last release
11 months ago

OrderBuilder

An Order builder API makes business-logic development more easier.


Features

  • Arbitrary-precision order-calculating based on decimal.js.
  • Common Pipe pattern API.

Core API

  1. OrderBuilder

    • config
      • PolicyPickStrategy
        • ItemBasedPolicyPickStrategy
        • OrderBasedPolicyPickStrategy
      • DiscountMethod
        • PriceWeightedAverageDiscountMethod
        • QuantityWeightedAverageDiscountMethod
      • RoundStrategy
        • EveryCalculationRoundStrategy
        • FinalPriceRoundStrategy
        • NoRoundRoundStrategy
  2. Policy

    • discount
      • ValueDiscount
      • PercentageDiscount
      • StepValueDiscount
      • StepPercentageDiscount
      • ItemGiveawayDiscount
  3. Condition

    • threshold
      • PriceThreshold
      • QuantityThreshold
    • requirement
      • ItemIncluded
      • ItemRequired
      • QuantityRequired
      • ItemExcluded
    • validator
      • CouponValidator

Documents

OrderBuilder

ArgumentsindexTypeRequired
overload1
builder0OrderBuilderfalse
overload2
options0OrderBuilderConstructorfalse

OrderBuilderConstructor

PropertiesTypeRequired
policiesPolicy[]true
discountMethod"price-weighted-average" | "quantity-weighted-average"false
roundStrategy"every-calculation" | "final-price-only" | "no-round"false
logisticsOrderLogisticsfalse
OrderBuilderReturnDescription
Properties
configOrderConfigGet the configuration of current builder.
policiesPolicies[]Get all the policies of the builder. (activated + none-activated)
hasBuiltOrdersbooleanChecks whether this builder has already built any instance of order.
Methods
buildOrderCreate an order instance, and lock the access of mutations on policy.
cloneOrderBuilderCreate a new OrderBuilder instance based on current instance.
addPolicy(policy: Policies)thisPush policy instance(s) into builder.policies (can active as builder.hasBuiltOrders is false).
addPolicy(policies: Policies[])
removePolicy(policy: Policy)thisRemove policy instance(s) from builder.policies based on instance reference or policy.id (can active as builder.hasBuiltOrders is false)
removePolicy(policy: string)
removePolicy(policies: Policy[])
removePolicy(policies: string[])
getPolicy(policyId: string)Policy | undefinedGet the specified policy instance by policy.id
setLogistics(logistics: OrderLogistics)thisSet the logistics of the order.

Order

OrderConstructor

PropertiesTypeRequired
itemsOrderItem[]true
couponsstring[]false
OrderReturnDescription
Properties
builderOrderBuilderGet the the owner of current order.
configOrderConfigGet the configuration from its builder.
policiesPolicies[]Get all the policies from its builder.
couponsstring[]Get all the coupons from current order.
itemsOrderItem[]Get all the items from current order.
itemManagerOrderItemManagerGet the item manager.
itemRecordsOrderItemRecord[]Get all the detail-records of item based on policies.
discountsPolicyDiscountDescription[]Get the descriptions of discount based on applied-policies.
discountValuenumberGet total discount in this order.
itemValuenumberGet total value of items in this order.
itemQuantitynumberGet total quantity of items in this order.
pricenumberGet final price of order.
logisticsOrderLogisticsGet logistics config of current order.
logisticsRecordOrderItemRecordGet logistics in the type of order-item-record.
Methods
subOrder(subCondition: SubOrderCondition)OrderCreate a new instance of Order based on the sub-condition on items and coupons.
addCoupon(coupon: string)thisPush coupon(s) into order.coupons.
addCoupon(coupons: string[])
removeCoupon(coupon: string)thisRemove coupons(s) from order.coupons
removeCoupon(coupons: string[])
addItem(item: OrderItem)thisPush item(s) into order.items.
addItem(items: OrderItem[])
removeItem(id: string, quantity: number)thisRemove item(s) from order.items.
removeItem(item: OrderItem)
removeItem(item: string)
removeItem(items: OrderItem[])
removeItem(items: string[])

Discount

ArgumentsindexTypeRequired
overload1
value0numbertrue
condition1Conditionfalse
options2DiscountOptionsfalse
overload2
value0numbertrue
conditions1Condition[]false
options2DiscountOptionsfalse
overload3
value0numbertrue
options1DiscountOptionsfalse

Interfaces Remark

InterfaceType
PoliciesPolicy | Policy[]
RemovePolicyPolicy | string

Basic Usage

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

const policy1 = new ValueDiscount(100, { id: 'DISCOUNT_1' });
const policy2 = new PercentageDiscount(0.8, { id: 'DISCOUNT_2' });

// Initialize an order-builder.
const builder = new OrderBuilder({
  policyPickStrategy: 'item-based',
  discountMethod: 'price-weighted-average',
  roundStrategy: 'every-calculation',
  policies: [policy1],
});

// Allowing conditional business-logic to determinate whether to mutate policies.
const order = builder
  .addPolicy(policy2) // 20% off
  .removePolicy('DISCOUNT_1') // or .removePolicy(policy1)
  .build({
    items: [
      {
        id: 'ItemA',
        name: 'Foo',
        unitPrice: 100,
        quantity: 2,
      },
      {
        id: 'ItemB',
        name: 'Bar',
        unitPrice: 50,
        quantity: 1,
      },
    ],
  });

builder.getPolicy('DISCOUNT_1'); // undefined
builder.getPolicy('DISCOUNT_2'); // reference to `policy2`
order.price // 250 * 0.8 = 200
order.itemRecords
// [
//   {
//     itemId: 'ItemA-1',
//     originItem: { id: 'ItemA', name: 'Foo', unitPrice: 100 },
//     initialValue: 100,
//     discountValue: 20,
//     finalPrice: 80,
//     discountRecords: [{ policyId: 'DISCOUNT_2', discountValue: 20 }],
//     appliedPolicies: [policy2]
//   },
//   {
//     itemId: 'ItemA-2',
//     originItem: { id: 'ItemA', name: 'Foo', unitPrice: 100 },
//     initialValue: 100,
//     discountValue: 20,
//     finalPrice: 80,
//     discountRecords: [{ policyId: 'DISCOUNT_2', discountValue: 20 }],
//     appliedPolicies: [policy2]
//   },
//   {
//     itemId: 'ItemB-1',
//     originItem: { id: 'ItemB', name: 'Bar', unitPrice: 50 },
//     initialValue: 50,
//     discountValue: 10,
//     finalPrice: 40,
//     discountRecords: [{ policyId: 'DISCOUNT_2', discountValue: 10 }],
//     appliedPolicies: [policy2]
//   }
// ]

Examples

Custom OrderItem Dto

import {
  /** Core */
  OrderItem,
  OrderBuilder,
  /** Conditions */
  ItemIncluded,
  PriceThreshold,
  QuantityThreshold,
  /** Discount policies */
  ValueDiscount,
  StepValueDiscount,
  PercentageDiscount,
  ItemGiveawayDiscount,
  StepPercentageDiscount,
} from '@rytass/order-builder';

// Allow custom order-item dto.
type TestOrderItem = OrderItem<{
  category: string,
  brand: string,
}>;

const items: TestOrderItem[] = [
  {
    id: 'A',
    name: '外套A',
    unitPrice: 1000,
    quantity: 1,
    category: 'jacket',
    brand: 'AJE',
  },
  {
    id: 'B',
    name: '外套B',
    unitPrice: 1500,
    quantity: 1,
    category: 'jacket',
    brand: 'N21',
  },
  {
    id: 'C',
    name: '鞋子C',
    unitPrice: 2000,
    quantity: 1,
    category: 'shoes',
    brand: 'N21',
  },
  {
    id: 'D',
    name: '鞋子D',
    unitPrice: 2500,
    quantity: 1,
    category: 'shoes',
    brand: 'Preen',
  },
  {
    id: 'E',
    name: '鞋子E',
    unitPrice: 3000,
    quantity: 1,
    category: 'shoes',
    brand: 'Preen',
  },
  {
    id: 'F',
    name: '飾品F',
    unitPrice: 4000,
    quantity: 1,
    category: 'accessory',
    brand: 'Swell',
  },
  {
    id: 'G',
    name: '飾品G',
    unitPrice: 5000,
    quantity: 1,
    category: 'accessory',
    brand: 'Swell',
  },
  {
    id: 'H',
    name: '飾品H',
    unitPrice: 6000,
    quantity: 1,
    category: 'accessory',
    brand: 'Swell',
  },
  {
    id: 'I',
    name: '飾品I',
    unitPrice: 6500,
    quantity: 1,
    category: 'accessory',
    brand: 'Boyy',
  },
];

Scenario 1 - 1

/**
 * 情境編號:甲
 * 
 * 決策法:擇優取一 (order-based)
 *
 * 情境敘述:
 * 1. 指定商品(A~F)滿3件 打9折
 * 2. 指定商品(C~I)每5000元 折600元
 * 3. 指定分類(鞋子)滿4000元 送最低價商品
 * 4. 指定分類(Swell)每1件 打9折
 *
 * 輸入:(1|2) & (3|4)
 * 預期結果:2 + 4
 * 購物車顯示金額 24856
 */

// 1. 指定商品(A~F)滿3件 打9折
const policy1 = new PercentageDiscount(
  0.9,
  new ItemIncluded({
    items: ['A', 'B', 'C', 'D', 'E', 'F'],
    threshold: 3,
  }),
  { onlyMatched: true },
);

// 2. 指定商品(C~I)每5000元 折600元
const policy2 = new StepValueDiscount(
  5000,
  600,
  new ItemIncluded<TestOrderItem>({
    items: ['C', 'D', 'E', 'F', 'G', 'H', 'I'],
  }),
  { stepUnit: 'price', onlyMatched: true },
);

// 3. 指定分類(鞋子)滿4000元 送最低價商品
const policy3 = new ItemGiveawayDiscount(
  1,
  new ItemIncluded<TestOrderItem>({
    scope: 'category',
    items: ['shoes'],
    conditions: [new PriceThreshold(4000)],
  }),
  { onlyMatched: true },
);

// 4. 指定分類(Swell)每1件 打9折
const policy4 = new StepPercentageDiscount(
  1,
  0.9,
  new ItemIncluded<TestOrderItem>({
    scope: 'brand',
    items: ['Swell'],
  }),
  {
    stepUnit: 'quantity',
    onlyMatched: true,
  }
);

// 擇優 (1|2) & (3|4)
const builder = new OrderBuilder({
  policyPickStrategy: 'order-based',
  policies: [
    [policy1, policy2],
    [policy3, policy4],
  ],
});

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

order.price // 24856

Scenario 2

/**
 * 情境編號:乙
 * 
 * 決策法:擇優取一 (order-based)
 *
 * 情境敘述:
 * 1. 指定商品(B~E)無條件 送最低價商品
 * 2. 指定商品(C~I)每3000元 折200元
 * 3. 指定分類(N21)滿2件 折100元
 * 4. 指定分類(飾品)每2件 打9折
 * 5. 指定分類(Boyy)滿5000元 打9折
 * 6. 全館 滿6件 送最低價商品
 *
 * 輸入:(1|2) & (3|4|5) & 6
 * 輸出:2 + 4 + 6
 * 購物車顯示金額 24868
 */

// 1. 指定商品(B~E)無條件 送最低價商品
const policy1 = new ItemGiveawayDiscount(
  1,
  new ItemIncluded<TestOrderItem>({
    items: ['B', 'C', 'D', 'E'],
  }),
  { onlyMatched: true }
);

// 2. 指定商品(C~I)每3000元 折200元
const policy2 = new StepValueDiscount(
  3000,
  200,
  new ItemIncluded<TestOrderItem>({
    items: ['C', 'D', 'E', 'F', 'G', 'H', 'I'],
    scope: 'id',
  }),
  { stepUnit: 'price', onlyMatched: true }
);

// 3. 指定分類(N21)滿2件 折100元
const policy3 = new ValueDiscount(
  100,
  new ItemIncluded<TestOrderItem>({
    items: ['N21'],
    scope: 'brand',
    threshold: 2,
  }),
  { onlyMatched: true }
);

// 4. 指定分類(飾品)每2件 打9折
const policy4 = new StepPercentageDiscount(
  2,
  0.9,
  new ItemIncluded<TestOrderItem>({
    items: ['accessory'],
    scope: 'category',
  }),
  { stepUnit: 'quantity', onlyMatched: true }
);

// 5. 指定分類(Boyy)滿5000元 打9折
const policy5 = new PercentageDiscount(
  0.9,
  new ItemIncluded<TestOrderItem>({
    items: ['Boyy'],
    scope: 'brand',
    conditions: [new PriceThreshold(5000)],
  }),
  { id: 'P5', onlyMatched: true }
);

// 6. 全館 滿6件 送最低價商品
const policy6 = new ItemGiveawayDiscount(1, new QuantityThreshold(6));

// 擇優 (1|2) & (3|4|5) & 6
const builder = new OrderBuilder({
  policyPickStrategy: 'order-based',
  policies: [
    [policy1, policy2],
    [policy3, policy4, policy5],
    policy6,
  ],
});

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

order.price // 24868

Scenario 3

/**
 * 情境編號:丙 - 1
 * 
 * 決策法:擇優取一 (order-based)
 *
 * 情境敘述:
 * 1. 全館 滿6件 送最低價商品
 * 2. 指定分類(Boyy)滿5000元 打9折
 * 3. 指定商品(B~E)無條件 送最低價商品
 * 4. 指定商品(C~I)每3000元 折200元
 * 5. 指定分類(鞋子)滿4000元 送最低價商品
 *
 * 輸入:1 & 2 & 3 & 4 & 5
 * 輸出:1 + 2 + 3 + 4 + 5
 * 購物車顯示金額 24677
 */

// 1. 全館 滿6件 送最低價商品
const policy1 = new ItemGiveawayDiscount(
  1,
  new QuantityThreshold(6),
);

// 2. 指定分類(Boyy)滿5000元 打9折
const policy2 = new PercentageDiscount(
  0.9,
  new ItemIncluded<TestOrderItem>({
    items: ['Boyy'],
    scope: 'brand',
    conditions: [new PriceThreshold(5000)],
  }),
  { onlyMatched: true }
);

// 3. 指定商品(B~E)無條件 送最低價商品
const policy3 = new ItemGiveawayDiscount(
  1,
  new ItemIncluded<TestOrderItem>({
    items: ['B''C', 'D', 'E'],
  }),
  { onlyMatched: true }
);

// 4. 指定商品(C~I)每3000元 折200元
const policy4 = new StepValueDiscount(
  3000,
  200,
  new ItemIncluded<TestOrderItem>({
    items: ['C', 'D', 'E', 'F', 'G', 'H', 'I'],
  }),
  { stepUnit: 'price', onlyMatched: true }
);

// 5. 指定分類(鞋子)滿4000元 送最低價商品
const policy5 = new ItemGiveawayDiscount(
  1,
  new ItemIncluded<TestOrderItem>({
    items: ['shoes'],
    scope: 'category',
    conditions: [new PriceThreshold(4000)],
  }),
  { onlyMatched: true }
);

const builder = new OrderBuilder({
  policyPickStrategy: 'order-based',
  policies: [
    policy1,
    policy2,
    policy3,
    policy4,
    policy5,
  ],
});

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

order.price // 24677

Scenario 3 - 2

/**
 * 情境編號:丙 - 2
 * 
 * 決策法:擇優取一 (order-based)
 *
 * 情境敘述:
 * 1. 全館 滿14000元 贈最低價品
 * 2. 指定商品(C~E)無條件 送最低價商品
 * 3. 指定分類(鞋子)滿6000元 送最低價商品
 * 4. 指定分類(Boyy)滿5000元 打9折
 * 5. 全館 滿9件 送最低價商品
 * 6. 指定商品(C~I)每3000元 折200元
 *
 * 輸入:1 & 2 & 3 & 4 & 5 & 6
 * 輸出:1 + 2 + 3 + 4 + 5 + 6
 * 購物車顯示金額 26250
 */

// 1. 全館 滿14000元 送最低價商品
const policy1 = new ItemGiveawayDiscount(
  1,
  new PriceThreshold(14000),
);

// 2. 指定商品(C~E)無條件 送最低價商品
const policy2 = new ItemGiveawayDiscount(
  1,
  new ItemIncluded<TestOrderItem>({
    items: ['C', 'D', 'E', 'F', 'G', 'H', 'I'],
  }),
  { onlyMatched: true }
);

// 3. 指定分類(鞋子)滿6000元 送最低價商品
const policy3 = new ItemGiveawayDiscount(
  1,
  new ItemIncluded<TestOrderItem>({
    items: ['shoes'],
    scope: 'category',
    conditions: [new PriceThreshold(6000)],
  }),
  { onlyMatched: true }
);

// 4. 指定分類(Boyy)滿5000元 打9折
const policy4 = new PercentageDiscount(
  0.9,
  new ItemIncluded<TestOrderItem>({
    scope: 'brand',
    items: ['Boyy'],
    conditions: [new PriceThreshold(5000)],
  }),
  { onlyMatched: true }
);

// 5. 全館 滿9件 送最低價商品
const policy5 = new ItemGiveawayDiscount(
  1,
  new QuantityThreshold(9),
);

// 6. 指定商品(C~I)每3000元 折200元
const policy6 = new StepValueDiscount(
  3000,
  200,
  new ItemIncluded({
    items: ['C', 'D', 'E', 'F''G', 'H', 'I'],
  }),
  { stepUnit: 'price', onlyMatched: true }
);

const builder = new OrderBuilder({
  policyPickStrategy: 'order-based',
  discountMethod: 'price-weighted-average',
  policies: [
    policy1,
    policy2,
    policy3,
    policy4,
    policy5,
    policy6,
  ],
});

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

order.price // 26250

Scenario 4

/**
  * 情境編號:丁
  * 
  * 決策法:擇優取一 (order-based)
  *
  * 情境敘述:
  * 1. 指定分類(飾品)無條件 折1,000 元
  * 2. 指定分類(Swell)滿10,000元 打9折
  * 3. 全館 滿15,000元 送最低價品
  * 4. 指定商品(F~I)無條件 打9折
  * 5. 指定分類(Boyy)滿5000 打9折
  *
  * 預期結果:(1&2&3) | (4&5)
  * 購物車顯示金額 28070
  */

// 1. 指定分類(飾品)無條件 折1,000 元
const policy1 = new ValueDiscount(
  1000,
  new ItemIncluded<TestOrderItem>({
    scope: 'category',
    items: ['accessory'],
  }),
  { onlyMatched: true }
);

// 2. 指定分類(Swell)滿10,000元 打9折
const policy2 = new PercentageDiscount(
  0.9,
  new ItemIncluded<TestOrderItem>({
    scope: 'brand',
    items: ['Swell'],
    conditions: [new PriceThreshold(10000)],
  }),
  { onlyMatched: true }
);

// 3. 全館 滿15,000元 送最低價品
const policy3 = new ItemGiveawayDiscount(
  1,
  new PriceThreshold(15000),
);

// 4. 指定商品(F~I)無條件 打9折
const policy4 = new PercentageDiscount(
  0.9,
  new ItemIncluded<TestOrderItem>({
    items: ['F', 'G', 'H', 'I'],
  }),
  { onlyMatched: true }
);

// 5. 指定分類(Boyy)滿5000 打9折
const policy5 = new PercentageDiscount(
  0.9,
  new ItemIncluded<TestOrderItem>({
    scope: 'brand',
    items: ['Boyy'],
    conditions: [new PriceThreshold(5000)],
  }),
  { onlyMatched: true }
);

const builder1 = new OrderBuilder({
  policyPickStrategy: 'order-based',
  policies: [
    policy1,
    policy2,
    policy3,
  ],
});

const builder2 = new OrderBuilder({
  policyPickStrategy: 'order-based',
  policies: [
    policy4,
    policy5,
  ],
});

const order1 = builder1.build({ items });
const order2 = builder2.build({ items });

order1.price // 28070
order2.price // 28765

Scenario 5

/**
 * 情境編號:戊
 *
 * 決策法:擇優取一 (order-based)
 *
 * 情境敘述:
 * 1. 指定商品(B~E)無條件 送最低價商品
 * 2. 全館 滿6件 送最低價商品
 * 3. 指定分類(飾品)每2件 打9折
 * 4. 指定分類(鞋子)滿4000元 送最低價商品
 * 5. 指定分類(Boyy)滿5000元 打9折
 * 6. 全館 滿15,000元 送最低價品
 *
 * 預期結果:(1&2&3) | (4&5&6)
 * 購物車顯示金額 24915
 */

// 1. 指定商品(B~E)無條件 送最低價商品
const policy1 = new ItemGiveawayDiscount(
  1,
  new ItemIncluded<TestOrderItem>({
    items: ['B', 'C', 'D', 'E'],
  }),
  { onlyMatched: true }
);

// 2.全館 滿6件 送最低價商品
const policy2 = new ItemGiveawayDiscount(
  1,
  new QuantityThreshold(6),
);

// 3. 指定分類(飾品)每2件 打9折
const policy3 = new StepPercentageDiscount(
  2,
  0.9,
  new ItemIncluded<TestOrderItem>({
    scope: 'category',
    items: ['accessory'],
  }),
  { stepUnit: 'quantity', onlyMatched: true }
);

// 4. 指定分類(鞋子)滿4000元 送最低價商品
const policy4 = new ItemGiveawayDiscount(
  1,
  new ItemIncluded<TestOrderItem>({
    scope: 'category',
    items: ['shoes'],
    conditions: [new PriceThreshold(4000)],
  }),
  { onlyMatched: true }
);

// 5. 指定分類(Boyy)滿5000元 打9折
const policy5 = new PercentageDiscount(
  0.9,
  new ItemIncluded<TestOrderItem>({
    scope: 'brand',
    items: ['Boyy'],
    conditions: [new PriceThreshold(5000)],
  }),
  { onlyMatched: true }
);

// 6. 全館 滿15,000元 送最低價品
const policy6 = new ItemGiveawayDiscount(
  1,
  new PriceThreshold(15000),
);

const builder1 = new OrderBuilder({
  policyPickStrategy: 'order-based',
  policies: [
    policy1,
    policy2,
    policy3,
  ],
});

const builder2 = new OrderBuilder({
  policyPickStrategy: 'order-based',
  policies: [
    policy4,
    policy5,
    policy6,
  ],
});

const order1 = builder1.build({ items });
const order2 = builder2.build({ items });

order1.price // 24915
order2.price // 27850

Scenario 1 - 2

/**
 * 情境編號:甲 - 2
 *
 * 決策法:最適組合 (item-based)
 *
 * 情境敘述:
 * 1. 指定商品(A~F)滿3件 打9折
 * 2. 指定商品(C~I)每5000元 折600元
 * 3. 指定分類(鞋子)滿4000元 送最低價商品
 * 4. 指定分類(Swell)每1件 打9折
 * 5. 全館 滿6件 贈紅利點數1000點
 * 6. 全館 滿6000元 免運
 *
 * 預期結果:
 * 取最優排列組合:2+4
 * 購物車顯示金額 24856
 */

// 指定商品(A~F)滿3件 打9折
const policy1 = new PercentageDiscount(
  0.9,
  new ItemIncluded({
    items: ['A', 'B', 'C', 'D', 'E', 'F'],
    threshold: 3,
  }),
  { id: 'SPECIFIED_A_F', onlyMatched: true }
);

// 2. 指定商品(C~I)每5000元 折600元
const policy2 = new StepValueDiscount(
  5000,
  600,
  new ItemIncluded<TestOrderItem>({
    items: ['C', 'D', 'E', 'F', 'G', 'H', 'I'],
  }),
  { id: 'SPECIFIED_C_I', stepUnit: 'price', onlyMatched: true }
);

// 3. 指定分類(鞋子)滿4000元 送最低價商品
const policy3 = new ItemGiveawayDiscount(
  1,
  new ItemIncluded<TestOrderItem>({
    scope: 'category',
    items: ['shoes'],
    conditions: [new PriceThreshold(4000)],
  }),
  { id: 'GIVEAWAY_BY_SHOES_1', onlyMatched: true }
);

// * 4. 指定分類(Swell)每1件 打9折
const policy4 = new StepPercentageDiscount(
  1,
  0.9,
  new ItemIncluded<TestOrderItem>({
    scope: 'brand',
    items: ['Swell'],
  }),
  {
    id: 'SPECIFIED_BRAND_BY_Swell_1',
    stepUnit: 'quantity',
    onlyMatched: true,
  }
);

// (1|2)&(3|4)
const builder1 = new OrderBuilder<TestOrderItem>({
  policyPickStrategy: 'item-based',
  policies: [
    [policy1, policy2],
    [policy3, policy4],
  ],
});

const order1 = builder1.build({ items });

const builder2 = new OrderBuilder<TestOrderItem>({
  policyPickStrategy: 'item-based',
  policies: [
    [policy1, policy2],
    policy3,
    policy4,
  ],
});

const order2 = builder2.build({ items });

order1.price === order2.price // true
order1.price // 22491
order2.price // 22491

order1.itemRecords
// [ 
//   {
//     itemId: 'A-1',
//     originItem: {
//       id: 'A',
//       name: '外套A',
//       unitPrice: 1000,
//       category: 'jacket',
//       brand: 'AJE',
//     },
//     initialValue: 1000,
//     discountValue: 100,
//     finalPrice: 900,
//     discountRecords: [
//       {
//         policyId: 'SPECIFIED_A_F',
//         discountValue: 100,
//       },
//     ],
//     appliedPolicies: [
//       {
//         prefix: 'DISCOUNT',
//         type: 'PERCENTAGE',
//         id: 'SPECIFIED_A_F',
//         value: 0.9,
//         conditions: [
//           {
//             type: 'QUANTITY',
//             items: ['A', 'B', 'C', 'D', 'E', 'F'],
//             threshold: 3,
//             conditions: [],
//             scope: 'id',
//           },
//         ],
//         options: {
//           id: 'SPECIFIED_A_F',
//           onlyMatched: true,
//         },
//       },
//     ],
//   },
//   ...

Logistics Fee

/**
 * 情境編號:總額滿 2000元 免運
 *
 * 情境敘述:
 * 1. 指定商品(B~E)滿2件 送最低價品
 * 2. 整筆訂單 滿 2000 元免運
 *
 * 預期結果: 4500 + 200 - 1500 - 200 = 3000
 * 購物車顯示金額 3000
 */
const originItems: TestOrderItem[] = [
  {
    id: 'A',
    name: '外套A',
    unitPrice: 1000,
    quantity: 1,
    category: 'jacket',
    brand: 'AJE',
  },
  {
    id: 'B',
    name: '外套B',
    unitPrice: 1500,
    quantity: 1,
    category: 'jacket',
    brand: 'N21',
  },
  {
    id: 'C',
    name: '鞋子C',
    unitPrice: 2000,
    quantity: 1,
    category: 'shoes',
    brand: 'N21',
  },
];

const order = new OrderBuilder()
  // 滿 2000 免運
  .setLogistics({
    price: 200,
    threshold: 2000,
    name: '運費',
  })
  // 指定商品 B, C, D, E 滿兩件送最低價品
  .addPolicy(
    new ItemGiveawayDiscount(
      1,
      new ItemIncluded<TestOrderItem>({
        items: ['B', 'C', 'D', 'E'],
        threshold: 2,
      }),
      { onlyMatched: true }
    )
  )
  .build({ items: originItems });

order.price // 4500 + 200 - 1500 - 200 = 3000

Matched Times & Excluded Calculating Policy

/**
 * 情境敘述:
 * 滿足條件贈送紅利點數、滿足條件可加購商品
 * 需要能單純判斷是否滿足特定條件、滿足幾次,不影響訂單計算的 policy 功能
 */
const items: TestOrderItem[] = [
  {
    id: 'A',
    name: '外套A',
    unitPrice: 1000,
    quantity: 1,
    category: 'jacket',
    brand: 'AJE',
  },
  {
    id: 'B',
    name: '外套B',
    unitPrice: 1500,
    quantity: 1,
    category: 'jacket',
    brand: 'N21',
  },
  {
    id: 'C',
    name: '鞋子C',
    unitPrice: 2000,
    quantity: 1,
    category: 'shoes',
    brand: 'N21',
  },
];

// Policy1: 每 2000 元折 200 元
const policy1 = new StepValueDiscount(
  2000,
  200,
  { stepUnit: 'price' },
);

// Policy1 滿足次數
new OrderBuilder()
  .addPolicy(policy1)
  .build({ items })
  .discounts
  .find(discount => discount.id === policy1.id)
  ?.matchedTimes; // step(4500, 2000) = 2

// Policy2: 每 1499 元打 8折 (Matched only Policy, will not participant in discounting)
const policy2 = new StepPercentageDiscount(
  1499,
  0.8,
  { stepUnit: 'price', excludedInCalculation: true }
);

const order = new OrderBuilder()
  .addPolicy(policy2)
  .build({ items });

// Policy2: 滿足次數
order
.discounts
.find(discount => discount.id === policy2id)
?.matchedTimes; // step(4500, 1499) = 3

// Policy2: Excluded in calculation.
order.discountValue // 0
0.0.21

11 months ago

0.0.20

1 year ago

0.0.19

1 year ago

0.0.15

2 years ago

0.0.16

2 years ago

0.0.17

2 years ago

0.0.18

2 years ago

0.0.13

2 years ago

0.0.14

2 years ago

0.0.10

2 years ago

0.0.11

2 years ago

0.0.12

2 years ago

0.0.9

2 years ago

0.0.8

2 years ago