'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( oadrRequestEvent, clientCertificateCn, clientCertificateFingerprint, ) { logger.info( 'retrieveEvents', oadrRequestEvent, clientCertificateCn, clientCertificateFingerprint, ); const requestVenId = oadrRequestEvent.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: oadrRequestEvent.requestId || '', requestId: oadrRequestEvent.requestId || '', vtnId: vtnId, events: convertToOadrEvents(event), }; } /* qualifiedEvent is the combination of eventId & modificationNumber */ function eventResponseMatchesValidEvent(eventResponse, oadrEvents) { return ( oadrEvents.filter(oadrEvent => { return ( oadrEvent.eventDescriptor.eventId === eventResponse.eventId && oadrEvent.eventDescriptor.modificationNumber === eventResponse.modificationNumber && oadrEvent.eventDescriptor.status !== 'cancelled' && oadrEvent.eventDescriptor.status !== 'completed' ); }).length > 0 ); } async function validateEventResponses(venId, eventResponses) { const event = await nantum.fetchEvent(venId); const oadrEvents = convertToOadrEvents(event); const staleResponses = eventResponses.filter( eventResponse => !eventResponseMatchesValidEvent(eventResponse, oadrEvents), ); if (staleResponses.length > 0) { const error = new Error('Event response references invalid event'); error.responseCode = '454'; throw error; } } async function updateOptType( oadrCreatedEvent, clientCertificateCn, clientCertificateFingerprint, ) { logger.info( 'updateOptType', oadrCreatedEvent, clientCertificateCn, clientCertificateFingerprint, ); const requestVenId = oadrCreatedEvent.venId; validateVenId(requestVenId, clientCertificateFingerprint, true); let opted = await nantum.fetchOpted(requestVenId); try { await validateEventResponses(requestVenId, oadrCreatedEvent.eventResponses); for (const eventResponse of oadrCreatedEvent.eventResponses) { // remove existing opts for this eventId opted = [ ...opted.filter( optedItem => optedItem.eventId !== eventResponse.eventId, ), ]; opted.push({ eventId: eventResponse.eventId, modificationNumber: eventResponse.modificationNumber, optType: eventResponse.optType, }); } await nantum.updateOpted(requestVenId, opted); return { _type: 'oadrResponse', responseCode: '200', responseDescription: 'OK', venId: clientCertificateFingerprint, }; } catch (e) { return { _type: 'oadrResponse', responseCode: e.responseCode || '454', responseDescription: e.message || 'Invalid event response received', venId: clientCertificateFingerprint, }; } } async function filterOutAcknowledgedEvents(venId, events) { const opted = (await nantum.fetchOpted(venId)) || []; return events.filter( event => opted.filter( optedItem => optedItem.eventId === event.eventDescriptor.eventId && optedItem.modificationNumber === event.eventDescriptor.modificationNumber, ).length === 0, ); } async function pollForEvents( oadrPoll, clientCertificateCn, clientCertificateFingerprint, ) { logger.info( 'pollForEvents', oadrPoll, clientCertificateCn, clientCertificateFingerprint, ); const requestVenId = oadrPoll.venId; validateVenId(requestVenId, clientCertificateFingerprint, true); const event = await nantum.fetchEvent(requestVenId); const filteredEvents = await filterOutAcknowledgedEvents( requestVenId, convertToOadrEvents(event), ); if (filteredEvents.length > 0) { return { _type: 'oadrDistributeEvent', responseCode: '200', responseDescription: 'OK', responseRequestId: '', // required field, but empty is allowed as per spec requestId: '', vtnId: vtnId, events: filteredEvents, }; } return undefined; } function validateVenId(requestVenId, clientCertificateFingerprint, required) { if (requestVenId === clientCertificateFingerprint) { return; } if (!required && requestVenId == null) { return; } if (required && requestVenId == null) { const error = new Error('VenID is missing'); error.responseCode = 452; throw error; } const error = new Error('VenID is invalid'); error.responseCode = 452; throw error; } module.exports = { pollForEvents, retrieveEvents, updateOptType, };