Building an EAI Middleware Layer
A Router That Picks the Destination by Transaction Code
Goal
Refine the business side's interface list into a routing table (data), and build a router that decides the destination by looking only at the message header, following the rule priority (ORG > EXACT > longer PREFIX > NONE).
Why it matters
If the hub's routing is wrong, instead of an error, messages quietly go to a different system. So you keep the rules in a table rather than in code, state who wins when they overlap in code rather than by row order, and leave which rule matched in an audit log. Returning an unroutable message as an E101 response instead of swallowing it is also the hub's responsibility — if swallowed, the sending channel waits until it times out.
Steps
- Read
/opt/lab/fixtures/eaimw/route/interfaces.csvand section 2 of/opt/lab/fixtures/eaimw/route/ROUTING.md, and create/root/eaimw/route/routes.csv. Headertx_code,target,mode,timeout_ms. Only rows whose status is운영(in operation); for the same transaction code, only the one row with the largest version (as an integer); transaction code and target with whitespace removed and uppercase; mode asSYNC/ASYNC; timeout_ms as an integer of seconds × 1000. Also load the prefix rules (CD*) as they are, and sort by the byte order of tx_code (LC_ALL=C sort). - Create
/root/eaimw/route/router.py.python3 router.py --routes <표> lookup <거래코드>(table, transaction code) finds the exact-match row, prints one line of JSON{"target": ..., "rule": "EXACT", "mode": ..., "timeout_ms": 정수}(where the integer goes in place of the placeholder) and exits with 0. If there is none, print{"target": null, "rule": "NONE"}and exit code 3. It must not crash even when the table has prefix rows. If--routesis omitted, it uses/root/eaimw/route/routes.csv. - Add the prefix rule. For
CD*, if the transaction code starts withCD,ruleisPREFIX. An exact match beats a prefix, and a longer prefix beats a shorter prefix. It must be independent of the table's row order. - Add the institution rule. In
lookup <거래코드> --rcv-org <기관>(transaction code, institution), if the institution is not100, it ends with 0 and{"target": "FEP", "rule": "ORG"}regardless of the transaction code. If--rcv-orgis omitted, it is our own bank (100). - Add
python3 router.py --routes <표> route <전문파일>(table, message file). Read the header with the common librarylhstd.parse, decide by TX_CODE and RCV_ORG, addguid,tx_codeandrcv_orgto the step 4 JSON and print it on one line (the exit codes are the same as lookup). For a message with a wrong format,E102 <사유>(E102 followed by the reason) on the first line and exit code 2. - Add
route <전문파일> --reply <경로>(message file, path). If the decision is NONE, uselhstd.replyto write a response message with response codeE101to that path, and exit with code 3. Do not create the file for a message that was routed. - Add the global option
--audit <경로>(path). When given, for every decision ofroute(including NONE) append one line of JSON to that file — keysts(decision time),guid,tx_code,rcv_org,rule,target. When not given, write nothing. - Run the whole inbox
/opt/lab/fixtures/eaimw/route/inbox/through the route of step 5 and create/root/eaimw/route/dispatch.csv. Headerfile,tx_code,rule,target, in file name order. A format error is파일,,E102,(with the file name in the first column), and for NONE leave target empty.
Notes
- Reading the common library: after
sys.path.insert(0, "/opt/lab/fixtures/eaimw/lib"),import lhstd. You did the header parsing yourself in module 1, so here you use the single common module. interfaces.csvis a file exported from Excel, so it has a BOM at the start. Python opens it withencoding="utf-8-sig".- Common mistake: comparing versions as strings (
"9" > "10"). Choosing the first matching row of the table (if you sort it, the result changes). - The grader builds a random table with new transaction codes and destination names each time and passes it with
--routes. If you write the destinations in the code, you cannot pass. - The global options (
--routes,--audit) come before the subcommands (lookup,route). Using argparse's subparsers gives this shape.
Refine the interface list into a routing table
From /opt/lab/fixtures/eaimw/route/interfaces.csv, pick only the latest version in operation and create /root/eaimw/route/routes.csv (tx_code,target,mode,timeout_ms).
The order matters — first keep only rows whose status is in operation, then for each transaction code (whitespace removed, uppercase) convert the version to int and pick the row with the largest. There is a code in which versions 9 and 10 exist together. Timeouts also include decimals like 1.5.
An exact-match router
The lookup of /root/eaimw/route/router.py finds exactly the same transaction code in the table and answers in JSON, and if there is none, ends with NONE and exit code 3.
Read the table with csv.DictReader and build a dictionary of transaction code → row. Skip rows ending in * at this step. The table path comes through --routes — the grader passes its own table. Not finding a destination is not an error but a result, so print the JSON and then end with 3.
The prefix rule — the longer wins
Treat rows ending in * as prefix rules. Exact match > long prefix > short prefix, regardless of the table's row order.
If you sort the prefix list by length in descending order, the first match is the longest. If you choose the "first matching row" by file order, the result changes the moment someone sorts the table. Compare with startswith — in matches even in the middle.
The institution rule comes first
Add --rcv-org to lookup, and if the receiving institution is not 100, send to FEP (rule ORG) regardless of the transaction code.
ORG is rank 1, so you look at it before looking in the table. Even a transaction code not in the table is FEP, not NONE, if it is addressed to an external institution. If you set the default of --rcv-org to our own bank, 100, the calls from the earlier steps keep working as they are.
Route by the message header
route reads the header with lhstd.parse and outputs a decision JSON (including guid, tx_code and rcv_org), and rejects format errors with E102 and exit code 2.
Read with open(file, 'rb') and pass it to lhstd.parse, and you get TX_CODE, RCV_ORG and GUID. You must look at the receiving institution, not the sending institution (SND_ORG) — a message an insurer sent to us must not go to the external system. Catch lhstd.FrameError and end with E102.
Return an unroutable message as E101
Add --reply to route, and if NONE, write an E101 response message to that path and end with exit code 3.
lhstd.reply(header, 'E101') builds a response message (bytes) that keeps the transaction code and GUID and swaps the institutions. Write the file in 'wb'. Do not create it for a message that was routed.
Leave the routing decision in an audit log
When the global option --audit is given, append one line of JSON containing ts, guid, tx_code, rcv_org, rule and target for every route decision (including NONE).
open(path, 'a') appends and 'w' erases and writes. An audit log must not be erased. If you leave only the destination, you cannot answer "why did it go there" — which rule matched (rule) is the key.
Dispatch the 30 messages in the inbox
Run the whole inbox through route and create /root/eaimw/route/dispatch.csv (file,tx_code,rule,target).
Run route for each file and branch on the exit code — 2 is a format error and 3 is NONE. If you count the results by destination, you can see where they crowd to one side. Do not force yourself to write the transaction code of a message with a wrong format.