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