@zoxllc/shopify-checkout-extensions 0.0.1
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/README.md +3 -0
- package/package.json +31 -0
- package/src/common/AndSelector.ts +35 -0
- package/src/common/BuyXGetY.ts +109 -0
- package/src/common/Campaign.ts +55 -0
- package/src/common/CampaignConfiguration.ts +10 -0
- package/src/common/CampaignFactory.ts +180 -0
- package/src/common/CartAmountQualifier.ts +64 -0
- package/src/common/CartHasItemQualifier.ts +64 -0
- package/src/common/CartQuantityQualifier.ts +73 -0
- package/src/common/CustomerEmailQualifier.ts +60 -0
- package/src/common/CustomerSubscriberQualifier.ts +32 -0
- package/src/common/CustomerTagQualifier.ts +56 -0
- package/src/common/DiscountCart.ts +130 -0
- package/src/common/DiscountInterface.ts +13 -0
- package/src/common/OrSelector.ts +35 -0
- package/src/common/PostCartAmountQualifier.ts +21 -0
- package/src/common/ProductHandleSelector.ts +29 -0
- package/src/common/ProductIdSelector.ts +28 -0
- package/src/common/ProductTagSelector.ts +54 -0
- package/src/common/ProductTypeSelector.ts +34 -0
- package/src/common/Qualifier.ts +67 -0
- package/src/common/SaleItemSelector.ts +35 -0
- package/src/common/Selector.ts +53 -0
- package/src/common/SubscriptionItemSelector.ts +21 -0
- package/src/generated/api.ts +2103 -0
- package/src/index.ts +39 -0
- package/src/lineItem/ConditionalDiscount.ts +102 -0
- package/src/lineItem/DiscountCodeList.ts +91 -0
- package/src/lineItem/FixedItemDiscount.ts +53 -0
- package/src/lineItem/PercentageDiscount.ts +46 -0
- package/tests/AndSelector.test.ts +27 -0
- package/tests/CartQuantityQualifier.test.ts +381 -0
- package/tests/CustomerSubscriberQualifier.test.ts +101 -0
- package/tests/DiscountCart.test.ts +115 -0
- package/tests/OrSelector.test.ts +27 -0
- package/tests/ProductTagSelector.test.ts +75 -0
- package/tests/Qualifier.test.ts +193 -0
- package/tests/SaleItemSelector.test.ts +113 -0
- package/tests/Selector.test.ts +83 -0
- package/tsconfig.json +25 -0
package/README.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zoxllc/shopify-checkout-extensions",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ZOX helper lib for shopify checkout extensions",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"directories": {
|
|
7
|
+
"test": "tests"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "vitest"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/zoxllc/shopify-checkout-extensions.git"
|
|
15
|
+
},
|
|
16
|
+
"author": "Sebastian Nievas",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/zoxllc/shopify-checkout-extensions/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/zoxllc/shopify-checkout-extensions#readme",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@shopify/shopify_function": "0.1.0",
|
|
24
|
+
"@types/lodash": "^4.14.202",
|
|
25
|
+
"javy": "0.1.1",
|
|
26
|
+
"lodash": "^4.17.21"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"vitest": "^1.3.1"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DiscountCart } from "./DiscountCart";
|
|
2
|
+
import type { Qualifier } from "./Qualifier";
|
|
3
|
+
import type { Selector } from "./Selector";
|
|
4
|
+
import type { CartLine } from "extensions/zox-product-discount/generated/api";
|
|
5
|
+
|
|
6
|
+
export class AndSelector {
|
|
7
|
+
is_a: "AndSelector";
|
|
8
|
+
conditions: (Qualifier | Selector)[];
|
|
9
|
+
|
|
10
|
+
constructor(conditions: (Qualifier | Selector)[]) {
|
|
11
|
+
this.is_a = "AndSelector";
|
|
12
|
+
this.conditions = conditions;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
match(subject: CartLine | DiscountCart, selector?: Selector) {
|
|
16
|
+
try {
|
|
17
|
+
const conditionsResult = this.conditions
|
|
18
|
+
.map((condition) => {
|
|
19
|
+
if (selector) {
|
|
20
|
+
return (condition as Qualifier).match(
|
|
21
|
+
subject as DiscountCart,
|
|
22
|
+
selector,
|
|
23
|
+
);
|
|
24
|
+
} else {
|
|
25
|
+
return (condition as Selector).match(subject as CartLine);
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
.filter((result) => result === true);
|
|
29
|
+
|
|
30
|
+
return conditionsResult.length == this.conditions.length;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Campaign } from "./Campaign";
|
|
2
|
+
import type { CartLine } from "../../../../extensions/zox-product-discount/generated/api";
|
|
3
|
+
import type { AndSelector } from "./AndSelector";
|
|
4
|
+
import type { DiscountCart } from "./DiscountCart";
|
|
5
|
+
import type { ApplyDiscount, DiscountInterface } from "./DiscountInterface";
|
|
6
|
+
import type { OrSelector } from "./OrSelector";
|
|
7
|
+
import type { Qualifier, QualifierBehavior } from "./Qualifier";
|
|
8
|
+
import type { Selector } from "./Selector";
|
|
9
|
+
|
|
10
|
+
export class BuyXGetY extends Campaign {
|
|
11
|
+
lineItemSelector?: Selector;
|
|
12
|
+
getItemSelector?: Selector;
|
|
13
|
+
discount: DiscountInterface;
|
|
14
|
+
buyX: number;
|
|
15
|
+
getY: number;
|
|
16
|
+
maxSets?: number;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
behavior: QualifierBehavior,
|
|
20
|
+
conditions: [AndSelector | OrSelector | Qualifier],
|
|
21
|
+
discount: DiscountInterface,
|
|
22
|
+
buyItemSelector: Selector,
|
|
23
|
+
getItemSelector: Selector,
|
|
24
|
+
buyX: number,
|
|
25
|
+
getY: number,
|
|
26
|
+
maxSets?: number
|
|
27
|
+
) {
|
|
28
|
+
super(behavior, conditions);
|
|
29
|
+
this.lineItemSelector = buyItemSelector;
|
|
30
|
+
this.getItemSelector = getItemSelector;
|
|
31
|
+
this.discount = discount;
|
|
32
|
+
this.buyX = buyX;
|
|
33
|
+
this.getY = getY;
|
|
34
|
+
this.maxSets = maxSets;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
run(discountCart: DiscountCart) {
|
|
38
|
+
if (this.qualifies(discountCart)) {
|
|
39
|
+
// Find the items that qualify for buyX
|
|
40
|
+
let applicableBuyItems = discountCart.cart.lines;
|
|
41
|
+
if (this.lineItemSelector) {
|
|
42
|
+
applicableBuyItems = discountCart.cart.lines.filter((lineItem) => {
|
|
43
|
+
return this.lineItemSelector?.match(lineItem as CartLine);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
applicableBuyItems.reduce((qty, item) => (qty += item.quantity), 0) <
|
|
49
|
+
this.buyX
|
|
50
|
+
) {
|
|
51
|
+
return discountCart;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Find the items that qualify for getX
|
|
55
|
+
let eligibleGetItems = discountCart.cart.lines;
|
|
56
|
+
if (this.getItemSelector) {
|
|
57
|
+
eligibleGetItems = discountCart.cart.lines.filter((lineItem) => {
|
|
58
|
+
return this.getItemSelector?.match(lineItem as CartLine);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check if cart qualifies for discounts and limit the discount sets
|
|
63
|
+
const purchasedQuantity = applicableBuyItems.reduce(
|
|
64
|
+
(total, item) => (total += item.quantity),
|
|
65
|
+
0
|
|
66
|
+
);
|
|
67
|
+
const discountableSets = this.maxSets
|
|
68
|
+
? [purchasedQuantity / this.buyX, this.maxSets].sort((a, b) => a - b)[0]
|
|
69
|
+
: Math.floor(purchasedQuantity / this.buyX);
|
|
70
|
+
|
|
71
|
+
if (discountableSets > 0) {
|
|
72
|
+
let discountableQuantity =
|
|
73
|
+
discountableSets *
|
|
74
|
+
(this.getY ? this.getY : discountCart.totalLineItemQuantity());
|
|
75
|
+
|
|
76
|
+
// Apply the discounts (sort to discount lower priced items first)
|
|
77
|
+
eligibleGetItems = eligibleGetItems.sort(
|
|
78
|
+
(item) => item.cost.amountPerQuantity.amount
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const itemsToApplyDiscounts: ApplyDiscount[] = [];
|
|
82
|
+
|
|
83
|
+
eligibleGetItems.forEach((lineItem) => {
|
|
84
|
+
if (discountableQuantity == 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (lineItem.quantity <= discountableQuantity) {
|
|
89
|
+
itemsToApplyDiscounts.push({ lineItem } as ApplyDiscount);
|
|
90
|
+
discountableQuantity -= lineItem.quantity;
|
|
91
|
+
} else {
|
|
92
|
+
itemsToApplyDiscounts.push({
|
|
93
|
+
lineItem,
|
|
94
|
+
maxDiscounts: discountableQuantity,
|
|
95
|
+
} as ApplyDiscount);
|
|
96
|
+
discountableQuantity = 0;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (itemsToApplyDiscounts.length) {
|
|
101
|
+
const discount = this.discount.apply(itemsToApplyDiscounts);
|
|
102
|
+
discountCart.addDiscount(discount);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return discountCart;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AndSelector } from "./AndSelector";
|
|
2
|
+
import type { DiscountCart } from "./DiscountCart";
|
|
3
|
+
import type { OrSelector } from "./OrSelector";
|
|
4
|
+
import type { Selector } from "./Selector";
|
|
5
|
+
import { type Qualifier, QualifierBehavior } from "./Qualifier";
|
|
6
|
+
|
|
7
|
+
interface CampaignInterface {
|
|
8
|
+
runWithHooks(cart: DiscountCart): DiscountCart;
|
|
9
|
+
beforeRun?(cart: DiscountCart): DiscountCart;
|
|
10
|
+
run?(cart: DiscountCart): DiscountCart;
|
|
11
|
+
afterRun(cart: DiscountCart): DiscountCart;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Campaign implements CampaignInterface {
|
|
15
|
+
behavior: QualifierBehavior;
|
|
16
|
+
qualifiers: [AndSelector | OrSelector | Qualifier];
|
|
17
|
+
lineItemSelector?: Selector;
|
|
18
|
+
message?: string;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
behavior: QualifierBehavior,
|
|
22
|
+
qualifiers: [AndSelector | OrSelector | Qualifier]
|
|
23
|
+
) {
|
|
24
|
+
this.behavior = behavior;
|
|
25
|
+
this.qualifiers = qualifiers;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
qualifies(discountCart: DiscountCart): boolean {
|
|
29
|
+
// I have a feeling this needs to be revised... Check campaign.rb
|
|
30
|
+
const qualifierResults = this.qualifiers.map((qualifier) =>
|
|
31
|
+
qualifier.match(discountCart, this.lineItemSelector)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (this.behavior == QualifierBehavior.ALL) {
|
|
35
|
+
return qualifierResults.filter((i) => i === false).length == 0;
|
|
36
|
+
} else if (this.behavior == QualifierBehavior.ANY) {
|
|
37
|
+
return qualifierResults.filter((i) => i === true).length > 0;
|
|
38
|
+
} else {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
runWithHooks(cart: DiscountCart) {
|
|
44
|
+
this.run(cart);
|
|
45
|
+
return this.afterRun(cart);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
run(cart: DiscountCart) {
|
|
49
|
+
return cart;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
afterRun(cart: DiscountCart) {
|
|
53
|
+
return cart;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { OrSelector } from "./OrSelector";
|
|
2
|
+
import type { Campaign } from "./Campaign";
|
|
3
|
+
import type { DiscountInterface } from "./DiscountInterface";
|
|
4
|
+
import type {
|
|
5
|
+
NumericalComparisonType,
|
|
6
|
+
Qualifier,
|
|
7
|
+
QualifierBehavior,
|
|
8
|
+
QualifierMatchType,
|
|
9
|
+
StringComparisonType,
|
|
10
|
+
} from "./Qualifier";
|
|
11
|
+
import type { MatchType, Selector } from "./Selector";
|
|
12
|
+
import { ConditionalDiscount } from "../lineItem/ConditionalDiscount";
|
|
13
|
+
import { FixedItemDiscount } from "../lineItem/FixedItemDiscount";
|
|
14
|
+
import { PercentageDiscount } from "../lineItem/PercentageDiscount";
|
|
15
|
+
import { AndSelector } from "./AndSelector";
|
|
16
|
+
import { BuyXGetY } from "./BuyXGetY";
|
|
17
|
+
import {
|
|
18
|
+
type CartAmountBehavior,
|
|
19
|
+
CartAmountQualifier,
|
|
20
|
+
} from "./CartAmountQualifier";
|
|
21
|
+
import {
|
|
22
|
+
type CartQuantityBehavior,
|
|
23
|
+
CartQuantityQualifier,
|
|
24
|
+
} from "./CartQuantityQualifier";
|
|
25
|
+
import { CustomerEmailQualifier } from "./CustomerEmailQualifier";
|
|
26
|
+
import { CustomerSubscriberQualifier } from "./CustomerSubscriberQualifier";
|
|
27
|
+
import { CustomerTagQualifier } from "./CustomerTagQualifier";
|
|
28
|
+
import { PostCartAmountQualifier } from "./PostCartAmountQualifier";
|
|
29
|
+
import { ProductIdSelector } from "./ProductIdSelector";
|
|
30
|
+
import { ProductTagSelector } from "./ProductTagSelector";
|
|
31
|
+
import { ProductTypeSelector } from "./ProductTypeSelector";
|
|
32
|
+
import {
|
|
33
|
+
SaleItemSelector,
|
|
34
|
+
type SaleItemSelectorMatchType,
|
|
35
|
+
} from "./SaleItemSelector";
|
|
36
|
+
import { SubscriptionItemSelector } from "./SubscriptionItemSelector";
|
|
37
|
+
|
|
38
|
+
export type InputConfig = {
|
|
39
|
+
__type: string;
|
|
40
|
+
inputs: (
|
|
41
|
+
| boolean
|
|
42
|
+
| number
|
|
43
|
+
| string
|
|
44
|
+
| InputConfig
|
|
45
|
+
| boolean[]
|
|
46
|
+
| number[]
|
|
47
|
+
| string[]
|
|
48
|
+
| InputConfig[]
|
|
49
|
+
)[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export class CampaignFactory {
|
|
53
|
+
static createFromConfig(config: InputConfig) {
|
|
54
|
+
return CampaignFactory.createInputsObject(config) as Campaign;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static parseInputConfig(
|
|
58
|
+
input:
|
|
59
|
+
| boolean
|
|
60
|
+
| number
|
|
61
|
+
| string
|
|
62
|
+
| InputConfig
|
|
63
|
+
| boolean[]
|
|
64
|
+
| number[]
|
|
65
|
+
| string[]
|
|
66
|
+
| InputConfig[]
|
|
67
|
+
) {
|
|
68
|
+
if (
|
|
69
|
+
typeof input == "string" ||
|
|
70
|
+
typeof input == "number" ||
|
|
71
|
+
typeof input == "boolean" ||
|
|
72
|
+
input === null
|
|
73
|
+
) {
|
|
74
|
+
return input;
|
|
75
|
+
} else {
|
|
76
|
+
return CampaignFactory.createInputsObject(input as InputConfig);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static createInputsObject(
|
|
81
|
+
config: InputConfig
|
|
82
|
+
):
|
|
83
|
+
| Campaign
|
|
84
|
+
| Qualifier
|
|
85
|
+
| AndSelector
|
|
86
|
+
| OrSelector
|
|
87
|
+
| Selector
|
|
88
|
+
| DiscountInterface
|
|
89
|
+
| undefined {
|
|
90
|
+
const args = config.inputs.map((input) => {
|
|
91
|
+
if (Array.isArray(input)) {
|
|
92
|
+
return input.map((i) => CampaignFactory.parseInputConfig(i));
|
|
93
|
+
} else {
|
|
94
|
+
return CampaignFactory.parseInputConfig(input);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
switch (config.__type) {
|
|
99
|
+
case "ConditionalDiscount":
|
|
100
|
+
return new ConditionalDiscount(
|
|
101
|
+
args[0] as QualifierBehavior,
|
|
102
|
+
args[1] as [AndSelector | OrSelector | Qualifier],
|
|
103
|
+
args[2] as DiscountInterface,
|
|
104
|
+
args[3] as Selector,
|
|
105
|
+
args[4] as number
|
|
106
|
+
);
|
|
107
|
+
case "BuyXGetY":
|
|
108
|
+
return new BuyXGetY(
|
|
109
|
+
args[0] as QualifierBehavior,
|
|
110
|
+
args[1] as [AndSelector | OrSelector | Qualifier],
|
|
111
|
+
args[2] as DiscountInterface,
|
|
112
|
+
args[3] as Selector,
|
|
113
|
+
args[4] as Selector,
|
|
114
|
+
args[5] as number,
|
|
115
|
+
args[6] as number,
|
|
116
|
+
args[7] as number
|
|
117
|
+
);
|
|
118
|
+
case "AndSelector":
|
|
119
|
+
return new AndSelector([...args] as (Selector | Qualifier)[]);
|
|
120
|
+
case "OrSelector":
|
|
121
|
+
return new OrSelector([...args] as (Selector | Qualifier)[]);
|
|
122
|
+
case "PostCartAmountQualifier":
|
|
123
|
+
return new PostCartAmountQualifier(
|
|
124
|
+
args[0] as NumericalComparisonType,
|
|
125
|
+
args[1] as number
|
|
126
|
+
);
|
|
127
|
+
case "CartAmountQualifier":
|
|
128
|
+
return new CartAmountQualifier(
|
|
129
|
+
args[0] as CartAmountBehavior,
|
|
130
|
+
args[1] as NumericalComparisonType,
|
|
131
|
+
args[2] as number
|
|
132
|
+
);
|
|
133
|
+
case "CartQuantityQualifier":
|
|
134
|
+
return new CartQuantityQualifier(
|
|
135
|
+
args[0] as CartQuantityBehavior,
|
|
136
|
+
args[1] as NumericalComparisonType,
|
|
137
|
+
args[2] as number
|
|
138
|
+
);
|
|
139
|
+
case "SaleItemSelector":
|
|
140
|
+
return new SaleItemSelector(args[0] as SaleItemSelectorMatchType);
|
|
141
|
+
case "ProductIdSelector":
|
|
142
|
+
return new ProductIdSelector(args[0] as MatchType, args[1] as string[]);
|
|
143
|
+
case "ProductTypeSelector":
|
|
144
|
+
return new ProductTypeSelector(
|
|
145
|
+
args[0] as MatchType,
|
|
146
|
+
args[1] as string[]
|
|
147
|
+
);
|
|
148
|
+
case "ProductTagSelector":
|
|
149
|
+
return new ProductTagSelector(
|
|
150
|
+
args[0] as QualifierMatchType,
|
|
151
|
+
args[1] as StringComparisonType,
|
|
152
|
+
args[2] as string[]
|
|
153
|
+
);
|
|
154
|
+
case "PercentageDiscount":
|
|
155
|
+
return new PercentageDiscount(args[0] as number, args[1] as string);
|
|
156
|
+
case "FixedItemDiscount":
|
|
157
|
+
return new FixedItemDiscount(
|
|
158
|
+
args[0] as number,
|
|
159
|
+
args[1] as boolean,
|
|
160
|
+
args[2] as string
|
|
161
|
+
);
|
|
162
|
+
case "SubscriptionItemSelector":
|
|
163
|
+
return new SubscriptionItemSelector(args[0] as MatchType);
|
|
164
|
+
case "CustomerSubscriberQualifier":
|
|
165
|
+
return new CustomerSubscriberQualifier(args[0] as QualifierMatchType);
|
|
166
|
+
case "CustomerEmailQualifier":
|
|
167
|
+
return new CustomerEmailQualifier(
|
|
168
|
+
args[0] as QualifierMatchType,
|
|
169
|
+
args[1] as StringComparisonType,
|
|
170
|
+
args[2] as string[]
|
|
171
|
+
);
|
|
172
|
+
case "CustomerTagQualifier":
|
|
173
|
+
return new CustomerTagQualifier(
|
|
174
|
+
args[0] as QualifierMatchType,
|
|
175
|
+
args[1] as StringComparisonType,
|
|
176
|
+
args[2] as string[]
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DiscountCart } from "./DiscountCart";
|
|
2
|
+
import { type NumericalComparisonType, Qualifier } from "./Qualifier";
|
|
3
|
+
import { type Selector } from "./Selector";
|
|
4
|
+
|
|
5
|
+
export enum CartAmountBehavior {
|
|
6
|
+
CART = ":cart",
|
|
7
|
+
ITEM = ":item",
|
|
8
|
+
DIFF_CART = ":diff_cart",
|
|
9
|
+
DIFF_ITEM = ":diff_item",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class CartAmountQualifier extends Qualifier {
|
|
13
|
+
behavior: CartAmountBehavior;
|
|
14
|
+
comparisonType: NumericalComparisonType;
|
|
15
|
+
amount: number;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
behavior: CartAmountBehavior,
|
|
19
|
+
comparisonType: NumericalComparisonType,
|
|
20
|
+
amount: number
|
|
21
|
+
) {
|
|
22
|
+
super();
|
|
23
|
+
this.behavior = behavior;
|
|
24
|
+
this.comparisonType = comparisonType;
|
|
25
|
+
this.amount = amount;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Determines if the carts amount
|
|
30
|
+
* @param cart
|
|
31
|
+
* @param selector
|
|
32
|
+
*/
|
|
33
|
+
match(discountCart: DiscountCart, selector?: Selector) {
|
|
34
|
+
let total =
|
|
35
|
+
parseFloat(discountCart.cart.cost.subtotalAmount.amount) -
|
|
36
|
+
discountCart.appliedDiscountTotal;
|
|
37
|
+
|
|
38
|
+
switch (this.behavior) {
|
|
39
|
+
case CartAmountBehavior.CART:
|
|
40
|
+
case CartAmountBehavior.ITEM:
|
|
41
|
+
return this.compareAmounts(total, this.comparisonType, this.amount);
|
|
42
|
+
case CartAmountBehavior.DIFF_CART:
|
|
43
|
+
return this.compareAmounts(
|
|
44
|
+
parseFloat(discountCart.cart.cost.subtotalAmount.amount) -
|
|
45
|
+
this.amount,
|
|
46
|
+
this.comparisonType,
|
|
47
|
+
total
|
|
48
|
+
);
|
|
49
|
+
case CartAmountBehavior.DIFF_ITEM:
|
|
50
|
+
const originalLineTotal = discountCart.cart.lines.reduce(
|
|
51
|
+
(runningTotal: number, line) => {
|
|
52
|
+
runningTotal += parseFloat(line.cost.subtotalAmount.amount);
|
|
53
|
+
return runningTotal;
|
|
54
|
+
},
|
|
55
|
+
total
|
|
56
|
+
);
|
|
57
|
+
return this.compareAmounts(
|
|
58
|
+
originalLineTotal - this.amount,
|
|
59
|
+
this.comparisonType,
|
|
60
|
+
total
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CartLine } from "extensions/zox-product-discount/generated/api";
|
|
2
|
+
import type { DiscountCart } from "./DiscountCart";
|
|
3
|
+
import { type NumericalComparisonType, Qualifier } from "./Qualifier";
|
|
4
|
+
import { type Selector } from "./Selector";
|
|
5
|
+
|
|
6
|
+
export enum CartHasItemBehavior {
|
|
7
|
+
QUANTITY = ":quantity",
|
|
8
|
+
SUBTOTAL = ":subtotal",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class CartHasItemQualifier extends Qualifier {
|
|
12
|
+
quantityOrSubtotal: CartHasItemBehavior;
|
|
13
|
+
comparisonType: NumericalComparisonType;
|
|
14
|
+
amount: number;
|
|
15
|
+
itemSelector: Selector;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
quantityOrSubtotal: CartHasItemBehavior,
|
|
19
|
+
comparisonType: NumericalComparisonType,
|
|
20
|
+
amount: number,
|
|
21
|
+
itemSelector: Selector
|
|
22
|
+
) {
|
|
23
|
+
super();
|
|
24
|
+
this.quantityOrSubtotal = quantityOrSubtotal;
|
|
25
|
+
this.comparisonType = comparisonType;
|
|
26
|
+
this.amount =
|
|
27
|
+
this.quantityOrSubtotal == CartHasItemBehavior.SUBTOTAL
|
|
28
|
+
? amount
|
|
29
|
+
: Math.floor(amount);
|
|
30
|
+
this.itemSelector = itemSelector;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Determines if the carts amount
|
|
35
|
+
* @param cart
|
|
36
|
+
* @param selector
|
|
37
|
+
*/
|
|
38
|
+
match(discountCart: DiscountCart, selector?: Selector) {
|
|
39
|
+
let total = 0;
|
|
40
|
+
|
|
41
|
+
switch (this.quantityOrSubtotal) {
|
|
42
|
+
case CartHasItemBehavior.QUANTITY:
|
|
43
|
+
total = discountCart.cart.lines.reduce((quantity, line) => {
|
|
44
|
+
return (
|
|
45
|
+
quantity +
|
|
46
|
+
(this.itemSelector.match(line as CartLine) ? line.quantity : 0)
|
|
47
|
+
);
|
|
48
|
+
}, 0);
|
|
49
|
+
break;
|
|
50
|
+
case CartHasItemBehavior.SUBTOTAL:
|
|
51
|
+
total = discountCart.cart.lines.reduce((amount, line) => {
|
|
52
|
+
return (
|
|
53
|
+
amount +
|
|
54
|
+
(this.itemSelector.match(line as CartLine)
|
|
55
|
+
? line.cost.subtotalAmount.amount
|
|
56
|
+
: 0)
|
|
57
|
+
);
|
|
58
|
+
}, 0);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return this.compareAmounts(total, this.comparisonType, this.amount);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { CartLine } from "extensions/zox-product-discount/generated/api";
|
|
2
|
+
import type { DiscountCart } from "./DiscountCart";
|
|
3
|
+
import { type NumericalComparisonType, Qualifier } from "./Qualifier";
|
|
4
|
+
import { type Selector } from "./Selector";
|
|
5
|
+
|
|
6
|
+
export enum CartQuantityBehavior {
|
|
7
|
+
CART = ":cart",
|
|
8
|
+
ITEM = ":item",
|
|
9
|
+
LINE_ANY = ":line_any",
|
|
10
|
+
LINE_ALL = ":line_all",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class CartQuantityQualifier extends Qualifier {
|
|
14
|
+
totalMethod: CartQuantityBehavior;
|
|
15
|
+
comparisonType: NumericalComparisonType;
|
|
16
|
+
quantity: number;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
totalMethod: CartQuantityBehavior,
|
|
20
|
+
comparisonType: NumericalComparisonType,
|
|
21
|
+
quantity: number
|
|
22
|
+
) {
|
|
23
|
+
super();
|
|
24
|
+
this.totalMethod = totalMethod;
|
|
25
|
+
this.comparisonType = comparisonType;
|
|
26
|
+
this.quantity = quantity;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Determines if the carts amount
|
|
31
|
+
* @param cart
|
|
32
|
+
* @param selector
|
|
33
|
+
*/
|
|
34
|
+
match(discountCart: DiscountCart, selector?: Selector) {
|
|
35
|
+
let total = 0;
|
|
36
|
+
|
|
37
|
+
switch (this.totalMethod) {
|
|
38
|
+
case CartQuantityBehavior.ITEM:
|
|
39
|
+
total = discountCart.cart.lines.reduce((totalQty, line) => {
|
|
40
|
+
return (
|
|
41
|
+
totalQty +
|
|
42
|
+
((selector ? selector.match(line as CartLine) : true)
|
|
43
|
+
? line.quantity
|
|
44
|
+
: 0)
|
|
45
|
+
);
|
|
46
|
+
}, 0);
|
|
47
|
+
break;
|
|
48
|
+
case CartQuantityBehavior.CART:
|
|
49
|
+
total = discountCart.totalLineItemQuantityExcludingGifts();
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
this.totalMethod == CartQuantityBehavior.LINE_ANY ||
|
|
55
|
+
this.totalMethod == CartQuantityBehavior.LINE_ALL
|
|
56
|
+
) {
|
|
57
|
+
const qualifiedItems = discountCart.cart.lines
|
|
58
|
+
.filter((line) => (selector ? selector.match(line as CartLine) : true))
|
|
59
|
+
.map((line) =>
|
|
60
|
+
this.compareAmounts(line.quantity, this.comparisonType, this.quantity)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
switch (this.totalMethod) {
|
|
64
|
+
case CartQuantityBehavior.LINE_ANY:
|
|
65
|
+
return qualifiedItems.indexOf(true) > -1;
|
|
66
|
+
case CartQuantityBehavior.LINE_ALL:
|
|
67
|
+
return qualifiedItems.indexOf(false) == -1;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
return this.compareAmounts(total, this.comparisonType, this.quantity);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { DiscountCart } from "./DiscountCart";
|
|
2
|
+
import {
|
|
3
|
+
StringComparisonType,
|
|
4
|
+
QualifierMatchType,
|
|
5
|
+
Qualifier,
|
|
6
|
+
} from "./Qualifier";
|
|
7
|
+
import { type Selector } from "./Selector";
|
|
8
|
+
|
|
9
|
+
export class CustomerEmailQualifier extends Qualifier {
|
|
10
|
+
invert: boolean;
|
|
11
|
+
matchCondition: StringComparisonType;
|
|
12
|
+
emails: string[];
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
matchType: QualifierMatchType,
|
|
16
|
+
matchCondition: StringComparisonType,
|
|
17
|
+
emails: string[]
|
|
18
|
+
) {
|
|
19
|
+
super();
|
|
20
|
+
this.invert = matchType == QualifierMatchType.DOES_NOT;
|
|
21
|
+
this.matchCondition = matchCondition;
|
|
22
|
+
this.emails = emails.map((e) => e.toLowerCase());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Determines if the customer's email matches
|
|
27
|
+
* @param discountCart DiscountCart
|
|
28
|
+
* @param selector Selector
|
|
29
|
+
*/
|
|
30
|
+
match(discountCart: DiscountCart, selector?: Selector) {
|
|
31
|
+
if (!discountCart.cart.buyerIdentity?.customer?.email) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let matchResult = false;
|
|
36
|
+
|
|
37
|
+
const email = discountCart.cart.buyerIdentity.customer.email;
|
|
38
|
+
|
|
39
|
+
switch (this.matchCondition) {
|
|
40
|
+
case StringComparisonType.MATCH:
|
|
41
|
+
matchResult = this.emails.includes(email);
|
|
42
|
+
break;
|
|
43
|
+
case StringComparisonType.CONTAINS:
|
|
44
|
+
matchResult = this.emails.filter((e) => e.includes(email)).length > 0;
|
|
45
|
+
break;
|
|
46
|
+
case StringComparisonType.START_WITH:
|
|
47
|
+
matchResult = this.emails.filter((e) => e.startsWith(email)).length > 0;
|
|
48
|
+
break;
|
|
49
|
+
case StringComparisonType.END_WITH:
|
|
50
|
+
matchResult = this.emails.filter((e) => e.endsWith(email)).length > 0;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.invert) {
|
|
55
|
+
return !matchResult;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return matchResult;
|
|
59
|
+
}
|
|
60
|
+
}
|