UNPKG

@openade/pel

Version:

Punto di Elaborazione (Elaboration Point) - Server library for managing PEMs and communicating with ADE

277 lines 12.6 kB
"use strict"; 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