test: refs #8361 enhance exchangeRateUpdate specs with additional scenarios
gitea/salix/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Javi Gallego 2025-01-07 10:45:51 +01:00
parent b1dda9f82b
commit 73d5d508ce
2 changed files with 217 additions and 65 deletions

View File

@ -13,66 +13,126 @@ module.exports = Self => {
}
});
Self.exchangeRateUpdate = async() => {
Self.exchangeRateUpdate = async(options = {}) => {
const models = Self.app.models;
const myOptions = {};
Object.assign(myOptions, options);
let createdTx = false;
if (!myOptions.transaction) {
myOptions.transaction = await Self.beginTransaction({});
createdTx = true;
}
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 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) {
const xmlDateWithoutTime = new Date(
xmlDate.getFullYear(),
xmlDate.getMonth(),
xmlDate.getDate()
);
if (!maxDate || xmlDateWithoutTime > maxDate) {
if (lastProcessedDate && xmlDateWithoutTime > lastProcessedDate) {
for (const code of ['USD', 'CNY', 'GBP']) {
const currency = await models.Currency.findOne(
{where: {code}},
myOptions
);
if (!currency)
throw new UserError(`Currency not found for code: ${code}`);
await fillMissingDates(
models, currency, lastProcessedDate, xmlDateWithoutTime, myOptions
);
}
}
}
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 currency = await models.Currency.findOne(
{where: {code: currencyCode}},
myOptions
);
if (!currency)
throw new UserError(`Currency not found for code: ${currencyCode}`);
const existingRate = await models.ReferenceRate.findOne({
where: {currencyFk: currency.id, dated: xmlDate}
});
where: {currencyFk: currency.id, dated: xmlDateWithoutTime}
}, myOptions);
if (existingRate) {
if (existingRate.value !== rate)
await existingRate.updateAttributes({value: rate});
await existingRate.updateAttributes({value: rate}, myOptions);
} else {
await models.ReferenceRate.create({
currencyFk: currency.id,
dated: xmlDate,
dated: xmlDateWithoutTime,
value: rate
});
}, myOptions);
}
}
}
}
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}
);
}
}
lastProcessedDate = xmlDateWithoutTime;
}
}
if (createdTx)
await myOptions.transaction.commit();
} catch (error) {
if (createdTx)
await myOptions.transaction.rollback();
throw error;
}
};
async function getLastValidRate(models, currencyId, date, myOptions) {
return models.ReferenceRate.findOne({
where: {currencyFk: currencyId, dated: {lt: date}},
order: 'dated DESC'
}, 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);
}
}
};
};

View File

@ -1,52 +1,144 @@
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);
});
});