UNPKG

@cap-js-community/sap-afc-sdk

Version:

SAP Advanced Financial Closing SDK for CDS

443 lines (419 loc) 14 kB
"use strict"; const cds = require("@sap/cds"); const fs = require("fs"); const path = require("path"); const BaseApplicationService = require("../common/BaseApplicationService"); const JobSchedulingError = require("./common/JobSchedulingError"); const { JobStatus, ResultType, MessageSeverity } = require("./common/codelist"); const { messageLocales } = require("../../src/util/helper"); const STATUS_TRANSITIONS = { [JobStatus.requested]: [JobStatus.running, JobStatus.cancelRequested, JobStatus.canceled], [JobStatus.running]: [ JobStatus.completed, JobStatus.completedWithWarning, JobStatus.completedWithError, JobStatus.failed, JobStatus.cancelRequested, ], [JobStatus.completed]: [], [JobStatus.completedWithWarning]: [], [JobStatus.completedWithError]: [], [JobStatus.failed]: [], [JobStatus.cancelRequested]: [ JobStatus.completed, JobStatus.completedWithWarning, JobStatus.completedWithError, JobStatus.failed, JobStatus.canceled, ], [JobStatus.canceled]: [], }; module.exports = class SchedulingProcessingService extends BaseApplicationService { constructor(...args) { super(...args); this.statusTransitions = STATUS_TRANSITIONS; } async init() { const { Job } = cds.entities("sapafcsdk.scheduling"); const { processJob, updateJob, cancelJob, syncJob, notify } = this.operations; this.before([processJob, updateJob, cancelJob], async (req) => { const ID = req.data.ID; const job = await SELECT.one(Job, (job) => { (job`.*`, job.parameters((parameter) => { parameter`.*`; })); }).where({ ID }); if (!job) { return req.reject(JobSchedulingError.jobNotFound(ID)); } req.job = job; }); this.on(processJob, async (req, next) => { const processingConfig = cds.env.requires?.["sap-afc-sdk"]?.mockProcessing; let results = []; if (processingConfig) { results = await this.mockJobProcessing(req, req.job, processingConfig); } await this.processJobUpdate(req, req.job, JobStatus.running, results); }); this.on(updateJob, async (req, next) => { await this.processJobUpdate(req, req.job, req.data.status, req.data.results); }); this.on(cancelJob, async (req, next) => { await this.processJobUpdate(req, req.job, JobStatus.canceled); }); this.on(syncJob, async (req, next) => { const processingConfig = cds.env.requires?.["sap-afc-sdk"]?.mockProcessing; if (processingConfig) { await this.mockJobSync(req); } }); this.on(notify, async (req, next) => { const processingConfig = cds.env.requires?.["sap-afc-sdk"]?.mockProcessing; if (processingConfig) { await this.mockNotification(req); } }); return super.init(); } async processJobUpdate(req, job, status, results) { const { Job, JobResult } = cds.entities("sapafcsdk.scheduling"); if (!status) { return req.reject(JobSchedulingError.statusValueMissing()); } if (!JobStatus[status]) { return req.reject(JobSchedulingError.invalidJobStatus(status)); } if (job.status_code === status) { return; } if (!(await this.checkStatusTransition(req, job, job.status_code, status))) { return req.reject(JobSchedulingError.statusTransitionNotAllowed(job.status_code, status)); } job.status_code = status; await UPDATE.entity(Job) .set({ status_code: status, }) .where({ ID: job.ID }); if (results && results.length > 0) { const insertResults = await this.checkJobResults(req, job, results); await INSERT.into(JobResult).entries(insertResults); } const schedulingWebsocketService = await cds.connect.to("sapafcsdk.scheduling.WebsocketService"); await schedulingWebsocketService.tx(req).emit( "jobStatusChanged", { IDs: [job.ID], status, }, { "x-eventQueue-referenceEntityKey": job.ID, }, ); } async checkStatusTransition(req, job, statusBefore, statusAfter) { return this.statusTransitions[statusBefore].includes(statusAfter); } async checkJobResults(req, job, results) { const locales = messageLocales(); return results.map((result) => { if (!result.name) { return req.reject(JobSchedulingError.resultNameMissing()); } if (!result.type) { return req.reject(JobSchedulingError.resultTypeMissing()); } switch (result.type) { case ResultType.link: if (!result.link) { return req.reject(JobSchedulingError.linkMissing(result.type)); } if (result.mimeType) { return req.reject(JobSchedulingError.mimeTypeNotAllowed(result.type)); } if (result.filename) { return req.reject(JobSchedulingError.filenameNotAllowed(result.type)); } if (result.data) { return req.reject(JobSchedulingError.dataNotAllowed(result.type)); } if (result.messages) { return req.reject(JobSchedulingError.messagesNotAllowed(result.type)); } break; case ResultType.data: if (!result.mimeType) { return req.reject(JobSchedulingError.mimeTypeMissing(result.type)); } if (!result.filename) { return req.reject(JobSchedulingError.filenameMissing(result.type)); } if (!result.data) { return req.reject(JobSchedulingError.dataMissing(result.type)); } if (result.link) { return req.reject(JobSchedulingError.linkNotAllowed(result.type)); } if (result.messages) { return req.reject(JobSchedulingError.messagesNotAllowed(result.type)); } break; case ResultType.message: if (!(result?.messages?.length > 0)) { return req.reject(JobSchedulingError.messagesMissing(result.type)); } for (const message of result.messages) { if (!message.code) { return req.reject(JobSchedulingError.codeMissing()); } message.values ||= []; if (!message.text) { const text = cds.i18n.messages.at(message.code, cds.env.i18n.default_language, message.values); if (!text) { return req.reject(JobSchedulingError.textMissing()); } message.text = text; } if (!message.texts) { message.texts = locales.map((locale) => { return { locale, text: cds.i18n.messages.at(message.code, locale, message.values), }; }); } else { for (const text of message.texts) { if (!text.locale) { return req.reject(JobSchedulingError.localeMissing()); } if (!locales.includes(text.locale)) { return req.reject(JobSchedulingError.invalidLocale(text.locale)); } if (!text.text) { text.text = cds.i18n.messages.at(message.code, text.locale, message.values); } } } if (!message.severity) { return req.reject(JobSchedulingError.severityMissing()); } if (!MessageSeverity[message.severity]) { return req.reject(JobSchedulingError.invalidMessageSeverity(message.severity)); } if (!message.createdAt) { message.createdAt = new Date().toISOString(); } } if (result.link) { return req.reject(JobSchedulingError.linkNotAllowed(result.type)); } if (result.mimeType) { return req.reject(JobSchedulingError.mimeTypeNotAllowed(result.type)); } if (result.filename) { return req.reject(JobSchedulingError.filenameNotAllowed(result.type)); } if (result.data) { return req.reject(JobSchedulingError.dataNotAllowed(result.type)); } break; default: return req.reject(JobSchedulingError.invalidResultType(result.type)); } return { job_ID: job.ID, name: result.name, type_code: result.type, link: result.link, mimeType: result.mimeType, filename: result.filename, data: result.data, messages: (result.messages || []).map((message) => { return { code: message.code, values: message.values, text: message.text, severity_code: message.severity, createdAt: message.createdAt, texts: message.texts .map((text) => { return { locale: text.locale, text: text.text, }; }) .filter((text) => text.locale && text.text), }; }), }; }); } async mockJobProcessing(req, job, config) { let min = config.min ?? 0; let max = config.max ?? 10; let processingTime = (Math.floor(Math.random() * (max - min)) + min) * 1000; let processingStatus = config.default ?? JobStatus.completed; let advancedMock = false; if (config.status && Object.keys(config.status).length > 0) { advancedMock = true; const statuses = Object.keys(config.status); min = 0; max = statuses.reduce((sum, status) => { return sum + config.status[status]; }, 0); const statusValue = Math.random() * (max - min) + min; let value = 0; for (const status of statuses) { if (value < statusValue) { processingStatus = status; } value += config.status[status]; } } const durationParameter = job.parameters.find((parameter) => parameter.definition_name === "duration"); if (durationParameter && parseFloat(durationParameter.value) > 0) { processingTime = parseFloat(durationParameter.value) * 1000; } const statusParameter = job.parameters.find((parameter) => parameter.definition_name === "status"); if (statusParameter && JobStatus[statusParameter.value]) { processingStatus = statusParameter.value; } const ID = job.ID; const updateResults = []; switch (processingStatus) { case JobStatus.completed: updateResults.push( { type: ResultType.message, name: "Message", messages: [ { code: "jobCompleted", severity: MessageSeverity.info, }, ], }, { type: ResultType.link, name: "Link", link: "https://sap.com", }, { type: ResultType.data, name: "Data", filename: "log.txt", mimeType: "text/plain", data: btoa(cds.i18n.messages.at("jobCompleted")), }, { type: ResultType.data, name: "Data", filename: "log.pdf", mimeType: "application/pdf", data: fs.readFileSync(path.join(__dirname, "./assets/log.pdf"), { encoding: "base64" }), }, ); break; case JobStatus.completedWithWarning: updateResults.push({ type: ResultType.message, name: "Message", messages: [ { code: "jobCompletedWithWarning", severity: MessageSeverity.warning, createdAt: new Date().toISOString(), }, ], }); break; case JobStatus.completedWithError: updateResults.push({ type: ResultType.message, name: "Message", messages: [ { code: "jobCompletedWithError", severity: MessageSeverity.error, }, ], }); break; case JobStatus.failed: updateResults.push({ type: ResultType.message, name: "Message", messages: [ { code: "jobFailed", severity: MessageSeverity.error, }, ], }); break; } await cds.queued(this).send( "updateJob", { ID, status: processingStatus, results: updateResults, }, { "x-eventQueue-referenceEntityKey": ID, "x-eventQueue-startAfter": new Date(Date.now() + processingTime), }, ); const mockResults = []; if (advancedMock) { mockResults.push({ type: ResultType.message, name: "Advanced Mocked Run", messages: [ { code: "jobAdvancedMock", severity: MessageSeverity.info, }, ], }); } else { mockResults.push({ type: ResultType.message, name: "Basic Mocked Run", messages: [ { code: "jobBasicMock", severity: MessageSeverity.info, }, ], }); } if (job.testRun) { mockResults.push({ type: ResultType.message, name: "Test Run", messages: [ { code: "jobTestRun", severity: MessageSeverity.info, }, ], }); } return mockResults; } async mockJobSync(req) { cds.log("sapafcsdk/jobsync").info("periodic sync job triggered"); } async mockNotification(req) { for (const notification of req.data.notifications) { cds.log("sapafcsdk/notification").info(notification); } } // async reportStatus(req, status) { // const afc = await cds.connect.to("afc"); // await afc.tx(req).reportStatus(status); // } };