event.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. 'use strict';
  2. const logger = require('../logger');
  3. const nantum = require('../modules/nantum');
  4. const { vtnId } = require('../config');
  5. function calculateEventStatus(notificationTime, startTime, endTime) {
  6. const nowMillis = new Date().getTime();
  7. if (nowMillis < new Date(startTime).getTime()) {
  8. return 'far';
  9. }
  10. if (nowMillis > new Date(endTime).getTime()) {
  11. return 'completed';
  12. }
  13. return 'active';
  14. }
  15. function calculateDurationSeconds(startTime, endTime) {
  16. return Math.round(
  17. (new Date(endTime).getTime() - new Date(startTime).getTime()) / 1000,
  18. );
  19. }
  20. function calculateDuration(startTime, endTime) {
  21. return `PT${calculateDurationSeconds(startTime, endTime)}S`;
  22. }
  23. function calculateNotificationDuration(notificationTime, startTime) {
  24. if (!notificationTime) {
  25. return 'PT0S';
  26. }
  27. return calculateDuration(notificationTime, startTime);
  28. }
  29. function calculateEventIntervals(eventInfoValues, eventDurationSeconds) {
  30. //TODO: this is likely incorrect. Get more details on the event_info_value data model.
  31. let result = [];
  32. for (let i = 0; i < eventInfoValues.length; i++) {
  33. const eventInfoValue = eventInfoValues[i];
  34. const nextOffset =
  35. i === eventInfoValues.length - 1
  36. ? eventDurationSeconds - eventInfoValue.timeOffset
  37. : eventInfoValues[i + 1].timeOffset;
  38. result.push({
  39. signalPayloads: [eventInfoValue.value],
  40. duration: `PT${nextOffset}S`,
  41. uid: `${i + 1}`,
  42. });
  43. }
  44. return result;
  45. }
  46. function calculateEventSignals(eventInstances, eventDurationSeconds) {
  47. return eventInstances.map(eventInstance => {
  48. return {
  49. signalName: eventInstance.event_type_id,
  50. signalId: '112233445566',
  51. signalType: 'level',
  52. intervals: calculateEventIntervals(
  53. eventInstance.event_info_values,
  54. eventDurationSeconds,
  55. ),
  56. };
  57. });
  58. }
  59. function convertToOadrEvents(nantumEvent) {
  60. if (!nantumEvent.dr_event_data) {
  61. // no event
  62. return [];
  63. }
  64. const nowMillis = new Date().getTime();
  65. if (
  66. nowMillis < new Date(nantumEvent.dr_event_data.notification_time).getTime()
  67. ) {
  68. return []; // not in the notification period yet
  69. }
  70. return [
  71. {
  72. eventDescriptor: {
  73. eventId: nantumEvent.event_identifier,
  74. modificationNumber: nantumEvent.event_mod_number,
  75. marketContext: 'http://MarketContext1',
  76. createdDateTime: '2020-04-14T16:06:39.000Z',
  77. eventStatus: calculateEventStatus(
  78. nantumEvent.dr_event_data.notification_time,
  79. nantumEvent.dr_event_data.start_time,
  80. nantumEvent.dr_event_data.end_time,
  81. ),
  82. testEvent: nantumEvent.test_event,
  83. priority: 0,
  84. },
  85. activePeriod: {
  86. startDate: nantumEvent.dr_event_data.start_time,
  87. duration: calculateDuration(
  88. nantumEvent.dr_event_data.start_time,
  89. nantumEvent.dr_event_data.end_time,
  90. ),
  91. notificationDuration: calculateNotificationDuration(
  92. nantumEvent.dr_event_data.notification_time,
  93. nantumEvent.dr_event_data.start_time,
  94. ),
  95. },
  96. signals: {
  97. event: calculateEventSignals(
  98. nantumEvent.dr_event_data.event_instance,
  99. calculateDurationSeconds(
  100. nantumEvent.dr_event_data.start_time,
  101. nantumEvent.dr_event_data.end_time,
  102. ),
  103. ),
  104. },
  105. target: {
  106. venId: [nantumEvent.client_id],
  107. },
  108. responseRequired: 'always',
  109. },
  110. ];
  111. }
  112. async function retrieveEvents(
  113. oadrRequestEvent,
  114. clientCertificateCn,
  115. clientCertificateFingerprint,
  116. ) {
  117. logger.info(
  118. 'retrieveEvents',
  119. oadrRequestEvent,
  120. clientCertificateCn,
  121. clientCertificateFingerprint,
  122. );
  123. const requestVenId = oadrRequestEvent.venId;
  124. if (!requestVenId) {
  125. const error = new Error('No VenID in request');
  126. error.responseCode = 452;
  127. throw error;
  128. }
  129. if (requestVenId !== clientCertificateFingerprint) {
  130. // as per certification item #512, venId MUST be case-sensitive
  131. const error = new Error('VenID does not match certificate');
  132. error.responseCode = 452;
  133. throw error;
  134. }
  135. if (!clientCertificateCn) {
  136. const error = new Error('Could not determine CN from client certificate');
  137. error.responseCode = 452;
  138. throw error;
  139. }
  140. const event = await nantum.fetchEvent(requestVenId);
  141. return {
  142. responseCode: '200',
  143. responseDescription: 'OK',
  144. responseRequestId: oadrRequestEvent.requestId || '',
  145. requestId: oadrRequestEvent.requestId || '',
  146. vtnId: vtnId,
  147. events: convertToOadrEvents(event),
  148. };
  149. }
  150. async function updateOptType(
  151. oadrCreatedEvent,
  152. clientCertificateCn,
  153. clientCertificateFingerprint,
  154. ) {
  155. logger.info(
  156. 'updateOptType',
  157. oadrCreatedEvent,
  158. clientCertificateCn,
  159. clientCertificateFingerprint,
  160. );
  161. const requestVenId = oadrCreatedEvent.venId;
  162. validateVenId(requestVenId, clientCertificateFingerprint, true);
  163. if (requestVenId !== clientCertificateFingerprint) {
  164. // as per certification item #512, venId MUST be case-sensitive
  165. const error = new Error('VenID does not match certificate');
  166. error.responseCode = 452;
  167. throw error;
  168. }
  169. const nantumRegistration = await nantum.fetchRegistration(requestVenId);
  170. const opted = nantumRegistration.opted || [];
  171. //TODO: more validation: VEN may opt into an event that doesn't exist. VEN may opt into an old version of an event
  172. // (modificationNumber doesn't match). May opt into a completed event. Indicate error(s) to client.
  173. for (const eventResponse of oadrCreatedEvent.eventResponses) {
  174. // remove existing opts for this eventId
  175. nantumRegistration.opted = [
  176. ...opted.filter(optedItem => optedItem.eventId !== eventResponse.eventId),
  177. ];
  178. nantumRegistration.opted.push({
  179. eventId: eventResponse.eventId,
  180. modificationNumber: eventResponse.modificationNumber,
  181. optType: eventResponse.optType,
  182. });
  183. await nantum.updateRegistration(nantumRegistration);
  184. }
  185. return {
  186. responseCode: '200',
  187. responseDescription: 'OK',
  188. venId: clientCertificateFingerprint,
  189. };
  190. }
  191. async function filterOutAcknowledgedEvents(venId, events) {
  192. const nantumRegistration = await nantum.fetchRegistration(venId);
  193. const opted = nantumRegistration.opted || [];
  194. return events.filter(
  195. event =>
  196. opted.filter(
  197. optedItem =>
  198. optedItem.eventId === event.eventDescriptor.eventId &&
  199. optedItem.modificationNumber ===
  200. event.eventDescriptor.modificationNumber,
  201. ).length === 0,
  202. );
  203. }
  204. async function pollForEvents(
  205. oadrPoll,
  206. clientCertificateCn,
  207. clientCertificateFingerprint,
  208. ) {
  209. logger.info(
  210. 'pollForEvents',
  211. oadrPoll,
  212. clientCertificateCn,
  213. clientCertificateFingerprint,
  214. );
  215. const requestVenId = oadrPoll.venId;
  216. validateVenId(requestVenId, clientCertificateFingerprint, true);
  217. const event = await nantum.fetchEvent(requestVenId);
  218. const filteredEvents = await filterOutAcknowledgedEvents(
  219. requestVenId,
  220. convertToOadrEvents(event),
  221. );
  222. if (filteredEvents.length > 0) {
  223. return {
  224. responseCode: '200',
  225. responseDescription: 'OK',
  226. responseRequestId: '', // required field, but empty is allowed as per spec
  227. requestId: '',
  228. vtnId: vtnId,
  229. events: filteredEvents,
  230. };
  231. }
  232. return undefined;
  233. }
  234. function validateVenId(requestVenId, clientCertificateFingerprint, required) {
  235. if (requestVenId === clientCertificateFingerprint) {
  236. return;
  237. }
  238. if (!required && requestVenId == null) {
  239. return;
  240. }
  241. if (required && requestVenId == null) {
  242. const error = new Error('VenID is missing');
  243. error.responseCode = 452;
  244. throw error;
  245. }
  246. const error = new Error('VenID is invalid');
  247. error.responseCode = 452;
  248. throw error;
  249. }
  250. module.exports = {
  251. pollForEvents,
  252. retrieveEvents,
  253. updateOptType,
  254. };