feature: migrate COBOL portfolio TRANSACTION entity to a Java 21 / Spring Boot 3 service - #258
devin-ai-integration[bot] wants to merge 3 commits into
Conversation
…ng Boot 3 service Co-Authored-By: Dana Gajewski <danagajewski2018@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| public BatchRunSummary runBatch(List<PortfolioTransaction> transactions) { | ||
| List<TransactionProcessingResult> results = new ArrayList<>(); | ||
| int readCount = 0; | ||
| int processCount = 0; | ||
| int errorCount = 0; | ||
| boolean aborted = false; | ||
|
|
||
| for (PortfolioTransaction transaction : sequenceService.inKeySequence(transactions)) { | ||
| if (errorCount > MAX_ERRORS) { | ||
| aborted = true; | ||
| break; | ||
| } | ||
| readCount++; | ||
| ValidationOutcome outcome = validator.validate(transaction); |
There was a problem hiding this comment.
🔍 Batch driver ordering is imposed by the service rather than by the input file
runBatch sorts its input with sequenceService.inKeySequence(...) before iterating. The COBOL driver simply performs sequential READs on TRANSACTION-FILE, which is declared ORGANIZATION IS SEQUENTIAL (see OQ-7), so the physical order of the extract governs both the processing order and, crucially, which records are left unread when the 100-error limit trips. Re-sorting means the abort point of the Java run can differ from the mainframe run for the same extract. If the intent is a literal port of 0000-MAIN, preserving the caller's order (and documenting that key order is only assumed) would be closer to the legacy behaviour.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Deliberate, and worth stating in the notes rather than changing — thanks for flagging the abort-point consequence.
TRANHIST is a KSDS, and a sequential read of a KSDS is a read in key order, so the sort reproduces the order the batch sees when it is fed from the file. The divergence you describe is real only if the extract is re-ordered before the job runs, which is exactly the ambiguity already recorded as OQ-7 (ORGANIZATION IS SEQUENTIAL in PORTTRAN.cbl vs. the KSDS definition). Since runBatch takes a List, a caller that wants literal file order can pass records already in that order, and for a key-ordered extract the sort is a no-op.
Left as is because dropping the sort would make the 100-error abort point depend on the caller's list order, which is less reproducible than the key order the KSDS guarantees. BR-21/OQ-7 in MIGRATION-NOTES carry the reasoning.
…inal-status processing, audit metadata on failed postings, raw TRN-TYPE in errors, collision-safe sequence assignment, portable browse) Co-Authored-By: Dana Gajewski <danagajewski2018@gmail.com>
…d document the missing gateway-level auth Co-Authored-By: Dana Gajewski <danagajewski2018@gmail.com>
Summary
Migrates one legacy entity — the portfolio transaction (
TRNREC.cpy, VSAM KSDSTRANHIST) — into a self-contained Java 21 / Spring Boot 3 service underjava-migration/. Purely additive: no COBOL file is touched, so the before/after sits side by side in the repo.What the COBOL did.
PORTTRAN.cblreads transactions, validates them (2100-series: portfolio exists, type inBU/SL/TR/FE, quantity/price/amount positive), and — in2210–2240— applies the position update per type (buy adds units and cost, sell checks available units then subtracts, transfer is unimplemented, fee subtracts cost only), writing an audit record whose action depends on the type.PORTVALD.cblis the field-level validation subroutine (PORT+ 4 digits, investment types, amount bounds, numeric return codes0–4).PRCSEQ00.cblsupplies the 1-up sequence-numbering convention.What the service does now. The same rules, each tagged with where it came from:
grep -r "2220-PROCESS-SELL" java-migration/srclands on the Java that replaces it — that is the traceability contract for this PR, applied to every migrated type and method.Three findings a reviewer should know about (all in
MIGRATION-NOTES.md§4, nothing guessed silently):2200-UPDATE-POSITIONSis unreachable inPORTTRAN.cbl— no paragraph performs it;2100-VALIDATE-TRANSACTIONonly bumps counters. Both readings are preserved rather than picked:runBatch()is the literal batch (validate + count, no posting, nothing written back — the file is openedINPUT), whileprocess()wires validation to the position update (the evident intent) and is what the REST API exposes. (OQ-6)TRN-AMOUNT— the batch only validates it. Amount derivation is implemented as the most literal reading of aCOMPUTEwithoutROUNDED:quantity × pricetruncated to 2 dp (RoundingMode.DOWN), and flagged as derived rule BR-22. (OQ-1)vsam-definitions.txtand the copybook disagree: the KSDS declaresRECORD LENGTH 300 / KEY LENGTH 20, the copybook is 152 bytes with a 28-byte key group. The copybook wins (it is the layout source of truth); the key is modelled as 28 bytes. (OQ-4)2130-CHECK-AMOUNTSexemptsTRfrom the price and amount checks but not from the quantity check — reproduced literally, with a test pinning it, and raised as OQ-3.What's in the module
PortfolioTransactionwith an@EmbeddedId TransactionKey; every field carries Javadoc naming its COBOL field and PIC clause; 88-levels becomeTransactionType(BU/SL/TR/FE) andTransactionStatus(P/D/F/R) enums persisted as the literal COBOL codesBigDecimalonly —DECIMAL(15,4)forTRN-QUANTITY/TRN-PRICE(S9(11)V9(4) COMP-3),DECIMAL(15,2)forTRN-AMOUNT(S9(13)V9(2) COMP-3); a test round-trips full 15-digit valuesV1__create_portfolio_transaction.sqlwith the composite key, exact precision/scale andCHECKconstraints on the 88-level code sets;ddl-auto: validatekeeps entity and schema in lockstepdocs/openapi.yaml, Swagger UI at/swagger-ui.htmlMIGRATION-NOTES.md: 23 numbered rules → paragraph → Java → test, the field mapping table with byte offsets, and the open questionsThe 28-byte VSAM key is the API resource id, so keyed reads read like the COBOL:
GET /api/v1/transactions/20240320093015PORT0001000001.Evidence of equivalence
87 tests, one or more per numbered rule (
MIGRATION-NOTES.mdmaps rule → test):Error text is asserted verbatim against the COBOL (
Insufficient units for sale,Transfer processing not implemented,Invalid Transaction Type: XX, …), so a divergence in wording fails the build.Test data
There is no ASCII extract of
TRANHISTin this repo — onlysrc/database/vsam/vsam-definitions.txt, which describes the KSDS but holds no records. Nothing was invented: the fixtures and the 8 rows indb/seed/V900__…sql(loaded only under theseedprofile) are derived from the copybook layout plus the repo's own generators —PORTTEST.cbl 2100-GENERATE-KEY('PORT' + WS-RECORD-COUNT) andTSTGEN00.cbl 2300-GEN-TRANSACTION. They are representative, not a production sample, and are labelled as such in the SQL, the tests and the notes. If an extract appears later, the remaining work is theCOMP-3decode for the three packed fields; the rest are display characters.Checks
java-migration/only.cobcon the base commit compilesPORTVALD.cblclean and already fails onPORTTRAN.cbl/PORTTEST.cbl/PRCSEQ00.cbl(FUNCTION 'USER-ID' unknown,PORT-ACCOUNT-NO not defined, missingPROGRAM-ID); this PR neither fixes nor worsens that.DB_URL/DB_USERNAME/DB_PASSWORDare environment-substituted, default H2 in-memory.Review follow-up (dd4a62f, 13f10b3)
The automated review found real gaps; each is fixed and pinned by a test, and each thread has the detail:
process()D/F/Rrecord is refused instead of being silently re-postedAUD-ACTIONwithAUD-STATUS = FAIL(the2300path is unconditional); a2100rejection branches to9000and stays unauditedInvalid Transaction Type: ZZPORT-TOTAL-UNITSon a sellP, instead of a bogusInsufficient units for salefailureS9(13)V9(2)is refused rather than corrupted or left to fail in the database (deviation recorded as OQ-10):param is nullidiom is replaced by derived queries, so the query no longer depends on the database typing a null bind parametergetTransactionDate()/getTransactionTime()returnOptionalunderResolverStyle.STRICT, soPIC Xvalues VSAM accepts cannot throw on a response pathportfolio.reference-validatorinstead of@ConditionalOnMissingBean, so a deployment override cannot be silently ignoredNot changed, deliberately: the module ships no authentication — the legacy access control is RACF/CICS and the target architecture puts Apigee in front, so the README now states explicitly that the service must sit behind the gateway. Say the word if you would rather it fail closed on its own with a deny-by-default
SecurityFilterChain.What remains
The 9 open questions in
MIGRATION-NOTES.md§4 need a legacy owner: chiefly the amount-derivation semantics (OQ-1), the unreachable position update (OQ-6), and the status lifecycle (OQ-5). Portfolio existence (BR-02) is behind aPortfolioReferenceValidatorinterface whose default implementation only checks thePORTVALDid format, because the portfolio master is a separate entity — the next migration slice supplies the real bean (OQ-2).Link to Devin session: https://app.devin.ai/sessions/2a80e53df2ca465c92e8b968919fe558
Requested by: @danagajewski
Devin Review