@sommpicks/sommpicks-shopify 24.12.0

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 (71) hide show
  1. package/Logger.ts +18 -0
  2. package/README.md +258 -0
  3. package/addTypings.sh +2 -0
  4. package/bitbucket-pipelines.yml +38 -0
  5. package/index.ts +132 -0
  6. package/package.json +57 -0
  7. package/publish.sh +20 -0
  8. package/services/CacheWrapper.ts +30 -0
  9. package/services/CountryCodeService.ts +507 -0
  10. package/shopify/ShopifyAppService.ts +109 -0
  11. package/shopify/ShopifyAssetService.ts +20 -0
  12. package/shopify/ShopifyBillingService.ts +73 -0
  13. package/shopify/ShopifyCartTrasnformationService.ts +207 -0
  14. package/shopify/ShopifyCollectionService.ts +523 -0
  15. package/shopify/ShopifyCustomerService.ts +472 -0
  16. package/shopify/ShopifyDeliveryCustomisationService.ts +220 -0
  17. package/shopify/ShopifyDiscountService.ts +131 -0
  18. package/shopify/ShopifyDraftOrderService.ts +125 -0
  19. package/shopify/ShopifyFulfillmentService.ts +41 -0
  20. package/shopify/ShopifyFunctionsProductDiscountsService.ts +166 -0
  21. package/shopify/ShopifyInventoryService.ts +415 -0
  22. package/shopify/ShopifyLocationService.ts +29 -0
  23. package/shopify/ShopifyOrderRefundsService.ts +138 -0
  24. package/shopify/ShopifyOrderRiskService.ts +19 -0
  25. package/shopify/ShopifyOrderService.ts +1143 -0
  26. package/shopify/ShopifyPageService.ts +62 -0
  27. package/shopify/ShopifyProductService.ts +772 -0
  28. package/shopify/ShopifyShippingZonesService.ts +37 -0
  29. package/shopify/ShopifyShopService.ts +101 -0
  30. package/shopify/ShopifyTemplateService.ts +30 -0
  31. package/shopify/ShopifyThemeService.ts +33 -0
  32. package/shopify/ShopifyUtils.ts +56 -0
  33. package/shopify/ShopifyWebhookService.ts +110 -0
  34. package/shopify/base/APIVersion.ts +4 -0
  35. package/shopify/base/AbstractService.ts +152 -0
  36. package/shopify/base/ErrorHelper.ts +24 -0
  37. package/shopify/errors/InspiraShopifyCustomError.ts +7 -0
  38. package/shopify/errors/InspiraShopifyError.ts +15 -0
  39. package/shopify/errors/InspiraShopifyUnableToReserveInventoryError.ts +7 -0
  40. package/shopify/helpers/ShopifyProductServiceHelper.ts +450 -0
  41. package/shopify/product/ShopifyProductCountService.ts +110 -0
  42. package/shopify/product/ShopifyProductListService.ts +333 -0
  43. package/shopify/product/ShopifyProductMetafieldsService.ts +405 -0
  44. package/shopify/product/ShopifyProductPublicationsService.ts +112 -0
  45. package/shopify/product/ShopifyVariantService.ts +584 -0
  46. package/shopify/router/ShopifyMandatoryRouter.ts +37 -0
  47. package/shopify/router/ShopifyRouter.ts +85 -0
  48. package/shopify/router/ShopifyRouterBis.ts +85 -0
  49. package/shopify/router/ShopifyRouterBisBis.ts +85 -0
  50. package/shopify/router/ShopifyRouterBisBisBis.ts +85 -0
  51. package/shopify/router/ShopifyRouterBisBisBisBis.ts +85 -0
  52. package/shopify/router/WebhookSkipMiddleware.ts +73 -0
  53. package/shopify/router/services/CryptoService.ts +26 -0
  54. package/shopify/router/services/HmacValidator.ts +36 -0
  55. package/shopify/router/services/OauthService.ts +17 -0
  56. package/shopify/router/services/RestUtils.ts +13 -0
  57. package/shopify/router/services/rateLimiter/MemoryStores.ts +46 -0
  58. package/shopify/router/services/rateLimiter/StoreRateLimiter.ts +46 -0
  59. package/test/README.md +223 -0
  60. package/test/router/ShopifyRouter.test.ts +71 -0
  61. package/test/router/WebhookSkipMiddleware.test.ts +86 -0
  62. package/test/router/services/HmacValidator.test.ts +24 -0
  63. package/test/router/services/RestUtils.test.ts +13 -0
  64. package/test/router/services/rateLimiter/StoreRateLimiter.test.ts +62 -0
  65. package/test/services/CacheWrapper.test.ts +30 -0
  66. package/test/shopify/ShopifyOrderService.test.ts +29 -0
  67. package/test/shopify/ShopifyProductService.test.ts +118 -0
  68. package/test/shopify/ShopifyWebhookService.test.ts +105 -0
  69. package/tsconfig.json +10 -0
  70. package/typings/axios.d.ts +8 -0
  71. package/typings/index.d.ts +1682 -0
@@ -0,0 +1,46 @@
1
+ import { NextFunction, Request, Response } from 'express-serve-static-core';
2
+
3
+ import MemoryStore from './MemoryStores';
4
+
5
+ export default class StoreRateLimiter {
6
+
7
+ private windowMs: number = 60 * 100;
8
+ private max: number = 5;
9
+ private message: string = 'Too many requests, please try again later.';
10
+ private statusCode: number = 200;
11
+ private store: MemoryStore;
12
+
13
+
14
+ constructor(windowMs: number, maxRequests: number) {
15
+ this.windowMs = windowMs;
16
+ this.max = maxRequests;
17
+ this.store = new MemoryStore(this.windowMs);
18
+ }
19
+
20
+ private keyGenerator = (req: Request): string => {
21
+ return req.headers['x-shopify-shop-domain'] as string;
22
+ };
23
+
24
+ private handler = (req: Request, res: Response): void => {
25
+ res.status(this.statusCode).send(this.message);
26
+ };
27
+
28
+ public rateReqLimit = (req: Request, res: Response, next: NextFunction) => {
29
+
30
+ const key = this.keyGenerator(req);
31
+
32
+ this.store.incr(key, (err: any, current: number, resetTime: number) => {
33
+ if (err) {
34
+ return next(err);
35
+ }
36
+
37
+ (req as any).rateLimit = { limit: this.max, current: current, remaining: Math.max(this.max - current, 0), resetTime: resetTime };
38
+
39
+ if (this.max && current > this.max) {
40
+ return this.handler(req, res);
41
+ } else {
42
+ next();
43
+ }
44
+ });
45
+ };
46
+ }
package/test/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # inspira-shopify
2
+
3
+ Shopify Rest API calls utility
4
+
5
+ ## HOW TO USE
6
+
7
+ ```
8
+
9
+ npm install --save inspira-shopify
10
+ ```
11
+
12
+ ### Use shopify rest calls
13
+
14
+ ```js
15
+
16
+ import ShopifyRest from 'inspira-shopify';
17
+
18
+
19
+ const inspiraShopify = new ShopifyRest('<your shop>.myshopify.com','<shopify token>');
20
+
21
+ //Override default options
22
+ const inspiraShopify = new ShopifyRest('<your shop>.myshopify.com','<shopify token>', {timeout:4000, retries: 3, debug: true, logger: mylogger});
23
+
24
+ //Create a customer example
25
+ inspiraShopify.customer.create(
26
+ {email:'wakeeekm@gmail.com', verified_email: true, first_name:'hweweji', last_name:'eqwewwert', phone:'07470874486'})
27
+ .catch((err) => { console.log( err)} )
28
+ .then((result) => { console.log(result)});
29
+
30
+ ```
31
+
32
+ ### Use shopify express Router
33
+
34
+ Set shopify endpoints for installing your app.
35
+
36
+ ```js
37
+
38
+ import { Router } from 'inspira-shopify';
39
+
40
+ ...
41
+
42
+ const app = express();
43
+
44
+ ...
45
+
46
+ function callback(token, shop, response, err) {
47
+ //manage your shopify token and send response to the client
48
+ if(err) { console.error(err)}
49
+ response.sendFile( ... );
50
+ }
51
+
52
+ app.use('/shopify', new Router( 'secret', 'key', 'scopes', 'app base url', callback).buildRoutes());
53
+
54
+ ```
55
+
56
+ ### Use Hmac utilities.
57
+
58
+ ```js
59
+
60
+ import { HmacUtils } from 'inspira-shopify';
61
+
62
+ ...
63
+
64
+ router.post('/<someendpoint>', HmacUtils.jsonWebhookParser, async (req, res) => {
65
+ const isHmacOk = await HmacUtils.checkHmac(req, 'SHOPIFY_SECRET');
66
+ if(!isHmacOk) throw 'Hmac not correct';
67
+ ...
68
+ });
69
+
70
+ ...
71
+
72
+ ```
73
+
74
+ ### Cache Shopify Calls
75
+
76
+ ```js
77
+
78
+ import { RestCacheWrapper } from 'inspira-shopify';
79
+
80
+ ...
81
+ private static methodCached = new RestCacheWrapper<Response Type>(<Items to holds in cache>, <Shopify rest method to be cached>);
82
+
83
+ await methodCached.execute(...args);
84
+ ...
85
+
86
+ ```
87
+
88
+ ### SHOPIFY REST OPTIONS
89
+
90
+ - **timeout**(ms): timeout while perfoming rest call. Default is 3000.
91
+ - **retries**: How many times will the call tried. It will retry when it is a network error or a 5xx error.
92
+ - **debug**: When debug is set to true it will log debug data. You must specify a logger.
93
+ - **logger**: Logger object used for logging data.
94
+
95
+ And example of a logger can be:
96
+
97
+ ```js
98
+
99
+ //using console for building my logger
100
+ const mylogger = {error: console.error, info: console.log};
101
+
102
+ new ShopifyRest('<your shop>.myshopify.com','<shopify token>', {timeout:4000, retries: 3, debug: true, logger: mylogger})
103
+ ```
104
+ ## HOW TO PUBLISH
105
+
106
+ Push a change with a new version set in package.json and bitbucket pipeline will automatically publish a new npm package.
107
+
108
+ **NOTE:** To understand when the version number needs to change refer to [npm documentation](https://docs.npmjs.com/getting-started/semantic-versioning)
109
+
110
+ ## Functionalities
111
+
112
+ - Customers
113
+ - getInBatch
114
+ - getById
115
+ - getByEmail
116
+ - getByTag
117
+ - getByLastNameContains
118
+ - addAddress
119
+ - removeAddress
120
+ - updateAddress
121
+ - addressAsDefault
122
+ - replacesAddressAsDefault
123
+ - addAddressAsDefault
124
+ - create
125
+ - update
126
+ - get(numOfCustomers)
127
+ - getMetafields
128
+ - getMetafield
129
+ - postMetafield
130
+ - deleteMetafield
131
+ - sendInvite
132
+ - Inventory
133
+ - setItemToTrackable
134
+ - setQuantity
135
+ - getLevel
136
+ - getInventoryItemsOfProduct
137
+ - adjustQuantityOfInventoryItem
138
+ - adjustQuantity
139
+ - setItemCostAndSkuToBe
140
+ - setItemCostToBe
141
+ - getInventoryLevelsOfProduct
142
+ - Draft Order
143
+ - create
144
+ - createAndSendInvoice
145
+ - updateShippingAddress
146
+ - createFromCart
147
+ - fromCartToDraft
148
+ - Order
149
+ - getFulfillments
150
+ - createFulfillments
151
+ - completeFulfillment
152
+ - openFulfillment
153
+ - cancelFulfillment
154
+ - fulfillAllItems
155
+ - getById
156
+ - addAdditionalAttribute
157
+ - setTags
158
+ - getAll
159
+ - getInBatch
160
+ - create
161
+ - applyWeightAndCountryBasedShippingToOrder
162
+ - getShippingByWeight
163
+ - updateShippingAddress
164
+ - getAvailableShippingRatesByAddress
165
+ - getAvailableShippingRatesByCustomerDefaultAddress
166
+ - applyShippingToOrder
167
+ - duplicate
168
+ - OrderRisks
169
+ - getFromOrderById
170
+ - OrderRefunds
171
+ - getOrderRefunds
172
+ - createRefunds
173
+ - Webhook
174
+ - deleteAll
175
+ - delete (By ID)
176
+ - create
177
+ - getAll
178
+ - deleteByTopic
179
+ - updateWebhooksAPI
180
+ - Fulfillment Service
181
+ - create
182
+ - getAll
183
+ - addTrackingInfoToFulfillment
184
+ - Shop
185
+ - ShopDetails
186
+ - getShippingRates
187
+ - Collections
188
+ - create
189
+ - createCollect
190
+ - getAll (limit 250)
191
+ - delete
192
+ - removeCollects (With TimerQueue)
193
+ - Themes
194
+ - getAll (limit 250)
195
+ - getMainTheme (returns null if no theme has been found)
196
+ - Template
197
+ - get
198
+ - put
199
+ - Assets
200
+ - put
201
+ - Locations
202
+ - getAll
203
+ - Pages
204
+ - post
205
+ - put
206
+ - postMetafields
207
+ - Discounts
208
+ - getPercentageDiscountCodeForItem
209
+ - getFixedDiscountCodeForOrder
210
+ - removeDiscount
211
+ - Billing
212
+ - cancelBilling
213
+ - requestBilling
214
+ - getRecurringApplicationCharge
215
+ - getApplicationCharge
216
+ - singlePayment
217
+ - addUsage
218
+ - Shopify express route needed for installing the App.
219
+ - Shopify Hmac validation utilities.
220
+ - Shopify Rest Utils.
221
+ - Cache functionality.
222
+
223
+
@@ -0,0 +1,71 @@
1
+ import ShopifyRouter from '../../shopify/router/ShopifyRouter';
2
+ import { OauthService } from '../../shopify/router/services/OauthService';
3
+ const getScopes = () => { return 'read_orders'; };
4
+
5
+ test('install route returns 400 if no shop has been specified', () => {
6
+ const callback = jest.fn();
7
+ const shopifyRouter = new ShopifyRouter('secret', 'key', 'appBaseUrl', '', getScopes, callback);
8
+
9
+ const req = { query: { shop: '' } };
10
+ const res = { status: jest.fn(() => { return { send: res.send }; }), send: jest.fn(), locals: {} };
11
+
12
+ shopifyRouter.installRoute(req, res);
13
+
14
+ expect(res.status).toBeCalledWith(400);
15
+ });
16
+
17
+ test('install route returns redirects to oauth url when shop is set', () => {
18
+ const callback = jest.fn();
19
+ const shopifyRouter = new ShopifyRouter('secret', 'key', 'appBaseUrl', '', getScopes, callback);
20
+
21
+ const req = { query: { shop: 'myshop.shopify.com', host: 'host', session: 'some session' } };
22
+ const res = { redirect: jest.fn(), cookie: jest.fn(), render: jest.fn(), locals: { scopes: 'read_orders'} };
23
+
24
+ shopifyRouter.installRoute(req, res);
25
+
26
+ expect(res.redirect).toBeCalledWith(expect.stringContaining('appBaseUrl/billing/created?host=host&shop=myshop.shopify.com&status=already_accepted'));
27
+ });
28
+
29
+ test('callback route returns 400 when shop hmac or code are missing as query params', () => {
30
+ const callback = jest.fn();
31
+ const shopifyRouter = new ShopifyRouter('secret', 'key', 'appBaseUrl', '', getScopes, callback);
32
+
33
+ const req = { query: { }, headers: { } };
34
+ const res = { status: jest.fn(() => { return { send: res.send }; }), send: jest.fn() };
35
+
36
+ shopifyRouter.callBackRoute(req, res);
37
+
38
+ expect(res.status).toBeCalledWith(400);
39
+ });
40
+
41
+ test('callback route get oauth token when everything is ok', () => {
42
+ const callback = jest.fn();
43
+ const shopifyRouter = new ShopifyRouter('secret', 'key', 'appBaseUrl', '', getScopes, callback);
44
+
45
+ OauthService.getOathToken = jest.fn();
46
+ (shopifyRouter as any).cryptoService.checkHash = jest.fn(() => true);
47
+ (shopifyRouter as any).cryptoService.generateHash = jest.fn();
48
+
49
+ const req = { query: { hmac: 'somehmac', code: 'somecode', shop: 'myshop.shopify.com' }, headers: { } };
50
+ const res = { status: jest.fn(() => { return { send: res.send }; }), send: jest.fn() };
51
+
52
+ shopifyRouter.callBackRoute(req, res);
53
+
54
+ expect(OauthService.getOathToken).toBeCalled();
55
+ });
56
+
57
+ test('callback route returns 401 when hmac is not correct', () => {
58
+ const callback = jest.fn();
59
+ const shopifyRouter = new ShopifyRouter( 'secret', 'key', 'appBaseUrl', '', getScopes, callback);
60
+
61
+ OauthService.getOathToken = jest.fn();
62
+ (shopifyRouter as any).cryptoService.checkHash = jest.fn(() => false);
63
+ (shopifyRouter as any).cryptoService.generateHash = jest.fn();
64
+
65
+ const req = { query: { hmac: 'somehmac', code: 'somecode', shop: 'myshop.shopify.com' }, headers: { } };
66
+ const res = { status: jest.fn(() => { return { send: res.send }; }), send: jest.fn() };
67
+
68
+ shopifyRouter.callBackRoute(req, res);
69
+
70
+ expect(res.status).toBeCalledWith(401);
71
+ });
@@ -0,0 +1,86 @@
1
+ import WebhookSkipMiddleware from '../../shopify/router/WebhookSkipMiddleware';
2
+ import { Request } from 'express-serve-static-core';
3
+
4
+ describe('WebhookSkipMiddleware for product should, ', () => {
5
+ test('Not skip to process when product id is not in the list', () => {
6
+ const status = jest.fn(() => {});
7
+ const next = jest.fn();
8
+
9
+ WebhookSkipMiddleware.addProductId(1234);
10
+ WebhookSkipMiddleware.productSkipMiddleware( { body: { id: 2345} } as Request, { sendStatus: status } as any, next );
11
+
12
+ expect(status).not.toHaveBeenCalled();
13
+ expect(next).toHaveBeenCalled();
14
+ });
15
+
16
+ test('skip to process when product id is in the list', () => {
17
+ const status = jest.fn(() => {});
18
+ const next = jest.fn();
19
+
20
+ WebhookSkipMiddleware.addProductId(1234);
21
+ WebhookSkipMiddleware.addProductId(1224);
22
+ WebhookSkipMiddleware.productSkipMiddleware({ body: { id: 1234} } as Request, { sendStatus: status } as any, next );
23
+
24
+ expect(status).toHaveBeenCalledWith(200);
25
+ expect(next).not.toHaveBeenCalled();
26
+ });
27
+
28
+ test('remove product id from the list after x time', async () => {
29
+ const status = jest.fn(() => {});
30
+ const next = jest.fn();
31
+ WebhookSkipMiddleware.clearProductList();
32
+ WebhookSkipMiddleware.awaitTime = 1000;
33
+ WebhookSkipMiddleware.addProductId(1234);
34
+ WebhookSkipMiddleware.addProductId(1224);
35
+ await sleep(2000);
36
+ WebhookSkipMiddleware.productSkipMiddleware({ body: { id: 1234 } } as Request, { sendStatus: status } as any, next );
37
+
38
+ expect(status).not.toHaveBeenCalled();
39
+ expect(next).toHaveBeenCalled();
40
+ });
41
+ });
42
+
43
+ describe('WebhookSkipMiddleware for inventory should, ', () => {
44
+ test('Not skip to process when product id is not in the list', () => {
45
+ const status = jest.fn(() => {});
46
+ const next = jest.fn();
47
+
48
+ WebhookSkipMiddleware.addInventoryId(1234);
49
+ WebhookSkipMiddleware.inventorySkipMiddleware( { body: { id: 2345} } as Request, { sendStatus: status } as any, next );
50
+
51
+ expect(status).not.toHaveBeenCalled();
52
+ expect(next).toHaveBeenCalled();
53
+ });
54
+
55
+ test('skip to process when product id is in the list', () => {
56
+ const status = jest.fn(() => {});
57
+ const next = jest.fn();
58
+
59
+ WebhookSkipMiddleware.addInventoryId(1234);
60
+ WebhookSkipMiddleware.addInventoryId(1224);
61
+ WebhookSkipMiddleware.inventorySkipMiddleware({ body: { id: 1234} } as Request, { sendStatus: status } as any, next );
62
+
63
+ expect(status).toHaveBeenCalledWith(200);
64
+ expect(next).not.toHaveBeenCalled();
65
+ });
66
+
67
+ test('remove product id from the list after x time', async () => {
68
+ const status = jest.fn(() => {});
69
+ const next = jest.fn();
70
+ WebhookSkipMiddleware.clearInventoryList();
71
+ WebhookSkipMiddleware.awaitTime = 1000;
72
+ WebhookSkipMiddleware.addInventoryId(1234);
73
+ WebhookSkipMiddleware.addInventoryId(1224);
74
+ await sleep(2000);
75
+ WebhookSkipMiddleware.inventorySkipMiddleware({ body: { id: 1234 } } as Request, { sendStatus: status } as any, next );
76
+
77
+ expect(status).not.toHaveBeenCalled();
78
+ expect(next).toHaveBeenCalled();
79
+ });
80
+ });
81
+
82
+ const sleep = (time: number): Promise<void> => {
83
+ return new Promise((resolve) => {
84
+ setTimeout(() => { resolve(); }, time);
85
+ });
86
+ };
@@ -0,0 +1,24 @@
1
+ import HmacValidator from '../../../shopify/router/services/HmacValidator';
2
+ import { CryptoService } from '../../../shopify/router/services/CryptoService';
3
+
4
+ test('getHmac should get shopify Hmac header', () => {
5
+ const req: any = { headers: { 'x-shopify-hmac-sha256': 'hmac' } };
6
+ const hmac = HmacValidator.getHmac(req);
7
+ expect(hmac).toBe('hmac');
8
+ });
9
+
10
+ test('checkHmac resolve to true when hmac is correct', () => {
11
+ const crypto = new CryptoService();
12
+ const hmac_generated = crypto.generateHashBase64('someRaw=someRawValue', 'signature');
13
+ const req: any = { headers: { 'x-shopify-hmac-sha256': hmac_generated }, rawBody: 'someRaw=someRawValue'};
14
+
15
+ const hmac = HmacValidator.checkHmac(req, 'signature');
16
+
17
+ expect(hmac).toBeTruthy();
18
+ });
19
+
20
+ test('checkHmac resolve to false when hmac is correct', () => {
21
+ const req:any = { headers: { 'x-shopify-hmac-sha256': 'wrong_hmac_generated' }, rawBody: 'someRaw=someRawValue'};
22
+ const hmac = HmacValidator.checkHmac(req, 'signature');
23
+ expect(hmac).toBeTruthy();
24
+ });
@@ -0,0 +1,13 @@
1
+ import RestUtils from '../../../shopify/router/services/RestUtils';
2
+
3
+ test('getShopFromRequestHeaders throws error when not shop exist in header', () => {
4
+ expect(RestUtils.getShopFromRequestHeaders.bind({headers: {'http-random-header': 'some randomness'}})).toThrow(Error);
5
+ });
6
+
7
+ test('getShopFromRequestHeaders throws error when shop is empty in header', () => {
8
+ expect(RestUtils.getShopFromRequestHeaders.bind({headers: {'x-shopify-shop-domain': ''}})).toThrow(Error);
9
+ });
10
+
11
+ test('getShopFromRequestHeaders returns shop existing in header', () => {
12
+ expect(RestUtils.getShopFromRequestHeaders({headers: {'x-shopify-shop-domain': 'myshop.myshopify.com'}})).toBe('myshop.myshopify.com');
13
+ });
@@ -0,0 +1,62 @@
1
+ import StoreRateLimiter from '../../../../shopify/router/services/rateLimiter/StoreRateLimiter';
2
+
3
+ const next = jest.fn();
4
+
5
+ afterEach(() => {
6
+ next.mockClear();
7
+ });
8
+
9
+ test('Adds a hit for the store and calls next', (done) => {
10
+ const storeLimiter = new StoreRateLimiter(1000, 1);
11
+ const req: any = { headers: {'x-shopify-shop-domain': 'test.myshopify.com'} };
12
+ const res: any = {};
13
+ storeLimiter.rateReqLimit(req, res, next);
14
+ setTimeout(()=> {
15
+ expect((storeLimiter as any).store.hits['test.myshopify.com']).toBe(1);
16
+ expect(next).toHaveBeenCalledTimes(1);
17
+ done();
18
+ }, 200);
19
+ });
20
+
21
+ test('Does resturn response when too many requests', (done) => {
22
+ const storeLimiter = new StoreRateLimiter(1000, 1);
23
+ const req: any = { headers: {'x-shopify-shop-domain': 'test.myshopify.com'} };
24
+ const send = jest.fn();
25
+ const res: any = { status: jest.fn(() => { return { send: send};})};
26
+ storeLimiter.rateReqLimit(req, res, next);
27
+ storeLimiter.rateReqLimit(req, res, next);
28
+ setTimeout(()=> {
29
+ expect((storeLimiter as any).store.hits['test.myshopify.com']).toBe(2);
30
+ expect(next).toHaveBeenCalledTimes(1);
31
+ expect(send).toHaveBeenCalledWith('Too many requests, please try again later.');
32
+ done();
33
+ }, 200);
34
+ });
35
+
36
+ test('Does reset all hits after window time', (done) => {
37
+ const storeLimiter = new StoreRateLimiter(1000, 1);
38
+ const req: any = { headers: {'x-shopify-shop-domain': 'test.myshopify.com'} };
39
+ const req1: any = { headers: {'x-shopify-shop-domain': 'test4.myshopify.com'} };
40
+ const res: any = { };
41
+ storeLimiter.rateReqLimit(req, res, next);
42
+ storeLimiter.rateReqLimit(req1, res, next);
43
+ setTimeout(()=> {
44
+ expect((storeLimiter as any).store.hits['test.myshopify.com']).toBe(1);
45
+ expect((storeLimiter as any).store.hits['test4.myshopify.com']).toBe(1);
46
+ done();
47
+ }, 200);
48
+ });
49
+
50
+ test('Does adds hits for different stores', (done) => {
51
+ const storeLimiter = new StoreRateLimiter(1000, 1);
52
+ const req: any = { headers: {'x-shopify-shop-domain': 'test.myshopify.com'} };
53
+ const req1: any = { headers: {'x-shopify-shop-domain': 'test4.myshopify.com'} };
54
+ const res: any = { };
55
+ storeLimiter.rateReqLimit(req, res, next);
56
+ storeLimiter.rateReqLimit(req1, res, next);
57
+ setTimeout(()=> {
58
+ expect((storeLimiter as any).store.hits['test.myshopify.com']).toBeUndefined();
59
+ expect((storeLimiter as any).store.hits['test4.myshopify.com']).toBeUndefined();
60
+ done();
61
+ }, 1100);
62
+ });
@@ -0,0 +1,30 @@
1
+ import CacheWrapper from '../../services/CacheWrapper';
2
+
3
+ interface IResponse {
4
+ test: string;
5
+ }
6
+
7
+ const cacheableTestFunction = jest.fn((id: string): string => {
8
+ return id + 'value';
9
+ });
10
+
11
+ beforeEach(()=> {
12
+ cacheableTestFunction.mockClear();
13
+ });
14
+
15
+ test('gets from cache when key exist', async () => {
16
+ const wrapper = new CacheWrapper<IResponse>(5, cacheableTestFunction);
17
+ await wrapper.execute('key_1');
18
+ await wrapper.execute('key_2');
19
+ await wrapper.execute('key_1');
20
+ expect(cacheableTestFunction).toHaveBeenCalledTimes(2);
21
+ });
22
+
23
+ test('clears cache when there are more entries than configured size', async () => {
24
+ const wrapper = new CacheWrapper<IResponse>(2, cacheableTestFunction);
25
+ await wrapper.execute('key_1');
26
+ await wrapper.execute('key_2');
27
+ await wrapper.execute('key_1');
28
+ expect((wrapper as any).records.size).toBe(2);
29
+ expect(cacheableTestFunction).toHaveBeenCalledTimes(2);
30
+ });
@@ -0,0 +1,29 @@
1
+ import { ShopifyUtils } from '../../shopify/ShopifyUtils';
2
+ import { ShopifyOrderService } from '../../shopify/ShopifyOrderService';
3
+
4
+
5
+ test('Substract Shipping tax lines form Global tax lines', async () => {
6
+ const mockAxiosInstance = {};
7
+
8
+ ShopifyUtils.getAxiosInstance = (): any => mockAxiosInstance;
9
+
10
+ const shopifyOrderService = new ShopifyOrderService(ShopifyUtils.getAxiosInstance('shop', 'token', 'key', null, null));
11
+ const taxLines: ITaxLine[] = (shopifyOrderService as any).substractShippingFromGlobalTaxLines([{ price: '2.3', title: 'Ohio State Tax'}, { price: '0.61', title: 'Licking County Tax'}], [{ price: '1.72', title: 'Ohio State Tax'}, { price: '0.45', title: 'Licking County Tax'}]);
12
+
13
+ expect(taxLines[0].price).toBe('0.58');
14
+ expect(taxLines[1].price).toBe('0.16');
15
+ });
16
+
17
+ test('createLineItemForOrderCalculation Should return line items', async () => {
18
+ const mockAxiosInstance = {};
19
+
20
+ ShopifyUtils.getAxiosInstance = (): any => mockAxiosInstance;
21
+
22
+ const shopifyOrderService = new ShopifyOrderService(ShopifyUtils.getAxiosInstance('shop', 'token', 'key', null, null));
23
+ const order = {'id':3833316147382,'admin_graphql_api_id':'gid://shopify/Order/3833316147382','app_id':1354745,'browser_ip':null,'buyer_accepts_marketing':false,'cancel_reason':null,'cancelled_at':null,'cart_token':null,'checkout_id':null,'checkout_token':null,'closed_at':null,'confirmed':true,'contact_email':'mike@alvio.online','created_at':'2021-06-15T10:30:43+01:00','currency':'USD','current_subtotal_price':'17.99','current_subtotal_price_set':{'shop_money':{'amount':'17.99','currency_code':'USD'},'presentment_money':{'amount':'17.99','currency_code':'USD'}},'current_total_discounts':'0.00','current_total_discounts_set':{'shop_money':{'amount':'0.00','currency_code':'USD'},'presentment_money':{'amount':'0.00','currency_code':'USD'}},'current_total_duties_set':null,'current_total_price':'23.97','current_total_price_set':{'shop_money':{'amount':'23.97','currency_code':'USD'},'presentment_money':{'amount':'23.97','currency_code':'USD'}},'current_total_tax':'1.08','current_total_tax_set':{'shop_money':{'amount':'1.08','currency_code':'USD'},'presentment_money':{'amount':'1.08','currency_code':'USD'}},'customer_locale':'en','device_id':null,'discount_codes':[],'email':'mike@alvio.online','financial_status':'paid','fulfillment_status':null,'gateway':'manual','landing_site':null,'landing_site_ref':null,'location_id':null,'name':'#1011','note':null,'note_attributes':[{'name':'Social CBD Infused Patch - 100mg to be fulfilled by','value':'Alvio USA Brand Store Demo, Order: #1014'}],'number':11,'order_number':1011,'order_status_url':'https://alvio-usa-store-multilocation-test.myshopify.com/57626886326/orders/ff2077653e24fa96f8ea8ad4ad348c87/authenticate?key=a0c6ccab6bcf12b8414cd07f51832bce','original_total_duties_set':null,'payment_gateway_names':['manual'],'phone':null,'presentment_currency':'USD','processed_at':'2021-06-15T10:30:43+01:00','processing_method':'manual','reference':null,'referring_site':null,'source_identifier':null,'source_name':'shopify_draft_order','source_url':null,'subtotal_price':'17.99','subtotal_price_set':{'shop_money':{'amount':'17.99','currency_code':'USD'},'presentment_money':{'amount':'17.99','currency_code':'USD'}},'tags':'Alvio USA Brand Store Demo, Supplier','tax_lines':[{'price':'1.08','rate':0.06,'title':'Idaho State Tax','price_set':{'shop_money':{'amount':'1.08','currency_code':'USD'},'presentment_money':{'amount':'1.08','currency_code':'USD'}}}],'taxes_included':false,'test':false,'token':'ff2077653e24fa96f8ea8ad4ad348c87','total_discounts':'0.00','total_discounts_set':{'shop_money':{'amount':'0.00','currency_code':'USD'},'presentment_money':{'amount':'0.00','currency_code':'USD'}},'total_line_items_price':'17.99','total_line_items_price_set':{'shop_money':{'amount':'17.99','currency_code':'USD'},'presentment_money':{'amount':'17.99','currency_code':'USD'}},'total_outstanding':'0.00','total_price':'23.97','total_price_set':{'shop_money':{'amount':'23.97','currency_code':'USD'},'presentment_money':{'amount':'23.97','currency_code':'USD'}},'total_price_usd':'23.97','total_shipping_price_set':{'shop_money':{'amount':'4.90','currency_code':'USD'},'presentment_money':{'amount':'4.90','currency_code':'USD'}},'total_tax':'1.08','total_tax_set':{'shop_money':{'amount':'1.08','currency_code':'USD'},'presentment_money':{'amount':'1.08','currency_code':'USD'}},'total_tip_received':'0.00','total_weight':21,'updated_at':'2021-06-15T10:30:56+01:00','user_id':74547429558,'billing_address':{'first_name':'mike','address1':'new test 2','phone':null,'city':'Idaho City','zip':'83631','province':'Idaho','country':'United States','last_name':'harding','address2':'','company':null,'latitude':43.9409512,'longitude':-115.776099,'name':'mike harding','country_code':'US','province_code':'ID'},'customer':{'id':5269776826550,'email':'mike@alvio.online','accepts_marketing':false,'created_at':'2021-06-11T11:56:37+01:00','updated_at':'2021-06-15T10:30:44+01:00','first_name':'Idaho ','last_name':'Oklahoma ','orders_count':6,'state':'disabled','total_spent':'143.16','last_order_id':3833316147382,'note':null,'verified_email':true,'multipass_identifier':null,'tax_exempt':false,'phone':null,'tags':'','last_order_name':'#1011','currency':'USD','accepts_marketing_updated_at':'2021-06-11T11:56:37+01:00','marketing_opt_in_level':null,'admin_graphql_api_id':'gid://shopify/Customer/5269776826550','default_address':{'id':6652095430838,'customer_id':5269776826550,'first_name':'mike','last_name':'harding','company':null,'address1':'new test 2','address2':'','city':'Idaho City','province':'Idaho','country':'United States','zip':'83631','phone':null,'name':'mike harding','province_code':'ID','country_code':'US','country_name':'United States','default':true}},'discount_applications':[],'fulfillments':[],'line_items':[{'id':9974946562230,'admin_graphql_api_id':'gid://shopify/LineItem/9974946562230','fulfillable_quantity':1,'fulfillment_service':'manual','fulfillment_status':null,'gift_card':false,'grams':22,'name':'Social CBD Infused Patch - 100mg','price':'17.99','price_set':{'shop_money':{'amount':'17.99','currency_code':'USD'},'presentment_money':{'amount':'17.99','currency_code':'USD'}},'product_exists':true,'product_id':6788510318774,'properties':[],'quantity':1,'requires_shipping':true,'sku':'SCL-PTCH-100','taxable':true,'title':'Social CBD Infused Patch - 100mg','total_discount':'0.00','total_discount_set':{'shop_money':{'amount':'0.00','currency_code':'USD'},'presentment_money':{'amount':'0.00','currency_code':'USD'}},'variant_id':39934048993462,'variant_inventory_management':null,'variant_title':null,'vendor':'Green Lanes','tax_lines':[{'price':'1.08','price_set':{'shop_money':{'amount':'1.08','currency_code':'USD'},'presentment_money':{'amount':'1.08','currency_code':'USD'}},'rate':0.06,'title':'Idaho State Tax'}],'duties':[],'discount_allocations':[]}],'refunds':[],'shipping_address':{'first_name':'mike','address1':'new test 2','phone':null,'city':'Idaho City','zip':'83631','province':'Idaho','country':'United States','last_name':'harding','address2':'','company':null,'latitude':43.9409512,'longitude':-115.776099,'name':'mike harding','country_code':'US','province_code':'ID'},'shipping_lines':[{'id':3260381233334,'carrier_identifier':null,'code':'Economy','delivery_category':null,'discounted_price':'4.90','discounted_price_set':{'shop_money':{'amount':'4.90','currency_code':'USD'},'presentment_money':{'amount':'4.90','currency_code':'USD'}},'phone':null,'price':'4.90','price_set':{'shop_money':{'amount':'4.90','currency_code':'USD'},'presentment_money':{'amount':'4.90','currency_code':'USD'}},'requested_fulfillment_service_id':null,'source':'shopify','title':'Economy','tax_lines':[],'discount_allocations':[]}]};
24
+ const lineItems = shopifyOrderService.createLineItemForOrderCalculation(order.line_items, 'price');
25
+
26
+ expect(lineItems[0].originalUnitPrice).toBe('17.99');
27
+ expect(lineItems[0].title).toBe('Social CBD Infused Patch - 100mg');
28
+ expect(lineItems[0].quantity).toBe(1);
29
+ });