@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.
Files changed (41) hide show
  1. package/README.md +3 -0
  2. package/package.json +31 -0
  3. package/src/common/AndSelector.ts +35 -0
  4. package/src/common/BuyXGetY.ts +109 -0
  5. package/src/common/Campaign.ts +55 -0
  6. package/src/common/CampaignConfiguration.ts +10 -0
  7. package/src/common/CampaignFactory.ts +180 -0
  8. package/src/common/CartAmountQualifier.ts +64 -0
  9. package/src/common/CartHasItemQualifier.ts +64 -0
  10. package/src/common/CartQuantityQualifier.ts +73 -0
  11. package/src/common/CustomerEmailQualifier.ts +60 -0
  12. package/src/common/CustomerSubscriberQualifier.ts +32 -0
  13. package/src/common/CustomerTagQualifier.ts +56 -0
  14. package/src/common/DiscountCart.ts +130 -0
  15. package/src/common/DiscountInterface.ts +13 -0
  16. package/src/common/OrSelector.ts +35 -0
  17. package/src/common/PostCartAmountQualifier.ts +21 -0
  18. package/src/common/ProductHandleSelector.ts +29 -0
  19. package/src/common/ProductIdSelector.ts +28 -0
  20. package/src/common/ProductTagSelector.ts +54 -0
  21. package/src/common/ProductTypeSelector.ts +34 -0
  22. package/src/common/Qualifier.ts +67 -0
  23. package/src/common/SaleItemSelector.ts +35 -0
  24. package/src/common/Selector.ts +53 -0
  25. package/src/common/SubscriptionItemSelector.ts +21 -0
  26. package/src/generated/api.ts +2103 -0
  27. package/src/index.ts +39 -0
  28. package/src/lineItem/ConditionalDiscount.ts +102 -0
  29. package/src/lineItem/DiscountCodeList.ts +91 -0
  30. package/src/lineItem/FixedItemDiscount.ts +53 -0
  31. package/src/lineItem/PercentageDiscount.ts +46 -0
  32. package/tests/AndSelector.test.ts +27 -0
  33. package/tests/CartQuantityQualifier.test.ts +381 -0
  34. package/tests/CustomerSubscriberQualifier.test.ts +101 -0
  35. package/tests/DiscountCart.test.ts +115 -0
  36. package/tests/OrSelector.test.ts +27 -0
  37. package/tests/ProductTagSelector.test.ts +75 -0
  38. package/tests/Qualifier.test.ts +193 -0
  39. package/tests/SaleItemSelector.test.ts +113 -0
  40. package/tests/Selector.test.ts +83 -0
  41. package/tsconfig.json +25 -0
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ export { ConditionalDiscount } from './lineItem/ConditionalDiscount';
2
+ export { DiscountCodeList } from './lineItem/DiscountCodeList';
3
+ export { FixedItemDiscount } from './lineItem/FixedItemDiscount';
4
+ export { PercentageDiscount } from './lineItem/PercentageDiscount';
5
+ export { AndSelector } from './common/AndSelector';
6
+ export { BuyXGetY } from './common/BuyXGetY';
7
+ export { Campaign } from './common/Campaign';
8
+ export type {
9
+ CampaignType,
10
+ CampaignConfiguration,
11
+ } from './common/CampaignConfiguration';
12
+ export {
13
+ CampaignFactory,
14
+ type InputConfig,
15
+ } from './common/CampaignFactory';
16
+ export { CartAmountQualifier } from './common/CartAmountQualifier';
17
+ export { CartHasItemQualifier } from './common/CartHasItemQualifier';
18
+ export { CartQuantityQualifier } from './common/CartQuantityQualifier';
19
+ export { CustomerEmailQualifier } from './common/CustomerEmailQualifier';
20
+ export { CustomerSubscriberQualifier } from './common/CustomerSubscriberQualifier';
21
+ export { CustomerTagQualifier } from './common/CustomerTagQualifier';
22
+ export { DiscountCart } from './common/DiscountCart';
23
+ export type { DiscountInterface } from './common/DiscountInterface';
24
+ export { OrSelector } from './common/OrSelector';
25
+ export { PostCartAmountQualifier } from './common/PostCartAmountQualifier';
26
+ export { ProductHandleSelector } from './common/ProductHandleSelector';
27
+ export { ProductIdSelector } from './common/ProductIdSelector';
28
+ export { ProductTagSelector } from './common/ProductTagSelector';
29
+ export { ProductTypeSelector } from './common/ProductTypeSelector';
30
+ export {
31
+ Qualifier,
32
+ QualifierBehavior,
33
+ QualifierMatchType,
34
+ NumericalComparisonType,
35
+ StringComparisonType,
36
+ } from './common/Qualifier';
37
+ export { SaleItemSelector } from './common/SaleItemSelector';
38
+ export { Selector } from './common/Selector';
39
+ export { SubscriptionItemSelector } from './common/SubscriptionItemSelector';
@@ -0,0 +1,102 @@
1
+ import type { CartLine } from '../generated/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 ConditionalDiscount extends Campaign {
17
+ discount: DiscountInterface;
18
+ itemsToDiscount?: number;
19
+
20
+ constructor(
21
+ behavior: QualifierBehavior,
22
+ qualifiers: [Qualifier | AndSelector | OrSelector],
23
+ discount: DiscountInterface,
24
+ lineItemSelector?: Selector,
25
+ maxDiscounts?: number
26
+ ) {
27
+ super(behavior, qualifiers);
28
+ this.discount = discount;
29
+ this.lineItemSelector = lineItemSelector;
30
+ this.itemsToDiscount =
31
+ maxDiscounts == 0 ? undefined : maxDiscounts;
32
+ }
33
+
34
+ run(discountCart: DiscountCart) {
35
+ // First we see if this cart qualifies for this discount
36
+ if (this.qualifies(discountCart)) {
37
+ // Now that it qualifies, we can determine the applicableLineItems
38
+ // to apply discounts to
39
+
40
+ // figure out applicable lineItems...
41
+ const applicableLineItems = discountCart.cart.lines
42
+ .filter((lineItem) => {
43
+ if (this.lineItemSelector === undefined) {
44
+ return true;
45
+ }
46
+ return this.lineItemSelector.match(
47
+ lineItem as CartLine
48
+ );
49
+ })
50
+ .sort((a, b) => {
51
+ const priceA = a.cost.amountPerQuantity
52
+ .amount as number;
53
+ const priceB = b.cost.amountPerQuantity
54
+ .amount as number;
55
+
56
+ if (priceA < priceB) return -1;
57
+ if (priceA == priceB) return 0;
58
+ return 1;
59
+ });
60
+
61
+ const itemsToApplyDiscounts: ApplyDiscount[] = [];
62
+
63
+ // Loop over the applicable line items and deduct the itemsToDiscount count
64
+ // as they are applied to not exceed a maximum.
65
+ applicableLineItems.forEach((lineItem) => {
66
+ // If there are no more items to discount, return/skip
67
+ if (this.itemsToDiscount === 0) return;
68
+
69
+ // If items to discount is not undefined, apply as many as possible
70
+ if (this.itemsToDiscount !== undefined) {
71
+ // There are more than enough qty to apply, apply the maximum
72
+ if (lineItem.quantity >= this.itemsToDiscount) {
73
+ itemsToApplyDiscounts.push({
74
+ lineItem,
75
+ maxDiscounts: this.itemsToDiscount,
76
+ } as ApplyDiscount);
77
+ this.itemsToDiscount = 0;
78
+ } else {
79
+ itemsToApplyDiscounts.push({
80
+ lineItem,
81
+ } as ApplyDiscount);
82
+ this.itemsToDiscount -= lineItem.quantity;
83
+ }
84
+ } else {
85
+ // items to discount is undefined, so apply to entire item count
86
+ itemsToApplyDiscounts.push({
87
+ lineItem,
88
+ } as ApplyDiscount);
89
+ }
90
+
91
+ if (itemsToApplyDiscounts.length) {
92
+ // append the discount to the discountCart singleton
93
+ const discount = this.discount.apply(
94
+ itemsToApplyDiscounts
95
+ );
96
+ discountCart.addDiscount(discount);
97
+ }
98
+ });
99
+ }
100
+ return discountCart;
101
+ }
102
+ }
@@ -0,0 +1,91 @@
1
+ import type { DiscountCart } from '../common/DiscountCart';
2
+ import type {
3
+ QualifierBehavior,
4
+ Qualifier,
5
+ } from '../common/Qualifier';
6
+ import type { Selector } from '../common/Selector';
7
+ import { Campaign } from '../common/Campaign';
8
+ import type { CartLine, Discount } from '../generated/api';
9
+ import { AndSelector } from '~/common/AndSelector';
10
+ import { OrSelector } from '~/common/OrSelector';
11
+
12
+ export class DiscountCodeList extends Campaign {
13
+ discountList: string[];
14
+
15
+ constructor(
16
+ behavior: QualifierBehavior,
17
+ qualifiers: [Qualifier | AndSelector | OrSelector],
18
+ lineItemSelector?: Selector,
19
+ discountList?: string[]
20
+ ) {
21
+ super(behavior, qualifiers);
22
+ this.lineItemSelector = lineItemSelector;
23
+ this.discountList = discountList ? discountList : [];
24
+ }
25
+
26
+ run(discountCart: DiscountCart) {
27
+ // First we see if this cart qualifies for this discount
28
+ if (this.qualifies(discountCart)) {
29
+ // Now that it qualifies, we can determine the applicableLineItems
30
+ // to apply discounts to
31
+
32
+ // figure out applicable lineItems...
33
+ const applicableLineItems = discountCart.cart.lines
34
+ .filter((lineItem) => {
35
+ if (this.lineItemSelector === undefined) {
36
+ return true;
37
+ }
38
+ return this.lineItemSelector.match(
39
+ lineItem as CartLine
40
+ );
41
+ })
42
+ .sort((a, b) => {
43
+ const priceA = a.cost.amountPerQuantity
44
+ .amount as number;
45
+ const priceB = b.cost.amountPerQuantity
46
+ .amount as number;
47
+
48
+ if (priceA < priceB) return -1;
49
+ if (priceA == priceB) return 0;
50
+ return 1;
51
+ });
52
+
53
+ // Loop over the applicable line items and deduct the itemsToDiscount count
54
+ // as they are applied to not exceed a maximum.
55
+ applicableLineItems.forEach((lineItem) => {
56
+ // If there are no more items to discount, return/skip
57
+ if (this.itemsToDiscount === 0) return;
58
+
59
+ let discount: Discount;
60
+
61
+ // If items to discount is not undefined, apply as many as possible
62
+ if (this.itemsToDiscount !== undefined) {
63
+ // There are more than enough qty to apply, apply the maximum
64
+ if (lineItem.quantity >= this.itemsToDiscount) {
65
+ discount = this.discount.apply(
66
+ lineItem as CartLine,
67
+ this.itemsToDiscount
68
+ );
69
+ this.itemsToDiscount = 0;
70
+ } else {
71
+ discount = this.discount.apply(
72
+ lineItem as CartLine
73
+ );
74
+ this.itemsToDiscount -= lineItem.quantity;
75
+ }
76
+ } else {
77
+ // items to discount is undefined, so apply to entire item count
78
+ discount = this.discount.apply(
79
+ lineItem as CartLine
80
+ );
81
+ }
82
+
83
+ if (discount) {
84
+ // append the discount to the discountCart singleton
85
+ discountCart.addDiscount(discount);
86
+ }
87
+ });
88
+ }
89
+ return discountCart;
90
+ }
91
+ }
@@ -0,0 +1,53 @@
1
+ import type {
2
+ Discount,
3
+ ProductVariant,
4
+ } from '../generated/api';
5
+ import type {
6
+ ApplyDiscount,
7
+ DiscountInterface,
8
+ } from '../common/DiscountInterface';
9
+
10
+ export class FixedItemDiscount
11
+ implements DiscountInterface
12
+ {
13
+ amount: number;
14
+ appliesToEachItem: boolean;
15
+ message: string;
16
+
17
+ constructor(
18
+ amount: number,
19
+ appliesToEachItem: boolean,
20
+ message: string
21
+ ) {
22
+ this.amount = amount;
23
+ this.appliesToEachItem = appliesToEachItem;
24
+ this.message = message;
25
+ }
26
+
27
+ apply(items: ApplyDiscount[]): Discount {
28
+ const targets = items.map(
29
+ ({ lineItem, maxDiscounts }) => {
30
+ const variant =
31
+ lineItem.merchandise as ProductVariant;
32
+ return {
33
+ productVariant: {
34
+ id: variant.id,
35
+ quantity:
36
+ maxDiscounts == 0 ? undefined : maxDiscounts,
37
+ },
38
+ };
39
+ }
40
+ );
41
+
42
+ return {
43
+ targets,
44
+ value: {
45
+ fixedAmount: {
46
+ amount: this.amount,
47
+ appliesToEachItem: this.appliesToEachItem,
48
+ },
49
+ },
50
+ message: this.message,
51
+ } as Discount;
52
+ }
53
+ }
@@ -0,0 +1,46 @@
1
+ import type {
2
+ Discount,
3
+ ProductVariant,
4
+ } from '../generated/api';
5
+ import type {
6
+ ApplyDiscount,
7
+ DiscountInterface,
8
+ } from '../common/DiscountInterface';
9
+
10
+ export class PercentageDiscount
11
+ implements DiscountInterface
12
+ {
13
+ discount: number;
14
+ message?: string;
15
+
16
+ constructor(percentage: number, message?: string) {
17
+ this.discount = percentage;
18
+ this.message = message;
19
+ }
20
+
21
+ apply(items: ApplyDiscount[]): Discount {
22
+ const targets = items.map(
23
+ ({ lineItem, maxDiscounts }) => {
24
+ const variant =
25
+ lineItem.merchandise as ProductVariant;
26
+ return {
27
+ productVariant: {
28
+ id: variant.id,
29
+ quantity:
30
+ maxDiscounts == 0 ? undefined : maxDiscounts,
31
+ },
32
+ };
33
+ }
34
+ );
35
+
36
+ return {
37
+ targets,
38
+ value: {
39
+ percentage: {
40
+ value: this.discount,
41
+ },
42
+ },
43
+ message: this.message,
44
+ };
45
+ }
46
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { AndSelector } from '../src/common/AndSelector';
3
+ import { CartLine } from '../src/generated/api';
4
+ import {
5
+ MockFalseyQualifier,
6
+ MockTruthyQualifier,
7
+ } from './Qualifier.test';
8
+
9
+ describe('AndSelector', () => {
10
+ it('Returns true if all conditions are met', () => {
11
+ const selector = new AndSelector([
12
+ new MockTruthyQualifier(),
13
+ new MockTruthyQualifier(),
14
+ ]);
15
+
16
+ expect(selector.match({} as CartLine)).toBeTruthy();
17
+ });
18
+
19
+ it('Returns false if any conditions are not met', () => {
20
+ const selector = new AndSelector([
21
+ new MockTruthyQualifier(),
22
+ new MockFalseyQualifier(),
23
+ ]);
24
+
25
+ expect(selector.match({} as CartLine)).toBeFalsy();
26
+ });
27
+ });