@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.
- package/Logger.ts +18 -0
- package/README.md +258 -0
- package/addTypings.sh +2 -0
- package/bitbucket-pipelines.yml +38 -0
- package/index.ts +132 -0
- package/package.json +57 -0
- package/publish.sh +20 -0
- package/services/CacheWrapper.ts +30 -0
- package/services/CountryCodeService.ts +507 -0
- package/shopify/ShopifyAppService.ts +109 -0
- package/shopify/ShopifyAssetService.ts +20 -0
- package/shopify/ShopifyBillingService.ts +73 -0
- package/shopify/ShopifyCartTrasnformationService.ts +207 -0
- package/shopify/ShopifyCollectionService.ts +523 -0
- package/shopify/ShopifyCustomerService.ts +472 -0
- package/shopify/ShopifyDeliveryCustomisationService.ts +220 -0
- package/shopify/ShopifyDiscountService.ts +131 -0
- package/shopify/ShopifyDraftOrderService.ts +125 -0
- package/shopify/ShopifyFulfillmentService.ts +41 -0
- package/shopify/ShopifyFunctionsProductDiscountsService.ts +166 -0
- package/shopify/ShopifyInventoryService.ts +415 -0
- package/shopify/ShopifyLocationService.ts +29 -0
- package/shopify/ShopifyOrderRefundsService.ts +138 -0
- package/shopify/ShopifyOrderRiskService.ts +19 -0
- package/shopify/ShopifyOrderService.ts +1143 -0
- package/shopify/ShopifyPageService.ts +62 -0
- package/shopify/ShopifyProductService.ts +772 -0
- package/shopify/ShopifyShippingZonesService.ts +37 -0
- package/shopify/ShopifyShopService.ts +101 -0
- package/shopify/ShopifyTemplateService.ts +30 -0
- package/shopify/ShopifyThemeService.ts +33 -0
- package/shopify/ShopifyUtils.ts +56 -0
- package/shopify/ShopifyWebhookService.ts +110 -0
- package/shopify/base/APIVersion.ts +4 -0
- package/shopify/base/AbstractService.ts +152 -0
- package/shopify/base/ErrorHelper.ts +24 -0
- package/shopify/errors/InspiraShopifyCustomError.ts +7 -0
- package/shopify/errors/InspiraShopifyError.ts +15 -0
- package/shopify/errors/InspiraShopifyUnableToReserveInventoryError.ts +7 -0
- package/shopify/helpers/ShopifyProductServiceHelper.ts +450 -0
- package/shopify/product/ShopifyProductCountService.ts +110 -0
- package/shopify/product/ShopifyProductListService.ts +333 -0
- package/shopify/product/ShopifyProductMetafieldsService.ts +405 -0
- package/shopify/product/ShopifyProductPublicationsService.ts +112 -0
- package/shopify/product/ShopifyVariantService.ts +584 -0
- package/shopify/router/ShopifyMandatoryRouter.ts +37 -0
- package/shopify/router/ShopifyRouter.ts +85 -0
- package/shopify/router/ShopifyRouterBis.ts +85 -0
- package/shopify/router/ShopifyRouterBisBis.ts +85 -0
- package/shopify/router/ShopifyRouterBisBisBis.ts +85 -0
- package/shopify/router/ShopifyRouterBisBisBisBis.ts +85 -0
- package/shopify/router/WebhookSkipMiddleware.ts +73 -0
- package/shopify/router/services/CryptoService.ts +26 -0
- package/shopify/router/services/HmacValidator.ts +36 -0
- package/shopify/router/services/OauthService.ts +17 -0
- package/shopify/router/services/RestUtils.ts +13 -0
- package/shopify/router/services/rateLimiter/MemoryStores.ts +46 -0
- package/shopify/router/services/rateLimiter/StoreRateLimiter.ts +46 -0
- package/test/README.md +223 -0
- package/test/router/ShopifyRouter.test.ts +71 -0
- package/test/router/WebhookSkipMiddleware.test.ts +86 -0
- package/test/router/services/HmacValidator.test.ts +24 -0
- package/test/router/services/RestUtils.test.ts +13 -0
- package/test/router/services/rateLimiter/StoreRateLimiter.test.ts +62 -0
- package/test/services/CacheWrapper.test.ts +30 -0
- package/test/shopify/ShopifyOrderService.test.ts +29 -0
- package/test/shopify/ShopifyProductService.test.ts +118 -0
- package/test/shopify/ShopifyWebhookService.test.ts +105 -0
- package/tsconfig.json +10 -0
- package/typings/axios.d.ts +8 -0
- 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
|
+
});
|