@openade/pel
Version:
Punto di Elaborazione (Elaboration Point) - Server library for managing PEMs and communicating with ADE
401 lines • 17.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuditServer = exports.AuditRequestStatus = void 0;
const express_1 = __importDefault(require("express"));
const node_crypto_1 = require("node:crypto");
var AuditRequestStatus;
(function (AuditRequestStatus) {
AuditRequestStatus["PRONTA"] = "PRONTA";
AuditRequestStatus["IN_ELABORAZIONE"] = "IN_ELABORAZIONE";
AuditRequestStatus["NON_DISPONIBILE"] = "NON_DISPONIBILE";
})(AuditRequestStatus || (exports.AuditRequestStatus = AuditRequestStatus = {}));
class AuditServer {
constructor(config) {
this.jobs = new Map();
this.server = null;
this.config = {
jobRetentionMs: 24 * 60 * 60 * 1000,
...config,
};
this.app = (0, express_1.default)();
this.setupMiddleware();
this.setupRoutes();
this.startJobCleanup();
}
setupMiddleware() {
this.app.use(express_1.default.json());
this.app.use(express_1.default.raw({ type: 'application/octet-stream', limit: '50mb' }));
if (this.config.enableCORS) {
this.app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
}
else {
next();
}
});
}
if (this.config.authMiddleware) {
this.app.use(this.config.authMiddleware);
}
this.app.use((err, req, res, _next) => {
console.error('Error:', err);
res.status(500).json({
anomalie: [
{
codice: 'ERR_INTERNAL',
descrizione: 'Errore non previsto',
},
],
});
});
}
setupRoutes() {
this.app.post('/audit/journal', async (req, res) => {
try {
const idPresaInCarico = (0, node_crypto_1.randomUUID)();
const job = {
id: idPresaInCarico,
type: 'journal',
status: AuditRequestStatus.IN_ELABORAZIONE,
requestData: req.body,
files: [],
createdAt: new Date(),
};
this.jobs.set(idPresaInCarico, job);
this.processJournalAudit(job).catch((err) => {
console.error('Error processing journal audit:', err);
job.status = AuditRequestStatus.NON_DISPONIBILE;
});
res.status(200).json({ idPresaInCarico });
}
catch {
res.status(500).json({
anomalie: [{ codice: 'ERR_INTERNAL', descrizione: 'Errore non previsto' }],
});
}
});
this.app.get('/audit/journal/:idPresaInCarico/stato', (req, res) => {
const job = this.jobs.get(req.params.idPresaInCarico);
if (!job || job.type !== 'journal') {
return res.status(404).json({
anomalie: [{ codice: 'ERR_NOT_FOUND', descrizione: 'Richiesta di Audit non trovata' }],
});
}
res.status(200).json({ stato: job.status });
});
this.app.get('/audit/journal/:idPresaInCarico', (req, res) => {
const job = this.jobs.get(req.params.idPresaInCarico);
if (!job || job.type !== 'journal') {
return res.status(404).json({
anomalie: [{ codice: 'ERR_NOT_FOUND', descrizione: 'IdPresoinCarico non trovato' }],
});
}
if (job.status !== AuditRequestStatus.PRONTA) {
return res.status(409).json({
anomalie: [
{
codice: 'ERR_NOT_READY',
descrizione: 'Richiesta correttamente elaborata ma requisiti non soddisfatti',
},
],
});
}
res.status(200).json({ archivi: job.files });
});
this.app.get('/audit/journal/:idPresaInCarico/zip/:nomeFile', async (req, res) => {
const job = this.jobs.get(req.params.idPresaInCarico);
if (!job || job.type !== 'journal') {
return res.status(404).json({
anomalie: [{ codice: 'ERR_NOT_FOUND', descrizione: 'IdPresoInCarico non trovato' }],
});
}
try {
const filePath = `audit/journal/${job.id}/${req.params.nomeFile}`;
const fileData = await this.config.storage.retrieve(filePath);
if (!fileData) {
return res.status(404).json({
anomalie: [{ codice: 'ERR_FILE_NOT_FOUND', descrizione: 'File non trovato' }],
});
}
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename="${req.params.nomeFile}"`);
res.send(fileData);
}
catch {
res.status(500).json({
anomalie: [{ codice: 'ERR_INTERNAL', descrizione: 'Errore non previsto' }],
});
}
});
this.app.post('/audit/dc', async (req, res) => {
try {
const { hash } = req.body;
if (!hash || !Array.isArray(hash)) {
return res.status(406).json({
anomalie: [
{ codice: 'ERR_INVALID_PARAMS', descrizione: 'Parametri di input non validi' },
],
});
}
const idPresaInCarico = (0, node_crypto_1.randomUUID)();
const job = {
id: idPresaInCarico,
type: 'dc',
status: AuditRequestStatus.IN_ELABORAZIONE,
requestData: { hash },
files: [],
createdAt: new Date(),
};
this.jobs.set(idPresaInCarico, job);
this.processDocumentAudit(job).catch((err) => {
console.error('Error processing document audit:', err);
job.status = AuditRequestStatus.NON_DISPONIBILE;
});
res.status(200).json({ idPresaInCarico });
}
catch {
res.status(500).json({
anomalie: [{ codice: 'ERR_INTERNAL', descrizione: 'Errore non previsto' }],
});
}
});
this.app.get('/audit/dc/:idPresaInCarico/stato', (req, res) => {
const job = this.jobs.get(req.params.idPresaInCarico);
if (!job || job.type !== 'dc') {
return res.status(404).json({
anomalie: [{ codice: 'ERR_NOT_FOUND', descrizione: 'Richiesta non trovata' }],
});
}
res.status(200).json({ stato: job.status });
});
this.app.get('/audit/dc/:idPresaInCarico', (req, res) => {
const job = this.jobs.get(req.params.idPresaInCarico);
if (!job || job.type !== 'dc') {
return res.status(404).json({
anomalie: [{ codice: 'ERR_NOT_FOUND', descrizione: 'IdPresoinCarico non trovato' }],
});
}
if (job.status !== AuditRequestStatus.PRONTA) {
return res.status(409).json({
anomalie: [
{
codice: 'ERR_NOT_READY',
descrizione: 'Richiesta correttamente elaborata ma requisiti non soddisfatti',
},
],
});
}
res.status(200).json({ dc: job.files });
});
this.app.get('/audit/dc/:idPresaInCarico/files/:nomeFile', async (req, res) => {
const job = this.jobs.get(req.params.idPresaInCarico);
if (!job || job.type !== 'dc') {
return res.status(404).json({
anomalie: [{ codice: 'ERR_NOT_FOUND', descrizione: 'IdPresoInCarico non trovato' }],
});
}
try {
const filePath = `audit/dc/${job.id}/${req.params.nomeFile}`;
const fileData = await this.config.storage.retrieve(filePath);
if (!fileData) {
return res.status(404).json({
anomalie: [{ codice: 'ERR_FILE_NOT_FOUND', descrizione: 'File non trovato' }],
});
}
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename="${req.params.nomeFile}"`);
res.send(fileData);
}
catch {
res.status(500).json({
anomalie: [{ codice: 'ERR_INTERNAL', descrizione: 'Errore non previsto' }],
});
}
});
}
async processJournalAudit(job) {
try {
console.log(`Processing journal audit request ${job.id}`);
const { emissionPointId, dateFrom, dateTo } = job.requestData;
const result = await this.config.database.listJournals({
emissionPointId: emissionPointId,
dateFrom: dateFrom,
dateTo: dateTo,
});
const journals = result.data || [];
if (journals.length === 0) {
console.warn('No journals found for audit request');
job.status = AuditRequestStatus.NON_DISPONIBILE;
return;
}
console.log(`Found ${journals.length} journals for audit`);
const encoder = new TextEncoder();
const files = [];
for (const journal of journals) {
const journalXML = this.generateJournalXML(journal);
const xmlData = encoder.encode(journalXML);
const dateStr = journal.dataRiferimento.replace(/[:-]/g, '');
const fileName = `J_${journal.identificativoPEM}_${dateStr}.xml`;
await this.config.storage.store(`audit/journal/${job.id}/${fileName}`, xmlData);
files.push({
nome: fileName,
size: xmlData.length.toString(),
});
}
job.files = files;
job.status = AuditRequestStatus.PRONTA;
job.completedAt = new Date();
console.log(`✓ Journal audit completed: ${files.length} files ready`);
}
catch (error) {
console.error('Failed to process journal audit:', error);
job.status = AuditRequestStatus.NON_DISPONIBILE;
}
}
generateJournalXML(journal) {
return `<?xml version="1.0" encoding="UTF-8"?>
<Journal versione="${journal.versione}">
<IdentificativoPEM>${journal.identificativoPEM}</IdentificativoPEM>
<DataRiferimento>${journal.dataRiferimento}</DataRiferimento>
<DataOraGenerazione>${journal.dataOraGenerazione}</DataOraGenerazione>
<NumeroVoci>${journal.numeroVoci || (journal.voci ? journal.voci.length : 0)}</NumeroVoci>
<ImportoTotaleGiornata>${journal.importoTotaleGiornata || 0}</ImportoTotaleGiornata>
</Journal>`;
}
async processDocumentAudit(job) {
try {
console.log(`Processing document audit request ${job.id}`);
const { hash, emissionPointId, dateFrom, dateTo } = job.requestData;
const encoder = new TextEncoder();
const files = [];
if (hash && Array.isArray(hash)) {
for (const documentHash of hash) {
try {
let document = null;
if (this.config.database && this.config.database.getDocumentByHash) {
document = await this.config.database.getDocumentByHash(documentHash);
}
if (!document) {
console.warn(`Document with hash ${documentHash} not found or method not implemented`);
continue;
}
const docXML = this.generateDocumentXML(document);
const xmlData = encoder.encode(docXML);
const dateStr = document.datiGenerali.dataOra.substring(0, 10).replace(/[:-]/g, '');
const fileName = `DC_${document.datiGenerali.numero}_${document.identificativoPEM}_${dateStr}.xml`;
await this.config.storage.store(`audit/dc/${job.id}/${fileName}`, xmlData);
files.push({
nome: fileName,
size: xmlData.length.toString(),
});
}
catch (err) {
console.error(`Error processing document hash ${documentHash}:`, err);
}
}
}
else if (emissionPointId || (dateFrom && dateTo)) {
const result = await this.config.database.listDocuments({
emissionPointId: emissionPointId,
dateFrom: dateFrom,
dateTo: dateTo,
});
const documents = result.data || [];
console.log(`Found ${documents.length} documents for audit`);
for (const document of documents) {
const docXML = this.generateDocumentXML(document);
const xmlData = encoder.encode(docXML);
const dateStr = document.datiGenerali.dataOra.substring(0, 10).replace(/[:-]/g, '');
const fileName = `DC_${document.datiGenerali.numero}_${document.identificativoPEM}_${dateStr}.xml`;
await this.config.storage.store(`audit/dc/${job.id}/${fileName}`, xmlData);
files.push({
nome: fileName,
size: xmlData.length.toString(),
});
}
}
if (files.length === 0) {
console.warn('No documents found for audit request');
job.status = AuditRequestStatus.NON_DISPONIBILE;
return;
}
job.files = files;
job.status = AuditRequestStatus.PRONTA;
job.completedAt = new Date();
console.log(`✓ Document audit completed: ${files.length} files ready`);
}
catch (error) {
console.error('Failed to process document audit:', error);
job.status = AuditRequestStatus.NON_DISPONIBILE;
}
}
generateDocumentXML(document) {
return `<?xml version="1.0" encoding="UTF-8"?>
<DocumentoCommerciale>
<IdentificativoPEM>${document.identificativoPEM}</IdentificativoPEM>
<DatiGenerali>
<Numero>${document.datiGenerali.numero}</Numero>
<DataOra>${document.datiGenerali.dataOra}</DataOra>
<TipoDocumento>${document.datiGenerali.tipoDocumento}</TipoDocumento>
</DatiGenerali>
<ImportoTotale>${document.importoTotale}</ImportoTotale>
</DocumentoCommerciale>`;
}
startJobCleanup() {
setInterval(() => {
const now = Date.now();
const retentionMs = this.config.jobRetentionMs;
for (const [id, job] of this.jobs.entries()) {
const jobAge = now - job.createdAt.getTime();
if (jobAge > retentionMs) {
this.jobs.delete(id);
console.log(`Cleaned up expired job: ${id}`);
}
}
}, 60 * 60 * 1000);
}
async start() {
return new Promise((resolve) => {
this.server = this.app.listen(this.config.port, () => {
console.log(`Audit server listening on port ${this.config.port}`);
console.log(`Endpoints:`);
console.log(` POST /audit/journal - Request journal audit`);
console.log(` GET /audit/journal/{id}/stato - Check journal status`);
console.log(` GET /audit/journal/{id} - List journal files`);
console.log(` GET /audit/journal/{id}/zip/{file} - Download journal file`);
console.log(` POST /audit/dc - Request document audit`);
console.log(` GET /audit/dc/{id}/stato - Check document status`);
console.log(` GET /audit/dc/{id} - List document files`);
console.log(` GET /audit/dc/{id}/files/{file} - Download document file`);
resolve();
});
});
}
async stop() {
return new Promise((resolve, reject) => {
if (this.server) {
this.server.close((err) => {
if (err)
reject(err);
else
resolve();
});
}
else {
resolve();
}
});
}
getApp() {
return this.app;
}
}
exports.AuditServer = AuditServer;
//# sourceMappingURL=audit.server.js.map