Skip to content

feat(crecust): translate CRECUST to Java#9

Merged
a2chang merged 3 commits intomainfrom
migrate/CRECUST
May 1, 2026
Merged

feat(crecust): translate CRECUST to Java#9
a2chang merged 3 commits intomainfrom
migrate/CRECUST

Conversation

@a2chang
Copy link
Copy Markdown
Contributor

@a2chang a2chang commented May 1, 2026

Source program: https://github.com/augment-solutions/cics-banking-sample-application-cbsa/blob/main/src/base/cobol_src/CRECUST.cbl

Mapped REST endpoints

  • POST /api/v1/crecust/insert

Notable decisions

  • Modeled the COBOL named-counter flow with SELECT ... FOR UPDATE on CONTROL, an optimistic UPDATE, and a Cockroach retry helper around the transaction.
  • Added strict ddMMuuuu date parsing so impossible calendar dates fail deterministically; kept COBOL-style DOB bounds and introduced fail code Z for invalid calendar dates.
  • Implemented the five credit-agency calls asynchronously on a virtual-thread executor and return fail code G when all agencies fail or time out.
  • Added a Flyway migration to back PROCTRAN.COUNTER with a sequence-backed default so audit inserts stay database-managed.
  • Reused the shared RFC-7807 ProblemDetail error style and typed business exceptions while redacting sensitive abend details from API responses.

COBOL fail-code mappings

  • T: Invalid Title
  • G: Credit check error/timeout
  • 3: Named counter ENQ failure
  • 5: Named counter READ failure
  • A-D: Async child process failures
  • E: Customer READ failure
  • F: Customer REWRITE failure
  • H: Proctran write failure
  • Z: Invalid Date of Birth (Java-specific addition)

Test coverage

  • Unit: CrecustServiceUnitTest
  • @SpringBootTest: CrecustServiceIntegrationTest
  • MockMvc: CrecustControllerWebMvcTest

@a2chang
Copy link
Copy Markdown
Contributor Author

a2chang commented May 1, 2026

augment review

@augmentcode
Copy link
Copy Markdown
Contributor

augmentcode Bot commented May 1, 2026

Test Coverage Guardian 🧪

Test coverage looks good. The new/changed behavior in this PR has adequate test coverage. No additional tests needed.

This PR includes:

  • CrecustServiceUnitTest: Comprehensive unit tests covering validation logic, error paths, and happy path scenarios
  • CrecustServiceIntegrationTest: End-to-end integration tests with real database interactions
  • CrecustControllerWebMvcTest: Web layer tests covering HTTP API, error handling, and security concerns (exception redaction)

All major code paths, edge cases, and error scenarios are properly tested.

Copy link
Copy Markdown

@augment-app-staging augment-app-staging Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 4 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

.forUpdate()
.fetchOne();
} catch (DataAccessException exception) {
throw rollbackFailure("3", "Unable to reserve the next customer number.");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching DataAccessException here and converting it into a business failure will also swallow Cockroach serialization failures (SQLSTATE 40001), preventing CrdbRetry.run(...) from retrying the transaction under contention.

Severity: high

Other Locations
  • src/main/java/com/augment/cbsa/repository/CrecustRepository.java:95
  • src/main/java/com/augment/cbsa/repository/CrecustRepository.java:108
  • src/main/java/com/augment/cbsa/repository/CrecustRepository.java:125

🤖 Was this useful? React with 👍 or 👎

int returnedScores = 0;
for (CompletableFuture<Optional<Integer>> future : futures) {
try {
Optional<Integer> maybeScore = future.join();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using future.join() with no timeout means one stuck credit-agency call can hang the entire request even though individual failures are otherwise tolerated.

Severity: medium

🤖 Was this useful? React with 👍 or 👎

customer.creditScore(),
toCobolDate(customer.csReviewDate()),
"Y",
""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The success response sets CommFailCode to an empty string, but this field looks like a 1-character commarea flag (often a space when unused); returning "" may break clients expecting a single-char value.

Severity: medium

🤖 Was this useful? React with 👍 or 👎

void createsCustomerAuditTrailAndControlRow() {
when(reviewDateRandom.nextInt(20)).thenReturn(3);

LocalDate today = LocalDate.now(java.time.Clock.systemUTC());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test computes today using Clock.systemUTC() separately from the service's injected Clock, so it can flake if the date rolls over between the two calls (e.g., around midnight).

Severity: low

🤖 Was this useful? React with 👍 or 👎

Copy link
Copy Markdown

@augment-app-staging augment-app-staging Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 4 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

int returnedScores = 0;
for (CompletableFuture<Optional<Integer>> future : futures) {
try {
Optional<Integer> maybeScore = future.join();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evaluateCredit() uses future.join() without any timeout handling; if a credit-agency call never completes, the request can block indefinitely and never return fail code G. Consider ensuring credit checks complete within a bounded time.

Severity: medium

🤖 Was this useful? React with 👍 or 👎

}

private String firstToken(String name) {
if (name.isEmpty() || Character.isWhitespace(name.charAt(0))) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

firstToken() returns "" when the name begins with whitespace, which means " Alice Example" bypasses title validation (empty title is allowed) and will be treated as valid. This looks like an input-validation bypass that could allow invalid titles/names to be persisted.

Severity: medium

🤖 Was this useful? React with 👍 or 👎

}

private CrecustResult invalidDateFailure(int cobolDate) {
int year = Integer.parseInt(String.format("%08d", cobolDate).substring(4));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invalidDateFailure() derives the year via String.format(...).substring(4) and Integer.parseInt(...); for negative cobolDate values this can throw NumberFormatException and turn a validation issue into a 500. Even if the web layer validates non-negative dates, this method is reachable from direct service calls/tests.

Severity: low

🤖 Was this useful? React with 👍 or 👎

) {

private static final Set<String> VALIDATION_FAIL_CODES = Set.of("T", "O", "Y", "Z");
private static final Set<String> CREDIT_FAIL_CODES = Set.of("A", "B", "C", "D", "E", "F", "G", "H");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CREDIT_FAIL_CODES includes E, F, and H, which appear non-credit-related and are treated as “credit failures” (HTTP 503 / title “Credit check unavailable”) via isCreditFailure(). If those codes are ever returned for persistence/audit failures, clients will receive a misleading 503 rather than an internal error.

Severity: medium

Other Locations
  • src/main/java/com/augment/cbsa/web/crecust/CrecustController.java:58
  • src/main/java/com/augment/cbsa/web/crecust/CrecustController.java:77

🤖 Was this useful? React with 👍 or 👎

… recategorize fail codes

- evaluateCredit: future.get(timeout) per agency so a hung credit-agency
  call cannot block the request indefinitely; cancel on timeout/interrupt.
- firstToken: stripLeading before tokenizing so a name with leading
  whitespace cannot bypass title validation by appearing to have an
  empty (no-title) prefix.
- invalidDateFailure: derive year via abs(cobolDate) % 10000 so negative
  or out-of-range cobolDate inputs cannot turn a validation issue into
  a 500.
- CrecustResult.CREDIT_FAIL_CODES: drop E (customer READ), F (customer
  REWRITE) and H (proctran write) — those are persistence failures and
  must fall through to 5xx, not be reported as 503 "Credit check
  unavailable". Mirror the change in CrecustController.failureTitle.
@a2chang
Copy link
Copy Markdown
Contributor Author

a2chang commented May 1, 2026

augment review

Copy link
Copy Markdown

@augment-app-staging augment-app-staging Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 4 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

return CrecustResult.failure("G", "Credit check could not be completed.");
}

Instant now = Instant.now(clock);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create() reads the injected clock multiple times (e.g., LocalDate.now(clock) and later Instant.now(clock)), so around midnight it can validate against one day but persist transactionDate/reviewDate using the next day. Consider deriving today/reviewDate from the same captured instant to avoid rare off-by-one-day behavior.

Severity: low

Other Locations
  • src/main/java/com/augment/cbsa/service/CrecustService.java:130
  • src/main/java/com/augment/cbsa/service/CrecustService.java:175

🤖 Was this useful? React with 👍 or 👎

} catch (ExecutionException | CompletionException exception) {
// Ignore individual credit-agency failures and average only successful replies.
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In evaluateCredit(), after catching InterruptedException the thread is re-interrupted but processing continues; if some scores were already collected, the request can still proceed to create a customer even though cancellation was signaled. Consider treating interruption as an immediate overall credit-check failure (or propagating) to avoid persisting work on interrupted threads.

Severity: medium

🤖 Was this useful? React with 👍 or 👎

@@ -0,0 +1,4 @@
CREATE SEQUENCE IF NOT EXISTS proctran_counter_seq;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration creates proctran_counter_seq but doesn’t initialize it to at least max(proctran.counter)+1, so applying it to a database with existing proctran rows can cause default counter values to collide with existing primary keys. Consider bumping/initializing the sequence during migration to make it safe on non-empty environments.

Severity: medium

🤖 Was this useful? React with 👍 or 👎


private String toDescription(CustomerDetails customer) {
return customer.sortcode()
+ String.format("%010d", customer.customerNumber())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toDescription() assumes customerNumber fits in 10 digits via %010d; if customerNumber ever exceeds 9_999_999_999, the formatted string will exceed the intended 40-char proctran.description layout and may fail inserts. Consider enforcing the 10-digit bound when allocating customer numbers to keep the audit description length stable.

Severity: low

🤖 Was this useful? React with 👍 or 👎

…quence init, customer-number bound

- CrecustService.create: capture Instant/today once at the start and
  thread today through validateDateOfBirth and evaluateCredit so all
  date/time fields derive from the same instant. Avoids rare
  off-by-one-day audit records when the request straddles midnight.
- CrecustService.evaluateCredit: on InterruptedException, set an
  'interrupted' flag, restore the interrupt and short-circuit the
  remaining futures, then return null so we never persist a customer
  on a thread where cancellation was signaled.
- V1__proctran_counter_default.sql: after creating proctran_counter_seq,
  setval(seq, max(counter)+1, false) so the DEFAULT cannot collide with
  existing PROCTRAN rows on a non-empty database while preserving the
  starting value of 1 on an empty table.
- CrecustRepository: enforce 10-digit MAX_CUSTOMER_NUMBER bound when
  allocating the next customer number so the %010d field in the
  PROCTRAN.DESCRIPTION layout cannot silently overflow.
@a2chang
Copy link
Copy Markdown
Contributor Author

a2chang commented May 1, 2026

augment review

Copy link
Copy Markdown

@augment-app-staging augment-app-staging Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

private static final int REVIEW_DATE_BOUND = 20;
private static final long CREDIT_AGENCY_TIMEOUT_SECONDS = 6;
private static final List<String> VALID_TITLES = List.of(
"Professor", "Mr", "Mrs", "Miss", "Ms", "Dr", "Drs", "Lord", "Sir", "Lady", ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including "" in VALID_TITLES means an empty/whitespace-only CommName can pass title validation; since the request DTOs only enforce @NotNull (not non-blank), this could allow persisting customers with blank names.

Severity: medium

🤖 Was this useful? React with 👍 or 👎

CustomerDetails customer = Objects.requireNonNull(result.customer(), "Successful response requires a customer");
return new CrecustResponseDto(new CrecustCommareaResponseDto(
EYE_CATCHER,
new CrecustKeyDto(Integer.parseInt(customer.sortcode()), customer.customerNumber()),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integer.parseInt(customer.sortcode()) will throw at runtime if cbsa.sortcode is ever misconfigured/non-numeric, and it will also drop any leading zeros that might be significant for a 6-digit sortcode.

Severity: medium

🤖 Was this useful? React with 👍 or 👎

.set(PROCTRAN.AMOUNT, new BigDecimal("0.00"))
.execute();
} catch (DataAccessException exception) {
throw new com.augment.cbsa.error.CbsaAbendException(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This maps PROCTRAN insert failures to a CbsaAbendException, but elsewhere (e.g., CrecustResult’s documented fail codes) persistence failures appear intended to surface as a CrecustResult fail code (like H). If clients rely on fail codes for audit-write failures, throwing an abend here may bypass that contract.

Severity: medium

🤖 Was this useful? React with 👍 or 👎

@a2chang
Copy link
Copy Markdown
Contributor Author

a2chang commented May 1, 2026

Auto-review cap reached (3 rounds). Remaining 3 medium suggestions tracked as #10, #11, #12. Merging.

@a2chang a2chang merged commit a3dced2 into main May 1, 2026
1 check passed
@a2chang a2chang deleted the migrate/CRECUST branch May 1, 2026 07:51
@a2chang a2chang mentioned this pull request May 1, 2026
22 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant