npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@rytass/order-builder

v0.0.21

Published

Rytass Utils OrderBuilder

Downloads

1

Readme

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

| Arguments | index | Type | Required | | -------------------- | ------- | ------------------------------- | -------- | | overload1 | | | | | builder | 0 | OrderBuilder | false | | overload2 | | | | | options | 0 | OrderBuilderConstructor | false |

OrderBuilderConstructor

| Properties | Type | Required | | -------------------- | ------------------------ | -------- | | policies | Policy[] | true | | discountMethod | "price-weighted-average" | "quantity-weighted-average" | false | | roundStrategy | "every-calculation" | "final-price-only" | "no-round" | false | | logistics | OrderLogistics | false |

| OrderBuilder | Return | Description | | ---------------------------------------- | ----------------- | ----------- | | Properties | | | | config | OrderConfig | Get the configuration of current builder. | | policies | Policies[] | Get all the policies of the builder. (activated + none-activated) | | hasBuiltOrders | boolean | Checks whether this builder has already built any instance of order. | | Methods | | | | build | Order | Create an order instance, and lock the access of mutations on policy. | | clone | OrderBuilder | Create a new OrderBuilder instance based on current instance. | | addPolicy(policy: Policies) | this | Push policy instance(s) into builder.policies (can active as builder.hasBuiltOrders is false). | | addPolicy(policies: Policies[]) | | | | removePolicy(policy: Policy) | this | Remove 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 | undefined | Get the specified policy instance by policy.id | | setLogistics(logistics: OrderLogistics)| this | Set the logistics of the order. |


Order

OrderConstructor

| Properties | Type | Required | | -------------------- | ------------- | -------- | | items | OrderItem[] | true | | coupons | string[] | false |

| Order | Return | Description | | ---------------------------------------- | ----------------- | ----------- | | Properties | | | | builder | OrderBuilder | Get the the owner of current order. | | config | OrderConfig | Get the configuration from its builder. | | policies | Policies[] | Get all the policies from its builder. | | coupons | string[] | Get all the coupons from current order. | | items | OrderItem[] | Get all the items from current order. | | itemManager | OrderItemManager | Get the item manager. | | itemRecords | OrderItemRecord[] | Get all the detail-records of item based on policies. | | discounts | PolicyDiscountDescription[] | Get the descriptions of discount based on applied-policies. | | discountValue | number | Get total discount in this order. | | itemValue | number | Get total value of items in this order. | | itemQuantity | number | Get total quantity of items in this order. | | price | number | Get final price of order. | | logistics | OrderLogistics | Get logistics config of current order. | | logisticsRecord | OrderItemRecord | Get logistics in the type of order-item-record. | | Methods | | | | subOrder(subCondition: SubOrderCondition)| Order | Create a new instance of Order based on the sub-condition on items and coupons. | | addCoupon(coupon: string) | this | Push coupon(s) into order.coupons. | | addCoupon(coupons: string[]) | | | | removeCoupon(coupon: string) | this | Remove coupons(s) from order.coupons | | removeCoupon(coupons: string[]) | | | | addItem(item: OrderItem) | this | Push item(s) into order.items. | | addItem(items: OrderItem[]) | | | | removeItem(id: string, quantity: number) | this | Remove item(s) from order.items. | | removeItem(item: OrderItem) | | | | removeItem(item: string) | | | | removeItem(items: OrderItem[]) | | | | removeItem(items: string[]) | | |


Discount

| Arguments | index | Type | Required | | -------------------- | ------- | --------------- | -------- | | overload1 | | | | | value | 0 | number | true | | condition | 1 | Condition | false | | options | 2 | DiscountOptions | false | | overload2 | | | | | value | 0 | number | true | | conditions | 1 | Condition[] | false | | options | 2 | DiscountOptions | false | | overload3 | | | | | value | 0 | number | true | | options | 1 | DiscountOptions | false |

Interfaces Remark

| Interface | Type | | -------------- | ---------------------- | | Policies | Policy | Policy[] | | RemovePolicy | Policy | 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