@zoxllc/shopify-checkout-extensions 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migrate.ts +64 -0
- package/package.json +2 -1
- package/src/common/AndSelector.ts +17 -10
- package/src/common/Campaign.ts +27 -12
- package/src/common/CampaignFactory.ts +103 -51
- package/src/common/CartHasItemQualifier.ts +1 -1
- package/src/common/CartQuantityQualifier.ts +1 -1
- package/src/common/CountryCodeQualifier.ts +49 -0
- package/src/common/CustomerEmailQualifier.ts +14 -7
- package/src/common/CustomerTagQualifier.ts +29 -16
- package/src/common/DiscountCart.ts +78 -51
- package/src/common/DiscountInterface.ts +17 -2
- package/src/common/OrSelector.ts +13 -8
- package/src/common/ProductHandleSelector.ts +5 -3
- package/src/common/ProductIdSelector.ts +10 -4
- package/src/common/ProductTagSelector.ts +2 -1
- package/src/common/ProductTypeSelector.ts +10 -4
- package/src/common/SaleItemSelector.ts +2 -1
- package/src/common/Selector.ts +11 -3
- package/src/common/SubscriptionItemSelector.ts +2 -1
- package/src/index.ts +1 -1
- package/src/{common → lineItem}/BuyXGetY.ts +7 -7
- package/src/lineItem/ConditionalDiscount.ts +1 -1
- package/src/lineItem/FixedItemDiscount.ts +2 -4
- package/src/lineItem/PercentageDiscount.ts +2 -4
- package/src/shipping/FixedDiscount.ts +37 -0
- package/src/shipping/RateNameSelector.ts +54 -0
- package/src/shipping/ShippingDiscount.ts +75 -0
- package/src/shipping/api.ts +2097 -0
- package/tests/AndSelector.test.ts +1 -1
- package/tests/CartQuantityQualifier.test.ts +1 -1
- package/tests/CountryCodeQualifier.test.ts +56 -0
- package/tests/CustomerSubscriberQualifier.test.ts +1 -1
- package/tests/CustomerTagQualifier.test.ts +71 -0
- package/tests/DiscountCart.test.ts +1 -1
- package/tests/OrSelector.test.ts +1 -1
- package/tests/ProductIdSelector.test.ts +83 -0
- package/tests/ProductTagSelector.test.ts +1 -1
- package/tests/Qualifier.test.ts +1 -1
- package/tests/RateNameSelector.test.ts +107 -0
- package/tests/SaleItemSelector.test.ts +1 -1
- package/tests/Selector.test.ts +1 -1
- /package/src/{generated → lineItem}/api.ts +0 -0
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import type { RunInput
|
|
1
|
+
import type { RunInput as LineItemRunInput } from '../lineItem/api';
|
|
2
|
+
import type { RunInput as ShippingRunInput } from '../shipping/api';
|
|
3
|
+
import { Discount } from './DiscountInterface';
|
|
4
|
+
|
|
5
|
+
type RunInput = {
|
|
6
|
+
cart: LineItemRunInput['cart'] & ShippingRunInput['cart'];
|
|
7
|
+
};
|
|
2
8
|
|
|
3
9
|
export class DiscountCart {
|
|
4
|
-
cart: RunInput[
|
|
10
|
+
cart: RunInput['cart'];
|
|
5
11
|
subtotalAmount: number;
|
|
6
12
|
appliedDiscountTotal: number;
|
|
7
13
|
discounts: Discount[];
|
|
8
14
|
|
|
9
|
-
constructor(cart: RunInput[
|
|
15
|
+
constructor(cart: RunInput['cart']) {
|
|
10
16
|
this.cart = cart;
|
|
11
17
|
this.subtotalAmount = 0;
|
|
12
18
|
this.appliedDiscountTotal = 0;
|
|
@@ -17,7 +23,7 @@ export class DiscountCart {
|
|
|
17
23
|
|
|
18
24
|
subtotalExcludingVariants(variants: string[]) {
|
|
19
25
|
return this.cart.lines.reduce((prev, curr) => {
|
|
20
|
-
if (curr.merchandise.__typename ==
|
|
26
|
+
if (curr.merchandise.__typename == 'ProductVariant') {
|
|
21
27
|
if (variants.indexOf(curr.merchandise.id) > -1) {
|
|
22
28
|
return prev - curr.cost.subtotalAmount.amount;
|
|
23
29
|
}
|
|
@@ -29,10 +35,10 @@ export class DiscountCart {
|
|
|
29
35
|
|
|
30
36
|
private subtotalExcludingGifts() {
|
|
31
37
|
return this.cart.lines.reduce((prev, curr) => {
|
|
32
|
-
if (curr.merchandise.__typename ==
|
|
38
|
+
if (curr.merchandise.__typename == 'ProductVariant') {
|
|
33
39
|
if (
|
|
34
40
|
curr.merchandise.product.hasTags.filter(
|
|
35
|
-
(t) => t.hasTag && t.tag ==
|
|
41
|
+
(t) => t.hasTag && t.tag == 'meta-exclude-gift'
|
|
36
42
|
).length
|
|
37
43
|
) {
|
|
38
44
|
return prev - curr.cost.subtotalAmount.amount;
|
|
@@ -44,15 +50,18 @@ export class DiscountCart {
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
totalLineItemQuantity() {
|
|
47
|
-
return this.cart.lines.reduce(
|
|
53
|
+
return this.cart.lines.reduce(
|
|
54
|
+
(total, item) => total + item.quantity,
|
|
55
|
+
0
|
|
56
|
+
);
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
totalLineItemQuantityExcludingGifts() {
|
|
51
60
|
return this.cart.lines.reduce((total, item) => {
|
|
52
|
-
if (item.merchandise.__typename ==
|
|
61
|
+
if (item.merchandise.__typename == 'ProductVariant') {
|
|
53
62
|
if (
|
|
54
63
|
item.merchandise.product.hasTags.filter(
|
|
55
|
-
(t) => t.hasTag && t.tag ==
|
|
64
|
+
(t) => t.hasTag && t.tag == 'meta-exclude-gift'
|
|
56
65
|
).length
|
|
57
66
|
) {
|
|
58
67
|
return total;
|
|
@@ -62,8 +71,9 @@ export class DiscountCart {
|
|
|
62
71
|
}, 0);
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
private setupCart(cart: RunInput[
|
|
66
|
-
this.subtotalAmount = cart.cost.subtotalAmount
|
|
74
|
+
private setupCart(cart: RunInput['cart']) {
|
|
75
|
+
this.subtotalAmount = cart.cost.subtotalAmount
|
|
76
|
+
.amount as number;
|
|
67
77
|
// Adjust the subtotal amount by excluding the value from gift items
|
|
68
78
|
this.subtotalAmount = this.subtotalExcludingGifts();
|
|
69
79
|
this.appliedDiscountTotal = 0;
|
|
@@ -72,51 +82,68 @@ export class DiscountCart {
|
|
|
72
82
|
|
|
73
83
|
addDiscount(discount: Discount) {
|
|
74
84
|
// Figure out how much discount has been applied
|
|
75
|
-
const totalDiscounts = discount.targets.reduce(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
85
|
+
const totalDiscounts = discount.targets.reduce(
|
|
86
|
+
(total, target) => {
|
|
87
|
+
// gather target lineItem
|
|
88
|
+
const lineItems = this.cart.lines.filter((line) =>
|
|
89
|
+
line.merchandise.__typename == 'ProductVariant'
|
|
90
|
+
? line.merchandise.id ==
|
|
91
|
+
target.productVariant.id
|
|
92
|
+
: false
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// loop over the line items and do maths
|
|
96
|
+
lineItems.forEach((line) => {
|
|
97
|
+
let discountableItems = target.productVariant
|
|
98
|
+
.quantity
|
|
99
|
+
? target.productVariant.quantity
|
|
100
|
+
: line.quantity;
|
|
101
|
+
|
|
102
|
+
const lineItemCost = parseFloat(
|
|
103
|
+
line.cost.amountPerQuantity.amount
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// TODO: Make sure we're not discounting more items than are available on the line item quantity
|
|
107
|
+
|
|
108
|
+
// When it's a fixed amount, calculate the items per but only if it applies to each
|
|
109
|
+
// individual item...
|
|
110
|
+
if (discount.value.fixedAmount) {
|
|
111
|
+
if (
|
|
112
|
+
discount.value.fixedAmount.appliesToEachItem
|
|
113
|
+
) {
|
|
114
|
+
total +=
|
|
115
|
+
lineItemCost -
|
|
116
|
+
discount.value.fixedAmount?.amount >
|
|
117
|
+
0
|
|
118
|
+
? discount.value.fixedAmount?.amount *
|
|
119
|
+
discountableItems
|
|
120
|
+
: lineItemCost * discountableItems;
|
|
121
|
+
} else {
|
|
122
|
+
total +=
|
|
123
|
+
lineItemCost -
|
|
124
|
+
discount.value.fixedAmount?.amount >
|
|
125
|
+
0
|
|
126
|
+
? discount.value.fixedAmount?.amount
|
|
127
|
+
: lineItemCost;
|
|
128
|
+
}
|
|
129
|
+
} else if (discount.value.percentage) {
|
|
102
130
|
total +=
|
|
103
|
-
lineItemCost
|
|
104
|
-
|
|
105
|
-
|
|
131
|
+
lineItemCost *
|
|
132
|
+
(discount.value.percentage?.value / 100) *
|
|
133
|
+
discountableItems;
|
|
106
134
|
}
|
|
107
|
-
}
|
|
108
|
-
total +=
|
|
109
|
-
lineItemCost *
|
|
110
|
-
(discount.value.percentage?.value / 100) *
|
|
111
|
-
discountableItems;
|
|
112
|
-
}
|
|
113
|
-
});
|
|
135
|
+
});
|
|
114
136
|
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
return total;
|
|
138
|
+
},
|
|
139
|
+
0
|
|
140
|
+
);
|
|
117
141
|
|
|
118
142
|
this.appliedDiscountTotal += totalDiscounts;
|
|
119
|
-
console.error(
|
|
143
|
+
console.error(
|
|
144
|
+
'appliedDiscountTotal',
|
|
145
|
+
this.appliedDiscountTotal
|
|
146
|
+
);
|
|
120
147
|
|
|
121
148
|
this.discounts.push(discount);
|
|
122
149
|
}
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CartDeliveryOption,
|
|
3
|
+
CartLine,
|
|
4
|
+
} from '../lineItem/api';
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
import type { Discount as ItemDiscount } from '../lineItem/api';
|
|
7
|
+
import type { Discount as ShippingDiscount } from '~/shipping/api';
|
|
8
|
+
|
|
9
|
+
export type Discount = ItemDiscount & ShippingDiscount;
|
|
10
|
+
|
|
11
|
+
type ApplyLineItemDiscount = {
|
|
4
12
|
lineItem: CartLine;
|
|
5
13
|
maxDiscounts?: number;
|
|
6
14
|
};
|
|
7
15
|
|
|
16
|
+
type ApplyShippingDiscount = {
|
|
17
|
+
deliveryOption: CartDeliveryOption;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ApplyDiscount = ApplyLineItemDiscount &
|
|
21
|
+
ApplyShippingDiscount;
|
|
22
|
+
|
|
8
23
|
export interface DiscountInterface {
|
|
9
24
|
apply(items: ApplyDiscount[]): Discount;
|
|
10
25
|
}
|
package/src/common/OrSelector.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import type { DiscountCart } from
|
|
2
|
-
import type { Qualifier } from
|
|
3
|
-
import type { Selector } from
|
|
4
|
-
import type { CartLine } from '../
|
|
1
|
+
import type { DiscountCart } from './DiscountCart';
|
|
2
|
+
import type { Qualifier } from './Qualifier';
|
|
3
|
+
import type { Selector } from './Selector';
|
|
4
|
+
import type { CartLine } from '../lineItem/api';
|
|
5
5
|
|
|
6
6
|
export class OrSelector {
|
|
7
|
-
is_a:
|
|
7
|
+
is_a: 'OrSelector';
|
|
8
8
|
conditions: (Selector | Qualifier)[];
|
|
9
9
|
|
|
10
10
|
constructor(conditions: (Selector | Qualifier)[]) {
|
|
11
|
-
this.is_a =
|
|
11
|
+
this.is_a = 'OrSelector';
|
|
12
12
|
this.conditions = conditions;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
match(
|
|
15
|
+
match(
|
|
16
|
+
item: CartLine | DiscountCart,
|
|
17
|
+
selector?: Selector
|
|
18
|
+
) {
|
|
16
19
|
try {
|
|
17
20
|
const conditionsResult = this.conditions
|
|
18
21
|
.map((condition) => {
|
|
@@ -22,7 +25,9 @@ export class OrSelector {
|
|
|
22
25
|
selector
|
|
23
26
|
);
|
|
24
27
|
} else {
|
|
25
|
-
return (condition as Selector).match(
|
|
28
|
+
return (condition as Selector).match(
|
|
29
|
+
item as CartLine
|
|
30
|
+
);
|
|
26
31
|
}
|
|
27
32
|
})
|
|
28
33
|
.filter((result) => result === true);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CartLine,
|
|
3
3
|
ProductVariant,
|
|
4
|
-
} from '../
|
|
5
|
-
import { MatchType, Selector } from
|
|
4
|
+
} from '../lineItem/api';
|
|
5
|
+
import { MatchType, Selector } from './Selector';
|
|
6
6
|
|
|
7
7
|
export class ProductHandleSelector extends Selector {
|
|
8
8
|
invert: boolean;
|
|
@@ -18,7 +18,9 @@ export class ProductHandleSelector extends Selector {
|
|
|
18
18
|
const variant = lineItem.merchandise as ProductVariant;
|
|
19
19
|
|
|
20
20
|
const matchResult =
|
|
21
|
-
this.handles.indexOf(
|
|
21
|
+
this.handles.indexOf(
|
|
22
|
+
variant.product.handle.toLowerCase()
|
|
23
|
+
) > -1;
|
|
22
24
|
|
|
23
25
|
if (this.invert) {
|
|
24
26
|
return !matchResult;
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CartLine,
|
|
3
3
|
ProductVariant,
|
|
4
|
-
} from '../
|
|
5
|
-
import { MatchType, Selector } from
|
|
4
|
+
} from '../lineItem/api';
|
|
5
|
+
import { MatchType, Selector } from './Selector';
|
|
6
6
|
|
|
7
7
|
export class ProductIdSelector extends Selector {
|
|
8
8
|
invert: boolean;
|
|
9
9
|
productIds: string[];
|
|
10
10
|
|
|
11
|
-
constructor(
|
|
11
|
+
constructor(
|
|
12
|
+
matchType: MatchType.IS_ONE | MatchType.NOT_ONE,
|
|
13
|
+
productIds: string[]
|
|
14
|
+
) {
|
|
12
15
|
super();
|
|
13
16
|
this.invert = matchType == MatchType.NOT_ONE;
|
|
14
17
|
this.productIds = productIds;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
match(subject: unknown): boolean;
|
|
17
21
|
match(lineItem: CartLine) {
|
|
18
22
|
const variant = lineItem.merchandise as ProductVariant;
|
|
19
23
|
|
|
20
|
-
const matchResult = this.productIds.includes(
|
|
24
|
+
const matchResult = this.productIds.includes(
|
|
25
|
+
variant.product.id
|
|
26
|
+
);
|
|
21
27
|
|
|
22
28
|
if (this.invert) {
|
|
23
29
|
return !matchResult;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CartLine,
|
|
3
3
|
ProductVariant,
|
|
4
|
-
} from '../
|
|
4
|
+
} from '../lineItem/api';
|
|
5
5
|
import { Selector } from './Selector';
|
|
6
6
|
import {
|
|
7
7
|
QualifierMatchType,
|
|
@@ -24,6 +24,7 @@ export class ProductTagSelector extends Selector {
|
|
|
24
24
|
this.tags = tags.map((t) => t.toLowerCase());
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
match(subject: unknown): boolean;
|
|
27
28
|
match(lineItem: CartLine) {
|
|
28
29
|
const variant = lineItem.merchandise as ProductVariant;
|
|
29
30
|
const productTags = variant.product.hasTags.reduce(
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CartLine,
|
|
3
3
|
ProductVariant,
|
|
4
|
-
} from '../
|
|
5
|
-
import { MatchType, Selector } from
|
|
4
|
+
} from '../lineItem/api';
|
|
5
|
+
import { MatchType, Selector } from './Selector';
|
|
6
6
|
|
|
7
7
|
export class ProductTypeSelector extends Selector {
|
|
8
8
|
invert: boolean;
|
|
9
9
|
productTypes: string[];
|
|
10
10
|
|
|
11
|
-
constructor(
|
|
11
|
+
constructor(
|
|
12
|
+
matchType: MatchType,
|
|
13
|
+
productTypes: string[]
|
|
14
|
+
) {
|
|
12
15
|
super();
|
|
13
16
|
this.invert = matchType == MatchType.NOT_ONE;
|
|
14
|
-
this.productTypes = productTypes.map((item) =>
|
|
17
|
+
this.productTypes = productTypes.map((item) =>
|
|
18
|
+
item.toLowerCase()
|
|
19
|
+
);
|
|
15
20
|
}
|
|
16
21
|
|
|
22
|
+
match(subject: unknown): boolean;
|
|
17
23
|
match(lineItem: CartLine) {
|
|
18
24
|
const variant = lineItem.merchandise as ProductVariant;
|
|
19
25
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CartLine } from '../
|
|
1
|
+
import { CartLine } from '../lineItem/api';
|
|
2
2
|
import { Selector } from './Selector';
|
|
3
3
|
|
|
4
4
|
export enum SaleItemSelectorMatchType {
|
|
@@ -16,6 +16,7 @@ export class SaleItemSelector extends Selector {
|
|
|
16
16
|
this.invert = matchType == SaleItemSelectorMatchType.IS;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
match(subject: unknown): boolean;
|
|
19
20
|
match(lineItem: CartLine) {
|
|
20
21
|
// Check to see if the item is NOT on sale
|
|
21
22
|
const matchResult =
|
package/src/common/Selector.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CartDeliveryOption } from '~/shipping/api';
|
|
2
|
+
import type { CartLine } from '../lineItem/api';
|
|
2
3
|
import { StringComparisonType } from './Qualifier';
|
|
3
4
|
|
|
4
5
|
export enum MatchType {
|
|
@@ -9,8 +10,15 @@ export enum MatchType {
|
|
|
9
10
|
'DOES_NOT' = ':does_not',
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export
|
|
13
|
-
match(subject: CartLine
|
|
13
|
+
export type SelectorInterface = {
|
|
14
|
+
match(subject: CartLine): boolean;
|
|
15
|
+
match(subject: CartDeliveryOption): boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class Selector implements SelectorInterface {
|
|
19
|
+
match(subject: CartLine): boolean;
|
|
20
|
+
match(subject: CartDeliveryOption): boolean;
|
|
21
|
+
match(subject: unknown): boolean {
|
|
14
22
|
return true;
|
|
15
23
|
}
|
|
16
24
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CartLine } from '../
|
|
1
|
+
import type { CartLine } from '../lineItem/api';
|
|
2
2
|
import { MatchType, Selector } from './Selector';
|
|
3
3
|
|
|
4
4
|
export class SubscriptionItemSelector extends Selector {
|
|
@@ -9,6 +9,7 @@ export class SubscriptionItemSelector extends Selector {
|
|
|
9
9
|
this.invert = matchType == MatchType.NOT_ONE;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
match(subject: unknown): boolean;
|
|
12
13
|
match(lineItem: CartLine) {
|
|
13
14
|
const matchResult =
|
|
14
15
|
lineItem.sellingPlanAllocation !== undefined;
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { ConditionalDiscount } from './lineItem/ConditionalDiscount';
|
|
|
2
2
|
export { FixedItemDiscount } from './lineItem/FixedItemDiscount';
|
|
3
3
|
export { PercentageDiscount } from './lineItem/PercentageDiscount';
|
|
4
4
|
export { AndSelector } from './common/AndSelector';
|
|
5
|
-
export { BuyXGetY } from './
|
|
5
|
+
export { BuyXGetY } from './lineItem/BuyXGetY';
|
|
6
6
|
export { Campaign } from './common/Campaign';
|
|
7
7
|
export type {
|
|
8
8
|
CampaignType,
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { Campaign } from
|
|
2
|
-
import type { AndSelector } from '
|
|
3
|
-
import type { DiscountCart } from '
|
|
1
|
+
import { Campaign } from '../common/Campaign';
|
|
2
|
+
import type { AndSelector } from '../common/AndSelector';
|
|
3
|
+
import type { DiscountCart } from '../common/DiscountCart';
|
|
4
4
|
import type {
|
|
5
5
|
ApplyDiscount,
|
|
6
6
|
DiscountInterface,
|
|
7
|
-
} from '
|
|
8
|
-
import type { OrSelector } from '
|
|
7
|
+
} from '../common/DiscountInterface';
|
|
8
|
+
import type { OrSelector } from '../common/OrSelector';
|
|
9
9
|
import type {
|
|
10
10
|
Qualifier,
|
|
11
11
|
QualifierBehavior,
|
|
12
|
-
} from '
|
|
13
|
-
import type { Selector } from '
|
|
12
|
+
} from '../common/Qualifier';
|
|
13
|
+
import type { Selector } from '../common/Selector';
|
|
14
14
|
|
|
15
15
|
export class BuyXGetY extends Campaign {
|
|
16
16
|
lineItemSelector?: Selector;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
import type { ProductVariant } from './api';
|
|
1
2
|
import type {
|
|
2
3
|
Discount,
|
|
3
|
-
ProductVariant,
|
|
4
|
-
} from '../generated/api';
|
|
5
|
-
import type {
|
|
6
4
|
ApplyDiscount,
|
|
7
5
|
DiscountInterface,
|
|
8
6
|
} from '../common/DiscountInterface';
|
|
@@ -41,6 +39,6 @@ export class PercentageDiscount
|
|
|
41
39
|
},
|
|
42
40
|
},
|
|
43
41
|
message: this.message,
|
|
44
|
-
};
|
|
42
|
+
} as Discount;
|
|
45
43
|
}
|
|
46
44
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApplyDiscount,
|
|
3
|
+
Discount,
|
|
4
|
+
DiscountInterface,
|
|
5
|
+
} from '../common/DiscountInterface';
|
|
6
|
+
|
|
7
|
+
export class FixedDiscount implements DiscountInterface {
|
|
8
|
+
amount: number;
|
|
9
|
+
message: string;
|
|
10
|
+
|
|
11
|
+
constructor(amount: number, message: string) {
|
|
12
|
+
this.amount = amount;
|
|
13
|
+
this.message = message;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
apply(items: ApplyDiscount[]): Discount {
|
|
17
|
+
const targets = items.map(
|
|
18
|
+
({ deliveryOption: { handle } }) => {
|
|
19
|
+
return {
|
|
20
|
+
deliveryOption: {
|
|
21
|
+
handle,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
targets,
|
|
29
|
+
value: {
|
|
30
|
+
fixedAmount: {
|
|
31
|
+
amount: this.amount,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
message: this.message,
|
|
35
|
+
} as Discount;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CartDeliveryOption } from '../shipping/api';
|
|
2
|
+
import { MatchType, Selector } from '../common/Selector';
|
|
3
|
+
import { StringComparisonType } from '../common/Qualifier';
|
|
4
|
+
|
|
5
|
+
export class RateNameSelector extends Selector {
|
|
6
|
+
invert: boolean;
|
|
7
|
+
matchCondition: StringComparisonType;
|
|
8
|
+
names: string[];
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
matchType: MatchType.DOES | MatchType.DOES_NOT,
|
|
12
|
+
matchCondition: StringComparisonType,
|
|
13
|
+
names: string[]
|
|
14
|
+
) {
|
|
15
|
+
super();
|
|
16
|
+
this.invert = matchType == MatchType.DOES_NOT;
|
|
17
|
+
this.matchCondition = matchCondition;
|
|
18
|
+
this.names = names.map((e) => e.toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
match(subject: unknown): boolean;
|
|
22
|
+
match(subject: CartDeliveryOption) {
|
|
23
|
+
let matchResult;
|
|
24
|
+
|
|
25
|
+
const title = subject?.title?.toLowerCase() || '';
|
|
26
|
+
|
|
27
|
+
switch (this.matchCondition) {
|
|
28
|
+
case StringComparisonType.MATCH:
|
|
29
|
+
matchResult = this.names.includes(title);
|
|
30
|
+
break;
|
|
31
|
+
case StringComparisonType.CONTAINS:
|
|
32
|
+
matchResult =
|
|
33
|
+
this.names.filter((e) => title.indexOf(e) > -1)
|
|
34
|
+
.length > 0;
|
|
35
|
+
break;
|
|
36
|
+
case StringComparisonType.START_WITH:
|
|
37
|
+
matchResult =
|
|
38
|
+
this.names.filter((e) => title.startsWith(e))
|
|
39
|
+
.length > 0;
|
|
40
|
+
break;
|
|
41
|
+
case StringComparisonType.END_WITH:
|
|
42
|
+
matchResult =
|
|
43
|
+
this.names.filter((e) => title.endsWith(e))
|
|
44
|
+
.length > 0;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (this.invert) {
|
|
49
|
+
return !matchResult;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return matchResult;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CartDeliveryOption } from './api';
|
|
2
|
+
import { Campaign } from '../common/Campaign';
|
|
3
|
+
import type { DiscountCart } from '../common/DiscountCart';
|
|
4
|
+
import type {
|
|
5
|
+
ApplyDiscount,
|
|
6
|
+
DiscountInterface,
|
|
7
|
+
} from '../common/DiscountInterface';
|
|
8
|
+
import type {
|
|
9
|
+
QualifierBehavior,
|
|
10
|
+
Qualifier,
|
|
11
|
+
} from '../common/Qualifier';
|
|
12
|
+
import type { Selector } from '../common/Selector';
|
|
13
|
+
import type { AndSelector } from '../common/AndSelector';
|
|
14
|
+
import type { OrSelector } from '../common/OrSelector';
|
|
15
|
+
|
|
16
|
+
export class ShippingDiscount extends Campaign {
|
|
17
|
+
discount: DiscountInterface;
|
|
18
|
+
rateSelector: Selector;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
behavior: QualifierBehavior,
|
|
22
|
+
qualifiers:
|
|
23
|
+
| (Qualifier | AndSelector | OrSelector)[]
|
|
24
|
+
| null,
|
|
25
|
+
discount: DiscountInterface,
|
|
26
|
+
rateSelector: Selector
|
|
27
|
+
) {
|
|
28
|
+
super(behavior, qualifiers);
|
|
29
|
+
this.discount = discount;
|
|
30
|
+
this.rateSelector = rateSelector;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
run(discountCart: DiscountCart) {
|
|
34
|
+
// First we see if this cart qualifies for this discount
|
|
35
|
+
if (this.qualifies(discountCart)) {
|
|
36
|
+
// Now that it qualifies, we can determine the applicableDeliveryOptions
|
|
37
|
+
// to apply discounts to by...
|
|
38
|
+
// Loop through all deliveryGroups
|
|
39
|
+
const applicableDiscounts =
|
|
40
|
+
discountCart.cart.deliveryGroups
|
|
41
|
+
.reduce((deliveryOptions, deliveryGroup) => {
|
|
42
|
+
// Loop through its delivery options
|
|
43
|
+
deliveryGroup.deliveryOptions.forEach((o) => {
|
|
44
|
+
const deliveryOption =
|
|
45
|
+
o as CartDeliveryOption;
|
|
46
|
+
// 2. Looping any deliveryOptions within the group
|
|
47
|
+
if (
|
|
48
|
+
this.rateSelector &&
|
|
49
|
+
this.rateSelector.match(deliveryOption)
|
|
50
|
+
) {
|
|
51
|
+
// if this is a match, we want to apply this discount
|
|
52
|
+
deliveryOptions.push(deliveryOption);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return deliveryOptions;
|
|
57
|
+
}, [] as CartDeliveryOption[])
|
|
58
|
+
.map((deliveryOption) => {
|
|
59
|
+
return {
|
|
60
|
+
deliveryOption: {
|
|
61
|
+
handle: deliveryOption.handle,
|
|
62
|
+
},
|
|
63
|
+
} as ApplyDiscount;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (applicableDiscounts.length) {
|
|
67
|
+
const discount = this.discount.apply(
|
|
68
|
+
applicableDiscounts
|
|
69
|
+
);
|
|
70
|
+
discountCart.addDiscount(discount);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return discountCart;
|
|
74
|
+
}
|
|
75
|
+
}
|