@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,166 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { print } from 'graphql';
3
+ import gql from 'graphql-tag';
4
+ import { Logger } from '../Logger';
5
+ import { AbstractService } from './base/AbstractService';
6
+ import ErrorHelper from './base/ErrorHelper';
7
+ import InspiraShopifyError from './errors/InspiraShopifyError';
8
+ import * as moment from 'moment';
9
+
10
+ export class ShopifyFunctionsProductDiscountsService extends AbstractService {
11
+
12
+ constructor(private axiosInstance: AxiosInstance) {
13
+ super();
14
+ }
15
+
16
+ /**
17
+ * Creates Product Discount Customization
18
+ *
19
+ * @param {string} functionId
20
+ * @param {string} metafieldVal
21
+ * @returns Promise
22
+ */
23
+ public create = async (functionId: string, title: string, metafieldVal: string): Promise<{id: number;}> => {
24
+ return new Promise<{id: number;}>(async (resolve, reject) => {
25
+ try {
26
+ Logger.info(`Creating Product Discount transformation: ${functionId} and metafield val ${metafieldVal}`);
27
+ const query = gql `mutation {
28
+ discountAutomaticAppCreate(automaticAppDiscount: {
29
+ title: "${title}",
30
+ functionId: "${functionId}"
31
+ combinesWith: {
32
+ orderDiscounts: true
33
+ productDiscounts: true
34
+ shippingDiscounts: true
35
+ }
36
+ startsAt: "${moment().format('YYYY-MM-DDTHH:mm:ss')}"
37
+ }) {
38
+ automaticAppDiscount {
39
+ discountId
40
+ }
41
+ userErrors {
42
+ field
43
+ message
44
+ }
45
+ }
46
+ }`;
47
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query),
48
+ variables: { input: functionId }}, { query_cost: 803 });
49
+
50
+ if(response && response.data && response.data.data && response.data.data.discountAutomaticAppCreate && response.data.data.discountAutomaticAppCreate.automaticAppDiscount ){
51
+ const productDiscountGraphQL = response.data.data.discountAutomaticAppCreate.automaticAppDiscount;
52
+ Logger.info(`Product discount created ${JSON.stringify(productDiscountGraphQL)}`);
53
+
54
+ const metafieldQuery = gql `mutation MetafieldsSet($customizationId: ID!, $configurationValue: String!) {
55
+ metafieldsSet(metafields: [
56
+ {
57
+ ownerId: $customizationId
58
+ namespace: "$app:product-discount"
59
+ key: "function-configuration"
60
+ value: $configurationValue
61
+ type: "json"
62
+ }
63
+ ]) {
64
+ metafields {
65
+ id
66
+ }
67
+ userErrors {
68
+ message
69
+ }
70
+ }
71
+ }`;
72
+
73
+ const metafieldResponse = await this.axiosInstance.post('/graphql.json', { query: print(metafieldQuery),
74
+ variables: { customizationId: productDiscountGraphQL.discountId, configurationValue: metafieldVal }}, { query_cost: 803 });
75
+
76
+ Logger.info(`Product discount metafield Meta response ${JSON.stringify(metafieldResponse.data)}`);
77
+ resolve({ id: this.getIdFromGraphId(productDiscountGraphQL.discountId) });
78
+ } else {
79
+ Logger.error(`Product Discount. Error is ${JSON.stringify(response.data.errors)}. All data is ${JSON.stringify(response.data)}` );
80
+ resolve(null);
81
+ }
82
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
83
+ });
84
+ };
85
+
86
+ /**
87
+ * Updates in Product Discount
88
+ *
89
+ * @param {number} customisationId
90
+ * @param {string} metafieldVal
91
+ * @returns Promise
92
+ */
93
+ public update = async (customisationId: number, metafieldVal: string): Promise<{id: number;}> => {
94
+ return new Promise<{id: number;}>(async (resolve, reject) => {
95
+ try {
96
+
97
+ Logger.info(`Product Discount update with ${customisationId}`);
98
+ const metafieldQuery = gql `mutation MetafieldsSet($customizationId: ID!, $configurationValue: String!) {
99
+ metafieldsSet(metafields: [
100
+ {
101
+ ownerId: $customizationId
102
+ namespace: "$app:product-discount"
103
+ key: "function-configuration"
104
+ value: $configurationValue
105
+ type: "json"
106
+ }
107
+ ]) {
108
+ metafields {
109
+ id
110
+ }
111
+ userErrors {
112
+ message
113
+ }
114
+ }
115
+ }`;
116
+
117
+ const metafieldResponse = await this.axiosInstance.post('/graphql.json', { query: print(metafieldQuery),
118
+ variables: { customizationId: this.getGraphProductDiscountIdFromId(customisationId), configurationValue: metafieldVal }}, { query_cost: 803 });
119
+
120
+ Logger.info(`Meta response ${JSON.stringify(metafieldResponse.data)}`);
121
+ resolve({ id: customisationId });
122
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
123
+ });
124
+ };
125
+
126
+ /**
127
+ * setMetafield in Product discount
128
+ *
129
+ * @param {number} customisationId
130
+ * @param {string} metafieldVal
131
+ * @param {string} key
132
+ * @returns Promise
133
+ */
134
+ public setMetafield = async (customisationId: number, metafieldVal: string, key: string): Promise<{id: number;}> => {
135
+ return new Promise<{id: number;}>(async (resolve, reject) => {
136
+ try {
137
+
138
+ Logger.info(`Product Discount meta updated with ${customisationId}`);
139
+ const metafieldQuery = gql `mutation MetafieldsSet($customizationId: ID!, $configurationValue: String!) {
140
+ metafieldsSet(metafields: [
141
+ {
142
+ ownerId: $customizationId
143
+ namespace: "$app:product-discount"
144
+ key: "${key}"
145
+ value: $configurationValue
146
+ type: "json"
147
+ }
148
+ ]) {
149
+ metafields {
150
+ id
151
+ }
152
+ userErrors {
153
+ message
154
+ }
155
+ }
156
+ }`;
157
+
158
+ const metafieldResponse = await this.axiosInstance.post('/graphql.json', { query: print(metafieldQuery),
159
+ variables: { customizationId: this.getGraphProductDiscountIdFromId(customisationId), configurationValue: metafieldVal }}, { query_cost: 803 });
160
+
161
+ Logger.info(`Set metafield on Product discount response ${JSON.stringify(metafieldResponse.data)}`);
162
+ resolve({ id: customisationId });
163
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
164
+ });
165
+ };
166
+ }
@@ -0,0 +1,415 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { Logger } from '../Logger';
3
+ import ErrorHelper from './base/ErrorHelper';
4
+ import { print } from 'graphql';
5
+ import gql from 'graphql-tag';
6
+ import InspiraShopifyError from './errors/InspiraShopifyError';
7
+ import { AbstractService } from './base/AbstractService';
8
+
9
+ export class ShopifyInventoryService extends AbstractService {
10
+
11
+ constructor(private axiosInstance: AxiosInstance) { super(); }
12
+
13
+ /**
14
+ *
15
+ * @param {number} inventoryId
16
+ * @param {boolean} trackable. Inventory quantity is traqcked by shopify
17
+ * @returns Promise
18
+ */
19
+ public setItemToBe = async (inventoryId: number, trackable: boolean): Promise<IInventoryItem> => {
20
+ return new Promise<IInventoryItem>(async (resolve, reject) => {
21
+ try {
22
+ Logger.info(`setItemToTrackable ${inventoryId}`);
23
+ const response = await this.axiosInstance.put(`/inventory_items/${inventoryId}.json`, { inventory_item: { id: inventoryId, tracked: trackable } });
24
+ resolve(response.data.inventory_item);
25
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
26
+ });
27
+ };
28
+
29
+ /**
30
+ * Updates Inventory cost per item.
31
+ *
32
+ * @param {number} inventoryId
33
+ * @param {number} cost. New cost per item
34
+ * @returns Promise true if updated correctly
35
+ */
36
+ public setItemCostToBe = async (inventoryId: number, cost: number): Promise<boolean> => {
37
+ const updatedInventoryItem = await this.setMultipleItemCostToBe([{ inventoryId: inventoryId, cost: cost }]);
38
+ return updatedInventoryItem?.length === 1 ? true : false;
39
+ };
40
+
41
+ /**
42
+ * Updates Inventory cost for multiple Inventory items
43
+ *
44
+ * @param {IIventoryItemCostChangeRequest[]} inventoryCostChanges
45
+ * @returns Promise inventory item IDs updated
46
+ */
47
+ public setMultipleItemCostToBe = async (inventoryCostChanges: IIventoryItemCostChangeRequest[]): Promise<number[]> => {
48
+ return new Promise<number[]>(async (resolve, reject) => {
49
+ try {
50
+ Logger.info(`Change Multiple Inventory costs: ${JSON.stringify(inventoryCostChanges)}`);
51
+
52
+ const inventoryCostChangeschunks: IIventoryItemCostChangeRequest[][] = this.sliceIntoChunks(inventoryCostChanges, 50);
53
+
54
+ if(inventoryCostChangeschunks && inventoryCostChangeschunks.length > 0) {
55
+ Logger.info(`Created ${inventoryCostChangeschunks.length} chunks`);
56
+
57
+ for(const inventoryCostChangeschunk of inventoryCostChangeschunks) {
58
+ const query = gql`mutation {
59
+ ${ inventoryCostChangeschunk.map((iv) => (`
60
+ InventoryItem${iv.inventoryId}: inventoryItemUpdate(id: "gid://shopify/InventoryItem/${iv.inventoryId}", input: { cost: ${iv.cost} }) {
61
+ inventoryItem {
62
+ id
63
+ }
64
+ userErrors {
65
+ field
66
+ message
67
+ }
68
+ }`)).join('')
69
+ }
70
+ }`;
71
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: inventoryCostChangeschunk.length * 10 });
72
+ if(response && response.data?.data){
73
+ Logger.info(`Inventory chunks completed ${JSON.stringify(response.data.data)}`);
74
+ const keys = Object.keys(response.data.data);
75
+ const itemIDsUpdated: number[] = [];
76
+ for(const key of keys) {
77
+ const graphQLItemID = response.data.data[key].inventoryItem?.id;
78
+ if(graphQLItemID){
79
+ itemIDsUpdated.push(this.getIdFromGraphId(graphQLItemID));
80
+ }
81
+ }
82
+ resolve(itemIDsUpdated);
83
+ } else {
84
+ Logger.error(`Inventory chunks failed ${JSON.stringify(response ? response.data : '')}`);
85
+ resolve([]);
86
+ }
87
+ }
88
+ } else {
89
+ Logger.error(`No inventory changes sent! ${JSON.stringify(inventoryCostChanges)}`);
90
+ resolve([]);
91
+ }
92
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
93
+ });
94
+ };
95
+
96
+ /**
97
+ * Updates Inventory cost & sku per item.
98
+ *
99
+ * @param {number} inventoryId
100
+ * @param {number} cost. New cost per item
101
+ * @param {string} sku. New sku per item
102
+ * @returns Promise
103
+ */
104
+ public setItemCostAndSkuToBe = async (inventoryId: number, cost: number, sku: string): Promise<IInventoryItem> => {
105
+ return new Promise<IInventoryItem>(async (resolve, reject) => {
106
+ try {
107
+ Logger.info(`setItemCostAndSkuToBe ${inventoryId}`);
108
+ const response = await this.axiosInstance.put(`/inventory_items/${inventoryId}.json`, { inventory_item: { id: inventoryId, cost: cost, sku: sku } });
109
+ resolve(response.data.inventory_item);
110
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
111
+ });
112
+ };
113
+
114
+ /**
115
+ * Sets a quantity to the inventory level
116
+ *
117
+ * @param {number} inventoryId
118
+ * @param {number} qty Absolute amount of Qty
119
+ * @param {number} locationId
120
+ * @returns Promise
121
+ */
122
+ public setQuantity = async (inventoryId: number, qty: number, locationId: number): Promise<IInventoryLevel> => {
123
+ return new Promise<IInventoryLevel>(async (resolve, reject) => {
124
+ try {
125
+ Logger.info(`setQuantity ${qty} in inventory ID ${inventoryId}`);
126
+ const response = await this.axiosInstance.post('/inventory_levels/set.json', { location_id: locationId, inventory_item_id: inventoryId, available: qty });
127
+ resolve(response.data.inventory_level);
128
+ } catch (error) {Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
129
+ });
130
+ };
131
+
132
+ /**
133
+ * Adjusts quantity to the inventory level. So it adds or removes the quantity from the actual value
134
+ *
135
+ * @param {number} inventoryId
136
+ * @param {number} qty Absolute amount of Qty
137
+ * @param {number} locationId
138
+ * @returns Promise
139
+ */
140
+ public adjustQuantity = async (inventoryId: number, qtyToAdjust: number, locationId: number): Promise<IInventoryLevel> => {
141
+ return new Promise<IInventoryLevel>(async (resolve, reject) => {
142
+ try {
143
+ Logger.info(`adjustsQuantity ${qtyToAdjust} in inventory ID ${inventoryId}`);
144
+ const response = await this.axiosInstance.post('/inventory_levels/adjust.json', { location_id: locationId, inventory_item_id: inventoryId, available_adjustment: qtyToAdjust });
145
+ resolve(response.data.inventory_level);
146
+ } catch (error) {Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
147
+ });
148
+ };
149
+
150
+ /**
151
+ * Adjusts quantity to the inventory levels of variant. So it adds or removes the quantity from the actual value.
152
+ * - If quantity is lower than the inventory level. It gets removed from it.
153
+ * - If quantity is bigger from the inventory level but there are more than one level it tries to remove a bit of stock from each level.
154
+ * - If quantity is bigger than the invenotry level and there isn't anymore levels then it removes the stock. A negative value will be placed
155
+ *
156
+ * @param {number} inventoryItemId
157
+ * @param {number} qtyToAdjust Absolute amount of Qty
158
+ * @returns Promise
159
+ */
160
+ public adjustQuantityOfInventoryItem = async (inventoryItemId: number, qtyToAdjust: number): Promise<void> => {
161
+ return new Promise<void>(async (resolve, reject) => {
162
+ try {
163
+ Logger.info(`adjustsQuantity for inventoryItem ${inventoryItemId}`);
164
+ const inventoryLevels = await this.getLevel(inventoryItemId);
165
+ Logger.info(`Inventory levels ${JSON.stringify(inventoryLevels)}`);
166
+ let dynamicQtyToAdjust = parseInt(qtyToAdjust.toString(), 10);
167
+ for(const level of inventoryLevels) {
168
+ if(level.available >= qtyToAdjust) {
169
+ this.setQuantity(level.inventory_item_id, level.available - qtyToAdjust, level.location_id);
170
+ dynamicQtyToAdjust = 0;
171
+ break;
172
+ }
173
+ }
174
+ if(dynamicQtyToAdjust > 0){
175
+ Logger.info('Not enough stock in any of the levels. We will remove it from the first level');
176
+ let levelIndex = 0;
177
+ const levelNumber = inventoryLevels.length;
178
+ for(const level of inventoryLevels) {
179
+ Logger.info(`Level ${JSON.stringify(level)}`);
180
+ if(level.available >= dynamicQtyToAdjust) {
181
+ this.setQuantity(level.inventory_item_id, level.available - dynamicQtyToAdjust, level.location_id);
182
+ dynamicQtyToAdjust = 0;
183
+ Logger.info(`Removing all from 1st level. Missing Qty ${dynamicQtyToAdjust} & ${level.location_id}`);
184
+ break;
185
+ } else if (level.available < dynamicQtyToAdjust && levelIndex !== levelNumber - 1) {
186
+ this.setQuantity(level.inventory_item_id, 0, level.location_id);
187
+ dynamicQtyToAdjust = dynamicQtyToAdjust - level.available;
188
+ Logger.info(`Removing part of it from ${level.inventory_item_id} level. Missing Qty ${dynamicQtyToAdjust} & ${level.location_id}`);
189
+ } else if (level.available < dynamicQtyToAdjust && levelIndex === levelNumber - 1) {
190
+ this.setQuantity(level.inventory_item_id, level.available - dynamicQtyToAdjust, level.location_id);
191
+ dynamicQtyToAdjust = 0;
192
+ Logger.info(`Removing the rest of it from ${level.inventory_item_id} level. Missing Qty ${dynamicQtyToAdjust} & ${level.location_id}`);
193
+ break;
194
+ } else {
195
+ Logger.info('Option of level and stock not detected');
196
+ }
197
+ levelIndex +=1;
198
+ }
199
+ } else {
200
+ Logger.info('All stock removed from one Level');
201
+ }
202
+ resolve();
203
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
204
+ });
205
+ };
206
+
207
+ public getLevel = async (inventoryId: number): Promise<IInventoryLevel[]> => {
208
+ return new Promise<IInventoryLevel[]>(async (resolve, reject) => {
209
+ try {
210
+ Logger.info(`getLevel for inventory ID ${inventoryId}`);
211
+ const response = await this.axiosInstance.get(`/inventory_levels.json?inventory_item_ids=${inventoryId}`);
212
+ resolve(response.data.inventory_levels);
213
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
214
+ });
215
+ };
216
+
217
+ /**
218
+ * Returns a list of all iventory Items of the product.
219
+ *
220
+ * @param {IProduct} product
221
+ * @returns Promise
222
+ */
223
+ public getInventoryItemsOfProduct = async (product: IProduct): Promise<IInventoryItemWithVariantGQL[]> => {
224
+ return new Promise<IInventoryItemWithVariantGQL[]>(async (resolve, reject) => {
225
+ try {
226
+ const productID = product ? product.id : 'empty product (no id)';
227
+ Logger.info(`Get Inventory Items for product ID ${productID}`);
228
+ if(!product || !product.variants) {
229
+ Logger.info(`No variants for product ID ${productID}`);
230
+ resolve([]);
231
+ } else {
232
+ const inventoryItemsWithVariant: IInventoryItemWithVariantGQL[] = [];
233
+ const inventoryItemIds = product.variants.map(v => v.inventory_item_id).join(',');
234
+
235
+ const queryIds = product.variants.map(v => this.getInventoryItemIdFromId(v.inventory_item_id));
236
+ const query = gql`
237
+ query getInventoryItems($queryIds: [ID!]!) {
238
+ nodes(ids: $queryIds) {
239
+ ... on InventoryItem {
240
+ id
241
+ measurement {
242
+ id
243
+ weight {
244
+ unit
245
+ value
246
+ }
247
+ }
248
+ unitCost {
249
+ amount
250
+ currencyCode
251
+ }
252
+ requiresShipping
253
+ tracked
254
+ variant {
255
+ id
256
+ price
257
+ }
258
+ }
259
+ }
260
+ }
261
+ `;
262
+
263
+ const resp = await this.axiosInstance.post('/graphql.json', { query: print(query), variables: {queryIds: queryIds} });
264
+ const inventoryItems: IInventoryItemGQL[] = resp.data.data.nodes;
265
+ if(inventoryItemIds && inventoryItems.length > 0){
266
+ for(const inventoryItem of inventoryItems) {
267
+ const variant = product.variants.find(v => v.inventory_item_id === this.getIdFromGraphId(inventoryItem.id));
268
+ const inventoryItemWithVariant: IInventoryItemWithVariantGQL = Object.assign(inventoryItem, { variant_price: parseFloat(variant.price), variant_id: variant.id });
269
+ inventoryItemsWithVariant.push(inventoryItemWithVariant);
270
+ }
271
+ }
272
+ Logger.info(`Inventory Items for product ID ${productID} is ${inventoryItemsWithVariant}`);
273
+ resolve(inventoryItemsWithVariant);
274
+ }
275
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
276
+ });
277
+ };
278
+
279
+ /**
280
+ * Returns a list of all iventory Levels of the product.
281
+ *
282
+ * @param {IProduct} product It must have its variants wioth its inventory items IDs.
283
+ * @returns Promise {IInventoryLevel[]}
284
+ */
285
+ public getInventoryLevelsOfProduct = async (product: IProduct): Promise<IInventoryLevel[]> => {
286
+ try {
287
+ const productID = product ? product.id : 'empty product (no id)';
288
+ Logger.info(`get Inventory Levels for product ID ${productID}`);
289
+ if(!product || !product.variants) {
290
+ Logger.info(`No variants for product ID ${productID}`);
291
+ return [];
292
+ } else {
293
+ const inventoryItemIds = product.variants.map(v => v.inventory_item_id);
294
+ const inventoryLevels: IInventoryLevel[] = await this.getInventoryLevelsOfInventoryItems(inventoryItemIds);
295
+ return inventoryLevels;
296
+ }
297
+ } catch (error) {
298
+ Logger.error('getInventoryLevelsOfProduct - ', error, ErrorHelper.getErrorFromResponse(error));
299
+ throw new InspiraShopifyError(error);
300
+ }
301
+ };
302
+
303
+ /**
304
+ * Returns a list of all iventory Levels given a list of invenotry item IDs
305
+ *
306
+ * @param {number[]} inventoryItemsIds
307
+ * @returns Promise {IInventoryLevel[]}
308
+ */
309
+ public getInventoryLevelsOfInventoryItems = async (inventoryItemsIds: number[]): Promise<IInventoryLevel[]> => {
310
+ try {
311
+ Logger.info(`get Inventory Levels for inventoryItemsIds ${inventoryItemsIds}`);
312
+ if(!inventoryItemsIds || inventoryItemsIds.length === 0) {
313
+ Logger.info(`No inventoryItemsIds ${inventoryItemsIds}`);
314
+ return [];
315
+ } else {
316
+ const inventoryItemIds = inventoryItemsIds.join(',');
317
+ const response = await this.axiosInstance.get(`/inventory_levels.json?inventory_item_ids=${inventoryItemIds}`);
318
+ const inventoryLevels: IInventoryLevel[] = response.data.inventory_levels;
319
+ return inventoryLevels;
320
+ }
321
+ } catch (error) {
322
+ Logger.error('getInventoryLevelsOfInventoryItems - ', error.message, ErrorHelper.getErrorFromResponse(error));
323
+ throw new InspiraShopifyError(error);
324
+ }
325
+ };
326
+
327
+ /**
328
+ * Returns a variant that is the parent of a inventory item
329
+ *
330
+ * @param {IInventoryItem} inventoryItem
331
+ * @returns Promise
332
+ */
333
+ public getInventoryItemVariant = async(inventoryItem: IInventoryItem): Promise<IVariant> => {
334
+ return new Promise<IVariant>(async (resolve, reject) => {
335
+ try {
336
+ Logger.info(`Get Inventory Variant for InventoryItem ${inventoryItem.id}`);
337
+ const query = gql` {
338
+ inventoryItem(id: "gid://shopify/InventoryItem/${inventoryItem.id}") {
339
+ variant {
340
+ id
341
+ product {
342
+ id
343
+ }
344
+ }
345
+ }
346
+ }`;
347
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 3});
348
+ if(response && response.data && response.data.data && response.data.data.inventoryItem && response.data.data.inventoryItem.variant){
349
+ const inventoryItemGraph = response.data.data.inventoryItem;
350
+ if(inventoryItemGraph.variant.length == 0) {
351
+ resolve(null);
352
+ }
353
+ Logger.info(`Variant found with id of ${inventoryItemGraph.variant.id}`);
354
+ inventoryItemGraph.variant.id = this.getIdFromGraphId(inventoryItemGraph.variant.id);
355
+ inventoryItemGraph.variant.product_id = this.getIdFromGraphId(inventoryItemGraph.variant.product.id);
356
+ resolve(inventoryItemGraph.variant);
357
+ } else {
358
+ Logger.error(`getInventoryItemVariant not the expected response. Data ${JSON.stringify(response.data.data)}. Error is ${JSON.stringify(response.data.errors)}` );
359
+ resolve(null);
360
+ }
361
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
362
+ });
363
+ };
364
+
365
+ /**
366
+ * Returns a variant that is the parent of a inventory level
367
+ * We use the admin_graphql_api_id here as the Inventorylevel webhook kinda sucks and doesn't give us the InventoryLevel id???
368
+ *
369
+ * @param {IInventoryLevel} inventoryLevel
370
+ * @returns Promise
371
+ * TODO: Test (Not used yet)
372
+ */
373
+ public getInventoryLevelVariant = async(inventoryLevel: IInventoryLevel): Promise<IVariant> => {
374
+ return new Promise<IVariant>(async (resolve, reject) => {
375
+ try {
376
+ Logger.info(inventoryLevel);
377
+ Logger.info(`Get Inventory Variant for InventoryLevel ${inventoryLevel.inventory_item_id}`);
378
+ if(inventoryLevel.inventory_item_id === undefined) {
379
+ resolve(null);
380
+ }
381
+ const query = gql` {
382
+ inventoryLevel(id: "${inventoryLevel.admin_graphql_api_id}") {
383
+ id
384
+ item{
385
+ variant {
386
+ id
387
+ product {
388
+ id
389
+ }
390
+ }
391
+ }
392
+ }
393
+ }`;
394
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 4 });
395
+ Logger.info(response.data.data);
396
+ if(response && response.data && response.data.data && response.data.data.inventoryLevel && response.data.data.inventoryLevel.item.variant){
397
+ const inventoryItemGraph = response.data.data.inventoryLevel;
398
+ if(inventoryItemGraph.item.variant.length == 0) {
399
+ resolve(null);
400
+ }
401
+ Logger.info(`Variant found with id of ${this.getIdFromGraphId(inventoryItemGraph.item.variant.id)}`);
402
+ inventoryItemGraph.item.variant.id = this.getIdFromGraphId(inventoryItemGraph.item.variant.id);
403
+ inventoryItemGraph.item.variant.product_id = this.getIdFromGraphId(inventoryItemGraph.item.variant.product.id);
404
+ resolve(inventoryItemGraph.item.variant);
405
+ } else {
406
+ Logger.error(`getInventoryLevelVariant not the expected response. Data ${JSON.stringify(response.data.data)}. Error is ${JSON.stringify(response.data.errors)}` );
407
+ resolve(null);
408
+ }
409
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
410
+ });
411
+ };
412
+
413
+
414
+
415
+ }
@@ -0,0 +1,29 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { Logger } from '../Logger';
3
+ import ErrorHelper from './base/ErrorHelper';
4
+ import InspiraShopifyError from './errors/InspiraShopifyError';
5
+
6
+ export class ShopifyLocationService {
7
+
8
+ constructor(private axiosInstance: AxiosInstance) { }
9
+
10
+ public getAll = async (): Promise<ILocation[]> => {
11
+ return new Promise<ILocation[]>(async (resolve, reject) => {
12
+ try {
13
+ Logger.info('getAll locations');
14
+ const response = await this.axiosInstance.get('/locations.json');
15
+ resolve(response.data.locations);
16
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
17
+ });
18
+ };
19
+
20
+ public getLocationById = async (id: number): Promise<ILocation> => {
21
+ return new Promise<ILocation>(async (resolve, reject) => {
22
+ try {
23
+ Logger.info(`get location with id of ${id}`);
24
+ const response = await this.axiosInstance.get(`/locations/${id}.json`);
25
+ resolve(response.data.location);
26
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
27
+ });
28
+ };
29
+ }