---
module: agent.nh-tcr-journal
title: NH TCR Journal — transaction + fault events from Nautilus Hyosung Teller Cash Recyclers
tab: Architect
order: 30
audience: dev
---

> **Internal · architecture.** Written 2026-07-01 from `hiveops-module-nh-tcr-journal` source. Verify against code before relying on specifics.

## Module shape

Package `com.hiveops.nhtcr`, two classes, no controllers/DB/Kafka (this is an agent module, not a microservice — see [technical.agent]):

- **`NhTcrJournalModule`** — the `AgentModule` SPI entry point (registered via `META-INF/services/com.hiveops.core.module.AgentModule`). Owns lifecycle (`initialize()` → `isEnabled()` → `start()` → `stop()`), the shared `ScheduledExecutorService`, config resolution, and the `IncidentEventClient` construction.
- **`TcrJournalMonitor`** — the actual ejournal follower + parser. A plain constructed object (`new TcrJournalMonitor(journalSource, atmName, client)`), not its own `AgentModule`.

Module identity from `NhTcrJournalModule`: `getName()` = `nh-tcr-journal`, `getVersion()` = `1.0.0`, `getLogPackage()` = `com.hiveops.nhtcr`.

## Activation gating (in `initialize()`)

Three gates, all fail-soft (log + return, `ready` stays `false`, `isEnabled()` returns `false` — no exception thrown to agent startup):

1. `device.type` (uppercased/trimmed) must equal `NH_TCR`.
2. A `JournalSource` must be resolvable from the ext properties — `findJournalSource()` scans `context.getExtensionProperties()` for the first entry with `journal.dir` set and `type` in `{NH_TCR, MAINJOURNAL}`, then builds `new JournalSource(journalDir, type, fileFormat, filenameFormat, atmName)` (`file.format` defaults to `UTF8`).
3. An endpoint must exist: `agent.endpoint` if set (prefix `agent`), else `incident.endpoint` (prefix `incident`).

On success it reads `nh.tcr.journal.poll.interval.sec` (default `60`), constructs the monitor, and schedules `poll()` on a single daemon thread named `nh-tcr-journal` via `scheduleAtFixedRate(this::poll, 0, pollSec, SECONDS)`. Note: unlike the sibling ATEC module, the poll interval property here **is** applied.

## Flow: tail → parse → detect → emit

```
NhTcrJournalModule (scheduler, every nh.tcr.journal.poll.interval.sec)
  → poll() → TcrJournalMonitor.poll()
      → journalSource.getCurrentJournalFile()   // resolves today's ej_..._{YYYY}{MM}{DD} file
      → RandomAccessFile tail from stored byte offset (UTF-16 LE, 2 MB/poll cap)
      → split into complete lines → processLine() per line
```

### Tail-by-offset, UTF-16 LE

`TcrJournalMonitor` uses byte-offset tailing, but with a TCR-specific twist — the file is decoded as **UTF-16 LE**:
- Tracks `currentFile` + `filePosition` (raw byte offset).
- Daily rollover: when `getCurrentJournalFile()` returns a different file, it resets `filePosition=0`, clears block state, logs `switched to ...`.
- Each poll seeks to `filePosition`, reads up to 2 MB, trims to an even byte count (UTF-16 needs pairs), decodes via `StandardCharsets.UTF_16LE.newDecoder()`, and splits on `\r?\n`.
- Only **complete** lines are processed; a trailing partial line is left unread. It advances `filePosition` by `charsProcessed * 2` (2 bytes/char) so the next poll resumes exactly where parsing stopped — the partial line is re-read once it completes.
- No explicit truncation guard: if `fileLength <= filePosition` the poll simply returns (no work), and a same-name rewrite would only be reprocessed if the file object changes at rollover.

### Line parsing (`processLine`)

`JOURNAL_LINE` regex: `^﻿?\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}-\d+\]\[(\d+)\]\[TCR\](\w*)(.*)` — captures `level`, `eventName`, `params`. (The optional `﻿?` absorbs a leading BOM.)

- A `level == 5` line with an **empty** event name opens a TRANSACTION RECORD block (`inTransactionBlock = true`); subsequent non-matching lines are buffered.
- Any other timestamp line first flushes an open block, then routes the event name through `handleEvent()`.
- Non-matching lines while a block is open are appended to the buffer; a blank line inside a block is buffered too.

### Detect → event mapping

`handleEvent()` (single-line device events):

| Journal `EventName` | Condition | Emitted event type |
|---|---|---|
| `CashDispenserStatusChanged` | params == `NODISPENSE` | `DISPENSER_JAM` |
| `BNAItemsRefused` | always | `DISPENSER_JAM` |
| `CashUnitThresholdChanged` | always | `CASSETTE_LOW` |
| `AcceptorStatusChanged` | params != `OK` | `SELF_TEST_FAILED` |

`flushTransactionBlock()` (multi-line): `extract()` pulls `KEY : value` fields (`TRANSACTION NO.`, `TRANSACTION TYPE`, `TRANSACTION RESULT`, `TRANSACTION AMOUNT`, `MACHINE BALANCE`, `TELLER ID`, `BRANCH NUMBER`) and `extractBracketed()` pulls `Error Code [..]`. If both transaction number and type are missing the block is dropped. Otherwise it builds a `TCR Transaction #.. | Type: .. | Result: ..` summary string and emits it as a single **`OPERATOR_ACTION`** event (error code appended only if non-zero).

## How events reach the platform

Every event goes through the shared `IncidentEventClient` (constructed once in `initialize()`), the same client class used by `journal-events` and the other event-reporting modules — see [technical.agent] for the full outbound API surface.

```
TcrJournalMonitor.sendEvent(eventType, details)
  → CreateJournalEventRequest.builder()
      .agentAtmId(atmId)        // ATM name string, resolved to incident's numeric atmId server-side
      .eventType(eventType)     // free-form String, not validated client-side
      .eventDetails(details)
      .eventSource("NH_TCR_JOURNAL")
      .build()
  → IncidentEventClient.sendEvent(req)
      → HTTP POST {agent.endpoint or incident.endpoint}/api/journal-events
```

`eventType` is a plain `String` on `CreateJournalEventRequest` (`hiveops-agent-journal` DTO). The four types this module emits (`OPERATOR_ACTION`, `DISPENSER_JAM`, `CASSETTE_LOW`, `SELF_TEST_FAILED`) are all standard HiveIQ event types recognized server-side; there is no TCR-specific event string here. Send failures (`IOException`) are caught and logged — **no retry or queueing**.

## Auth / endpoint selection

`initialize()` picks the property prefix from whether `agent.endpoint` is set (`agent`) or not (`incident`), then `HttpClientHelper.loadSettingsFromProperties(props, prefix)` resolves timeouts, institution key, and the bearer token (`agent.auth.token`). No bespoke auth in this module — it is 100% the shared agent HTTP settings pattern.

## Platform abstraction

None used directly. The follower reads files via `RandomAccessFile` / `java.nio` and resolves the daily filename through `JournalSource` — no `PlatformService`/`PathResolver` dependency. In practice NH TCR machines are Windows, but the constraint is enforced by deployment convention (`device.type=NH_TCR` gating) rather than a runtime OS check.

## Cross-links
- [technical.agent] — module system, lifecycle, shared HTTP client settings, full outbound API surface
- [platform.module-map]
