@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,1143 @@
1
+ import { AbstractService } from './base/AbstractService';
2
+ import { AxiosInstance } from 'axios';
3
+ import CountryCodeService from '../services/CountryCodeService';
4
+ import ErrorHelper from './base/ErrorHelper';
5
+ import InspiraShopifyError from './errors/InspiraShopifyError';
6
+ import { Logger } from '../Logger';
7
+ import { ShopifyShopService } from './ShopifyShopService';
8
+ import gql from 'graphql-tag';
9
+ import { print } from 'graphql';
10
+ import InspiraShopifyCustomError from './errors/InspiraShopifyCustomError';
11
+ import InspiraShopifyUnableToReserveInventoryError from './errors/InspiraShopifyUnableToReserveInventoryError';
12
+
13
+ export class ShopifyOrderService extends AbstractService{
14
+
15
+ private shopifyShopService: ShopifyShopService;
16
+
17
+ private draftOrderGraphQLMutation = gql`
18
+ mutation draftOrderCalculate($input: DraftOrderInput!) {
19
+ draftOrderCalculate(input: $input) {
20
+ calculatedDraftOrder {
21
+ totalTax
22
+ taxLines {
23
+ priceSet {
24
+ shopMoney{
25
+ amount
26
+ currencyCode
27
+ }
28
+ }
29
+ title
30
+ rate
31
+ }
32
+ shippingLine {
33
+ taxLines {
34
+ rate
35
+ title
36
+ priceSet {
37
+ shopMoney {
38
+ amount
39
+ currencyCode
40
+ }
41
+ }
42
+ }
43
+ }
44
+ availableShippingRates{
45
+ handle
46
+ title
47
+ price {
48
+ amount
49
+ currencyCode
50
+ }
51
+ }
52
+ }
53
+ userErrors {
54
+ field
55
+ message
56
+ }
57
+ }
58
+ }
59
+ `;
60
+
61
+ constructor(private axiosInstance: AxiosInstance) {
62
+ super();
63
+ this.shopifyShopService = new ShopifyShopService(axiosInstance);
64
+ }
65
+
66
+ public getFulfillments = async (orderId: number): Promise<IFulFillment[]> => {
67
+ return new Promise<IFulFillment[]>(async (resolve, reject) => {
68
+ try {
69
+ Logger.info(`ShopifyOrderService - getFulfillments -> for order ID ${orderId}`);
70
+ const response = await this.axiosInstance.get(`/orders/${orderId}/fulfillments.json`);
71
+ resolve(response.data.fulfillments);
72
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
73
+ });
74
+ };
75
+
76
+ public getFulfillmentOrders = async (orderId: number): Promise<IFulFillmentOrder[]> => {
77
+ return new Promise<IFulFillmentOrder[]>(async (resolve, reject) => {
78
+ try {
79
+ Logger.info(`ShopifyOrderService - getFulfillmentOrders -> for order ID ${orderId}`);
80
+ const response = await this.axiosInstance.get(`/orders/${orderId}/fulfillment_orders.json`);
81
+ resolve(response.data.fulfillment_orders);
82
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
83
+ });
84
+ };
85
+
86
+ /**
87
+ * Gets all orders after the date given, can also take a timestring
88
+ *
89
+ * @param {string} dateTime
90
+ * @returns Promise
91
+ */
92
+ public getByMinDateUpdated = async (dateTime: string): Promise<IOrder[]> => {
93
+ return new Promise<IOrder[]>(async (resolve, reject) => {
94
+ try {
95
+ Logger.info(`ShopifyOrderService - getByMinDateUpdated -> getting order from the: ${dateTime}`);
96
+ const response = await this.axiosInstance.get(`/orders.json?status=open&created_at_min=${dateTime}`);
97
+ resolve(response.data.orders);
98
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
99
+ });
100
+ };
101
+
102
+ /**
103
+ * Gets all orders before the date given
104
+ *
105
+ * @param {string} dateTime
106
+ * @returns Promise
107
+ */
108
+ public getByMaxDateUpdated = async (dateTime: string): Promise<IOrder[]> => {
109
+ return new Promise<IOrder[]>(async (resolve, reject) => {
110
+ try {
111
+ Logger.info(`ShopifyOrderService - getByMaxDateUpdated ->getting order past the: ${dateTime}`);
112
+ const response = await this.axiosInstance.get(`/orders.json?status=open&created_at_max=${dateTime}`);
113
+ resolve(response.data.orders);
114
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
115
+ });
116
+ };
117
+
118
+ public getById = async (orderId: number): Promise<IOrder> => {
119
+ return new Promise<IOrder>(async (resolve, reject) => {
120
+ try {
121
+ Logger.info(`ShopifyOrderService - getById -> getting order for ID ${orderId}`);
122
+ const response = await this.axiosInstance.get(`/orders/${orderId}.json`);
123
+ resolve(response.data.order);
124
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
125
+ });
126
+ };
127
+
128
+ public getByIdWithOptions = async (orderId: number, options: { add_tx?: boolean; }): Promise<IOrder> => {
129
+ try {
130
+ Logger.info(`ShopifyOrderService - getByIdWithOptions -> Get product with id ${orderId}`);
131
+ let ordderToReturn: IOrder = null;
132
+
133
+ const orderQuery = `{ order(id: "${this.getGraphOrderIdFromId(orderId)}") {
134
+ id
135
+ name
136
+ ${options.add_tx === false ? '' : 'transactions { formattedGateway gateway authorizationCode }'}
137
+ }
138
+ }`;
139
+ Logger.info(`ShopifyOrderService - getByIdWithOptions -> Order graphQL query going in ${orderQuery}`);
140
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${orderQuery}`) }, { query_cost: 80 });
141
+ if (response && response.data && response.data.data && response.data.data.order) {
142
+ const responseOrder: ShopifygraphQl.IGenericOrderResponse = response.data.data.order;
143
+ ordderToReturn = {
144
+ id: this.getIdFromGraphId(responseOrder.id),
145
+ name: responseOrder.name,
146
+ transaction: responseOrder.transaction ? responseOrder.transaction : null
147
+ };
148
+ }
149
+ Logger.info(`ShopifyOrderService - getByIdWithOptions -> ${ordderToReturn?.id} Products returning`);
150
+ return ordderToReturn;
151
+ } catch (error) { this.logErrorAndThrow(error); }
152
+ };
153
+
154
+ public getInBatchGraphQL = async (channel: 'web', gateWay: 'shopify_payments', cursor: string): Promise<IOrderBatch> => {
155
+ try {
156
+ Logger.info(`ShopifyOrderService - getInBatchGraphQL - channel ${channel}, gateWay: ${gateWay}`);
157
+ let ordersToReturn: IOrderInBatch[] = [];
158
+ const queryParams = `channel:${channel},gateway:${gateWay}`;
159
+
160
+ const orderQuery = `{ orders(first: 250, ${ cursor ? `after: "${cursor}",` : ''} sortKey: ID, reverse: true, query: "${queryParams}") {
161
+ pageInfo {
162
+ hasNextPage
163
+ hasPreviousPage
164
+ startCursor
165
+ endCursor
166
+ }
167
+ edges {
168
+ cursor
169
+ node {
170
+ id
171
+ createdAt
172
+ email
173
+ customAttributes {
174
+ key
175
+ value
176
+ }
177
+ customer {
178
+ id
179
+ tags
180
+ defaultEmailAddress {
181
+ marketingOptInLevel
182
+ marketingState
183
+ marketingUpdatedAt
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ }
190
+ }`;
191
+ Logger.info(`ShopifyProductListService - getInBatchGraphQL -> Order graphQL query going in ${orderQuery}`);
192
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${orderQuery}`) }, { query_cost: 250 });
193
+ let nextCursor: string = null;
194
+ if (response && response.data && response.data.data && response.data.data.orders) {
195
+ const responseOrder: { pageInfo: ShopifygraphQl.IPageInfo, edges: { cursor: string; node: IOrderInBatch; }[]} = response.data.data.orders;
196
+ nextCursor = responseOrder.pageInfo.hasNextPage ? responseOrder.pageInfo.endCursor : null;
197
+ ordersToReturn = responseOrder.edges.map(edge => edge.node);
198
+ }
199
+ Logger.info(`ShopifyProductListService - getInBatchGraphQL -> ${ordersToReturn?.length} Orders returning`);
200
+ return { orders: ordersToReturn, nextCursor: nextCursor };
201
+ } catch (error) { this.logErrorAndThrow(error); }
202
+ };
203
+
204
+ public getInBatch = async (sinceId: number, pageNumber: number, getBachesNumber: boolean, orderstatus?: IOrderStatus): Promise<IOrderBatch_DEPRECATED> => {
205
+ return new Promise<IOrderBatch_DEPRECATED>(async (resolve, reject) => {
206
+ try {
207
+ Logger.info(`Getting orders since id ${sinceId}`);
208
+ if(!orderstatus) { orderstatus = 'any'; }
209
+ let batches = 0;
210
+ let count = 0;
211
+ if(getBachesNumber) {
212
+ const response = await this.axiosInstance.get('/orders/count.json?status=any');
213
+ count = response.data.count;
214
+ batches = parseInt(((count / pageNumber) + 1).toString());
215
+ }
216
+ const orderBatch: IOrderBatch_DEPRECATED = { count: count, batches : batches, orders: [], nextSinceId: 0 };
217
+ const ordersGetURL = `/orders.json?status=${orderstatus}&limit=${pageNumber}&since_id=${sinceId}`;
218
+ Logger.info(`Getting orders since id URL ${ordersGetURL}`);
219
+ const ordersRetrieved = await this.axiosInstance.get(ordersGetURL);
220
+ if(ordersRetrieved.data.orders && ordersRetrieved.data.orders.length > 0) {
221
+ orderBatch.orders = ordersRetrieved.data.orders;
222
+ orderBatch.nextSinceId = orderBatch.orders[orderBatch.orders.length - 1].id;
223
+ } else {
224
+ Logger.info('Getting orders is returned empty');
225
+ orderBatch.orders = [];
226
+ orderBatch.nextSinceId = 0;
227
+ }
228
+ resolve(orderBatch);
229
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
230
+ });
231
+ };
232
+
233
+ public getAll = async (orderstatus?: IOrderStatus): Promise<IOrder[]> => {
234
+ return new Promise<IOrder[]>(async (resolve, reject) => {
235
+ try {
236
+ if(!orderstatus) { orderstatus = 'any'; }
237
+ Logger.info('ShopifyOrderService - getAll -> getting orders');
238
+ let orders: IOrder[] = [];
239
+ const response = await this.axiosInstance.get('/orders/count.json');
240
+ const count = response.data.count;
241
+ Logger.info(`ShopifyOrderService - getAll -> Getting ${count} orders`);
242
+ while (orders.length < count) {
243
+ let ordersGetURL = `/orders.json?status=${orderstatus}&limit=250&since_id=`;
244
+ if(orders.length > 1) {
245
+ ordersGetURL += orders[orders.length - 1].id;
246
+ } else {
247
+ ordersGetURL += '0';
248
+ }
249
+ const currentPage = (parseInt((orders.length / 250).toFixed(0)) + 1);
250
+ Logger.info('ShopifyOrderService - getAll -> Orders Get URL -> ', ordersGetURL );
251
+ const ordersRetrieved = await this.axiosInstance.get(ordersGetURL);
252
+ orders = orders.concat(ordersRetrieved.data.orders);
253
+ Logger.info('ShopifyOrderService - getAll -> Orders retrieved -> ' + orders.length);
254
+ const nextPage = (parseInt((orders.length / 250).toFixed(0)) + 1);
255
+ if(currentPage === nextPage) {
256
+ Logger.info('ShopifyOrderService - getAll -> Orders next page is the current page so, we are breaking the loop!');
257
+ break;
258
+ }
259
+ }
260
+ resolve(orders);
261
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
262
+ });
263
+ };
264
+
265
+ /**
266
+ * Adds a metafield.
267
+ *
268
+ * @param {number} id - order ID
269
+ * @param {IMetafield} metafield to be saved
270
+ * @returns Promise
271
+ */
272
+ public addMetafield = async (id: number, metafield: IMetafield): Promise<IMetafield> => {
273
+ return new Promise<IMetafield>(async (resolve, reject) => {
274
+ try {
275
+ Logger.info(`ShopifyOrderService - addMetafield -> Adding Metafield for order id -> ${id}`);
276
+ const response = await this.axiosInstance.post(`/orders/${id}/metafields.json`, { metafield: metafield });
277
+ resolve(response.data.metafield);
278
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
279
+ });
280
+ };
281
+
282
+ /**
283
+ * Gets an order only with attributes and line item ids. Gets a max of 200 lineitems
284
+ *
285
+ * @param {number} orderId
286
+ * @returns Promise
287
+ */
288
+ public getOrderMinimalById = async (orderId: number): Promise<IOrderMinimal> => {
289
+ return new Promise<IOrderMinimal>(async (resolve, reject) => {
290
+ try {
291
+ Logger.info(`ShopifyOrderService - getOrderMinimalById -> Get Minimal order with ID: ${orderId}`);
292
+ const query = gql`{
293
+ order(id: "gid://shopify/Order/${orderId}") {
294
+ lineItems(first: 200) {
295
+ edges {
296
+ node {
297
+ id
298
+ unfulfilledQuantity
299
+ customAttributes {
300
+ key
301
+ value
302
+ }
303
+ product {
304
+ id
305
+ }
306
+ variant {
307
+ id
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }`;
314
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 803 });
315
+ if(response && response.data && response.data.data && response.data.data.order){
316
+ const orderGraph = response.data.data.order;
317
+
318
+ resolve({ id: orderId,
319
+ line_items: orderGraph.lineItems.edges.map( (gi) =>
320
+ ({ id: this.getIdFromGraphId(gi.node.id),
321
+ unfulfilledQuantity: gi.node.unfulfilledQuantity,
322
+ properties: gi.node.customAttributes.map( (attr) => ({ value: attr.value, name: attr.key } as IProperty) ),
323
+ variant_id: gi.node.variant ? this.getIdFromGraphId(gi.node.variant.id) : null,
324
+ product_id: gi.node.product ? this.getIdFromGraphId(gi.node.product.id) : null } as ILineItemMinimal ) )});
325
+ } else {
326
+ Logger.error(`ShopifyOrderService - getOrderMinimalById -> getOrderMinimalById not the expected response. Data ${JSON.stringify(response.data.data)}. Error is ${JSON.stringify(response.data.errors)}` );
327
+ resolve(null);
328
+ }
329
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
330
+ });
331
+ };
332
+
333
+ /**
334
+ * Gets an order only with attributes and line item ids. Gets a max of 200 line items
335
+ *
336
+ * @param {number} orderId
337
+ * @returns Promise
338
+ */
339
+ public getOrderProductTagsById = async (orderId: number): Promise<IOrderProductTags> => {
340
+ return new Promise<IOrderProductTags>(async (resolve, reject) => {
341
+ try {
342
+ Logger.info(`ShopifyOrderService - getOrderProductTagsById ->Get getOrderProductTagsById order with ID: ${orderId}`);
343
+ const query = gql`{
344
+ order(id: "gid://shopify/Order/${orderId}") {
345
+ phone
346
+ email
347
+ customer {
348
+ firstName
349
+ lastName
350
+ }
351
+ customAttributes {
352
+ key
353
+ value
354
+ }
355
+ shippingAddress {
356
+ address1
357
+ zip
358
+ firstName
359
+ lastName
360
+ city
361
+ province
362
+ address2
363
+ }
364
+ billingAddress {
365
+ address1
366
+ zip
367
+ firstName
368
+ lastName
369
+ city
370
+ province
371
+ address2
372
+ }
373
+ note
374
+ lineItems(first: 200) {
375
+ edges {
376
+ node {
377
+ id
378
+ product {
379
+ id
380
+ tags
381
+ }
382
+ variant {
383
+ title
384
+ id
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+ }`;
391
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 803 });
392
+ if(response && response.data && response.data.data && response.data.data.order){
393
+ const orderGraph = response.data.data.order;
394
+
395
+ resolve({ id: orderId,
396
+ phone: orderGraph.phone,
397
+ note: orderGraph.note,
398
+ note_attributes: (orderGraph.customAttributes && orderGraph.customAttributes.length ? orderGraph.customAttributes.map(attr => ({ name: attr.key, value: attr.value})): []) as IAttr[],
399
+ email: orderGraph.email,
400
+ shipping_address: orderGraph.shippingAddress ? { address1: orderGraph.shippingAddress.address1, address2: orderGraph.shippingAddress.address2, zip: orderGraph.shippingAddress.zip,
401
+ city: orderGraph.shippingAddress.city, last_name: orderGraph.shippingAddress.lastName, first_name: orderGraph.shippingAddress.firstName, country: orderGraph.shippingAddress.country,
402
+ province: orderGraph.shippingAddress.province, phone: orderGraph.shippingAddress.phone } as IAddress : null,
403
+ billing_address: orderGraph.billingAddress ? { address1: orderGraph.billingAddress.address1, address2: orderGraph.billingAddress.address2, zip: orderGraph.billingAddress.zip,
404
+ city: orderGraph.billingAddress.city, last_name: orderGraph.billingAddress.lastName, first_name: orderGraph.billingAddress.firstName, country: orderGraph.billingAddress.country,
405
+ province: orderGraph.billingAddress.province, phone: orderGraph.billingAddress.phone } as IAddress : null,
406
+ customer: { first_name: orderGraph.customer.firstName, last_name: orderGraph.customer.lastName },
407
+ line_items: orderGraph.lineItems.edges.map( (gi) =>
408
+ ({ id: this.getIdFromGraphId(gi.node.id), title: gi.node.variant?.title ? gi.node.variant?.title : '',
409
+ variant_id: gi.node.variant ? this.getIdFromGraphId(gi.node.variant.id) : null,
410
+ product: gi.node.product ? { id: this.getIdFromGraphId(gi.node.product.id), tags: gi.node.product.tags } : null } as any ) )});
411
+ } else {
412
+ Logger.error(`ShopifyOrderService - getOrderProductTagsById -> not the expected response. Data ${JSON.stringify(response.data.data)}. Error is ${JSON.stringify(response.data.errors)}` );
413
+ resolve(null);
414
+ }
415
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
416
+ });
417
+ };
418
+
419
+ public getCount = async (orderstatus?: IOrderStatus): Promise<number> => {
420
+ return new Promise<number>(async (resolve, reject) => {
421
+ try {
422
+ if(!orderstatus) { orderstatus = 'any'; }
423
+ Logger.info('ShopifyOrderService - getCount -> getting orders count');
424
+ const response = await this.axiosInstance.get('/orders/count.json');
425
+ const count = response.data.count;
426
+ resolve(count);
427
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
428
+ });
429
+ };
430
+
431
+ public createFulfillments = async (orderId: number, orderFulfillmentPetitions: IFulFillmentLine[], notifyCustomer: boolean, shipmentStatus: IShipmentStatus): Promise<IFulFillment> => {
432
+ return new Promise<IFulFillment>(async (resolve, reject) => {
433
+ try {
434
+ const fulfillmentOrders = await this.getFulfillmentOrders(orderId);
435
+ Logger.info(`ShopifyOrderService - createFulfillments -> Retrieved fulfillment Orders for order ID ${orderId}`, `${JSON.stringify(fulfillmentOrders)}`);
436
+ Logger.info(`ShopifyOrderService - createFulfillments -> Fulfillment Petitions for order ID ${orderId}`, `${JSON.stringify(orderFulfillmentPetitions)}`);
437
+ const fulfillment: IFulfillmentsPost = { notify_customer: notifyCustomer, line_items_by_fulfillment_order: [] };
438
+ for(const fulfillmentOrder of fulfillmentOrders) {
439
+ for(const fulfillmentOrderLineItem of fulfillmentOrder.line_items) {
440
+ if(fulfillmentOrderLineItem.fulfillable_quantity > 0) {
441
+ const orderFulfillmentPetitionIndex = orderFulfillmentPetitions.findIndex((orf) => orf && orf.line_item_id === fulfillmentOrderLineItem.line_item_id);
442
+ if(orderFulfillmentPetitionIndex > -1) {
443
+ const orderFulfillmentPetition = orderFulfillmentPetitions[orderFulfillmentPetitionIndex];
444
+ if(orderFulfillmentPetition) {
445
+ const quantityToFulfill = fulfillmentOrderLineItem.fulfillable_quantity >= orderFulfillmentPetition.quantity_delivered ? orderFulfillmentPetition.quantity_delivered : fulfillmentOrderLineItem.fulfillable_quantity;
446
+ const existingFulfilmentToPost = fulfillment.line_items_by_fulfillment_order.findIndex(f => f.fulfillment_order_id === fulfillmentOrder.id);
447
+ if(existingFulfilmentToPost > -1) {
448
+ fulfillment.line_items_by_fulfillment_order[existingFulfilmentToPost].fulfillment_order_line_items.push({ id: fulfillmentOrderLineItem.id, quantity: quantityToFulfill });
449
+ } else {
450
+ fulfillment.line_items_by_fulfillment_order.push({ fulfillment_order_id: fulfillmentOrder.id, fulfillment_order_line_items: [ { id: fulfillmentOrderLineItem.id, quantity: quantityToFulfill }]});
451
+ }
452
+ if(quantityToFulfill < orderFulfillmentPetition.quantity_delivered) {
453
+ orderFulfillmentPetition.quantity_delivered = orderFulfillmentPetition.quantity_delivered - quantityToFulfill;
454
+ } else {
455
+ delete orderFulfillmentPetitions[orderFulfillmentPetitionIndex];
456
+ }
457
+ }
458
+ } else {
459
+ Logger.info(`ShopifyOrderService - createFulfillments -> petition found for ${JSON.stringify(fulfillmentOrderLineItem)}`);
460
+ }
461
+ } else {
462
+ Logger.info(`ShopifyOrderService - createFulfillments -> Fulfillment order line item does does not have fulfillable_quantity ${JSON.stringify(fulfillmentOrderLineItem)}`);
463
+ }
464
+ }
465
+ }
466
+ Logger.info(`ShopifyOrderService - createFulfillments -> Petitions not being attended ${JSON.stringify(orderFulfillmentPetitions)}`);
467
+ if(fulfillment.line_items_by_fulfillment_order && fulfillment.line_items_by_fulfillment_order.length > 0) {
468
+ const response = await this.axiosInstance.post('/fulfillments.json', { fulfillment: fulfillment });
469
+ const fulfillmentCreated: IFulFillment = response.data.fulfillment;
470
+ Logger.info(`ShopifyOrderService - createFulfillments -> Fulfillment created ${JSON.stringify(fulfillmentCreated)}`);
471
+ if(shipmentStatus) {
472
+ try {
473
+ const event = await this.addShipmentStatusToFulfillment(orderId, fulfillmentCreated.id, shipmentStatus);
474
+ Logger.info(`ShopifyOrderService - createFulfillments ->Event created ${JSON.stringify(event)}`);
475
+ } catch(e) {
476
+ Logger.error(`ShopifyOrderService - createFulfillments -> Failed to add event to fulfillment ${e.message}`);
477
+ }
478
+ } else {
479
+ Logger.info('ShopifyOrderService - createFulfillments -> Shipment status was empty');
480
+ }
481
+ resolve(fulfillmentCreated);
482
+ } else {
483
+ Logger.info(`ShopifyOrderService - createFulfillments -> Nothing to fulfill from petitions ${JSON.stringify(orderFulfillmentPetitions)}`);
484
+ resolve(null);
485
+ }
486
+ } catch (error) { Logger.error(`ShopifyOrderService - createFulfillments -> Error while creating fulfillments for orderId ${orderId}`, error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
487
+ });
488
+ };
489
+
490
+ public addShipmentStatusToFulfillment = async (orderId: number, fulfillmentid: number, shipmentStatus: IShipmentStatus): Promise<IFulfillmentEvent> => {
491
+ return new Promise<IFulfillmentEvent>(async (resolve, reject) => {
492
+ try {
493
+ Logger.info(`ShopifyOrderService - addShipmentStatusToFulfillment -> Creating fulfillment event (status ${shipmentStatus}) for order id ${orderId} and fulfillment id ${fulfillmentid}`);
494
+ const response = await this.axiosInstance.post(`/orders/${orderId}/fulfillments/${fulfillmentid}/events.json`, { event: { status: shipmentStatus } });
495
+ resolve(response.data.fulfillment_event);
496
+ } catch (error) { Logger.error(`ShopifyOrderService - addShipmentStatusToFulfillment -> Error while creating fulfillment event for orderId ${orderId} and fulfillment ${fulfillmentid}`, error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
497
+ });
498
+ };
499
+
500
+ public getMinMaxDateOrders = (min: string, max: string): Promise<IOrder[]> => {
501
+ return new Promise<IOrder[]>(async (resolve, reject) => {
502
+ try {
503
+ Logger.info(`ShopifyOrderService - getMinMaxDateOrders -> Getting orders from min ${min} and max ${max}`);
504
+ const response = await this.axiosInstance.get(`/orders.json?status=any&created_at_max=${max}&created_at_min=${min}&limit=250`);
505
+ resolve(response.data.orders);
506
+ } catch (error) { Logger.error('ShopifyOrderService - getMinMaxDateOrders -> Error while getting min max orders ', error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
507
+ });
508
+ };
509
+
510
+ public completeFulfillment = async (orderId: number, fullfilmentId: number): Promise<IFulFillment> => {
511
+ return new Promise<IFulFillment>(async (resolve, reject) => {
512
+ try {
513
+ Logger.info(`ShopifyOrderService - completeFulfillment -> Compliting fulfillment for order ID ${orderId}`);
514
+ const response = await this.axiosInstance.post(`/orders/${orderId}/fulfillments/${fullfilmentId}/complete.json`);
515
+ resolve(response.data.fulfillment);
516
+ } catch (error) { Logger.error('ShopifyOrderService - completeFulfillment -> Error while compliting fulfillment for orderId ' + orderId, error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
517
+ });
518
+ };
519
+
520
+ public openFulfillment = async (orderId: number, fullfilmentId: number): Promise<IFulFillment> => {
521
+ return new Promise<IFulFillment>(async (resolve, reject) => {
522
+ try {
523
+ Logger.info(`ShopifyOrderService - openFulfillment -> Opening fulfillment for order ID ${orderId}`);
524
+ const response = await this.axiosInstance.post(`/orders/${orderId}/fulfillments/${fullfilmentId}/open.json`);
525
+ resolve(response.data.fulfillment);
526
+ } catch (error) { Logger.error('ShopifyOrderService - openFulfillment -> Error while opening fulfillment for orderId ' + orderId, error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
527
+ });
528
+ };
529
+
530
+ public cancelFulfillment = async (orderId: number, fullfilmentId: number): Promise<IFulFillment> => {
531
+ return new Promise<IFulFillment>(async (resolve, reject) => {
532
+ try {
533
+ Logger.info(`ShopifyOrderService - cancelFulfillment -> Canceling fulfillment for order ID ${orderId}`);
534
+ const response = await this.axiosInstance.post(`/orders/${orderId}/fulfillments/${fullfilmentId}/cancel.json`);
535
+ resolve(response.data.fulfillment);
536
+ } catch (error) { Logger.error('ShopifyOrderService - cancelFulfillment -> Error while canceling fulfillment for orderId ' + orderId, error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
537
+ });
538
+ };
539
+
540
+ public addAdditionalAttribute = async (orderId: number, attributeName: string, attributeValue: string): Promise<IOrder> => {
541
+ return new Promise<IOrder>(async (resolve, reject) => {
542
+ try {
543
+ Logger.info(`ShopifyOrderService - addAdditionalAttribute -> Adding attribute to order with ID ${orderId}`);
544
+ let additionalAttributes: IAttr[] = [];
545
+ const orderFromShopify = await this.getById(orderId);
546
+ if(orderFromShopify.note_attributes) {
547
+ additionalAttributes = orderFromShopify.note_attributes;
548
+ const attr = additionalAttributes.find(a => a.name === attributeName);
549
+ if(attr) { attr.value = attributeValue; }
550
+ else { additionalAttributes.push({name: attributeName, value: attributeValue}); }
551
+ } else {
552
+ additionalAttributes.push({name: attributeName, value: attributeValue});
553
+ }
554
+ Logger.info('ShopifyOrderService - addAdditionalAttribute -> Adding attributes to order ', additionalAttributes);
555
+ const order = { id: orderId, note_attributes: additionalAttributes };
556
+ const response = await this.axiosInstance.put(`/orders/${orderId}.json`, { order: order });
557
+ resolve(response.data.order);
558
+ } catch (error) { Logger.error('ShopifyOrderService - addAdditionalAttribute -> Error while adding note attribute to order with Id ' + orderId, error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
559
+ });
560
+ };
561
+
562
+
563
+ /**
564
+ * Adds attr on top of the existing ones.
565
+ *
566
+ * @param {number} orderId
567
+ * @param {{attributeName: string, attributeValue: string}[]} attributes to be added
568
+ * @returns
569
+ */
570
+ public addAdditionalAttributes = async (orderId: number, attributes: {attributeName: string, attributeValue: string}[]): Promise<IOrder> => {
571
+ return new Promise<IOrder>(async (resolve, reject) => {
572
+ try {
573
+ Logger.info(`ShopifyOrderService - addAdditionalAttributes -> Adding attributes to order with ID ${orderId}`);
574
+ let additionalAttributes: IAttr[] = [];
575
+ const orderFromShopify = await this.getById(orderId);
576
+ if(orderFromShopify.note_attributes) {
577
+ additionalAttributes = orderFromShopify.note_attributes;
578
+ for (const attribute of attributes) {
579
+ const attr = additionalAttributes.find(a => a.name === attribute.attributeName);
580
+ if(attr) { attr.value = attribute.attributeValue; }
581
+ else { additionalAttributes.push({name: attribute.attributeName, value: attribute.attributeValue}); }
582
+ }
583
+ } else {
584
+ for (const attribute of attributes) {
585
+ additionalAttributes.push({name: attribute.attributeName, value: attribute.attributeValue});
586
+ }
587
+ }
588
+ Logger.info('ShopifyOrderService - addAdditionalAttributes -> Adding attributes to order ', additionalAttributes);
589
+ const order = { id: orderId, note_attributes: additionalAttributes };
590
+ const response = await this.axiosInstance.put(`/orders/${orderId}.json`, { order: order });
591
+ resolve(response.data.order);
592
+ } catch (error) { Logger.error('Error while adding note attribute to order with Id ' + orderId, error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
593
+ });
594
+ };
595
+
596
+ /**
597
+ * Sets a new tags to the order. Tags on the order will getoverwritten by this ones.
598
+ *
599
+ * @param {number} orderId
600
+ * @param {string} tags
601
+ * @returns Promise
602
+ */
603
+ public setTags = async (orderId: number, tags: string): Promise<IOrder> => {
604
+ return new Promise<IOrder>(async (resolve, reject) => {
605
+ try {
606
+ Logger.info(`ShopifyOrderService - setTags -> Adding tags to order with ID ${orderId}, overwrites`);
607
+ const order = { id: orderId, tags: tags };
608
+ const response = await this.axiosInstance.put(`/orders/${orderId}.json`, { order: order });
609
+ resolve(response.data.order);
610
+ } catch (error) { Logger.error('ShopifyOrderService - setTags -> Error while adding tags to order with Id ' + orderId, error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
611
+ });
612
+ };
613
+
614
+ /**
615
+ *
616
+ * Adds one or more tags to an order. It does add them toghether with the existing ones.
617
+ *
618
+ * @param {number} orderId order id number
619
+ * @param {string} tagsToAdd tags to add separated by coma
620
+ * @returns Promise array with errors
621
+ */
622
+ public addTags = async (orderId: number, tagsToAdd: string): Promise<string[]> => {
623
+ return new Promise<string[]>(async (resolve, reject) => {
624
+ try {
625
+ const query = gql`mutation {
626
+ tagsAdd(id: "${this.getGraphOrderIdFromId(orderId)}", tags: "${this.escapeQuotes(tagsToAdd)}") {
627
+ node {
628
+ id
629
+ }
630
+ userErrors {
631
+ field
632
+ message
633
+ }
634
+ }
635
+ }`;
636
+
637
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 100 });
638
+ const errors: string[] = [];
639
+ Logger.info(`ShopifyOrderService - addTags -> Add tags to order response: ${JSON.stringify(response.data)} `);
640
+ if(response && response.data && response.data.data){
641
+ const updatesResponse = response.data.data;
642
+ for(const key in updatesResponse){
643
+ const err = updatesResponse[key].userErrors;
644
+ if(err && err.length > 0) {
645
+ errors.push(err);
646
+ }
647
+ }
648
+ } else {
649
+ errors.push('No response arrived');
650
+ }
651
+ resolve(errors);
652
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
653
+ });
654
+ };
655
+
656
+ /**
657
+ *
658
+ * Removes one or more tags to an order.
659
+ *
660
+ * @param {number} orderId order id number
661
+ * @param {string} tagsToRemove tags to remove separated by coma
662
+ * @returns Promise array with errors
663
+ */
664
+ public removeTags = async (orderId: number, tagsToRemove: string): Promise<string[]> => {
665
+ return new Promise<string[]>(async (resolve, reject) => {
666
+ try {
667
+ const query = gql`mutation {
668
+ tagsRemove(id: "${this.getGraphOrderIdFromId(orderId)}", tags: "${this.escapeQuotes(tagsToRemove)}") {
669
+ node {
670
+ id
671
+ }
672
+ userErrors {
673
+ field
674
+ message
675
+ }
676
+ }
677
+ }`;
678
+
679
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 100 });
680
+ const errors: string[] = [];
681
+ Logger.info(`ShopifyOrderService - removeTags -> Remove tags to order response: ${JSON.stringify(response.data)} `);
682
+ if(response && response.data && response.data.data){
683
+ const updatesResponse = response.data.data;
684
+ for(const key in updatesResponse){
685
+ const err = updatesResponse[key].userErrors;
686
+ if(err && err.length > 0) {
687
+ errors.push(err);
688
+ }
689
+ }
690
+ } else {
691
+ errors.push('No response arrived');
692
+ }
693
+ resolve(errors);
694
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
695
+ });
696
+ };
697
+
698
+ /**
699
+ * Creates an order in the store.
700
+ * If 'inventoryPolicy' is not set, then, the default one is 'decrement_obeying_policy'
701
+ *
702
+ * @param {IOrder} order
703
+ * @param {'bypass'|'decrement_ignoring_policy'|'decrement_obeying_policy'} inventoryBehaviour?
704
+ * @returns Promise
705
+ */
706
+ public create = async (order: IOrder, inventoryBehaviour?: 'bypass' | 'decrement_ignoring_policy' | 'decrement_obeying_policy'): Promise<IOrder> => {
707
+ return new Promise<IOrder>(async (resolve, reject) => {
708
+ try {
709
+ Logger.info('ShopifyOrderService - create -> Creating order', order);
710
+ order.inventory_behaviour = inventoryBehaviour ? inventoryBehaviour : 'decrement_obeying_policy';
711
+ const response = await this.axiosInstance.post('/orders.json', { order: order });
712
+ resolve(response.data.order);
713
+ } catch (error) {
714
+ const errorMsg = ErrorHelper.getErrorFromResponse(error);
715
+ Logger.error('ShopifyOrderService - create -> Error while creating order', error, errorMsg);
716
+ if(errorMsg && errorMsg.toString().indexOf('Unable to reserve inventory') > -1) {
717
+ reject(new InspiraShopifyUnableToReserveInventoryError(errorMsg));
718
+ } else {
719
+ reject(new InspiraShopifyError(errorMsg));
720
+ }
721
+ }
722
+ });
723
+ };
724
+
725
+ /**
726
+ * Creates transaction (set 'source' as 'external' to create without needing authorisation)
727
+ *
728
+ * @param {number} orderId
729
+ * @param {ITransaction} transaction
730
+ * @returns Promise
731
+ */
732
+ public createTransaction = async (orderId: number, transaction: ITransaction): Promise<ITransaction> => {
733
+ return new Promise<ITransaction>(async (resolve, reject) => {
734
+ try {
735
+ Logger.info(`ShopifyOrderService - createTransaction -> Creating transaction for order: ${orderId}, amount: ${transaction.amount}`);
736
+ const response = await this.axiosInstance.post(`/orders/${orderId}/transactions.json`, { transaction: transaction });
737
+ resolve(response.data.transaction);
738
+ } catch (error) { Logger.error('ShopifyOrderService - createTransaction -> Error while creating transaction', error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
739
+ });
740
+ };
741
+
742
+ /**
743
+ * Duplicates an order
744
+ *
745
+ * @param {number} orderId
746
+ * @returns Promise
747
+ */
748
+ public duplicate = async (orderId: number): Promise<IOrder> => {
749
+ return new Promise<IOrder>(async (resolve, reject) => {
750
+ try {
751
+ Logger.info(`ShopifyOrderService - duplicate -> Duplicating order: ${orderId}`);
752
+ const order = await this.getById(orderId);
753
+ const orderClone: IOrder = JSON.parse( JSON.stringify(order));
754
+ delete orderClone.id;
755
+ delete orderClone.order_number;
756
+ delete orderClone.order_status_url;
757
+ delete orderClone.processed_at;
758
+ delete orderClone.created_at;
759
+ delete orderClone.updated_at;
760
+ delete orderClone.name;
761
+ delete orderClone.number;
762
+ delete orderClone.token;
763
+ delete orderClone.customer;
764
+ delete orderClone.admin_graphql_api_id;
765
+ orderClone.customer = { id: order.customer.id } as any;
766
+ for (const item of orderClone.line_items) {
767
+ delete item.id;
768
+ delete item.fulfillable_quantity;
769
+ delete item.fulfillment_service;
770
+ delete item.fulfillment_status;
771
+ delete item.admin_graphql_api_id;
772
+ }
773
+ for(const shippingLine of orderClone.shipping_lines) {
774
+ delete shippingLine.id;
775
+ }
776
+ const duplicatedOrder = await this.create(orderClone);
777
+ resolve(duplicatedOrder);
778
+ } catch (error) { Logger.error('ShopifyOrderService - duplicate -> Error while duplicating order', error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
779
+ });
780
+ };
781
+
782
+ /**
783
+ * Updates Shipping Address in a order.
784
+ *
785
+ * @param {number} orderId
786
+ * @param {IAddress} shippingAddress
787
+ * @returns Promise
788
+ */
789
+ public updateShippingAddress = async (orderId: number, shippingAddress: IAddress): Promise<IOrder> => {
790
+ return new Promise<IOrder>(async (resolve, reject) => {
791
+ try {
792
+ Logger.info(`ShopifyOrderService - updateShippingAddress -> Updating order shipping address with id ${orderId}`, shippingAddress);
793
+ const response = await this.axiosInstance.put(`/orders/${orderId}.json`, { order: { id: orderId, shipping_address: shippingAddress} });
794
+ resolve(response.data.order);
795
+ } catch (error) { Logger.error('ShopifyOrderService - updateShippingAddress -> Error updating order shipping address', error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
796
+ });
797
+ };
798
+
799
+ public cancel = async (orderId: number): Promise<IOrder> => {
800
+ return new Promise<IOrder>(async (resolve, reject) => {
801
+ try {
802
+ Logger.info('ShopifyOrderService - cancel -> Cancelling order', orderId);
803
+ const response = await this.axiosInstance.post(`orders/${orderId}/cancel.json`, {});
804
+ resolve(response.data.order);
805
+ } catch (error) { Logger.error('ShopifyOrderService - cancel -> Error while cancelling order', error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
806
+ });
807
+ };
808
+
809
+ public applyWeightAndCountryBasedShippingToOrder = async (order: IOrder): Promise<IOrder> => {
810
+ return new Promise<IOrder>(async (resolve, reject) => {
811
+ try {
812
+ const shippingZones = await this.shopifyShopService.getShippingRates();
813
+ const countryCode = this.getCountryCodeFromOrder(order);
814
+ Logger.info(`ShopifyOrderService - applyWeightAndCountryBasedShippingToOrder -> Country code identified ${countryCode}`);
815
+ const orderTotalWeight = order.line_items.reduce((a: number,b: ILineItem) => { if(b.requires_shipping) { return (a + b.quantity * b.grams); } else { return a; }}, 0) / 1000;
816
+ let shippingZoneSelected = shippingZones.find(zone => (zone.countries.map(country => country.code).join(',') + ',').indexOf(`${countryCode},`) > -1);
817
+ if(!shippingZoneSelected) {
818
+ Logger.info(`ShopifyOrderService - applyWeightAndCountryBasedShippingToOrder -> Country code not found on existing specific rates. Looking for Rest of the world rate using * instead of ${countryCode}`);
819
+ shippingZoneSelected = shippingZones.find(zone => (zone.countries.map(country => country.code).join(',') + ',').indexOf('*,') > -1);
820
+ }
821
+ Logger.info(`ShopifyOrderService - applyWeightAndCountryBasedShippingToOrder -> Shipping Zone ${JSON.stringify(shippingZoneSelected)}`);
822
+ if(shippingZoneSelected){
823
+ Logger.info(`ShopifyOrderService - applyWeightAndCountryBasedShippingToOrder -> Shipping Zone found for country ${countryCode}`, shippingZoneSelected);
824
+ if(shippingZoneSelected.weight_based_shipping_rates && shippingZoneSelected.weight_based_shipping_rates.length > 0) {
825
+ const shippingRateSelected = shippingZoneSelected.weight_based_shipping_rates.find(shippingRate => ((shippingRate.weight_low < orderTotalWeight && shippingRate.weight_high > orderTotalWeight) || shippingRate.weight_low === orderTotalWeight));
826
+ if(shippingRateSelected){
827
+ Logger.info('ShopifyOrderService - applyWeightAndCountryBasedShippingToOrder -> Adding SHIPPING RATE to order', shippingRateSelected);
828
+ order.shipping_lines = [{code: shippingZoneSelected.name, title: shippingZoneSelected.name, price: shippingRateSelected.price, source: 'shopify', tax_lines: []}];
829
+ } else {
830
+ Logger.info(`ShopifyOrderService - applyWeightAndCountryBasedShippingToOrder -> No Shipping Rate found for weight ${orderTotalWeight}`);
831
+ }
832
+ } else {
833
+ Logger.info('ShopifyOrderService - applyWeightAndCountryBasedShippingToOrder -> Shipping Zone is not Weight based');
834
+ }
835
+ } else {
836
+ Logger.info(`ShopifyOrderService - applyWeightAndCountryBasedShippingToOrder -> NO Shipping Zone found for country ${countryCode}`);
837
+ }
838
+ resolve(order);
839
+ } catch (error) { Logger.error('ShopifyOrderService - applyWeightAndCountryBasedShippingToOrder -> Error while cancelling order', error); reject(Error(error)); }
840
+ });
841
+ };
842
+
843
+ public getShippingByWeight = async (country: string, weight: number): Promise<IShippingLine> => {
844
+ return new Promise<IShippingLine>(async (resolve, reject) => {
845
+ try {
846
+ let shippingLine: IShippingLine = null;
847
+ const shippingZones = await this.shopifyShopService.getShippingRates();
848
+ const countryCode = CountryCodeService.getCountryCode(country);
849
+ const shippingZoneSelected = shippingZones.find(zone => (zone.countries.map(country => country.code).join(',') + ',').indexOf(countryCode + ',') > -1);
850
+ if(shippingZoneSelected){
851
+ Logger.info(`ShopifyOrderService - getShippingByWeight -> Shipping Zone found for country ${countryCode}`, shippingZoneSelected);
852
+ if(shippingZoneSelected.weight_based_shipping_rates && shippingZoneSelected.weight_based_shipping_rates.length > 0) {
853
+ const shippingRateSelected = shippingZoneSelected.weight_based_shipping_rates.find(shippingRate => (shippingRate.weight_low <= weight && shippingRate.weight_high >= weight));
854
+ if(shippingRateSelected){
855
+ Logger.info('ShopifyOrderService - getShippingByWeight -> Adding SHIPPING RATE to order', shippingRateSelected);
856
+ shippingLine = {code: shippingZoneSelected.name, title: shippingZoneSelected.name, price: shippingRateSelected.price, source: 'shopify', tax_lines: []};
857
+ } else {
858
+ Logger.info(`ShopifyOrderService - getShippingByWeight -> No Shipping Rate found for weight ${weight}`);
859
+ }
860
+ } else {
861
+ Logger.info('ShopifyOrderService - getShippingByWeight -> Shipping Zone is not Weight based');
862
+ }
863
+ } else {
864
+ Logger.info(`ShopifyOrderService - getShippingByWeight -> NO Shipping Zone found for country ${countryCode}`);
865
+ }
866
+ resolve(shippingLine);
867
+ } catch (error) { Logger.error('ShopifyOrderService - getShippingByWeight -> Error while cancelling order', error); reject(Error(error)); }
868
+ });
869
+ };
870
+
871
+ public createLineItemForOrderCalculation = (lineItems : ILineItem[], applyTaxesUsing: 'variantId' | 'price'): ILineItemForDraftCalculation[] => {
872
+ const lineItemsForCalculation: ILineItemForDraftCalculation[] = lineItems.map( (li) => {
873
+ const qty =parseInt(`${li.quantity}`, 10);
874
+ if(li.variant_id && applyTaxesUsing === 'variantId') {
875
+ return { variantId: `gid://shopify/ProductVariant/${li.variant_id}`, quantity: qty };
876
+ } else {
877
+ return { originalUnitPrice: `${li.price}`, taxable: li.taxable ?? false, requiresShipping: true, title: li.title ? li.title : `fake title ${li.price}`, quantity: qty };
878
+ }
879
+ });
880
+ Logger.info(`ShopifyOrderService - createLineItemForOrderCalculation -> LineItems. Original ${JSON.stringify(lineItems)} and final input ${JSON.stringify(lineItemsForCalculation)}`);
881
+ return lineItemsForCalculation;
882
+ };
883
+
884
+ /**
885
+ * Returns taxes & available shippings
886
+ * If variant id is provided and applyTaxesUsing is variantId then the variant is used if not uses the price provided
887
+ *
888
+ * @param {ILineItem[]} lineItems
889
+ * @param {IAddress} address
890
+ * @param {IShippingInput} shipping of the order or null if no shipping
891
+ * @param {'variantId' | 'price'} applyTaxesUsing
892
+ *
893
+ * @returns Promise
894
+ */
895
+ public getAvailableShippingRatesByAddress = async (lineItems: ILineItem[], address: IAddress, shipping: IShippingInput, applyTaxesUsing: 'variantId' | 'price'): Promise<ICalculatedDraftOrder> => {
896
+ return new Promise<ICalculatedDraftOrder>(async (resolve, reject) => {
897
+ try {
898
+ Logger.info(`ShopifyOrderService - getAvailableShippingRatesByAddress: items ${JSON.stringify(lineItems)} and address ${JSON.stringify(address)}`);
899
+
900
+ const countryCode = (!address.country_code || (address.country_code && (address.country_code.toLowerCase() === 'uk' || address.country_code.toLowerCase() === '*'))) ? 'GB' : address.country_code;
901
+ const shippingAddress = { provinceCode: address.province_code, province: address.province, firstName: address.first_name,
902
+ address1: address.address1, address2: address.address2, city: address.city, country: address.country,
903
+ countryCode: countryCode };
904
+
905
+ if(address.zip) {
906
+ (shippingAddress as any).zip = address.zip;
907
+ }
908
+
909
+ const orderInput = { lineItems: this.createLineItemForOrderCalculation(lineItems, applyTaxesUsing), shippingAddress: shippingAddress };
910
+
911
+ if(shipping) { (orderInput as any).shippingLine = shipping; }
912
+
913
+ Logger.info(`ShopifyOrderService - getAvailableShippingRatesByAddress: input variable ${JSON.stringify(orderInput)}`);
914
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(this.draftOrderGraphQLMutation), variables: { input: orderInput } }, { query_cost: 16 });
915
+ const taxLines: ITaxLine[] = this.getOrderTaxLines(response.data.data.draftOrderCalculate);
916
+ let shippingTaxLines: ITaxLine[] = [];
917
+ if(shipping) {
918
+ shippingTaxLines = this.getOrderShippingTaxLines(response.data.data.draftOrderCalculate);
919
+ }
920
+ resolve({ shippingTaxLines: shippingTaxLines, taxLines: taxLines, availableShippings: response.data.data.draftOrderCalculate.calculatedDraftOrder.availableShippingRates, totalTaxes: response.data.data.draftOrderCalculate.calculatedDraftOrder.totalTax} as ICalculatedDraftOrder);
921
+ } catch (error) { Logger.error(`ShopifyOrderService - getAvailableShippingRatesByAddress -> ${error.message}`, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
922
+ });
923
+ };
924
+
925
+ /**
926
+ * If variant id is provided and applyTaxesUsing is variantId then the variant is used if not uses the price provided
927
+ *
928
+ * @param {ILineItem[]} lineItems of the order
929
+ * @param {number} customerId of the order
930
+ * @param {IShippingInput} shipping of the order or null if no shipping
931
+ * @returns Promise
932
+ */
933
+ public getAvailableShippingRatesByCustomerDefaultAddress = async (lineItems: ILineItem[], customerId: number, shipping: IShippingInput, applyTaxesUsing: 'variantId' | 'price'): Promise<ICalculatedDraftOrder> => {
934
+ return new Promise<ICalculatedDraftOrder>(async (resolve, reject) => {
935
+ try {
936
+ Logger.info(`ShopifyOrderService - getAvailableShippingRatesByCustomerDefaultAddress: items ${JSON.stringify(lineItems)} and customer ${customerId}`);
937
+ const orderInput = {
938
+ lineItems: this.createLineItemForOrderCalculation(lineItems, applyTaxesUsing),
939
+ customerId: `gid://shopify/Customer/${customerId}`,
940
+ useCustomerDefaultAddress: true
941
+ };
942
+ if(shipping) { (orderInput as any).shippingLine = shipping; }
943
+ Logger.info(`ShopifyOrderService - getAvailableShippingRatesByCustomerDefaultAddress: input variable ${JSON.stringify(orderInput)}`);
944
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(this.draftOrderGraphQLMutation), variables: { input: orderInput } }, { query_cost: 16 });
945
+ Logger.info(`ShopifyOrderService - getAvailableShippingRatesByCustomerDefaultAddress: calculated draft order ${JSON.stringify(response.data.data.draftOrderCalculate)}`);
946
+ const taxLines: ITaxLine[] = this.getOrderTaxLines(response.data.data.draftOrderCalculate);
947
+ let shippingTaxLines: ITaxLine[] = [];
948
+ if(shipping) {
949
+ shippingTaxLines = this.getOrderShippingTaxLines(response.data.data.draftOrderCalculate);
950
+ }
951
+ resolve({ shippingTaxLines: shippingTaxLines, taxLines: taxLines, availableShippings: response.data.data.draftOrderCalculate.calculatedDraftOrder.availableShippingRates, totalTaxes: response.data.data.draftOrderCalculate.calculatedDraftOrder.totalTax} as ICalculatedDraftOrder);
952
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
953
+ });
954
+ };
955
+
956
+ /**
957
+ * Apply Shipping and taxes to an order.
958
+ * If variant id is provided and applyTaxesUsing is variantId then the variant is used if not uses the price provided.
959
+ *
960
+ * @param {IOrder} order. Order needs to have a customer or a shipping address set
961
+ * @param {'cheapest'|'expensivest'} method
962
+ * @param {boolean} addTaxes
963
+ * @param {'variantId'|'price'} applyTaxesUsing
964
+ * @returns Promise returns order with Shipping lines applied.
965
+ */
966
+ public applyShippingToOrder = async (order: IOrder, method: 'cheapest' | 'expensivest', addTaxes: boolean, applyTaxesUsing: 'variantId' | 'price'): Promise<IOrder> => {
967
+ return new Promise<IOrder>(async (resolve, reject) => {
968
+ try {
969
+ const countryCode = this.getCountryCodeFromOrder(order);
970
+ Logger.info(`ShopifyOrderService - applyShippingToOrder -> Country code identified ${countryCode}`);
971
+ let shippingRates: IAvailableShippingRate[] = [];
972
+ let calculatedDraftOrder: ICalculatedDraftOrder = null;
973
+ if(order.shipping_address) {
974
+ order.shipping_address.country_code = countryCode;
975
+ calculatedDraftOrder = await this.getAvailableShippingRatesByAddress(order.line_items, order.shipping_address, null, applyTaxesUsing);
976
+ } else if(order.customer) {
977
+ calculatedDraftOrder = await this.getAvailableShippingRatesByCustomerDefaultAddress(order.line_items, order.customer.id, null, applyTaxesUsing);
978
+ } else {
979
+ reject(new InspiraShopifyCustomError('Order misses Shipping Address and Customer so noShipping rate can be calculated'));
980
+ }
981
+ shippingRates = calculatedDraftOrder.availableShippings;
982
+ if(shippingRates && shippingRates.length > 0){
983
+ shippingRates.sort((sr1, sr2) => parseFloat(sr1.price.amount) - parseFloat(sr2.price.amount) );
984
+ const shippingRateSelected = method === 'cheapest' ? shippingRates[0] : shippingRates[shippingRates.length - 1];
985
+ Logger.info(`ShopifyOrderService - applyShippingToOrder -> Shipping Rate selected ${JSON.stringify(shippingRateSelected)}`);
986
+ if(shippingRateSelected) {
987
+ order.shipping_lines = [{code: shippingRateSelected.handle, title: shippingRateSelected.title, price: shippingRateSelected.price.amount, source: 'shopify', tax_lines: []}];
988
+ } else {
989
+ Logger.info('ShopifyOrderService - applyShippingToOrder -> No shipping rate found');
990
+ }
991
+ } else {
992
+ Logger.info('ShopifyOrderService - applyShippingToOrder -> NO Shipping Rates are applicable to the order');
993
+ }
994
+ if(addTaxes) {
995
+ order = await this.applyTaxesToOrder(order, applyTaxesUsing);
996
+ }
997
+ resolve(order);
998
+ } catch (error) { Logger.error('ShopifyOrderService - applyShippingToOrder -> Error while cancelling order', error); reject(new InspiraShopifyError(error)); }
999
+ });
1000
+ };
1001
+
1002
+ /**
1003
+ * Apply taxes to an order.
1004
+ * If variant id is provided and applyTaxesUsing is variantId then the variant is used if not uses the price provided.
1005
+ *
1006
+ * @param {IOrder} order. Order needs to have a customer or a shipping address set
1007
+ * @param {'variantId'|'price'} applyTaxesUsing
1008
+ * @returns Promise returns order with taxes applied.
1009
+ */
1010
+ public applyTaxesToOrder = async (order: IOrder, applyTaxesUsing: 'variantId' | 'price' ): Promise<IOrder> => {
1011
+ return new Promise<IOrder>(async (resolve, reject) => {
1012
+ try {
1013
+ const countryCode = this.getCountryCodeFromOrder(order);
1014
+ Logger.info(`ShopifyOrderService -> applyTaxesToOrder -> Country code identified ${countryCode}`);
1015
+ let totalTaxes: number = 0;
1016
+ let taxLines: ITaxLine[] = [];
1017
+ let shippingTaxLines: ITaxLine[] = [];
1018
+ let calculatedDraftOrder = null;
1019
+ if(order.shipping_address) {
1020
+ order.shipping_address.country_code = countryCode;
1021
+ calculatedDraftOrder = await this.getAvailableShippingRatesByAddress(order.line_items, order.shipping_address, this.getShippingInput(order), applyTaxesUsing);
1022
+ } else if(order.customer) {
1023
+ calculatedDraftOrder = await this.getAvailableShippingRatesByCustomerDefaultAddress(order.line_items, order.customer.id, this.getShippingInput(order), applyTaxesUsing);
1024
+ } else {
1025
+ reject(new InspiraShopifyCustomError('ShopifyOrderService -> applyTaxesToOrder -> Order misses Shipping Address and Customer so taxes can be calculated'));
1026
+ }
1027
+ totalTaxes = calculatedDraftOrder.totalTaxes;
1028
+ taxLines = calculatedDraftOrder.taxLines;
1029
+ shippingTaxLines = calculatedDraftOrder.shippingTaxLines;
1030
+ order.total_tax = totalTaxes;
1031
+ order.tax_lines = this.substractShippingFromGlobalTaxLines(taxLines, shippingTaxLines);
1032
+ if(order.shipping_lines && order.shipping_lines.length > 0 && shippingTaxLines.length > 0) {
1033
+ order.shipping_lines[0].tax_lines = shippingTaxLines;
1034
+ }
1035
+ resolve(order);
1036
+ } catch (error) { Logger.error('ShopifyOrderService -> applyTaxesToOrder -> Error while cancelling order', error); reject(new InspiraShopifyError(error)); }
1037
+ });
1038
+ };
1039
+
1040
+ /**
1041
+ *
1042
+ * @param {number} orderId
1043
+ * @param metafield
1044
+ * @returns order with the metafield
1045
+ */
1046
+ public getOrderWithMetafiled = async (orderId: number, metafield: {namespace: string; key: string; }): Promise<IOrderMetafield> => {
1047
+ return new Promise<IOrderMetafield>(async (resolve, reject) => {
1048
+ try {
1049
+ if(metafield && metafield.namespace && metafield.key) {
1050
+ Logger.info(`ShopifyOrderService -> getOrderWithMetafiled -> Get order with metafield: order id ${orderId} and metafield ${JSON.stringify(metafield)}`);
1051
+ const query = gql`{
1052
+ order(id: "${this.getGraphOrderIdFromId(orderId)}") {
1053
+ metafield(namespace: "${metafield.namespace}", key: "${metafield.key}") {
1054
+ value
1055
+ }
1056
+ }
1057
+ }`;
1058
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(query) }, { query_cost: 4 });
1059
+ if(response && response.data && response.data.data && response.data.data.order){
1060
+ const orderGraph = response.data.data.order;
1061
+ resolve({ id: orderId, metafield: { namespace: metafield.namespace, key: metafield.key, value: orderGraph.metafield && orderGraph.metafield.value ? orderGraph.metafield.value : null }});
1062
+ } else {
1063
+ reject(new InspiraShopifyError({ message: JSON.stringify(response.data.errors) }));
1064
+ }
1065
+ } else {
1066
+ Logger.info('ShopifyOrderService -> getOrderWithMetafiled -> No metafield provided');
1067
+ }
1068
+ } catch (error) { Logger.error(error, ErrorHelper.getErrorFromResponse(error)); reject(new InspiraShopifyError(error)); }
1069
+ });
1070
+ };
1071
+
1072
+ private substractShippingFromGlobalTaxLines = (taxLines: ITaxLine[], shippingTaxLines : ITaxLine[]): ITaxLine[] => {
1073
+ if(shippingTaxLines && shippingTaxLines.length > 0) {
1074
+ for(const shippingTaxLine of shippingTaxLines) {
1075
+ const globalTaxLine = taxLines.find((txLine) => txLine.title === shippingTaxLine.title);
1076
+ const globalTxLinePrice = parseFloat(globalTaxLine.price);
1077
+ const shippingTxLinePrice = parseFloat(shippingTaxLine.price);
1078
+ if(globalTxLinePrice > shippingTxLinePrice) {
1079
+ globalTaxLine.price = (parseFloat(globalTaxLine.price) - parseFloat(shippingTaxLine.price)).toFixed(2);
1080
+ }
1081
+ }
1082
+ }
1083
+ return taxLines;
1084
+ };
1085
+
1086
+ private getShippingInput = (order: IOrder): IShippingInput => {
1087
+ try {
1088
+ if(order.shipping_lines && order.shipping_lines.length > 0) {
1089
+ let price = 0;
1090
+ for(const shippingLine of order.shipping_lines) {
1091
+ price += parseFloat(shippingLine.price);
1092
+ }
1093
+ return { price: price.toString(), title: 'Shipping Sum'};
1094
+ } else {
1095
+ return null;
1096
+ }
1097
+ } catch(e) {
1098
+ return null;
1099
+ }
1100
+ };
1101
+
1102
+ private getCountryCodeFromOrder = (order: IOrder): string => {
1103
+ let countryCode = '';
1104
+ let country = '';
1105
+ if(order.shipping_address) {
1106
+ countryCode = order.shipping_address.country_code;
1107
+ country = order.shipping_address.country;
1108
+ }
1109
+ if(!countryCode && !country) {
1110
+ countryCode = 'GB';
1111
+ } else if (!countryCode && country) {
1112
+ countryCode = CountryCodeService.getCountryCode(country);
1113
+ }
1114
+ return countryCode;
1115
+ };
1116
+
1117
+ private getOrderTaxLines = (draftOrderCalculate: IShopifyDraftOrderCalculate): ITaxLine[] => {
1118
+ Logger.info('ShopifyOrderService -> getOrderTaxLines Draft order calculated', JSON.stringify(draftOrderCalculate.calculatedDraftOrder));
1119
+ const taxLines: ITaxLine[] = this.getTaxLines(draftOrderCalculate.calculatedDraftOrder.taxLines);
1120
+ return taxLines;
1121
+ };
1122
+
1123
+ private getOrderShippingTaxLines = (draftOrderCalculate: IShopifyDraftOrderCalculate): ITaxLine[] => {
1124
+ Logger.info('ShopifyOrderService -> getOrderShippingTaxLines Draft order calculated', JSON.stringify(draftOrderCalculate.calculatedDraftOrder));
1125
+ const taxLines: ITaxLine[] = draftOrderCalculate.calculatedDraftOrder.shippingLine &&
1126
+ draftOrderCalculate.calculatedDraftOrder.shippingLine.taxLines &&
1127
+ draftOrderCalculate.calculatedDraftOrder.shippingLine.taxLines.length > 0 ?
1128
+ this.getTaxLines(draftOrderCalculate.calculatedDraftOrder.shippingLine.taxLines) : [];
1129
+ return taxLines;
1130
+ };
1131
+
1132
+ private getTaxLines = (shopifyTaxLines: IShopifyTaxLine[]): ITaxLine[] => {
1133
+ const taxLines: ITaxLine[] = [];
1134
+ for(const taxLine of shopifyTaxLines) {
1135
+ try {
1136
+ taxLines.push({ price: taxLine.priceSet.shopMoney.amount, title: taxLine.title, rate: taxLine.rate });
1137
+ }catch(e) {
1138
+ Logger.error('Error while adding Tax Line', e);
1139
+ }
1140
+ }
1141
+ return taxLines;
1142
+ };
1143
+ }