diff --git a/tests/equivalence/cbsa-updacc/port/provenance.json b/tests/equivalence/cbsa-updacc/port/provenance.json new file mode 100644 index 000000000..883431859 --- /dev/null +++ b/tests/equivalence/cbsa-updacc/port/provenance.json @@ -0,0 +1,20 @@ +{ + "format": "porting-loop-provenance/1", + "program": "UPDACC", + "written_by": "model (the #4023 follow-up porting loop), not a person", + "model": "claude-sonnet-5-5", + "backend": "claude-code-headless: claude -p, no tools, only the ticket's prompt", + "proposed": "2026-10-02T19:14:10+00:00", + "attempt": 1, + "attempts": [ + { + "n": 1, + "verdict": "proven" + } + ], + "ticket_sha256": "e9b880bccd60e2bde150bb21663bad3865897f31d074b9b712ce05916985ec5d", + "prompt_tokens": 45401, + "proof": "tests/tools/equivalence.py run cbsa-updacc", + "edited_after": "none: the files are the model's answer as the loop stored it", + "licence": "derived from the CICS Bank Sample Application (EPL-2.0; Copyright IBM Corp.) -- see LICENSE and NOTICE in this case's directory" +} diff --git a/tests/equivalence/cbsa-updacc/port/service/UpdaccService.java b/tests/equivalence/cbsa-updacc/port/service/UpdaccService.java new file mode 100644 index 000000000..281f0ebce --- /dev/null +++ b/tests/equivalence/cbsa-updacc/port/service/UpdaccService.java @@ -0,0 +1,198 @@ +package com.gitgalaxy.modernized.service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.gitgalaxy.modernized.batch.Sysout; +import com.gitgalaxy.modernized.cics.CicsTask; +import com.gitgalaxy.modernized.dto.contract.UpdaccDfhcommarea; +import com.gitgalaxy.modernized.entity.vsam.CobolRecords; +import com.gitgalaxy.modernized.repository.db2.AccountRepository; +import com.gitgalaxy.modernized.repository.db2.Db2Dates; +import com.gitgalaxy.modernized.util.CobolCompare; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class UpdaccService { + + private static final Logger log = LoggerFactory.getLogger(UpdaccService.class); + + /** COPY SORTCODE: 77 SORTCODE PIC 9(6) VALUE 987654. */ + private static final int SORTCODE = 987654; + + private final AccountRepository accountRepository; + + public void executeUpdacc(/* Parameters mapped from Controller */) { + log.info("Executing modernized business logic for UPDACC"); + // UPDACC has no entry other than the CICS task (runTask) and LINK (handleLink); nothing to do here. + } + + /** PROCEDURE DIVISION run as a CICS task: PREMIERE / A010, then GET-ME-OUT-OF-HERE (EXEC CICS RETURN). */ + public void runTask(CicsTask task) { + log.info("Updacc: runTask"); + if (!task.hasCommarea()) { + // The original addresses DFHCOMMAREA without checking EIBCALEN (storage violation, ASRA). Kept: no guard. + task.abend("ASRA"); + return; + } + UpdaccDfhcommarea comm = task.commarea(UpdaccDfhcommarea.class); + updateAccount(comm); // A010: PERFORM UPDATE-ACCOUNT-DB2 + task.returnTransid(null, null); // GET-ME-OUT-OF-HERE: EXEC CICS RETURN + } + + /** Another program LINKed / XCTLed to this one: the same logic on the caller's COMMAREA. */ + public UpdaccDfhcommarea handleLink(UpdaccDfhcommarea request) { + log.info("Updacc: handleLink"); + updateAccount(request); + return request; + } + + /** A010 + UPDATE-ACCOUNT-DB2 (UAD010..UAD999). */ + private void updateAccount(UpdaccDfhcommarea comm) { + // A010 + comm.setCommScode(String.format(Locale.ROOT, "%06d", SORTCODE)); // MOVE SORTCODE TO COMM-SCODE + int desiredSortCode = SORTCODE; // MOVE SORTCODE TO DESIRED-SORT-CODE + + // UAD010 + int commAccno = comm.getCommAccno() == null ? 0 : comm.getCommAccno(); + int desiredAccNo = Math.floorMod(commAccno, 100000000); // MOVE COMM-ACCNO TO DESIRED-ACC-NO + String hvSortcode = String.format(Locale.ROOT, "%06d", desiredSortCode); + String hvAccNo = String.format(Locale.ROOT, "%08d", desiredAccNo); + + Map key = new HashMap<>(); + key.put("hvAccountSortcode", hvSortcode); + key.put("hvAccountAccNo", hvAccNo); + + // EXEC SQL SELECT ... (UPDACC.cbl:191) + int sqlcode = 0; + Map row = null; + try { + row = accountRepository.selectL191Updacc(key); + } catch (EmptyResultDataAccessException e) { + sqlcode = 100; + } catch (DataAccessException e) { + sqlcode = -904; + } + String hvEyecatcher = null; + String hvCustNo = null; + String hvOpened = null; + String hvLastStmt = null; + String hvNextStmt = null; + BigDecimal hvAvailBal = null; + BigDecimal hvActualBal = null; + if (row != null) { + // No indicator variables in the original: a NULL column is SQLCODE -305. + Object eye = col(row, "ACCOUNT_EYECATCHER"); + Object cust = col(row, "ACCOUNT_CUSTOMER_NUMBER"); + Object opened = col(row, "ACCOUNT_OPENED"); + Object last = col(row, "ACCOUNT_LAST_STATEMENT"); + Object next = col(row, "ACCOUNT_NEXT_STATEMENT"); + Object avail = col(row, "ACCOUNT_AVAILABLE_BALANCE"); + Object actual = col(row, "ACCOUNT_ACTUAL_BALANCE"); + if (eye == null || cust == null || opened == null || last == null || next == null + || avail == null || actual == null + || col(row, "ACCOUNT_TYPE") == null || col(row, "ACCOUNT_INTEREST_RATE") == null + || col(row, "ACCOUNT_OVERDRAFT_LIMIT") == null) { + sqlcode = -305; + } else { + hvEyecatcher = CobolRecords.fit(eye.toString(), 4, CobolRecords.charset()); + hvCustNo = CobolRecords.fit(cust.toString(), 10, CobolRecords.charset()); + hvOpened = Db2Dates.date(opened); + hvLastStmt = Db2Dates.date(last); + hvNextStmt = Db2Dates.date(next); + hvAvailBal = CobolRecords.decimal(avail); + hvActualBal = CobolRecords.decimal(actual); + } + } + + // IF SQLCODE NOT = 0 + if (sqlcode != 0) { + comm.setCommSuccess("N"); + Sysout.display("ERROR: UPDACC returned ", sqlcodeDisplay(sqlcode), " on SELECT"); + return; // GO TO UAD999 + } + + // IF (COMM-ACC-TYPE = SPACES OR COMM-ACC-TYPE(1:1) = ' ') + String accType = comm.getCommAccType() == null ? "" : comm.getCommAccType(); + if (CobolCompare.eq(accType, "") || accType.charAt(0) == ' ') { + comm.setCommSuccess("N"); + Sysout.display("ERROR: UPDACC has invalid account-type"); + return; // GO TO UAD999 + } + + // MOVE COMM-ACC-TYPE / COMM-OVERDRAFT / COMM-INT-RATE TO the host variables + String hvAccType = CobolRecords.fit(accType, 8, CobolRecords.charset()); + int commOverdraft = comm.getCommOverdraft() == null ? 0 : comm.getCommOverdraft(); + int hvOverdraft = Math.floorMod(commOverdraft, 1000000000); // S9(9) COMP + BigDecimal commRate = comm.getCommIntRate() == null ? BigDecimal.ZERO : comm.getCommIntRate(); + BigDecimal hvIntRate = commRate.setScale(2, RoundingMode.DOWN); + + // EXEC SQL UPDATE ... (UPDACC.cbl:278) + Map upd = new HashMap<>(key); + upd.put("hvAccountAccType", hvAccType); + upd.put("hvAccountIntRate", hvIntRate); + upd.put("hvAccountOverdraftLim", hvOverdraft); + sqlcode = 0; + try { + int n = accountRepository.updateL278Updacc(upd); + if (n == 0) { + sqlcode = 100; + } + } catch (DataAccessException e) { + sqlcode = -904; + } + if (sqlcode != 0) { + comm.setCommSuccess("N"); + Sysout.display("ERROR: UPDACC returned ", sqlcodeDisplay(sqlcode), " on UPDATE"); + return; // GO TO UAD999 + } + + // Success: refresh the COMMAREA from the host variables + comm.setCommEye(hvEyecatcher); + comm.setCommCustno(hvCustNo); + comm.setCommScode(hvSortcode); + comm.setCommAccno(CobolRecords.numval(hvAccNo.strip()).intValue()); + comm.setCommAccType(hvAccType); + comm.setCommIntRate(hvIntRate); + comm.setCommOpened(ddmmyyyy(hvOpened)); + comm.setCommOverdraft(hvOverdraft); + comm.setCommLastStmtDt(ddmmyyyy(hvLastStmt)); + comm.setCommNextStmtDt(ddmmyyyy(hvNextStmt)); + comm.setCommAvailBal(hvAvailBal.setScale(2, RoundingMode.DOWN)); + comm.setCommActualBal(hvActualBal.setScale(2, RoundingMode.DOWN)); + comm.setCommSuccess("Y"); + } + + /** MOVE X(10) 'yyyy-mm-dd' TO DB2-DATE-REFORMAT, then YR / MNTH / DAY into the DDMMYYYY redefined group. */ + private static int ddmmyyyy(String iso) { + int yr = CobolRecords.numval(iso.substring(0, 4)).intValue(); + int mnth = CobolRecords.numval(iso.substring(5, 7)).intValue(); + int day = CobolRecords.numval(iso.substring(8, 10)).intValue(); + return day * 1000000 + mnth * 10000 + yr; + } + + /** SQLCODE-DISPLAY: PIC S9(8) SIGN LEADING SEPARATE. */ + private static String sqlcodeDisplay(int sqlcode) { + return String.format(Locale.ROOT, "%+09d", sqlcode); + } + + private static Object col(Map row, String name) { + for (Map.Entry e : row.entrySet()) { + if (e.getKey().equalsIgnoreCase(name)) { + return e.getValue(); + } + } + return null; + } + +} diff --git a/tests/equivalence/genapp-lgapvs01/port/provenance.json b/tests/equivalence/genapp-lgapvs01/port/provenance.json new file mode 100644 index 000000000..d7c147a46 --- /dev/null +++ b/tests/equivalence/genapp-lgapvs01/port/provenance.json @@ -0,0 +1,20 @@ +{ + "format": "porting-loop-provenance/1", + "program": "LGAPVS01", + "written_by": "model (the #4023 follow-up porting loop), not a person", + "model": "claude-sonnet-5-5", + "backend": "claude-code-headless: claude -p, no tools, only the ticket's prompt", + "proposed": "2026-10-02T20:44:17+00:00", + "attempt": 1, + "attempts": [ + { + "n": 1, + "verdict": "proven" + } + ], + "ticket_sha256": "b03177770a3425c101a82dca803aa14e76829123968f8232260b23b6c097af01", + "prompt_tokens": 37694, + "proof": "tests/tools/equivalence.py run genapp-lgapvs01", + "edited_after": "none: the files are the model's answer as the loop stored it", + "licence": "derived from the CICS General Insurance Application (EPL-2.0; Copyright IBM Corp.) -- see LICENSE and NOTICE in this case's directory" +} diff --git a/tests/equivalence/genapp-lgapvs01/port/service/Lgapvs01Service.java b/tests/equivalence/genapp-lgapvs01/port/service/Lgapvs01Service.java new file mode 100644 index 000000000..3ba6b6014 --- /dev/null +++ b/tests/equivalence/genapp-lgapvs01/port/service/Lgapvs01Service.java @@ -0,0 +1,223 @@ +package com.gitgalaxy.modernized.service; + +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.gitgalaxy.modernized.cics.CicsTask; +import com.gitgalaxy.modernized.dto.contract.Lgacdb01CaErrorMsg; +import com.gitgalaxy.modernized.dto.contract.Lgapvs01Dfhcommarea; +import com.gitgalaxy.modernized.entity.vsam.CobolEdit; +import com.gitgalaxy.modernized.entity.vsam.WfPolicyInfo; +import com.gitgalaxy.modernized.entity.vsam.WfPolicyInfoKey; +import com.gitgalaxy.modernized.repository.vsam.WfPolicyInfoRepository; +import com.gitgalaxy.modernized.util.CobolCompare; +import java.math.BigDecimal; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.transaction.annotation.Transactional; + +/** + * Response handling (field testing: field-tested (6 public / 0 private estates)): + * WRITE at line 135 tests NORMAL + */ +@Service +@Transactional +@RequiredArgsConstructor +public class Lgapvs01Service { + + private static final Logger log = LoggerFactory.getLogger(Lgapvs01Service.class); + + /** Offset of CA-POLICY-SPECIFIC inside CA-REQUEST-SPECIFIC (CA-POLICY-NUM 10 + CA-POLICY-COMMON 72). */ + private static final int SPEC = 82; + + private final ObjectProvider lgstsqService; + private final WfPolicyInfoRepository wfPolicyInfoRepository; + + public void executeLgapvs01(/* Parameters mapped from Controller */) { + log.info("Executing modernized business logic for lgapvs01"); + // The program's logic is a CICS task: see runTask. + } + + /** A CICS transaction entered the program. TODO: [AI AGENT] implement from the program's business rules. */ + public Lgapvs01Dfhcommarea handleTransaction(String transid, Lgapvs01Dfhcommarea request) { + log.info("Lgapvs01: handleTransaction"); + return request; + } + + /** One task of LGAPVS01 (VSAM KSDS policy record ADD). */ + public void runTask(CicsTask task) { + log.info("Lgapvs01: runTask"); + if (!task.hasCommarea()) { + // No DFHCOMMAREA: the original addresses storage that is not there (ASRA). + task.abend("ASRA"); + return; + } + Lgapvs01Dfhcommarea ca = task.commarea(Lgapvs01Dfhcommarea.class); + + // MAINLINE: Move EIBCALEN To WS-Commarea-Len (never used afterwards) + String spec = ca.getCaRequestSpecific() == null ? "" : ca.getCaRequestSpecific(); + + // MAINLINE: Move CA-Request-ID(4:1) To WF-Request-ID + String wfRequestId = fit(ca.getCaRequestId(), 3, 1); + // MAINLINE: Move CA-Policy-Num To WF-Policy-Num (PIC 9(10) to X(10): the digits as held) + String wfPolicyNum = fit(spec, 0, 10); + // MAINLINE: Move CA-Customer-Num To WF-Customer-Num + String wfCustomerNum = digits(ca.getCaCustomerNum(), 10); + + // MAINLINE: Evaluate WF-Request-ID (every redefinition of WF-Policy-Data covers all 43 bytes) + String data; + if (CobolCompare.eq(wfRequestId, "C")) { + data = fit(spec, SPEC + 255, 8) // CA-B-Postcode -> WF-B-Postcode + + fit(spec, SPEC + 843, 4) // CA-B-Status -> WF-B-Status + + fit(spec, SPEC + 285, 31); // CA-B-Customer -> WF-B-Customer (truncated) + } else if (CobolCompare.eq(wfRequestId, "E")) { + data = fit(spec, SPEC, 1) // CA-E-WITH-PROFITS + + fit(spec, SPEC + 1, 1) // CA-E-EQUITIES + + fit(spec, SPEC + 2, 1) // CA-E-MANAGED-FUND + + fit(spec, SPEC + 3, 10) // CA-E-FUND-NAME + + fit(spec, SPEC + 21, 30); // CA-E-LIFE-ASSURED (X(31) -> X(30), truncated) + } else if (CobolCompare.eq(wfRequestId, "H")) { + data = fit(spec, SPEC, 15) // CA-H-PROPERTY-TYPE + + fit(spec, SPEC + 15, 3) // CA-H-BEDROOMS + + fit(spec, SPEC + 18, 8) // CA-H-VALUE + + fit(spec, SPEC + 50, 8) // CA-H-POSTCODE -> WF-H-POSTCODE + + fit(spec, SPEC + 26, 9); // CA-H-HOUSE-NAME X(20) -> X(9), truncated + } else if (CobolCompare.eq(wfRequestId, "M")) { + data = fit(spec, SPEC, 15) // CA-M-MAKE + + fit(spec, SPEC + 15, 15) // CA-M-MODEL + + fit(spec, SPEC + 30, 6) // CA-M-VALUE + + fit(spec, SPEC + 36, 7); // CA-M-REGNUMBER + } else { + data = " ".repeat(43); // When Other: Move Spaces To WF-Policy-Data + } + + // MAINLINE: Exec CICS Write File('KSDSPOLY') From(WF-Policy-Info) Ridfld(WF-Policy-Key) RESP(WS-RESP) + WfPolicyInfoKey key = new WfPolicyInfoKey(); + key.setWfRequestId(wfRequestId); + key.setWfCustomerNum(wfCustomerNum); + key.setWfPolicyNum(wfPolicyNum); + WfPolicyInfo rec = new WfPolicyInfo(); + rec.setId(key); + rec.setWfPolicyData(data); + int wsResp = task.write("KSDSPOLY", wfPolicyInfoRepository.existsById(key), + () -> wfPolicyInfoRepository.save(rec)); + + // MAINLINE: If WS-RESP Not = DFHRESP(NORMAL) + if (wsResp != 0) { + // Move EIBRESP2 To WS-RESP2: CicsTask.write exposes only RESP, so RESP2 is taken as 0. + int wsResp2 = 0; + ca.setCaReturnCode(80); // MOVE '80' TO CA-RETURN-CODE + writeErrorMessage(task, ca, wsResp, wsResp2); // PERFORM WRITE-ERROR-MESSAGE + task.returnTransid(null, null); // EXEC CICS RETURN END-EXEC + return; + } + + // A-EXIT: EXIT. GOBACK (a RETURN) + task.returnTransid(null, null); + } + + /** WRITE-ERROR-MESSAGE paragraph. */ + private void writeErrorMessage(CicsTask task, Lgapvs01Dfhcommarea ca, int wsResp, int wsResp2) { + // EXEC CICS ASKTIME ABSTIME(WS-ABSTIME) + long abstime = task.asktime(); + // EXEC CICS FORMATTIME ABSTIME MMDDYYYY(WS-DATE) TIME(WS-TIME): no DATESEP / TIMESEP + String wsDate = CicsTask.formatDate(abstime, "MMDDYYYY", ""); + String wsTime = CicsTask.formatTime(abstime, ""); + + String spec = ca.getCaRequestSpecific() == null ? "" : ca.getCaRequestSpecific(); + // MOVE WS-DATE TO EM-DATE / WS-TIME TO EM-TIME (truncated on the right) + String emDate = fit(wsDate, 0, 8); + String emTime = fit(wsTime, 0, 6); + String emCusnum = digits(ca.getCaCustomerNum(), 10); // Move CA-Customer-Num To EM-Cusnum + String emPolnum = fit(spec, 0, 10); // Move CA-Policy-Num To EM-POLNUM + // Move WS-RESP To EM-RespRC (PIC +9(5)) + String emResp = CobolEdit.format("+9(5)", BigDecimal.valueOf(wsResp), false, null); + String emResp2 = CobolEdit.format("+9(5)", BigDecimal.valueOf(wsResp2), false, null); + + String errorMsg = emDate + " " + emTime + " LGAPVS01" + + " PNUM=" + emPolnum + + " CNUM=" + emCusnum + + " Write file KSDSPOLY" + + " RESP=" + emResp + + " RESP2=" + emResp2; // 101 bytes + + // EXEC CICS LINK PROGRAM('LGSTSQ') COMMAREA(ERROR-MSG) LENGTH(LENGTH OF ERROR-MSG) + // TODO: layout mismatch kept from the source: LGSTSQ's commarea is CA-ERROR-MSG (90 data bytes); + // the first 90 bytes of ERROR-MSG are mapped onto caData. + Lgacdb01CaErrorMsg first = new Lgacdb01CaErrorMsg(); + first.setCaData(fit(errorMsg, 0, 90)); + task.link("LGSTSQ", first, 101); + + // IF EIBCALEN > 0 + Integer eibcalen = task.eibcalen(); + int len = eibcalen == null ? 32500 : eibcalen; + if (len > 0) { + String whole = commareaText(ca); + Lgacdb01CaErrorMsg msg = new Lgacdb01CaErrorMsg(); + if (len < 91) { + // MOVE DFHCOMMAREA(1:EIBCALEN) TO CA-DATA (space padded) + msg.setCaData(fit(whole, 0, len) + " ".repeat(90 - len)); + } else { + // MOVE DFHCOMMAREA(1:90) TO CA-DATA + msg.setCaData(fit(whole, 0, 90)); + } + // EXEC CICS LINK PROGRAM('LGSTSQ') COMMAREA(CA-ERROR-MSG) LENGTH(LENGTH OF CA-ERROR-MSG) + task.link("LGSTSQ", msg, 99); + } + } + + /** DFHCOMMAREA as its bytes read: CA-REQUEST-ID, CA-RETURN-CODE, CA-CUSTOMER-NUM, CA-REQUEST-SPECIFIC. */ + private static String commareaText(Lgapvs01Dfhcommarea ca) { + int rc = ca.getCaReturnCode() == null ? 0 : ca.getCaReturnCode(); + return fit(ca.getCaRequestId(), 0, 6) + + String.format(Locale.ROOT, "%02d", Math.floorMod(rc, 100)) + + digits(ca.getCaCustomerNum(), 10) + + fit(ca.getCaRequestSpecific(), 0, 32482); + } + + /** `len` characters of `s` from `off`, space padded (MOVE to / from PIC X). */ + private static String fit(String s, int off, int len) { + String v = s == null ? "" : s; + StringBuilder out = new StringBuilder(len); + for (int i = 0; i < len; i++) { + int p = off + i; + out.append(p < v.length() ? v.charAt(p) : ' '); + } + return out.toString(); + } + + /** A PIC 9(n) value as its n zoned digits (high-order digits lost, as COBOL stores it). */ + private static String digits(Long value, int n) { + long v = value == null ? 0L : Math.abs(value); + String s = Long.toString(v); + return s.length() > n ? s.substring(s.length() - n) : "0".repeat(n - s.length()) + s; + } + + /** Another program LINKed / XCTLed to this one. TODO: [AI AGENT] implement from the program's business rules. */ + public Lgapvs01Dfhcommarea handleLink(Lgapvs01Dfhcommarea request) { + log.info("Lgapvs01: handleLink"); + return request; + } + + /** EXEC CICS LINK PROGRAM(LGSTSQ) at base/src/lgapvs01.cbl:169, base/src/lgapvs01.cbl:176, base/src/lgapvs01.cbl:182. + * Call targets field testing: open (6 public / 0 private estates). */ + // TODO: this site passes ERROR-MSG; LGSTSQ receives CA-ERROR-MSG (base/src/lgacdb01.cbl) -- map one layout onto the other + public Lgacdb01CaErrorMsg linkLgstsq(Lgacdb01CaErrorMsg request) { + return lgstsqService.getObject().handleLink(request); + } + + /** .GENAPP.KSDSPOLY as CICS file KSDSPOLY at base/src/lgapvs01.cbl:135; VSAM defines field testing: open (3 public / 0 private estates). */ + public WfPolicyInfo writeKsdspoly(WfPolicyInfo record) { + // CICS WRITE adds a record: DUPREC when its key is already on file + Optional onFile = wfPolicyInfoRepository.findById(record.getId()); + if (onFile.isPresent()) { + throw new org.springframework.dao.DuplicateKeyException( + "DUPREC: CICS file KSDSPOLY already holds the key of this record"); + } + return wfPolicyInfoRepository.save(record); + } + +} diff --git a/tests/equivalence/genapp-lgicdb01/port/provenance.json b/tests/equivalence/genapp-lgicdb01/port/provenance.json new file mode 100644 index 000000000..46203e76d --- /dev/null +++ b/tests/equivalence/genapp-lgicdb01/port/provenance.json @@ -0,0 +1,20 @@ +{ + "format": "porting-loop-provenance/1", + "program": "LGICDB01", + "written_by": "model (the #4023 follow-up porting loop), not a person", + "model": "claude-sonnet-5-5", + "backend": "claude-code-headless: claude -p, no tools, only the ticket's prompt", + "proposed": "2026-10-02T20:43:41+00:00", + "attempt": 1, + "attempts": [ + { + "n": 1, + "verdict": "proven" + } + ], + "ticket_sha256": "f8f3c55e8a0c8db3768ed55c428459cf424a31a91b73e2ad8cf080a45566fabe", + "prompt_tokens": 39275, + "proof": "tests/tools/equivalence.py run genapp-lgicdb01", + "edited_after": "none: the files are the model's answer as the loop stored it", + "licence": "derived from the CICS General Insurance Application (EPL-2.0; Copyright IBM Corp.) -- see LICENSE and NOTICE in this case's directory" +} diff --git a/tests/equivalence/genapp-lgicdb01/port/service/Lgicdb01Service.java b/tests/equivalence/genapp-lgicdb01/port/service/Lgicdb01Service.java new file mode 100644 index 000000000..c4c2f9f85 --- /dev/null +++ b/tests/equivalence/genapp-lgicdb01/port/service/Lgicdb01Service.java @@ -0,0 +1,271 @@ +package com.gitgalaxy.modernized.service; + +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.gitgalaxy.modernized.cics.CicsTask; +import com.gitgalaxy.modernized.dto.contract.Lgacdb01CaErrorMsg; +import com.gitgalaxy.modernized.dto.contract.Lgicdb01Dfhcommarea; +import com.gitgalaxy.modernized.entity.vsam.CobolRecords; +import com.gitgalaxy.modernized.exception.*; +import com.gitgalaxy.modernized.repository.db2.CustomerRepository; +import com.gitgalaxy.modernized.repository.db2.Db2Dates; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.PessimisticLockingFailureException; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.charset.Charset; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +@Service +@Transactional +@RequiredArgsConstructor +public class Lgicdb01Service { + + private static final Logger log = LoggerFactory.getLogger(Lgicdb01Service.class); + + private static final int DFHCOMMAREA_LEN = 32500; + private static final int SPECIFIC_LEN = 32482; + private static final int WS_CUSTOMER_LEN = 72; // WS-CUSTOMER-LEN (lgpolicy.cpy) + private static final int WS_CA_HEADERTRAILER_LEN = 18; // WS-CA-HEADERTRAILER-LEN + + private final ObjectProvider lgstsqService; + private final CustomerRepository customerRepository; + + /** The program is a CICS transaction (DSCI) with no batch entry: its logic is in runTask. */ + public void executeLgicdb01(/* Parameters mapped from Controller */) { + log.info("Executing modernized business logic for lgicdb01"); + } + + /** A CICS transaction entered the program. */ + public Lgicdb01Dfhcommarea handleTransaction(String transid, Lgicdb01Dfhcommarea request) { + log.info("Lgicdb01: handleTransaction"); + return request; + } + + /** One task of LGICDB01: MAINLINE SECTION, GET-CUSTOMER-INFO, WRITE-ERROR-MESSAGE. */ + public void runTask(CicsTask task) { + log.info("Lgicdb01: runTask"); + Charset cs = CobolRecords.charset(); + + // MAINLINE: INITIALIZE WS-HEADER / MOVE EIBTRNID, EIBTRMID, EIBTASKN -- debug fields, never output. + + // MAINLINE: IF EIBCALEN IS EQUAL TO ZERO + if (!task.hasCommarea() || (task.eibcalen() != null && task.eibcalen() == 0)) { + // MOVE ' NO COMMAREA RECEIVED' TO EM-VARIABLE (47 bytes, group move: pads with spaces) + String emVariable = CobolRecords.fit(" NO COMMAREA RECEIVED", 47, cs); + if (!writeErrorMessage(task, null, 0, emVariable, 0, cs)) { + return; + } + task.abend("LGCA"); // ABEND ABCODE('LGCA') NODUMP; no HANDLE ABEND, so the task ends + return; + } + + Lgicdb01Dfhcommarea ca = task.commarea(Lgicdb01Dfhcommarea.class); + Integer eibcalen = task.eibcalen(); + int calen = eibcalen == null ? DFHCOMMAREA_LEN : eibcalen; + + // MAINLINE: MOVE '00' TO CA-RETURN-CODE + ca.setCaReturnCode(0); + // MOVE EIBCALEN TO WS-CALEN / SET WS-ADDR-DFHCOMMAREA / INITIALIZE DB2-IN-INTEGERS: no output. + + // MAINLINE: MOVE WS-CUSTOMER-LEN TO WS-REQUIRED-CA-LEN; ADD WS-CA-HEADERTRAILER-LEN + int requiredCaLen = WS_CUSTOMER_LEN + WS_CA_HEADERTRAILER_LEN; + if (calen < requiredCaLen) { + ca.setCaReturnCode(98); + task.returnTransid(null, null); // EXEC CICS RETURN + return; + } + + // MOVE CA-CUSTOMER-NUM TO DB2-CUSTOMERNUMBER-INT (S9(9) COMP: high-order digit lost) + long custNum = ca.getCaCustomerNum() == null ? 0L : ca.getCaCustomerNum(); + long db2CustomerNumber = custNum % 1_000_000_000L; + // MOVE CA-CUSTOMER-NUM TO EM-CUSNUM (X(10), the 10 digits as stored) + String emCusnum = String.format(Locale.ROOT, "%010d", custNum); + + // MAINLINE: PERFORM GET-CUSTOMER-INFO + if (!getCustomerInfo(task, ca, db2CustomerNumber, emCusnum, calen, cs)) { + return; + } + + // MAINLINE-END: EXEC CICS RETURN + task.returnTransid(null, null); + } + + /** GET-CUSTOMER-INFO; false when the task has ended (RETURN or an abend inside). */ + private boolean getCustomerInfo(CicsTask task, Lgicdb01Dfhcommarea ca, long db2CustomerNumber, + String emCusnum, int calen, Charset cs) { + int sqlcode; + Map row = null; + try { + Map params = new HashMap<>(); + params.put("db2CustomernumberInt", db2CustomerNumber); + row = customerRepository.selectL169Lgicdb01(params); + sqlcode = 0; + // a NULL column with no indicator variable is SQLCODE -305 in DB2 + for (String col : COLUMNS) { + if (row.get(col) == null) { + sqlcode = -305; + break; + } + } + } catch (EmptyResultDataAccessException e) { + sqlcode = 100; + } catch (IncorrectResultSizeDataAccessException e) { + sqlcode = -811; + } catch (PessimisticLockingFailureException e) { + sqlcode = -913; + } catch (DataAccessException e) { + sqlcode = otherSqlcode(e); + } + + // SELECT ... INTO: host variables are set only on SQLCODE 0 (the row's columns overlay CA-REQUEST-SPECIFIC) + if (sqlcode == 0) { + StringBuilder sb = new StringBuilder( + CobolRecords.fit(ca.getCaRequestSpecific(), SPECIFIC_LEN, cs)); + put(sb, 0, 10, row.get("FIRSTNAME"), cs); // CA-FIRST-NAME + put(sb, 10, 20, row.get("LASTNAME"), cs); // CA-LAST-NAME + put(sb, 30, 10, row.get("DATEOFBIRTH"), cs); // CA-DOB + put(sb, 40, 20, row.get("HOUSENAME"), cs); // CA-HOUSE-NAME + put(sb, 60, 4, row.get("HOUSENUMBER"), cs); // CA-HOUSE-NUM + put(sb, 64, 8, row.get("POSTCODE"), cs); // CA-POSTCODE + put(sb, 75, 20, row.get("PHONEMOBILE"), cs); // CA-PHONE-MOBILE + put(sb, 95, 20, row.get("PHONEHOME"), cs); // CA-PHONE-HOME + put(sb, 115, 100, row.get("EMAILADDRESS"), cs); // CA-EMAIL-ADDRESS + ca.setCaRequestSpecific(sb.toString()); + } + + // Evaluate SQLCODE + if (sqlcode == 0) { + ca.setCaReturnCode(0); + } else if (sqlcode == 100) { + ca.setCaReturnCode(1); + } else if (sqlcode == -913) { + ca.setCaReturnCode(1); + } else { + ca.setCaReturnCode(90); + // EM-VARIABLE: ' CNUM=' EM-CUSNUM EM-SQLREQ(spaces) ' SQLCODE=' EM-SQLRC (set in WRITE-ERROR-MESSAGE) + String emVariable = " CNUM=" + emCusnum + " ".repeat(16) + " SQLCODE="; + if (!writeErrorMessage(task, ca, calen, emVariable, sqlcode, cs)) { + return false; + } + task.returnTransid(null, null); // EXEC CICS RETURN + return false; + } + return true; + } + + private static final String[] COLUMNS = {"FIRSTNAME", "LASTNAME", "DATEOFBIRTH", "HOUSENAME", + "HOUSENUMBER", "POSTCODE", "PHONEMOBILE", "PHONEHOME", "EMAILADDRESS"}; + + /** + * WRITE-ERROR-MESSAGE. `emVariable` is EM-VARIABLE as the caller left it (47 bytes, or its first 41 + * when EM-SQLRC is still to be set). Returns false when the task ended (a failed LINK abended it). + */ + private boolean writeErrorMessage(CicsTask task, Lgicdb01Dfhcommarea ca, int calen, String emVariable, + int sqlcode, Charset cs) { + // MOVE SQLCODE TO EM-SQLRC (PIC +9(5)): last 6 bytes of EM-VARIABLE + String emSqlrc = (sqlcode < 0 ? "-" : "+") + + String.format(Locale.ROOT, "%05d", Math.abs((long) sqlcode) % 100000L); + String variable = CobolRecords.fit(emVariable, 47, cs).substring(0, 41) + emSqlrc; + + // EXEC CICS ASKTIME / FORMATTIME MMDDYYYY(WS-DATE) TIME(WS-TIME) + long abstime = task.asktime(); + String wsDate = CobolRecords.fit(CicsTask.formatDate(abstime, "MMDDYYYY", ""), 10, cs); + String wsTime = CobolRecords.fit(CicsTask.formatTime(abstime, ""), 8, cs); + String emDate = wsDate.substring(0, 8); // MOVE WS-DATE TO EM-DATE + String emTime = wsTime.substring(0, 6); // MOVE WS-TIME TO EM-TIME + + // ERROR-MSG: EM-DATE, FILLER, EM-TIME, ' LGICUS01', EM-VARIABLE (71 bytes) + String errorMsg = emDate + " " + emTime + " LGICUS01" + variable; + + // EXEC CICS LINK PROGRAM('LGSTSQ') COMMAREA(ERROR-MSG) LENGTH(71) + Lgacdb01CaErrorMsg msg = new Lgacdb01CaErrorMsg(); + msg.setCaData(errorMsg); + if (!linked(task, task.link("LGSTSQ", msg, errorMsg.length()))) { + return false; + } + + // IF EIBCALEN > 0 (EIBCALEN is 0 on the no-commarea path) + if (ca != null && calen > 0) { + int n = Math.min(calen, 90); // < 91: DFHCOMMAREA(1:EIBCALEN); else DFHCOMMAREA(1:90) + Lgacdb01CaErrorMsg caMsg = new Lgacdb01CaErrorMsg(); + caMsg.setCaData(CobolRecords.fit(commareaText(ca, cs).substring(0, n), 90, cs)); + // EXEC CICS LINK PROGRAM('LGSTSQ') COMMAREA(CA-ERROR-MSG) LENGTH(99) + if (!linked(task, task.link("LGSTSQ", caMsg, 99))) { + return false; + } + } + return true; + } + + /** After a LINK with no RESP clause: an unhandled condition abends the task (default action). */ + private boolean linked(CicsTask task, String resp) { + if (!"NORMAL".equals(resp)) { + task.abendOnCondition(resp); + return false; + } + return !task.ended(); + } + + /** The first 90 bytes of DFHCOMMAREA as text: request id, return code, customer number, specific data. */ + private static String commareaText(Lgicdb01Dfhcommarea ca, Charset cs) { + int rc = ca.getCaReturnCode() == null ? 0 : ca.getCaReturnCode(); + long num = ca.getCaCustomerNum() == null ? 0L : ca.getCaCustomerNum(); + return CobolRecords.fit(ca.getCaRequestId(), 6, cs) + + String.format(Locale.ROOT, "%02d", rc % 100) + + String.format(Locale.ROOT, "%010d", num) + + CobolRecords.fit(ca.getCaRequestSpecific(), 72, cs); + } + + /** MOVE a fetched column to a PIC X host variable at `offset` of CA-REQUEST-SPECIFIC. */ + private static void put(StringBuilder sb, int offset, int width, Object value, Charset cs) { + sb.replace(offset, offset + width, CobolRecords.fit(text(value), width, cs)); + } + + private static String text(Object v) { + if (v == null) { + return ""; + } + if (v instanceof java.sql.Date || v instanceof java.time.LocalDate) { + return Db2Dates.date(v); // DB2 DATE in a character host variable (iso) + } + return v.toString(); + } + + /** SQLCODE for a failure the program treats as WHEN OTHER: the negated vendor code, else -1. */ + private static int otherSqlcode(DataAccessException e) { + Throwable t = e.getRootCause(); + if (t instanceof SQLException se && se.getErrorCode() != 0) { + return -Math.abs(se.getErrorCode()); + } + return -1; + } + + /** Another program LINKed / XCTLed to this one. */ + public Lgicdb01Dfhcommarea handleLink(Lgicdb01Dfhcommarea request) { + log.info("Lgicdb01: handleLink"); + return request; + } + + /** EXEC CICS LINK PROGRAM(LGSTSQ) at base/src/lgicdb01.cbl:225, :233, :239 (runTask links through the task). */ + public Lgacdb01CaErrorMsg linkLgstsq(Lgacdb01CaErrorMsg request) { + return lgstsqService.getObject().handleLink(request); + } + + /** + * EXEC CICS ABEND ABCODE(LGCA) at base/src/lgicdb01.cbl:122. runTask does not use this: it ends the + * task through task.abend("LGCA") instead of throwing. + */ + public void abendLgcaL122() { + throw new CicsAbendException("LGCA", "LGICDB01", "base/src/lgicdb01.cbl:122"); + } + +}