From e54372809339f0f8fd3efb74edd43ef0ba588933 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:33:22 +0000 Subject: [PATCH 1/7] Add Java migration foundation and HISTLD00 reference slice Co-Authored-By: Eashan Sinha --- java/.gitignore | 1 + java/MIGRATION.md | 140 ++++++++++++ java/pom.xml | 58 +++++ .../com/portfolio/PortfolioApplication.java | 18 ++ .../portfolio/batch/BatchControlService.java | 82 +++++++ .../batch/ErrorLimitExceededException.java | 14 ++ .../portfolio/batch/HistoryItemProcessor.java | 112 +++++++++ .../portfolio/batch/HistoryItemWriter.java | 40 ++++ .../portfolio/batch/HistoryLoadJobConfig.java | 127 ++++++++++ .../portfolio/batch/HistoryLoadJobRunner.java | 61 +++++ .../com/portfolio/batch/HistoryLoadStats.java | 56 +++++ .../portfolio/common/DataSourceConfig.java | 31 +++ .../common/ErrorHandlingService.java | 79 +++++++ .../common/FileProcessingException.java | 32 +++ .../common/SqlProcessingException.java | 32 +++ .../portfolio/common/TransactionHelper.java | 76 ++++++ .../com/portfolio/domain/BatchControl.java | 148 ++++++++++++ .../java/com/portfolio/domain/ErrorLog.java | 117 ++++++++++ .../portfolio/domain/InvestmentPosition.java | 113 +++++++++ .../com/portfolio/domain/PortfolioMaster.java | 92 ++++++++ .../com/portfolio/domain/PositionHistory.java | 171 ++++++++++++++ .../com/portfolio/domain/ReturnCodeLog.java | 88 +++++++ .../portfolio/domain/TransactionHistory.java | 103 +++++++++ .../domain/TransactionHistoryFileRecord.java | 151 ++++++++++++ .../portfolio/model/copybook/AuditRecord.java | 73 ++++++ .../model/copybook/BatchControlConstants.java | 58 +++++ .../model/copybook/BatchControlRecord.java | 103 +++++++++ .../model/copybook/CheckpointControl.java | 114 +++++++++ .../model/copybook/CommonConstants.java | 38 +++ .../model/copybook/ErrorMessage.java | 70 ++++++ .../model/copybook/HistoryRecord.java | 73 ++++++ .../model/copybook/PortfolioRecord.java | 67 ++++++ .../model/copybook/PortfolioValidation.java | 33 +++ .../model/copybook/PositionRecord.java | 62 +++++ .../model/copybook/ProcessSequenceRecord.java | 142 ++++++++++++ .../model/copybook/ReturnCodeArea.java | 92 ++++++++ .../model/copybook/SqlStatusCodes.java | 32 +++ .../model/copybook/TransactionRecord.java | 78 +++++++ .../repository/BatchControlRepository.java | 8 + .../repository/ErrorLogRepository.java | 8 + .../repository/PositionHistoryRepository.java | 8 + .../TransactionHistoryFileRepository.java | 9 + java/src/main/resources/application.yml | 33 +++ java/src/main/resources/sample-data.sql | 16 ++ .../portfolio/batch/HistoryLoadJobTest.java | 216 ++++++++++++++++++ 45 files changed, 3275 insertions(+) create mode 100644 java/.gitignore create mode 100644 java/MIGRATION.md create mode 100644 java/pom.xml create mode 100644 java/src/main/java/com/portfolio/PortfolioApplication.java create mode 100644 java/src/main/java/com/portfolio/batch/BatchControlService.java create mode 100644 java/src/main/java/com/portfolio/batch/ErrorLimitExceededException.java create mode 100644 java/src/main/java/com/portfolio/batch/HistoryItemProcessor.java create mode 100644 java/src/main/java/com/portfolio/batch/HistoryItemWriter.java create mode 100644 java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java create mode 100644 java/src/main/java/com/portfolio/batch/HistoryLoadJobRunner.java create mode 100644 java/src/main/java/com/portfolio/batch/HistoryLoadStats.java create mode 100644 java/src/main/java/com/portfolio/common/DataSourceConfig.java create mode 100644 java/src/main/java/com/portfolio/common/ErrorHandlingService.java create mode 100644 java/src/main/java/com/portfolio/common/FileProcessingException.java create mode 100644 java/src/main/java/com/portfolio/common/SqlProcessingException.java create mode 100644 java/src/main/java/com/portfolio/common/TransactionHelper.java create mode 100644 java/src/main/java/com/portfolio/domain/BatchControl.java create mode 100644 java/src/main/java/com/portfolio/domain/ErrorLog.java create mode 100644 java/src/main/java/com/portfolio/domain/InvestmentPosition.java create mode 100644 java/src/main/java/com/portfolio/domain/PortfolioMaster.java create mode 100644 java/src/main/java/com/portfolio/domain/PositionHistory.java create mode 100644 java/src/main/java/com/portfolio/domain/ReturnCodeLog.java create mode 100644 java/src/main/java/com/portfolio/domain/TransactionHistory.java create mode 100644 java/src/main/java/com/portfolio/domain/TransactionHistoryFileRecord.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/AuditRecord.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/BatchControlConstants.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/BatchControlRecord.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/CheckpointControl.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/CommonConstants.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/ErrorMessage.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/HistoryRecord.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/PortfolioRecord.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/PortfolioValidation.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/PositionRecord.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/ProcessSequenceRecord.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/ReturnCodeArea.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/SqlStatusCodes.java create mode 100644 java/src/main/java/com/portfolio/model/copybook/TransactionRecord.java create mode 100644 java/src/main/java/com/portfolio/repository/BatchControlRepository.java create mode 100644 java/src/main/java/com/portfolio/repository/ErrorLogRepository.java create mode 100644 java/src/main/java/com/portfolio/repository/PositionHistoryRepository.java create mode 100644 java/src/main/java/com/portfolio/repository/TransactionHistoryFileRepository.java create mode 100644 java/src/main/resources/application.yml create mode 100644 java/src/main/resources/sample-data.sql create mode 100644 java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java diff --git a/java/.gitignore b/java/.gitignore new file mode 100644 index 00000000..2f7896d1 --- /dev/null +++ b/java/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/java/MIGRATION.md b/java/MIGRATION.md new file mode 100644 index 00000000..37243248 --- /dev/null +++ b/java/MIGRATION.md @@ -0,0 +1,140 @@ +# COBOL → Java Migration Guide + +This directory contains the Java migration of the Enterprise COBOL Investment +Portfolio Management System. The original COBOL source remains under `src/` +for reference. The first vertical slice migrated end-to-end is **HISTLD00** +(Position History DB2 Load), which establishes the conventions below for all +subsequent programs. + +## Project layout + +``` +java/ +├── pom.xml Maven, Java 17, Spring Boot 3 (Batch + Data JPA + H2) +└── src/main/java/com/portfolio/ + ├── model/copybook/ Step 1: copybook record layouts → POJOs + ├── domain/ Step 2: DB2 DDL + VSAM KSDS → JPA entities + ├── repository/ Spring Data JPA repositories + ├── common/ Step 3: DB2CONN/DB2CMT/ERRPROC migrations + └── batch/ Step 4: HISTLD00 Spring Batch job +``` + +## Mapping conventions + +### Copybook → class (`model/copybook/`) + +Each `01` record layout in `src/copybook/` becomes one Java class with the +same field names (COBOL-CASE → camelCase) and the original PIC clause +preserved in Javadoc. Level-88 condition names and constant copybooks +(BCHCON, COMMON) become `public static final` constants. `OCCURS n TIMES` +tables become `List`. + +| COBOL | Java | +|---|---| +| `HISTREC` | `HistoryRecord` | +| `BCHCTL` | `BatchControlRecord` | +| `BCHCON` | `BatchControlConstants` | +| `CKPRST` | `CheckpointControl` | +| `PRCSEQ` | `ProcessSequenceRecord` | +| `TRNREC` | `TransactionRecord` | +| `POSREC` | `PositionRecord` | +| `PORTFLIO` | `PortfolioRecord` | +| `AUDITLOG` | `AuditRecord` | +| `RTNCODE`/`RETHND` | `ReturnCodeArea` | +| `ERRHAND` | `ErrorMessage` | +| `PORTVAL` | `PortfolioValidation` | +| `SQLCA` | `SqlStatusCodes` (constants only — see below) | +| `DBTBLS` (host variables) | JPA entities in `domain/` | + +### PIC → Java type + +| COBOL PIC | Java type | +|---|---| +| `PIC X(n)` | `String` (length documented / `@Column(length = n)`) | +| `PIC 9(n)` / `PIC S9(n)` display | `int` / `long` (`String` if it encodes a date/time) | +| `PIC S9(4) COMP` | `int` | +| `PIC S9(9) COMP` | `long` (counters) or `int` (codes) | +| `PIC S9(n)V9(m) COMP-3` (packed decimal, financial) | `BigDecimal` — always, never `double` | +| Dates `PIC X(8)`/`PIC X(10)` (YYYYMMDD / ISO) | `LocalDate` in entities, `String` in raw copybook models | +| Times `PIC X(6)`/`PIC X(8)` | `LocalTime` in entities | +| Timestamps `PIC X(26)` | `LocalDateTime` in entities | +| Level-88 values | `static final` constants | + +### VSAM KSDS → relational table (`domain/`) + +Each VSAM indexed file becomes a table named `VSAM_` whose composite +primary key (`@EmbeddedId`) is exactly the COBOL `RECORD KEY`: + +| VSAM file | Entity | Key (COBOL RECORD KEY) | +|---|---|---| +| TRANHIST | `TransactionHistoryFileRecord` | trans date + time + portfolio + sequence (`TH-KEY`) | +| BCHCTL | `BatchControl` | job name + process date + sequence (`BCT-KEY`) | + +DB2 tables from `src/database/db2/` map 1:1 to entities: `POSHIST` → +`PositionHistory`, `ERRLOG` → `ErrorLog`, `RTNCODES` → `ReturnCodeLog`, +`PORTFOLIO_MASTER` → `PortfolioMaster`, `INVESTMENT_POSITIONS` → +`InvestmentPosition`, `TRANSACTION_HISTORY` → `TransactionHistory`. +`DECIMAL(p,s)` → `BigDecimal` with matching `precision`/`scale`. + +### EXEC SQL → JPA / Spring Data + +- Singleton `SELECT`/`INSERT`/`UPDATE` → Spring Data repository methods. +- Cursors → paged reads (`RepositoryItemReader`) or streaming queries. +- `SQLCODE` checks → exceptions: non-zero SQLCODE branches become + `SqlProcessingException` (or Spring's `DataAccessException`); code-specific + behavior is preserved explicitly (e.g. `-803` duplicate → existence check). +- Two-character `FILE STATUS` checks → `FileProcessingException`, preserving + the original status value (e.g. `'23'` record not found). + +### Common subprograms → shared infrastructure (`common/`) + +| COBOL | Java | +|---|---| +| `DB2CONN` (connect/disconnect/status, retry) | `DataSourceConfig` — Spring Boot pooled `DataSource` (HikariCP) | +| `DB2CMT` (commit/rollback/savepoint, frequency) | `TransactionHelper` + Spring transactions; in batch jobs, chunk boundaries | +| `ERRPROC` (format + write ERRLOG, return severity) | `ErrorHandlingService.logError(...)` → `ERRLOG` table, REQUIRES_NEW | + +### JCL → Spring Batch + +| JCL / COBOL batch concept | Spring Batch | +|---|---| +| Job step running a program | `Job` + `Step` bean (`HistoryLoadJobConfig`) | +| Sequential file read loop | `ItemReader` | +| Record validation / field moves | `ItemProcessor` | +| DB2 INSERT + commit threshold | `ItemWriter` + chunk size (HISTLD00: 1000) | +| Checkpoint REWRITE of BCHCTL | `ChunkListener.afterChunk` → `BatchControlService` | +| `MOVE ... TO RETURN-CODE` | `ExitCodeGenerator` (JVM exit code) | +| Abort condition (`WS-ERROR-COUNT > 100`) | exception from processor fails the job | +| JCL parameters (dates etc.) | `JobParameters` | + +### CICS → REST (deferred) + +The online layer (`src/programs/online/`, `src/maps/`) is **not** migrated in +this pass. BMS screens and CICS transactions do not translate mechanically — +they require a UI/API redesign (REST controllers + a separate frontend, +pseudo-conversational state → stateless requests, EIBAID keys → HTTP verbs). +Flagged as a separate redesign effort. The online copybooks are already +modeled in `model/copybook/` so the future REST layer can reuse them. + +## Reference slice: HISTLD00 + +`src/programs/batch/HISTLD00.cbl` → `com.portfolio.batch`: + +- Reads `VSAM_TRANHIST` in RECORD KEY order (`RepositoryItemReader`). +- Validates and maps TH-* → PH-* fields (`HistoryItemProcessor`). +- Inserts into `POSHIST`, skipping duplicates like SQLCODE `-803` + (`HistoryItemWriter`). +- Commits every 1000 records via the chunk size (WS-COMMIT-THRESHOLD). +- Updates the batch control record after each commit and at job end + (`BatchControlService`). +- Counts errors (`HistoryLoadStats`); aborts when the count exceeds 100 and + returns the error count as the process exit code (`HistoryLoadJobRunner`). + +## Building and running + +```bash +cd java +mvn test # runs the HISTLD00 job tests against embedded H2 +mvn spring-boot:run -Dspring-boot.run.profiles=histld00 \ + -Dspring-boot.run.arguments=20240320 # sample run with seeded data +``` diff --git a/java/pom.xml b/java/pom.xml new file mode 100644 index 00000000..80c51e26 --- /dev/null +++ b/java/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.portfolio + portfolio-mgmt + 0.1.0-SNAPSHOT + portfolio-mgmt + Java migration of the Enterprise COBOL Investment Portfolio Management System + + + 17 + + + + + org.springframework.boot + spring-boot-starter-batch + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.batch + spring-batch-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/java/src/main/java/com/portfolio/PortfolioApplication.java b/java/src/main/java/com/portfolio/PortfolioApplication.java new file mode 100644 index 00000000..d9817f45 --- /dev/null +++ b/java/src/main/java/com/portfolio/PortfolioApplication.java @@ -0,0 +1,18 @@ +package com.portfolio; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Java migration of the Enterprise COBOL Investment Portfolio Management System. + * + *

The CICS online layer (src/programs/online, src/maps) is intentionally NOT + * migrated here — it is flagged as a separate redesign effort (CICS → REST). + */ +@SpringBootApplication +public class PortfolioApplication { + + public static void main(String[] args) { + System.exit(SpringApplication.exit(SpringApplication.run(PortfolioApplication.class, args))); + } +} diff --git a/java/src/main/java/com/portfolio/batch/BatchControlService.java b/java/src/main/java/com/portfolio/batch/BatchControlService.java new file mode 100644 index 00000000..056c9618 --- /dev/null +++ b/java/src/main/java/com/portfolio/batch/BatchControlService.java @@ -0,0 +1,82 @@ +package com.portfolio.batch; + +import com.portfolio.common.FileProcessingException; +import com.portfolio.domain.BatchControl; +import com.portfolio.repository.BatchControlRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * Batch control file handling from HISTLD00 (1300-INIT-CHECKPOINTS, + * 2310-UPDATE-CHECKPOINT) against the BCHCTL migration table. + * + *

Status values are from {@code src/copybook/batch/BCHCON.cpy}: + * 'A' = active, 'C'/'D' = complete/done, 'E' = error. + */ +@Service +public class BatchControlService { + + private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm:ss"); + + /** DB2-style 26-character timestamp, matching BCT-ATTEMPT-TS / BCT-COMPLETE-TS PIC X(26). */ + private static final DateTimeFormatter TS_FMT = + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH.mm.ss.SSSSSS"); + + private final BatchControlRepository repository; + + public BatchControlService(BatchControlRepository repository) { + this.repository = repository; + } + + /** + * HISTLD00 1300-INIT-CHECKPOINTS: read the control record for the job and + * mark it active. INVALID KEY (record not found) becomes a + * {@link FileProcessingException} with FILE STATUS '23'. + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public BatchControl.Key markActive(String jobName, String processDate) { + BatchControl control = find(jobName, processDate); + control.setStatus("A"); + control.setStartTime(LocalTime.now().format(TIME_FMT)); + control.setAttemptTimestamp(LocalDateTime.now().format(TS_FMT)); + control.setRestartCount(control.getRestartCount() + 1); + repository.save(control); + return control.getKey(); + } + + /** HISTLD00 2310-UPDATE-CHECKPOINT: persist read/written counters. */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void updateCheckpoint(String jobName, String processDate, + long recordsRead, long recordsWritten) { + BatchControl control = find(jobName, processDate); + control.setRecordsRead(recordsRead); + control.setRecordsWritten(recordsWritten); + repository.save(control); + } + + /** Final control update at job end: status, counters, and return code. */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void markComplete(String jobName, String processDate, + long recordsRead, long recordsWritten, int returnCode) { + BatchControl control = find(jobName, processDate); + control.setStatus(returnCode > HistoryLoadStats.MAX_ERRORS ? "E" : "C"); + control.setRecordsRead(recordsRead); + control.setRecordsWritten(recordsWritten); + control.setReturnCode(returnCode); + control.setEndTime(LocalTime.now().format(TIME_FMT)); + control.setCompleteTimestamp(LocalDateTime.now().format(TS_FMT)); + repository.save(control); + } + + private BatchControl find(String jobName, String processDate) { + return repository.findById(new BatchControl.Key(jobName, processDate, 1)) + .orElseThrow(() -> new FileProcessingException( + "Control record not found for job " + jobName + + " date " + processDate, "23")); + } +} diff --git a/java/src/main/java/com/portfolio/batch/ErrorLimitExceededException.java b/java/src/main/java/com/portfolio/batch/ErrorLimitExceededException.java new file mode 100644 index 00000000..128ba3f4 --- /dev/null +++ b/java/src/main/java/com/portfolio/batch/ErrorLimitExceededException.java @@ -0,0 +1,14 @@ +package com.portfolio.batch; + +/** + * Thrown to abort the HISTLD00 job when the error count exceeds 100, + * matching the COBOL loop condition + * {@code PERFORM 2000-PROCESS UNTIL END-OF-FILE OR WS-ERROR-COUNT > 100}. + */ +public class ErrorLimitExceededException extends RuntimeException { + + public ErrorLimitExceededException(long errorCount) { + super("HISTLD00 aborted: error count " + errorCount + " exceeded limit of " + + HistoryLoadStats.MAX_ERRORS); + } +} diff --git a/java/src/main/java/com/portfolio/batch/HistoryItemProcessor.java b/java/src/main/java/com/portfolio/batch/HistoryItemProcessor.java new file mode 100644 index 00000000..d9093b6d --- /dev/null +++ b/java/src/main/java/com/portfolio/batch/HistoryItemProcessor.java @@ -0,0 +1,112 @@ +package com.portfolio.batch; + +import com.portfolio.common.ErrorHandlingService; +import com.portfolio.domain.PositionHistory; +import com.portfolio.domain.TransactionHistoryFileRecord; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Set; + +/** + * Validation + mapping step of the HISTLD00 migration + * (COBOL 2200-LOAD-TO-DB2 field moves TH-* → PH-*). + * + *

Invalid records are counted (WS-ERROR-COUNT), logged via the ERRPROC + * migration, and filtered (return null) so the load continues — until the + * error count exceeds 100, at which point the job aborts, matching + * {@code UNTIL END-OF-FILE OR WS-ERROR-COUNT > 100}. + */ +@Component +public class HistoryItemProcessor + implements ItemProcessor { + + private static final Set VALID_TRANS_TYPES = Set.of("BU", "SL", "TR", "FE"); + + private final HistoryLoadStats stats; + private final ErrorHandlingService errorHandlingService; + + public HistoryItemProcessor(HistoryLoadStats stats, ErrorHandlingService errorHandlingService) { + this.stats = stats; + this.errorHandlingService = errorHandlingService; + } + + @Override + public PositionHistory process(TransactionHistoryFileRecord item) { + stats.incrementRecordsRead(); + + String validationError = validate(item); + if (validationError != null) { + long errors = stats.incrementErrorCount(); + errorHandlingService.logError("HISTLD00", "V", 2, "HIST0001", + validationError, String.valueOf(item.getKey() == null ? null + : item.getKey().getPortfolioId() + "/" + item.getKey().getSequenceNo())); + if (errors > HistoryLoadStats.MAX_ERRORS) { + throw new ErrorLimitExceededException(errors); + } + return null; + } + + PositionHistory history = new PositionHistory(); + history.setKey(new PositionHistory.Key( + item.getAccountNo(), + item.getKey().getPortfolioId(), + item.getKey().getTransDate(), + item.getKey().getTransTime())); + history.setTransType(item.getTransType()); + history.setSecurityId(item.getSecurityId()); + history.setQuantity(item.getQuantity()); + history.setPrice(item.getPrice()); + history.setAmount(item.getAmount()); + history.setFees(item.getFees()); + history.setTotalAmount(item.getTotalAmount()); + history.setCostBasis(item.getCostBasis()); + history.setGainLoss(item.getGainLoss()); + + // POSHIST audit columns (PROCESS_DATE/TIME default CURRENT DATE/TIME in DDL) + LocalDateTime now = LocalDateTime.now(); + history.setProcessDate(now.toLocalDate()); + history.setProcessTime(now.toLocalTime()); + history.setProgramId("HISTLD00"); + history.setUserId(System.getProperty("user.name", "BATCH")); + history.setAuditTimestamp(now); + return history; + } + + private String validate(TransactionHistoryFileRecord item) { + if (item.getKey() == null + || item.getKey().getTransDate() == null + || item.getKey().getTransTime() == null + || isBlank(item.getKey().getPortfolioId())) { + return "Missing transaction key fields"; + } + if (isBlank(item.getAccountNo())) { + return "Missing account number"; + } + if (item.getTransType() == null || !VALID_TRANS_TYPES.contains(item.getTransType())) { + return "Invalid transaction type: " + item.getTransType(); + } + if (isBlank(item.getSecurityId())) { + return "Missing security ID"; + } + if (isNullOrNegativeRequired(item.getQuantity()) + || isNullOrNegativeRequired(item.getPrice()) + || item.getAmount() == null + || item.getTotalAmount() == null + || item.getCostBasis() == null + || item.getGainLoss() == null) { + return "Missing or invalid numeric fields"; + } + return null; + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } + + private static boolean isNullOrNegativeRequired(BigDecimal value) { + return value == null || value.signum() < 0; + } +} diff --git a/java/src/main/java/com/portfolio/batch/HistoryItemWriter.java b/java/src/main/java/com/portfolio/batch/HistoryItemWriter.java new file mode 100644 index 00000000..6d7347e1 --- /dev/null +++ b/java/src/main/java/com/portfolio/batch/HistoryItemWriter.java @@ -0,0 +1,40 @@ +package com.portfolio.batch; + +import com.portfolio.domain.PositionHistory; +import com.portfolio.repository.PositionHistoryRepository; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +/** + * Insert step of the HISTLD00 migration (COBOL + * {@code EXEC SQL INSERT INTO POSHIST VALUES (:POSHIST-RECORD)}). + * + *

Duplicate handling: HISTLD00 treats SQLCODE -803 (duplicate key) as a + * no-op ({@code IF SQLCODE = -803 CONTINUE}); here a record whose key already + * exists is skipped without counting as written or as an error. Chunk-based + * commits by Spring Batch replace the manual WS-COMMIT-THRESHOLD (1000) + * commit logic in 2300-CHECK-COMMIT. + */ +@Component +public class HistoryItemWriter implements ItemWriter { + + private final PositionHistoryRepository repository; + private final HistoryLoadStats stats; + + public HistoryItemWriter(PositionHistoryRepository repository, HistoryLoadStats stats) { + this.repository = repository; + this.stats = stats; + } + + @Override + public void write(Chunk chunk) { + for (PositionHistory item : chunk) { + if (repository.existsById(item.getKey())) { + continue; + } + repository.save(item); + stats.incrementRecordsWritten(); + } + } +} diff --git a/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java b/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java new file mode 100644 index 00000000..7004c6b9 --- /dev/null +++ b/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java @@ -0,0 +1,127 @@ +package com.portfolio.batch; + +import com.portfolio.domain.PositionHistory; +import com.portfolio.domain.TransactionHistoryFileRecord; +import com.portfolio.repository.TransactionHistoryFileRepository; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.ChunkListener; +import org.springframework.batch.item.data.RepositoryItemReader; +import org.springframework.batch.item.data.builder.RepositoryItemReaderBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Sort; +import org.springframework.transaction.PlatformTransactionManager; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Spring Batch migration of {@code src/programs/batch/HISTLD00.cbl} + * (Position History DB2 Load) — the reference vertical slice. + * + *

COBOL → Spring Batch mapping: + *

    + *
  • Sequential READ of the indexed TRANSACTION-HISTORY file (2100) → + * {@link RepositoryItemReader} over the VSAM_TRANHIST table, ordered by + * the COBOL RECORD KEY
  • + *
  • 2200-LOAD-TO-DB2 field mapping/validation → {@link HistoryItemProcessor}
  • + *
  • INSERT INTO POSHIST + SQLCODE -803 handling → {@link HistoryItemWriter}
  • + *
  • 2300-CHECK-COMMIT with WS-COMMIT-THRESHOLD 1000 → chunk size 1000 + * (each chunk boundary is a commit)
  • + *
  • 2310-UPDATE-CHECKPOINT (REWRITE of the BCHCTL record) → afterChunk + * listener updating the batch control table
  • + *
  • WS-ERROR-COUNT > 100 abort → {@link ErrorLimitExceededException} + * thrown from the processor
  • + *
  • MOVE WS-ERROR-COUNT TO RETURN-CODE → process exit code from + * {@link HistoryLoadJobRunner}
  • + *
+ */ +@Configuration +public class HistoryLoadJobConfig { + + /** WS-COMMIT-THRESHOLD PIC S9(4) COMP VALUE 1000. */ + public static final int COMMIT_THRESHOLD = 1000; + + public static final String JOB_NAME = "histld00Job"; + public static final String PROGRAM_ID = "HISTLD00"; + + @Bean + public RepositoryItemReader historyItemReader( + TransactionHistoryFileRepository repository) { + Map sorts = new LinkedHashMap<>(); + sorts.put("key.transDate", Sort.Direction.ASC); + sorts.put("key.transTime", Sort.Direction.ASC); + sorts.put("key.portfolioId", Sort.Direction.ASC); + sorts.put("key.sequenceNo", Sort.Direction.ASC); + return new RepositoryItemReaderBuilder() + .name("historyItemReader") + .repository(repository) + .methodName("findAll") + .pageSize(COMMIT_THRESHOLD) + .sorts(sorts) + .build(); + } + + @Bean + public Step histld00Step(JobRepository jobRepository, + PlatformTransactionManager transactionManager, + RepositoryItemReader historyItemReader, + HistoryItemProcessor processor, + HistoryItemWriter writer, + BatchControlService batchControlService, + HistoryLoadStats stats) { + return new StepBuilder("histld00Step", jobRepository) + .chunk(COMMIT_THRESHOLD, transactionManager) + .reader(historyItemReader) + .processor(processor) + .writer(writer) + .listener(new ChunkListener() { + @Override + public void afterChunk(ChunkContext context) { + JobParameters params = context.getStepContext() + .getStepExecution().getJobParameters(); + batchControlService.updateCheckpoint( + PROGRAM_ID, + params.getString("processDate"), + stats.getRecordsRead(), + stats.getRecordsWritten()); + } + }) + .build(); + } + + @Bean + public Job histld00Job(JobRepository jobRepository, + Step histld00Step, + BatchControlService batchControlService, + HistoryLoadStats stats) { + return new JobBuilder(JOB_NAME, jobRepository) + .listener(new JobExecutionListener() { + @Override + public void beforeJob(JobExecution jobExecution) { + stats.reset(); + batchControlService.markActive(PROGRAM_ID, + jobExecution.getJobParameters().getString("processDate")); + } + + @Override + public void afterJob(JobExecution jobExecution) { + batchControlService.markComplete(PROGRAM_ID, + jobExecution.getJobParameters().getString("processDate"), + stats.getRecordsRead(), + stats.getRecordsWritten(), + (int) Math.min(stats.getErrorCount(), Integer.MAX_VALUE)); + } + }) + .start(histld00Step) + .build(); + } +} diff --git a/java/src/main/java/com/portfolio/batch/HistoryLoadJobRunner.java b/java/src/main/java/com/portfolio/batch/HistoryLoadJobRunner.java new file mode 100644 index 00000000..b6e63a33 --- /dev/null +++ b/java/src/main/java/com/portfolio/batch/HistoryLoadJobRunner.java @@ -0,0 +1,61 @@ +package com.portfolio.batch; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * JCL → Spring Boot mapping: the HISTLD00 job step from + * {@code src/jcl/HISTLOAD.jcl} becomes this runner, activated with the + * {@code histld00} profile. The COBOL RETURN-CODE (error count) becomes the + * JVM exit code via {@link ExitCodeGenerator}. + */ +@Component +@Profile("histld00") +public class HistoryLoadJobRunner implements ApplicationRunner, ExitCodeGenerator { + + private final JobLauncher jobLauncher; + private final Job histld00Job; + private final HistoryLoadStats stats; + + public HistoryLoadJobRunner(JobLauncher jobLauncher, Job histld00Job, HistoryLoadStats stats) { + this.jobLauncher = jobLauncher; + this.histld00Job = histld00Job; + this.stats = stats; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + String processDate = args.getNonOptionArgs().isEmpty() + ? LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + : args.getNonOptionArgs().get(0); + JobParameters params = new JobParametersBuilder() + .addString("processDate", processDate) + .addLong("startedAt", System.currentTimeMillis()) + .toJobParameters(); + JobExecution execution = jobLauncher.run(histld00Job, params); + + // HISTLD00 3400-DISPLAY-STATS + System.out.println("HISTLD00 Processing Statistics:"); + System.out.println(" Records Read: " + stats.getRecordsRead()); + System.out.println(" Records Written: " + stats.getRecordsWritten()); + System.out.println(" Errors: " + stats.getErrorCount()); + System.out.println(" Job Status: " + execution.getStatus()); + } + + /** MOVE WS-ERROR-COUNT TO RETURN-CODE. */ + @Override + public int getExitCode() { + return (int) Math.min(stats.getErrorCount(), 255); + } +} diff --git a/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java b/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java new file mode 100644 index 00000000..432555a0 --- /dev/null +++ b/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java @@ -0,0 +1,56 @@ +package com.portfolio.batch; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Migration of HISTLD00's WS-COUNTERS working storage: records read, records + * written, and error count. The error count becomes the process exit code + * (COBOL {@code MOVE WS-ERROR-COUNT TO RETURN-CODE}). + */ +@Component +public class HistoryLoadStats { + + /** WS-ERROR-COUNT limit: HISTLD00 stops processing when the count exceeds 100. */ + public static final int MAX_ERRORS = 100; + + private final AtomicLong recordsRead = new AtomicLong(); + private final AtomicLong recordsWritten = new AtomicLong(); + private final AtomicLong errorCount = new AtomicLong(); + + public void reset() { + recordsRead.set(0); + recordsWritten.set(0); + errorCount.set(0); + } + + public long incrementRecordsRead() { + return recordsRead.incrementAndGet(); + } + + public long incrementRecordsWritten() { + return recordsWritten.incrementAndGet(); + } + + public long incrementErrorCount() { + return errorCount.incrementAndGet(); + } + + public long getRecordsRead() { + return recordsRead.get(); + } + + public long getRecordsWritten() { + return recordsWritten.get(); + } + + public long getErrorCount() { + return errorCount.get(); + } + + /** COBOL: UNTIL ... WS-ERROR-COUNT > 100. */ + public boolean errorLimitExceeded() { + return errorCount.get() > MAX_ERRORS; + } +} diff --git a/java/src/main/java/com/portfolio/common/DataSourceConfig.java b/java/src/main/java/com/portfolio/common/DataSourceConfig.java new file mode 100644 index 00000000..0e09f27f --- /dev/null +++ b/java/src/main/java/com/portfolio/common/DataSourceConfig.java @@ -0,0 +1,31 @@ +package com.portfolio.common; + +import org.springframework.context.annotation.Configuration; + +/** + * Migration of {@code src/programs/common/DB2CONN.cbl} (DB2 Connection Manager). + * + *

DB2CONN implemented CONN/DISC/STAT functions with manual retry + * (WS-MAX-RETRIES = 3) around {@code EXEC SQL CONNECT}. In Java this entire + * responsibility moves to the Spring Boot datasource layer: + * + *

    + *
  • CONN (1000-CONNECT, retry loop) → HikariCP connection pool with + * acquisition retry/timeout, configured via {@code spring.datasource.*} + * properties (H2 for tests/samples, DB2 JDBC URL in production).
  • + *
  • DISC (2000-DISCONNECT, COMMIT + CONNECT RESET) → connections returned + * to the pool after each transaction commits.
  • + *
  • STAT (3000-CHECK-STATUS, SELECT CURRENT SERVER) → pool validation / + * {@code connection-test-query}.
  • + *
  • Connection-error return codes (LS-SQLCODE, RC 12) → runtime + * {@code DataAccessException}s from Spring.
  • + *
+ * + *

No explicit beans are needed: Spring Boot auto-configures the pooled + * {@code DataSource}, JPA {@code EntityManagerFactory}, and + * {@code PlatformTransactionManager}. This class exists as the documented + * anchor point for datasource customizations in future slices. + */ +@Configuration +public class DataSourceConfig { +} diff --git a/java/src/main/java/com/portfolio/common/ErrorHandlingService.java b/java/src/main/java/com/portfolio/common/ErrorHandlingService.java new file mode 100644 index 00000000..209d4b85 --- /dev/null +++ b/java/src/main/java/com/portfolio/common/ErrorHandlingService.java @@ -0,0 +1,79 @@ +package com.portfolio.common; + +import com.portfolio.domain.ErrorLog; +import com.portfolio.repository.ErrorLogRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * Migration of {@code src/programs/common/ERRPROC.cbl} (Standard Error + * Processing Subroutine). + * + *

ERRPROC accepted a program ID, category, code, severity, text, and details + * (LS-ERROR-REQUEST), appended a formatted record to the ERRLOG file, displayed + * it, and returned the severity. Here the sequential ERRLOG file becomes the + * ERRLOG table ({@code src/database/db2/ERRLOG.sql}), DISPLAY becomes SLF4J + * logging, and the returned severity feeds job-level error counting. + * + *

Errors are logged in a new transaction (REQUIRES_NEW) so the log record + * survives a rollback of the failing unit of work — matching the COBOL + * behavior where ERRLOG was a separate file untouched by DB2 ROLLBACK. + */ +@Service +public class ErrorHandlingService { + + private static final Logger log = LoggerFactory.getLogger(ErrorHandlingService.class); + + private final ErrorLogRepository errorLogRepository; + + public ErrorHandlingService(ErrorLogRepository errorLogRepository) { + this.errorLogRepository = errorLogRepository; + } + + /** + * Logs an error (ERRPROC 2000-PROCESS-ERROR) and returns the severity, + * mirroring {@code MOVE LS-SEVERITY TO LS-RETURN-CODE}. + * + * @param programId LS-PROGRAM-ID PIC X(8) + * @param errorType ERR-CATEGORY mapped to ERRLOG.ERROR_TYPE — S=System, + * D=Database, V=Validation, P=Processing + * @param severity LS-SEVERITY (ERRLOG.ERROR_SEVERITY 1-4) + * @param errorCode LS-ERROR-CODE + * @param message LS-ERROR-TEXT PIC X(80) + * @param details LS-ERROR-DETAILS PIC X(256), may be null + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public int logError(String programId, String errorType, int severity, + String errorCode, String message, String details) { + LocalDateTime now = LocalDateTime.now(); + + ErrorLog entry = new ErrorLog(); + entry.setKey(new ErrorLog.Key(now, programId)); + entry.setErrorType(errorType); + entry.setErrorSeverity(severity); + entry.setErrorCode(errorCode); + entry.setErrorMessage(truncate(message, 200)); + entry.setProcessDate(now.toLocalDate()); + entry.setProcessTime(now.toLocalTime()); + entry.setUserId(System.getProperty("user.name", "BATCH")); + entry.setAdditionalInfo(truncate(details, 500)); + errorLogRepository.save(entry); + + log.error("ERROR DETECTED program={} type={} code={} severity={} message={} details={}", + programId, errorType, errorCode, severity, message, details); + + return severity; + } + + private static String truncate(String value, int max) { + if (value == null) { + return null; + } + return value.length() <= max ? value : value.substring(0, max); + } +} diff --git a/java/src/main/java/com/portfolio/common/FileProcessingException.java b/java/src/main/java/com/portfolio/common/FileProcessingException.java new file mode 100644 index 00000000..cdaad16c --- /dev/null +++ b/java/src/main/java/com/portfolio/common/FileProcessingException.java @@ -0,0 +1,32 @@ +package com.portfolio.common; + +/** + * Replaces COBOL two-character FILE STATUS checks (e.g. HISTLD00's + * {@code IF WS-TH-STATUS NOT = '00' ... PERFORM 9000-ERROR-ROUTINE}). + * + *

Convention: any non-'00' FILE STATUS branch in COBOL becomes a thrown + * {@code FileProcessingException} in Java; the original two-character status + * (when meaningful) is preserved in {@link #getFileStatus()}. + */ +public class FileProcessingException extends RuntimeException { + + /** Original COBOL FILE STATUS value (e.g. "23" = record not found), or null. */ + private final String fileStatus; + + public FileProcessingException(String message) { + this(message, null, null); + } + + public FileProcessingException(String message, String fileStatus) { + this(message, fileStatus, null); + } + + public FileProcessingException(String message, String fileStatus, Throwable cause) { + super(message, cause); + this.fileStatus = fileStatus; + } + + public String getFileStatus() { + return fileStatus; + } +} diff --git a/java/src/main/java/com/portfolio/common/SqlProcessingException.java b/java/src/main/java/com/portfolio/common/SqlProcessingException.java new file mode 100644 index 00000000..308b994a --- /dev/null +++ b/java/src/main/java/com/portfolio/common/SqlProcessingException.java @@ -0,0 +1,32 @@ +package com.portfolio.common; + +/** + * Replaces COBOL SQLCA return-code checks ({@code IF SQLCODE = 0 ... ELSE ...}). + * + *

Convention: any non-zero SQLCODE branch in COBOL becomes a thrown + * {@code SqlProcessingException}. The original SQLCODE (when known) is kept in + * {@link #getSqlCode()} so callers can preserve code-specific behavior — e.g. + * HISTLD00 ignores duplicate inserts (SQLCODE -803), which in Java becomes an + * existence check / duplicate-key handling rather than an error. + */ +public class SqlProcessingException extends RuntimeException { + + /** SQLCODE for a duplicate key insert, ignored by HISTLD00 (2200-LOAD-TO-DB2). */ + public static final int SQLCODE_DUPLICATE = -803; + + private final int sqlCode; + + public SqlProcessingException(String message, int sqlCode) { + super(message); + this.sqlCode = sqlCode; + } + + public SqlProcessingException(String message, int sqlCode, Throwable cause) { + super(message, cause); + this.sqlCode = sqlCode; + } + + public int getSqlCode() { + return sqlCode; + } +} diff --git a/java/src/main/java/com/portfolio/common/TransactionHelper.java b/java/src/main/java/com/portfolio/common/TransactionHelper.java new file mode 100644 index 00000000..b27fe07a --- /dev/null +++ b/java/src/main/java/com/portfolio/common/TransactionHelper.java @@ -0,0 +1,76 @@ +package com.portfolio.common; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Migration of {@code src/programs/common/DB2CMT.cbl} (DB2 Commit Controller). + * + *

Mapping of DB2CMT functions: + *

    + *
  • INIT (1000-INITIALIZE) → {@link #resetStatistics()}
  • + *
  • CMIT with LS-COMMIT-FREQ (2000/2100) → {@link #commitIfDue(long, int, boolean)} + * / {@link #executeInTransaction(TransactionCallback)}; in Spring Batch, + * periodic commits are the chunk boundary instead
  • + *
  • RBAK (3000-ROLLBACK) → {@link TransactionStatus#setRollbackOnly()} inside a + * callback, or a thrown runtime exception that rolls the transaction back
  • + *
  • SAVE/REST (4000/5000 savepoints) → nested transactions + * (PROPAGATION_NESTED) when needed; not required by the HISTLD00 slice
  • + *
  • STAT (6000-STATISTICS) → {@link #getCommitCount()} / {@link #getRollbackCount()}
  • + *
  • Non-zero SQLCODE + RC 8 branches → {@link SqlProcessingException} / + * Spring {@code TransactionException}s
  • + *
+ */ +@Component +public class TransactionHelper { + + private final TransactionTemplate transactionTemplate; + private final AtomicLong commitCount = new AtomicLong(); + private final AtomicLong rollbackCount = new AtomicLong(); + + public TransactionHelper(PlatformTransactionManager transactionManager) { + this.transactionTemplate = new TransactionTemplate(transactionManager); + } + + /** Runs the callback in a transaction; commit on success, rollback on exception. */ + public T executeInTransaction(TransactionCallback callback) { + try { + T result = transactionTemplate.execute(callback); + commitCount.incrementAndGet(); + return result; + } catch (RuntimeException e) { + rollbackCount.incrementAndGet(); + throw e; + } + } + + /** + * Equivalent of DB2CMT 2000-COMMIT: commit only when the number of records + * processed reaches the commit frequency, or when forced. + * + * @return true if a commit boundary is due (caller runs its unit of work + * via {@link #executeInTransaction(TransactionCallback)}) + */ + public boolean commitIfDue(long recordsProcessed, int commitFrequency, boolean force) { + return force || recordsProcessed >= commitFrequency; + } + + /** DB2CMT INIT function. */ + public void resetStatistics() { + commitCount.set(0); + rollbackCount.set(0); + } + + public long getCommitCount() { + return commitCount.get(); + } + + public long getRollbackCount() { + return rollbackCount.get(); + } +} diff --git a/java/src/main/java/com/portfolio/domain/BatchControl.java b/java/src/main/java/com/portfolio/domain/BatchControl.java new file mode 100644 index 00000000..5d40b73e --- /dev/null +++ b/java/src/main/java/com/portfolio/domain/BatchControl.java @@ -0,0 +1,148 @@ +package com.portfolio.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Relational model of the VSAM KSDS batch control file (BCHCTL), the file + * HISTLD00 opens I-O for checkpointing. Record layout: + * {@code src/copybook/batch/BCHCTL.cpy} (01 BATCH-CONTROL-RECORD). + * + *

VSAM→table convention: primary key = COBOL RECORD KEY BCT-KEY + * (BCT-JOB-NAME, BCT-PROCESS-DATE, BCT-SEQUENCE-NO). The OCCURS 10 TIMES + * prerequisite table is not needed by the HISTLD00 slice and is deferred. + */ +@Entity +@Table(name = "VSAM_BCHCTL") +public class BatchControl { + + @EmbeddedId + private Key key; + + /** BCT-STATUS PIC X(1) — R/A/W/D/E. */ + @Column(name = "STATUS", length = 1, nullable = false) + private String status; + + /** BCT-STEP-NAME PIC X(8). */ + @Column(name = "STEP_NAME", length = 8) + private String stepName; + + /** BCT-PROGRAM-NAME PIC X(8). */ + @Column(name = "PROGRAM_NAME", length = 8) + private String programName; + + /** BCT-START-TIME PIC X(8). */ + @Column(name = "START_TIME", length = 8) + private String startTime; + + /** BCT-END-TIME PIC X(8). */ + @Column(name = "END_TIME", length = 8) + private String endTime; + + /** Records read counter maintained by HISTLD00 checkpointing (2310-UPDATE-CHECKPOINT). */ + @Column(name = "RECORDS_READ", nullable = false) + private long recordsRead; + + /** Records written counter maintained by HISTLD00 checkpointing. */ + @Column(name = "RECORDS_WRITTEN", nullable = false) + private long recordsWritten; + + /** BCT-RETURN-CODE PIC S9(4) COMP. */ + @Column(name = "RETURN_CODE", nullable = false) + private int returnCode; + + /** BCT-ERROR-DESC PIC X(80). */ + @Column(name = "ERROR_DESC", length = 80) + private String errorDesc; + + /** BCT-RESTART-COUNT PIC 9(2) COMP. */ + @Column(name = "RESTART_COUNT", nullable = false) + private int restartCount; + + /** BCT-ATTEMPT-TS PIC X(26). */ + @Column(name = "ATTEMPT_TS", length = 26) + private String attemptTimestamp; + + /** BCT-COMPLETE-TS PIC X(26). */ + @Column(name = "COMPLETE_TS", length = 26) + private String completeTimestamp; + + /** Composite primary key = BCT-KEY (job name + process date + sequence no). */ + @Embeddable + public static class Key implements Serializable { + + /** BCT-JOB-NAME PIC X(8). */ + @Column(name = "JOB_NAME", length = 8, nullable = false) + private String jobName; + + /** BCT-PROCESS-DATE PIC X(8) — YYYYMMDD. */ + @Column(name = "PROCESS_DATE", length = 8, nullable = false) + private String processDate; + + /** BCT-SEQUENCE-NO PIC 9(4). */ + @Column(name = "SEQUENCE_NO", nullable = false) + private int sequenceNo; + + public Key() {} + + public Key(String jobName, String processDate, int sequenceNo) { + this.jobName = jobName; + this.processDate = processDate; + this.sequenceNo = sequenceNo; + } + + public String getJobName() { return jobName; } + public void setJobName(String jobName) { this.jobName = jobName; } + public String getProcessDate() { return processDate; } + public void setProcessDate(String processDate) { this.processDate = processDate; } + public int getSequenceNo() { return sequenceNo; } + public void setSequenceNo(int sequenceNo) { this.sequenceNo = sequenceNo; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Key key)) return false; + return sequenceNo == key.sequenceNo + && Objects.equals(jobName, key.jobName) + && Objects.equals(processDate, key.processDate); + } + + @Override + public int hashCode() { + return Objects.hash(jobName, processDate, sequenceNo); + } + } + + public Key getKey() { return key; } + public void setKey(Key key) { this.key = key; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getStepName() { return stepName; } + public void setStepName(String stepName) { this.stepName = stepName; } + public String getProgramName() { return programName; } + public void setProgramName(String programName) { this.programName = programName; } + public String getStartTime() { return startTime; } + public void setStartTime(String startTime) { this.startTime = startTime; } + public String getEndTime() { return endTime; } + public void setEndTime(String endTime) { this.endTime = endTime; } + public long getRecordsRead() { return recordsRead; } + public void setRecordsRead(long recordsRead) { this.recordsRead = recordsRead; } + public long getRecordsWritten() { return recordsWritten; } + public void setRecordsWritten(long recordsWritten) { this.recordsWritten = recordsWritten; } + public int getReturnCode() { return returnCode; } + public void setReturnCode(int returnCode) { this.returnCode = returnCode; } + public String getErrorDesc() { return errorDesc; } + public void setErrorDesc(String errorDesc) { this.errorDesc = errorDesc; } + public int getRestartCount() { return restartCount; } + public void setRestartCount(int restartCount) { this.restartCount = restartCount; } + public String getAttemptTimestamp() { return attemptTimestamp; } + public void setAttemptTimestamp(String attemptTimestamp) { this.attemptTimestamp = attemptTimestamp; } + public String getCompleteTimestamp() { return completeTimestamp; } + public void setCompleteTimestamp(String completeTimestamp) { this.completeTimestamp = completeTimestamp; } +} diff --git a/java/src/main/java/com/portfolio/domain/ErrorLog.java b/java/src/main/java/com/portfolio/domain/ErrorLog.java new file mode 100644 index 00000000..1eac955d --- /dev/null +++ b/java/src/main/java/com/portfolio/domain/ErrorLog.java @@ -0,0 +1,117 @@ +package com.portfolio.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Objects; + +/** + * JPA entity for the DB2 ERRLOG table ({@code src/database/db2/ERRLOG.sql}); + * the corresponding COBOL host structure is 01 ERRLOG-RECORD in + * {@code src/copybook/db2/DBTBLS.cpy}. + * + *

Primary key: (ERROR_TIMESTAMP, PROGRAM_ID). + */ +@Entity +@Table(name = "ERRLOG") +public class ErrorLog { + + @EmbeddedId + private Key key; + + /** EL-ERROR-TYPE PIC X(1) / ERROR_TYPE CHAR(1) — S=System, A=Application, D=Data. */ + @Column(name = "ERROR_TYPE", length = 1, nullable = false) + private String errorType; + + /** EL-ERROR-SEVERITY PIC S9(4) COMP / ERROR_SEVERITY INTEGER — 1=Info, 2=Warn, 3=Error, 4=Severe. */ + @Column(name = "ERROR_SEVERITY", nullable = false) + private int errorSeverity; + + /** EL-ERROR-CODE PIC X(8) / ERROR_CODE CHAR(8). */ + @Column(name = "ERROR_CODE", length = 8, nullable = false) + private String errorCode; + + /** EL-ERROR-MESSAGE PIC X(200) / ERROR_MESSAGE VARCHAR(200). */ + @Column(name = "ERROR_MESSAGE", length = 200, nullable = false) + private String errorMessage; + + /** EL-PROCESS-DATE PIC X(10) / PROCESS_DATE DATE. */ + @Column(name = "PROCESS_DATE", nullable = false) + private LocalDate processDate; + + /** EL-PROCESS-TIME PIC X(8) / PROCESS_TIME TIME. */ + @Column(name = "PROCESS_TIME", nullable = false) + private LocalTime processTime; + + /** EL-USER-ID PIC X(8) / USER_ID CHAR(8). */ + @Column(name = "USER_ID", length = 8, nullable = false) + private String userId; + + /** EL-ADDITIONAL-INFO PIC X(500) / ADDITIONAL_INFO VARCHAR(500). */ + @Column(name = "ADDITIONAL_INFO", length = 500) + private String additionalInfo; + + /** Composite primary key (ERROR_TIMESTAMP, PROGRAM_ID). */ + @Embeddable + public static class Key implements Serializable { + + /** EL-ERROR-TIMESTAMP PIC X(26) / ERROR_TIMESTAMP TIMESTAMP. */ + @Column(name = "ERROR_TIMESTAMP", nullable = false) + private LocalDateTime errorTimestamp; + + /** EL-PROGRAM-ID PIC X(8) / PROGRAM_ID CHAR(8). */ + @Column(name = "PROGRAM_ID", length = 8, nullable = false) + private String programId; + + public Key() {} + + public Key(LocalDateTime errorTimestamp, String programId) { + this.errorTimestamp = errorTimestamp; + this.programId = programId; + } + + public LocalDateTime getErrorTimestamp() { return errorTimestamp; } + public void setErrorTimestamp(LocalDateTime errorTimestamp) { this.errorTimestamp = errorTimestamp; } + public String getProgramId() { return programId; } + public void setProgramId(String programId) { this.programId = programId; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Key key)) return false; + return Objects.equals(errorTimestamp, key.errorTimestamp) + && Objects.equals(programId, key.programId); + } + + @Override + public int hashCode() { + return Objects.hash(errorTimestamp, programId); + } + } + + public Key getKey() { return key; } + public void setKey(Key key) { this.key = key; } + public String getErrorType() { return errorType; } + public void setErrorType(String errorType) { this.errorType = errorType; } + public int getErrorSeverity() { return errorSeverity; } + public void setErrorSeverity(int errorSeverity) { this.errorSeverity = errorSeverity; } + public String getErrorCode() { return errorCode; } + public void setErrorCode(String errorCode) { this.errorCode = errorCode; } + public String getErrorMessage() { return errorMessage; } + public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + public LocalDate getProcessDate() { return processDate; } + public void setProcessDate(LocalDate processDate) { this.processDate = processDate; } + public LocalTime getProcessTime() { return processTime; } + public void setProcessTime(LocalTime processTime) { this.processTime = processTime; } + public String getUserId() { return userId; } + public void setUserId(String userId) { this.userId = userId; } + public String getAdditionalInfo() { return additionalInfo; } + public void setAdditionalInfo(String additionalInfo) { this.additionalInfo = additionalInfo; } +} diff --git a/java/src/main/java/com/portfolio/domain/InvestmentPosition.java b/java/src/main/java/com/portfolio/domain/InvestmentPosition.java new file mode 100644 index 00000000..f627010d --- /dev/null +++ b/java/src/main/java/com/portfolio/domain/InvestmentPosition.java @@ -0,0 +1,113 @@ +package com.portfolio.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * JPA entity for the DB2 INVESTMENT_POSITIONS table + * ({@code src/database/db2/db2-definitions.sql}); the corresponding VSAM/COBOL + * record is 01 POSITION-RECORD in {@code src/copybook/common/POSREC.cpy}. + * + *

Primary key: (PORTFOLIO_ID, INVESTMENT_ID, POSITION_DATE). + */ +@Entity +@Table(name = "INVESTMENT_POSITIONS") +public class InvestmentPosition { + + @EmbeddedId + private Key key; + + /** QUANTITY DECIMAL(18,4) / POS-QUANTITY PIC S9(11)V9(4) COMP-3. */ + @Column(name = "QUANTITY", precision = 18, scale = 4, nullable = false) + private BigDecimal quantity; + + /** COST_BASIS DECIMAL(18,2) / POS-COST-BASIS PIC S9(13)V9(2) COMP-3. */ + @Column(name = "COST_BASIS", precision = 18, scale = 2, nullable = false) + private BigDecimal costBasis; + + /** MARKET_VALUE DECIMAL(18,2) / POS-MARKET-VALUE PIC S9(13)V9(2) COMP-3. */ + @Column(name = "MARKET_VALUE", precision = 18, scale = 2, nullable = false) + private BigDecimal marketValue; + + /** CURRENCY_CODE CHAR(3) / POS-CURRENCY PIC X(03). */ + @Column(name = "CURRENCY_CODE", length = 3, nullable = false) + private String currencyCode; + + /** LAST_MAINT_DATE TIMESTAMP / POS-LAST-MAINT-DATE PIC X(26). */ + @Column(name = "LAST_MAINT_DATE", nullable = false) + private LocalDateTime lastMaintDate; + + /** LAST_MAINT_USER VARCHAR(8) / POS-LAST-MAINT-USER PIC X(08). */ + @Column(name = "LAST_MAINT_USER", length = 8, nullable = false) + private String lastMaintUser; + + /** Composite primary key (PORTFOLIO_ID, INVESTMENT_ID, POSITION_DATE). */ + @Embeddable + public static class Key implements Serializable { + + /** PORTFOLIO_ID CHAR(8) / POS-PORTFOLIO-ID PIC X(08). */ + @Column(name = "PORTFOLIO_ID", length = 8, nullable = false) + private String portfolioId; + + /** INVESTMENT_ID CHAR(10) / POS-INVESTMENT-ID PIC X(10). */ + @Column(name = "INVESTMENT_ID", length = 10, nullable = false) + private String investmentId; + + /** POSITION_DATE DATE / POS-DATE PIC X(08). */ + @Column(name = "POSITION_DATE", nullable = false) + private LocalDate positionDate; + + public Key() {} + + public Key(String portfolioId, String investmentId, LocalDate positionDate) { + this.portfolioId = portfolioId; + this.investmentId = investmentId; + this.positionDate = positionDate; + } + + public String getPortfolioId() { return portfolioId; } + public void setPortfolioId(String portfolioId) { this.portfolioId = portfolioId; } + public String getInvestmentId() { return investmentId; } + public void setInvestmentId(String investmentId) { this.investmentId = investmentId; } + public LocalDate getPositionDate() { return positionDate; } + public void setPositionDate(LocalDate positionDate) { this.positionDate = positionDate; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Key key)) return false; + return Objects.equals(portfolioId, key.portfolioId) + && Objects.equals(investmentId, key.investmentId) + && Objects.equals(positionDate, key.positionDate); + } + + @Override + public int hashCode() { + return Objects.hash(portfolioId, investmentId, positionDate); + } + } + + public Key getKey() { return key; } + public void setKey(Key key) { this.key = key; } + public BigDecimal getQuantity() { return quantity; } + public void setQuantity(BigDecimal quantity) { this.quantity = quantity; } + public BigDecimal getCostBasis() { return costBasis; } + public void setCostBasis(BigDecimal costBasis) { this.costBasis = costBasis; } + public BigDecimal getMarketValue() { return marketValue; } + public void setMarketValue(BigDecimal marketValue) { this.marketValue = marketValue; } + public String getCurrencyCode() { return currencyCode; } + public void setCurrencyCode(String currencyCode) { this.currencyCode = currencyCode; } + public LocalDateTime getLastMaintDate() { return lastMaintDate; } + public void setLastMaintDate(LocalDateTime lastMaintDate) { this.lastMaintDate = lastMaintDate; } + public String getLastMaintUser() { return lastMaintUser; } + public void setLastMaintUser(String lastMaintUser) { this.lastMaintUser = lastMaintUser; } +} diff --git a/java/src/main/java/com/portfolio/domain/PortfolioMaster.java b/java/src/main/java/com/portfolio/domain/PortfolioMaster.java new file mode 100644 index 00000000..b4b62110 --- /dev/null +++ b/java/src/main/java/com/portfolio/domain/PortfolioMaster.java @@ -0,0 +1,92 @@ +package com.portfolio.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * JPA entity for the DB2 PORTFOLIO_MASTER table + * ({@code src/database/db2/db2-definitions.sql}). + */ +@Entity +@Table(name = "PORTFOLIO_MASTER") +public class PortfolioMaster { + + /** PORTFOLIO_ID CHAR(8). */ + @Id + @Column(name = "PORTFOLIO_ID", length = 8, nullable = false) + private String portfolioId; + + /** ACCOUNT_TYPE CHAR(2). */ + @Column(name = "ACCOUNT_TYPE", length = 2, nullable = false) + private String accountType; + + /** BRANCH_ID CHAR(2). */ + @Column(name = "BRANCH_ID", length = 2, nullable = false) + private String branchId; + + /** CLIENT_ID CHAR(10). */ + @Column(name = "CLIENT_ID", length = 10, nullable = false) + private String clientId; + + /** PORTFOLIO_NAME VARCHAR(50). */ + @Column(name = "PORTFOLIO_NAME", length = 50, nullable = false) + private String portfolioName; + + /** CURRENCY_CODE CHAR(3). */ + @Column(name = "CURRENCY_CODE", length = 3, nullable = false) + private String currencyCode; + + /** RISK_LEVEL CHAR(1). */ + @Column(name = "RISK_LEVEL", length = 1, nullable = false) + private String riskLevel; + + /** STATUS CHAR(1) — A=Active, C=Closed, S=Suspended. */ + @Column(name = "STATUS", length = 1, nullable = false) + private String status; + + /** OPEN_DATE DATE. */ + @Column(name = "OPEN_DATE", nullable = false) + private LocalDate openDate; + + /** CLOSE_DATE DATE (nullable). */ + @Column(name = "CLOSE_DATE") + private LocalDate closeDate; + + /** LAST_MAINT_DATE TIMESTAMP. */ + @Column(name = "LAST_MAINT_DATE", nullable = false) + private LocalDateTime lastMaintDate; + + /** LAST_MAINT_USER VARCHAR(8). */ + @Column(name = "LAST_MAINT_USER", length = 8, nullable = false) + private String lastMaintUser; + + public String getPortfolioId() { return portfolioId; } + public void setPortfolioId(String portfolioId) { this.portfolioId = portfolioId; } + public String getAccountType() { return accountType; } + public void setAccountType(String accountType) { this.accountType = accountType; } + public String getBranchId() { return branchId; } + public void setBranchId(String branchId) { this.branchId = branchId; } + public String getClientId() { return clientId; } + public void setClientId(String clientId) { this.clientId = clientId; } + public String getPortfolioName() { return portfolioName; } + public void setPortfolioName(String portfolioName) { this.portfolioName = portfolioName; } + public String getCurrencyCode() { return currencyCode; } + public void setCurrencyCode(String currencyCode) { this.currencyCode = currencyCode; } + public String getRiskLevel() { return riskLevel; } + public void setRiskLevel(String riskLevel) { this.riskLevel = riskLevel; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public LocalDate getOpenDate() { return openDate; } + public void setOpenDate(LocalDate openDate) { this.openDate = openDate; } + public LocalDate getCloseDate() { return closeDate; } + public void setCloseDate(LocalDate closeDate) { this.closeDate = closeDate; } + public LocalDateTime getLastMaintDate() { return lastMaintDate; } + public void setLastMaintDate(LocalDateTime lastMaintDate) { this.lastMaintDate = lastMaintDate; } + public String getLastMaintUser() { return lastMaintUser; } + public void setLastMaintUser(String lastMaintUser) { this.lastMaintUser = lastMaintUser; } +} diff --git a/java/src/main/java/com/portfolio/domain/PositionHistory.java b/java/src/main/java/com/portfolio/domain/PositionHistory.java new file mode 100644 index 00000000..a929bc2e --- /dev/null +++ b/java/src/main/java/com/portfolio/domain/PositionHistory.java @@ -0,0 +1,171 @@ +package com.portfolio.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Objects; + +/** + * JPA entity for the DB2 POSHIST table + * ({@code src/database/db2/POSHIST.sql}); the corresponding COBOL host + * structure is 01 POSHIST-RECORD in {@code src/copybook/db2/DBTBLS.cpy}. + * + *

Primary key: (ACCOUNT_NO, PORTFOLIO_ID, TRANS_DATE, TRANS_TIME). + * Packed-decimal columns (COMP-3 / DECIMAL) map to {@link BigDecimal}. + */ +@Entity +@Table(name = "POSHIST") +public class PositionHistory { + + @EmbeddedId + private Key key; + + /** PH-TRANS-TYPE PIC X(2) / TRANS_TYPE CHAR(2) — BU/SL/TR/FE. */ + @Column(name = "TRANS_TYPE", length = 2, nullable = false) + private String transType; + + /** PH-SECURITY-ID PIC X(12) / SECURITY_ID CHAR(12). */ + @Column(name = "SECURITY_ID", length = 12, nullable = false) + private String securityId; + + /** PH-QUANTITY PIC S9(12)V9(3) COMP-3 / QUANTITY DECIMAL(15,3). */ + @Column(name = "QUANTITY", precision = 15, scale = 3, nullable = false) + private BigDecimal quantity; + + /** PH-PRICE PIC S9(12)V9(3) COMP-3 / PRICE DECIMAL(15,3). */ + @Column(name = "PRICE", precision = 15, scale = 3, nullable = false) + private BigDecimal price; + + /** PH-AMOUNT PIC S9(13)V9(2) COMP-3 / AMOUNT DECIMAL(15,2). */ + @Column(name = "AMOUNT", precision = 15, scale = 2, nullable = false) + private BigDecimal amount; + + /** PH-FEES PIC S9(13)V9(2) COMP-3 / FEES DECIMAL(15,2) DEFAULT 0. */ + @Column(name = "FEES", precision = 15, scale = 2, nullable = false) + private BigDecimal fees = BigDecimal.ZERO; + + /** PH-TOTAL-AMOUNT PIC S9(13)V9(2) COMP-3 / TOTAL_AMOUNT DECIMAL(15,2). */ + @Column(name = "TOTAL_AMOUNT", precision = 15, scale = 2, nullable = false) + private BigDecimal totalAmount; + + /** PH-COST-BASIS PIC S9(13)V9(2) COMP-3 / COST_BASIS DECIMAL(15,2). */ + @Column(name = "COST_BASIS", precision = 15, scale = 2, nullable = false) + private BigDecimal costBasis; + + /** PH-GAIN-LOSS PIC S9(13)V9(2) COMP-3 / GAIN_LOSS DECIMAL(15,2). */ + @Column(name = "GAIN_LOSS", precision = 15, scale = 2, nullable = false) + private BigDecimal gainLoss; + + /** PH-PROCESS-DATE PIC X(10) / PROCESS_DATE DATE. */ + @Column(name = "PROCESS_DATE", nullable = false) + private LocalDate processDate; + + /** PH-PROCESS-TIME PIC X(8) / PROCESS_TIME TIME. */ + @Column(name = "PROCESS_TIME", nullable = false) + private LocalTime processTime; + + /** PH-PROGRAM-ID PIC X(8) / PROGRAM_ID CHAR(8). */ + @Column(name = "PROGRAM_ID", length = 8, nullable = false) + private String programId; + + /** PH-USER-ID PIC X(8) / USER_ID CHAR(8). */ + @Column(name = "USER_ID", length = 8, nullable = false) + private String userId; + + /** PH-AUDIT-TIMESTAMP PIC X(26) / AUDIT_TIMESTAMP TIMESTAMP. */ + @Column(name = "AUDIT_TIMESTAMP", nullable = false) + private LocalDateTime auditTimestamp; + + /** Composite primary key (ACCOUNT_NO, PORTFOLIO_ID, TRANS_DATE, TRANS_TIME). */ + @Embeddable + public static class Key implements Serializable { + + /** PH-ACCOUNT-NO PIC X(8) / ACCOUNT_NO CHAR(8). */ + @Column(name = "ACCOUNT_NO", length = 8, nullable = false) + private String accountNo; + + /** PH-PORTFOLIO-ID PIC X(10) / PORTFOLIO_ID CHAR(10). */ + @Column(name = "PORTFOLIO_ID", length = 10, nullable = false) + private String portfolioId; + + /** PH-TRANS-DATE PIC X(10) / TRANS_DATE DATE. */ + @Column(name = "TRANS_DATE", nullable = false) + private LocalDate transDate; + + /** PH-TRANS-TIME PIC X(8) / TRANS_TIME TIME. */ + @Column(name = "TRANS_TIME", nullable = false) + private LocalTime transTime; + + public Key() {} + + public Key(String accountNo, String portfolioId, LocalDate transDate, LocalTime transTime) { + this.accountNo = accountNo; + this.portfolioId = portfolioId; + this.transDate = transDate; + this.transTime = transTime; + } + + public String getAccountNo() { return accountNo; } + public void setAccountNo(String accountNo) { this.accountNo = accountNo; } + public String getPortfolioId() { return portfolioId; } + public void setPortfolioId(String portfolioId) { this.portfolioId = portfolioId; } + public LocalDate getTransDate() { return transDate; } + public void setTransDate(LocalDate transDate) { this.transDate = transDate; } + public LocalTime getTransTime() { return transTime; } + public void setTransTime(LocalTime transTime) { this.transTime = transTime; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Key key)) return false; + return Objects.equals(accountNo, key.accountNo) + && Objects.equals(portfolioId, key.portfolioId) + && Objects.equals(transDate, key.transDate) + && Objects.equals(transTime, key.transTime); + } + + @Override + public int hashCode() { + return Objects.hash(accountNo, portfolioId, transDate, transTime); + } + } + + public Key getKey() { return key; } + public void setKey(Key key) { this.key = key; } + public String getTransType() { return transType; } + public void setTransType(String transType) { this.transType = transType; } + public String getSecurityId() { return securityId; } + public void setSecurityId(String securityId) { this.securityId = securityId; } + public BigDecimal getQuantity() { return quantity; } + public void setQuantity(BigDecimal quantity) { this.quantity = quantity; } + public BigDecimal getPrice() { return price; } + public void setPrice(BigDecimal price) { this.price = price; } + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + public BigDecimal getFees() { return fees; } + public void setFees(BigDecimal fees) { this.fees = fees; } + public BigDecimal getTotalAmount() { return totalAmount; } + public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; } + public BigDecimal getCostBasis() { return costBasis; } + public void setCostBasis(BigDecimal costBasis) { this.costBasis = costBasis; } + public BigDecimal getGainLoss() { return gainLoss; } + public void setGainLoss(BigDecimal gainLoss) { this.gainLoss = gainLoss; } + public LocalDate getProcessDate() { return processDate; } + public void setProcessDate(LocalDate processDate) { this.processDate = processDate; } + public LocalTime getProcessTime() { return processTime; } + public void setProcessTime(LocalTime processTime) { this.processTime = processTime; } + public String getProgramId() { return programId; } + public void setProgramId(String programId) { this.programId = programId; } + public String getUserId() { return userId; } + public void setUserId(String userId) { this.userId = userId; } + public LocalDateTime getAuditTimestamp() { return auditTimestamp; } + public void setAuditTimestamp(LocalDateTime auditTimestamp) { this.auditTimestamp = auditTimestamp; } +} diff --git a/java/src/main/java/com/portfolio/domain/ReturnCodeLog.java b/java/src/main/java/com/portfolio/domain/ReturnCodeLog.java new file mode 100644 index 00000000..8a2cbc0b --- /dev/null +++ b/java/src/main/java/com/portfolio/domain/ReturnCodeLog.java @@ -0,0 +1,88 @@ +package com.portfolio.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * JPA entity for the DB2 RTNCODES table ({@code src/database/db2/RTNCODES.sql}). + * Primary key: (TIMESTAMP, PROGRAM_ID). + */ +@Entity +@Table(name = "RTNCODES") +public class ReturnCodeLog { + + @EmbeddedId + private Key key; + + /** RETURN_CODE INTEGER. */ + @Column(name = "RETURN_CODE", nullable = false) + private int returnCode; + + /** HIGHEST_CODE INTEGER. */ + @Column(name = "HIGHEST_CODE", nullable = false) + private int highestCode; + + /** STATUS_CODE CHAR(1). */ + @Column(name = "STATUS_CODE", length = 1, nullable = false) + private String statusCode; + + /** MESSAGE_TEXT VARCHAR(80). */ + @Column(name = "MESSAGE_TEXT", length = 80) + private String messageText; + + /** Composite primary key (TIMESTAMP, PROGRAM_ID). */ + @Embeddable + public static class Key implements Serializable { + + /** TIMESTAMP column (named LOG_TIMESTAMP to avoid the SQL reserved word). */ + @Column(name = "LOG_TIMESTAMP", nullable = false) + private LocalDateTime timestamp; + + /** PROGRAM_ID CHAR(8). */ + @Column(name = "PROGRAM_ID", length = 8, nullable = false) + private String programId; + + public Key() {} + + public Key(LocalDateTime timestamp, String programId) { + this.timestamp = timestamp; + this.programId = programId; + } + + public LocalDateTime getTimestamp() { return timestamp; } + public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } + public String getProgramId() { return programId; } + public void setProgramId(String programId) { this.programId = programId; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Key key)) return false; + return Objects.equals(timestamp, key.timestamp) + && Objects.equals(programId, key.programId); + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, programId); + } + } + + public Key getKey() { return key; } + public void setKey(Key key) { this.key = key; } + public int getReturnCode() { return returnCode; } + public void setReturnCode(int returnCode) { this.returnCode = returnCode; } + public int getHighestCode() { return highestCode; } + public void setHighestCode(int highestCode) { this.highestCode = highestCode; } + public String getStatusCode() { return statusCode; } + public void setStatusCode(String statusCode) { this.statusCode = statusCode; } + public String getMessageText() { return messageText; } + public void setMessageText(String messageText) { this.messageText = messageText; } +} diff --git a/java/src/main/java/com/portfolio/domain/TransactionHistory.java b/java/src/main/java/com/portfolio/domain/TransactionHistory.java new file mode 100644 index 00000000..35d77e4a --- /dev/null +++ b/java/src/main/java/com/portfolio/domain/TransactionHistory.java @@ -0,0 +1,103 @@ +package com.portfolio.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * JPA entity for the DB2 TRANSACTION_HISTORY table + * ({@code src/database/db2/db2-definitions.sql}); the corresponding COBOL + * record is 01 TRANSACTION-RECORD in {@code src/copybook/common/TRNREC.cpy}. + * + *

TRANSACTION_ID format: YYYYMMDDHHMMSS + 6-digit sequence. + */ +@Entity +@Table(name = "TRANSACTION_HISTORY") +public class TransactionHistory { + + /** TRANSACTION_ID CHAR(20). */ + @Id + @Column(name = "TRANSACTION_ID", length = 20, nullable = false) + private String transactionId; + + /** PORTFOLIO_ID CHAR(8) / TRN-PORTFOLIO-ID PIC X(08). */ + @Column(name = "PORTFOLIO_ID", length = 8, nullable = false) + private String portfolioId; + + /** TRANSACTION_DATE DATE / TRN-DATE PIC X(08). */ + @Column(name = "TRANSACTION_DATE", nullable = false) + private LocalDate transactionDate; + + /** TRANSACTION_TIME TIME / TRN-TIME PIC X(06). */ + @Column(name = "TRANSACTION_TIME", nullable = false) + private LocalTime transactionTime; + + /** INVESTMENT_ID CHAR(10) / TRN-INVESTMENT-ID PIC X(10). */ + @Column(name = "INVESTMENT_ID", length = 10, nullable = false) + private String investmentId; + + /** TRANSACTION_TYPE CHAR(2) / TRN-TYPE PIC X(02) — BU/SL/TR/FE. */ + @Column(name = "TRANSACTION_TYPE", length = 2, nullable = false) + private String transactionType; + + /** QUANTITY DECIMAL(18,4) / TRN-QUANTITY PIC S9(11)V9(4) COMP-3. */ + @Column(name = "QUANTITY", precision = 18, scale = 4, nullable = false) + private BigDecimal quantity; + + /** PRICE DECIMAL(18,4) / TRN-PRICE PIC S9(11)V9(4) COMP-3. */ + @Column(name = "PRICE", precision = 18, scale = 4, nullable = false) + private BigDecimal price; + + /** AMOUNT DECIMAL(18,2) / TRN-AMOUNT PIC S9(13)V9(2) COMP-3. */ + @Column(name = "AMOUNT", precision = 18, scale = 2, nullable = false) + private BigDecimal amount; + + /** CURRENCY_CODE CHAR(3) / TRN-CURRENCY PIC X(03). */ + @Column(name = "CURRENCY_CODE", length = 3, nullable = false) + private String currencyCode; + + /** STATUS CHAR(1) / TRN-STATUS PIC X(01) — P/F/R (per DDL notes). */ + @Column(name = "STATUS", length = 1, nullable = false) + private String status; + + /** PROCESS_DATE TIMESTAMP / TRN-PROCESS-DATE PIC X(26). */ + @Column(name = "PROCESS_DATE", nullable = false) + private LocalDateTime processDate; + + /** PROCESS_USER VARCHAR(8) / TRN-PROCESS-USER PIC X(08). */ + @Column(name = "PROCESS_USER", length = 8, nullable = false) + private String processUser; + + public String getTransactionId() { return transactionId; } + public void setTransactionId(String transactionId) { this.transactionId = transactionId; } + public String getPortfolioId() { return portfolioId; } + public void setPortfolioId(String portfolioId) { this.portfolioId = portfolioId; } + public LocalDate getTransactionDate() { return transactionDate; } + public void setTransactionDate(LocalDate transactionDate) { this.transactionDate = transactionDate; } + public LocalTime getTransactionTime() { return transactionTime; } + public void setTransactionTime(LocalTime transactionTime) { this.transactionTime = transactionTime; } + public String getInvestmentId() { return investmentId; } + public void setInvestmentId(String investmentId) { this.investmentId = investmentId; } + public String getTransactionType() { return transactionType; } + public void setTransactionType(String transactionType) { this.transactionType = transactionType; } + public BigDecimal getQuantity() { return quantity; } + public void setQuantity(BigDecimal quantity) { this.quantity = quantity; } + public BigDecimal getPrice() { return price; } + public void setPrice(BigDecimal price) { this.price = price; } + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + public String getCurrencyCode() { return currencyCode; } + public void setCurrencyCode(String currencyCode) { this.currencyCode = currencyCode; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public LocalDateTime getProcessDate() { return processDate; } + public void setProcessDate(LocalDateTime processDate) { this.processDate = processDate; } + public String getProcessUser() { return processUser; } + public void setProcessUser(String processUser) { this.processUser = processUser; } +} diff --git a/java/src/main/java/com/portfolio/domain/TransactionHistoryFileRecord.java b/java/src/main/java/com/portfolio/domain/TransactionHistoryFileRecord.java new file mode 100644 index 00000000..791f5410 --- /dev/null +++ b/java/src/main/java/com/portfolio/domain/TransactionHistoryFileRecord.java @@ -0,0 +1,151 @@ +package com.portfolio.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Objects; + +/** + * Relational model of the VSAM KSDS TRANHIST file + * ({@code src/database/vsam/vsam-definitions.txt}), the input file read + * sequentially by HISTLD00 (SELECT TRANSACTION-HISTORY ... RECORD KEY IS TH-KEY). + * + *

VSAM→table convention: the KSDS is modeled as a table whose composite + * primary key is the COBOL RECORD KEY — here (TRANS_DATE, TRANS_TIME, + * PORTFOLIO_ID, SEQUENCE_NO) per the TRANHIST key structure. The data fields + * carry the TH-* fields that HISTLD00 maps into POSHIST columns. + */ +@Entity +@Table(name = "VSAM_TRANHIST") +public class TransactionHistoryFileRecord { + + @EmbeddedId + private Key key; + + /** TH-ACCOUNT-NO PIC X(8). */ + @Column(name = "ACCOUNT_NO", length = 8, nullable = false) + private String accountNo; + + /** TH-TRANS-TYPE PIC X(2) — BU/SL/TR/FE. */ + @Column(name = "TRANS_TYPE", length = 2, nullable = false) + private String transType; + + /** TH-SECURITY-ID PIC X(12). */ + @Column(name = "SECURITY_ID", length = 12, nullable = false) + private String securityId; + + /** TH-QUANTITY PIC S9(12)V9(3) COMP-3. */ + @Column(name = "QUANTITY", precision = 15, scale = 3, nullable = false) + private BigDecimal quantity; + + /** TH-PRICE PIC S9(12)V9(3) COMP-3. */ + @Column(name = "PRICE", precision = 15, scale = 3, nullable = false) + private BigDecimal price; + + /** TH-AMOUNT PIC S9(13)V9(2) COMP-3. */ + @Column(name = "AMOUNT", precision = 15, scale = 2, nullable = false) + private BigDecimal amount; + + /** TH-FEES PIC S9(13)V9(2) COMP-3. */ + @Column(name = "FEES", precision = 15, scale = 2, nullable = false) + private BigDecimal fees = BigDecimal.ZERO; + + /** TH-TOTAL-AMOUNT PIC S9(13)V9(2) COMP-3. */ + @Column(name = "TOTAL_AMOUNT", precision = 15, scale = 2, nullable = false) + private BigDecimal totalAmount; + + /** TH-COST-BASIS PIC S9(13)V9(2) COMP-3. */ + @Column(name = "COST_BASIS", precision = 15, scale = 2, nullable = false) + private BigDecimal costBasis; + + /** TH-GAIN-LOSS PIC S9(13)V9(2) COMP-3. */ + @Column(name = "GAIN_LOSS", precision = 15, scale = 2, nullable = false) + private BigDecimal gainLoss; + + /** + * Composite primary key = VSAM TRANHIST record key: + * Transaction Date (8) + Transaction Time (6) + Portfolio ID (8) + Sequence No (6). + */ + @Embeddable + public static class Key implements Serializable { + + /** Transaction date component of TH-KEY (YYYYMMDD in VSAM). */ + @Column(name = "TRANS_DATE", nullable = false) + private LocalDate transDate; + + /** Transaction time component of TH-KEY (HHMMSS in VSAM). */ + @Column(name = "TRANS_TIME", nullable = false) + private LocalTime transTime; + + /** Portfolio ID component of TH-KEY PIC X(8); widened to X(10) to match POSHIST. */ + @Column(name = "PORTFOLIO_ID", length = 10, nullable = false) + private String portfolioId; + + /** Sequence number component of TH-KEY PIC X(6). */ + @Column(name = "SEQUENCE_NO", length = 6, nullable = false) + private String sequenceNo; + + public Key() {} + + public Key(LocalDate transDate, LocalTime transTime, String portfolioId, String sequenceNo) { + this.transDate = transDate; + this.transTime = transTime; + this.portfolioId = portfolioId; + this.sequenceNo = sequenceNo; + } + + public LocalDate getTransDate() { return transDate; } + public void setTransDate(LocalDate transDate) { this.transDate = transDate; } + public LocalTime getTransTime() { return transTime; } + public void setTransTime(LocalTime transTime) { this.transTime = transTime; } + public String getPortfolioId() { return portfolioId; } + public void setPortfolioId(String portfolioId) { this.portfolioId = portfolioId; } + public String getSequenceNo() { return sequenceNo; } + public void setSequenceNo(String sequenceNo) { this.sequenceNo = sequenceNo; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Key key)) return false; + return Objects.equals(transDate, key.transDate) + && Objects.equals(transTime, key.transTime) + && Objects.equals(portfolioId, key.portfolioId) + && Objects.equals(sequenceNo, key.sequenceNo); + } + + @Override + public int hashCode() { + return Objects.hash(transDate, transTime, portfolioId, sequenceNo); + } + } + + public Key getKey() { return key; } + public void setKey(Key key) { this.key = key; } + public String getAccountNo() { return accountNo; } + public void setAccountNo(String accountNo) { this.accountNo = accountNo; } + public String getTransType() { return transType; } + public void setTransType(String transType) { this.transType = transType; } + public String getSecurityId() { return securityId; } + public void setSecurityId(String securityId) { this.securityId = securityId; } + public BigDecimal getQuantity() { return quantity; } + public void setQuantity(BigDecimal quantity) { this.quantity = quantity; } + public BigDecimal getPrice() { return price; } + public void setPrice(BigDecimal price) { this.price = price; } + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + public BigDecimal getFees() { return fees; } + public void setFees(BigDecimal fees) { this.fees = fees; } + public BigDecimal getTotalAmount() { return totalAmount; } + public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; } + public BigDecimal getCostBasis() { return costBasis; } + public void setCostBasis(BigDecimal costBasis) { this.costBasis = costBasis; } + public BigDecimal getGainLoss() { return gainLoss; } + public void setGainLoss(BigDecimal gainLoss) { this.gainLoss = gainLoss; } +} diff --git a/java/src/main/java/com/portfolio/model/copybook/AuditRecord.java b/java/src/main/java/com/portfolio/model/copybook/AuditRecord.java new file mode 100644 index 00000000..50d5bf43 --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/AuditRecord.java @@ -0,0 +1,73 @@ +package com.portfolio.model.copybook; + +/** + * Migrated from copybook {@code src/copybook/common/AUDITLOG.cpy} (01 AUDIT-RECORD). + */ +public class AuditRecord { + + /** AUD-TIMESTAMP PIC X(26). */ + private String timestamp; + + /** AUD-SYSTEM-ID PIC X(8). */ + private String systemId; + + /** AUD-USER-ID PIC X(8). */ + private String userId; + + /** AUD-PROGRAM PIC X(8). */ + private String program; + + /** AUD-TERMINAL PIC X(8). */ + private String terminal; + + /** AUD-TYPE PIC X(4) — TRAN/USER/SYST (level-88s). */ + private String type; + + /** AUD-ACTION PIC X(8) — CREATE/UPDATE/DELETE/INQUIRE/LOGIN/LOGOUT/STARTUP/SHUTDOWN (level-88s). */ + private String action; + + /** AUD-STATUS PIC X(4) — SUCC/FAIL/WARN (level-88s). */ + private String status; + + /** AUD-PORTFOLIO-ID PIC X(8). */ + private String portfolioId; + + /** AUD-ACCOUNT-NO PIC X(10). */ + private String accountNo; + + /** AUD-BEFORE-IMAGE PIC X(100). */ + private String beforeImage; + + /** AUD-AFTER-IMAGE PIC X(100). */ + private String afterImage; + + /** AUD-MESSAGE PIC X(100). */ + private String message; + + public String getTimestamp() { return timestamp; } + public void setTimestamp(String timestamp) { this.timestamp = timestamp; } + public String getSystemId() { return systemId; } + public void setSystemId(String systemId) { this.systemId = systemId; } + public String getUserId() { return userId; } + public void setUserId(String userId) { this.userId = userId; } + public String getProgram() { return program; } + public void setProgram(String program) { this.program = program; } + public String getTerminal() { return terminal; } + public void setTerminal(String terminal) { this.terminal = terminal; } + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public String getAction() { return action; } + public void setAction(String action) { this.action = action; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getPortfolioId() { return portfolioId; } + public void setPortfolioId(String portfolioId) { this.portfolioId = portfolioId; } + public String getAccountNo() { return accountNo; } + public void setAccountNo(String accountNo) { this.accountNo = accountNo; } + public String getBeforeImage() { return beforeImage; } + public void setBeforeImage(String beforeImage) { this.beforeImage = beforeImage; } + public String getAfterImage() { return afterImage; } + public void setAfterImage(String afterImage) { this.afterImage = afterImage; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/java/src/main/java/com/portfolio/model/copybook/BatchControlConstants.java b/java/src/main/java/com/portfolio/model/copybook/BatchControlConstants.java new file mode 100644 index 00000000..bc3fe4cf --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/BatchControlConstants.java @@ -0,0 +1,58 @@ +package com.portfolio.model.copybook; + +/** + * Migrated from copybook {@code src/copybook/batch/BCHCON.cpy} + * (01 BATCH-CONTROL-CONSTANTS). + */ +public final class BatchControlConstants { + + private BatchControlConstants() {} + + // Process status values (BCT-STAT-VALUES, PIC X(1)) + public static final String STAT_READY = "R"; + public static final String STAT_ACTIVE = "A"; + public static final String STAT_WAITING = "W"; + public static final String STAT_DONE = "D"; + public static final String STAT_ERROR = "E"; + + // Return code thresholds (BCT-RC-THRESHOLDS, PIC S9(4) COMP) + public static final int RC_SUCCESS = 0; + public static final int RC_WARNING = 4; + public static final int RC_ERROR = 8; + public static final int RC_SEVERE = 12; + public static final int RC_CRITICAL = 16; + + // Process control values (BCT-CTRL-VALUES, PIC 9(n) COMP) + public static final int MAX_PREREQ = 10; + public static final int MAX_RESTARTS = 3; + public static final int WAIT_INTERVAL_SECONDS = 300; + public static final int MAX_WAIT_TIME_SECONDS = 3600; + + // Process types (BCT-PROC-TYPES, PIC X(3)) + public static final String TYPE_INITIAL = "INI"; + public static final String TYPE_UPDATE = "UPD"; + public static final String TYPE_REPORT = "RPT"; + public static final String TYPE_CLEANUP = "CLN"; + + // Dependency types (BCT-DEP-TYPES, PIC X(1)) + public static final String DEP_REQUIRED = "R"; + public static final String DEP_OPTIONAL = "O"; + public static final String DEP_EXCLUSIVE = "X"; + + // Special process names (BCT-PROC-NAMES, PIC X(8)) + public static final String START_OF_DAY = "STARTDAY"; + public static final String END_OF_DAY = "ENDDAY"; + public static final String EMERGENCY = "EMERGENCY"; + + // Control file record types (BCT-REC-TYPES, PIC X(1)) + public static final String REC_CONTROL = "C"; + public static final String REC_PROCESS = "P"; + public static final String REC_DEPEND = "D"; + public static final String REC_HISTORY = "H"; + + // Standard messages (BCT-MESSAGES, PIC X(30)) + public static final String MSG_STARTING = "Process starting..."; + public static final String MSG_COMPLETE = "Process completed successfully"; + public static final String MSG_FAILED = "Process failed - check errors"; + public static final String MSG_WAITING = "Waiting for prerequisites"; +} diff --git a/java/src/main/java/com/portfolio/model/copybook/BatchControlRecord.java b/java/src/main/java/com/portfolio/model/copybook/BatchControlRecord.java new file mode 100644 index 00000000..d3b9ee36 --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/BatchControlRecord.java @@ -0,0 +1,103 @@ +package com.portfolio.model.copybook; + +import java.util.ArrayList; +import java.util.List; + +/** + * Migrated from copybook {@code src/copybook/batch/BCHCTL.cpy} (01 BATCH-CONTROL-RECORD). + * + *

Job-level control and process sequencing record stored on the VSAM batch + * control file. Key = BCT-KEY (job name + process date + sequence no). + */ +public class BatchControlRecord { + + /** BCT-JOB-NAME PIC X(8). */ + private String jobName; + + /** BCT-PROCESS-DATE PIC X(8) — YYYYMMDD. */ + private String processDate; + + /** BCT-SEQUENCE-NO PIC 9(4). */ + private int sequenceNo; + + /** BCT-STATUS PIC X(1) — R=Ready, A=Active, W=Waiting, D=Done, E=Error (level-88s). */ + private String status; + + /** BCT-STEP-NAME PIC X(8). */ + private String stepName; + + /** BCT-PROGRAM-NAME PIC X(8). */ + private String programName; + + /** BCT-START-TIME PIC X(8). */ + private String startTime; + + /** BCT-END-TIME PIC X(8). */ + private String endTime; + + /** BCT-PREREQ-JOBS OCCURS 10 TIMES (BCT-PREREQ-COUNT PIC 9(2) COMP tracks count). */ + private List prerequisiteJobs = new ArrayList<>(); + + /** BCT-RETURN-CODE PIC S9(4) COMP. */ + private int returnCode; + + /** BCT-ERROR-DESC PIC X(80). */ + private String errorDesc; + + /** BCT-RESTART-COUNT PIC 9(2) COMP. */ + private int restartCount; + + /** BCT-ATTEMPT-TS PIC X(26). */ + private String attemptTimestamp; + + /** BCT-COMPLETE-TS PIC X(26). */ + private String completeTimestamp; + + /** + * One entry of BCT-PREREQ-JOBS OCCURS 10 TIMES. + */ + public static class PrerequisiteJob { + /** BCT-PREREQ-NAME PIC X(8). */ + private String name; + /** BCT-PREREQ-SEQ PIC 9(4). */ + private int sequence; + /** BCT-PREREQ-RC PIC S9(4) COMP. */ + private int returnCode; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getSequence() { return sequence; } + public void setSequence(int sequence) { this.sequence = sequence; } + public int getReturnCode() { return returnCode; } + public void setReturnCode(int returnCode) { this.returnCode = returnCode; } + } + + public String getJobName() { return jobName; } + public void setJobName(String jobName) { this.jobName = jobName; } + public String getProcessDate() { return processDate; } + public void setProcessDate(String processDate) { this.processDate = processDate; } + public int getSequenceNo() { return sequenceNo; } + public void setSequenceNo(int sequenceNo) { this.sequenceNo = sequenceNo; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getStepName() { return stepName; } + public void setStepName(String stepName) { this.stepName = stepName; } + public String getProgramName() { return programName; } + public void setProgramName(String programName) { this.programName = programName; } + public String getStartTime() { return startTime; } + public void setStartTime(String startTime) { this.startTime = startTime; } + public String getEndTime() { return endTime; } + public void setEndTime(String endTime) { this.endTime = endTime; } + public List getPrerequisiteJobs() { return prerequisiteJobs; } + public void setPrerequisiteJobs(List prerequisiteJobs) { this.prerequisiteJobs = prerequisiteJobs; } + public int getReturnCode() { return returnCode; } + public void setReturnCode(int returnCode) { this.returnCode = returnCode; } + public String getErrorDesc() { return errorDesc; } + public void setErrorDesc(String errorDesc) { this.errorDesc = errorDesc; } + public int getRestartCount() { return restartCount; } + public void setRestartCount(int restartCount) { this.restartCount = restartCount; } + public String getAttemptTimestamp() { return attemptTimestamp; } + public void setAttemptTimestamp(String attemptTimestamp) { this.attemptTimestamp = attemptTimestamp; } + public String getCompleteTimestamp() { return completeTimestamp; } + public void setCompleteTimestamp(String completeTimestamp) { this.completeTimestamp = completeTimestamp; } +} diff --git a/java/src/main/java/com/portfolio/model/copybook/CheckpointControl.java b/java/src/main/java/com/portfolio/model/copybook/CheckpointControl.java new file mode 100644 index 00000000..8f6c8cfd --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/CheckpointControl.java @@ -0,0 +1,114 @@ +package com.portfolio.model.copybook; + +import java.util.ArrayList; +import java.util.List; + +/** + * Migrated from copybook {@code src/copybook/batch/CKPRST.cpy} + * (01 CHECKPOINT-CONTROL and 01 CHECKPOINT-RECORD). + * + *

Program-level checkpoint/restart control structure. In the Spring Batch + * migration, most of this responsibility is handled by the Spring Batch + * JobRepository/ExecutionContext, but the structure is preserved for programs + * that carry explicit checkpoint state. + */ +public class CheckpointControl { + + /** CK-PROGRAM-ID PIC X(8). */ + private String programId; + + /** CK-RUN-DATE PIC X(8) — YYYYMMDD. */ + private String runDate; + + /** CK-RUN-TIME PIC X(6) — HHMMSS. */ + private String runTime; + + /** CK-STATUS PIC X(1) — I=Initial, A=Active, C=Complete, F=Failed, R=Restarted (level-88s). */ + private String status; + + /** CK-RECORDS-READ PIC 9(9) COMP. */ + private long recordsRead; + + /** CK-RECORDS-PROC PIC 9(9) COMP. */ + private long recordsProcessed; + + /** CK-RECORDS-ERROR PIC 9(9) COMP. */ + private long recordsError; + + /** CK-RESTART-COUNT PIC 9(2) COMP. */ + private int restartCount; + + /** CK-LAST-KEY PIC X(50). */ + private String lastKey; + + /** CK-LAST-TIME PIC X(26). */ + private String lastTime; + + /** CK-PHASE PIC X(2) — 00=Init, 10=Read, 20=Proc, 30=Updt, 40=Term (level-88s). */ + private String phase; + + /** CK-FILE-STATUS OCCURS 5 TIMES. */ + private List fileStatuses = new ArrayList<>(); + + /** CK-COMMIT-FREQ PIC 9(5) COMP VALUE 1000. */ + private int commitFrequency = 1000; + + /** CK-MAX-ERRORS PIC 9(3) COMP VALUE 100. */ + private int maxErrors = 100; + + /** CK-MAX-RESTARTS PIC 9(2) COMP VALUE 3. */ + private int maxRestarts = 3; + + /** CK-RESTART-MODE PIC X(1) — N=Normal, R=Restart, C=Recover (level-88s). */ + private String restartMode = "N"; + + /** One entry of CK-FILE-STATUS OCCURS 5 TIMES. */ + public static class FileStatusEntry { + /** CK-FILE-NAME PIC X(8). */ + private String fileName; + /** CK-FILE-POS PIC X(50). */ + private String filePosition; + /** CK-FILE-STATUS PIC X(2). */ + private String fileStatus; + + public String getFileName() { return fileName; } + public void setFileName(String fileName) { this.fileName = fileName; } + public String getFilePosition() { return filePosition; } + public void setFilePosition(String filePosition) { this.filePosition = filePosition; } + public String getFileStatus() { return fileStatus; } + public void setFileStatus(String fileStatus) { this.fileStatus = fileStatus; } + } + + public String getProgramId() { return programId; } + public void setProgramId(String programId) { this.programId = programId; } + public String getRunDate() { return runDate; } + public void setRunDate(String runDate) { this.runDate = runDate; } + public String getRunTime() { return runTime; } + public void setRunTime(String runTime) { this.runTime = runTime; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public long getRecordsRead() { return recordsRead; } + public void setRecordsRead(long recordsRead) { this.recordsRead = recordsRead; } + public long getRecordsProcessed() { return recordsProcessed; } + public void setRecordsProcessed(long recordsProcessed) { this.recordsProcessed = recordsProcessed; } + public long getRecordsError() { return recordsError; } + public void setRecordsError(long recordsError) { this.recordsError = recordsError; } + public int getRestartCount() { return restartCount; } + public void setRestartCount(int restartCount) { this.restartCount = restartCount; } + public String getLastKey() { return lastKey; } + public void setLastKey(String lastKey) { this.lastKey = lastKey; } + public String getLastTime() { return lastTime; } + public void setLastTime(String lastTime) { this.lastTime = lastTime; } + public String getPhase() { return phase; } + public void setPhase(String phase) { this.phase = phase; } + public List getFileStatuses() { return fileStatuses; } + public void setFileStatuses(List fileStatuses) { this.fileStatuses = fileStatuses; } + public int getCommitFrequency() { return commitFrequency; } + public void setCommitFrequency(int commitFrequency) { this.commitFrequency = commitFrequency; } + public int getMaxErrors() { return maxErrors; } + public void setMaxErrors(int maxErrors) { this.maxErrors = maxErrors; } + public int getMaxRestarts() { return maxRestarts; } + public void setMaxRestarts(int maxRestarts) { this.maxRestarts = maxRestarts; } + public String getRestartMode() { return restartMode; } + public void setRestartMode(String restartMode) { this.restartMode = restartMode; } +} diff --git a/java/src/main/java/com/portfolio/model/copybook/CommonConstants.java b/java/src/main/java/com/portfolio/model/copybook/CommonConstants.java new file mode 100644 index 00000000..99da3363 --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/CommonConstants.java @@ -0,0 +1,38 @@ +package com.portfolio.model.copybook; + +/** + * Migrated from copybook {@code src/copybook/common/COMMON.cpy} + * (RETURN-CODES, STATUS-CODES, TRANSACTION-TYPES, CURRENCY-CODES). + */ +public final class CommonConstants { + + private CommonConstants() {} + + // Return codes (PIC S9(4)) + public static final int RC_SUCCESS = 0; + public static final int RC_WARNING = 4; + public static final int RC_ERROR = 8; + public static final int RC_SEVERE = 12; + public static final int RC_CRITICAL = 16; + + // Status codes (PIC X(01)) + public static final String STATUS_ACTIVE = "A"; + public static final String STATUS_CLOSED = "C"; + public static final String STATUS_PENDING = "P"; + public static final String STATUS_SUSPENDED = "S"; + public static final String STATUS_FAILED = "F"; + public static final String STATUS_REVERSED = "R"; + + // Transaction types (PIC X(02)) + public static final String TRN_TYPE_BUY = "BU"; + public static final String TRN_TYPE_SELL = "SL"; + public static final String TRN_TYPE_TRANSFER = "TR"; + public static final String TRN_TYPE_FEE = "FE"; + + // Currency codes (PIC X(03)) + public static final String CURR_USD = "USD"; + public static final String CURR_EUR = "EUR"; + public static final String CURR_GBP = "GBP"; + public static final String CURR_JPY = "JPY"; + public static final String CURR_CAD = "CAD"; +} diff --git a/java/src/main/java/com/portfolio/model/copybook/ErrorMessage.java b/java/src/main/java/com/portfolio/model/copybook/ErrorMessage.java new file mode 100644 index 00000000..34a21297 --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/ErrorMessage.java @@ -0,0 +1,70 @@ +package com.portfolio.model.copybook; + +/** + * Migrated from copybook {@code src/copybook/common/ERRHAND.cpy} (01 ERR-MESSAGE) + * plus its categories, return codes, and VSAM status constants. The online + * variant {@code src/copybook/online/ERRHND.cpy} shares the same intent and is + * covered by this class for batch purposes. + */ +public class ErrorMessage { + + /** ERR-DATE PIC X(10). */ + private String date; + + /** ERR-TIME PIC X(8). */ + private String time; + + /** ERR-PROGRAM PIC X(8). */ + private String program; + + /** ERR-CATEGORY PIC X(2) — VS=VSAM, VL=Validation, PR=Processing, SY=System. */ + private String category; + + /** ERR-CODE PIC X(4). */ + private String code; + + /** ERR-SEVERITY PIC S9(4) COMP — 0/4/8/12/16. */ + private int severity; + + /** ERR-TEXT PIC X(80). */ + private String text; + + /** ERR-DETAILS PIC X(256). */ + private String details; + + // Error categories (ERR-CATEGORIES, PIC X(2)) + public static final String CAT_VSAM = "VS"; + public static final String CAT_VALIDATION = "VL"; + public static final String CAT_PROCESSING = "PR"; + public static final String CAT_SYSTEM = "SY"; + + // Standard return codes (ERR-RETURN-CODES, PIC S9(4) COMP) + public static final int RC_SUCCESS = 0; + public static final int RC_WARNING = 4; + public static final int RC_ERROR = 8; + public static final int RC_SEVERE = 12; + public static final int RC_TERMINAL = 16; + + // VSAM file statuses (ERR-VSAM-STATUSES, PIC X(2)) — replaced by exceptions in Java + public static final String VSAM_SUCCESS = "00"; + public static final String VSAM_EOF = "10"; + public static final String VSAM_DUPKEY = "22"; + public static final String VSAM_NOTFND = "23"; + + public String getDate() { return date; } + public void setDate(String date) { this.date = date; } + public String getTime() { return time; } + public void setTime(String time) { this.time = time; } + public String getProgram() { return program; } + public void setProgram(String program) { this.program = program; } + public String getCategory() { return category; } + public void setCategory(String category) { this.category = category; } + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } + public int getSeverity() { return severity; } + public void setSeverity(int severity) { this.severity = severity; } + public String getText() { return text; } + public void setText(String text) { this.text = text; } + public String getDetails() { return details; } + public void setDetails(String details) { this.details = details; } +} diff --git a/java/src/main/java/com/portfolio/model/copybook/HistoryRecord.java b/java/src/main/java/com/portfolio/model/copybook/HistoryRecord.java new file mode 100644 index 00000000..d73c4e98 --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/HistoryRecord.java @@ -0,0 +1,73 @@ +package com.portfolio.model.copybook; + +/** + * Migrated from copybook {@code src/copybook/common/HISTREC.cpy} (01 HISTORY-RECORD). + * + *

VSAM history/audit-trail record. Key = HIST-KEY (portfolio id + date + time + seq no). + */ +public class HistoryRecord { + + /** HIST-PORTFOLIO-ID PIC X(08). */ + private String portfolioId; + + /** HIST-DATE PIC X(08) — YYYYMMDD. */ + private String date; + + /** HIST-TIME PIC X(06) — HHMMSS. */ + private String time; + + /** HIST-SEQ-NO PIC X(04). */ + private String seqNo; + + /** HIST-RECORD-TYPE PIC X(02) — PT=Portfolio, PS=Position, TR=Transaction (level-88s). */ + private String recordType; + + /** HIST-ACTION-CODE PIC X(01) — A=Add, C=Change, D=Delete (level-88s). */ + private String actionCode; + + /** HIST-BEFORE-IMAGE PIC X(400). */ + private String beforeImage; + + /** HIST-AFTER-IMAGE PIC X(400). */ + private String afterImage; + + /** HIST-REASON-CODE PIC X(04). */ + private String reasonCode; + + /** HIST-PROCESS-DATE PIC X(26). */ + private String processDate; + + /** HIST-PROCESS-USER PIC X(08). */ + private String processUser; + + public static final String TYPE_PORTFOLIO = "PT"; + public static final String TYPE_POSITION = "PS"; + public static final String TYPE_TRANSACTION = "TR"; + + public static final String ACTION_ADD = "A"; + public static final String ACTION_CHANGE = "C"; + public static final String ACTION_DELETE = "D"; + + public String getPortfolioId() { return portfolioId; } + public void setPortfolioId(String portfolioId) { this.portfolioId = portfolioId; } + public String getDate() { return date; } + public void setDate(String date) { this.date = date; } + public String getTime() { return time; } + public void setTime(String time) { this.time = time; } + public String getSeqNo() { return seqNo; } + public void setSeqNo(String seqNo) { this.seqNo = seqNo; } + public String getRecordType() { return recordType; } + public void setRecordType(String recordType) { this.recordType = recordType; } + public String getActionCode() { return actionCode; } + public void setActionCode(String actionCode) { this.actionCode = actionCode; } + public String getBeforeImage() { return beforeImage; } + public void setBeforeImage(String beforeImage) { this.beforeImage = beforeImage; } + public String getAfterImage() { return afterImage; } + public void setAfterImage(String afterImage) { this.afterImage = afterImage; } + public String getReasonCode() { return reasonCode; } + public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; } + public String getProcessDate() { return processDate; } + public void setProcessDate(String processDate) { this.processDate = processDate; } + public String getProcessUser() { return processUser; } + public void setProcessUser(String processUser) { this.processUser = processUser; } +} diff --git a/java/src/main/java/com/portfolio/model/copybook/PortfolioRecord.java b/java/src/main/java/com/portfolio/model/copybook/PortfolioRecord.java new file mode 100644 index 00000000..7ed8f73e --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/PortfolioRecord.java @@ -0,0 +1,67 @@ +package com.portfolio.model.copybook; + +import java.math.BigDecimal; + +/** + * Migrated from copybook {@code src/copybook/common/PORTFLIO.cpy} (01 PORT-RECORD). + * + *

Portfolio master record. Key = PORT-KEY (portfolio id + account no). + */ +public class PortfolioRecord { + + /** PORT-ID PIC X(8). */ + private String id; + + /** PORT-ACCOUNT-NO PIC X(10). */ + private String accountNo; + + /** PORT-CLIENT-NAME PIC X(30). */ + private String clientName; + + /** PORT-CLIENT-TYPE PIC X(1) — I=Individual, C=Corporate, T=Trust (level-88s). */ + private String clientType; + + /** PORT-CREATE-DATE PIC 9(8) — YYYYMMDD. */ + private int createDate; + + /** PORT-LAST-MAINT PIC 9(8) — YYYYMMDD. */ + private int lastMaint; + + /** PORT-STATUS PIC X(1) — A=Active, C=Closed, S=Suspended (level-88s). */ + private String status; + + /** PORT-TOTAL-VALUE PIC S9(13)V99 COMP-3. */ + private BigDecimal totalValue; + + /** PORT-CASH-BALANCE PIC S9(13)V99 COMP-3. */ + private BigDecimal cashBalance; + + /** PORT-LAST-USER PIC X(8). */ + private String lastUser; + + /** PORT-LAST-TRANS PIC 9(8). */ + private int lastTrans; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getAccountNo() { return accountNo; } + public void setAccountNo(String accountNo) { this.accountNo = accountNo; } + public String getClientName() { return clientName; } + public void setClientName(String clientName) { this.clientName = clientName; } + public String getClientType() { return clientType; } + public void setClientType(String clientType) { this.clientType = clientType; } + public int getCreateDate() { return createDate; } + public void setCreateDate(int createDate) { this.createDate = createDate; } + public int getLastMaint() { return lastMaint; } + public void setLastMaint(int lastMaint) { this.lastMaint = lastMaint; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public BigDecimal getTotalValue() { return totalValue; } + public void setTotalValue(BigDecimal totalValue) { this.totalValue = totalValue; } + public BigDecimal getCashBalance() { return cashBalance; } + public void setCashBalance(BigDecimal cashBalance) { this.cashBalance = cashBalance; } + public String getLastUser() { return lastUser; } + public void setLastUser(String lastUser) { this.lastUser = lastUser; } + public int getLastTrans() { return lastTrans; } + public void setLastTrans(int lastTrans) { this.lastTrans = lastTrans; } +} diff --git a/java/src/main/java/com/portfolio/model/copybook/PortfolioValidation.java b/java/src/main/java/com/portfolio/model/copybook/PortfolioValidation.java new file mode 100644 index 00000000..7f024130 --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/PortfolioValidation.java @@ -0,0 +1,33 @@ +package com.portfolio.model.copybook; + +import java.math.BigDecimal; + +/** + * Migrated from copybook {@code src/copybook/common/PORTVAL.cpy} + * (validation return codes, error messages, and constants). + */ +public final class PortfolioValidation { + + private PortfolioValidation() {} + + // Validation return codes (VAL-RETURN-CODES, PIC S9(4)) + public static final int VAL_SUCCESS = 0; + public static final int VAL_INVALID_ID = 1; + public static final int VAL_INVALID_ACCT = 2; + public static final int VAL_INVALID_TYPE = 3; + public static final int VAL_INVALID_AMT = 4; + + // Validation error messages (VAL-ERROR-MESSAGES, PIC X(50)) + public static final String ERR_ID = "Invalid Portfolio ID format"; + public static final String ERR_ACCT = "Invalid Account Number format"; + public static final String ERR_TYPE = "Invalid Investment Type"; + public static final String ERR_AMT = "Amount outside valid range"; + + // Validation constants (VAL-CONSTANTS) + /** VAL-MIN-AMOUNT PIC S9(13)V99. */ + public static final BigDecimal MIN_AMOUNT = new BigDecimal("-9999999999999.99"); + /** VAL-MAX-AMOUNT PIC S9(13)V99. */ + public static final BigDecimal MAX_AMOUNT = new BigDecimal("9999999999999.99"); + /** VAL-ID-PREFIX PIC X(4). */ + public static final String ID_PREFIX = "PORT"; +} diff --git a/java/src/main/java/com/portfolio/model/copybook/PositionRecord.java b/java/src/main/java/com/portfolio/model/copybook/PositionRecord.java new file mode 100644 index 00000000..1749e85c --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/PositionRecord.java @@ -0,0 +1,62 @@ +package com.portfolio.model.copybook; + +import java.math.BigDecimal; + +/** + * Migrated from copybook {@code src/copybook/common/POSREC.cpy} (01 POSITION-RECORD). + * + *

Key = POS-KEY (portfolio id + date + investment id). + */ +public class PositionRecord { + + /** POS-PORTFOLIO-ID PIC X(08). */ + private String portfolioId; + + /** POS-DATE PIC X(08) — YYYYMMDD. */ + private String date; + + /** POS-INVESTMENT-ID PIC X(10). */ + private String investmentId; + + /** POS-QUANTITY PIC S9(11)V9(4) COMP-3. */ + private BigDecimal quantity; + + /** POS-COST-BASIS PIC S9(13)V9(2) COMP-3. */ + private BigDecimal costBasis; + + /** POS-MARKET-VALUE PIC S9(13)V9(2) COMP-3. */ + private BigDecimal marketValue; + + /** POS-CURRENCY PIC X(03). */ + private String currency; + + /** POS-STATUS PIC X(01) — A=Active, C=Closed, P=Pending (level-88s). */ + private String status; + + /** POS-LAST-MAINT-DATE PIC X(26). */ + private String lastMaintDate; + + /** POS-LAST-MAINT-USER PIC X(08). */ + private String lastMaintUser; + + public String getPortfolioId() { return portfolioId; } + public void setPortfolioId(String portfolioId) { this.portfolioId = portfolioId; } + public String getDate() { return date; } + public void setDate(String date) { this.date = date; } + public String getInvestmentId() { return investmentId; } + public void setInvestmentId(String investmentId) { this.investmentId = investmentId; } + public BigDecimal getQuantity() { return quantity; } + public void setQuantity(BigDecimal quantity) { this.quantity = quantity; } + public BigDecimal getCostBasis() { return costBasis; } + public void setCostBasis(BigDecimal costBasis) { this.costBasis = costBasis; } + public BigDecimal getMarketValue() { return marketValue; } + public void setMarketValue(BigDecimal marketValue) { this.marketValue = marketValue; } + public String getCurrency() { return currency; } + public void setCurrency(String currency) { this.currency = currency; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getLastMaintDate() { return lastMaintDate; } + public void setLastMaintDate(String lastMaintDate) { this.lastMaintDate = lastMaintDate; } + public String getLastMaintUser() { return lastMaintUser; } + public void setLastMaintUser(String lastMaintUser) { this.lastMaintUser = lastMaintUser; } +} diff --git a/java/src/main/java/com/portfolio/model/copybook/ProcessSequenceRecord.java b/java/src/main/java/com/portfolio/model/copybook/ProcessSequenceRecord.java new file mode 100644 index 00000000..2ca84dc4 --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/ProcessSequenceRecord.java @@ -0,0 +1,142 @@ +package com.portfolio.model.copybook; + +import java.util.ArrayList; +import java.util.List; + +/** + * Migrated from copybook {@code src/copybook/batch/PRCSEQ.cpy} + * (01 PROCESS-SEQUENCE-RECORD). + * + *

Batch process scheduling/sequencing definition. Key = PSR-KEY + * (process id + version). + */ +public class ProcessSequenceRecord { + + /** PSR-PROCESS-ID PIC X(8). */ + private String processId; + + /** PSR-VERSION PIC 9(2). */ + private int version; + + /** PSR-DESCRIPTION PIC X(30). */ + private String description; + + /** PSR-TYPE PIC X(3) — INI/PRC/RPT/TRM (level-88s). */ + private String type; + + /** PSR-FREQ PIC X(1) — D=Daily, W=Weekly, M=Monthly (level-88s). */ + private String frequency; + + /** PSR-START-TIME PIC 9(4) — HHMM. */ + private int startTime; + + /** PSR-MAX-TIME PIC 9(4) — minutes. */ + private int maxTime; + + /** PSR-DEP-ENTRY OCCURS 10 TIMES (PSR-DEP-COUNT PIC 9(2) COMP tracks count). */ + private List dependencies = new ArrayList<>(); + + /** PSR-PROGRAM PIC X(8). */ + private String program; + + /** PSR-PARM PIC X(50). */ + private String parameter; + + /** PSR-MAX-RC PIC S9(4) COMP. */ + private int maxReturnCode; + + /** PSR-RESTART PIC X(1) — Y=Restartable, N=No restart (level-88s). */ + private String restartable; + + /** PSR-ACTIVE-DAYS PIC X(7) — one Y/N flag per weekday (level-88s WEEKDAY/WEEKEND/ALL-DAYS). */ + private String activeDays; + + /** PSR-MONTH-END PIC X(1) — Y=run on last day of month. */ + private String monthEnd; + + /** PSR-HOLIDAY-RUN PIC X(1) — Y=run on holidays, N=skip. */ + private String holidayRun; + + /** PSR-RECOVERY-PGM PIC X(8). */ + private String recoveryProgram; + + /** PSR-RECOVERY-PARM PIC X(50). */ + private String recoveryParameter; + + /** PSR-ERROR-LIMIT PIC 9(4) COMP. */ + private int errorLimit; + + /** PSR-CREATE-DATE PIC X(10). */ + private String createDate; + + /** PSR-CREATE-USER PIC X(8). */ + private String createUser; + + /** PSR-UPDATE-DATE PIC X(10). */ + private String updateDate; + + /** PSR-UPDATE-USER PIC X(8). */ + private String updateUser; + + /** One entry of PSR-DEP-ENTRY OCCURS 10 TIMES. */ + public static class Dependency { + /** PSR-DEP-ID PIC X(8). */ + private String id; + /** PSR-DEP-TYPE PIC X(1) — H=Hard, S=Soft (level-88s). */ + private String type; + /** PSR-DEP-RC PIC S9(4) COMP. */ + private int returnCode; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public int getReturnCode() { return returnCode; } + public void setReturnCode(int returnCode) { this.returnCode = returnCode; } + } + + public String getProcessId() { return processId; } + public void setProcessId(String processId) { this.processId = processId; } + public int getVersion() { return version; } + public void setVersion(int version) { this.version = version; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public String getFrequency() { return frequency; } + public void setFrequency(String frequency) { this.frequency = frequency; } + public int getStartTime() { return startTime; } + public void setStartTime(int startTime) { this.startTime = startTime; } + public int getMaxTime() { return maxTime; } + public void setMaxTime(int maxTime) { this.maxTime = maxTime; } + public List getDependencies() { return dependencies; } + public void setDependencies(List dependencies) { this.dependencies = dependencies; } + public String getProgram() { return program; } + public void setProgram(String program) { this.program = program; } + public String getParameter() { return parameter; } + public void setParameter(String parameter) { this.parameter = parameter; } + public int getMaxReturnCode() { return maxReturnCode; } + public void setMaxReturnCode(int maxReturnCode) { this.maxReturnCode = maxReturnCode; } + public String getRestartable() { return restartable; } + public void setRestartable(String restartable) { this.restartable = restartable; } + public String getActiveDays() { return activeDays; } + public void setActiveDays(String activeDays) { this.activeDays = activeDays; } + public String getMonthEnd() { return monthEnd; } + public void setMonthEnd(String monthEnd) { this.monthEnd = monthEnd; } + public String getHolidayRun() { return holidayRun; } + public void setHolidayRun(String holidayRun) { this.holidayRun = holidayRun; } + public String getRecoveryProgram() { return recoveryProgram; } + public void setRecoveryProgram(String recoveryProgram) { this.recoveryProgram = recoveryProgram; } + public String getRecoveryParameter() { return recoveryParameter; } + public void setRecoveryParameter(String recoveryParameter) { this.recoveryParameter = recoveryParameter; } + public int getErrorLimit() { return errorLimit; } + public void setErrorLimit(int errorLimit) { this.errorLimit = errorLimit; } + public String getCreateDate() { return createDate; } + public void setCreateDate(String createDate) { this.createDate = createDate; } + public String getCreateUser() { return createUser; } + public void setCreateUser(String createUser) { this.createUser = createUser; } + public String getUpdateDate() { return updateDate; } + public void setUpdateDate(String updateDate) { this.updateDate = updateDate; } + public String getUpdateUser() { return updateUser; } + public void setUpdateUser(String updateUser) { this.updateUser = updateUser; } +} diff --git a/java/src/main/java/com/portfolio/model/copybook/ReturnCodeArea.java b/java/src/main/java/com/portfolio/model/copybook/ReturnCodeArea.java new file mode 100644 index 00000000..282c8533 --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/ReturnCodeArea.java @@ -0,0 +1,92 @@ +package com.portfolio.model.copybook; + +/** + * Migrated from copybook {@code src/copybook/common/RTNCODE.cpy} (01 RETURN-CODE-AREA) + * and {@code src/copybook/common/RETHND.cpy} (01 RETURN-HANDLING). + * + *

Return-code management area used by RTNCDE00 and callers. Standard code + * thresholds: 0=Success, 4=Warning, 8=Error, 12=Severe, 16=Critical. + */ +public class ReturnCodeArea { + + /** RC-REQUEST-TYPE PIC X — I=Initialize, S=Set, G=Get, L=Log, A=Analyze (level-88s). */ + private String requestType; + + /** RC-PROGRAM-ID PIC X(8). */ + private String programId; + + /** RC-CURRENT-CODE PIC S9(4) COMP. */ + private int currentCode; + + /** RC-HIGHEST-CODE PIC S9(4) COMP. */ + private int highestCode; + + /** RC-NEW-CODE PIC S9(4) COMP. */ + private int newCode; + + /** RC-STATUS PIC X — S=Success, W=Warning, E=Error, F=Severe (level-88s). */ + private String status; + + /** RC-MESSAGE PIC X(80). */ + private String message; + + /** RC-RESPONSE-CODE PIC S9(8) COMP. */ + private int responseCode; + + /** RC-START-TIME PIC X(26). */ + private String startTime; + + /** RC-END-TIME PIC X(26). */ + private String endTime; + + /** RC-TOTAL-CODES PIC S9(8) COMP. */ + private int totalCodes; + + /** RC-MAX-CODE PIC S9(4) COMP. */ + private int maxCode; + + /** RC-MIN-CODE PIC S9(4) COMP. */ + private int minCode; + + /** RC-RETURN-VALUE PIC S9(4) COMP. */ + private int returnValue; + + /** RC-HIGHEST-RETURN PIC S9(4) COMP. */ + private int highestReturn; + + /** RC-RETURN-STATUS PIC X. */ + private String returnStatus; + + public String getRequestType() { return requestType; } + public void setRequestType(String requestType) { this.requestType = requestType; } + public String getProgramId() { return programId; } + public void setProgramId(String programId) { this.programId = programId; } + public int getCurrentCode() { return currentCode; } + public void setCurrentCode(int currentCode) { this.currentCode = currentCode; } + public int getHighestCode() { return highestCode; } + public void setHighestCode(int highestCode) { this.highestCode = highestCode; } + public int getNewCode() { return newCode; } + public void setNewCode(int newCode) { this.newCode = newCode; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + public int getResponseCode() { return responseCode; } + public void setResponseCode(int responseCode) { this.responseCode = responseCode; } + public String getStartTime() { return startTime; } + public void setStartTime(String startTime) { this.startTime = startTime; } + public String getEndTime() { return endTime; } + public void setEndTime(String endTime) { this.endTime = endTime; } + public int getTotalCodes() { return totalCodes; } + public void setTotalCodes(int totalCodes) { this.totalCodes = totalCodes; } + public int getMaxCode() { return maxCode; } + public void setMaxCode(int maxCode) { this.maxCode = maxCode; } + public int getMinCode() { return minCode; } + public void setMinCode(int minCode) { this.minCode = minCode; } + public int getReturnValue() { return returnValue; } + public void setReturnValue(int returnValue) { this.returnValue = returnValue; } + public int getHighestReturn() { return highestReturn; } + public void setHighestReturn(int highestReturn) { this.highestReturn = highestReturn; } + public String getReturnStatus() { return returnStatus; } + public void setReturnStatus(String returnStatus) { this.returnStatus = returnStatus; } +} diff --git a/java/src/main/java/com/portfolio/model/copybook/SqlStatusCodes.java b/java/src/main/java/com/portfolio/model/copybook/SqlStatusCodes.java new file mode 100644 index 00000000..fa071f48 --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/SqlStatusCodes.java @@ -0,0 +1,32 @@ +package com.portfolio.model.copybook; + +/** + * Migrated from copybook {@code src/copybook/db2/SQLCA.cpy} (01 SQL-STATUS-CODES). + * + *

The SQLCA itself (SQLCODE/SQLSTATE communication area) has no direct Java + * equivalent: SQL error signalling is replaced by exceptions + * ({@code DataAccessException} hierarchy / {@link com.portfolio.common.DatabaseException}). + * The well-known SQLSTATE values checked by the COBOL programs are preserved here. + */ +public final class SqlStatusCodes { + + private SqlStatusCodes() {} + + /** SQL-SUCCESS PIC X(5) VALUE '00000'. */ + public static final String SUCCESS = "00000"; + /** SQL-NOT-FOUND PIC X(5) VALUE '02000'. */ + public static final String NOT_FOUND = "02000"; + /** SQL-DUP-KEY PIC X(5) VALUE '23505'. */ + public static final String DUPLICATE_KEY = "23505"; + /** SQL-DEADLOCK PIC X(5) VALUE '40001'. */ + public static final String DEADLOCK = "40001"; + /** SQL-TIMEOUT PIC X(5) VALUE '40003'. */ + public static final String TIMEOUT = "40003"; + /** SQL-CONNECTION-ERROR PIC X(5) VALUE '08001'. */ + public static final String CONNECTION_ERROR = "08001"; + /** SQL-DB-ERROR PIC X(5) VALUE '58004'. */ + public static final String DB_ERROR = "58004"; + + /** DB2 SQLCODE -803: duplicate key on insert (tolerated by HISTLD00). */ + public static final int SQLCODE_DUPLICATE = -803; +} diff --git a/java/src/main/java/com/portfolio/model/copybook/TransactionRecord.java b/java/src/main/java/com/portfolio/model/copybook/TransactionRecord.java new file mode 100644 index 00000000..fdef6b01 --- /dev/null +++ b/java/src/main/java/com/portfolio/model/copybook/TransactionRecord.java @@ -0,0 +1,78 @@ +package com.portfolio.model.copybook; + +import java.math.BigDecimal; + +/** + * Migrated from copybook {@code src/copybook/common/TRNREC.cpy} (01 TRANSACTION-RECORD). + * + *

Key = TRN-KEY (date + time + portfolio id + sequence no). + * Packed-decimal (COMP-3) financial fields are mapped to {@link BigDecimal}. + */ +public class TransactionRecord { + + /** TRN-DATE PIC X(08) — YYYYMMDD. */ + private String date; + + /** TRN-TIME PIC X(06) — HHMMSS. */ + private String time; + + /** TRN-PORTFOLIO-ID PIC X(08). */ + private String portfolioId; + + /** TRN-SEQUENCE-NO PIC X(06). */ + private String sequenceNo; + + /** TRN-INVESTMENT-ID PIC X(10). */ + private String investmentId; + + /** TRN-TYPE PIC X(02) — BU=Buy, SL=Sell, TR=Transfer, FE=Fee (level-88s). */ + private String type; + + /** TRN-QUANTITY PIC S9(11)V9(4) COMP-3. */ + private BigDecimal quantity; + + /** TRN-PRICE PIC S9(11)V9(4) COMP-3. */ + private BigDecimal price; + + /** TRN-AMOUNT PIC S9(13)V9(2) COMP-3. */ + private BigDecimal amount; + + /** TRN-CURRENCY PIC X(03). */ + private String currency; + + /** TRN-STATUS PIC X(01) — P=Pending, D=Done, F=Failed, R=Reversed (level-88s). */ + private String status; + + /** TRN-PROCESS-DATE PIC X(26). */ + private String processDate; + + /** TRN-PROCESS-USER PIC X(08). */ + private String processUser; + + public String getDate() { return date; } + public void setDate(String date) { this.date = date; } + public String getTime() { return time; } + public void setTime(String time) { this.time = time; } + public String getPortfolioId() { return portfolioId; } + public void setPortfolioId(String portfolioId) { this.portfolioId = portfolioId; } + public String getSequenceNo() { return sequenceNo; } + public void setSequenceNo(String sequenceNo) { this.sequenceNo = sequenceNo; } + public String getInvestmentId() { return investmentId; } + public void setInvestmentId(String investmentId) { this.investmentId = investmentId; } + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public BigDecimal getQuantity() { return quantity; } + public void setQuantity(BigDecimal quantity) { this.quantity = quantity; } + public BigDecimal getPrice() { return price; } + public void setPrice(BigDecimal price) { this.price = price; } + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + public String getCurrency() { return currency; } + public void setCurrency(String currency) { this.currency = currency; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getProcessDate() { return processDate; } + public void setProcessDate(String processDate) { this.processDate = processDate; } + public String getProcessUser() { return processUser; } + public void setProcessUser(String processUser) { this.processUser = processUser; } +} diff --git a/java/src/main/java/com/portfolio/repository/BatchControlRepository.java b/java/src/main/java/com/portfolio/repository/BatchControlRepository.java new file mode 100644 index 00000000..b07ba684 --- /dev/null +++ b/java/src/main/java/com/portfolio/repository/BatchControlRepository.java @@ -0,0 +1,8 @@ +package com.portfolio.repository; + +import com.portfolio.domain.BatchControl; +import org.springframework.data.jpa.repository.JpaRepository; + +/** Repository over the batch control table (VSAM BCHCTL migration). */ +public interface BatchControlRepository extends JpaRepository { +} diff --git a/java/src/main/java/com/portfolio/repository/ErrorLogRepository.java b/java/src/main/java/com/portfolio/repository/ErrorLogRepository.java new file mode 100644 index 00000000..0809c6cc --- /dev/null +++ b/java/src/main/java/com/portfolio/repository/ErrorLogRepository.java @@ -0,0 +1,8 @@ +package com.portfolio.repository; + +import com.portfolio.domain.ErrorLog; +import org.springframework.data.jpa.repository.JpaRepository; + +/** Repository over the ERRLOG table (written by the ERRPROC migration). */ +public interface ErrorLogRepository extends JpaRepository { +} diff --git a/java/src/main/java/com/portfolio/repository/PositionHistoryRepository.java b/java/src/main/java/com/portfolio/repository/PositionHistoryRepository.java new file mode 100644 index 00000000..be2b8cd8 --- /dev/null +++ b/java/src/main/java/com/portfolio/repository/PositionHistoryRepository.java @@ -0,0 +1,8 @@ +package com.portfolio.repository; + +import com.portfolio.domain.PositionHistory; +import org.springframework.data.jpa.repository.JpaRepository; + +/** Repository over the POSHIST table (target of the HISTLD00 load). */ +public interface PositionHistoryRepository extends JpaRepository { +} diff --git a/java/src/main/java/com/portfolio/repository/TransactionHistoryFileRepository.java b/java/src/main/java/com/portfolio/repository/TransactionHistoryFileRepository.java new file mode 100644 index 00000000..f1193b53 --- /dev/null +++ b/java/src/main/java/com/portfolio/repository/TransactionHistoryFileRepository.java @@ -0,0 +1,9 @@ +package com.portfolio.repository; + +import com.portfolio.domain.TransactionHistoryFileRecord; +import org.springframework.data.jpa.repository.JpaRepository; + +/** Repository over the VSAM TRANHIST migration table (input of HISTLD00). */ +public interface TransactionHistoryFileRepository + extends JpaRepository { +} diff --git a/java/src/main/resources/application.yml b/java/src/main/resources/application.yml new file mode 100644 index 00000000..c1235f4b --- /dev/null +++ b/java/src/main/resources/application.yml @@ -0,0 +1,33 @@ +spring: + datasource: + # Embedded H2 stands in for DB2; point at a DB2 JDBC URL in production. + url: jdbc:h2:mem:portfolio;DB_CLOSE_DELAY=-1 + driver-class-name: org.h2.Driver + username: sa + password: "" + jpa: + hibernate: + ddl-auto: create + open-in-view: false + sql: + init: + mode: never + batch: + jdbc: + initialize-schema: always + job: + enabled: false # jobs are launched explicitly (HistoryLoadJobRunner / tests) + +--- +# Sample run profile: loads a small TRANHIST + BCHCTL dataset, then runs HISTLD00. +# mvn spring-boot:run -Dspring-boot.run.profiles=histld00 +spring: + config: + activate: + on-profile: histld00 + jpa: + defer-datasource-initialization: true + sql: + init: + mode: always + data-locations: classpath:sample-data.sql diff --git a/java/src/main/resources/sample-data.sql b/java/src/main/resources/sample-data.sql new file mode 100644 index 00000000..c1718a12 --- /dev/null +++ b/java/src/main/resources/sample-data.sql @@ -0,0 +1,16 @@ +-- Small sample dataset for the HISTLD00 reference run (histld00 profile). +-- VSAM_TRANHIST = migrated TRANSACTION-HISTORY KSDS input file. +INSERT INTO VSAM_TRANHIST (TRANS_DATE, TRANS_TIME, PORTFOLIO_ID, SEQUENCE_NO, + ACCOUNT_NO, TRANS_TYPE, SECURITY_ID, QUANTITY, PRICE, AMOUNT, FEES, + TOTAL_AMOUNT, COST_BASIS, GAIN_LOSS) VALUES +('2024-03-20', '09:30:00', 'PORT00001', '000001', 'ACCT0001', 'BU', 'IBM ', 100.000, 185.500, 18550.00, 9.99, 18559.99, 18559.99, 0.00), +('2024-03-20', '09:31:00', 'PORT00001', '000002', 'ACCT0001', 'BU', 'MSFT ', 50.000, 420.250, 21012.50, 9.99, 21022.49, 21022.49, 0.00), +('2024-03-20', '10:15:00', 'PORT00002', '000003', 'ACCT0002', 'SL', 'IBM ', 25.000, 186.000, 4650.00, 9.99, 4640.01, 4500.00, 140.01), +('2024-03-20', '11:00:00', 'PORT00002', '000004', 'ACCT0002', 'FE', 'CASH ', 0.000, 0.000, 25.00, 0.00, 25.00, 0.00, 0.00), +('2024-03-20', '14:45:00', 'PORT00003', '000005', 'ACCT0003', 'TR', 'AAPL ', 10.000, 172.100, 1721.00, 0.00, 1721.00, 1721.00, 0.00); + +-- VSAM_BCHCTL = migrated batch control KSDS; HISTLD00 control record. +INSERT INTO VSAM_BCHCTL (JOB_NAME, PROCESS_DATE, SEQUENCE_NO, STATUS, + STEP_NAME, PROGRAM_NAME, RECORDS_READ, RECORDS_WRITTEN, RETURN_CODE, + RESTART_COUNT) VALUES +('HISTLD00', '20240320', 1, 'R', 'STEP010', 'HISTLD00', 0, 0, 0, 0); diff --git a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java new file mode 100644 index 00000000..1c407b1f --- /dev/null +++ b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java @@ -0,0 +1,216 @@ +package com.portfolio.batch; + +import com.portfolio.domain.BatchControl; +import com.portfolio.domain.PositionHistory; +import com.portfolio.domain.TransactionHistoryFileRecord; +import com.portfolio.repository.BatchControlRepository; +import com.portfolio.repository.ErrorLogRepository; +import com.portfolio.repository.PositionHistoryRepository; +import com.portfolio.repository.TransactionHistoryFileRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * End-to-end tests of the HISTLD00 Spring Batch migration against embedded H2. + */ +@SpringBootTest +class HistoryLoadJobTest { + + private static final String PROCESS_DATE = "20240320"; + private static final LocalDate TRANS_DATE = LocalDate.of(2024, 3, 20); + + @Autowired private JobLauncher jobLauncher; + @Autowired private Job histld00Job; + @Autowired private HistoryLoadStats stats; + @Autowired private TransactionHistoryFileRepository tranHistRepository; + @Autowired private PositionHistoryRepository positionHistoryRepository; + @Autowired private BatchControlRepository batchControlRepository; + @Autowired private ErrorLogRepository errorLogRepository; + + @BeforeEach + void setUp() { + cleanUp(); + BatchControl control = new BatchControl(); + control.setKey(new BatchControl.Key("HISTLD00", PROCESS_DATE, 1)); + control.setStatus("R"); + control.setStepName("STEP010"); + control.setProgramName("HISTLD00"); + batchControlRepository.save(control); + } + + @AfterEach + void cleanUp() { + tranHistRepository.deleteAll(); + positionHistoryRepository.deleteAll(); + batchControlRepository.deleteAll(); + errorLogRepository.deleteAll(); + } + + private JobExecution runJob() throws Exception { + JobParameters params = new JobParametersBuilder() + .addString("processDate", PROCESS_DATE) + .addLong("startedAt", System.nanoTime()) + .toJobParameters(); + return jobLauncher.run(histld00Job, params); + } + + private TransactionHistoryFileRecord validRecord(String portfolioId, String seq, LocalTime time) { + TransactionHistoryFileRecord rec = new TransactionHistoryFileRecord(); + rec.setKey(new TransactionHistoryFileRecord.Key(TRANS_DATE, time, portfolioId, seq)); + rec.setAccountNo("ACCT0001"); + rec.setTransType("BU"); + rec.setSecurityId("IBM"); + rec.setQuantity(new BigDecimal("100.000")); + rec.setPrice(new BigDecimal("185.500")); + rec.setAmount(new BigDecimal("18550.00")); + rec.setFees(new BigDecimal("9.99")); + rec.setTotalAmount(new BigDecimal("18559.99")); + rec.setCostBasis(new BigDecimal("18559.99")); + rec.setGainLoss(BigDecimal.ZERO); + return rec; + } + + @Test + void loadsValidRecordsIntoPositionHistory() throws Exception { + tranHistRepository.save(validRecord("PORT00001", "000001", LocalTime.of(9, 30))); + tranHistRepository.save(validRecord("PORT00002", "000002", LocalTime.of(10, 15))); + + JobExecution execution = runJob(); + + assertThat(execution.getStatus()).isEqualTo(BatchStatus.COMPLETED); + assertThat(positionHistoryRepository.count()).isEqualTo(2); + assertThat(stats.getRecordsRead()).isEqualTo(2); + assertThat(stats.getRecordsWritten()).isEqualTo(2); + assertThat(stats.getErrorCount()).isZero(); + + Optional loaded = positionHistoryRepository.findById( + new PositionHistory.Key("ACCT0001", "PORT00001", TRANS_DATE, LocalTime.of(9, 30))); + assertThat(loaded).isPresent(); + assertThat(loaded.get().getQuantity()).isEqualByComparingTo("100.000"); + assertThat(loaded.get().getTotalAmount()).isEqualByComparingTo("18559.99"); + assertThat(loaded.get().getProgramId()).isEqualTo("HISTLD00"); + } + + @Test + void updatesBatchControlRecordWithCountersAndStatus() throws Exception { + tranHistRepository.save(validRecord("PORT00001", "000001", LocalTime.of(9, 30))); + + runJob(); + + BatchControl control = batchControlRepository + .findById(new BatchControl.Key("HISTLD00", PROCESS_DATE, 1)).orElseThrow(); + assertThat(control.getStatus()).isEqualTo("C"); + assertThat(control.getRecordsRead()).isEqualTo(1); + assertThat(control.getRecordsWritten()).isEqualTo(1); + assertThat(control.getReturnCode()).isZero(); + assertThat(control.getRestartCount()).isEqualTo(1); + } + + @Test + void skipsDuplicateRecordsLikeSqlcodeMinus803() throws Exception { + TransactionHistoryFileRecord rec = validRecord("PORT00001", "000001", LocalTime.of(9, 30)); + tranHistRepository.save(rec); + + // Pre-existing POSHIST row with the same key = duplicate insert (-803) + PositionHistory existing = new PositionHistory(); + existing.setKey(new PositionHistory.Key("ACCT0001", "PORT00001", TRANS_DATE, LocalTime.of(9, 30))); + existing.setTransType("BU"); + existing.setSecurityId("IBM"); + existing.setQuantity(BigDecimal.ONE); + existing.setPrice(BigDecimal.ONE); + existing.setAmount(BigDecimal.ONE); + existing.setFees(BigDecimal.ZERO); + existing.setTotalAmount(BigDecimal.ONE); + existing.setCostBasis(BigDecimal.ONE); + existing.setGainLoss(BigDecimal.ZERO); + existing.setProcessDate(TRANS_DATE); + existing.setProcessTime(LocalTime.NOON); + existing.setProgramId("HISTLD00"); + existing.setUserId("TEST"); + existing.setAuditTimestamp(TRANS_DATE.atTime(LocalTime.NOON)); + positionHistoryRepository.save(existing); + + JobExecution execution = runJob(); + + assertThat(execution.getStatus()).isEqualTo(BatchStatus.COMPLETED); + assertThat(positionHistoryRepository.count()).isEqualTo(1); + // duplicate not counted as written and not counted as an error + assertThat(stats.getRecordsWritten()).isZero(); + assertThat(stats.getErrorCount()).isZero(); + // original row untouched (COBOL CONTINUE on -803) + PositionHistory unchanged = positionHistoryRepository.findById(existing.getKey()).orElseThrow(); + assertThat(unchanged.getQuantity()).isEqualByComparingTo(BigDecimal.ONE); + } + + @Test + void countsValidationErrorsAndLogsToErrlog() throws Exception { + tranHistRepository.save(validRecord("PORT00001", "000001", LocalTime.of(9, 30))); + TransactionHistoryFileRecord bad = validRecord("PORT00002", "000002", LocalTime.of(10, 0)); + bad.setTransType("XX"); + tranHistRepository.save(bad); + + JobExecution execution = runJob(); + + assertThat(execution.getStatus()).isEqualTo(BatchStatus.COMPLETED); + assertThat(positionHistoryRepository.count()).isEqualTo(1); + assertThat(stats.getErrorCount()).isEqualTo(1); + assertThat(errorLogRepository.count()).isEqualTo(1); + assertThat(errorLogRepository.findAll().get(0).getErrorMessage()) + .contains("Invalid transaction type"); + + BatchControl control = batchControlRepository + .findById(new BatchControl.Key("HISTLD00", PROCESS_DATE, 1)).orElseThrow(); + assertThat(control.getReturnCode()).isEqualTo(1); + assertThat(control.getStatus()).isEqualTo("C"); + } + + @Test + void abortsWhenErrorCountExceedsOneHundred() throws Exception { + for (int i = 0; i < 105; i++) { + TransactionHistoryFileRecord bad = validRecord("PORT00001", + String.format("%06d", i + 1), + LocalTime.of(9, 0).plusSeconds(i)); + bad.setTransType("XX"); + tranHistRepository.save(bad); + } + + JobExecution execution = runJob(); + + assertThat(execution.getStatus()).isEqualTo(BatchStatus.FAILED); + assertThat(stats.getErrorCount()).isEqualTo(101); + assertThat(execution.getAllFailureExceptions()) + .anyMatch(e -> e instanceof ErrorLimitExceededException); + + BatchControl control = batchControlRepository + .findById(new BatchControl.Key("HISTLD00", PROCESS_DATE, 1)).orElseThrow(); + assertThat(control.getStatus()).isEqualTo("E"); + assertThat(control.getReturnCode()).isEqualTo(101); + } + + @Test + void failsWhenControlRecordMissing() throws Exception { + batchControlRepository.deleteAll(); + tranHistRepository.save(validRecord("PORT00001", "000001", LocalTime.of(9, 30))); + + JobExecution execution = runJob(); + + assertThat(execution.getStatus()).isEqualTo(BatchStatus.FAILED); + assertThat(positionHistoryRepository.count()).isZero(); + } +} From 5c2baf441ce1dcca6ef5a047bc2a03f455167915 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:41:59 +0000 Subject: [PATCH 2/7] Record failed jobs as status E, avoid ERRLOG key collisions, step-scope reader Co-Authored-By: Eashan Sinha --- .../com/portfolio/batch/BatchControlService.java | 5 +++-- .../com/portfolio/batch/HistoryLoadJobConfig.java | 6 +++++- .../portfolio/common/ErrorHandlingService.java | 15 ++++++++++++++- .../com/portfolio/batch/HistoryLoadJobTest.java | 2 ++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/java/src/main/java/com/portfolio/batch/BatchControlService.java b/java/src/main/java/com/portfolio/batch/BatchControlService.java index 056c9618..4201edce 100644 --- a/java/src/main/java/com/portfolio/batch/BatchControlService.java +++ b/java/src/main/java/com/portfolio/batch/BatchControlService.java @@ -62,9 +62,10 @@ public void updateCheckpoint(String jobName, String processDate, /** Final control update at job end: status, counters, and return code. */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void markComplete(String jobName, String processDate, - long recordsRead, long recordsWritten, int returnCode) { + long recordsRead, long recordsWritten, int returnCode, + boolean jobFailed) { BatchControl control = find(jobName, processDate); - control.setStatus(returnCode > HistoryLoadStats.MAX_ERRORS ? "E" : "C"); + control.setStatus(jobFailed || returnCode > HistoryLoadStats.MAX_ERRORS ? "E" : "C"); control.setRecordsRead(recordsRead); control.setRecordsWritten(recordsWritten); control.setReturnCode(returnCode); diff --git a/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java b/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java index 7004c6b9..a3fba24c 100644 --- a/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java +++ b/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java @@ -12,7 +12,9 @@ import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.ChunkListener; +import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.item.data.RepositoryItemReader; import org.springframework.batch.item.data.builder.RepositoryItemReaderBuilder; import org.springframework.context.annotation.Bean; @@ -54,6 +56,7 @@ public class HistoryLoadJobConfig { public static final String PROGRAM_ID = "HISTLD00"; @Bean + @StepScope public RepositoryItemReader historyItemReader( TransactionHistoryFileRepository repository) { Map sorts = new LinkedHashMap<>(); @@ -118,7 +121,8 @@ public void afterJob(JobExecution jobExecution) { jobExecution.getJobParameters().getString("processDate"), stats.getRecordsRead(), stats.getRecordsWritten(), - (int) Math.min(stats.getErrorCount(), Integer.MAX_VALUE)); + (int) Math.min(stats.getErrorCount(), Integer.MAX_VALUE), + jobExecution.getStatus() == BatchStatus.FAILED); } }) .start(histld00Step) diff --git a/java/src/main/java/com/portfolio/common/ErrorHandlingService.java b/java/src/main/java/com/portfolio/common/ErrorHandlingService.java index 209d4b85..5185fa80 100644 --- a/java/src/main/java/com/portfolio/common/ErrorHandlingService.java +++ b/java/src/main/java/com/portfolio/common/ErrorHandlingService.java @@ -50,7 +50,7 @@ public ErrorHandlingService(ErrorLogRepository errorLogRepository) { @Transactional(propagation = Propagation.REQUIRES_NEW) public int logError(String programId, String errorType, int severity, String errorCode, String message, String details) { - LocalDateTime now = LocalDateTime.now(); + LocalDateTime now = uniqueTimestamp(programId); ErrorLog entry = new ErrorLog(); entry.setKey(new ErrorLog.Key(now, programId)); @@ -70,6 +70,19 @@ public int logError(String programId, String errorType, int severity, return severity; } + /** + * The ERRLOG key is (ERROR_TIMESTAMP, PROGRAM_ID); two errors from the same + * program within the clock resolution would silently overwrite each other, + * so the timestamp is nudged forward by a microsecond until it is unique. + */ + private LocalDateTime uniqueTimestamp(String programId) { + LocalDateTime ts = LocalDateTime.now(); + while (errorLogRepository.existsById(new ErrorLog.Key(ts, programId))) { + ts = ts.plusNanos(1_000); + } + return ts; + } + private static String truncate(String value, int max) { if (value == null) { return null; diff --git a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java index 1c407b1f..f7c3f837 100644 --- a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java +++ b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java @@ -194,6 +194,8 @@ void abortsWhenErrorCountExceedsOneHundred() throws Exception { assertThat(execution.getStatus()).isEqualTo(BatchStatus.FAILED); assertThat(stats.getErrorCount()).isEqualTo(101); + // every counted error has its own ERRLOG row (no key collisions) + assertThat(errorLogRepository.count()).isEqualTo(101); assertThat(execution.getAllFailureExceptions()) .anyMatch(e -> e instanceof ErrorLimitExceededException); From 7fbadb35ae81dbde129e4b4df6c315c298ebef90 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:50:12 +0000 Subject: [PATCH 3/7] Use BCHCON status constants; record completed jobs as 'D' not 'C' Co-Authored-By: Eashan Sinha --- .../java/com/portfolio/batch/BatchControlService.java | 9 ++++++--- .../java/com/portfolio/batch/HistoryLoadJobTest.java | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/java/src/main/java/com/portfolio/batch/BatchControlService.java b/java/src/main/java/com/portfolio/batch/BatchControlService.java index 4201edce..d3aa1ef2 100644 --- a/java/src/main/java/com/portfolio/batch/BatchControlService.java +++ b/java/src/main/java/com/portfolio/batch/BatchControlService.java @@ -1,6 +1,7 @@ package com.portfolio.batch; import com.portfolio.common.FileProcessingException; +import com.portfolio.model.copybook.BatchControlConstants; import com.portfolio.domain.BatchControl; import com.portfolio.repository.BatchControlRepository; import org.springframework.stereotype.Service; @@ -16,7 +17,7 @@ * 2310-UPDATE-CHECKPOINT) against the BCHCTL migration table. * *

Status values are from {@code src/copybook/batch/BCHCON.cpy}: - * 'A' = active, 'C'/'D' = complete/done, 'E' = error. + * 'R' = ready, 'A' = active, 'W' = waiting, 'D' = done, 'E' = error. */ @Service public class BatchControlService { @@ -41,7 +42,7 @@ public BatchControlService(BatchControlRepository repository) { @Transactional(propagation = Propagation.REQUIRES_NEW) public BatchControl.Key markActive(String jobName, String processDate) { BatchControl control = find(jobName, processDate); - control.setStatus("A"); + control.setStatus(BatchControlConstants.STAT_ACTIVE); control.setStartTime(LocalTime.now().format(TIME_FMT)); control.setAttemptTimestamp(LocalDateTime.now().format(TS_FMT)); control.setRestartCount(control.getRestartCount() + 1); @@ -65,7 +66,9 @@ public void markComplete(String jobName, String processDate, long recordsRead, long recordsWritten, int returnCode, boolean jobFailed) { BatchControl control = find(jobName, processDate); - control.setStatus(jobFailed || returnCode > HistoryLoadStats.MAX_ERRORS ? "E" : "C"); + control.setStatus(jobFailed || returnCode > HistoryLoadStats.MAX_ERRORS + ? BatchControlConstants.STAT_ERROR + : BatchControlConstants.STAT_DONE); control.setRecordsRead(recordsRead); control.setRecordsWritten(recordsWritten); control.setReturnCode(returnCode); diff --git a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java index f7c3f837..f7c13142 100644 --- a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java +++ b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java @@ -115,7 +115,7 @@ void updatesBatchControlRecordWithCountersAndStatus() throws Exception { BatchControl control = batchControlRepository .findById(new BatchControl.Key("HISTLD00", PROCESS_DATE, 1)).orElseThrow(); - assertThat(control.getStatus()).isEqualTo("C"); + assertThat(control.getStatus()).isEqualTo("D"); assertThat(control.getRecordsRead()).isEqualTo(1); assertThat(control.getRecordsWritten()).isEqualTo(1); assertThat(control.getReturnCode()).isZero(); @@ -177,7 +177,7 @@ void countsValidationErrorsAndLogsToErrlog() throws Exception { BatchControl control = batchControlRepository .findById(new BatchControl.Key("HISTLD00", PROCESS_DATE, 1)).orElseThrow(); assertThat(control.getReturnCode()).isEqualTo(1); - assertThat(control.getStatus()).isEqualTo("C"); + assertThat(control.getStatus()).isEqualTo("D"); } @Test From 1a917a43d2298115526ffa5f20d3a726150923db Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:04:35 +0000 Subject: [PATCH 4/7] Tolerate non-duplicate POSHIST insert errors like DB2-ERROR-ROUTINE Co-Authored-By: Eashan Sinha --- .../portfolio/batch/HistoryItemWriter.java | 21 ++++++++- .../portfolio/batch/HistoryLoadJobConfig.java | 24 +++++++++- .../com/portfolio/batch/HistoryLoadStats.java | 4 +- .../portfolio/batch/HistoryLoadJobTest.java | 44 ++++++++++++++++++- 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/java/src/main/java/com/portfolio/batch/HistoryItemWriter.java b/java/src/main/java/com/portfolio/batch/HistoryItemWriter.java index 6d7347e1..ef938ab7 100644 --- a/java/src/main/java/com/portfolio/batch/HistoryItemWriter.java +++ b/java/src/main/java/com/portfolio/batch/HistoryItemWriter.java @@ -5,6 +5,8 @@ import org.springframework.batch.item.Chunk; import org.springframework.batch.item.ItemWriter; import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; /** * Insert step of the HISTLD00 migration (COBOL @@ -15,6 +17,13 @@ * exists is skipped without counting as written or as an error. Chunk-based * commits by Spring Batch replace the manual WS-COMMIT-THRESHOLD (1000) * commit logic in 2300-CHECK-COMMIT. + * + *

Other insert failures propagate as {@code DataAccessException} and are + * handled by the step's skip policy, which counts them toward WS-ERROR-COUNT + * like COBOL's DB2-ERROR-ROUTINE. Each item is flushed individually so a + * failure is attributed to the correct record, and the written counter is + * updated only after the chunk transaction commits so rolled-back chunks are + * not counted. */ @Component public class HistoryItemWriter implements ItemWriter { @@ -29,12 +38,20 @@ public HistoryItemWriter(PositionHistoryRepository repository, HistoryLoadStats @Override public void write(Chunk chunk) { + long written = 0; for (PositionHistory item : chunk) { if (repository.existsById(item.getKey())) { continue; } - repository.save(item); - stats.incrementRecordsWritten(); + repository.saveAndFlush(item); + written++; } + final long delta = written; + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + stats.addRecordsWritten(delta); + } + }); } } diff --git a/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java b/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java index a3fba24c..88c485cd 100644 --- a/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java +++ b/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java @@ -18,8 +18,11 @@ import org.springframework.batch.item.data.RepositoryItemReader; import org.springframework.batch.item.data.builder.RepositoryItemReaderBuilder; import org.springframework.context.annotation.Bean; +import org.springframework.batch.core.SkipListener; import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataAccessException; import org.springframework.data.domain.Sort; +import com.portfolio.common.ErrorHandlingService; import org.springframework.transaction.PlatformTransactionManager; import java.util.LinkedHashMap; @@ -80,12 +83,31 @@ public Step histld00Step(JobRepository jobRepository, HistoryItemProcessor processor, HistoryItemWriter writer, BatchControlService batchControlService, - HistoryLoadStats stats) { + HistoryLoadStats stats, + ErrorHandlingService errorHandlingService) { return new StepBuilder("histld00Step", jobRepository) .chunk(COMMIT_THRESHOLD, transactionManager) .reader(historyItemReader) .processor(processor) .writer(writer) + // COBOL 2200-LOAD-TO-DB2: an INSERT failure other than SQLCODE + // -803 increments WS-ERROR-COUNT and processing continues until + // the count exceeds 100 (WS-ERROR-COUNT > 100 abort). + .faultTolerant() + .processorNonTransactional() + .skipPolicy((throwable, skipCount) -> + throwable instanceof DataAccessException + && stats.getErrorCount() < HistoryLoadStats.MAX_ERRORS) + .listener(new SkipListener() { + @Override + public void onSkipInWrite(PositionHistory item, Throwable t) { + stats.incrementErrorCount(); + errorHandlingService.logError(PROGRAM_ID, "S", 3, "HIST0002", + "POSHIST insert failed: " + t.getMessage(), + String.valueOf(item.getKey().getAccountNo() + "/" + + item.getKey().getPortfolioId())); + } + }) .listener(new ChunkListener() { @Override public void afterChunk(ChunkContext context) { diff --git a/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java b/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java index 432555a0..840ba6b5 100644 --- a/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java +++ b/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java @@ -29,8 +29,8 @@ public long incrementRecordsRead() { return recordsRead.incrementAndGet(); } - public long incrementRecordsWritten() { - return recordsWritten.incrementAndGet(); + public long addRecordsWritten(long delta) { + return recordsWritten.addAndGet(delta); } public long incrementErrorCount() { diff --git a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java index f7c13142..a408c006 100644 --- a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java +++ b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java @@ -18,6 +18,7 @@ import org.springframework.batch.core.launch.JobLauncher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; import java.math.BigDecimal; import java.time.LocalDate; @@ -42,6 +43,7 @@ class HistoryLoadJobTest { @Autowired private PositionHistoryRepository positionHistoryRepository; @Autowired private BatchControlRepository batchControlRepository; @Autowired private ErrorLogRepository errorLogRepository; + @Autowired private JdbcTemplate jdbcTemplate; @BeforeEach void setUp() { @@ -197,7 +199,7 @@ void abortsWhenErrorCountExceedsOneHundred() throws Exception { // every counted error has its own ERRLOG row (no key collisions) assertThat(errorLogRepository.count()).isEqualTo(101); assertThat(execution.getAllFailureExceptions()) - .anyMatch(e -> e instanceof ErrorLimitExceededException); + .anyMatch(HistoryLoadJobTest::causedByErrorLimit); BatchControl control = batchControlRepository .findById(new BatchControl.Key("HISTLD00", PROCESS_DATE, 1)).orElseThrow(); @@ -205,6 +207,46 @@ void abortsWhenErrorCountExceedsOneHundred() throws Exception { assertThat(control.getReturnCode()).isEqualTo(101); } + @Test + void countsDbInsertErrorsAndContinuesLikeDb2ErrorRoutine() throws Exception { + tranHistRepository.save(validRecord("PORT00001", "000001", LocalTime.of(9, 30))); + TransactionHistoryFileRecord bad = validRecord("PORT00002", "000002", LocalTime.of(10, 0)); + bad.setSecurityId("BADSEC"); + tranHistRepository.save(bad); + tranHistRepository.save(validRecord("PORT00003", "000003", LocalTime.of(10, 30))); + + // Simulate a DB2 insert failure (non-zero SQLCODE other than -803) + jdbcTemplate.execute( + "ALTER TABLE POSHIST ADD CONSTRAINT CHK_TEST_SEC CHECK (SECURITY_ID <> 'BADSEC')"); + try { + JobExecution execution = runJob(); + + assertThat(execution.getStatus()).isEqualTo(BatchStatus.COMPLETED); + assertThat(positionHistoryRepository.count()).isEqualTo(2); + assertThat(stats.getRecordsWritten()).isEqualTo(2); + assertThat(stats.getErrorCount()).isEqualTo(1); + assertThat(errorLogRepository.count()).isEqualTo(1); + assertThat(errorLogRepository.findAll().get(0).getErrorMessage()) + .contains("POSHIST insert failed"); + + BatchControl control = batchControlRepository + .findById(new BatchControl.Key("HISTLD00", PROCESS_DATE, 1)).orElseThrow(); + assertThat(control.getStatus()).isEqualTo("D"); + assertThat(control.getReturnCode()).isEqualTo(1); + } finally { + jdbcTemplate.execute("ALTER TABLE POSHIST DROP CONSTRAINT CHK_TEST_SEC"); + } + } + + private static boolean causedByErrorLimit(Throwable t) { + for (Throwable c = t; c != null; c = c.getCause()) { + if (c instanceof ErrorLimitExceededException) { + return true; + } + } + return false; + } + @Test void failsWhenControlRecordMissing() throws Exception { batchControlRepository.deleteAll(); From 52291b00232d53044621224924a19d627c237b01 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:17:59 +0000 Subject: [PATCH 5/7] Only count restarts after unclean runs; default ddl-auto to validate Co-Authored-By: Eashan Sinha --- .../main/java/com/portfolio/batch/BatchControlService.java | 6 +++++- .../main/java/com/portfolio/batch/HistoryLoadJobConfig.java | 2 ++ java/src/main/resources/application.yml | 6 +++++- .../test/java/com/portfolio/batch/HistoryLoadJobTest.java | 2 +- java/src/test/resources/application.properties | 1 + 5 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 java/src/test/resources/application.properties diff --git a/java/src/main/java/com/portfolio/batch/BatchControlService.java b/java/src/main/java/com/portfolio/batch/BatchControlService.java index d3aa1ef2..d872ae30 100644 --- a/java/src/main/java/com/portfolio/batch/BatchControlService.java +++ b/java/src/main/java/com/portfolio/batch/BatchControlService.java @@ -42,10 +42,14 @@ public BatchControlService(BatchControlRepository repository) { @Transactional(propagation = Propagation.REQUIRES_NEW) public BatchControl.Key markActive(String jobName, String processDate) { BatchControl control = find(jobName, processDate); + // Restart = previous attempt did not finish cleanly (still ACTIVE or ERROR) + if (BatchControlConstants.STAT_ACTIVE.equals(control.getStatus()) + || BatchControlConstants.STAT_ERROR.equals(control.getStatus())) { + control.setRestartCount(control.getRestartCount() + 1); + } control.setStatus(BatchControlConstants.STAT_ACTIVE); control.setStartTime(LocalTime.now().format(TIME_FMT)); control.setAttemptTimestamp(LocalDateTime.now().format(TS_FMT)); - control.setRestartCount(control.getRestartCount() + 1); repository.save(control); return control.getKey(); } diff --git a/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java b/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java index 88c485cd..51d02f9f 100644 --- a/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java +++ b/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java @@ -94,6 +94,8 @@ public Step histld00Step(JobRepository jobRepository, // -803 increments WS-ERROR-COUNT and processing continues until // the count exceeds 100 (WS-ERROR-COUNT > 100 abort). .faultTolerant() + // Required: the processor counts reads/errors, so it must not + // be re-run during the fault-tolerant chunk scan. .processorNonTransactional() .skipPolicy((throwable, skipCount) -> throwable instanceof DataAccessException diff --git a/java/src/main/resources/application.yml b/java/src/main/resources/application.yml index c1235f4b..7cf1b28a 100644 --- a/java/src/main/resources/application.yml +++ b/java/src/main/resources/application.yml @@ -7,7 +7,9 @@ spring: password: "" jpa: hibernate: - ddl-auto: create + # Safe default for a real (DB2) datasource; the histld00 sample profile + # and tests override this to create the embedded H2 schema. + ddl-auto: validate open-in-view: false sql: init: @@ -26,6 +28,8 @@ spring: activate: on-profile: histld00 jpa: + hibernate: + ddl-auto: create defer-datasource-initialization: true sql: init: diff --git a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java index a408c006..f960f8c8 100644 --- a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java +++ b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java @@ -121,7 +121,7 @@ void updatesBatchControlRecordWithCountersAndStatus() throws Exception { assertThat(control.getRecordsRead()).isEqualTo(1); assertThat(control.getRecordsWritten()).isEqualTo(1); assertThat(control.getReturnCode()).isZero(); - assertThat(control.getRestartCount()).isEqualTo(1); + assertThat(control.getRestartCount()).isZero(); } @Test diff --git a/java/src/test/resources/application.properties b/java/src/test/resources/application.properties new file mode 100644 index 00000000..530896fe --- /dev/null +++ b/java/src/test/resources/application.properties @@ -0,0 +1 @@ +spring.jpa.hibernate.ddl-auto=create From 7dda782583e3f3e6de61333e41a6b264c52c501f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:25:17 +0000 Subject: [PATCH 6/7] Count and log the insert error that breaches the 100-error limit Co-Authored-By: Eashan Sinha --- .../portfolio/batch/HistoryLoadJobConfig.java | 37 ++++++++++++++++++- .../com/portfolio/batch/HistoryLoadStats.java | 4 ++ .../portfolio/batch/HistoryLoadJobTest.java | 34 +++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java b/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java index 51d02f9f..2044174a 100644 --- a/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java +++ b/java/src/main/java/com/portfolio/batch/HistoryLoadJobConfig.java @@ -14,6 +14,9 @@ import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.ChunkListener; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.item.data.RepositoryItemReader; import org.springframework.batch.item.data.builder.RepositoryItemReaderBuilder; @@ -97,19 +100,40 @@ public Step histld00Step(JobRepository jobRepository, // Required: the processor counts reads/errors, so it must not // be re-run during the fault-tolerant chunk scan. .processorNonTransactional() + // skipCount is Spring Batch's live write-skip counter, so the + // WS-ERROR-COUNT > 100 limit combines validation errors (in + // stats) with insert failures as they happen. During the run, + // stats.errorCount holds validation errors only; write skips + // are folded in once in afterStep below. .skipPolicy((throwable, skipCount) -> throwable instanceof DataAccessException - && stats.getErrorCount() < HistoryLoadStats.MAX_ERRORS) + && stats.getErrorCount() + skipCount < HistoryLoadStats.MAX_ERRORS) .listener(new SkipListener() { @Override public void onSkipInWrite(PositionHistory item, Throwable t) { - stats.incrementErrorCount(); errorHandlingService.logError(PROGRAM_ID, "S", 3, "HIST0002", "POSHIST insert failed: " + t.getMessage(), String.valueOf(item.getKey().getAccountNo() + "/" + item.getKey().getPortfolioId())); } }) + .listener(new StepExecutionListener() { + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + stats.addErrorCount(stepExecution.getWriteSkipCount()); + // The insert failure that breaches the limit is not + // skipped (it aborts the step); count and log it once + // here so RETURN-CODE is 101 like the validation path. + if (stepExecution.getStatus() == BatchStatus.FAILED + && stepExecution.getFailureExceptions().stream() + .anyMatch(HistoryLoadJobConfig::causedByDataAccess)) { + stats.incrementErrorCount(); + errorHandlingService.logError(PROGRAM_ID, "S", 3, "HIST0002", + "POSHIST insert failed: error limit exceeded", PROGRAM_ID); + } + return stepExecution.getExitStatus(); + } + }) .listener(new ChunkListener() { @Override public void afterChunk(ChunkContext context) { @@ -125,6 +149,15 @@ public void afterChunk(ChunkContext context) { .build(); } + private static boolean causedByDataAccess(Throwable t) { + for (Throwable c = t; c != null; c = c.getCause()) { + if (c instanceof DataAccessException) { + return true; + } + } + return false; + } + @Bean public Job histld00Job(JobRepository jobRepository, Step histld00Step, diff --git a/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java b/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java index 840ba6b5..130fa6ff 100644 --- a/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java +++ b/java/src/main/java/com/portfolio/batch/HistoryLoadStats.java @@ -33,6 +33,10 @@ public long addRecordsWritten(long delta) { return recordsWritten.addAndGet(delta); } + public long addErrorCount(long delta) { + return errorCount.addAndGet(delta); + } + public long incrementErrorCount() { return errorCount.incrementAndGet(); } diff --git a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java index f960f8c8..119b9ea3 100644 --- a/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java +++ b/java/src/test/java/com/portfolio/batch/HistoryLoadJobTest.java @@ -238,6 +238,40 @@ void countsDbInsertErrorsAndContinuesLikeDb2ErrorRoutine() throws Exception { } } + @Test + void abortsWithSameReturnCodeWhenInsertErrorsExceedOneHundred() throws Exception { + for (int i = 0; i < 105; i++) { + TransactionHistoryFileRecord bad = validRecord("PORT00001", + String.format("%06d", i + 1), + LocalTime.of(9, 0).plusSeconds(i)); + bad.setSecurityId("BADSEC"); + tranHistRepository.save(bad); + } + + jdbcTemplate.execute( + "ALTER TABLE POSHIST ADD CONSTRAINT CHK_TEST_SEC CHECK (SECURITY_ID <> 'BADSEC')"); + try { + JobExecution execution = runJob(); + + assertThat(execution.getStatus()).isEqualTo(BatchStatus.FAILED); + // same RETURN-CODE as the validation-error abort path + assertThat(stats.getErrorCount()).isEqualTo(101); + // per-item logs from the aborted chunk roll back with it (as in + // COBOL, where uncommitted ERRLOG inserts are lost on ROLLBACK); + // the aborting error itself is logged in its own transaction + assertThat(errorLogRepository.count()).isEqualTo(1); + assertThat(errorLogRepository.findAll().get(0).getErrorMessage()) + .contains("error limit exceeded"); + + BatchControl control = batchControlRepository + .findById(new BatchControl.Key("HISTLD00", PROCESS_DATE, 1)).orElseThrow(); + assertThat(control.getStatus()).isEqualTo("E"); + assertThat(control.getReturnCode()).isEqualTo(101); + } finally { + jdbcTemplate.execute("ALTER TABLE POSHIST DROP CONSTRAINT CHK_TEST_SEC"); + } + } + private static boolean causedByErrorLimit(Throwable t) { for (Throwable c = t; c != null; c = c.getCause()) { if (c instanceof ErrorLimitExceededException) { From ed7214a643c855ef4bd952d54af947e0df2cdf25 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:36:41 +0000 Subject: [PATCH 7/7] Truncate USER_ID to CHAR(8) like COBOL PIC X(8); fix stale Javadoc link Co-Authored-By: Eashan Sinha --- .../java/com/portfolio/batch/HistoryItemProcessor.java | 8 +++++++- .../java/com/portfolio/common/ErrorHandlingService.java | 3 ++- .../java/com/portfolio/model/copybook/SqlStatusCodes.java | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/java/src/main/java/com/portfolio/batch/HistoryItemProcessor.java b/java/src/main/java/com/portfolio/batch/HistoryItemProcessor.java index d9093b6d..e30bd1b0 100644 --- a/java/src/main/java/com/portfolio/batch/HistoryItemProcessor.java +++ b/java/src/main/java/com/portfolio/batch/HistoryItemProcessor.java @@ -70,11 +70,17 @@ public PositionHistory process(TransactionHistoryFileRecord item) { history.setProcessDate(now.toLocalDate()); history.setProcessTime(now.toLocalTime()); history.setProgramId("HISTLD00"); - history.setUserId(System.getProperty("user.name", "BATCH")); + history.setUserId(currentUserId()); history.setAuditTimestamp(now); return history; } + /** USER_ID is CHAR(8); COBOL PIC X(8) truncated silently. */ + private static String currentUserId() { + String user = System.getProperty("user.name", "BATCH"); + return user.length() <= 8 ? user : user.substring(0, 8); + } + private String validate(TransactionHistoryFileRecord item) { if (item.getKey() == null || item.getKey().getTransDate() == null diff --git a/java/src/main/java/com/portfolio/common/ErrorHandlingService.java b/java/src/main/java/com/portfolio/common/ErrorHandlingService.java index 5185fa80..cb79dfd2 100644 --- a/java/src/main/java/com/portfolio/common/ErrorHandlingService.java +++ b/java/src/main/java/com/portfolio/common/ErrorHandlingService.java @@ -60,7 +60,8 @@ public int logError(String programId, String errorType, int severity, entry.setErrorMessage(truncate(message, 200)); entry.setProcessDate(now.toLocalDate()); entry.setProcessTime(now.toLocalTime()); - entry.setUserId(System.getProperty("user.name", "BATCH")); + // USER_ID is CHAR(8); COBOL PIC X(8) truncated silently + entry.setUserId(truncate(System.getProperty("user.name", "BATCH"), 8)); entry.setAdditionalInfo(truncate(details, 500)); errorLogRepository.save(entry); diff --git a/java/src/main/java/com/portfolio/model/copybook/SqlStatusCodes.java b/java/src/main/java/com/portfolio/model/copybook/SqlStatusCodes.java index fa071f48..d9f31817 100644 --- a/java/src/main/java/com/portfolio/model/copybook/SqlStatusCodes.java +++ b/java/src/main/java/com/portfolio/model/copybook/SqlStatusCodes.java @@ -5,7 +5,7 @@ * *

The SQLCA itself (SQLCODE/SQLSTATE communication area) has no direct Java * equivalent: SQL error signalling is replaced by exceptions - * ({@code DataAccessException} hierarchy / {@link com.portfolio.common.DatabaseException}). + * ({@code DataAccessException} hierarchy / {@link com.portfolio.common.SqlProcessingException}). * The well-known SQLSTATE values checked by the COBOL programs are preserved here. */ public final class SqlStatusCodes {