diff --git a/.superpowers/sdd/2026-08-03-issuance-request-implementation/task-4-report.md b/.superpowers/sdd/2026-08-03-issuance-request-implementation/task-4-report.md index b0a4b77..78f3b5d 100644 --- a/.superpowers/sdd/2026-08-03-issuance-request-implementation/task-4-report.md +++ b/.superpowers/sdd/2026-08-03-issuance-request-implementation/task-4-report.md @@ -31,3 +31,13 @@ Implemented Task 4 application commands, query views, commercial-bank orchestrat - The repository's existing MyBatis audit handler derives `created_by` and `updated_by` from the authenticated Spring Security account; application operation signatures retain the authenticated account name expected by the next REST layer. - JSON payload includes the required request identity, bank/organization fields, denomination rows, currency, timestamp, digest, and signature. `orgCode` and `bankCode` both carry the aggregate's commercial-bank code to support either naming convention at the integration boundary. - No known Task 4 scope issues remain. + +## Fix Round 1: Explicit Audit Actor + +- Root cause: every commercial-bank write accepted `authenticatedAccountName` but discarded it before repository persistence, allowing the MyBatis audit handler to use its `SYSTEM` fallback whenever no Spring Security context was present. +- Added an actor-aware repository save boundary. The commercial-bank service now passes its supplied authenticated account for create, draft update, message preparation, digest, signature, packaging, and send. The MyBatis repository writes that actor to both audit columns on insert and only `updated_by` on later writes, preserving the original creator. +- Added a persistence regression that exercises all of those operations through the Spring application service and reads the actual `issuance_request` row after each operation. +- RED: `mvn '-Dtest=MybatisIssuanceRequestRepositoryTest' test -B` failed as expected with `expected: "creator" but was: "SYSTEM"`. +- GREEN: `mvn '-Dtest=CommercialBankIssuanceApplicationServiceTest,CentralBankIssuanceQueryServiceTest,MybatisIssuanceRequestRepositoryTest' test -B` passed with 10 tests and 0 failures/errors. +- JDK 8: `mvn test -B` using Temurin `1.8.0_502` passed with 46 tests and 0 failures/errors. +- `git diff --check` completed without whitespace errors before commit. diff --git a/src/main/java/com/yau/digitalrmb/issuance/application/service/CommercialBankIssuanceApplicationService.java b/src/main/java/com/yau/digitalrmb/issuance/application/service/CommercialBankIssuanceApplicationService.java index de20709..4819a6d 100644 --- a/src/main/java/com/yau/digitalrmb/issuance/application/service/CommercialBankIssuanceApplicationService.java +++ b/src/main/java/com/yau/digitalrmb/issuance/application/service/CommercialBankIssuanceApplicationService.java @@ -64,7 +64,7 @@ public class CommercialBankIssuanceApplicationService { IssuanceRequest request = IssuanceRequest.create(new IssuanceApplicationId(id), requestNo(id), command.getBankCode(), command.getOrganizationId(), command.getTotalAmount(), command.getCurrency(), command.getDenominations()); - repository.save(request); + repository.save(request, auditActor(authenticatedAccountName)); return IssuanceRequestView.from(request); } catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { throw validationError(exception); @@ -76,7 +76,7 @@ public class CommercialBankIssuanceApplicationService { try { IssuanceRequest request = requireRequest(id); request.updateDraft(command.getTotalAmount(), command.getCurrency(), command.getDenominations()); - repository.save(request); + repository.save(request, auditActor(authenticatedAccountName)); return IssuanceRequestView.from(request); } catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { throw validationError(exception); @@ -91,7 +91,7 @@ public class CommercialBankIssuanceApplicationService { String message = messageComposer.compose(request.getBankCode(), request.getOrganizationId(), request.getTotalAmount(), request.getDenominations(), request.getCurrency(), timestamp); request.prepareMessage(timestamp, message); - repository.save(request); + repository.save(request, auditActor(authenticatedAccountName)); return IssuanceRequestView.from(request); } catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { throw validationError(exception); @@ -104,7 +104,7 @@ public class CommercialBankIssuanceApplicationService { IssuanceRequest request = requireRequest(id); SignedIssuancePayload signedPayload = signatureService.sign(SIGNING_KEY_REF, request.getMessageText()); request.recordDigest(signedPayload.getDigest()); - repository.save(request); + repository.save(request, auditActor(authenticatedAccountName)); return IssuanceRequestView.from(request); } catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { throw validationError(exception); @@ -117,7 +117,7 @@ public class CommercialBankIssuanceApplicationService { IssuanceRequest request = requireRequest(id); SignedIssuancePayload signedPayload = signatureService.sign(SIGNING_KEY_REF, request.getMessageText()); request.recordSignature(signedPayload.getSigningKeyRef(), signedPayload.getSignature()); - repository.save(request); + repository.save(request, auditActor(authenticatedAccountName)); return IssuanceRequestView.from(request); } catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { throw validationError(exception); @@ -129,7 +129,7 @@ public class CommercialBankIssuanceApplicationService { try { IssuanceRequest request = requireRequest(id); request.packagePayload(payloadFor(request)); - repository.save(request); + repository.save(request, auditActor(authenticatedAccountName)); return IssuanceRequestView.from(request); } catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { throw validationError(exception); @@ -141,7 +141,7 @@ public class CommercialBankIssuanceApplicationService { try { IssuanceRequest request = requireRequest(id); request.sendToCentralBank(Instant.now()); - repository.save(request); + repository.save(request, auditActor(authenticatedAccountName)); return IssuanceRequestView.from(request); } catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) { throw validationError(exception); @@ -207,6 +207,13 @@ public class CommercialBankIssuanceApplicationService { return "ISSUE_REQ_" + id.toString().replace("-", "").toUpperCase(Locale.ROOT); } + private static String auditActor(String authenticatedAccountName) { + if (authenticatedAccountName == null || authenticatedAccountName.trim().isEmpty()) { + throw new IllegalArgumentException("authenticated account name must not be blank"); + } + return authenticatedAccountName; + } + private static BusinessException validationError(RuntimeException exception) { return new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage()); } diff --git a/src/main/java/com/yau/digitalrmb/issuance/domain/repository/IssuanceRequestRepository.java b/src/main/java/com/yau/digitalrmb/issuance/domain/repository/IssuanceRequestRepository.java index 000f93d..ef874ed 100644 --- a/src/main/java/com/yau/digitalrmb/issuance/domain/repository/IssuanceRequestRepository.java +++ b/src/main/java/com/yau/digitalrmb/issuance/domain/repository/IssuanceRequestRepository.java @@ -9,6 +9,10 @@ import java.util.Optional; public interface IssuanceRequestRepository { void save(IssuanceRequest request); + default void save(IssuanceRequest request, String auditActor) { + save(request); + } + Optional findById(IssuanceApplicationId id); Optional findInventoryByBankCode(String bankCode); diff --git a/src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/repository/MybatisIssuanceRequestRepository.java b/src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/repository/MybatisIssuanceRequestRepository.java index a608ff0..6bf4a69 100644 --- a/src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/repository/MybatisIssuanceRequestRepository.java +++ b/src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/repository/MybatisIssuanceRequestRepository.java @@ -22,6 +22,7 @@ import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Objects; import java.util.UUID; @Repository @@ -41,11 +42,24 @@ public class MybatisIssuanceRequestRepository implements IssuanceRequestReposito @Override @Transactional public void save(IssuanceRequest request) { + saveInternal(request, null); + } + + @Override + @Transactional + public void save(IssuanceRequest request, String auditActor) { + saveInternal(request, Objects.requireNonNull(auditActor, "auditActor must not be null")); + } + + private void saveInternal(IssuanceRequest request, String auditActor) { String requestId = request.getId().value().toString(); IssuanceRequestEntity entity = toEntity(request); - if (requestMapper.selectById(requestId) == null) { + IssuanceRequestEntity existing = requestMapper.selectById(requestId); + if (existing == null) { + assignAuditActorForInsert(entity, auditActor); requestMapper.insert(entity); } else { + assignAuditActorForUpdate(entity, auditActor); requestMapper.updateById(entity); } @@ -56,6 +70,19 @@ public class MybatisIssuanceRequestRepository implements IssuanceRequestReposito } } + private void assignAuditActorForInsert(IssuanceRequestEntity entity, String auditActor) { + if (auditActor != null) { + entity.setCreatedBy(auditActor); + entity.setUpdatedBy(auditActor); + } + } + + private void assignAuditActorForUpdate(IssuanceRequestEntity entity, String auditActor) { + if (auditActor != null) { + entity.setUpdatedBy(auditActor); + } + } + @Override public Optional findById(IssuanceApplicationId id) { String requestId = id.value().toString(); diff --git a/src/test/java/com/yau/digitalrmb/issuance/infrastructure/persistence/MybatisIssuanceRequestRepositoryTest.java b/src/test/java/com/yau/digitalrmb/issuance/infrastructure/persistence/MybatisIssuanceRequestRepositoryTest.java index 0f33f1a..8d49820 100644 --- a/src/test/java/com/yau/digitalrmb/issuance/infrastructure/persistence/MybatisIssuanceRequestRepositoryTest.java +++ b/src/test/java/com/yau/digitalrmb/issuance/infrastructure/persistence/MybatisIssuanceRequestRepositoryTest.java @@ -1,5 +1,8 @@ package com.yau.digitalrmb.issuance.infrastructure.persistence; +import com.yau.digitalrmb.issuance.application.command.CreateIssuanceRequestCommand; +import com.yau.digitalrmb.issuance.application.command.UpdateIssuanceRequestCommand; +import com.yau.digitalrmb.issuance.application.service.CommercialBankIssuanceApplicationService; import com.yau.digitalrmb.issuance.domain.model.CentralBankReceiveStatus; import com.yau.digitalrmb.issuance.domain.model.DenominationItem; import com.yau.digitalrmb.issuance.domain.model.IssuanceBankInventory; @@ -9,6 +12,7 @@ import com.yau.digitalrmb.issuance.domain.model.IssuanceRequestStatus; import com.yau.digitalrmb.issuance.domain.repository.IssuanceRequestRepository; import com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceBankInventoryEntity; import com.yau.digitalrmb.issuance.infrastructure.persistence.mapper.IssuanceBankInventoryMapper; +import com.yau.digitalrmb.issuance.infrastructure.persistence.mapper.IssuanceRequestMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -32,6 +36,12 @@ class MybatisIssuanceRequestRepositoryTest { @Autowired private IssuanceBankInventoryMapper inventoryMapper; + @Autowired + private IssuanceRequestMapper requestMapper; + + @Autowired + private CommercialBankIssuanceApplicationService commercialBankService; + @Test void savesAndRestoresTheRequestWithItsDenominationsAndCentralBankStatus() { IssuanceRequest sentRequest = sentRequest(); @@ -100,6 +110,42 @@ class MybatisIssuanceRequestRepositoryTest { assertThat(restored.getDenominations()).containsExactly(new DenominationItem(100, 500)); } + @Test + void commercialBankWritesPersistTheSuppliedActorForCreateUpdateAndTransitions() { + UUID requestId = commercialBankService.create(createCommand(), "creator").getId(); + assertAuditActor(requestId, "creator", "creator"); + + commercialBankService.update(requestId, updateCommand(), "updater"); + assertAuditActor(requestId, "creator", "updater"); + commercialBankService.prepareMessage(requestId, "preparer"); + assertAuditActor(requestId, "creator", "preparer"); + commercialBankService.digest(requestId, "digester"); + assertAuditActor(requestId, "creator", "digester"); + commercialBankService.sign(requestId, "signer"); + assertAuditActor(requestId, "creator", "signer"); + commercialBankService.packagePayload(requestId, "packager"); + assertAuditActor(requestId, "creator", "packager"); + commercialBankService.send(requestId, "sender"); + assertAuditActor(requestId, "creator", "sender"); + } + + private CreateIssuanceRequestCommand createCommand() { + return new CreateIssuanceRequestCommand("BKCHCNBJ00001", "ORG_3A4B5C6D7E8F", new BigDecimal("50000.00"), + "DC", Arrays.asList(new DenominationItem(100, 400), new DenominationItem(50, 200))); + } + + private UpdateIssuanceRequestCommand updateCommand() { + return new UpdateIssuanceRequestCommand(new BigDecimal("50000.00"), "DC", + Arrays.asList(new DenominationItem(100, 500))); + } + + private void assertAuditActor(UUID requestId, String expectedCreatedBy, String expectedUpdatedBy) { + com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceRequestEntity entity = + requestMapper.selectById(requestId.toString()); + assertThat(entity.getCreatedBy()).isEqualTo(expectedCreatedBy); + assertThat(entity.getUpdatedBy()).isEqualTo(expectedUpdatedBy); + } + private IssuanceRequest sentRequest() { UUID id = UUID.fromString("12345678-1234-1234-1234-1234567890ab"); IssuanceRequest request = IssuanceRequest.create(new IssuanceApplicationId(id),