'use strict'; const logger = require('../logger'); const nantum = require('../modules/nantum'); const { vtnId } = require('../config'); function calculateEventStatus(notificationTime, startTime, endTime) { const nowMillis = new Date().getTime(); if (nowMillis < new Date(startTime).getTime()) { return 'far'; } if (nowMillis > new Date(endTime).getTime()) { return 'completed'; } return 'active'; } function calculateDurationSeconds(startTime, endTime) { return Math.round( (new Date(endTime).getTime() - new Date(startTime).getTime()) / 1000, ); } function calculateDuration(startTime, endTime) { return `PT${calculateDurationSeconds(startTime, endTime)}S`; } function calculateNotificationDuration(notificationTime, startTime) { if (!notificationTime) { return 'PT0S'; } return calculateDuration(notificationTime, startTime); } function calculateEventIntervals(eventInfoValues, eventDurationSeconds) { //TODO: this is likely incorrect. Get more details on the event_info_value data model. let result = []; for (let i = 0; i < eventInfoValues.length; i++) { const eventInfoValue = eventInfoValues[i]; const nextOffset = i === eventInfoValues.length - 1 ? eventDurationSeconds - eventInfoValue.timeOffset : eventInfoValues[i + 1].timeOffset; result.push({ signalPayloads: [eventInfoValue.value], duration: `PT${nextOffset}S`, uid: `${i + 1}`, }); } return result; } function calculateEventSignals(eventInstances, eventDurationSeconds) { return eventInstances.map(eventInstance => { return { signalName: eventInstance.event_type_id, signalId: '112233445566', signalType: 'level', intervals: calculateEventIntervals( eventInstance.event_info_values, eventDurationSeconds, ), }; }); } function convertToOadrEvents(nantumEvent) { if (!nantumEvent.dr_event_data) { // no event return []; } const nowMillis = new Date().getTime(); if ( nowMillis < new Date(nantumEvent.dr_event_data.notification_time).getTime() ) { return []; // not in the notification period yet } return [ { eventDescriptor: { eventId: nantumEvent.event_identifier, modificationNumber: nantumEvent.event_mod_number, marketContext: 'http://MarketContext1', createdDateTime: '2020-04-14T16:06:39.000Z', eventStatus: calculateEventStatus( nantumEvent.dr_event_data.notification_time, nantumEvent.dr_event_data.start_time, nantumEvent.dr_event_data.end_time, ), testEvent: nantumEvent.test_event, priority: 0, }, activePeriod: { startDate: nantumEvent.dr_event_data.start_time, duration: calculateDuration( nantumEvent.dr_event_data.start_time, nantumEvent.dr_event_data.end_time, ), notificationDuration: calculateNotificationDuration( nantumEvent.dr_event_data.notification_time, nantumEvent.dr_event_data.start_time, ), }, signals: { event: calculateEventSignals( nantumEvent.dr_event_data.event_instance, calculateDurationSeconds( nantumEvent.dr_event_data.start_time, nantumEvent.dr_event_data.end_time, ), ), }, target: { venId: [nantumEvent.client_id], }, responseRequired: 'always', }, ]; } async function retrieveEvents( obj, clientCertificateCn, clientCertificateFingerprint, ) { logger.info( 'retrieveEvents', obj, clientCertificateCn, clientCertificateFingerprint, ); const requestVenId = obj.venId; if (!requestVenId) { const error = new Error('No VenID in request'); error.responseCode = 452; throw error; } if (requestVenId !== clientCertificateFingerprint) { // as per certification item #512, venId MUST be case-sensitive const error = new Error('VenID does not match certificate'); error.responseCode = 452; throw error; } if (!clientCertificateCn) { const error = new Error('Could not determine CN from client certificate'); error.responseCode = 452; throw error; } const event = await nantum.fetchEvent(requestVenId); return { responseCode: '200', responseDescription: 'OK', responseRequestId: obj.requestId || '', requestId: obj.requestId || '', vtnId: vtnId, events: convertToOadrEvents(event), }; } async function updateOptType( oadrCreatedEvent, clientCertificateCn, clientCertificateFingerprint, ) { logger.info( 'updateOptType', oadrCreatedEvent, clientCertificateCn, clientCertificateFingerprint, ); const requestVenId = oadrCreatedEvent.venId; if (!requestVenId) { const error = new Error('No VenID in request'); error.responseCode = 452; throw error; } if (requestVenId !== clientCertificateFingerprint) { // as per certification item #512, venId MUST be case-sensitive const error = new Error('VenID does not match certificate'); error.responseCode = 452; throw error; } const nantumRegistration = await nantum.fetchRegistration(requestVenId); const opted = nantumRegistration.opted || []; //TODO: more validation: VEN may opt into an event that doesn't exist. VEN may opt into an old version of an event // (modificationNumber doesn't match). May opt into a completed event. Indicate error(s) to client. for (const eventResponse of oadrCreatedEvent.eventResponses) { // remove existing opts for this eventId nantumRegistration.opted = [ ...opted.filter(optedItem => optedItem.eventId !== eventResponse.eventId), ]; nantumRegistration.opted.push({ eventId: eventResponse.eventId, modificationNumber: eventResponse.modificationNumber, optType: eventResponse.optType, }); await nantum.updateRegistration(nantumRegistration); } return { responseCode: '200', responseDescription: 'OK', venId: clientCertificateFingerprint, }; } async function filterOutAcknowledgedEvents(venId, events) { const nantumRegistration = await nantum.fetchRegistration(venId); const opted = nantumRegistration.opted || []; return events.filter( event => opted.filter( optedItem => optedItem.eventId === event.eventDescriptor.eventId && optedItem.modificationNumber === event.eventDescriptor.modificationNumber, ).length === 0, ); } async function pollForEvents( obj, clientCertificateCn, clientCertificateFingerprint, ) { logger.info( 'pollForEvents', obj, clientCertificateCn, clientCertificateFingerprint, ); const requestVenId = obj.venId; if (!requestVenId) { const error = new Error('No VenID in request'); error.responseCode = 452; throw error; } if (requestVenId !== clientCertificateFingerprint) { // as per certification item #512, venId MUST be case-sensitive const error = new Error('VenID does not match certificate'); error.responseCode = 452; throw error; } if (!clientCertificateCn) { const error = new Error('Could not determine CN from client certificate'); error.responseCode = 452; throw error; } const event = await nantum.fetchEvent(requestVenId); const filteredEvents = await filterOutAcknowledgedEvents( requestVenId, convertToOadrEvents(event), ); if (filteredEvents.length > 0) { return { responseCode: '200', responseDescription: 'OK', responseRequestId: '', // required field, but empty is allowed as per spec requestId: '', vtnId: vtnId, events: filteredEvents, }; } return undefined; } module.exports = { pollForEvents, retrieveEvents, updateOptType, };