@zoxllc/shopify-checkout-extensions 0.0.2 → 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 +38 -22
- package/src/common/CartQuantityQualifier.ts +41 -20
- 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 -54
- package/src/common/DiscountInterface.ts +15 -3
- 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 +19 -10
- package/src/common/ProductTypeSelector.ts +10 -4
- package/src/common/SaleItemSelector.ts +2 -1
- package/src/common/Selector.ts +26 -13
- package/src/common/SubscriptionItemSelector.ts +5 -3
- package/src/index.ts +1 -1
- package/src/{common → lineItem}/BuyXGetY.ts +42 -20
- 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,11 +1,16 @@
|
|
|
1
|
-
import { Campaign } from
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import type {
|
|
1
|
+
import { Campaign } from '../common/Campaign';
|
|
2
|
+
import type { AndSelector } from '../common/AndSelector';
|
|
3
|
+
import type { DiscountCart } from '../common/DiscountCart';
|
|
4
|
+
import type {
|
|
5
|
+
ApplyDiscount,
|
|
6
|
+
DiscountInterface,
|
|
7
|
+
} from '../common/DiscountInterface';
|
|
8
|
+
import type { OrSelector } from '../common/OrSelector';
|
|
9
|
+
import type {
|
|
10
|
+
Qualifier,
|
|
11
|
+
QualifierBehavior,
|
|
12
|
+
} from '../common/Qualifier';
|
|
13
|
+
import type { Selector } from '../common/Selector';
|
|
9
14
|
|
|
10
15
|
export class BuyXGetY extends Campaign {
|
|
11
16
|
lineItemSelector?: Selector;
|
|
@@ -39,14 +44,19 @@ export class BuyXGetY extends Campaign {
|
|
|
39
44
|
// Find the items that qualify for buyX
|
|
40
45
|
let applicableBuyItems = discountCart.cart.lines;
|
|
41
46
|
if (this.lineItemSelector) {
|
|
42
|
-
applicableBuyItems = discountCart.cart.lines.filter(
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
applicableBuyItems = discountCart.cart.lines.filter(
|
|
48
|
+
(lineItem) => {
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
return this.lineItemSelector?.match(lineItem);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
if (
|
|
48
|
-
applicableBuyItems.reduce(
|
|
49
|
-
|
|
56
|
+
applicableBuyItems.reduce(
|
|
57
|
+
(qty, item) => (qty += item.quantity),
|
|
58
|
+
0
|
|
59
|
+
) < this.buyX
|
|
50
60
|
) {
|
|
51
61
|
return discountCart;
|
|
52
62
|
}
|
|
@@ -54,9 +64,12 @@ export class BuyXGetY extends Campaign {
|
|
|
54
64
|
// Find the items that qualify for getX
|
|
55
65
|
let eligibleGetItems = discountCart.cart.lines;
|
|
56
66
|
if (this.getItemSelector) {
|
|
57
|
-
eligibleGetItems = discountCart.cart.lines.filter(
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
eligibleGetItems = discountCart.cart.lines.filter(
|
|
68
|
+
(lineItem) => {
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
return this.getItemSelector?.match(lineItem);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
// Check if cart qualifies for discounts and limit the discount sets
|
|
@@ -65,13 +78,18 @@ export class BuyXGetY extends Campaign {
|
|
|
65
78
|
0
|
|
66
79
|
);
|
|
67
80
|
const discountableSets = this.maxSets
|
|
68
|
-
? [
|
|
81
|
+
? [
|
|
82
|
+
purchasedQuantity / this.buyX,
|
|
83
|
+
this.maxSets,
|
|
84
|
+
].sort((a, b) => a - b)[0]
|
|
69
85
|
: Math.floor(purchasedQuantity / this.buyX);
|
|
70
86
|
|
|
71
87
|
if (discountableSets > 0) {
|
|
72
88
|
let discountableQuantity =
|
|
73
89
|
discountableSets *
|
|
74
|
-
(this.getY
|
|
90
|
+
(this.getY
|
|
91
|
+
? this.getY
|
|
92
|
+
: discountCart.totalLineItemQuantity());
|
|
75
93
|
|
|
76
94
|
// Apply the discounts (sort to discount lower priced items first)
|
|
77
95
|
eligibleGetItems = eligibleGetItems.sort(
|
|
@@ -86,7 +104,9 @@ export class BuyXGetY extends Campaign {
|
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
if (lineItem.quantity <= discountableQuantity) {
|
|
89
|
-
itemsToApplyDiscounts.push({
|
|
107
|
+
itemsToApplyDiscounts.push({
|
|
108
|
+
lineItem,
|
|
109
|
+
} as ApplyDiscount);
|
|
90
110
|
discountableQuantity -= lineItem.quantity;
|
|
91
111
|
} else {
|
|
92
112
|
itemsToApplyDiscounts.push({
|
|
@@ -98,7 +118,9 @@ export class BuyXGetY extends Campaign {
|
|
|
98
118
|
});
|
|
99
119
|
|
|
100
120
|
if (itemsToApplyDiscounts.length) {
|
|
101
|
-
const discount = this.discount.apply(
|
|
121
|
+
const discount = this.discount.apply(
|
|
122
|
+
itemsToApplyDiscounts
|
|
123
|
+
);
|
|
102
124
|
discountCart.addDiscount(discount);
|
|
103
125
|
}
|
|
104
126
|
}
|
|
@@ -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
|
+
}
|