refs #4823 Many changes

This commit is contained in:
Guillermo Bonet 2023-05-15 14:19:43 +02:00
parent 2a195194b2
commit 7202a0c339
14 changed files with 285 additions and 221 deletions

View File

@ -56,8 +56,12 @@ SYNC_SUPPLIER = true
SYNC_CONN = true
SYNC_TRADEITEM = true
MAX_REQUEST_RETRIES = 3
#DEV OPTIONS
SUPPLIERS_ALWAYS_CONN = false
```
## Guidelines
- In case a new model is created, it should follow the following structure:

View File

@ -1,4 +1,4 @@
import { models, checkConn, closeConn } from './models/sequelize.js';
import { checkConn, closeConn } from './models/sequelize.js';
import * as utils from './utils.js';
import moment from 'moment';
import chalk from 'chalk';
@ -10,7 +10,7 @@ class Floriday {
async start() {
try {
await utils.checkConfig();
this.tokenExpirationDate = await utils.requestToken(models);
await utils.requestToken();
if (JSON.parse(env.SYNC_SEQUENCE)) await utils.syncSequence();
if (JSON.parse(env.SYNC_SUPPLIER)) await utils.syncSuppliers();
if (JSON.parse(env.SYNC_CONN)) await utils.syncConn();
@ -36,8 +36,8 @@ class Floriday {
const intervalTime = JSON.parse(env.IS_PRODUCTION)
? env.MS_PRODUCTION_SCHEDULE
: env.MS_TEST_SCHEDULE;
while (true) {
this.continueSchedule = true;
while (this.continueSchedule) {
try {
await this.trunk();
await new Promise(resolve => setTimeout(resolve, intervalTime));
@ -64,6 +64,7 @@ class Floriday {
}
async stop() {
this.continueSchedule = false;
await utils.deleteConnections();
await closeConn();
console.warn(chalk.dim('Bye, come back soon 👋'))

View File

@ -10,7 +10,7 @@ const connections = {
export default (sequelize) => {
const connection = sequelize.define(
'connections',
'supplier_connections',
connections,
{
timestamps: false,

View File

@ -1,59 +1,71 @@
import { Sequelize } from 'sequelize';
const suppliers = {
supplierOrganizationId: {
type: Sequelize.STRING,
allowNull: false,
primaryKey: true,
},
sequenceNumber: {
type: Sequelize.INTEGER,
allowNull: false,
primaryKey: true,
},
companyGln: {
type: Sequelize.STRING,
},
name: {
type: Sequelize.STRING,
},
commercialName: {
type: Sequelize.STRING,
},
email: {
type: Sequelize.STRING,
},
phone: {
type: Sequelize.STRING,
},
website: {
type: Sequelize.STRING,
},
rfhRelationId: {
type: Sequelize.INTEGER,
},
paymentProviders: {
type: Sequelize.STRING,
},
endDate: {
type: Sequelize.DATE,
},
mailingAddress: {
type: Sequelize.JSON,
},
physicalAddress: {
type: Sequelize.JSON,
},
isConnected: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
supplierOrganizationId: {
type: Sequelize.STRING,
allowNull: false,
primaryKey: true,
},
sequenceNumber: {
type: Sequelize.INTEGER,
allowNull: false,
},
companyGln: {
type: Sequelize.STRING,
},
name: {
type: Sequelize.STRING,
},
commercialName: {
type: Sequelize.STRING,
},
email: {
type: Sequelize.STRING,
},
phone: {
type: Sequelize.STRING,
},
website: {
type: Sequelize.STRING,
},
rfhRelationId: {
type: Sequelize.INTEGER,
},
paymentProviders: {
type: Sequelize.JSON,
},
endDate: {
type: Sequelize.DATE,
},
mailingAddress: {
type: Sequelize.JSON,
},
physicalAddress: {
type: Sequelize.JSON,
},
isConnected: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
lastSync: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
},
created: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
},
};
export default (sequelize) => {
const Suppliers = sequelize.define('supplier', suppliers, {
timestamps: false,
freezeTableName: true,
});
return Suppliers;
const Suppliers = sequelize.define(
'supplier',
suppliers, {
timestamps: false,
freezeTableName: true,
}
);
return Suppliers;
};

View File

@ -55,15 +55,32 @@ const supplyLine = {
},
isCustomerSpecific: {
type: Sequelize.BOOLEAN,
}
},
tradeItemId : {
type: Sequelize.STRING,
},
supplierOrganizationId : {
type: Sequelize.STRING,
},
lastSync: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
},
created: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
},
};
export default (sequelize) => {
const SupplyLine = sequelize.define('supplyLine', supplyLine, {
timestamps: false,
freezeTableName: true,
});
return SupplyLine;
const SupplyLine = sequelize.define(
'supplyLine',
supplyLine, {
timestamps: false,
freezeTableName: true,
}
);
return SupplyLine;
};

View File

@ -1,24 +1,27 @@
import { Sequelize } from 'sequelize';
const volumePrices = {
supplyLineId: {
type: Sequelize.STRING,
primaryKey: true,
},
unit: {
type: Sequelize.STRING,
primaryKey: true,
},
pricePerPiece: {
type: Sequelize.DECIMAL(10,2),
primaryKey: true,
},
supplyLineId: {
type: Sequelize.STRING,
primaryKey: true,
},
unit: {
type: Sequelize.STRING,
primaryKey: true,
},
pricePerPiece: {
type: Sequelize.DECIMAL(10,2),
primaryKey: true,
},
};
export default (sequelize) => {
const VolumePrices = sequelize.define('volumePrices', volumePrices, {
timestamps: false,
freezeTableName: true,
});
return VolumePrices;
const VolumePrices = sequelize.define(
'supplyLine_volumePrices',
volumePrices, {
timestamps: false,
freezeTableName: true,
}
);
return VolumePrices;
};

View File

@ -1,15 +1,18 @@
import { Sequelize } from 'sequelize';
const botanicalNames = {
name: {
type: Sequelize.STRING,
},
name: {
type: Sequelize.STRING,
},
};
export default (sequelize) => {
const BotanicalNames = sequelize.define('botanicalNames', botanicalNames, {
timestamps: false,
freezeTableName: true,
});
return BotanicalNames;
const BotanicalNames = sequelize.define(
'tradeItem_botanicalNames',
botanicalNames, {
timestamps: false,
freezeTableName: true,
}
);
return BotanicalNames;
};

View File

@ -11,7 +11,7 @@ const characteristics = {
export default (sequelize) => {
const Characteristics = sequelize.define(
'characteristics',
'tradeItem_characteristics',
characteristics,
{
timestamps: false,

View File

@ -8,7 +8,7 @@ const countryOfOriginIsoCodes = {
export default (sequelize) => {
const CountryOfOriginIsoCodes = sequelize.define(
'countryOfOriginIsoCodes',
'tradeItem_countryOfOriginIsoCodes',
countryOfOriginIsoCodes,
{
timestamps: false,

View File

@ -20,7 +20,10 @@ const packingConfigurations = {
layersPerLoadCarrier: {
type: Sequelize.INTEGER,
},
additionalPricePerPiece: {
additionalPricePerPiece_currency: {
type: Sequelize.STRING,
},
additionalPricePerPiece_value: {
type: Sequelize.STRING,
},
transportHeightInCm: {
@ -36,7 +39,7 @@ const packingConfigurations = {
export default (sequelize) => {
const PackingConfigurations = sequelize.define(
'packingConfig',
'tradeItem_packingConfigurations',
packingConfigurations,
{
timestamps: false,

View File

@ -17,7 +17,7 @@ const photos = {
};
export default (sequelize) => {
const Photos = sequelize.define('photos', photos, {
const Photos = sequelize.define('tradeItem_photos', photos, {
timestamps: false,
freezeTableName: true,
});

View File

@ -15,7 +15,7 @@ const seasonalPeriod = {
};
export default (sequelize) => {
const SeasonalPeriod = sequelize.define('seasonalPeriod', seasonalPeriod, {
const SeasonalPeriod = sequelize.define('tradeItem_seasonalPeriod', seasonalPeriod, {
timestamps: false,
freezeTableName: true,
});

View File

@ -5,9 +5,6 @@ const tradeItem = {
type: Sequelize.STRING,
primaryKey: true,
},
supplierOrganizationId: {
type: Sequelize.STRING
},
code: {
type: Sequelize.STRING
},
@ -34,7 +31,20 @@ const tradeItem = {
},
isHiddenInCatalog: {
type: Sequelize.BOOLEAN,
}
},
supplierOrganizationId: {
type: Sequelize.STRING
},
lastSync: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
},
created: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
},
};
export default (sequelize) => {

249
utils.js
View File

@ -8,12 +8,11 @@ import ora from 'ora';
const env = process.env;
/**
* Gets the Access Token from the client config table
* Gets the Access Token
*
* @param {sequelize.models} models
* @returns {Date} tokenExpirationDate formated as YYYY-MM-DD HH:mm:ss
* @param {Boolean} isForce Force to request new token
*/
export async function requestToken() {
export async function requestToken(isForce = false) {
let spinner = ora(`Requesting new token...`).start();
try {
const clientConfigData = await models.clientConfig.findOne();
@ -24,7 +23,7 @@ export async function requestToken() {
tokenExpirationDate = clientConfigData.tokenExpiration;
}
if (!token || !tokenExpirationDate || moment().isAfter(tokenExpirationDate)) {
if (isForce || !token || !tokenExpirationDate || moment().isAfter(tokenExpirationDate)) {
let clientId, clientSecret
if (JSON.parse(env.USE_SECRETS_DB)) {
clientId = clientConfigData.clientId;
@ -45,11 +44,9 @@ export async function requestToken() {
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
.join('&')
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
}
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
const response = (await vnRequest('GET', env.API_ENDPOINT, data, headers)).data;
const response = (await vnRequest('POST', env.API_ENDPOINT, data, headers)).data;
if (response.statusText = 'OK')
spinner.succeed();
@ -67,13 +64,8 @@ export async function requestToken() {
response.access_token,
tokenExpirationDate
);
return tokenExpirationDate;
} else {
spinner.text = 'Using stored token...'
spinner.succeed();
return tokenExpirationDate;
}
} else
spinner.succeed('Using stored token...');
} catch (err) {
spinner.fail();
throw err;
@ -86,10 +78,7 @@ export async function requestToken() {
* @returns {string}
*/
export async function getCurrentToken() {
if (moment().isAfter(await getCurrentTokenExpiration()))
return await requestToken(models);
else
return (await models.clientConfig.findOne()).currentToken;
return (await models.clientConfig.findOne()).currentToken;
}
/**
@ -289,19 +278,10 @@ export async function syncSuppliers(){
if (timeFinish)
spinner.text = spinner.text + ` (${timeLeft})`
await models.supplier.upsert({
...supplier,
supplierOrganizationId: supplier.organizationId,
sequenceNumber: supplier.sequenceNumber,
companyGln: supplier.companyGln ? supplier.companyGln : null,
name: supplier.name ? supplier.name : null,
commercialName: supplier.commercialName ? supplier.commercialName : null,
email: supplier.email ? supplier.email : null,
phone: supplier.phone ? supplier.phone : null,
website: supplier.website ? supplier.website : null,
rfhRelationId: supplier.rfhRelationId ? supplier.rfhRelationId : null,
paymentProviders: supplier.paymentProviders.length ? `${supplier.paymentProviders}` : null,
endDate: supplier.endDate ? supplier.endDate : null,
mailingAddress: supplier.mailingAddress ? supplier.mailingAddress : null,
physicalAddress: supplier.physicalAddress ? supplier.physicalAddress : null,
isConnected: JSON.parse(env.SUPPLIERS_ALWAYS_CONN),
lastSync: moment(),
});
};
await syncSequence(curSequenceNumber, 'supplier', maxSequenceNumber);
@ -337,7 +317,7 @@ export async function syncConn(){
let i = 1;
for (let connection of connections){
spinner.text = `Creating ${i++} connections...`
spinner.text = `Creating ${i++} of ${connections.length} connections...`
let remoteConnection = remoteConnections.find(remoteConnection => remoteConnection == connection.supplierOrganizationId);
if (!remoteConnection){
@ -378,8 +358,7 @@ export async function syncTradeItems(){
const suppliers = await models.supplier.findAll({
where: { isConnected: true }
});
let i = 0;
let x = 0;
let i = 0, x = 1;
for (let supplier of suppliers) {
let headers = {
'Content-Type': 'application/json',
@ -394,7 +373,7 @@ export async function syncTradeItems(){
for (let tradeItem of tradeItems) {
await insertItem(tradeItem);
spinner.text = `Syncing ${i++} trade items of [${x++}|${suppliers.length}] suppliers...`
spinner.text = `Syncing ${i++} trade items of [${x}|${suppliers.length}] suppliers...`
};
} catch (err) {
spinner.fail();
@ -452,8 +431,6 @@ export async function syncSupplyLines(){
resolve(supplyLines);
} catch (err) {
if (err.message = 'Request failed with status code 429')
console.log('Request failed with status code 429 - Too many request');
resolve([]);
}
});
@ -505,26 +482,8 @@ export async function syncSupplyLines(){
}
await models.supplyLine.upsert({
supplyLineId: line.supplyLineId,
status: line.status,
supplierOrganizationId: line.supplierOrganizationId,
pricePerPiece_currency: line.pricePerPiece.currency,
pricePerPiece_value: line.pricePerPiece.value,
numberOfPieces: line.numberOfPieces,
deliveryPeriod_startDateTime: line.deliveryPeriod.startDateTime,
deliveryPeriod_endDateTime: line.deliveryPeriod.endDateTime,
orderPeriod_startDateTime: line.orderPeriod.startDateTime,
orderPeriod_endDateTime: line.orderPeriod.endDateTime,
warehouseId: line.warehouseId,
sequenceNumber: line.sequenceNumber,
type: line.type,
isDeleted: line.isDeleted,
salesUnit: line.salesUnit,
agreementReference_code: line.agreementReference.code ? line.agreementReference.code : null,
agreementReference_description: line.agreementReference.description ? line.agreementReference.description : null,
isLimited: line.isLimited,
isCustomerSpecific: line.isCustomerSpecific,
tradeItemId: line.tradeItemId,
...supplyLine,
lastSync: moment(),
});
for (let volumePrice of line.volumePrices)
@ -574,17 +533,46 @@ export async function newSyncSupplyLines() {
});
}).map(supplier => supplier.supplierOrganizationId);
let i = 1;
let i = 0, x = 1;
for (let supplier of suppliers) {
spinner.text = `(NEW) Syncing ${i++} supply lines...`
spinner.text = `(NEW) Syncing ${i} supply lines of [${x++}|${suppliers.length}] suppliers...`
const params = new URLSearchParams({
supplierOrganizationId: supplier,
}).toString();
let supplyLines = (await vnRequest('GET',`${env.API_URL}/supply-lines?${params}`, null, headers)).data;
if (!supplyLines.length) continue
for (let supplyLine of supplyLines)
console.log(supplyLine)
for (let supplyLine of supplyLines) {
let tradeItem = await models.tradeItem.findOne({
where: { tradeItemId: supplyLine.tradeItemId }
});
if (!tradeItem) {
let tradeItem = (await vnRequest('GET', `${env.API_URL}/trade-items?tradeItemIds=${supplyLine.tradeItemId}`, null, headers)).data;
insertItem(tradeItem[0])
}
spinner.text = `(NEW) Syncing ${i++} supply lines of [${x}|${suppliers.length}] suppliers...`
await models.supplyLine.upsert({
...supplyLine,
pricePerPiece_currency: supplyLine.pricePerPiece ? supplyLine.pricePerPiece.currency : null,
pricePerPiece_value: supplyLine.pricePerPiece ? supplyLine.pricePerPiece.value : null,
deliveryPeriod_startDateTime: supplyLine.deliveryPeriod ? supplyLine.deliveryPeriod.startDateTime : null,
deliveryPeriod_endDateTime: supplyLine.deliveryPeriod ? supplyLine.deliveryPeriod.endDateTime : null,
orderPeriod_startDateTime: supplyLine.orderPeriod ? supplyLine.orderPeriod.startDateTime : null,
orderPeriod_endDateTime: supplyLine.orderPeriod ? supplyLine.orderPeriod.endDateTime : null,
agreementReference_code: supplyLine.agreementReference ? supplyLine.agreementReference.code : null,
agreementReference_description: supplyLine.agreementReference ? supplyLine.agreementReference.description : null,
lastSync: moment(),
});
for (let volumePrice of supplyLine.volumePrices)
await models.volumePrices.upsert({
...volumePrice,
supplyLineId: supplyLine.supplyLineId,
});
}
}
spinner.succeed();
}
@ -612,72 +600,77 @@ export async function insertItem(tradeItem) {
// Upsert trade item
await models.tradeItem.upsert({
...tradeItem,
isDeleted: tradeItem.isDeleted,
isCustomerSpecific: tradeItem.isCustomerSpecific,
isHiddenInCatalog: tradeItem.isHiddenInCatalog,
lastSync: moment(),
}, { transaction: tx });
// Upsert characteristics
if (tradeItem.characteristics)
for (const characteristic of tradeItem.characteristics) {
await models.characteristic.upsert({
...characteristic,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
if (tradeItem.characteristics.length)
for (const characteristic of tradeItem.characteristics) {
await models.characteristic.upsert({
...characteristic,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
// Upsert seasonal periods
if (tradeItem.seasonalPeriods)
for (const seasonalPeriod of tradeItem.seasonalPeriods) {
await models.seasonalPeriod.upsert({
...seasonalPeriod,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
if (tradeItem.seasonalPeriods.length)
for (const seasonalPeriod of tradeItem.seasonalPeriods) {
await models.seasonalPeriod.upsert({
...seasonalPeriod,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
// Upsert photos
if (tradeItem.photos)
for (const photo of tradeItem.photos) {
await models.photo.upsert({
...photo,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
if (tradeItem.photos.length)
for (const photo of tradeItem.photos) {
await models.photo.upsert({
...photo,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
// Upsert packing configurations
if (tradeItem.packingConfigurations)
for (const packingConfiguration of tradeItem.packingConfigurations) {
const uuid = uuidv4();
if (tradeItem.packingConfigurations.length)
for (const packingConfiguration of tradeItem.packingConfigurations) {
const uuid = uuidv4();
await models.packingConfiguration.upsert({
packingConfigurationId: uuid,
...packingConfiguration,
additionalPricePerPiece: JSON.stringify(packingConfiguration.additionalPricePerPiece),
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
await models.packingConfiguration.upsert({
packingConfigurationId: uuid,
...packingConfiguration,
additionalPricePerPiece_currency: packingConfiguration.additionalPricePerPiece.currency,
additionalPricePerPiece_value: packingConfiguration.additionalPricePerPiece.value,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
await models.package.upsert({
...packingConfiguration.package,
packingConfigurationFk: uuid,
}, { transaction: tx });
}
await models.package.upsert({
...packingConfiguration.package,
packingConfigurationFk: uuid,
}, { transaction: tx });
}
// Upsert country of origin ISO codes
if (tradeItem.countryOfOriginIsoCodes)
for (const isoCode of tradeItem.countryOfOriginIsoCodes || []) {
await models.countryOfOriginIsoCode.upsert({
isoCode,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
if (tradeItem.countryOfOriginIsoCodes.length)
for (const isoCode of tradeItem.countryOfOriginIsoCodes || []) {
await models.countryOfOriginIsoCode.upsert({
isoCode,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
// Upsert botanical names
if (tradeItem.botanicalNames)
for (const botanicalName of tradeItem.botanicalNames) {
await models.botanicalName.upsert({
...botanicalName,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
if (tradeItem.botanicalNames.length)
for (const botanicalName of tradeItem.botanicalNames) {
await models.botanicalName.upsert({
name: botanicalName,
tradeItemId: tradeItem.tradeItemId,
}, { transaction: tx });
}
await tx.commit();
} catch (err) {
@ -720,24 +713,42 @@ export async function deleteConnections() {
*
* @return {array}
**/
export async function vnRequest(method = 'GET', url, data = null, headers = {}) {
let response;
export async function vnRequest(method, url, data, headers) {
for(let i = 0; i < env.MAX_REQUEST_RETRIES; i++) {
try {
if (['GET', 'DELETE'].includes(method))
response = await axios({method, url, headers});
return await axios({method, url, headers});
else
response = await axios({method, url, data, headers});
break;
return await axios({method, url, data, headers});
} catch (err) {
if (err.code == 'ECONNRESET')
warning(err)
else
criticalError(err)
await sleep(1000)
switch (err.code) {
case 'ECONNRESET': // Client network socket TLS
warning(err);
await sleep(1000);
break;
case 'ECONNABORTED':
case 'ECONNREFUSED':
case 'ERR_BAD_REQUEST':
switch (err.response.status) {
case 429: // Too Many Requests
warning(err);
await sleep(3400); // Stipulated by floryday
case 401: // Unauthorized
warning(err);
await requestToken(true);
headers.Authorization ?
headers.Authorization = `Bearer ${await getCurrentToken()}` :
criticalError(err);
break;
default:
criticalError(err);
}
break;
default:
criticalError(err);
}
}
}
return response;
}
/**