event.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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. /* qualifiedEvent is the combination of eventId & modificationNumber */
  151. function eventResponseMatchesValidEvent(eventResponse, oadrEvents) {
  152. return (
  153. oadrEvents.filter(oadrEvent => {
  154. return (
  155. oadrEvent.eventDescriptor.eventId === eventResponse.eventId &&
  156. oadrEvent.eventDescriptor.modificationNumber ===
  157. eventResponse.modificationNumber &&
  158. oadrEvent.eventDescriptor.status !== 'cancelled' &&
  159. oadrEvent.eventDescriptor.status !== 'completed'
  160. );
  161. }).length > 0
  162. );
  163. }
  164. async function validateEventResponses(venId, eventResponses) {
  165. const event = await nantum.fetchEvent(venId);
  166. const oadrEvents = convertToOadrEvents(event);
  167. const staleResponses = eventResponses.filter(
  168. eventResponse => !eventResponseMatchesValidEvent(eventResponse, oadrEvents),
  169. );
  170. if (staleResponses.length > 0) {
  171. const error = new Error('Event response references invalid event');
  172. error.responseCode = '454';
  173. throw error;
  174. }
  175. }
  176. async function updateOptType(
  177. oadrCreatedEvent,
  178. clientCertificateCn,
  179. clientCertificateFingerprint,
  180. ) {
  181. logger.info(
  182. 'updateOptType',
  183. oadrCreatedEvent,
  184. clientCertificateCn,
  185. clientCertificateFingerprint,
  186. );
  187. const requestVenId = oadrCreatedEvent.venId;
  188. validateVenId(requestVenId, clientCertificateFingerprint, true);
  189. let opted = await nantum.fetchOpted(requestVenId);
  190. try {
  191. await validateEventResponses(requestVenId, oadrCreatedEvent.eventResponses);
  192. for (const eventResponse of oadrCreatedEvent.eventResponses) {
  193. // remove existing opts for this eventId
  194. opted = [
  195. ...opted.filter(
  196. optedItem => optedItem.eventId !== eventResponse.eventId,
  197. ),
  198. ];
  199. opted.push({
  200. eventId: eventResponse.eventId,
  201. modificationNumber: eventResponse.modificationNumber,
  202. optType: eventResponse.optType,
  203. });
  204. }
  205. await nantum.updateOpted(requestVenId, opted);
  206. return {
  207. responseCode: '200',
  208. responseDescription: 'OK',
  209. venId: clientCertificateFingerprint,
  210. };
  211. } catch (e) {
  212. return {
  213. responseCode: e.responseCode || '454',
  214. responseDescription: e.message || 'Invalid event response received',
  215. venId: clientCertificateFingerprint,
  216. };
  217. }
  218. }
  219. async function filterOutAcknowledgedEvents(venId, events) {
  220. const opted = (await nantum.fetchOpted(venId)) || [];
  221. return events.filter(
  222. event =>
  223. opted.filter(
  224. optedItem =>
  225. optedItem.eventId === event.eventDescriptor.eventId &&
  226. optedItem.modificationNumber ===
  227. event.eventDescriptor.modificationNumber,
  228. ).length === 0,
  229. );
  230. }
  231. async function pollForEvents(
  232. oadrPoll,
  233. clientCertificateCn,
  234. clientCertificateFingerprint,
  235. ) {
  236. logger.info(
  237. 'pollForEvents',
  238. oadrPoll,
  239. clientCertificateCn,
  240. clientCertificateFingerprint,
  241. );
  242. const requestVenId = oadrPoll.venId;
  243. validateVenId(requestVenId, clientCertificateFingerprint, true);
  244. const event = await nantum.fetchEvent(requestVenId);
  245. const filteredEvents = await filterOutAcknowledgedEvents(
  246. requestVenId,
  247. convertToOadrEvents(event),
  248. );
  249. if (filteredEvents.length > 0) {
  250. return {
  251. responseCode: '200',
  252. responseDescription: 'OK',
  253. responseRequestId: '', // required field, but empty is allowed as per spec
  254. requestId: '',
  255. vtnId: vtnId,
  256. events: filteredEvents,
  257. };
  258. }
  259. return undefined;
  260. }
  261. function validateVenId(requestVenId, clientCertificateFingerprint, required) {
  262. if (requestVenId === clientCertificateFingerprint) {
  263. return;
  264. }
  265. if (!required && requestVenId == null) {
  266. return;
  267. }
  268. if (required && requestVenId == null) {
  269. const error = new Error('VenID is missing');
  270. error.responseCode = 452;
  271. throw error;
  272. }
  273. const error = new Error('VenID is invalid');
  274. error.responseCode = 452;
  275. throw error;
  276. }
  277. module.exports = {
  278. pollForEvents,
  279. retrieveEvents,
  280. updateOptType,
  281. };