CentralBackend/services/tax/taxService.js
2025-12-06 11:14:55 +01:00

121 lines
3.6 KiB
JavaScript

const { parse } = require('csv-parse/sync');
const UnitOfWork = require('../../database/UnitOfWork');
const TaxRepository = require('../../repositories/tax/taxRepository');
const HEADERS = [
'Country',
'Super-Reduced Rate (%)',
'Reduced Rate (%)',
'Parking Rate (%)',
'Standard Rate (%)'
];
function parseRate(raw) {
if (!raw) return null;
const cleaned = String(raw).replace(/"/g, '').replace(/%/g, '').trim();
if (!cleaned || cleaned === '-') return null;
const parts = cleaned.split('/').map((p) => p.trim().replace(',', '.'));
const nums = parts.map((p) => Number(p)).filter((n) => !Number.isNaN(n));
if (!nums.length) return null;
return Math.max(...nums);
}
function extractCountry(value) {
const str = String(value || '').trim();
const match = str.match(/^(.*)\((\w{2,3})\)\s*$/);
if (match) return { name: match[1].trim(), code: match[2].trim().toUpperCase() };
return { name: str, code: str.slice(0, 2).toUpperCase() };
}
function normalizeRows(rows) {
return rows.map((row) => {
const country = extractCountry(row['Country']);
return {
country_name: country.name,
country_code: country.code,
rates: {
standard_rate: parseRate(row['Standard Rate (%)']),
reduced_rate: parseRate(row['Reduced Rate (%)']),
super_reduced_rate: parseRate(row['Super-Reduced Rate (%)']),
parking_rate: parseRate(row['Parking Rate (%)'])
}
};
});
}
async function importRates(entries, actorUserId) {
const uow = new UnitOfWork();
await uow.start();
const repo = new TaxRepository(uow);
try {
const uniqueCountries = [];
const seen = new Set();
for (const e of entries) {
if (!seen.has(e.country_code)) {
uniqueCountries.push({ code: e.country_code, name: e.country_name });
seen.add(e.country_code);
}
}
const countryStats = await repo.upsertCountries(uniqueCountries, actorUserId);
const codeMap = await repo.getCountriesByCodes(Array.from(seen));
const payloads = entries
.map((e) => ({ country_id: codeMap[e.country_code]?.id, rates: e.rates }))
.filter((p) => p.country_id);
const rateStats = await repo.bulkImportVatRates(payloads, actorUserId);
await uow.commit();
return {
totalRows: entries.length,
countriesCreated: countryStats.created,
countriesUpdated: countryStats.updated,
ratesUpdated: rateStats.ratesUpdated,
ratesSkipped: rateStats.ratesSkipped
};
} catch (err) {
await uow.rollback();
throw err;
}
}
async function listCurrentRates() {
const uow = new UnitOfWork();
await uow.start();
const repo = new TaxRepository(uow);
try {
const data = await repo.getAllCurrentVatRates();
await uow.commit();
return data;
} catch (err) {
await uow.rollback();
throw err;
}
}
async function getHistory(countryCode) {
const uow = new UnitOfWork();
await uow.start();
const repo = new TaxRepository(uow);
try {
const country = await repo.getCountryByCode(countryCode);
if (!country) throw new Error('Country not found');
const history = await repo.getVatHistory(country.id);
await uow.commit();
return { country, history };
} catch (err) {
await uow.rollback();
throw err;
}
}
module.exports = {
parseCsv(buffer) {
const records = parse(buffer, { columns: true, skip_empty_lines: true, trim: true });
const headers = Object.keys(records[0] || {});
const missing = HEADERS.filter((h) => !headers.includes(h));
if (missing.length) throw new Error(`Missing headers: ${missing.join(', ')}`);
return normalizeRows(records);
},
importRates,
listCurrentRates,
getHistory
};