Skip to content

Migrate POSTTRAN STEP15 (CBTRN02C) to Java 21 + Spring Batch with byte-parity harness - #197

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1788239485-posttran-migration
Open

devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1788239485-posttran-migration

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 1, 2026

Copy link
Copy Markdown

Summary

First migration slice of the CardDemo mainframe modernization: POSTTRAN.jcl STEP15 → CBTRN02C
(daily transaction posting) reimplemented in Java 21 + Spring Batch, with the legacy COBOL kept as
the reference oracle. Scope selection and the source map were approved in Phase 1; this PR is the
implementation and its evidence. No legacy source is modified.

The migrated step reproduces the legacy program byte for byte on both input sets — posted
transactions, account after-images, category balances, the reject file, SYSOUT and the return code:

input set posted rejects reject reasons reached RC
shipped sample, 300 records 262 38 102 only 4
generated mock set, 11 records 6 5 100, 101, 102, 103 4

One field is normalised out of the diff: TRAN-PROC-TS, stamped from FUNCTION CURRENT-DATE
(CBTRN02C.cbl:692-705) and therefore never equal across two runs. It is asserted separately for
format, and the migrated clock is injectable so a run can be pinned.

Reproduce with GnuCOBOL + Java 21 + Maven:

migration/posttran/harness/parity.sh                                     # mock set
migration/posttran/harness/parity.sh migration/posttran/testdata/shipped # the drop's own sample
cd migration/posttran/java && mvn test                                   # replays against recorded baselines

Why a generated mock set was needed

The shipped 300-record sample fires exactly one of the four reject reasons — all 38 rejects are
0102 OVERLIMIT. Reasons 100/101/103, the TCATBAL create branch, the negative-amount branch and
both >= boundaries are never reached, so parity on it alone proves little.
harness/gen_mock_data.py derives inputs that reach all of them from the drop's own accounts, cards
and category balances (real records, real limits; boundary amounts computed from them), and the
legacy program's behaviour on each was recorded and matched rather than assumed.

That data exposed one ordering detail worth flagging, faithfully preserved in the port:

// CBTRN02C.cbl:403-419 — 103 is assigned unconditionally *after* the limit check
if (accountRecord.creditLimit().compareTo(tempBal) < 0) outcome = OVERLIMIT;   // 102
if (accountRecord.expirationDate().compareTo(tran.origDate()) < 0) outcome = EXPIRED; // 103 overwrites

A transaction that is both overlimit and expired is reported as 103; the overlimit condition
leaves no trace in the reject record.

Implementation notes

  • Sign handling decides whether the money is right. Amounts are zoned decimal with an
    overpunched sign, so the legacy program must be compiled cobc -x -fsign=EBCDIC — without it
    GnuCOBOL reads every negative amount as positive, which is how the Phase 1 report's "43 rejects"
    figure arose (corrected to 38 in this PR). codec/Zoned implements the same convention;
    BigDecimal carries every amount.
  • Records are edited in place, not rebuilt from parsed fields, so FILLER and untouched bytes
    survive — a precondition for a byte-level diff.
  • io/KeyedStore stands in for the four VSAM KSDS: an in-memory keyed map unloaded in key
    order, which is what makes the diff against the legacy IDCAMS-style unload possible. Swapping in
    a database is confined to that one class.
  • Chunk size is 1 — the step validates each transaction against account state it has just
    rewritten, so records are not independent.
  • Every behaviour in TransactionPoster carries the COBOL paragraph name and cited source lines.
  • Maven, not Gradle: Gradle is not installed on this machine. Worth revisiting if the module grows.

What this does not prove

This is self-consistency parity against GnuCOBOL, not against z/OS — a mainframe baseline
(§6.1 of the Phase 1 report) is still needed to show the reference itself is faithful. The
CEE3ABD abend path and the TRANFILE OPEN OUTPUT / DISP=SHR question remain unvalidated, and
parity covers one step, not the job stream.

Details: migration/posttran/README.md; full source map and Phase 2 outcome in
docs/phase1-posttran-inventory.md.

Devin-Org: engineering

Link to Devin session: https://app.devin.ai/sessions/4e51bfff94734bab8893f0e376063f0d
Open in Devin Desktop: https://app.devin.ai/desktop/session/4e51bfff94734bab8893f0e376063f0d?variant=devin


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Devin Review (Staging)

… with legacy parity harness

Co-Authored-By: Parker Duff <pwjduff@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants