test: refs #8361 enhance exchangeRateUpdate specs with additional scenarios #3340
|
@ -158,13 +158,13 @@ INSERT INTO `account`.`mailForward`(`account`, `forwardTo`)
|
|||
|
||||
|
||||
|
||||
INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`)
|
||||
INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`, `hasToDownloadRate`)
|
||||
VALUES
|
||||
(1, 'EUR', 'Euro', 1),
|
||||
(2, 'USD', 'Dollar USA', 1.4),
|
||||
(3, 'GBP', 'Libra', 1),
|
||||
(4, 'JPY', 'Yen Japones', 1),
|
||||
(5, 'CNY', 'Yuan Chino', 1.2);
|
||||
(1, 'EUR', 'Euro', 1, FALSE),
|
||||
(2, 'USD', 'Dollar USA', 1.4, TRUE),
|
||||
(3, 'GBP', 'Libra', 1, TRUE),
|
||||
(4, 'JPY', 'Yen Japones', 1, FALSE),
|
||||
(5, 'CNY', 'Yuan Chino', 1.2, TRUE);
|
||||
|
||||
INSERT INTO `vn`.`country`(`id`, `name`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`)
|
||||
VALUES
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE `vn`.`currency`
|
||||
ADD COLUMN `hasToDownloadRate` TINYINT(1) NOT NULL DEFAULT 0 comment 'Si se guarda el tipo de cambio diariamente en referenceRate';
|
|
@ -0,0 +1,3 @@
|
|||
UPDATE `vn`.`currency`
|
||||
SET `hasToDownloadRate` = TRUE
|
||||
WHERE `code` IN ('USD', 'CNY', 'GBP');
|
|
@ -13,66 +13,114 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.exchangeRateUpdate = async() => {
|
||||
Self.exchangeRateUpdate = async(options = {}) => {
|
||||
const models = Self.app.models;
|
||||
const myOptions = {};
|
||||
let tx;
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
jgallego marked this conversation as resolved
Outdated
|
||||
if (!myOptions.transaction) {
|
||||
tx = await Self.beginTransaction({});
|
||||
myOptions.transaction = tx;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml');
|
||||
const xmlData = response.data;
|
||||
|
||||
const doc = new DOMParser({errorHandler: {warning: () => {}}})?.parseFromString(xmlData, 'text/xml');
|
||||
const doc = new DOMParser({errorHandler: {warning: () => {}}})
|
||||
.parseFromString(xmlData, 'text/xml');
|
||||
const cubes = doc?.getElementsByTagName('Cube');
|
||||
if (!cubes || cubes.length === 0)
|
||||
throw new UserError('No cubes found. Exiting the method.');
|
||||
|
||||
const models = Self.app.models;
|
||||
|
||||
const maxDateRecord = await models.ReferenceRate.findOne({order: 'dated DESC'});
|
||||
|
||||
const currencies = await models.Currency.find({where: {hasToDownloadRate: true}}, myOptions);
|
||||
const maxDateRecord = await models.ReferenceRate.findOne({order: 'dated DESC'}, myOptions);
|
||||
const maxDate = maxDateRecord?.dated ? new Date(maxDateRecord.dated) : null;
|
||||
let lastProcessedDate = maxDate;
|
||||
|
||||
for (const cube of Array.from(cubes)) {
|
||||
if (cube.nodeType === doc.ELEMENT_NODE && cube.attributes.getNamedItem('time')) {
|
||||
const xmlDate = new Date(cube.getAttribute('time'));
|
||||
const xmlDateWithoutTime = new Date(xmlDate.getFullYear(), xmlDate.getMonth(), xmlDate.getDate());
|
||||
if (!maxDate || maxDate < xmlDateWithoutTime) {
|
||||
for (const rateCube of Array.from(cube.childNodes)) {
|
||||
if (rateCube.nodeType === doc.ELEMENT_NODE) {
|
||||
const currencyCode = rateCube.getAttribute('currency');
|
||||
const rate = rateCube.getAttribute('rate');
|
||||
if (['USD', 'CNY', 'GBP'].includes(currencyCode)) {
|
||||
const currency = await models.Currency.findOne({where: {code: currencyCode}});
|
||||
if (!currency) throw new UserError(`Currency not found for code: ${currencyCode}`);
|
||||
const existingRate = await models.ReferenceRate.findOne({
|
||||
where: {currencyFk: currency.id, dated: xmlDate}
|
||||
});
|
||||
const xmlDateWithoutTime = new Date(
|
||||
xmlDate.getFullYear(),
|
||||
xmlDate.getMonth(),
|
||||
xmlDate.getDate()
|
||||
);
|
||||
|
||||
if (existingRate) {
|
||||
if (existingRate.value !== rate)
|
||||
await existingRate.updateAttributes({value: rate});
|
||||
} else {
|
||||
await models.ReferenceRate.create({
|
||||
currencyFk: currency.id,
|
||||
dated: xmlDate,
|
||||
value: rate
|
||||
});
|
||||
}
|
||||
const monday = 1;
|
||||
if (xmlDateWithoutTime.getDay() === monday) {
|
||||
const saturday = new Date(xmlDateWithoutTime);
|
||||
saturday.setDate(xmlDateWithoutTime.getDate() - 2);
|
||||
const sunday = new Date(xmlDateWithoutTime);
|
||||
sunday.setDate(xmlDateWithoutTime.getDate() - 1);
|
||||
|
||||
for (const date of [saturday, sunday]) {
|
||||
await models.ReferenceRate.upsertWithWhere(
|
||||
{currencyFk: currency.id, dated: date},
|
||||
{currencyFk: currency.id, dated: date, value: rate}
|
||||
if (!maxDate || xmlDateWithoutTime > maxDate) {
|
||||
if (lastProcessedDate && xmlDateWithoutTime > lastProcessedDate) {
|
||||
carlosap
commented
Se puede hacer dinámico (no se si vale la pena) haciendo un DISTINCT currencyFk de la tabla referenceRate Se puede hacer dinámico (no se si vale la pena) haciendo un DISTINCT currencyFk de la tabla referenceRate
jgallego
commented
No se quieren importar todas las monedas que hay en currency No se quieren importar todas las monedas que hay en currency
carlosap
commented
Añadiendo una columna a la tabla currencyFk o bien una nueva tabla de 'configuración' Añadiendo una columna a la tabla currencyFk o bien una nueva tabla de 'configuración'
Más que nada porque si en un futuro se requiere añadir o eliminar una moneda no tener que cambiar código
|
||||
for (const currency of currencies) {
|
||||
await fillMissingDates(
|
||||
models, currency, lastProcessedDate, xmlDateWithoutTime, myOptions
|
||||
);
|
||||
jgallego marked this conversation as resolved
alexm
commented
Falta la traduccio (si se vol) Falta la traduccio (si se vol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const rateCube of Array.from(cube.childNodes)) {
|
||||
if (rateCube.nodeType === doc.ELEMENT_NODE) {
|
||||
const currencyCode = rateCube.getAttribute('currency');
|
||||
const rate = rateCube.getAttribute('rate');
|
||||
const currency = currencies.find(c => c.code === currencyCode);
|
||||
if (currency) {
|
||||
const existingRate = await models.ReferenceRate.findOne({
|
||||
where: {currencyFk: currency.id, dated: xmlDateWithoutTime}
|
||||
}, myOptions);
|
||||
|
||||
if (existingRate) {
|
||||
if (existingRate.value !== rate)
|
||||
await existingRate.updateAttributes({value: rate}, myOptions);
|
||||
} else {
|
||||
await models.ReferenceRate.create({
|
||||
currencyFk: currency.id,
|
||||
dated: xmlDateWithoutTime,
|
||||
value: rate
|
||||
}, myOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastProcessedDate = xmlDateWithoutTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (tx) await tx.commit();
|
||||
} catch (error) {
|
||||
if (tx) await tx.rollback();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
async function getLastValidRate(models, currencyId, date, myOptions) {
|
||||
return models.ReferenceRate.findOne({
|
||||
where: {currencyFk: currencyId, dated: {lt: date}},
|
||||
order: 'dated DESC'
|
||||
jgallego marked this conversation as resolved
alexm
commented
Aixina despres es if (tx) await tx.commit(); Aixina despres es if (tx) await tx.commit();
|
||||
}, myOptions);
|
||||
}
|
||||
|
||||
async function fillMissingDates(models, currency, startDate, endDate, myOptions) {
|
||||
const cursor = new Date(startDate);
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
while (cursor < endDate) {
|
||||
const existingRate = await models.ReferenceRate.findOne({
|
||||
where: {currencyFk: currency.id, dated: cursor}
|
||||
}, myOptions);
|
||||
|
||||
if (!existingRate) {
|
||||
const lastValid = await getLastValidRate(models, currency.id, cursor, myOptions);
|
||||
if (lastValid) {
|
||||
await models.ReferenceRate.create({
|
||||
currencyFk: currency.id,
|
||||
dated: new Date(cursor),
|
||||
value: lastValid.value
|
||||
}, myOptions);
|
||||
}
|
||||
}
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,52 +1,190 @@
|
|||
describe('exchangeRateUpdate functionality', function() {
|
||||
const axios = require('axios');
|
||||
const models = require('vn-loopback/server/server').models;
|
||||
let tx; let options;
|
||||
|
||||
beforeEach(function() {
|
||||
spyOn(axios, 'get').and.returnValue(Promise.resolve({
|
||||
data: `<Cube>
|
||||
<Cube time='2024-04-12'>
|
||||
function formatYmd(d) {
|
||||
const mm = (d.getMonth() + 1).toString().padStart(2, '0');
|
||||
const dd = d.getDate().toString().padStart(2, '0');
|
||||
return `${d.getFullYear()}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
afterEach(async() => {
|
||||
await tx.rollback();
|
||||
});
|
||||
|
||||
beforeEach(async() => {
|
||||
tx = await models.Sale.beginTransaction({});
|
||||
options = {transaction: tx};
|
||||
spyOn(axios, 'get').and.returnValue(Promise.resolve({data: ''}));
|
||||
});
|
||||
|
||||
it('should process XML data and create rates', async function() {
|
||||
const d1 = Date.vnNew();
|
||||
const d4 = Date.vnNew();
|
||||
d4.setDate(d4.getDate() + 1);
|
||||
const xml = `<Cube>
|
||||
<Cube time='${formatYmd(d1)}'>
|
||||
<Cube currency='USD' rate='1.1'/>
|
||||
<Cube currency='CNY' rate='1.2'/>
|
||||
</Cube>
|
||||
</Cube>`
|
||||
}));
|
||||
});
|
||||
|
||||
it('should process XML data and update or create rates in the database', async function() {
|
||||
<Cube time='${formatYmd(d4)}'>
|
||||
<Cube currency='USD' rate='1.3'/>
|
||||
</Cube>
|
||||
</Cube>`;
|
||||
axios.get.and.returnValue(Promise.resolve({data: xml}));
|
||||
spyOn(models.ReferenceRate, 'findOne').and.returnValue(Promise.resolve(null));
|
||||
spyOn(models.ReferenceRate, 'create').and.returnValue(Promise.resolve());
|
||||
await models.InvoiceIn.exchangeRateUpdate(options);
|
||||
|
||||
await models.InvoiceIn.exchangeRateUpdate();
|
||||
|
||||
expect(models.ReferenceRate.create).toHaveBeenCalledTimes(2);
|
||||
expect(models.ReferenceRate.create).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should not create or update rates when no XML data is available', async function() {
|
||||
it('should handle no data', async function() {
|
||||
axios.get.and.returnValue(Promise.resolve({}));
|
||||
spyOn(models.ReferenceRate, 'create');
|
||||
|
||||
let thrownError = null;
|
||||
let e;
|
||||
try {
|
||||
await models.InvoiceIn.exchangeRateUpdate();
|
||||
} catch (error) {
|
||||
thrownError = error;
|
||||
await models.InvoiceIn.exchangeRateUpdate(options);
|
||||
} catch (err) {
|
||||
e = err;
|
||||
}
|
||||
|
||||
expect(thrownError.message).toBe('No cubes found. Exiting the method.');
|
||||
expect(e.message).toBe('No cubes found. Exiting the method.');
|
||||
expect(models.ReferenceRate.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async function() {
|
||||
it('should handle errors', async function() {
|
||||
axios.get.and.returnValue(Promise.reject(new Error('Network error')));
|
||||
let error;
|
||||
|
||||
let e;
|
||||
try {
|
||||
await models.InvoiceIn.exchangeRateUpdate();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
await models.InvoiceIn.exchangeRateUpdate(options);
|
||||
} catch (err) {
|
||||
e = err;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe('Network error');
|
||||
expect(e).toBeDefined();
|
||||
expect(e.message).toBe('Network error');
|
||||
});
|
||||
|
||||
it('should update existing rate', async function() {
|
||||
const existingRate = await models.ReferenceRate.findOne({
|
||||
order: 'id DESC'
|
||||
}, options);
|
||||
|
||||
if (!existingRate) return fail('No ReferenceRate records in DB');
|
||||
|
||||
const currency = await models.Currency.findById(existingRate.currencyFk, null, options);
|
||||
|
||||
const xml = `<Cube>
|
||||
<Cube time='${formatYmd(existingRate.dated)}'>
|
||||
<Cube currency='${currency.code}' rate='2.22'/>
|
||||
</Cube>
|
||||
</Cube>`;
|
||||
|
||||
axios.get.and.returnValue(Promise.resolve({data: xml}));
|
||||
|
||||
await models.InvoiceIn.exchangeRateUpdate(options);
|
||||
|
||||
const updatedRate = await models.ReferenceRate.findById(existingRate.id, null, options);
|
||||
|
||||
expect(updatedRate.value).toBeCloseTo('2.22');
|
||||
});
|
||||
|
||||
it('should not update if same rate', async function() {
|
||||
const existingRate = await models.ReferenceRate.findOne({order: 'id DESC'}, options);
|
||||
if (!existingRate) return fail('No existing ReferenceRate in DB');
|
||||
|
||||
const currency = await models.Currency.findById(existingRate.currencyFk, null, options);
|
||||
|
||||
const oldValue = existingRate.value;
|
||||
const xml = `<Cube>
|
||||
<Cube time='${formatYmd(existingRate.dated)}'>
|
||||
<Cube currency='${currency.code}' rate='${oldValue}'/>
|
||||
</Cube>
|
||||
</Cube>`;
|
||||
|
||||
axios.get.and.returnValue(Promise.resolve({data: xml}));
|
||||
|
||||
await models.InvoiceIn.exchangeRateUpdate(options);
|
||||
|
||||
const updatedRate = await models.ReferenceRate.findById(existingRate.id, null, options);
|
||||
|
||||
expect(updatedRate.value).toBe(oldValue);
|
||||
});
|
||||
|
||||
it('should backfill missing dates', async function() {
|
||||
const lastRate = await models.ReferenceRate.findOne({order: 'dated DESC'}, options);
|
||||
if (!lastRate) return fail('No existing ReferenceRate data in DB');
|
||||
|
||||
const currency = await models.Currency.findById(lastRate.currencyFk, null, options);
|
||||
|
||||
const d1 = new Date(lastRate.dated);
|
||||
d1.setDate(d1.getDate() + 1);
|
||||
const d4 = new Date(lastRate.dated);
|
||||
d4.setDate(d4.getDate() + 4);
|
||||
|
||||
const xml = `<Cube>
|
||||
<Cube time='${formatYmd(d1)}'>
|
||||
<Cube currency='${currency.code}' rate='1.0'/>
|
||||
</Cube>
|
||||
<Cube time='${formatYmd(d4)}'>
|
||||
<Cube currency='${currency.code}' rate='2.0'/>
|
||||
</Cube>
|
||||
</Cube>`;
|
||||
|
||||
axios.get.and.returnValue(Promise.resolve({data: xml}));
|
||||
|
||||
const beforeCount = await models.ReferenceRate.count({}, options);
|
||||
await models.InvoiceIn.exchangeRateUpdate(options);
|
||||
const afterCount = await models.ReferenceRate.count({}, options);
|
||||
|
||||
expect(afterCount - beforeCount).toBe(4);
|
||||
});
|
||||
|
||||
it('should create entries for day1 and day2 from the feed, and not backfill day3', async function() {
|
||||
const lastRate = await models.ReferenceRate.findOne({order: 'dated DESC'}, options);
|
||||
if (!lastRate) return fail('No existing ReferenceRate data in DB');
|
||||
|
||||
const currency = await models.Currency.findById(lastRate.currencyFk, null, options);
|
||||
if (!currency) return fail(`No currency for ID ${lastRate.currencyFk}`);
|
||||
|
||||
const day1 = new Date(lastRate.dated);
|
||||
day1.setDate(day1.getDate() + 1);
|
||||
|
||||
const day2 = new Date(lastRate.dated);
|
||||
day2.setDate(day2.getDate() + 2);
|
||||
|
||||
const day3 = new Date(lastRate.dated);
|
||||
day3.setDate(day3.getDate() + 3);
|
||||
|
||||
const xml = `<Cube>
|
||||
<Cube time='${formatYmd(day1)}'>
|
||||
<Cube currency='${currency.code}' rate='1.1'/>
|
||||
</Cube>
|
||||
<Cube time='${formatYmd(day2)}'>
|
||||
<Cube currency='${currency.code}' rate='2.2'/>
|
||||
</Cube>
|
||||
</Cube>`;
|
||||
|
||||
axios.get.and.returnValue(Promise.resolve({data: xml}));
|
||||
|
||||
await models.InvoiceIn.exchangeRateUpdate(options);
|
||||
|
||||
const day3Record = await models.ReferenceRate.findOne({
|
||||
where: {currencyFk: currency.id, dated: day3}
|
||||
}, options);
|
||||
|
||||
expect(day3Record).toBeNull();
|
||||
|
||||
const day1Record = await models.ReferenceRate.findOne({
|
||||
where: {currencyFk: currency.id, dated: day1}
|
||||
}, options);
|
||||
const day2Record = await models.ReferenceRate.findOne({
|
||||
where: {currencyFk: currency.id, dated: day2}
|
||||
}, options);
|
||||
|
||||
expect(day1Record.value).toBeCloseTo('1.1');
|
||||
expect(day2Record.value).toBeCloseTo('2.2');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,9 @@
|
|||
},
|
||||
"ratio": {
|
||||
"type": "number"
|
||||
},
|
||||
"hasToDownloadRate": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
|
|
Loading…
Reference in New Issue
Soles gastar esta forma
Podriem gastar esta forma:
i despres fer
await myOptions.transaction?.commit();