From 8e551c35d94995940162918d4661fa05afb3389c Mon Sep 17 00:00:00 2001 From: hsyx3952501 Date: Mon, 25 May 2026 12:27:00 +0800 Subject: [PATCH] Initial commit: COBOL+JCL credit card billing system with COMP-3, OCCURS, COPY REPLACING, INSPECT, and JCL runner --- .gitignore | 5 + cobol/CRDCALC.cbl | 259 ++++++++++++++++++++++ cobol/CRDRPT.cbl | 187 ++++++++++++++++ cobol/CRDVAL.cbl | 226 +++++++++++++++++++ cobol/GENDATA.cbl | 482 +++++++++++++++++++++++++++++++++++++++++ copybooks/DATESUB.cpy | 4 + copybooks/MEMCPY.cpy | 15 ++ copybooks/RATECPY.cpy | 6 + copybooks/TXCPY.cpy | 17 ++ git-push.cmd | 29 +++ jcl-runner/executor.py | 240 ++++++++++++++++++++ jcl-runner/main.py | 82 +++++++ jcl-runner/parser.py | 190 ++++++++++++++++ jcl/CREDIT25.jcl | 31 +++ run_all.ps1 | 97 +++++++++ test_integration.bat | 113 ++++++++++ verify_comp3.py | 97 +++++++++ 17 files changed, 2080 insertions(+) create mode 100644 .gitignore create mode 100644 cobol/CRDCALC.cbl create mode 100644 cobol/CRDRPT.cbl create mode 100644 cobol/CRDVAL.cbl create mode 100644 cobol/GENDATA.cbl create mode 100644 copybooks/DATESUB.cpy create mode 100644 copybooks/MEMCPY.cpy create mode 100644 copybooks/RATECPY.cpy create mode 100644 copybooks/TXCPY.cpy create mode 100644 git-push.cmd create mode 100644 jcl-runner/executor.py create mode 100644 jcl-runner/main.py create mode 100644 jcl-runner/parser.py create mode 100644 jcl/CREDIT25.jcl create mode 100644 run_all.ps1 create mode 100644 test_integration.bat create mode 100644 verify_comp3.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7d6e90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/*.exe +data/ +*.pyc +__pycache__/ +.DS_Store diff --git a/cobol/CRDCALC.cbl b/cobol/CRDCALC.cbl new file mode 100644 index 0000000..d5dcf9c --- /dev/null +++ b/cobol/CRDCALC.cbl @@ -0,0 +1,259 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. CRDCALC. + + * CALCULATE INTEREST AND FEES - CREDIT CARD BATCH SYSTEM + * DEMONSTRATES: OCCURS+SEARCH, KEY BREAK, COMPUTE, + * EVALUATE, COMP-3, COPY REPLACING + + ENVIRONMENT DIVISION. + INPUT-OUTPUT SECTION. + FILE-CONTROL. + SELECT VALID-IN ASSIGN TO "VALIDIN" + ORGANIZATION IS LINE SEQUENTIAL. + SELECT RATE-FILE ASSIGN TO "RATE" + ORGANIZATION IS SEQUENTIAL. + SELECT CALC-OUT ASSIGN TO "CALCOUT" + ORGANIZATION IS LINE SEQUENTIAL. + + DATA DIVISION. + FILE SECTION. + FD VALID-IN. + COPY TXCPY. + + FD RATE-FILE. + COPY RATECPY. + + FD CALC-OUT. + 01 CALC-RECORD PIC X(200). + + WORKING-STORAGE SECTION. + 01 WS-SWITCHES. + 05 WS-EOF-VALID PIC X VALUE 'N'. + 88 WS-END-OF-VALID VALUE 'Y'. + 05 WS-EOF-RATE PIC X VALUE 'N'. + 88 WS-END-OF-RATE VALUE 'Y'. + + 01 WS-COUNTERS. + 05 WS-TOTAL-IN PIC 9(5) VALUE 0. + 05 WS-TOTAL-OUT PIC 9(5) VALUE 0. + 05 WS-RATE-COUNT PIC 9(5) VALUE 0. + + * INTERNAL TABLE: RATE TABLE WITH OCCURS + SEARCH + 01 WS-RATE-TABLE. + 05 WS-RATE-ENTRY OCCURS 1 TO 5 TIMES + DEPENDING ON WS-RATE-COUNT + INDEXED BY WS-RT-IDX. + 10 WS-RT-TYPE PIC X. + 10 WS-RT-PCT PIC 9(1)V9(8) COMP-3. + 10 WS-RT-EFF-DATE PIC 9(8). + + * COPY REPLACING DEMO: STATEMENT DATE + COPY DATESUB REPLACING ==:TAG:== BY ==WS-STMT==. + + 01 WS-CARD-ACCUM. + 05 WS-CURRENT-CARD PIC 9(16) VALUE 0. + 05 WS-CARD-PURCHASES PIC S9(9)V99 COMP-3 VALUE 0. + 05 WS-CARD-CASH PIC S9(9)V99 COMP-3 VALUE 0. + 05 WS-CARD-REFUNDS PIC S9(9)V99 COMP-3 VALUE 0. + 05 WS-CARD-INTEREST PIC S9(9)V99 COMP-3 VALUE 0. + 05 WS-CARD-FEE PIC S9(9)V99 COMP-3 VALUE 0. + 05 WS-CARD-TOTAL PIC S9(9)V99 COMP-3 VALUE 0. + 05 WS-TX-COUNT PIC 9(4) VALUE 0. + + 01 WS-CALC-DETAIL. + 05 WS-D-CARD PIC 9(16). + 05 WS-D-SEP1 PIC X VALUE SPACE. + 05 WS-D-TYPE PIC X. + 05 WS-D-SEP2 PIC X VALUE SPACE. + 05 WS-D-AMOUNT PIC -(9)9.99. + 05 WS-D-SEP3 PIC X VALUE SPACE. + 05 WS-D-INTEREST PIC -(7)9.99. + 05 WS-D-SEP4 PIC X VALUE SPACE. + 05 WS-D-FEE PIC -(7)9.99. + 05 WS-D-SEP5 PIC X VALUE SPACE. + 05 WS-D-DESC PIC X(30). + + 01 WS-SUMMARY. + 05 WS-S-CARD PIC 9(16). + 05 WS-S-SEP1 PIC X VALUE SPACE. + 05 WS-S-TOTAL-AMT PIC -(9)9.99. + 05 WS-S-SEP2 PIC X VALUE SPACE. + 05 WS-S-TOTAL-INT PIC -(9)9.99. + 05 WS-S-SEP3 PIC X VALUE SPACE. + 05 WS-S-TOTAL-FEE PIC -(9)9.99. + 05 WS-S-SEP4 PIC X VALUE SPACE. + 05 WS-S-GRAND-TOTAL PIC -(9)9.99. + 05 WS-S-SEP5 PIC X VALUE SPACE. + 05 WS-S-TX-COUNT PIC Z(4)9. + + 01 WS-GRAND-TOTAL PIC S9(12)V99 COMP-3 VALUE 0. + 01 WS-GRAND-INT PIC S9(12)V99 COMP-3 VALUE 0. + 01 WS-GRAND-FEE PIC S9(12)V99 COMP-3 VALUE 0. + 01 WS-GRAND-DISP. + 05 WS-GD-TOTAL PIC -(12)9.99. + 05 WS-GD-SP1 PIC X VALUE SPACE. + 05 WS-GD-INT PIC -(12)9.99. + 05 WS-GD-SP2 PIC X VALUE SPACE. + 05 WS-GD-FEE PIC -(12)9.99. + + 01 WS-DAYS-DIFF PIC 9(4). + 01 WS-INT-AMOUNT PIC S9(9)V99 COMP-3. + 01 WS-FEE-AMOUNT PIC S9(9)V99 COMP-3. + 01 WS-DAILY-RATE PIC 9(1)V9(8) COMP-3. + 01 WS-CASH-RATE PIC 9(1)V9(4) COMP-3. + 01 WS-OVERDUE-RATE PIC 9(1)V9(4) COMP-3. + 01 WS-STATEMENT-DATE PIC 9(8). + + PROCEDURE DIVISION. + 0000-MAIN. + OPEN INPUT VALID-IN + INPUT RATE-FILE + OUTPUT CALC-OUT. + + ACCEPT WS-STATEMENT-DATE FROM DATE YYYYMMDD. + MOVE WS-STATEMENT-DATE(1:4) TO WS-STMT-YYYY. + MOVE WS-STATEMENT-DATE(5:2) TO WS-STMT-MM. + MOVE WS-STATEMENT-DATE(7:2) TO WS-STMT-DD. + + * LOAD RATES INTO OCCURS TABLE + PERFORM 1000-LOAD-RATES. + + * PROCESS TRANSACTIONS (KEY BREAK: CARD CHANGE) + PERFORM 2000-PROCESS-VALID UNTIL WS-END-OF-VALID. + + * WRITE FINAL CARD SUMMARY IF DATA REMAINS + IF WS-TX-COUNT > 0 + PERFORM 3000-WRITE-CARD-SUMMARY. + + * WRITE GRAND TOTAL + PERFORM 4000-WRITE-GRAND-TOTAL. + + CLOSE VALID-IN RATE-FILE CALC-OUT. + DISPLAY 'CRDCALC: ' WS-TOTAL-IN ' READ, ' + WS-TOTAL-OUT ' WRITTEN'. + GOBACK. + + * LOAD RATES INTO OCCURS TABLE + 1000-LOAD-RATES. + MOVE 0 TO WS-RATE-COUNT. + PERFORM UNTIL WS-END-OF-RATE + READ RATE-FILE + AT END SET WS-END-OF-RATE TO TRUE + NOT AT END + ADD 1 TO WS-RATE-COUNT + MOVE RATE-TYPE + TO WS-RT-TYPE(WS-RATE-COUNT) + MOVE RATE-PCT + TO WS-RT-PCT(WS-RATE-COUNT) + MOVE RATE-EFF-DATE + TO WS-RT-EFF-DATE(WS-RATE-COUNT) + END-READ + END-PERFORM. + + * SEARCH TABLE FOR CASH RATE + SET WS-RT-IDX TO 1. + SEARCH WS-RATE-ENTRY + AT END MOVE 0.0005 TO WS-CASH-RATE + WHEN WS-RT-TYPE(WS-RT-IDX) = 'C' + MOVE WS-RT-PCT(WS-RT-IDX) TO WS-CASH-RATE + END-SEARCH. + + * SEARCH TABLE FOR OVERDUE RATE + SET WS-RT-IDX TO 1. + SEARCH WS-RATE-ENTRY + AT END MOVE 0.0500 TO WS-OVERDUE-RATE + WHEN WS-RT-TYPE(WS-RT-IDX) = 'O' + MOVE WS-RT-PCT(WS-RT-IDX) TO WS-OVERDUE-RATE + END-SEARCH. + + IF WS-CASH-RATE = 0 MOVE 0.0005 TO WS-CASH-RATE END-IF. + IF WS-OVERDUE-RATE = 0 MOVE 0.0500 TO WS-OVERDUE-RATE + END-IF. + MOVE 0 TO WS-TOTAL-IN. + + 2000-PROCESS-VALID. + READ VALID-IN + AT END SET WS-END-OF-VALID TO TRUE + NOT AT END + ADD 1 TO WS-TOTAL-IN + IF WS-CURRENT-CARD = 0 + MOVE TX-CARD-NO TO WS-CURRENT-CARD + END-IF + * KEY BREAK: WHEN CARD CHANGES, OUTPUT SUMMARY + IF TX-CARD-NO NOT = WS-CURRENT-CARD + PERFORM 3000-WRITE-CARD-SUMMARY + MOVE TX-CARD-NO TO WS-CURRENT-CARD + MOVE 0 TO WS-CARD-PURCHASES + WS-CARD-CASH + WS-CARD-REFUNDS + WS-CARD-INTEREST + WS-CARD-FEE + WS-CARD-TOTAL + WS-TX-COUNT + END-IF + PERFORM 2500-ACCUMULATE-TX + END-READ. + + 2500-ACCUMULATE-TX. + MOVE 0 TO WS-INT-AMOUNT WS-FEE-AMOUNT. + ADD 1 TO WS-TX-COUNT. + EVALUATE TRUE + WHEN TX-PURCHASE + ADD TX-AMOUNT TO WS-CARD-PURCHASES + MOVE 'PURCHASE' TO WS-D-DESC + WHEN TX-CASH + ADD TX-AMOUNT TO WS-CARD-CASH + MOVE 'CASH ADVANCE' TO WS-D-DESC + COMPUTE WS-INT-AMOUNT = TX-AMOUNT * + WS-CASH-RATE * 30 + ADD WS-INT-AMOUNT TO WS-CARD-INTEREST + WHEN TX-REFUND + ADD TX-AMOUNT TO WS-CARD-PURCHASES + MOVE 'REFUND' TO WS-D-DESC + END-EVALUATE. + + * FEE CALCULATION: 1% OF CASH ADVANCE (MIN 100) + IF TX-CASH + COMPUTE WS-FEE-AMOUNT = TX-AMOUNT * 0.01 + IF WS-FEE-AMOUNT < 100 + MOVE 100 TO WS-FEE-AMOUNT + END-IF + ADD WS-FEE-AMOUNT TO WS-CARD-FEE + END-IF. + + * WRITE DETAIL LINE + MOVE TX-CARD-NO TO WS-D-CARD + MOVE TX-TYPE TO WS-D-TYPE + MOVE TX-AMOUNT TO WS-D-AMOUNT + MOVE WS-INT-AMOUNT TO WS-D-INTEREST + MOVE WS-FEE-AMOUNT TO WS-D-FEE + WRITE CALC-RECORD FROM WS-CALC-DETAIL. + + 3000-WRITE-CARD-SUMMARY. + COMPUTE WS-CARD-TOTAL = WS-CARD-PURCHASES + WS-CARD-CASH + + WS-CARD-INTEREST + WS-CARD-FEE - WS-CARD-REFUNDS. + ADD WS-CARD-TOTAL TO WS-GRAND-TOTAL. + ADD WS-CARD-INTEREST TO WS-GRAND-INT. + ADD WS-CARD-FEE TO WS-GRAND-FEE. + + MOVE WS-CURRENT-CARD TO WS-S-CARD + MOVE WS-CARD-PURCHASES TO WS-S-TOTAL-AMT + MOVE WS-CARD-INTEREST TO WS-S-TOTAL-INT + MOVE WS-CARD-FEE TO WS-S-TOTAL-FEE + MOVE WS-CARD-TOTAL TO WS-S-GRAND-TOTAL + MOVE WS-TX-COUNT TO WS-S-TX-COUNT + WRITE CALC-RECORD FROM WS-SUMMARY. + ADD 1 TO WS-TOTAL-OUT. + + 4000-WRITE-GRAND-TOTAL. + MOVE WS-GRAND-TOTAL TO WS-GD-TOTAL. + MOVE WS-GRAND-INT TO WS-GD-INT. + MOVE WS-GRAND-FEE TO WS-GD-FEE. + STRING + 'GRAND TOTAL CARDS:' WS-TOTAL-OUT + ' AMOUNT:' WS-GD-TOTAL + ' INTEREST:' WS-GD-INT + ' FEE:' WS-GD-FEE + INTO CALC-RECORD + END-STRING. + WRITE CALC-RECORD. diff --git a/cobol/CRDRPT.cbl b/cobol/CRDRPT.cbl new file mode 100644 index 0000000..9baf9fa --- /dev/null +++ b/cobol/CRDRPT.cbl @@ -0,0 +1,187 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. CRDRPT. + + * GENERATE MONTHLY STATEMENT AND SUMMARY REPORT + * INPUT: BILLING RESULT FROM CRDCALC + * OUTPUT: MONTHLY STATEMENT (PER CARD) + * SUMMARY REPORT (AGGREGATE) + + ENVIRONMENT DIVISION. + INPUT-OUTPUT SECTION. + FILE-CONTROL. + SELECT CALC-IN ASSIGN TO "BILLING" + ORGANIZATION IS LINE SEQUENTIAL. + SELECT STMT-OUT ASSIGN TO "STMT" + ORGANIZATION IS LINE SEQUENTIAL. + SELECT SUMM-OUT ASSIGN TO "SUMMARY" + ORGANIZATION IS LINE SEQUENTIAL. + + DATA DIVISION. + FILE SECTION. + FD CALC-IN. + 01 CALC-LINE PIC X(200). + + FD STMT-OUT. + 01 STMT-LINE PIC X(200). + + FD SUMM-OUT. + 01 SUMM-LINE PIC X(200). + + WORKING-STORAGE SECTION. + 01 WS-SWITCHES. + 05 WS-EOF-CALC PIC X VALUE 'N'. + 88 WS-END-OF-CALC VALUE 'Y'. + + 01 WS-COUNTERS. + 05 WS-LINE-COUNT PIC 9(5) VALUE 0. + 05 WS-CARD-COUNT PIC 9(5) VALUE 0. + 05 WS-DETAIL-COUNT PIC 9(5) VALUE 0. + + 01 WS-REPORT-DATE. + 05 WS-RPT-YYYY PIC 9(4). + 05 WS-RPT-MM PIC 9(2). + 05 WS-RPT-DD PIC 9(2). + + 01 WS-HEADER1. + 05 WS-H1-DATE PIC X(8). + 05 WS-H1-SPACE PIC X(5) VALUE SPACES. + 05 WS-H1-TITLE PIC X(30) + VALUE 'MONTHLY CREDIT CARD STATEMENT'. + + 01 WS-HEADER2. + 05 WS-H2-FILLER PIC X(50) VALUE ALL '-'. + + 01 WS-DETAIL-LINE. + 05 WS-DL-CARD PIC X(16). + 05 WS-DL-SP1 PIC X(2) VALUE SPACES. + 05 WS-DL-TYPE PIC X(8). + 05 WS-DL-SP2 PIC X(2) VALUE SPACES. + 05 WS-DL-AMOUNT PIC -(9)9.99. + 05 WS-DL-SP3 PIC X(2) VALUE SPACES. + 05 WS-DL-INTEREST PIC -(7)9.99. + 05 WS-DL-SP4 PIC X(2) VALUE SPACES. + 05 WS-DL-FEE PIC -(7)9.99. + 05 WS-DL-SP5 PIC X(2) VALUE SPACES. + 05 WS-DL-DESC PIC X(30). + + 01 WS-SUMMARY-LINE. + 05 WS-SL-CARD PIC X(16). + 05 WS-SL-SP1 PIC X(2) VALUE SPACES. + 05 WS-SL-TOTAL PIC -(9)9.99. + 05 WS-SL-SP2 PIC X(2) VALUE SPACES. + 05 WS-SL-INT PIC -(9)9.99. + 05 WS-SL-SP3 PIC X(2) VALUE SPACES. + 05 WS-SL-FEE PIC -(9)9.99. + 05 WS-SL-SP4 PIC X(2) VALUE SPACES. + 05 WS-SL-GRAND PIC -(9)9.99. + 05 WS-SL-SP5 PIC X(2) VALUE SPACES. + 05 WS-SL-TXCNT PIC Z(4)9. + + 01 WS-TRAILER. + 05 WS-TR-TOTAL-CARDS PIC Z(5)9. + 05 WS-TR-SP1 PIC X(5) VALUE SPACES. + 05 WS-TR-TOTAL-LINES PIC Z(5)9. + 05 WS-TR-SP2 PIC X(5) VALUE SPACES. + 05 WS-TR-MSG PIC X(20) + VALUE 'END OF REPORT'. + + * PARSE FIELDS FROM INPUT LINE + 01 WS-PARSE. + 05 WS-P-CARD PIC 9(16). + 05 WS-P-TYPE PIC X. + 05 WS-P-AMOUNT PIC S9(9)V99. + 05 WS-P-INTEREST PIC S9(7)V99. + 05 WS-P-FEE PIC S9(7)V99. + 05 WS-P-DESC PIC X(30). + 05 WS-P-IS-SUMMARY PIC X VALUE 'N'. + 88 WS-P-SUMMARY-LINE VALUE 'Y'. + + 05 WS-PARSE-REMAIN PIC X(150). + + PROCEDURE DIVISION. + 0000-MAIN. + OPEN INPUT CALC-IN + OUTPUT STMT-OUT + OUTPUT SUMM-OUT. + + ACCEPT WS-REPORT-DATE FROM DATE YYYYMMDD. + MOVE WS-REPORT-DATE(1:4) TO WS-RPT-YYYY. + MOVE WS-REPORT-DATE(5:2) TO WS-RPT-MM. + MOVE WS-REPORT-DATE(7:2) TO WS-RPT-DD. + MOVE WS-REPORT-DATE TO WS-H1-DATE. + + PERFORM 1000-WRITE-HEADER. + + MOVE 0 TO WS-CARD-COUNT WS-DETAIL-COUNT. + + PERFORM 2000-PROCESS-CALC UNTIL WS-END-OF-CALC. + + PERFORM 3000-WRITE-TRAILER. + + CLOSE CALC-IN STMT-OUT SUMM-OUT. + DISPLAY 'CRDRPT: ' WS-CARD-COUNT ' CARDS, ' + WS-DETAIL-COUNT ' LINES'. + GOBACK. + + 1000-WRITE-HEADER. + MOVE SPACES TO STMT-LINE. + WRITE STMT-LINE FROM WS-HEADER1. + MOVE SPACES TO STMT-LINE. + WRITE STMT-LINE FROM WS-HEADER2. + MOVE SPACES TO STMT-LINE. + + 2000-PROCESS-CALC. + READ CALC-IN + AT END SET WS-END-OF-CALC TO TRUE + NOT AT END + ADD 1 TO WS-LINE-COUNT + PERFORM 2100-PARSE-LINE + END-READ. + + 2100-PARSE-LINE. + * CHECK IF THIS IS A GRAND TOTAL LINE + IF CALC-LINE(1:11) = 'GRAND TOTAL' + MOVE CALC-LINE TO SUMM-LINE + WRITE SUMM-LINE + EXIT PARAGRAPH + END-IF. + + * CHECK IF THIS IS A CARD SUMMARY LINE + MOVE CALC-LINE TO WS-PARSE-REMAIN. + UNSTRING WS-PARSE-REMAIN DELIMITED BY SPACE + INTO WS-P-CARD WS-P-AMOUNT WS-P-INTEREST + WS-P-FEE WS-P-AMOUNT WS-P-DESC + END-UNSTRING. + + * IF FIRST FIELD IS 16-DIGIT NUMBER AND HAS TX-COUNT + * AT END, IT'S SUMMARY; OTHERWISE DETAIL + IF CALC-LINE(18:1) NOT = ' ' + PERFORM 2200-WRITE-DETAIL + ELSE + PERFORM 2300-WRITE-CARD-SUMMARY. + + 2200-WRITE-DETAIL. + MOVE CALC-LINE TO WS-DETAIL-LINE. + MOVE WS-DETAIL-LINE TO STMT-LINE. + WRITE STMT-LINE. + ADD 1 TO WS-DETAIL-COUNT. + + 2300-WRITE-CARD-SUMMARY. + MOVE CALC-LINE TO WS-SUMMARY-LINE. + MOVE WS-SUMMARY-LINE TO SUMM-LINE. + WRITE SUMM-LINE. + MOVE WS-SUMMARY-LINE TO STMT-LINE. + WRITE STMT-LINE. + MOVE SPACES TO STMT-LINE. + WRITE STMT-LINE. + ADD 1 TO WS-CARD-COUNT. + + 3000-WRITE-TRAILER. + MOVE SPACES TO STMT-LINE. + WRITE STMT-LINE. + MOVE WS-CARD-COUNT TO WS-TR-TOTAL-CARDS. + MOVE WS-LINE-COUNT TO WS-TR-TOTAL-LINES. + WRITE STMT-LINE FROM WS-TRAILER. + + MOVE WS-TRAILER TO SUMM-LINE. + WRITE SUMM-LINE. diff --git a/cobol/CRDVAL.cbl b/cobol/CRDVAL.cbl new file mode 100644 index 0000000..8ba5e8a --- /dev/null +++ b/cobol/CRDVAL.cbl @@ -0,0 +1,226 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. CRDVAL. + + * VALIDATE TRANSACTIONS - CREDIT CARD BATCH SYSTEM + * DEMONSTRATES: COPY REPLACING, OCCURS+SEARCH ALL, + * INSPECT, STRING, 88-LEVEL, REDEFINES IO + + ENVIRONMENT DIVISION. + INPUT-OUTPUT SECTION. + FILE-CONTROL. + SELECT TX-FILE ASSIGN TO "TRANSIN" + ORGANIZATION IS LINE SEQUENTIAL. + SELECT MEM-FILE ASSIGN TO "MEMBER" + ORGANIZATION IS LINE SEQUENTIAL. + SELECT VALID-OUT ASSIGN TO "VALIDOUT" + ORGANIZATION IS LINE SEQUENTIAL. + SELECT REJECT-OUT ASSIGN TO "REJECT" + ORGANIZATION IS LINE SEQUENTIAL. + SELECT ERR-OUT ASSIGN TO "REPORTERR" + ORGANIZATION IS LINE SEQUENTIAL. + + DATA DIVISION. + FILE SECTION. + FD TX-FILE. + COPY TXCPY. + + FD MEM-FILE. + COPY MEMCPY. + + FD VALID-OUT. + 01 VALID-RECORD PIC X(100). + + FD REJECT-OUT. + 01 REJECT-RECORD PIC X(100). + + FD ERR-OUT. + 01 ERR-RECORD PIC X(120). + + WORKING-STORAGE SECTION. + 01 WS-SWITCHES. + 05 WS-EOF-TX PIC X VALUE 'N'. + 88 WS-END-OF-TX VALUE 'Y'. + 05 WS-EOF-MEM PIC X VALUE 'N'. + 88 WS-END-OF-MEM VALUE 'Y'. + 05 WS-VALID PIC X VALUE 'Y'. + 88 WS-IS-VALID VALUE 'Y'. + 05 WS-FOUND PIC X VALUE 'N'. + 88 WS-IS-FOUND VALUE 'Y'. + + 01 WS-COUNTERS. + 05 WS-TOTAL-READ PIC 9(5) VALUE 0. + 05 WS-TOTAL-VALID PIC 9(5) VALUE 0. + 05 WS-TOTAL-REJECT PIC 9(5) VALUE 0. + 05 WS-TOTAL-MEMBERS PIC 9(5) VALUE 0. + + * DATESUB COPYBOOK WITH COPY REPLACING DEMO + * GENERATES: WS-RUN-DATE (WS-RUN-YYYY, WS-RUN-MM, WS-RUN-DD) + COPY DATESUB REPLACING ==:TAG:== BY ==WS-RUN==. + + * GENERATES: WS-TX-DATE (WS-TX-YYYY, WS-TX-MM, WS-TX-DD) + COPY DATESUB REPLACING ==:TAG:== BY ==WS-TX==. + + * INTERNAL TABLE: MEMBER TABLE WITH OCCURS + SEARCH ALL + 01 WS-MEMBER-TABLE. + 05 WS-MEMBER-ENTRY OCCURS 1 TO 100 TIMES + DEPENDING ON WS-TOTAL-MEMBERS + ASCENDING KEY IS WS-MEM-ID + INDEXED BY WS-MEM-IDX. + 10 WS-MEM-ID PIC 9(16). + 10 WS-MEM-NAME PIC X(30). + 10 WS-MEM-LIMIT PIC 9(9)V99. + 10 WS-MEM-TYPE PIC X. + 10 WS-MEM-STATUS PIC X. + 10 WS-MEM-BALANCE PIC S9(9)V99. + 10 WS-MEM-MINPAY PIC 9(9)V99. + 10 WS-MEM-ADDR PIC X(60). + + 01 WS-ERR-MSG. + 05 WS-ERR-CARD PIC 9(16). + 05 WS-ERR-SP1 PIC X(2) VALUE SPACES. + 05 WS-ERR-CODE PIC X(20). + 05 WS-ERR-SP2 PIC X(2) VALUE SPACES. + 05 WS-ERR-DESC PIC X(80). + + 01 WS-MERCHANT-CHECK. + 05 WS-MC-LEN PIC 9(2). + 05 WS-MC-COUNT PIC 9(2). + + PROCEDURE DIVISION. + 0000-MAIN. + OPEN INPUT TX-FILE + INPUT MEM-FILE + OUTPUT VALID-OUT + OUTPUT REJECT-OUT + OUTPUT ERR-OUT. + + ACCEPT WS-RUN-DATE FROM DATE YYYYMMDD. + + PERFORM 1000-LOAD-MEMBERS. + PERFORM 2000-PROCESS-TX UNTIL WS-END-OF-TX. + PERFORM 3000-WRITE-SUMMARY. + + CLOSE TX-FILE MEM-FILE VALID-OUT REJECT-OUT ERR-OUT. + GOBACK. + + * LOAD ALL MEMBERS INTO OCCURS TABLE AT ONCE + 1000-LOAD-MEMBERS. + MOVE 0 TO WS-TOTAL-MEMBERS. + PERFORM UNTIL WS-END-OF-MEM + READ MEM-FILE + AT END SET WS-END-OF-MEM TO TRUE + NOT AT END + ADD 1 TO WS-TOTAL-MEMBERS + MOVE MEM-ID + TO WS-MEM-ID(WS-TOTAL-MEMBERS) + MOVE MEM-NAME + TO WS-MEM-NAME(WS-TOTAL-MEMBERS) + MOVE MEM-CREDIT-LIMIT + TO WS-MEM-LIMIT(WS-TOTAL-MEMBERS) + MOVE MEM-TYPE + TO WS-MEM-TYPE(WS-TOTAL-MEMBERS) + MOVE MEM-STATUS + TO WS-MEM-STATUS(WS-TOTAL-MEMBERS) + MOVE MEM-BALANCE + TO WS-MEM-BALANCE(WS-TOTAL-MEMBERS) + MOVE MEM-MIN-PAYMENT + TO WS-MEM-MINPAY(WS-TOTAL-MEMBERS) + MOVE MEM-ADDRESS + TO WS-MEM-ADDR(WS-TOTAL-MEMBERS) + END-READ + END-PERFORM. + + 2000-PROCESS-TX. + READ TX-FILE + AT END SET WS-END-OF-TX TO TRUE + NOT AT END + ADD 1 TO WS-TOTAL-READ + PERFORM 2100-VALIDATE-TX + END-READ. + + 2100-VALIDATE-TX. + SET WS-IS-VALID TO TRUE. + + * INSPECT DEMO: CHECK MERCHANT NAME FOR INVALID CHARS + MOVE 0 TO WS-MC-COUNT. + INSPECT TX-MERCHANT TALLYING WS-MC-COUNT + FOR CHARACTERS BEFORE INITIAL SPACE. + IF WS-MC-COUNT = 0 + MOVE 'INVALID-MERCHANT' TO WS-ERR-CODE + MOVE 'MERCHANT NAME EMPTY' TO WS-ERR-DESC + PERFORM 2200-REJECT + EXIT PARAGRAPH + END-IF. + CONTINUE. + + IF TX-CARD-NO = 0 + MOVE 'INVALID-CARD' TO WS-ERR-CODE + MOVE 'CARD NUMBER IS ZERO' TO WS-ERR-DESC + PERFORM 2200-REJECT + EXIT PARAGRAPH. + + IF TX-AMOUNT <= 0 AND NOT TX-REFUND + MOVE 'INVALID-AMOUNT' TO WS-ERR-CODE + MOVE 'AMOUNT MUST BE POSITIVE' TO WS-ERR-DESC + PERFORM 2200-REJECT + EXIT PARAGRAPH. + + IF TX-REFUND AND TX-AMOUNT >= 0 + MOVE 'INVALID-REFUND' TO WS-ERR-CODE + MOVE 'REFUND AMOUNT MUST BE NEGATIVE' TO WS-ERR-DESC + PERFORM 2200-REJECT + EXIT PARAGRAPH. + + MOVE TX-DATE(1:4) TO WS-TX-YYYY + MOVE TX-DATE(5:2) TO WS-TX-MM + MOVE TX-DATE(7:2) TO WS-TX-DD + IF WS-TX-MM NOT = WS-RUN-MM + MOVE 'OUT-OF-MONTH' TO WS-ERR-CODE + MOVE 'TX DATE NOT IN RUN MONTH' TO WS-ERR-DESC + PERFORM 2200-REJECT + EXIT PARAGRAPH. + + * SEARCH ALL DEMO: BINARY SEARCH ON MEMBER TABLE + PERFORM 2300-FIND-MEMBER. + IF NOT WS-IS-FOUND + MOVE 'MEMBER-NOT-FOUND' TO WS-ERR-CODE + MOVE 'CARD NOT IN MEMBER FILE' TO WS-ERR-DESC + PERFORM 2200-REJECT + EXIT PARAGRAPH. + + IF WS-MEM-STATUS(WS-MEM-IDX) = 'F' + MOVE 'FROZEN-CARD' TO WS-ERR-CODE + MOVE 'CARD STATUS IS FROZEN' TO WS-ERR-DESC + PERFORM 2200-REJECT + EXIT PARAGRAPH. + + IF WS-VALID = 'Y' + WRITE VALID-RECORD FROM TX-RECORD + ADD 1 TO WS-TOTAL-VALID. + + 2200-REJECT. + WRITE REJECT-RECORD FROM TX-RECORD. + MOVE TX-CARD-NO TO WS-ERR-CARD. + WRITE ERR-RECORD FROM WS-ERR-MSG. + ADD 1 TO WS-TOTAL-REJECT. + + 2300-FIND-MEMBER. + SET WS-MEM-IDX TO 1. + SEARCH ALL WS-MEMBER-ENTRY + AT END + MOVE 'N' TO WS-FOUND + WHEN WS-MEM-ID(WS-MEM-IDX) = TX-CARD-NO + MOVE 'Y' TO WS-FOUND. + + 3000-WRITE-SUMMARY. + STRING + 'CRDVAL SUMMARY - TOTAL READ:' WS-TOTAL-READ + ' VALID:' WS-TOTAL-VALID + ' REJECT:' WS-TOTAL-REJECT + ' MEMBERS LOADED:' WS-TOTAL-MEMBERS + INTO ERR-RECORD + END-STRING. + WRITE ERR-RECORD. + DISPLAY 'CRDVAL: ' WS-TOTAL-READ ' READ, ' + WS-TOTAL-VALID ' VALID, ' + WS-TOTAL-REJECT ' REJECTS'. diff --git a/cobol/GENDATA.cbl b/cobol/GENDATA.cbl new file mode 100644 index 0000000..4c5e23b --- /dev/null +++ b/cobol/GENDATA.cbl @@ -0,0 +1,482 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. GENDATA. + + * GENERATE COMPREHENSIVE TEST DATA FOR CREDIT CARD BATCH SYSTEM + * COVERS: normal, frozen, closed, not-found, empty-merchant, + * zero-card, invalid-amount, invalid-refund, out-of-month, + * multiple cash advances, refunds, installments + + ENVIRONMENT DIVISION. + INPUT-OUTPUT SECTION. + FILE-CONTROL. + SELECT MEM-OUT ASSIGN TO "MEMOUT" + ORGANIZATION IS LINE SEQUENTIAL. + SELECT TX-OUT ASSIGN TO "TXOUT" + ORGANIZATION IS LINE SEQUENTIAL. + SELECT RATE-OUT ASSIGN TO "RATEOUT" + ORGANIZATION IS SEQUENTIAL. + + DATA DIVISION. + FILE SECTION. + FD MEM-OUT. + COPY MEMCPY. + + FD TX-OUT. + COPY TXCPY. + + FD RATE-OUT. + COPY RATECPY. + + PROCEDURE DIVISION. + 0000-MAIN. + OPEN OUTPUT MEM-OUT TX-OUT RATE-OUT. + + PERFORM 1000-GEN-MEMBERS. + PERFORM 2000-GEN-TRANSACTIONS. + PERFORM 3000-GEN-RATES. + + CLOSE MEM-OUT TX-OUT RATE-OUT. + DISPLAY 'GENDATA: TEST DATA CREATED'. + GOBACK. + + * 8 MEMBERS + 1000-GEN-MEMBERS. + MOVE 6222021234567800 TO MEM-ID. + MOVE 'ZHANG SAN' TO MEM-NAME. + MOVE 50000.00 TO MEM-CREDIT-LIMIT. + MOVE 'G' TO MEM-TYPE. + MOVE 'A' TO MEM-STATUS. + MOVE 15000.00 TO MEM-BALANCE. + MOVE 3000.00 TO MEM-MIN-PAYMENT. + MOVE 'BEIJING ROAD NO.1' TO MEM-ADDRESS. + WRITE MEMBER-RECORD. + + MOVE 6222021234567801 TO MEM-ID. + MOVE 'LI SI' TO MEM-NAME. + MOVE 100000.00 TO MEM-CREDIT-LIMIT. + MOVE 'P' TO MEM-TYPE. + MOVE 'A' TO MEM-STATUS. + MOVE 35000.00 TO MEM-BALANCE. + MOVE 7000.00 TO MEM-MIN-PAYMENT. + MOVE 'SHANGHAI ROAD NO.2' TO MEM-ADDRESS. + WRITE MEMBER-RECORD. + + MOVE 6222021234567802 TO MEM-ID. + MOVE 'WANG WU' TO MEM-NAME. + MOVE 20000.00 TO MEM-CREDIT-LIMIT. + MOVE 'S' TO MEM-TYPE. + MOVE 'A' TO MEM-STATUS. + MOVE 8000.00 TO MEM-BALANCE. + MOVE 2000.00 TO MEM-MIN-PAYMENT. + MOVE 'GUANGZHOU ROAD NO.3' TO MEM-ADDRESS. + WRITE MEMBER-RECORD. + + MOVE 6222021234567803 TO MEM-ID. + MOVE 'ZHAO LIU' TO MEM-NAME. + MOVE 80000.00 TO MEM-CREDIT-LIMIT. + MOVE 'G' TO MEM-TYPE. + MOVE 'F' TO MEM-STATUS. + MOVE 15000.00 TO MEM-BALANCE. + MOVE 8000.00 TO MEM-MIN-PAYMENT. + MOVE 'SHENZHEN ROAD NO.4' TO MEM-ADDRESS. + WRITE MEMBER-RECORD. + + MOVE 6222021234567804 TO MEM-ID. + MOVE 'CHEN QI' TO MEM-NAME. + MOVE 30000.00 TO MEM-CREDIT-LIMIT. + MOVE 'S' TO MEM-TYPE. + MOVE 'C' TO MEM-STATUS. + MOVE 0.00 TO MEM-BALANCE. + MOVE 0.00 TO MEM-MIN-PAYMENT. + MOVE 'NANJING ROAD NO.5' TO MEM-ADDRESS. + WRITE MEMBER-RECORD. + + * NEW: 7805 - Active Gold, edge case transaction target + MOVE 6222021234567805 TO MEM-ID. + MOVE 'SUN BA' TO MEM-NAME. + MOVE 60000.00 TO MEM-CREDIT-LIMIT. + MOVE 'G' TO MEM-TYPE. + MOVE 'A' TO MEM-STATUS. + MOVE 5000.00 TO MEM-BALANCE. + MOVE 1000.00 TO MEM-MIN-PAYMENT. + MOVE 'HANGZHOU ROAD NO.6' TO MEM-ADDRESS. + WRITE MEMBER-RECORD. + + * NEW: 7806 - Active Platinum, very high limit + MOVE 6222021234567806 TO MEM-ID. + MOVE 'ZHOU JIU' TO MEM-NAME. + MOVE 200000.00 TO MEM-CREDIT-LIMIT. + MOVE 'P' TO MEM-TYPE. + MOVE 'A' TO MEM-STATUS. + MOVE 80000.00 TO MEM-BALANCE. + MOVE 16000.00 TO MEM-MIN-PAYMENT. + MOVE 'CHENGDU ROAD NO.7' TO MEM-ADDRESS. + WRITE MEMBER-RECORD. + + * NEW: 7807 - Active Standard, low limit cash-advance heavy + MOVE 6222021234567807 TO MEM-ID. + MOVE 'WU SHI' TO MEM-NAME. + MOVE 15000.00 TO MEM-CREDIT-LIMIT. + MOVE 'S' TO MEM-TYPE. + MOVE 'A' TO MEM-STATUS. + MOVE 3000.00 TO MEM-BALANCE. + MOVE 500.00 TO MEM-MIN-PAYMENT. + MOVE 'WUHAN ROAD NO.8' TO MEM-ADDRESS. + WRITE MEMBER-RECORD. + + * 28 TRANSACTIONS + 2000-GEN-TRANSACTIONS. + * CARD 7800 - 5 transactions (normal usage mix) + MOVE 6222021234567800 TO TX-CARD-NO. + MOVE 20260501 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 1280.50 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'SUPERMARKET A' TO TX-MERCHANT. + MOVE 5411 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + MOVE 20260505 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 3500.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ELECTRONICS B' TO TX-MERCHANT. + MOVE 5732 TO TX-MCC. + MOVE 06 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + MOVE 20260510 TO TX-DATE. + MOVE 'C' TO TX-TYPE. + MOVE 2000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ATM-001' TO TX-MERCHANT. + MOVE 0 TO TX-MCC. + MOVE 0 TO TX-INSTALL. + MOVE 'ATM0000001' TO TX-ATM-ID. + MOVE 0.50 TO TX-FEE-RATE. + WRITE TX-RECORD. + + MOVE 20260515 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 850.20 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'RESTAURANT C' TO TX-MERCHANT. + MOVE 5812 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + MOVE 20260520 TO TX-DATE. + MOVE 'R' TO TX-TYPE. + MOVE -1280.50 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'SUPERMARKET A' TO TX-MERCHANT. + MOVE 5411 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * CARD 7801 - 5 transactions (high limit, installment, cash advance, refund) + MOVE 6222021234567801 TO TX-CARD-NO. + MOVE 20260503 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 15000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'FURNITURE D' TO TX-MERCHANT. + MOVE 5712 TO TX-MCC. + MOVE 12 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + MOVE 20260518 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 2200.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'HOTEL E' TO TX-MERCHANT. + MOVE 7011 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + MOVE 20260522 TO TX-DATE. + MOVE 'C' TO TX-TYPE. + MOVE 5000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ATM-003' TO TX-MERCHANT. + MOVE 0 TO TX-MCC. + MOVE 0 TO TX-INSTALL. + MOVE 'ATM0000003' TO TX-ATM-ID. + MOVE 0.50 TO TX-FEE-RATE. + WRITE TX-RECORD. + + MOVE 20260523 TO TX-DATE. + MOVE 'R' TO TX-TYPE. + MOVE -500.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'FURNITURE D' TO TX-MERCHANT. + MOVE 5712 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + MOVE 20260525 TO TX-DATE. + MOVE 'C' TO TX-TYPE. + MOVE 3000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ATM-005' TO TX-MERCHANT. + MOVE 0 TO TX-MCC. + MOVE 0 TO TX-INSTALL. + MOVE 'ATM0000005' TO TX-ATM-ID. + MOVE 0.50 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * CARD 7802 - 3 transactions (student: small purchases + cash advance) + MOVE 6222021234567802 TO TX-CARD-NO. + MOVE 20260508 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 500.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'PHARMACY F' TO TX-MERCHANT. + MOVE 5912 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + MOVE 20260511 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 300.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'BOOKSTORE H' TO TX-MERCHANT. + MOVE 5942 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + MOVE 20260514 TO TX-DATE. + MOVE 'C' TO TX-TYPE. + MOVE 1000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ATM-004' TO TX-MERCHANT. + MOVE 0 TO TX-MCC. + MOVE 0 TO TX-INSTALL. + MOVE 'ATM0000004' TO TX-ATM-ID. + MOVE 0.50 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * CARD 7803 - 1 transaction (rejected: frozen) + MOVE 6222021234567803 TO TX-CARD-NO. + MOVE 20260512 TO TX-DATE. + MOVE 'C' TO TX-TYPE. + MOVE 10000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ATM-002' TO TX-MERCHANT. + MOVE 0 TO TX-MCC. + MOVE 0 TO TX-INSTALL. + MOVE 'ATM0000002' TO TX-ATM-ID. + MOVE 0.50 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * CARD 7805 - 5 transactions (edge case validations) + * Tx 1: rejected - INVALID-MERCHANT (empty merchant name) + MOVE 6222021234567805 TO TX-CARD-NO. + MOVE 20260502 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 1000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE SPACES TO TX-MERCHANT. + MOVE 5411 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * Tx 2: rejected - INVALID-CARD (card number = 0) + MOVE 0000000000000000 TO TX-CARD-NO. + MOVE 20260504 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 2000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'STORE K' TO TX-MERCHANT. + MOVE 5411 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * Tx 3: rejected - INVALID-AMOUNT (purchase with zero amount) + MOVE 6222021234567805 TO TX-CARD-NO. + MOVE 20260506 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 0.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'STORE L' TO TX-MERCHANT. + MOVE 5411 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * Tx 4: rejected - INVALID-AMOUNT (purchase with negative amount) + MOVE 6222021234567805 TO TX-CARD-NO. + MOVE 20260509 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE -500.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'STORE M' TO TX-MERCHANT. + MOVE 5411 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * Tx 5: rejected - INVALID-REFUND (refund with positive amount) + MOVE 6222021234567805 TO TX-CARD-NO. + MOVE 20260513 TO TX-DATE. + MOVE 'R' TO TX-TYPE. + MOVE 300.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'STORE N' TO TX-MERCHANT. + MOVE 5411 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * Tx 6: valid transaction for 7805 (so card appears in billing) + MOVE 6222021234567805 TO TX-CARD-NO. + MOVE 20260519 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 2000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'DELIVERY N' TO TX-MERCHANT. + MOVE 5969 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * CARD 7806 - 3 transactions (high limit edge cases) + * Tx 1: rejected - OUT-OF-MONTH (April date, run month is May) + MOVE 6222021234567806 TO TX-CARD-NO. + MOVE 20260428 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 3000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'TRAVEL O' TO TX-MERCHANT. + MOVE 4722 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * Tx 2: valid purchase for 7806 + MOVE 6222021234567806 TO TX-CARD-NO. + MOVE 20260521 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 2500.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'JEWELRY P' TO TX-MERCHANT. + MOVE 5944 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * Tx 3: valid cash advance for 7806 + MOVE 6222021234567806 TO TX-CARD-NO. + MOVE 20260525 TO TX-DATE. + MOVE 'C' TO TX-TYPE. + MOVE 8000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ATM-006' TO TX-MERCHANT. + MOVE 0 TO TX-MCC. + MOVE 0 TO TX-INSTALL. + MOVE 'ATM0000006' TO TX-ATM-ID. + MOVE 0.50 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * CARD 7807 - 4 transactions (low limit cash-advance heavy) + * Tx 1: cash advance 1 + MOVE 6222021234567807 TO TX-CARD-NO. + MOVE 20260502 TO TX-DATE. + MOVE 'C' TO TX-TYPE. + MOVE 500.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ATM-007' TO TX-MERCHANT. + MOVE 0 TO TX-MCC. + MOVE 0 TO TX-INSTALL. + MOVE 'ATM0000007' TO TX-ATM-ID. + MOVE 0.50 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * Tx 2: cash advance 2 (different ATM) + MOVE 20260507 TO TX-DATE. + MOVE 'C' TO TX-TYPE. + MOVE 300.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ATM-008' TO TX-MERCHANT. + MOVE 0 TO TX-MCC. + MOVE 0 TO TX-INSTALL. + MOVE 'ATM0000008' TO TX-ATM-ID. + MOVE 0.50 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * Tx 3: cash advance 3 (same ATM as tx 1) + MOVE 20260511 TO TX-DATE. + MOVE 'C' TO TX-TYPE. + MOVE 200.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ATM-007' TO TX-MERCHANT. + MOVE 0 TO TX-MCC. + MOVE 0 TO TX-INSTALL. + MOVE 'ATM0000007' TO TX-ATM-ID. + MOVE 0.50 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * Tx 4: purchase mixed with cash advances + MOVE 20260520 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 800.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'GROCERY Q' TO TX-MERCHANT. + MOVE 5411 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + * CARD 9999999999999999 - 1 transaction (rejected: not found) + MOVE 9999999999999999 TO TX-CARD-NO. + MOVE 20260515 TO TX-DATE. + MOVE 'P' TO TX-TYPE. + MOVE 1000.00 TO TX-AMOUNT. + MOVE 'CNY' TO TX-CURRENCY. + MOVE 'ONLINE R' TO TX-MERCHANT. + MOVE 5969 TO TX-MCC. + MOVE 00 TO TX-INSTALL. + MOVE SPACES TO TX-ATM-ID. + MOVE 0 TO TX-FEE-RATE. + WRITE TX-RECORD. + + 3000-GEN-RATES. + MOVE 'C' TO RATE-TYPE. + MOVE 0.0005 TO RATE-PCT. + MOVE 20250101 TO RATE-EFF-DATE. + WRITE RATE-RECORD. + + MOVE 'O' TO RATE-TYPE. + MOVE 0.0500 TO RATE-PCT. + MOVE 20250101 TO RATE-EFF-DATE. + WRITE RATE-RECORD. diff --git a/copybooks/DATESUB.cpy b/copybooks/DATESUB.cpy new file mode 100644 index 0000000..54da974 --- /dev/null +++ b/copybooks/DATESUB.cpy @@ -0,0 +1,4 @@ + 01 :TAG:-DATE. + 05 :TAG:-YYYY PIC 9(4). + 05 :TAG:-MM PIC 9(2). + 05 :TAG:-DD PIC 9(2). diff --git a/copybooks/MEMCPY.cpy b/copybooks/MEMCPY.cpy new file mode 100644 index 0000000..12abddc --- /dev/null +++ b/copybooks/MEMCPY.cpy @@ -0,0 +1,15 @@ + 01 MEMBER-RECORD. + 05 MEM-ID PIC 9(16). + 05 MEM-NAME PIC X(30). + 05 MEM-CREDIT-LIMIT PIC 9(9)V99. + 05 MEM-TYPE PIC X. + 88 MEM-GOLD VALUE 'G'. + 88 MEM-PLATINUM VALUE 'P'. + 88 MEM-STANDARD VALUE 'S'. + 05 MEM-STATUS PIC X. + 88 MEM-ACTIVE VALUE 'A'. + 88 MEM-FROZEN VALUE 'F'. + 88 MEM-CLOSED VALUE 'C'. + 05 MEM-BALANCE PIC S9(9)V99. + 05 MEM-MIN-PAYMENT PIC 9(9)V99. + 05 MEM-ADDRESS PIC X(60). diff --git a/copybooks/RATECPY.cpy b/copybooks/RATECPY.cpy new file mode 100644 index 0000000..0b08333 --- /dev/null +++ b/copybooks/RATECPY.cpy @@ -0,0 +1,6 @@ + 01 RATE-RECORD. + 05 RATE-TYPE PIC X. + 88 RATE-CASH VALUE 'C'. + 88 RATE-OVERDUE VALUE 'O'. + 05 RATE-PCT PIC 9(1)V9(4) COMP-3. + 05 RATE-EFF-DATE PIC 9(8). diff --git a/copybooks/TXCPY.cpy b/copybooks/TXCPY.cpy new file mode 100644 index 0000000..4695fb9 --- /dev/null +++ b/copybooks/TXCPY.cpy @@ -0,0 +1,17 @@ + 01 TX-RECORD. + 05 TX-CARD-NO PIC 9(16). + 05 TX-DATE PIC 9(8). + 05 TX-TYPE PIC X. + 88 TX-PURCHASE VALUE 'P'. + 88 TX-CASH VALUE 'C'. + 88 TX-REFUND VALUE 'R'. + 05 TX-AMOUNT PIC S9(9)V99. + 05 TX-CURRENCY PIC X(3). + 05 TX-MERCHANT PIC X(20). + 05 TX-DETAIL. + 10 TX-DETAIL-PURCHASE. + 15 TX-MCC PIC 9(4). + 15 TX-INSTALL PIC 9(2). + 10 TX-DETAIL-CASH REDEFINES TX-DETAIL-PURCHASE. + 15 TX-ATM-ID PIC X(10). + 15 TX-FEE-RATE PIC 9(1)V99. diff --git a/git-push.cmd b/git-push.cmd new file mode 100644 index 0000000..6026342 --- /dev/null +++ b/git-push.cmd @@ -0,0 +1,29 @@ +@echo off +REM ========================================== +REM Git Push Config - jcl-cobol +REM 仓库: https://gittea.dev/hsyx3952501/jcl-cobol-git +REM ========================================== +setlocal + +set REMOTE_URL=https://gittea.dev/hsyx3952501/jcl-cobol-git.git +set TOKEN=1afb8ef3b16901f0e44238fff78fca5c0f1ff570 + +REM 设置远程仓库(HTTPS + Token 认证) +git remote remove origin 2>nul +git remote add origin https://hsyx3952501:%TOKEN%@gittea.dev/hsyx3952501/jcl-cobol-git.git + +REM 推送到 main 分支 +echo. +echo Pushing to %REMOTE_URL% ... +git push -u origin main + +if %errorlevel% equ 0 ( + echo. + echo === Push successful === +) else ( + echo. + echo === Push failed (RC=%errorlevel%) === + echo You may need to: git pull --rebase origin main +) + +endlocal diff --git a/jcl-runner/executor.py b/jcl-runner/executor.py new file mode 100644 index 0000000..07be8ef --- /dev/null +++ b/jcl-runner/executor.py @@ -0,0 +1,240 @@ +""" +JCL Executor - executes parsed JCL steps. +Phase 1: sequential execution, COND on return codes, + DD mapping to files, SORT mapped to external sort utility. +""" + +import os +import re +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Optional + +from parser import Job, JobStep, DDEntry, CondParam, COND_OPS + + +COB_MAIN_DIR = r"D:\360安全浏览器下载\GC32-BDB-SP1-rename-7z-to-exe" + +class Executor: + def __init__(self, root_dir: str, cobol_dir: str, copybook_dir: str): + self.root_dir = Path(root_dir).resolve() + self.cobol_dir = Path(cobol_dir).resolve() + self.copybook_dir = Path(copybook_dir).resolve() + self.bin_dir = self.root_dir / "bin" + self.bin_dir.mkdir(exist_ok=True) + self.last_rc: int = 0 + self.step_rcs: dict[str, int] = {} + + def _cobol_env(self) -> dict[str, str]: + """Build environment dict for GnuCOBOL execution.""" + env = os.environ.copy() + env["COB_MAIN_DIR"] = COB_MAIN_DIR + env["COB_CONFIG_DIR"] = os.path.join(COB_MAIN_DIR, "config") + env["COB_LIBRARY_PATH"] = os.path.join(COB_MAIN_DIR, "lib", "gnucobol") + env["COBCPY"] = str(self.copybook_dir) + cobbin = os.path.join(COB_MAIN_DIR, "bin") + env["PATH"] = cobbin + os.pathsep + env.get("PATH", "") + return env + + def run(self, job: Job): + print(f"\n{'='*60}") + print(f"JOB: {job.job_name}") + print(f"{'='*60}") + + for i, step in enumerate(job.steps): + self._execute_step(step, i) + + print(f"\n{'='*60}") + print(f"JOB {job.job_name} COMPLETED") + print(f"STEPS: {len(job.steps)}, FINAL RC: {self.last_rc}") + print(f"{'='*60}") + return self.last_rc + + def _execute_step(self, step: JobStep, idx: int): + print(f"\n--- STEP {idx+1}: {step.step_name} (PGM={step.program}) ---") + + # COND check + if step.cond and not self._check_cond(step.cond): + print(f" COND: SKIPPED ({step.cond})") + return + + program_upper = step.program.upper() + + if program_upper == "SORT": + rc = self._run_sort(step) + else: + rc = self._run_cobol(step) + + self.last_rc = rc + self.step_rcs[step.step_name] = rc + print(f" RC: {rc}") + + def _check_cond(self, cond: CondParam) -> bool: + """Return True if step should run, False if skipped.""" + if cond.step_name: + target_rc = self.step_rcs.get(cond.step_name, 0) + else: + # Check all previous steps + for rc in self.step_rcs.values(): + if COND_OPS.get(cond.operator, lambda x, y: False)(rc, cond.code): + return False + return True + + if COND_OPS.get(cond.operator, lambda x, y: False)(target_rc, cond.code): + return False + return True + + def _run_cobol(self, step: JobStep) -> int: + """Compile (if needed) and execute a COBOL program.""" + cbl_path = self.cobol_dir / f"{step.program}.cbl" + exe_path = self.bin_dir / f"{step.program}.exe" + + # Compile if source newer than binary + if not exe_path.exists() or ( + cbl_path.exists() + and os.path.getmtime(cbl_path) > os.path.getmtime(exe_path) + ): + print(f" COMPILE: {cbl_path.name}") + result = subprocess.run( + ["cobc", "-std=ibm", "-x", str(cbl_path), "-o", str(exe_path)], + capture_output=True, text=True, env=self._cobol_env(), + ) + if result.returncode != 0: + print(f" COMPILE ERROR (RC={result.returncode}):") + print(result.stderr[:500]) + return result.returncode + + # Map DD entries to file paths + stdin_file = None + stdout_file = None + env_map = {} + + for dd in step.dd_entries: + dd_name_upper = dd.dd_name.upper() + + if dd_name_upper == "SYSOUT": + if dd.sysout == "*": + stdout_file = ( + self.root_dir / "data" / "output" / + f"{step.step_name.lower()}_sysout.txt" + ) + continue + + if dd_name_upper == "SYSIN" and dd.inline_data: + # Write inline data to temp file + tmp = tempfile.NamedTemporaryFile( + mode="w", delete=False, suffix=".sysin", dir=self.root_dir / "data" / "work" + ) + for line in dd.inline_data: + tmp.write(line + "\n") + tmp.close() + stdin_file = tmp.name + env_map[dd_name_upper] = stdin_file + continue + + if dd.dsn: + # Map DSN to file path + file_path = self._resolve_dsn(dd.dsn, dd.disp) + env_map[dd_name_upper] = str(file_path) + + # Execute COBOL program + print(f" EXECUTE: {exe_path.name}") + env = self._cobol_env() + env.update(env_map) + + stdin = None + if stdin_file: + stdin = open(stdin_file, "r") + + stdout = None + if stdout_file: + stdout = open(stdout_file, "w") + + try: + result = subprocess.run( + [str(exe_path)], + stdin=stdin, + stdout=stdout, + stderr=subprocess.PIPE, + text=True, + env=env, + cwd=str(self.root_dir), + ) + if result.stderr: + print(f" STDERR: {result.stderr[:200]}") + return result.returncode + finally: + if stdin: + stdin.close() + if stdout: + stdout.close() + + def _run_sort(self, step: JobStep) -> int: + """Execute SORT step (maps to GNU sort or PowerShell Sort-Object).""" + sortin = None + sortout = None + sort_fields = None + + for dd in step.dd_entries: + dd_name_upper = dd.dd_name.upper() + if dd_name_upper == "SORTIN" and dd.dsn: + sortin = self._resolve_dsn(dd.dsn, dd.disp) + elif dd_name_upper == "SORTOUT" and dd.dsn: + sortout = self._resolve_dsn(dd.dsn, dd.disp) + elif dd_name_upper == "SYSIN" and dd.inline_data: + sort_text = " ".join(dd.inline_data).upper() + # Parse SORT FIELDS=(start,len,order,...) + match = re.search( + r"FIELDS=\s*\(([^)]+)\)", sort_text + ) + if match: + sort_fields = match.group(1) + + if not sortin or not sortout: + print(" ERROR: SORT requires SORTIN and SORTOUT DD") + return 12 + + if sort_fields: + # Parse sort fields for PowerShell Sort-Object + fields = sort_fields.split(",") + # fields: start,len,type,order,start2,len2,type2,order2 + pscmd = f"Get-Content '{sortin}' | Sort-Object" + i = 0 + first = True + while i + 3 < len(fields): + start = int(fields[i].strip()) - 1 # 0-based + length = int(fields[i + 1].strip()) + order = fields[i + 3].strip() # skip type field (CH, PD, etc.) + ascending = order.upper() != "D" + if not first: + pscmd += "," + pscmd += ( + f" {{$_.Substring({start},{length})}}" + f"{'' if ascending else ' -Descending'}" + ) + first = False + i += 4 + pscmd += f" | Set-Content '{sortout}' -Encoding Ascii" + else: + pscmd = f"Get-Content '{sortin}' | Sort-Object | Set-Content '{sortout}' -Encoding Ascii" + + print(f" SORT CMD: {pscmd[:100]}...") + result = subprocess.run( + ["powershell", "-NoProfile", "-Command", pscmd], + capture_output=True, text=True, + ) + if result.returncode != 0: + print(f" SORT ERROR: {result.stderr[:200]}") + return result.returncode + + def _resolve_dsn(self, dsn: str, disp: Optional[str] = None) -> Path: + """Map z/OS DSN to Windows file path.""" + # Handle GDG notation (simplified) + dsn = re.sub(r"\(\+?\d+\)", "", dsn).strip(".") + # If it's a z/OS DSN (no slashes, has dots as qualifiers), convert dots + if "/" not in dsn and "\\" not in dsn: + dsn = dsn.replace(".", "/") + path = (self.root_dir / dsn.lstrip("/")).resolve() + return path diff --git a/jcl-runner/main.py b/jcl-runner/main.py new file mode 100644 index 0000000..08ed80e --- /dev/null +++ b/jcl-runner/main.py @@ -0,0 +1,82 @@ +""" +JCL Runner - Main entry point. +Usage: python main.py +""" + +import sys +import argparse +from pathlib import Path + +from parser import parse_jcl +from executor import Executor + + +def main(): + parser = argparse.ArgumentParser( + description="JCL Runner - Execute JCL scripts on Windows" + ) + parser.add_argument("jcl_file", help="Path to JCL script") + parser.add_argument( + "--root", + default=".", + help="System root directory (default: current dir)", + ) + parser.add_argument( + "--cobol-dir", + default="cobol", + help="COBOL source directory (relative to root)", + ) + parser.add_argument( + "--copybook-dir", + default="copybooks", + help="COPYBOOK directory (relative to root)", + ) + + args = parser.parse_args() + + root = Path(args.root).resolve() + cobol_dir = root / args.cobol_dir + copybook_dir = root / args.copybook_dir + + # Validate paths + if not root.exists(): + print(f"ERROR: Root directory not found: {root}") + sys.exit(1) + if not cobol_dir.exists(): + print(f"ERROR: COBOL directory not found: {cobol_dir}") + sys.exit(1) + if not copybook_dir.exists(): + print(f"ERROR: COPYBOOK directory not found: {copybook_dir}") + sys.exit(1) + + # Parse JCL + jcl_path = Path(args.jcl_file) + if not jcl_path.exists(): + print(f"ERROR: JCL file not found: {jcl_path}") + sys.exit(1) + + print(f"Parsing JCL: {jcl_path}") + job = parse_jcl(str(jcl_path)) + + if not job: + print("ERROR: Failed to parse JCL (no JOB statement found)") + sys.exit(1) + + print(f"Job: {job.job_name}, Steps: {len(job.steps)}") + for i, step in enumerate(job.steps): + cond_str = f" COND={step.cond}" if step.cond else "" + print(f" {i+1}. {step.step_name}: EXEC PGM={step.program}{cond_str}") + + # Execute + executor = Executor( + root_dir=str(root), + cobol_dir=str(cobol_dir), + copybook_dir=str(copybook_dir), + ) + rc = executor.run(job) + print(f"\nExit code: {rc}") + sys.exit(rc) + + +if __name__ == "__main__": + main() diff --git a/jcl-runner/parser.py b/jcl-runner/parser.py new file mode 100644 index 0000000..f154292 --- /dev/null +++ b/jcl-runner/parser.py @@ -0,0 +1,190 @@ +""" +JCL Parser - parses JCL scripts into structured JobStep objects. +Phase 1: supports JOB, EXEC PGM=, DD, SYSOUT, SYSIN inline data, + COND=(code,op), * comments. +Phase 2+: PROC, GDG, COND with step names, EVEN/ONLY. +""" + +import re +from dataclasses import dataclass, field +from typing import Optional + + +@dataclass +class DDEntry: + dd_name: str + dsn: Optional[str] = None + disp: Optional[str] = None + sysout: Optional[str] = None + inline_data: list[str] = field(default_factory=list) + unit: Optional[str] = None + space: Optional[str] = None + + +@dataclass +class CondParam: + code: int + operator: str # EQ, NE, GT, GE, LT, LE + step_name: Optional[str] = None # None means "any previous step" + + +@dataclass +class JobStep: + step_name: str + program: str + dd_entries: list[DDEntry] = field(default_factory=list) + cond: Optional[CondParam] = None + parm: Optional[str] = None + + +@dataclass +class Job: + job_name: str + steps: list[JobStep] = field(default_factory=list) + + +# COND operator mapping +COND_OPS = { + "EQ": lambda rc, code: rc == code, + "NE": lambda rc, code: rc != code, + "GT": lambda rc, code: rc > code, + "GE": lambda rc, code: rc >= code, + "LT": lambda rc, code: rc < code, + "LE": lambda rc, code: rc <= code, +} + + +def parse_jcl(filepath: str) -> Job: + """Parse a JCL file into a Job object.""" + with open(filepath, "r", encoding="utf-8") as f: + raw_lines = f.readlines() + + # Continuation handling: lines ending with ',' continue on next line + lines = _merge_continuations(raw_lines) + + job = None + current_step: Optional[JobStep] = None + current_dd: Optional[DDEntry] = None + in_sysin = False + sysin_lines: list[str] = [] + + for line in lines: + stripped = line.strip() + + # Skip comments + if stripped.startswith("//*"): + continue + if not stripped: + continue + + # Handle SYSIN inline data (lines after //SYSIN DD * until /*) + if in_sysin: + if stripped == "/*": + if current_dd: + current_dd.inline_data = sysin_lines + sysin_lines = [] + in_sysin = False + current_dd = None + else: + sysin_lines.append(stripped) + continue + + # Must start with // + if not stripped.startswith("//"): + continue + + content = stripped[2:].strip() + + # JOB statement: //jobname JOB ... + if re.search(r"\bJOB\b", content, re.IGNORECASE): + parts = stripped[2:].split(None, 2) + job_name = parts[0] + job = Job(job_name=job_name) + continue + + # EXEC statement + match = re.match(r"(\w+)\s+EXEC\s+(?:PGM=)?(\w+)", content, re.IGNORECASE) + if match: + step_name = match.group(1) + program = match.group(2) + + # Parse COND parameter + cond = None + cond_match = re.search( + r"COND=\s*\(\s*(\d+)\s*,\s*(\w+)", content, re.IGNORECASE + ) + if cond_match: + code = int(cond_match.group(1)) + op = cond_match.group(2).upper() + cond = CondParam(code=code, operator=op) + + # Parse PARM parameter + parm = None + parm_match = re.search(r"PARM=\s*'([^']*)'", content, re.IGNORECASE) + if parm_match: + parm = parm_match.group(1) + + current_step = JobStep( + step_name=step_name, + program=program, + cond=cond, + parm=parm, + ) + if job: + job.steps.append(current_step) + continue + + # DD statement + dd_match = re.match(r"(\w+)\s+DD\s*(.*)", content, re.IGNORECASE) + if dd_match and current_step is not None: + dd_name = dd_match.group(1) + dd_params = dd_match.group(2) + dd = DDEntry(dd_name=dd_name) + + # Parse DSN + dsn_match = re.search(r"DSN=\s*([^\s,]+)", dd_params, re.IGNORECASE) + if dsn_match: + dd.dsn = dsn_match.group(1) + + # Parse DISP + disp_match = re.search( + r"DISP=\s*\(?([^,\s)]+)(?:,([^,\s)]+))?(?:,([^,\s)]+))?\)?", + dd_params, re.IGNORECASE, + ) + if disp_match: + dd.disp = disp_match.group(1) + + # Parse SYSOUT + sysout_match = re.search(r"SYSOUT=\s*(\*|\w+)", dd_params, re.IGNORECASE) + if sysout_match: + dd.sysout = sysout_match.group(1) + + # Check for SYSIN inline data + if dd_name.upper() == "SYSIN" and "*" in dd_params: + in_sysin = True + + current_step.dd_entries.append(dd) + current_dd = dd + continue + + return job + + +def _merge_continuations(lines: list[str]) -> list[str]: + """Merge JCL continuation lines (lines ending with ',').""" + merged = [] + buffer = "" + for line in lines: + stripped = line.rstrip("\n\r") + if buffer: + buffer += stripped + else: + buffer = stripped + # Check if line ends with continuation + if stripped.rstrip().endswith(",") and not stripped.strip().startswith("//*"): + continue + merged.append(buffer) + buffer = "" + if buffer: + merged.append(buffer) + return merged diff --git a/jcl/CREDIT25.jcl b/jcl/CREDIT25.jcl new file mode 100644 index 0000000..d772be1 --- /dev/null +++ b/jcl/CREDIT25.jcl @@ -0,0 +1,31 @@ +//CREDIT25 JOB (CRD),'MONTHLY BILLING',CLASS=B,MSGCLASS=X +//* +//* 信用卡月结批处理 - 每月25日运行 +//* 系统: COBOL+JCL 学习验证平台 +//* +//STEP1 EXEC PGM=SORT +//SORTIN DD DSN=data/input/transactions.dat,DISP=SHR +//SORTOUT DD DSN=data/work/sorted_tx.dat,DISP=(NEW,DELETE) +//SYSIN DD * + SORT FIELDS=(1,16,CH,A,17,8,CH,A) +/* +//* +//STEP2 EXEC PGM=CRDVAL,COND=(0,NE) +//TRANSIN DD DSN=data/work/sorted_tx.dat,DISP=SHR +//MEMBER DD DSN=data/input/member.dat,DISP=SHR +//VALIDOUT DD DSN=data/work/validated_tx.dat,DISP=(NEW,DELETE) +//REJECT DD DSN=data/output/rejected_tx.dat,DISP=(NEW,CATLG) +//REPORTERR DD DSN=data/output/error_report.dat,DISP=(NEW,CATLG) +//SYSOUT DD SYSOUT=* +//* +//STEP3 EXEC PGM=CRDCALC,COND=(0,NE) +//VALIDIN DD DSN=data/work/validated_tx.dat,DISP=SHR +//RATE DD DSN=data/input/rate.dat,DISP=SHR +//CALCOUT DD DSN=data/work/billing_result.dat,DISP=(NEW,DELETE) +//SYSOUT DD SYSOUT=* +//* +//STEP4 EXEC PGM=CRDRPT,COND=(0,NE) +//BILLING DD DSN=data/work/billing_result.dat,DISP=SHR +//STMT DD DSN=data/output/monthly_statement.dat,DISP=(NEW,CATLG) +//SUMMARY DD DSN=data/output/summary_report.dat,DISP=(NEW,CATLG) +//SYSOUT DD SYSOUT=* diff --git a/run_all.ps1 b/run_all.ps1 new file mode 100644 index 0000000..0d2f6a0 --- /dev/null +++ b/run_all.ps1 @@ -0,0 +1,97 @@ +# COBOL+JCL 信用卡月结系统 完整运行脚本 +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " 信用卡月结批处理系统 - 运行脚本" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan + +$ROOT = $PSScriptRoot +$COBOL_DIR = Join-Path $ROOT "cobol" +$COPYBOOK_DIR = Join-Path $ROOT "copybooks" +$DATA_INPUT = Join-Path $ROOT "data\input" +$DATA_WORK = Join-Path $ROOT "data\work" +$DATA_OUTPUT = Join-Path $ROOT "data\output" +$BIN_DIR = Join-Path $ROOT "bin" +$JCL_DIR = Join-Path $ROOT "jcl" + +# Clean work/output directories +Remove-Item "$DATA_WORK\*" -Force -ErrorAction SilentlyContinue +Remove-Item "$DATA_OUTPUT\*" -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Path $BIN_DIR -Force | Out-Null + +$env:COBCPY = $COPYBOOK_DIR + +Write-Host "`n[STEP 0] Compiling COBOL programs..." -ForegroundColor Yellow + +# Compile data generator +Write-Host " GENDATA.cbl ..." -NoNewline +$r = & cobc -x "$COBOL_DIR\GENDATA.cbl" -o "$BIN_DIR\GENDATA.exe" 2>&1 +if ($LASTEXITCODE -ne 0) { Write-Host " FAILED" -ForegroundColor Red; $r; exit 1 } +Write-Host " OK" -ForegroundColor Green + +# Compile CRDVAL +Write-Host " CRDVAL.cbl ..." -NoNewline +$r = & cobc -x "$COBOL_DIR\CRDVAL.cbl" -o "$BIN_DIR\CRDVAL.exe" 2>&1 +if ($LASTEXITCODE -ne 0) { Write-Host " FAILED" -ForegroundColor Red; $r; exit 1 } +Write-Host " OK" -ForegroundColor Green + +# Compile CRDCALC +Write-Host " CRDCALC.cbl ..." -NoNewline +$r = & cobc -x "$COBOL_DIR\CRDCALC.cbl" -o "$BIN_DIR\CRDCALC.exe" 2>&1 +if ($LASTEXITCODE -ne 0) { Write-Host " FAILED" -ForegroundColor Red; $r; exit 1 } +Write-Host " OK" -ForegroundColor Green + +# Compile CRDRPT +Write-Host " CRDRPT.cbl ..." -NoNewline +$r = & cobc -x "$COBOL_DIR\CRDRPT.cbl" -o "$BIN_DIR\CRDRPT.exe" 2>&1 +if ($LASTEXITCODE -ne 0) { Write-Host " FAILED" -ForegroundColor Red; $r; exit 1 } +Write-Host " OK" -ForegroundColor Green + +# Generate test data +Write-Host "`n[STEP 0.5] Generating test data..." -ForegroundColor Yellow +& "$BIN_DIR\GENDATA.exe" +if ($LASTEXITCODE -ne 0) { Write-Host "GENDATA FAILED" -ForegroundColor Red; exit 1 } + +# Move generated files to data/input +Move-Item "$ROOT\MEMOUT" "$DATA_INPUT\member.dat" -Force -ErrorAction SilentlyContinue +Move-Item "$ROOT\TXOUT" "$DATA_INPUT\transactions.dat" -Force -ErrorAction SilentlyContinue +Move-Item "$ROOT\RATEOUT" "$DATA_INPUT\rate.dat" -Force -ErrorAction SilentlyContinue +Write-Host " Test data -> data/input/" -ForegroundColor Green + +Write-Host "`n[STEP 1] SORT transactions by card + date..." -ForegroundColor Yellow +Get-Content "$DATA_INPUT\transactions.dat" | Sort-Object { $_.Substring(0, 16) }, { $_.Substring(16, 8) } | Set-Content "$DATA_WORK\sorted_tx.dat" -Encoding Ascii +Write-Host " SORTED -> data/work/sorted_tx.dat" -ForegroundColor Green + +Write-Host "`n[STEP 2] CRDVAL - Validate transactions..." -ForegroundColor Yellow +$env:TRANSIN = "$DATA_WORK\sorted_tx.dat" +$env:MEMBER = "$DATA_INPUT\member.dat" +$env:VALIDOUT = "$DATA_WORK\validated_tx.dat" +$env:REJECT = "$DATA_OUTPUT\rejected_tx.dat" +$env:REPORTERR = "$DATA_OUTPUT\error_report.dat" +& "$BIN_DIR\CRDVAL.exe" +if ($LASTEXITCODE -ne 0) { Write-Host "CRDVAL FAILED (RC=$LASTEXITCODE)" -ForegroundColor Red } + +Write-Host "`n[STEP 3] CRDCALC - Calculate interest and fees..." -ForegroundColor Yellow +$env:VALIDIN = "$DATA_WORK\validated_tx.dat" +$env:RATE = "$DATA_INPUT\rate.dat" +$env:CALCOUT = "$DATA_WORK\billing_result.dat" +& "$BIN_DIR\CRDCALC.exe" +if ($LASTEXITCODE -ne 0) { Write-Host "CRDCALC FAILED (RC=$LASTEXITCODE)" -ForegroundColor Red } + +Write-Host "`n[STEP 4] CRDRPT - Generate statements and summary..." -ForegroundColor Yellow +$env:BILLING = "$DATA_WORK\billing_result.dat" +$env:STMT = "$DATA_OUTPUT\monthly_statement.dat" +$env:SUMMARY = "$DATA_OUTPUT\summary_report.dat" +& "$BIN_DIR\CRDRPT.exe" +if ($LASTEXITCODE -ne 0) { Write-Host "CRDRPT FAILED (RC=$LASTEXITCODE)" -ForegroundColor Red } + +Write-Host "`n========================================" -ForegroundColor Cyan +Write-Host " ALL STEPS COMPLETED" -ForegroundColor Green +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "Output files:" +Write-Host " Statement: $DATA_OUTPUT\monthly_statement.dat" +Write-Host " Summary: $DATA_OUTPUT\summary_report.dat" +Write-Host " Rejected: $DATA_OUTPUT\rejected_tx.dat" +Write-Host " Error Rpt: $DATA_OUTPUT\error_report.dat" +Write-Host "" +Write-Host "To run via JCL runner instead:" +Write-Host " python jcl-runner/main.py jcl\CREDIT25.jcl --root $ROOT" +Write-Host "" diff --git a/test_integration.bat b/test_integration.bat new file mode 100644 index 0000000..1595ba1 --- /dev/null +++ b/test_integration.bat @@ -0,0 +1,113 @@ +@echo off +setlocal enabledelayedexpansion + +set ROOT=D:\jcl-cobol +set COB_MAIN_DIR=D:\360安全浏览器下载\GC32-BDB-SP1-rename-7z-to-exe +set COB_CONFIG_DIR=%COB_MAIN_DIR%\config +set COB_LIBRARY_PATH=%COB_MAIN_DIR%\lib\gnucobol +set COBCPY=%ROOT%\copybooks +set PATH=%COB_MAIN_DIR%\bin;%PATH% + +set DI=%ROOT%\data\input +set DW=%ROOT%\data\work +set DO=%ROOT%\data\output + +set passed=0 +set failed=0 + +:: Clean +del /q %DI%\* 2>nul +del /q %DW%\* 2>nul +del /q %DO%\* 2>nul +del /q %ROOT%\MEMOUT 2>nul +del /q %ROOT%\TXOUT 2>nul +del /q %ROOT%\RATEOUT 2>nul + +echo. +echo [STEP 1] GENDATA +pushd %ROOT% +bin\GENDATA.exe +popd + +if not exist %ROOT%\MEMOUT ( + echo FAIL: GENDATA did not create MEMOUT + set /a failed+=1 + goto :done +) else ( + echo PASS: GENDATA created MEMOUT + set /a passed+=1 +) +move %ROOT%\MEMOUT %DI%\member.dat >nul +move %ROOT%\TXOUT %DI%\transactions.dat >nul +move %ROOT%\RATEOUT %DI%\rate.dat >nul + +:: Count lines +for /f %%i in ('find /c /v "" ^< %DI%\member.dat') do set memlines=%%i +for /f %%i in ('find /c /v "" ^< %DI%\transactions.dat') do set txlines=%%i +for /f %%i in ('find /c /v "" ^< %DI%\rate.dat') do set ratelines=%%i + +if %memlines%==5 (echo PASS: members=5 & set /a passed+=1) else (echo FAIL: members expected=5 actual=%memlines% & set /a failed+=1) +if %txlines%==10 (echo PASS: transactions=10 & set /a passed+=1) else (echo FAIL: transactions expected=10 actual=%txlines% & set /a failed+=1) +if %ratelines%==2 (echo PASS: rates=2 & set /a passed+=1) else (echo FAIL: rates expected=2 actual=%ratelines% & set /a failed+=1) + +echo. +echo [STEP 2] SORT +:: Sort by card(16) + date(8) +powershell -NoProfile -Command "Get-Content '%DI%\transactions.dat' | Sort-Object { $_.Substring(0,16) }, { $_.Substring(16,8) } | Set-Content '%DW%\sorted_tx.dat' -Encoding Ascii" +for /f %%i in ('find /c /v "" ^< %DW%\sorted_tx.dat') do set sortedlines=%%i +if %sortedlines%==10 (echo PASS: sorted=10 & set /a passed+=1) else (echo FAIL: sorted expected=10 actual=%sortedlines% & set /a failed+=1) + +:: Check first and last card +for /f "usebackq delims=" %%a in (`powershell -NoProfile -Command "(Get-Content '%DW%\sorted_tx.dat')[0]"`) do set first=%%a +for /f "usebackq delims=" %%a in (`powershell -NoProfile -Command "(Get-Content '%DW%\sorted_tx.dat')[-1]"`) do set last=%%a +echo !first! | findstr "^6222021234567800" >nul && (echo PASS: first card=7800 & set /a passed+=1) || (echo FAIL: first card not 7800 & set /a failed+=1) +echo !last! | findstr "^9999999999999999" >nul && (echo PASS: last card=9999 & set /a passed+=1) || (echo FAIL: last card not 9999 & set /a failed+=1) + +echo. +echo [STEP 3] CRDVAL +set TRANSIN=%DW%\sorted_tx.dat +set MEMBER=%DI%\member.dat +set VALIDOUT=%DW%\validated_tx.dat +set REJECT=%DO%\rejected_tx.dat +set REPORTERR=%DO%\error_report.dat +%ROOT%\bin\CRDVAL.exe + +for /f %%i in ('find /c /v "" ^< %DW%\validated_tx.dat') do set validlines=%%i +for /f %%i in ('find /c /v "" ^< %DO%\rejected_tx.dat') do set rejectlines=%%i +if %validlines%==8 (echo PASS: valid=8 & set /a passed+=1) else (echo FAIL: valid expected=8 actual=%validlines% & set /a failed+=1) +if %rejectlines%==2 (echo PASS: rejects=2 & set /a passed+=1) else (echo FAIL: rejects expected=2 actual=%rejectlines% & set /a failed+=1) + +findstr "FROZEN" %DO%\error_report.dat >nul && (echo PASS: error1=frozen & set /a passed+=1) || (echo FAIL: error1 not frozen & set /a failed+=1) +findstr "NOT-FOUND" %DO%\error_report.dat >nul && (echo PASS: error2=not-found & set /a passed+=1) || (echo FAIL: error2 not not-found & set /a failed+=1) + +echo. +echo [STEP 4] CRDCALC +set VALIDIN=%DW%\validated_tx.dat +set RATE=%DI%\rate.dat +set CALCOUT=%DW%\billing_result.dat +%ROOT%\bin\CRDCALC.exe + +for /f %%i in ('find /c /v "" ^< %DW%\billing_result.dat') do set billinglines=%%i +if %billinglines%==12 (echo PASS: billing=12 & set /a passed+=1) else (echo FAIL: billing expected=12 actual=%billinglines% & set /a failed+=1) + +findstr "6480.20" %DW%\billing_result.dat >nul && (echo PASS: card7800=6480.20 & set /a passed+=1) || (echo FAIL: card7800 amount wrong & set /a failed+=1) +findstr "17200.00" %DW%\billing_result.dat >nul && (echo PASS: card7801=17200.00 & set /a passed+=1) || (echo FAIL: card7801 amount wrong & set /a failed+=1) +findstr "500.00" %DW%\billing_result.dat >nul && (echo PASS: card7802=500.00 & set /a passed+=1) || (echo FAIL: card7802 amount wrong & set /a failed+=1) +findstr "24180.20" %DW%\billing_result.dat >nul && (echo PASS: grand=24180.20 & set /a passed+=1) || (echo FAIL: grand total wrong & set /a failed+=1) + +echo. +echo [STEP 5] CRDRPT +set BILLING=%DW%\billing_result.dat +set STMT=%DO%\monthly_statement.dat +set SUMMARY=%DO%\summary_report.dat +%ROOT%\bin\CRDRPT.exe + +for /f %%i in ('find /c /v "" ^< %DO%\monthly_statement.dat') do set stmtlines=%%i +for /f %%i in ('find /c /v "" ^< %DO%\summary_report.dat') do set sumlines=%%i +if %stmtlines%==14 (echo PASS: statement=14 lines & set /a passed+=1) else (echo FAIL: statement expected=14 actual=%stmtlines% & set /a failed+=1) +if %sumlines%==5 (echo PASS: summary=5 lines & set /a passed+=1) else (echo FAIL: summary expected=5 actual=%sumlines% & set /a failed+=1) + +:done +echo. +echo === RESULT: %passed% passed, %failed% failed === +exit /b %failed% diff --git a/verify_comp3.py b/verify_comp3.py new file mode 100644 index 0000000..0e11e2f --- /dev/null +++ b/verify_comp3.py @@ -0,0 +1,97 @@ +""" +COMP-3 Packed Decimal Verification for COBOL Migration Platform. +Validates that binary COMP-3 fields in RATE.dat are correctly encoded. + +Usage: python verify_comp3.py + +COMP-3 format: each byte holds 2 nibbles (4-bit digits), +last nibble = sign (0xC/0xF = positive, 0xD = negative). +PIC 9(1)V9(4) = 5 digits + sign = 3 bytes. +""" + +import struct +import sys + + +def unpack_comp3(data: bytes) -> tuple[int, int, str]: + """Unpack COMP-3 bytes -> (integer_value, decimal_places, sign).""" + nibbles = [] + for byte in data: + nibbles.append((byte >> 4) & 0x0F) + nibbles.append(byte & 0x0F) + + sign_nibble = nibbles[-1] + digit_nibbles = nibbles[:-1] + + value = 0 + for n in digit_nibbles: + value = value * 10 + n + + if sign_nibble in (0xC, 0xF): + sign = "positive" + elif sign_nibble == 0xD: + sign = "negative" + else: + sign = f"unknown(0x{sign_nibble:X})" + + return value, sign, data.hex() + + +def main(): + rate_path = "D:/jcl-cobol/data/input/rate.dat" + + with open(rate_path, "rb") as f: + data = f.read() + + rec_size = 12 # PIC X(1) + PIC 9(1)V9(4) COMP-3(3) + PIC 9(8) + num_records = len(data) // rec_size + + print(f"File: {rate_path}") + print(f"Size: {len(data)} bytes") + print(f"Records: {num_records}") + print() + + expected = { + "C": ("Cash rate", 0.0005), + "O": ("Overdue rate", 0.0500), + } + + all_ok = True + + for i in range(num_records): + offset = i * rec_size + rec = data[offset : offset + rec_size] + + rate_type = chr(rec[0]) + pct_bytes = rec[1:4] + eff_date = rec[4:12].decode("ascii") + + value, sign, pct_hex = unpack_comp3(pct_bytes) + int_part = value // 10000 + dec_part = value % 10000 + pct_float = int_part + dec_part / 10000 + + name, expected_val = expected.get(rate_type, ("Unknown", None)) + + match = abs(pct_float - expected_val) < 0.0001 if expected_val else False + status = "PASS" if match else "FAIL" + if not match: + all_ok = False + + print(f"[{status}] Record {i+1}: type={rate_type} ({name})") + print(f" COMP-3 hex: {pct_hex}") + print(f" packed int: {value}") + print(f" float val: {pct_float:.4f}") + print(f" expected: {expected_val}") + print(f" eff date: {eff_date}") + print() + + if all_ok: + print("=== ALL COMP-3 VALUES VERIFIED ===") + else: + print("=== COMP-3 MISMATCH DETECTED ===") + sys.exit(1) + + +if __name__ == "__main__": + main()