---
module: agent.atm-config-sync
title: ATM Config Sync — pushes ATM hardware/software/cassette config to the platform
tab: Architect
order: 30
audience: dev
---

> **Internal · architecture.** Written 2026-07-01 from `hiveops-module-atm-config-sync` source. Verify against code before relying on specifics.

## Module shape

Package `com.hiveops.configsync`, a **single class**, no controllers/DB/Kafka (this is an agent module, not a microservice — see [technical.agent]):

- **`AtmConfigSyncModule`** — the `AgentModule` SPI entry point (registered via `META-INF/services/com.hiveops.core.module.AgentModule`). Owns the full lifecycle (`initialize()` → `isEnabled()` → `start()` → `stop()`), the `IncidentEventClient`, and a single-thread `ScheduledExecutorService`.

No separate monitors or parsers of its own — it **orchestrates existing agent parsers/readers** (from `hiveops-version`, `hiveops-agent-journal`, and the platform layer) and assembles their output into one DTO. All heavy lifting (applog parsing, MoniPlus2S parsing, cassette/version reads) is delegated to shared, `provided`-scope components supplied by the fat JAR at runtime.

## Lifecycle & flow

Unlike the log-tailing modules, this is a **poll-and-assemble-then-POST** module, not a log tail:

```
initialize(ctx)
  → resolve endpoint: agent.endpoint (preferred) else incident.endpoint; if neither → return (ready=false)
  → prefix = agent.endpoint != null ? "agent" : "incident"
  → HttpClientSettings via HttpClientHelper.loadSettingsFromProperties(props, prefix)
  → new IncidentEventClient(settings, atmName, country)
  → ready = true
  → syncAtmConfig()            // one immediate sync at startup

isEnabled(ctx) → ready         // true only if an endpoint was resolved

start()
  → ConfigResyncRegistry.register(this::syncAtmConfig)   // on-demand RESYNC_CONFIG hook
  → ScheduledExecutorService (daemon thread "config-resync")
      .scheduleAtFixedRate(syncAtmConfig, initialDelay=0, period=6h)

stop() → scheduler.shutdownNow()
```

### `syncAtmConfig()` — the one method that does everything

```
awaitReconnectJitter()                     // ConnectionStateTracker — post-reconnect anti-stampede
AtmConfigSyncRequest req = new(...)
  req.atmName / country / agentVersion      // from ModuleContext + agentContext.getVersion()
  req.startupDetectionMethod                // startup-info.properties → startup.detection.method (if file exists)
  req.timezone = TimeZone.getDefault().getID()
  req.institutionKey                        // incident.institution.key (only)
  // platform ConfigurationReader (Optional-returning reads, each set only if present):
  req.systemName / customizingVersion / hotfixVersion / countrySpecificVersion
  req.cassettes  = readCassetteDenominations() → List<CassetteConfig>(position, currency, value)
  // applog file (only if applog.events.dir set):
  req.hyosungVersion = HyosungVersionParser.parseFromFile(applogFile)   // app/SDK/GSDK/Nextware
  req.systemInfo     = AppLogHeaderParser.parseFromFile(applogFile)     // OS + installed programs/packages
  // MoniPlus2S dir (moniplus2s.dir, or C:\Hyosung\MoniPlus2S on Windows):
  req.moniplus2sConfig        = MoniPlus2SConfigParser.parse()
  req.cassettes (fallback)    = parseCassettesFromHostNoteTypes(...)    // only if cassettes still empty
  req.moniplus2sLayeredConfig = MoniPlus2SConfigParser.parseAllLayers()
client.syncConfig(req)                       // POST — see below
```

Every step is best-effort: a null/absent source is skipped, and the whole body is wrapped in try/catch that logs `ATM config sync failed: <msg>` (no rethrow, no retry queue). The scheduler's next 6h tick is the retry.

### Applog file resolution
`applog.events.dir` + `applog.events.filename.format` (default `APLog{YYYY}{MM}{DD}.log`) feed an `AppLogSource(dir, format, atmName)`, whose `getCurrentLogFile()` returns the file for today. That single file is handed to both `HyosungVersionParser` and `AppLogHeaderParser`.

### MoniPlus2S path handling
Java `.properties` parsing strips single backslashes, so `moniplus2s.dir=C:\Hyosung\MoniPlus2S` arrives as `C:HyosungMoniPlus2S`. The module checks `new File(dir).isDirectory()`; if it fails it logs a warning, nulls the value, and (on Windows) falls back to the hardcoded `C:\Hyosung\MoniPlus2S`.

## How the payload reaches the platform

Transport is the shared `IncidentEventClient` (from `hiveops-agent-journal`) — the same client used by the event modules, but a **different method and endpoint**:

```
IncidentEventClient.syncConfig(AtmConfigSyncRequest req)
  → POST {agent.endpoint | incident.endpoint}/api/atms/config/sync
     Content-Type: application/json     (Jackson-serialized AtmConfigSyncRequest)
  → accepts HTTP 200 / 201 / 202; anything else → logErrorResponse + throw IOException
```

Note this is **not** `/api/journal-events` — config sync has its own `/api/atms/config/sync` endpoint. The backend keys off `atmName`/`country` in the body to find/update the device record. In production the base URL points at **agent-proxy** (`agent.endpoint`), which forwards to the device/incident backends; the legacy `incident.endpoint` path targets incident directly.

## On-demand resync wiring

`ConfigResyncRegistry` (in `hiveops-core`) is a static single-slot registry decoupling the `RESYNC_CONFIG` command handler from the resync implementation:

```
AtmConfigSyncModule.start() → ConfigResyncRegistry.register(this::syncAtmConfig)   // sets one volatile callback
CommandCenterModule (RESYNC_CONFIG task) → ConfigResyncRegistry.trigger() → callback.run()
```

It holds exactly **one** `volatile Runnable` — last registrant wins. The registry's javadoc references `JournalEventModule` as the historical registrant, but in the current tree **`AtmConfigSyncModule` is the only caller of `register()`**, so it owns the `RESYNC_CONFIG` trigger. If another module ever also registers, whichever `start()`s last silently overwrites the other. If nothing registered (module disabled), `trigger()` logs a warning and does nothing.

## Platform abstraction

Uses the platform layer directly (unlike the file-tailing modules):

- `context.getPlatformService().getConfigurationReader()` → `readSystemName / readCustomizingVersion / readHotfixVersion / readCountrySpecificVersion` (all `Optional`) and `readCassetteDenominations()`.
- `context.getPlatformService().isWindows()` → gates the `C:\Hyosung\MoniPlus2S` fallback.

The `ConfigurationReader` abstracts Windows (registry) vs Linux (file/env) config sources, so the same module works cross-platform without OS branching in this class (aside from the Windows-only MoniPlus2S default).

## Cross-links
- [technical.agent] — module system, lifecycle, `ModuleContext`, shared `IncidentEventClient` / HTTP settings, platform abstraction
- [platform.module-map]
