@openade/pel
Version:
Punto di Elaborazione (Elaboration Point) - Server library for managing PEMs and communicating with ADE
277 lines • 12.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.PELServer = void 0;
const express_1 = __importStar(require("express"));
const uuid_1 = require("uuid");
class PELServer {
constructor(config) {
this.storage = config.storage;
this.database = config.database;
this.adeClient = config.adeClient;
this.port = config.port || 4000;
this.app = (0, express_1.default)();
this.router = (0, express_1.Router)();
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
this.app.use(express_1.default.json({ limit: '10mb' }));
this.app.use(express_1.default.urlencoded({ extended: true }));
this.app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
}
setupRoutes() {
this.router.get('/api/session/seed', async (req, res) => {
try {
const sessionId = (0, uuid_1.v4)();
const seed = this.generateSeed();
await this.storage.store(`seeds/${sessionId}.json`, new TextEncoder().encode(JSON.stringify({ sessionId, seed, timestamp: new Date().toISOString() })));
res.json({ sessionId, seed });
}
catch (error) {
console.error('Error generating session seed:', error);
res.status(500).json({ error: 'Failed to generate session seed' });
}
});
this.router.post('/api/document', async (req, res) => {
try {
const document = req.body;
if (!this.validateDocument(document)) {
return res.status(400).json({ error: 'Invalid document' });
}
const messageId = (0, uuid_1.v4)();
const receiveTime = new Date().toISOString();
const docDate = document.datiGenerali.dataOra.substring(0, 10);
const docNumber = document.datiGenerali.numero;
const path = `documents/${document.identificativoPEM}/${docDate}/${docNumber}.xml`;
await this.storage.store(path, new TextEncoder().encode(JSON.stringify(document)));
await this.database.saveDocument(document);
const emitTime = new Date(document.datiGenerali.dataOra).getTime();
const receiveTimeMs = new Date(receiveTime).getTime();
const timeDiff = receiveTimeMs - emitTime;
console.log(`Document received: ${docNumber} (transmission time: ${timeDiff}ms)`);
res.json({ messageId, received: receiveTime });
}
catch (error) {
console.error('Error receiving document:', error);
res.status(500).json({ error: 'Failed to save document' });
}
});
this.router.post('/api/journal', async (req, res) => {
try {
const journal = req.body;
if (!this.validateJournal(journal)) {
return res.status(400).json({ error: 'Invalid journal' });
}
const messageId = (0, uuid_1.v4)();
const isValid = await this.verifyJournalIntegrity(journal);
if (!isValid) {
console.error('Journal integrity check failed');
await this.reportAnomaly({
type: 'JOURNAL_INTEGRITY_ERROR',
pemId: journal.identificativoPEM,
details: `Journal for ${journal.dataRiferimento} failed integrity check`,
timestamp: new Date().toISOString(),
});
}
const path = `journals/${journal.identificativoPEM}/${journal.dataRiferimento}/${journal.dataOraGenerazione.replace(/:/g, '-')}.xml`;
await this.storage.store(path, new TextEncoder().encode(JSON.stringify(journal)));
await this.database.saveJournal(journal);
this.generateDailyReceipts(journal).catch((err) => console.error('Error generating daily receipts:', err));
res.json({ messageId, status: 'received' });
}
catch (error) {
console.error('Error receiving journal:', error);
res.status(500).json({ error: 'Failed to save journal' });
}
});
this.router.post('/api/anomaly', async (req, res) => {
try {
const anomaly = req.body;
await this.reportAnomaly(anomaly);
res.json({ status: 'recorded' });
}
catch (error) {
console.error('Error recording anomaly:', error);
res.status(500).json({ error: 'Failed to record anomaly' });
}
});
this.app.use(this.router);
}
generateSeed() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let seed = '';
for (let i = 0; i < 32; i++) {
seed += chars.charAt(Math.floor(Math.random() * chars.length));
}
return seed;
}
validateDocument(doc) {
return !!(doc.datiGenerali?.numero &&
doc.datiGenerali?.dataOra &&
doc.identificativoPEM &&
doc.contribuente &&
doc.dettaglioLinee &&
doc.datiRiepilogo);
}
validateJournal(journal) {
return !!(journal.versione &&
journal.identificativoPEM &&
journal.dataRiferimento &&
journal.dataOraGenerazione &&
journal.voci &&
Array.isArray(journal.voci));
}
async verifyJournalIntegrity(journal) {
if (!journal.voci || journal.voci.length === 0) {
console.warn('Journal has no entries to verify');
return false;
}
try {
for (let i = 0; i < journal.voci.length; i++) {
const entry = journal.voci[i];
if (entry.numeroProgressivo !== i + 1) {
console.error(`Journal entry numbering error at ${i}: expected ${i + 1}, got ${entry.numeroProgressivo}`);
return false;
}
if (!entry.dataOra || !entry.tipo || entry.importo === undefined) {
console.error(`Journal entry ${i} missing required fields`);
return false;
}
}
const calculatedTotal = journal.voci.reduce((sum, entry) => sum + entry.importo, 0);
const expectedTotal = journal.importoTotaleGiornata;
if (Math.abs(calculatedTotal - expectedTotal) > 0.01) {
console.error(`Journal total mismatch: calculated=${calculatedTotal}, expected=${expectedTotal}`);
return false;
}
console.log(`✓ Journal integrity verified (${journal.voci.length} entries, €${calculatedTotal.toFixed(2)})`);
return true;
}
catch (error) {
console.error('Error verifying journal integrity:', error);
return false;
}
}
async reportAnomaly(anomaly) {
console.warn('Anomaly detected:', anomaly);
const path = `anomalies/${anomaly.pemId}/${anomaly.timestamp}.json`;
await this.storage.store(path, new TextEncoder().encode(JSON.stringify(anomaly)));
}
async generateDailyReceipts(journal) {
try {
console.log(`Generating daily receipts for PEM ${journal.identificativoPEM} on ${journal.dataRiferimento}`);
const result = await this.database.listDocuments({
emissionPointId: journal.identificativoPEM,
dateFrom: journal.dataRiferimento,
dateTo: journal.dataRiferimento,
});
const documents = result.data || [];
if (documents.length === 0) {
console.warn('No documents found for journal, skipping daily receipts generation');
return;
}
const vatMap = new Map();
let totalAmount = 0;
for (const doc of documents) {
totalAmount += doc.importoTotale;
for (const riepilogo of doc.datiRiepilogo) {
const key = riepilogo.aliquotaIVA !== undefined
? `VAT_${riepilogo.aliquotaIVA}`
: `NAT_${riepilogo.natura}`;
const existing = vatMap.get(key);
if (existing) {
existing.imponibile += riepilogo.imponibile;
existing.imposta += riepilogo.imposta;
}
else {
vatMap.set(key, {
imponibile: riepilogo.imponibile,
imposta: riepilogo.imposta,
aliquotaIVA: riepilogo.aliquotaIVA,
natura: riepilogo.natura,
});
}
}
}
const dailyReceipts = {
versione: '1.0',
contribuente: {
partitaIVA: documents[0].contribuente.partitaIVA,
codiceFiscale: documents[0].contribuente.codiceFiscale,
},
identificativoPEM: journal.identificativoPEM,
dataRiferimento: journal.dataRiferimento,
dataOraTrasmissione: new Date().toISOString(),
divisa: 'EUR',
numeroDocumenti: documents.length,
importoTotale: totalAmount,
riepilogoIVA: Array.from(vatMap.values()),
};
await this.database.saveDailyReceipts(dailyReceipts);
const outcome = await this.adeClient.trasmissioneCorrispettivi(dailyReceipts);
console.log(`✓ Daily receipts transmitted: ${documents.length} documents, €${totalAmount.toFixed(2)}`);
console.log(` VAT breakdown: ${vatMap.size} rates/natures`);
console.log(` ADE Outcome: ${outcome.codiceEsito || 'OK'}`);
}
catch (error) {
console.error('Error generating daily receipts:', error);
throw error;
}
}
start() {
return new Promise((resolve) => {
this.app.listen(this.port, () => {
console.log(`PEL Server listening on port ${this.port}`);
console.log('Endpoints:');
console.log(` GET /api/session/seed - Get session seed for PEM`);
console.log(` POST /api/document - Receive document from PEM`);
console.log(` POST /api/journal - Receive journal from PEM`);
console.log(` POST /api/anomaly - Receive anomaly report from PEM`);
resolve();
});
});
}
getApp() {
return this.app;
}
}
exports.PELServer = PELServer;
//# sourceMappingURL=pel.server.js.map