-
Notifications
You must be signed in to change notification settings - Fork 0
migrate(DELACC): translate DELACC to Java #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package com.augment.cbsa.domain; | ||
|
|
||
| public record DelaccRequest(long accountNumber) { | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package com.augment.cbsa.domain; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| public record DelaccResult( | ||
| boolean deleteSuccess, | ||
| String failCode, | ||
| long accountNumber, | ||
| AccountDetails account, | ||
| String message | ||
| ) { | ||
|
|
||
| public DelaccResult { | ||
| Objects.requireNonNull(failCode, "failCode must not be null"); | ||
|
|
||
| if (deleteSuccess && account == null) { | ||
| throw new IllegalArgumentException("Successful results must include an account"); | ||
| } | ||
| if (!deleteSuccess && account != null) { | ||
| throw new IllegalArgumentException("Failure results must not include an account"); | ||
| } | ||
| if (!deleteSuccess && (message == null || message.isBlank())) { | ||
| throw new IllegalArgumentException("Failure results must include a non-blank message"); | ||
| } | ||
| } | ||
|
|
||
| public static DelaccResult success(AccountDetails account) { | ||
| Objects.requireNonNull(account, "account must not be null"); | ||
| return new DelaccResult(true, " ", account.accountNumber(), account, null); | ||
| } | ||
|
|
||
| public static DelaccResult failure(String failCode, long accountNumber, String message) { | ||
| Objects.requireNonNull(failCode, "failCode must not be null"); | ||
| Objects.requireNonNull(message, "message must not be null"); | ||
| return new DelaccResult(false, failCode, accountNumber, null, message); | ||
| } | ||
|
|
||
| public boolean isNotFoundFailure() { | ||
| return !deleteSuccess && "1".equals(failCode); | ||
| } | ||
| } |
95 changes: 95 additions & 0 deletions
95
src/main/java/com/augment/cbsa/repository/DelaccRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package com.augment.cbsa.repository; | ||
|
|
||
| import com.augment.cbsa.domain.AccountDetails; | ||
| import com.augment.cbsa.jooq.tables.records.AccountRecord; | ||
| import java.time.LocalDate; | ||
| import java.time.LocalTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.Locale; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import org.jooq.DSLContext; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import static com.augment.cbsa.jooq.Tables.ACCOUNT; | ||
| import static com.augment.cbsa.jooq.Tables.PROCTRAN; | ||
|
|
||
| @Repository | ||
| public class DelaccRepository { | ||
|
|
||
| private static final DateTimeFormatter ACCOUNT_AUDIT_DATE_FORMATTER = DateTimeFormatter.ofPattern("ddMMyyyy", Locale.ROOT); | ||
|
|
||
| private final DSLContext dsl; | ||
|
|
||
| public DelaccRepository(DSLContext dsl) { | ||
| this.dsl = Objects.requireNonNull(dsl, "dsl must not be null"); | ||
| } | ||
|
|
||
| public Optional<AccountDetails> findBySortcodeAndAccountNumberForUpdate(String sortcode, long accountNumber) { | ||
| return dsl.selectFrom(ACCOUNT) | ||
| .where(ACCOUNT.SORTCODE.eq(sortcode)) | ||
| .and(ACCOUNT.ACCOUNT_NUMBER.eq(accountNumber)) | ||
| .forUpdate() | ||
| .fetchOptional(this::toDomain); | ||
| } | ||
|
|
||
| public int deleteAccount(String sortcode, long accountNumber) { | ||
| return dsl.deleteFrom(ACCOUNT) | ||
| .where(ACCOUNT.SORTCODE.eq(sortcode)) | ||
| .and(ACCOUNT.ACCOUNT_NUMBER.eq(accountNumber)) | ||
| .execute(); | ||
| } | ||
|
|
||
| public void insertAccountDeletionAudit( | ||
| AccountDetails account, | ||
| long transactionReference, | ||
| LocalDate transactionDate, | ||
| LocalTime transactionTime | ||
| ) { | ||
| Objects.requireNonNull(account, "account must not be null"); | ||
| Objects.requireNonNull(transactionDate, "transactionDate must not be null"); | ||
| Objects.requireNonNull(transactionTime, "transactionTime must not be null"); | ||
|
|
||
| dsl.insertInto(PROCTRAN) | ||
| .set(PROCTRAN.SORTCODE, account.sortcode()) | ||
| .set(PROCTRAN.LOGICAL_DELETE, false) | ||
| .set(PROCTRAN.TRAN_DATE, transactionDate) | ||
| .set(PROCTRAN.TRAN_TIME, transactionTime) | ||
| .set(PROCTRAN.TRAN_REF, transactionReference) | ||
| .set(PROCTRAN.TRAN_TYPE, "ODA") | ||
| .set(PROCTRAN.DESCRIPTION, toAccountDeletionDescription(account)) | ||
| .set(PROCTRAN.AMOUNT, account.actualBalance()) | ||
| .execute(); | ||
| } | ||
|
|
||
| private AccountDetails toDomain(AccountRecord record) { | ||
| return new AccountDetails( | ||
| record.getSortcode(), | ||
| record.getCustomerNumber(), | ||
| record.getAccountNumber(), | ||
| record.getAccountType(), | ||
| record.getInterestRate(), | ||
| record.getOpened(), | ||
| record.getOverdraftLimit(), | ||
| record.getLastStmtDate(), | ||
| record.getNextStmtDate(), | ||
| record.getAvailableBalance(), | ||
| record.getActualBalance() | ||
| ); | ||
| } | ||
|
|
||
| private String toAccountDeletionDescription(AccountDetails account) { | ||
| return String.format( | ||
| Locale.ROOT, | ||
| "%010d%-8.8s%s%sDELETE", | ||
| account.customerNumber(), | ||
| account.accountType(), | ||
| toCobolDate(account.lastStatementDate()), | ||
| toCobolDate(account.nextStatementDate()) | ||
| ); | ||
| } | ||
|
|
||
| private String toCobolDate(LocalDate date) { | ||
| return date == null ? "00000000" : date.format(ACCOUNT_AUDIT_DATE_FORMATTER); | ||
| } | ||
| } |
137 changes: 137 additions & 0 deletions
137
src/main/java/com/augment/cbsa/service/DelaccService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| package com.augment.cbsa.service; | ||
|
|
||
| import com.augment.cbsa.domain.AccountDetails; | ||
| import com.augment.cbsa.domain.DelaccRequest; | ||
| import com.augment.cbsa.domain.DelaccResult; | ||
| import com.augment.cbsa.error.CbsaAbendException; | ||
| import com.augment.cbsa.repository.CrdbRetry; | ||
| import com.augment.cbsa.repository.DelaccRepository; | ||
| import java.sql.SQLException; | ||
| import java.time.Clock; | ||
| import java.time.Instant; | ||
| import java.time.LocalDate; | ||
| import java.time.LocalTime; | ||
| import java.time.ZoneOffset; | ||
| import java.time.temporal.ChronoUnit; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import org.jooq.DSLContext; | ||
| import org.jooq.exception.DataAccessException; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.support.TransactionTemplate; | ||
|
|
||
| @Service | ||
| public class DelaccService { | ||
|
|
||
| private static final String NOT_FOUND_CODE = "1"; | ||
| private static final String DELETE_FAILURE_CODE = "3"; | ||
| private static final String READ_ABEND_CODE = "HRAC"; | ||
| private static final String PROCTRAN_ABEND_CODE = "HWPT"; | ||
|
|
||
| private final DelaccRepository delaccRepository; | ||
| private final DSLContext dsl; | ||
| private final TransactionTemplate transactionTemplate; | ||
| private final String sortcode; | ||
| private final Clock clock; | ||
|
|
||
| public DelaccService( | ||
| DelaccRepository delaccRepository, | ||
| DSLContext dsl, | ||
| TransactionTemplate transactionTemplate, | ||
| @Value("${cbsa.sortcode}") String sortcode, | ||
| Clock clock | ||
| ) { | ||
| this.delaccRepository = Objects.requireNonNull(delaccRepository, "delaccRepository must not be null"); | ||
| this.dsl = Objects.requireNonNull(dsl, "dsl must not be null"); | ||
| this.transactionTemplate = Objects.requireNonNull(transactionTemplate, "transactionTemplate must not be null"); | ||
| this.sortcode = Objects.requireNonNull(sortcode, "sortcode must not be null"); | ||
| this.clock = Objects.requireNonNull(clock, "clock must not be null"); | ||
| } | ||
|
|
||
| public DelaccResult delete(DelaccRequest request) { | ||
| Objects.requireNonNull(request, "request must not be null"); | ||
|
|
||
| return Objects.requireNonNull( | ||
| CrdbRetry.run(dsl, () -> transactionTemplate.execute(status -> deleteWithinTransaction(request))), | ||
| "transactionTemplate returned null result" | ||
| ); | ||
| } | ||
|
|
||
| private DelaccResult deleteWithinTransaction(DelaccRequest request) { | ||
| Optional<AccountDetails> account; | ||
| try { | ||
| account = delaccRepository.findBySortcodeAndAccountNumberForUpdate(sortcode, request.accountNumber()); | ||
| } catch (DataAccessException exception) { | ||
| if (isSerializationFailure(exception)) { | ||
| throw exception; | ||
| } | ||
| throw new CbsaAbendException(READ_ABEND_CODE, "DELACC failed to read the account data.", exception); | ||
| } | ||
|
|
||
| if (account.isEmpty()) { | ||
| return DelaccResult.failure( | ||
| NOT_FOUND_CODE, | ||
| request.accountNumber(), | ||
| "Account number %d was not found.".formatted(request.accountNumber()) | ||
| ); | ||
| } | ||
|
|
||
| AccountDetails accountDetails = account.get(); | ||
|
|
||
| try { | ||
| int deletedRows = delaccRepository.deleteAccount(sortcode, request.accountNumber()); | ||
| if (deletedRows == 0) { | ||
| return deleteFailure(request.accountNumber()); | ||
| } | ||
| if (deletedRows > 1) { | ||
| // Unique (sortcode, account_number) makes >1 impossible; throw to roll back | ||
| // any unexpected over-delete and fail loudly instead of committing silently. | ||
| throw new CbsaAbendException(PROCTRAN_ABEND_CODE, | ||
| "DELACC unexpectedly deleted %d rows for account %d.".formatted(deletedRows, request.accountNumber())); | ||
| } | ||
| } catch (DataAccessException exception) { | ||
| if (isSerializationFailure(exception)) { | ||
| throw exception; | ||
| } | ||
| return deleteFailure(request.accountNumber()); | ||
| } | ||
|
|
||
| Instant now = Instant.now(clock); | ||
|
|
||
| try { | ||
| delaccRepository.insertAccountDeletionAudit( | ||
| accountDetails, | ||
| Math.max(0L, now.toEpochMilli()), | ||
| LocalDate.ofInstant(now, ZoneOffset.UTC), | ||
| LocalTime.ofInstant(now, ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS) | ||
| ); | ||
| } catch (DataAccessException exception) { | ||
| if (isSerializationFailure(exception)) { | ||
| throw exception; | ||
| } | ||
| throw new CbsaAbendException(PROCTRAN_ABEND_CODE, "DELACC failed to write the account deletion audit trail.", exception); | ||
| } | ||
|
|
||
| return DelaccResult.success(accountDetails); | ||
| } | ||
|
|
||
| private DelaccResult deleteFailure(long accountNumber) { | ||
| return DelaccResult.failure( | ||
| DELETE_FAILURE_CODE, | ||
| accountNumber, | ||
| "Account number %d could not be deleted.".formatted(accountNumber) | ||
| ); | ||
| } | ||
|
|
||
| private static boolean isSerializationFailure(Throwable throwable) { | ||
| Throwable current = throwable; | ||
| while (current != null) { | ||
| if (current instanceof SQLException sqlException && "40001".equals(sqlException.getSQLState())) { | ||
| return true; | ||
| } | ||
| current = current.getCause(); | ||
| } | ||
| return false; | ||
| } | ||
| } |
114 changes: 114 additions & 0 deletions
114
src/main/java/com/augment/cbsa/web/delacc/DelaccController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| package com.augment.cbsa.web.delacc; | ||
|
|
||
| import com.augment.cbsa.domain.AccountDetails; | ||
| import com.augment.cbsa.domain.DelaccRequest; | ||
| import com.augment.cbsa.domain.DelaccResult; | ||
| import com.augment.cbsa.service.DelaccService; | ||
| import com.augment.cbsa.web.delacc.dto.DelaccCommareaResponseDto; | ||
| import com.augment.cbsa.web.delacc.dto.DelaccRequestDto; | ||
| import com.augment.cbsa.web.delacc.dto.DelaccResponseDto; | ||
| import jakarta.validation.Valid; | ||
| import jakarta.validation.constraints.Max; | ||
| import jakarta.validation.constraints.PositiveOrZero; | ||
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.Locale; | ||
| import java.util.Objects; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ProblemDetail; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.validation.annotation.Validated; | ||
| import org.springframework.web.bind.annotation.DeleteMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @Validated | ||
| @RequestMapping("/api/v1/delacc") | ||
| public class DelaccController { | ||
|
|
||
| private static final String EYE_CATCHER = "ACCT"; | ||
| private static final DateTimeFormatter COBOL_DATE_FORMATTER = DateTimeFormatter.ofPattern("ddMMyyyy", Locale.ROOT); | ||
|
|
||
| private final DelaccService delaccService; | ||
|
|
||
| public DelaccController(DelaccService delaccService) { | ||
| this.delaccService = Objects.requireNonNull(delaccService, "delaccService must not be null"); | ||
| } | ||
|
|
||
| @DeleteMapping("/remove/{accno}") | ||
| public ResponseEntity<?> delete( | ||
| @PathVariable("accno") | ||
| @PositiveOrZero | ||
| @Max(99_999_999L) | ||
| long accountNumber, | ||
| @Valid @RequestBody DelaccRequestDto requestDto | ||
| ) { | ||
| Objects.requireNonNull(requestDto, "requestDto must not be null"); | ||
|
|
||
| DelaccResult result = delaccService.delete(new DelaccRequest(accountNumber)); | ||
| if (!result.deleteSuccess()) { | ||
| return ResponseEntity.status(failureStatus(result)).body(failureBody(result)); | ||
| } | ||
|
|
||
| return ResponseEntity.ok(toResponse(result)); | ||
| } | ||
|
|
||
| private HttpStatus failureStatus(DelaccResult result) { | ||
| if (result.isNotFoundFailure()) { | ||
| return HttpStatus.NOT_FOUND; | ||
| } | ||
| return HttpStatus.INTERNAL_SERVER_ERROR; | ||
| } | ||
|
|
||
| private ProblemDetail failureBody(DelaccResult result) { | ||
| HttpStatus status = failureStatus(result); | ||
| ProblemDetail problemDetail = ProblemDetail.forStatus(status); | ||
| problemDetail.setTitle(failureTitle(result)); | ||
| problemDetail.setDetail(result.message()); | ||
| problemDetail.setProperty("failCode", result.failCode()); | ||
| return problemDetail; | ||
| } | ||
|
|
||
| private String failureTitle(DelaccResult result) { | ||
| if (result.isNotFoundFailure()) { | ||
| return "Account not found"; | ||
| } | ||
| return "Account deletion failed"; | ||
| } | ||
|
|
||
| private DelaccResponseDto toResponse(DelaccResult result) { | ||
| AccountDetails account = Objects.requireNonNull(result.account(), "Successful response requires an account"); | ||
| return new DelaccResponseDto(new DelaccCommareaResponseDto( | ||
| EYE_CATCHER, | ||
| String.format(Locale.ROOT, "%010d", account.customerNumber()), | ||
| account.sortcode(), | ||
| account.accountNumber(), | ||
| account.accountType(), | ||
| account.interestRate(), | ||
| toCobolDate(account.opened()), | ||
| account.overdraftLimit().longValueExact(), | ||
| toCobolDate(account.lastStatementDate()), | ||
| toCobolDate(account.nextStatementDate()), | ||
| account.availableBalance(), | ||
| account.actualBalance(), | ||
| "Y", | ||
| "0", | ||
| "Y", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "" | ||
| )); | ||
| } | ||
|
|
||
| private int toCobolDate(LocalDate date) { | ||
| if (date == null) { | ||
| return 0; | ||
| } | ||
| return Integer.parseInt(date.format(COBOL_DATE_FORMATTER)); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DelaccRequestDtois validated but its fields aren’t used (the delete is driven solely by the{accno}path variable), so a mismatched body (e.g., differentDelAccAccno/sortcode) would still delete the path account. Consider rejecting inconsistent requests (or documenting that the body is ignored) to reduce the risk of accidental deletions.Severity: medium
🤖 Was this useful? React with 👍 or 👎