@sommpicks/sommpicks-shopify 24.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Logger.ts +18 -0
- package/README.md +258 -0
- package/addTypings.sh +2 -0
- package/bitbucket-pipelines.yml +38 -0
- package/index.ts +132 -0
- package/package.json +57 -0
- package/publish.sh +20 -0
- package/services/CacheWrapper.ts +30 -0
- package/services/CountryCodeService.ts +507 -0
- package/shopify/ShopifyAppService.ts +109 -0
- package/shopify/ShopifyAssetService.ts +20 -0
- package/shopify/ShopifyBillingService.ts +73 -0
- package/shopify/ShopifyCartTrasnformationService.ts +207 -0
- package/shopify/ShopifyCollectionService.ts +523 -0
- package/shopify/ShopifyCustomerService.ts +472 -0
- package/shopify/ShopifyDeliveryCustomisationService.ts +220 -0
- package/shopify/ShopifyDiscountService.ts +131 -0
- package/shopify/ShopifyDraftOrderService.ts +125 -0
- package/shopify/ShopifyFulfillmentService.ts +41 -0
- package/shopify/ShopifyFunctionsProductDiscountsService.ts +166 -0
- package/shopify/ShopifyInventoryService.ts +415 -0
- package/shopify/ShopifyLocationService.ts +29 -0
- package/shopify/ShopifyOrderRefundsService.ts +138 -0
- package/shopify/ShopifyOrderRiskService.ts +19 -0
- package/shopify/ShopifyOrderService.ts +1143 -0
- package/shopify/ShopifyPageService.ts +62 -0
- package/shopify/ShopifyProductService.ts +772 -0
- package/shopify/ShopifyShippingZonesService.ts +37 -0
- package/shopify/ShopifyShopService.ts +101 -0
- package/shopify/ShopifyTemplateService.ts +30 -0
- package/shopify/ShopifyThemeService.ts +33 -0
- package/shopify/ShopifyUtils.ts +56 -0
- package/shopify/ShopifyWebhookService.ts +110 -0
- package/shopify/base/APIVersion.ts +4 -0
- package/shopify/base/AbstractService.ts +152 -0
- package/shopify/base/ErrorHelper.ts +24 -0
- package/shopify/errors/InspiraShopifyCustomError.ts +7 -0
- package/shopify/errors/InspiraShopifyError.ts +15 -0
- package/shopify/errors/InspiraShopifyUnableToReserveInventoryError.ts +7 -0
- package/shopify/helpers/ShopifyProductServiceHelper.ts +450 -0
- package/shopify/product/ShopifyProductCountService.ts +110 -0
- package/shopify/product/ShopifyProductListService.ts +333 -0
- package/shopify/product/ShopifyProductMetafieldsService.ts +405 -0
- package/shopify/product/ShopifyProductPublicationsService.ts +112 -0
- package/shopify/product/ShopifyVariantService.ts +584 -0
- package/shopify/router/ShopifyMandatoryRouter.ts +37 -0
- package/shopify/router/ShopifyRouter.ts +85 -0
- package/shopify/router/ShopifyRouterBis.ts +85 -0
- package/shopify/router/ShopifyRouterBisBis.ts +85 -0
- package/shopify/router/ShopifyRouterBisBisBis.ts +85 -0
- package/shopify/router/ShopifyRouterBisBisBisBis.ts +85 -0
- package/shopify/router/WebhookSkipMiddleware.ts +73 -0
- package/shopify/router/services/CryptoService.ts +26 -0
- package/shopify/router/services/HmacValidator.ts +36 -0
- package/shopify/router/services/OauthService.ts +17 -0
- package/shopify/router/services/RestUtils.ts +13 -0
- package/shopify/router/services/rateLimiter/MemoryStores.ts +46 -0
- package/shopify/router/services/rateLimiter/StoreRateLimiter.ts +46 -0
- package/test/README.md +223 -0
- package/test/router/ShopifyRouter.test.ts +71 -0
- package/test/router/WebhookSkipMiddleware.test.ts +86 -0
- package/test/router/services/HmacValidator.test.ts +24 -0
- package/test/router/services/RestUtils.test.ts +13 -0
- package/test/router/services/rateLimiter/StoreRateLimiter.test.ts +62 -0
- package/test/services/CacheWrapper.test.ts +30 -0
- package/test/shopify/ShopifyOrderService.test.ts +29 -0
- package/test/shopify/ShopifyProductService.test.ts +118 -0
- package/test/shopify/ShopifyWebhookService.test.ts +105 -0
- package/tsconfig.json +10 -0
- package/typings/axios.d.ts +8 -0
- package/typings/index.d.ts +1682 -0
|
@@ -0,0 +1,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
|
+
}
|