<?xml version="1.0" encoding="UTF-8"?>
<!--
  ADT / ORM / ORU to normalized events (event-schema §5 to §8, api-platform §12.3, §17).
  The source transformer builds a deterministic `{ "events": [...] }` body (event ids derive from
  the feed and MSH-10, so a resend is a duplicate, AC-API-22); the queued JavaScript Writer sends it
  to POST /v1/events/batch at dispatch time with a fresh token. Adjust the location, id-system and
  code-system values in the transformer to the site's mapping (or prefer the raw HL7 sender, which
  maps server-side through the mapping profile).
-->
<channel version="4.5.0">
  <id>0b1d2c3e-4f50-4a61-8b72-93a4b5c6d7e2</id>
  <nextMetaDataId>2</nextMetaDataId>
  <name>Integration API - Normalized events</name>
  <description>ADT A01/A04 arrival, A02 location, A03 departure, A11 cancel; ORM O01 order placed; ORU R01 order resulted.</description>
  <revision>1</revision>
  <sourceConnector version="4.5.0">
    <metaDataId>0</metaDataId>
    <name>sourceConnector</name>
    <properties class="com.mirth.connect.connectors.tcp.TcpReceiverProperties" version="4.5.0">
      <pluginProperties/>
      <listenerConnectorProperties version="4.5.0">
        <host>0.0.0.0</host>
        <port>6662</port>
      </listenerConnectorProperties>
      <sourceConnectorProperties version="4.5.0">
        <responseVariable>Auto-generate (After source transformer)</responseVariable>
        <respondAfterProcessing>false</respondAfterProcessing>
        <processBatch>false</processBatch>
        <firstResponse>false</firstResponse>
        <processingThreads>1</processingThreads>
        <queueBufferSize>1000</queueBufferSize>
      </sourceConnectorProperties>
      <transmissionModeProperties class="com.mirth.connect.plugins.mllpmode.MLLPModeProperties">
        <pluginPointName>MLLP</pluginPointName>
        <startOfMessageBytes>0B</startOfMessageBytes>
        <endOfMessageBytes>1C0D</endOfMessageBytes>
        <useMLLPv2>false</useMLLPv2>
        <ackBytes>06</ackBytes>
        <nackBytes>15</nackBytes>
        <maxRetries>2</maxRetries>
      </transmissionModeProperties>
      <serverMode>true</serverMode>
      <receiveTimeout>0</receiveTimeout>
      <bufferSize>65536</bufferSize>
      <maxConnections>10</maxConnections>
      <keepConnectionOpen>true</keepConnectionOpen>
      <dataTypeBinary>false</dataTypeBinary>
      <charsetEncoding>DEFAULT_ENCODING</charsetEncoding>
      <respondOnNewConnection>0</respondOnNewConnection>
    </properties>
    <transformer version="4.5.0">
      <elements>
        <com.mirth.connect.plugins.javascriptstep.JavaScriptStep version="4.5.0">
          <name>Build normalized events</name>
          <sequenceNumber>0</sequenceNumber>
          <enabled>true</enabled>
          <script><![CDATA[
// Deterministic: the same inbound message always builds the same body (no clock, no token).
var SPEC_VERSION = '1.1';
var FEED = { ADT: 'adt', ORM: 'orders', ORU: 'results' };
var SYSTEM = configurationMap.get('ingest.api.sourceSystem') || 'epic';
var ID_SYSTEM = configurationMap.get('ingest.api.visitIdSystem') || 'epic-csn';
var CODE_SYSTEM = configurationMap.get('ingest.api.orderCodeSystem') || 'urn:site:orderable';
var SITE = configurationMap.get('ingest.api.site') || undefined;

function hl7Time(ts) {
  // YYYYMMDDHHMM[SS][.S+][+/-ZZZZ] to ISO 8601 with offset (the site offset when absent).
  var s = String(ts);
  var m = s.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})?(?:\.\d+)?([+-]\d{4})?$/);
  if (!m) {
    return null;
  }
  var offset = m[7] || (configurationMap.get('ingest.api.siteOffset') || '-0500');
  return m[1] + '-' + m[2] + '-' + m[3] + 'T' + m[4] + ':' + m[5] + ':' + (m[6] || '00') +
    offset.substring(0, 3) + ':' + offset.substring(3);
}

var type = msg['MSH']['MSH.9']['MSH.9.1'].toString();
var trigger = msg['MSH']['MSH.9']['MSH.9.2'].toString();
var controlId = msg['MSH']['MSH.10']['MSH.10.1'].toString();
var feed = FEED[type] || 'other';
var occurredAt = hl7Time(msg['EVN']['EVN.2']['EVN.2.1'].toString()) ||
  hl7Time(msg['MSH']['MSH.7']['MSH.7.1'].toString());
var visit = msg['PV1']['PV1.19']['PV1.19.1'].toString();
var base = function (suffix, eventType, data) {
  var event = {
    data: data,
    id: SYSTEM + ':' + feed + ':' + controlId + ':' + suffix,
    occurredAt: occurredAt,
    source: { feed: feed, kind: 'ehr', messageId: controlId, messageType: type + '^' + trigger, system: SYSTEM },
    specVersion: SPEC_VERSION,
    type: eventType
  };
  if (visit !== '') {
    event.encounter = { externalId: visit, idSystem: ID_SYSTEM };
  }
  if (SITE) {
    event.siteCode = SITE;
  }
  return event;
};
var location = function () {
  var code = msg['PV1']['PV1.3']['PV1.3.1'].toString();
  var bed = msg['PV1']['PV1.3']['PV1.3.3'].toString();
  return code === '' ? undefined : { code: bed === '' ? code : code + '-' + bed };
};

var events = [];
if (type === 'ADT' && (trigger === 'A01' || trigger === 'A04')) {
  events.push(base('arrived', 'encounter.arrived', {
    arrivedAt: hl7Time(msg['PV1']['PV1.44']['PV1.44.1'].toString()) || occurredAt,
    location: location(),
    patient: { mrn: msg['PID']['PID.3']['PID.3.1'].toString() || undefined }
  }));
} else if (type === 'ADT' && trigger === 'A02') {
  var where = location();
  if (where !== undefined) {
    events.push(base('location', 'encounter.location_changed', {
      effectiveAt: occurredAt,
      location: where,
      reason: 'transfer'
    }));
  }
} else if (type === 'ADT' && trigger === 'A03') {
  events.push(base('departed', 'encounter.departed', {
    departedAt: hl7Time(msg['PV1']['PV1.45']['PV1.45.1'].toString()) || occurredAt,
    disposition: 'unknown'
  }));
} else if (type === 'ADT' && trigger === 'A11') {
  events.push(base('cancelled', 'encounter.cancelled', { reason: 'entered_in_error' }));
} else if (type === 'ORM' && trigger === 'O01') {
  var obr = msg['OBR'];
  events.push(base('order-' + obr['OBR.2']['OBR.2.1'].toString(), 'order.placed', {
    category: 'lab',
    code: { code: obr['OBR.4']['OBR.4.1'].toString(), system: CODE_SYSTEM },
    order: { placerOrderId: obr['OBR.2']['OBR.2.1'].toString() },
    orderedAt: hl7Time(msg['ORC']['ORC.9']['ORC.9.1'].toString()) || occurredAt
  }));
} else if (type === 'ORU' && trigger === 'R01') {
  var result = msg['OBR'];
  var status = result['OBR.25']['OBR.25.1'].toString();
  events.push(base('result-' + result['OBR.2']['OBR.2.1'].toString() + '-' + status, 'order.status_changed', {
    order: { placerOrderId: result['OBR.2']['OBR.2.1'].toString() },
    // Never the result itself: only that the order moved (product rule: results are never shown).
    status: status === 'P' ? 'preliminary' : status === 'C' ? 'corrected' : 'resulted'
  }));
}

if (events.length === 0) {
  // Not mapped by this template: nothing to send (filter these at the engine).
  destinationSet.removeAll();
}
channelMap.put('ingestBody', JSON.stringify({ events: events }));
]]></script>
        </com.mirth.connect.plugins.javascriptstep.JavaScriptStep>
      </elements>
      <inboundDataType>HL7V2</inboundDataType>
      <outboundDataType>HL7V2</outboundDataType>
    </transformer>
    <filter version="4.5.0">
      <elements/>
    </filter>
    <transportName>TCP Listener</transportName>
    <mode>SOURCE</mode>
    <enabled>true</enabled>
    <waitForPrevious>true</waitForPrevious>
  </sourceConnector>
  <destinationConnectors>
    <connector version="4.5.0">
      <metaDataId>1</metaDataId>
      <name>Integration API /v1/events/batch</name>
      <properties class="com.mirth.connect.connectors.js.JavaScriptDispatcherProperties" version="4.5.0">
        <pluginProperties/>
        <destinationConnectorProperties version="4.5.0">
          <queueEnabled>true</queueEnabled>
          <sendFirst>false</sendFirst>
          <retryIntervalMillis>10000</retryIntervalMillis>
          <regenerateTemplate>true</regenerateTemplate>
          <retryCount>0</retryCount>
          <rotate>false</rotate>
          <includeFilterTransformer>false</includeFilterTransformer>
          <threadCount>1</threadCount>
          <threadAssignmentVariable/>
          <validateResponse>false</validateResponse>
          <resourceIds class="linked-hash-map">
            <entry>
              <string>Default Resource</string>
              <string>[Default Resource]</string>
            </entry>
          </resourceIds>
          <queueBufferSize>1000</queueBufferSize>
          <reattachAttachments>true</reattachAttachments>
        </destinationConnectorProperties>
        <script><![CDATA[
if (ingestPaused()) {
  return new Response(QUEUED, '', 'Paused for Retry-After', '');
}
var body = String($('ingestBody'));
var key = 'mirth-' + channelId + '-' + connectorMessage.getMessageId();
var result = ingestSend('/v1/events/batch', 'application/json', body, key);
if (result.status === 200) {
  // Per-event results: rejected events go to the error channel; the batch never blocks the queue.
  var parsed = JSON.parse(result.body);
  for (var i = 0; i < parsed.results.length; i++) {
    if (parsed.results[i].status === 'rejected') {
      router.routeMessage('Integration API errors', JSON.stringify(parsed.results[i]));
    }
  }
  return new Response(SENT, result.body, 'Accepted', '');
}
return ingestResponse(result, 'Integration API errors');
]]></script>
      </properties>
      <transformer version="4.5.0">
        <elements/>
        <inboundDataType>HL7V2</inboundDataType>
        <outboundDataType>HL7V2</outboundDataType>
      </transformer>
      <responseTransformer version="4.5.0">
        <elements/>
        <inboundDataType>RAW</inboundDataType>
        <outboundDataType>RAW</outboundDataType>
      </responseTransformer>
      <filter version="4.5.0">
        <elements/>
      </filter>
      <transportName>JavaScript Writer</transportName>
      <mode>DESTINATION</mode>
      <enabled>true</enabled>
      <waitForPrevious>true</waitForPrevious>
    </connector>
  </destinationConnectors>
  <preprocessingScript>return message;</preprocessingScript>
  <postprocessingScript>return;</postprocessingScript>
  <deployScript>return;</deployScript>
  <undeployScript>return;</undeployScript>
  <properties version="4.5.0">
    <clearGlobalChannelMap>true</clearGlobalChannelMap>
    <messageStorageMode>DEVELOPMENT</messageStorageMode>
    <encryptData>true</encryptData>
    <removeContentOnCompletion>true</removeContentOnCompletion>
    <removeOnlyFilteredOnCompletion>false</removeOnlyFilteredOnCompletion>
    <removeAttachmentsOnCompletion>true</removeAttachmentsOnCompletion>
    <initialState>STARTED</initialState>
    <storeAttachments>false</storeAttachments>
    <metaDataColumns/>
    <attachmentProperties version="4.5.0">
      <type>None</type>
      <properties/>
    </attachmentProperties>
    <resourceIds class="linked-hash-map">
      <entry>
        <string>Default Resource</string>
        <string>[Default Resource]</string>
      </entry>
    </resourceIds>
  </properties>
</channel>
