603 lines
20 KiB
JavaScript
603 lines
20 KiB
JavaScript
import moment from 'moment';
|
|
import fetch from 'node-fetch';
|
|
import dotenv from 'dotenv';
|
|
import { models } from './models/index.js';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import cliProgress from 'cli-progress';
|
|
dotenv.config();
|
|
|
|
/**
|
|
* The Endpoint where the Access Token is requested
|
|
*/
|
|
const _accessTokenEndpoint = 'https://idm.staging.floriday.io/oauth2/ausmw6b47z1BnlHkw0h7/v1/token';
|
|
|
|
const BASE_CUSTOMER_URL = 'https://api.staging.floriday.io/customers-api/2022v2/';
|
|
|
|
/**
|
|
* Gets the Access Token from the client config table
|
|
*
|
|
* @param {sequelize.models} models
|
|
* @returns {Date} tokenExpirationDate formated as YYYY-MM-DD HH:mm:ss
|
|
*/
|
|
async function getClientToken() {
|
|
const clientConfigData = await models.clientConfig.findAll();
|
|
|
|
const now = moment().format('YYYY-MM-DD HH:mm:ss');
|
|
const tokenExpirationDate = clientConfigData[0].tokenExpiration;
|
|
|
|
if (clientConfigData[0].tokenExpiration == null || moment(now).isAfter(tokenExpirationDate)) {
|
|
let clientId = clientConfigData[0].clientId;
|
|
let clientSecret = clientConfigData[0].clientSecret;
|
|
|
|
const tokenRequest = await fetch(_accessTokenEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}&scope=role:app catalog:read supply:read organization:read network:write network:read`,
|
|
});
|
|
|
|
const tokenResponse = await tokenRequest.json();
|
|
|
|
if (tokenRequest.status === 200) {
|
|
console.log('Token request successful');
|
|
} else {
|
|
throw new Error(
|
|
`Token request failed with status ${tokenRequest.status}`
|
|
);
|
|
}
|
|
|
|
const accessToken = tokenResponse.access_token;
|
|
let now = moment().format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
let tokenExpirationDate = moment(now)
|
|
.add(tokenResponse.expires_in, 's')
|
|
.format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
await updateClientConfig(
|
|
clientId,
|
|
clientSecret,
|
|
accessToken,
|
|
tokenExpirationDate
|
|
);
|
|
|
|
return tokenExpirationDate;
|
|
} else {
|
|
console.log('Using the current token...');
|
|
return tokenExpirationDate;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the Access Token in the client config table
|
|
*
|
|
* @param {sequelize.models} models
|
|
* @param {String} clientId
|
|
* @param {String} clientSecret
|
|
* @param {String} accessToken
|
|
* @param {String} tokenExpirationDate
|
|
*/
|
|
async function updateClientConfig(clientId, clientSecret, accessToken, tokenExpirationDate) {
|
|
try {
|
|
console.log('Updating the client config with the new token...');
|
|
await models.clientConfig.update(
|
|
{
|
|
clientId: clientId,
|
|
clientSecret: clientSecret,
|
|
currentToken: accessToken,
|
|
tokenExpiration: tokenExpirationDate,
|
|
},
|
|
{
|
|
where: {
|
|
id: 1,
|
|
},
|
|
}
|
|
);
|
|
console.log('Client config updated, new Token set');
|
|
console.log('New token expiration date: ', tokenExpirationDate);
|
|
} catch (error) {
|
|
console.log('There was a error while updating the client config');
|
|
console.log(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* returns the current Access Token
|
|
*
|
|
* @returns
|
|
*/
|
|
async function getJWT() {
|
|
const clientConfigData = await models.clientConfig.findAll();
|
|
return clientConfigData[0].currentToken;
|
|
}
|
|
|
|
/**
|
|
* pauses the execution of the script for the given amount of milliseconds
|
|
*
|
|
* @param {integer} ms
|
|
* @returns A promise that resolves after ms milliseconds.
|
|
*/
|
|
async function sleep(ms) {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, ms);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns the uuidv4 string with the prefix 'Vn-'
|
|
*
|
|
* @param {string} uuidv4 - uuidv4 string
|
|
* @returns
|
|
*/
|
|
async function vnUuid(uuidv4) {
|
|
return 'Vn-' + uuidv4;
|
|
}
|
|
/**
|
|
* Returns the uuidv4 string without the prefix 'Vn-'
|
|
*
|
|
* @param {string} uuidv4 - uuidv4 string with prefix 'Vn-'
|
|
* @returns
|
|
*/
|
|
async function removeVnPrefix(uuidv4) {
|
|
return uuidv4.replace('Vn-', '');
|
|
}
|
|
|
|
/**
|
|
* Recieves an array of functions and executes them in a queue with the given concurrency
|
|
*
|
|
* @param {Array} fnArray
|
|
* @param {Number} concurrency
|
|
* @returns
|
|
*/
|
|
async function asyncQueue(fnArray, concurrency = 1) {
|
|
|
|
const results = []; // 1
|
|
|
|
const queue = fnArray.map((fn, index) => () => fn().then((result) => results[index] = result));
|
|
|
|
const run = async () => { // 2
|
|
const fn = queue.shift();
|
|
if (fn) {
|
|
await fn();
|
|
await run();
|
|
}
|
|
};
|
|
|
|
const promises = []; // 3
|
|
while (concurrency--) { // 4
|
|
promises.push(run());
|
|
}
|
|
|
|
await Promise.all(promises); // 5
|
|
|
|
return results;
|
|
|
|
}
|
|
// 1. Create an array of functions that will be executed in a queue
|
|
// 2. Create a function that will execute the functions in the queue
|
|
// 3. Create an array of promises that will execute the run function
|
|
// 4. Execute the run function while the concurrency is greater than 0
|
|
// 5. Return the results
|
|
|
|
|
|
/**
|
|
*
|
|
* Inserts every tradeitem from the organization into the DB
|
|
*
|
|
* @param {*} organizationGln
|
|
*/
|
|
async function getTradeitems(organizationGln) {
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + await getJWT(),
|
|
'X-Api-Key': process.env.API_KEY,
|
|
};
|
|
|
|
// Get the organization id from the organizationGln
|
|
|
|
const organizationsUrl = `${BASE_CUSTOMER_URL}organizations/gln/${organizationGln}`;
|
|
const organizationsRequest = await fetch(organizationsUrl, {
|
|
method: 'GET',
|
|
headers: headers,
|
|
});
|
|
|
|
const organizationsResponse = await organizationsRequest.json();
|
|
|
|
const organizationId = organizationsResponse.organizationId;
|
|
console.log('Organization ID: ', organizationId);
|
|
|
|
// Get the tradeitems from the organization
|
|
|
|
const tradeitemsUrl = `${BASE_CUSTOMER_URL}trade-items?supplierOrganizationId=${organizationId}`;
|
|
const tradeitemsRequest = await fetch(tradeitemsUrl, {
|
|
method: 'GET',
|
|
headers: headers,
|
|
});
|
|
|
|
const tradeitemsResponse = await tradeitemsRequest.json();
|
|
|
|
// If there is no tradeitems
|
|
if (tradeitemsResponse.length == 0) {
|
|
return;
|
|
}
|
|
|
|
// if there is at least one tradeitem, save it in the database
|
|
|
|
//console.log(tradeitemsResponse[0]);
|
|
|
|
try {
|
|
|
|
if (tradeitemsResponse.length > 0 && tradeitemsResponse != null) {
|
|
|
|
let bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_grey);
|
|
bar.start(tradeitemsResponse.length, 0);
|
|
|
|
await tradeitemsResponse.forEach(async item => {
|
|
|
|
bar.increment();
|
|
|
|
try {
|
|
|
|
item.tradeItemId = await vnUuid(item.tradeItemId);
|
|
|
|
await models.suppliers.upsert({
|
|
supplierId: await vnUuid(organizationId),
|
|
supplierGln: organizationGln,
|
|
});
|
|
|
|
await models.tradeItem.upsert({
|
|
tradeItemId: item.tradeItemId,
|
|
supplierOrganizationId: organizationGln,
|
|
supplierOrganizationUuid: await vnUuid(organizationId),
|
|
code: item.code,
|
|
gtin: item.gtin,
|
|
vbnProductCode: item.vbnProductCode,
|
|
name: item.name,
|
|
isDeleted: item.isDeleted,
|
|
sequenceNumber: item.sequenceNumber,
|
|
tradeItemVersion: item.tradeItemVersion,
|
|
isCustomerSpecific: item.isCustomerSpecific,
|
|
isHiddenInCatalog: item.isHiddenInCatalog,
|
|
});
|
|
|
|
await item.characteristics.forEach((characteristic) => {
|
|
models.characteristics.upsert({
|
|
tradeItemFk: item.tradeItemId,
|
|
vbnCode: characteristic.vbnCode,
|
|
vbnValueCode: characteristic.vbnValueCode,
|
|
});
|
|
});
|
|
|
|
await item.seasonalPeriods.forEach((seasonalPeriod) => {
|
|
models.seasonalPeriod.upsert({
|
|
tradeItemFk: item.tradeItemId,
|
|
startWeek: seasonalPeriod.startWeek,
|
|
endWeek: seasonalPeriod.endWeek,
|
|
});
|
|
|
|
});
|
|
|
|
await item.photos.forEach(async (photo) => {
|
|
|
|
photo.id = await vnUuid(photo.id);
|
|
|
|
models.photos.upsert({
|
|
tradeItemFk: item.tradeItemId,
|
|
photoId: photo.id,
|
|
url: photo.url,
|
|
seasonalPeriodFk: JSON.stringify(photo.seasonalPeriod),
|
|
type: photo.type,
|
|
primary: photo.primary,
|
|
});
|
|
});
|
|
|
|
await item.packingConfigurations.forEach(async (packagingConfiguration) => {
|
|
|
|
let uuid = uuidv4();
|
|
uuid = await vnUuid(uuid);
|
|
|
|
await models.packingConfigurations.upsert({
|
|
tradeItemFk: item.tradeItemId,
|
|
packingConfigurationId: uuid,
|
|
piecesPerPackage: packagingConfiguration.piecesPerPackage,
|
|
bunchesPerPackage: packagingConfiguration.bunchesPerPackage,
|
|
photoUrl: packagingConfiguration.photoUrl,
|
|
packagesPerLayer: packagingConfiguration.packagesPerLayer,
|
|
layersPerLoadCarrier: packagingConfiguration.layersPerLoadCarrier,
|
|
transportHeightInCm: packagingConfiguration.transportHeightInCm,
|
|
loadCarrierType: packagingConfiguration.loadCarrierType,
|
|
isPrimary: packagingConfiguration.isPrimary,
|
|
additionalPricePerPiece: JSON.stringify(packagingConfiguration.additionalPricePerPiece),
|
|
});
|
|
|
|
await models.package.upsert({
|
|
packingConfigurationsId: uuid,
|
|
vbnPackageCode: packagingConfiguration.package.vbnPackageCode,
|
|
vbnPackageValueCode: packagingConfiguration.package.vbnPackageValueCode,
|
|
});
|
|
|
|
});
|
|
|
|
await item.botanicalNames.forEach((botanicalName) => {
|
|
models.botanicalNames.upsert({
|
|
tradeItemFk: item.tradeItemId,
|
|
name: botanicalName,
|
|
});
|
|
});
|
|
|
|
await item.countryOfOriginIsoCodes.forEach((countryOfOriginIsoCode) => {
|
|
models.countryOfOriginIsoCodes.upsert({
|
|
tradeItemFk: item.tradeItemId,
|
|
isoCode: countryOfOriginIsoCode,
|
|
});
|
|
});
|
|
|
|
} catch (error) {
|
|
console.log('Missing data for the tradeitem: ' + item.tradeItemId);
|
|
bar.stop();
|
|
return;
|
|
}
|
|
|
|
});
|
|
bar.stop();
|
|
}
|
|
|
|
} catch (error) {
|
|
console.log('There was an error while saving the data to the database, trying again in 5 seconds...');
|
|
await sleep(5000);
|
|
await getTradeitems(organizationGln);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a single tradeitem from the API and inserts it into the database
|
|
*
|
|
* @param {*} tradeItemId
|
|
*/
|
|
async function getTradeItem(tradeItemId) {
|
|
|
|
try {
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${await getJWT()}`,
|
|
'X-Api-Key': process.env.API_KEY,
|
|
};
|
|
|
|
const tradeitemUrl = `${BASE_CUSTOMER_URL}trade-items/${tradeItemId}`;
|
|
|
|
const tradeitemRequest = await fetch(tradeitemUrl, {
|
|
method: 'GET',
|
|
headers: headers,
|
|
});
|
|
|
|
const tradeitemResponse = await tradeitemRequest.json();
|
|
|
|
tradeitemResponse.tradeItemId = await vnUuid(tradeitemResponse.tradeItemId);
|
|
|
|
await models.tradeItem.upsert({
|
|
tradeItemId: tradeitemResponse.tradeItemId,
|
|
supplierOrganizationId: tradeitemResponse.supplierOrganizationGln,
|
|
supplierOrganizationUuid: await vnUuid(tradeitemResponse.supplierOrganizationId),
|
|
code: tradeitemResponse.code,
|
|
gtin: tradeitemResponse.gtin,
|
|
vbnProductCode: tradeitemResponse.vbnProductCode,
|
|
name: tradeitemResponse.name,
|
|
isDeleted: tradeitemResponse.isDeleted,
|
|
sequenceNumber: tradeitemResponse.sequenceNumber,
|
|
tradeItemVersion: tradeitemResponse.tradeItemVersion,
|
|
isCustomerSpecific: tradeitemResponse.isCustomerSpecific,
|
|
isHiddenInCatalog: tradeitemResponse.isHiddenInCatalog,
|
|
});
|
|
|
|
await tradeitemResponse.characteristics.forEach((characteristic) => {
|
|
models.characteristics.upsert({
|
|
tradeItemFk: tradeitemResponse.tradeItemId,
|
|
vbnCode: characteristic.vbnCode,
|
|
vbnValueCode: characteristic.vbnValueCode,
|
|
});
|
|
});
|
|
|
|
await tradeitemResponse.seasonalPeriods.forEach((seasonalPeriod) => {
|
|
models.seasonalPeriod.upsert({
|
|
tradeItemFk: tradeitemResponse.tradeItemId,
|
|
startWeek: seasonalPeriod.startWeek,
|
|
endWeek: seasonalPeriod.endWeek,
|
|
});
|
|
|
|
});
|
|
|
|
await tradeitemResponse.photos.forEach(async (photo) => {
|
|
|
|
photo.id = await vnUuid(photo.id);
|
|
|
|
models.photos.upsert({
|
|
tradeItemFk: tradeitemResponse.tradeItemId,
|
|
photoId: photo.id,
|
|
url: photo.url,
|
|
seasonalPeriodFk: JSON.stringify(photo.seasonalPeriod),
|
|
type: photo.type,
|
|
primary: photo.primary,
|
|
});
|
|
});
|
|
|
|
await tradeitemResponse.packingConfigurations.forEach(async (packagingConfiguration) => {
|
|
|
|
let uuid = uuidv4();
|
|
uuid = await vnUuid(uuid);
|
|
|
|
await models.packingConfigurations.upsert({
|
|
tradeItemFk: tradeitemResponse.tradeItemId,
|
|
packingConfigurationId: uuid,
|
|
piecesPerPackage: packagingConfiguration.piecesPerPackage,
|
|
bunchesPerPackage: packagingConfiguration.bunchesPerPackage,
|
|
photoUrl: packagingConfiguration.photoUrl,
|
|
packagesPerLayer: packagingConfiguration.packagesPerLayer,
|
|
layersPerLoadCarrier: packagingConfiguration.layersPerLoadCarrier,
|
|
transportHeightInCm: packagingConfiguration.transportHeightInCm,
|
|
loadCarrierType: packagingConfiguration.loadCarrierType,
|
|
isPrimary: packagingConfiguration.isPrimary,
|
|
additionalPricePerPiece: JSON.stringify(packagingConfiguration.additionalPricePerPiece),
|
|
});
|
|
|
|
await models.package.upsert({
|
|
packingConfigurationsId: uuid,
|
|
vbnPackageCode: packagingConfiguration.package.vbnPackageCode,
|
|
vbnPackageValueCode: packagingConfiguration.package.vbnPackageValueCode,
|
|
});
|
|
|
|
});
|
|
|
|
await tradeitemResponse.botanicalNames.forEach((botanicalName) => {
|
|
models.botanicalNames.upsert({
|
|
tradeItemFk: tradeitemResponse.tradeItemId,
|
|
name: botanicalName,
|
|
});
|
|
});
|
|
|
|
await tradeitemResponse.countryOfOriginIsoCodes.forEach((countryOfOriginIsoCode) => {
|
|
models.countryOfOriginIsoCodes.upsert({
|
|
tradeItemFk: tradeitemResponse.tradeItemId,
|
|
isoCode: countryOfOriginIsoCode,
|
|
});
|
|
});
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.log('There was an error while saving the data to the database, trying again in 5 seconds...');
|
|
console.log(error.message);
|
|
await sleep(5000);
|
|
await getTradeItem(tradeItemId);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Gets the stock for all suppliers
|
|
*
|
|
* Does this by getting all suppliers and then calling the getStockBySupplier function for each supplier
|
|
*/
|
|
async function getStock() {
|
|
|
|
const suppliers = await models.suppliers.findAll();
|
|
|
|
const supplierQueue = [];
|
|
|
|
suppliers.forEach(async (supplier) => {
|
|
supplierQueue.push(await getStockBySupplier(supplier.supplierId));
|
|
});
|
|
|
|
await asyncQueue(supplierQueue);
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns the stock for a given supplier
|
|
*
|
|
* @param {*} supplierId
|
|
* @returns void - inserts the stock into the database
|
|
*/
|
|
async function getStockBySupplier(supplierId) {
|
|
|
|
supplierId = await removeVnPrefix(supplierId);
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${await getJWT()}`,
|
|
'X-Api-Key': process.env.API_KEY,
|
|
};
|
|
|
|
const stockUrl = `${BASE_CUSTOMER_URL}supply-lines?supplierOrganizationId=${supplierId}`;
|
|
|
|
let stockRequest;
|
|
|
|
try {
|
|
stockRequest = await fetch(stockUrl, {
|
|
method: 'GET',
|
|
headers: headers,
|
|
});
|
|
} catch (error) {
|
|
console.log(error.message);
|
|
return;
|
|
}
|
|
|
|
const stockResponse = await stockRequest.json();
|
|
|
|
if (stockResponse.length == 0) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
|
|
if (stockResponse.length > 0 && stockResponse != null) {
|
|
|
|
let bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_grey);
|
|
bar.start(stockResponse.length, 0);
|
|
|
|
await stockResponse.forEach(async supplyLine => {
|
|
|
|
bar.increment();
|
|
|
|
let alreadyExists = await models.supplyLines.findOne({
|
|
where: {
|
|
supplyLineId: supplyLine.supplyLineId,
|
|
}
|
|
});
|
|
|
|
if (alreadyExists) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
|
|
await models.supplyLines.upsert({
|
|
supplyLineId: supplyLine.supplyLineId,
|
|
status: supplyLine.status,
|
|
supplierOrganizationId: await vnUuid(supplyLine.supplierOrganizationId),
|
|
pricePerPiece: supplyLine.pricePerPiece,
|
|
numberOfPieces: supplyLine.numberOfPieces,
|
|
deliveryPeriod: supplyLine.deliveryPeriod,
|
|
orderPeriod: supplyLine.orderPeriod,
|
|
warehouseId: await vnUuid(supplyLine.warehouseId),
|
|
sequenceNumber: supplyLine.sequenceNumber,
|
|
type: supplyLine.type,
|
|
isDeleted: supplyLine.isDeleted,
|
|
salesUnit: supplyLine.salesUnit,
|
|
agreementReferenceCode: supplyLine.agreementReference.code,
|
|
agreementReferenceDescription: supplyLine.agreementReference.description,
|
|
isLimited: supplyLine.isLimited,
|
|
isCustomerSpecific: supplyLine.isCustomerSpecific,
|
|
tradeItemFk: await vnUuid(supplyLine.tradeItemId),
|
|
});
|
|
|
|
supplyLine.volumePrices.forEach(async (volumePrice) => {
|
|
await models.volumePrices.upsert({
|
|
supplyLineFk: supplyLine.supplyLineId,
|
|
unit: volumePrice.unit,
|
|
pricePerPiece: volumePrice.pricePerPiece,
|
|
});
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
if (error.name == 'SequelizeForeignKeyConstraintError') {
|
|
bar.stop();
|
|
console.log('Missing data for the tradeitem: ' + supplyLine.tradeItemId);
|
|
console.log('Trying to get the tradeitem data...');
|
|
await getTradeItem(supplyLine.tradeItemId);
|
|
return;
|
|
}
|
|
}
|
|
|
|
});
|
|
bar.stop();
|
|
}
|
|
} catch (error) {
|
|
console.log('There was an error while saving the data to the database, trying again in 5 seconds...');
|
|
await sleep(5000);
|
|
await getStockBySupplier(supplierId);
|
|
}
|
|
|
|
}
|
|
|
|
export { getClientToken, updateClientConfig, getJWT, getTradeitems, sleep, asyncQueue, getStock }; |