feat: add issuance application workflow

agent/payment-training-progress
chenyuan 4 weeks ago
parent 4739757d61
commit 34c3d01a65

@ -0,0 +1,33 @@
# Task 4 Report: Issuance Application Workflow
## Scope
Implemented Task 4 application commands, query views, commercial-bank orchestration, and the read-only central-bank query. No REST controller or interface DTO was added.
## Changes
- Added create/update commands and commercial-bank inventory/request views under `issuance/application`.
- Added transactional commercial-bank operations for create, update, message preparation, SM3 digest recording, SM2 signing with `sm2-key-02`, JSON packaging, and idempotent sending.
- Generated request numbers as `ISSUE_REQ_` followed by an upper-case UUID suffix.
- Added request/inventory error mapping to `RESOURCE_NOT_FOUND` and domain argument/state error mapping to `VALIDATION_ERROR`.
- Added a central-bank read-only query that exposes persisted request receipt state, receipt time, and payload data.
- Registered the message composer and SM3/SM2 signing service as Spring beans.
- Added focused application tests for end-to-end send visibility, repeated sends, invalid post-prepare update, central-bank payload/receipt lookup, and missing request mapping.
## Test evidence
- RED: `mvn '-Dtest=CommercialBankIssuanceApplicationServiceTest,CentralBankIssuanceQueryServiceTest' test -B` failed at test compilation before implementation because the application command, query, and service classes did not exist.
- GREEN: the same focused command passed with 5 tests and 0 failures/errors.
- JDK 8: `mvn test -B` using Temurin `1.8.0_502` passed with 45 tests and 0 failures/errors.
- During full-suite verification, Spring reported an ambiguous service constructor. The application-context failure reproduced the problem; annotating the intended three-dependency constructor with `@Autowired` restored application-context startup. The focused and full JDK 8 suites were then rerun successfully.
- `git diff --check` completed without whitespace errors.
## Commit
`feat: add issuance application workflow`
## Review and concerns
- 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.

@ -0,0 +1,20 @@
package com.yau.digitalrmb.issuance.application;
import com.yau.digitalrmb.issuance.domain.service.IssuanceMessageComposer;
import com.yau.digitalrmb.issuance.domain.service.IssuanceSignatureService;
import com.yau.digitalrmb.issuance.infrastructure.crypto.BouncyCastleIssuanceSignatureService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class IssuanceApplicationConfiguration {
@Bean
public IssuanceMessageComposer issuanceMessageComposer() {
return new IssuanceMessageComposer();
}
@Bean
public IssuanceSignatureService issuanceSignatureService() {
return new BouncyCastleIssuanceSignatureService();
}
}

@ -0,0 +1,32 @@
package com.yau.digitalrmb.issuance.application.command;
import com.yau.digitalrmb.issuance.domain.model.DenominationItem;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class CreateIssuanceRequestCommand {
private final String bankCode;
private final String organizationId;
private final BigDecimal totalAmount;
private final String currency;
private final List<DenominationItem> denominations;
public CreateIssuanceRequestCommand(String bankCode, String organizationId, BigDecimal totalAmount, String currency,
List<DenominationItem> denominations) {
this.bankCode = bankCode;
this.organizationId = organizationId;
this.totalAmount = totalAmount;
this.currency = currency;
this.denominations = denominations == null ? null
: Collections.unmodifiableList(new ArrayList<DenominationItem>(denominations));
}
public String getBankCode() { return bankCode; }
public String getOrganizationId() { return organizationId; }
public BigDecimal getTotalAmount() { return totalAmount; }
public String getCurrency() { return currency; }
public List<DenominationItem> getDenominations() { return denominations; }
}

@ -0,0 +1,25 @@
package com.yau.digitalrmb.issuance.application.command;
import com.yau.digitalrmb.issuance.domain.model.DenominationItem;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class UpdateIssuanceRequestCommand {
private final BigDecimal totalAmount;
private final String currency;
private final List<DenominationItem> denominations;
public UpdateIssuanceRequestCommand(BigDecimal totalAmount, String currency, List<DenominationItem> denominations) {
this.totalAmount = totalAmount;
this.currency = currency;
this.denominations = denominations == null ? null
: Collections.unmodifiableList(new ArrayList<DenominationItem>(denominations));
}
public BigDecimal getTotalAmount() { return totalAmount; }
public String getCurrency() { return currency; }
public List<DenominationItem> getDenominations() { return denominations; }
}

@ -0,0 +1,23 @@
package com.yau.digitalrmb.issuance.application.query;
import java.math.BigDecimal;
public final class CommercialBankInventoryView {
private final String bankCode;
private final BigDecimal currentBalance;
private final BigDecimal warningThreshold;
private final BigDecimal suggestedSupplementAmount;
public CommercialBankInventoryView(String bankCode, BigDecimal currentBalance, BigDecimal warningThreshold,
BigDecimal suggestedSupplementAmount) {
this.bankCode = bankCode;
this.currentBalance = currentBalance;
this.warningThreshold = warningThreshold;
this.suggestedSupplementAmount = suggestedSupplementAmount;
}
public String getBankCode() { return bankCode; }
public BigDecimal getCurrentBalance() { return currentBalance; }
public BigDecimal getWarningThreshold() { return warningThreshold; }
public BigDecimal getSuggestedSupplementAmount() { return suggestedSupplementAmount; }
}

@ -0,0 +1,68 @@
package com.yau.digitalrmb.issuance.application.query;
import com.yau.digitalrmb.issuance.domain.model.DenominationItem;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequest;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public final class IssuanceRequestView {
private final UUID id;
private final String requestNo;
private final String bankCode;
private final String organizationId;
private final BigDecimal totalAmount;
private final String currency;
private final List<DenominationItem> denominations;
private final String requestTimestamp;
private final String messageText;
private final String digest;
private final String signature;
private final String signingKeyRef;
private final String payloadJson;
private final String status;
private final String centralBankReceiveStatus;
private final Instant centralBankReceivedAt;
private IssuanceRequestView(IssuanceRequest request) {
this.id = request.getId().value();
this.requestNo = request.getRequestNo();
this.bankCode = request.getBankCode();
this.organizationId = request.getOrganizationId();
this.totalAmount = request.getTotalAmount();
this.currency = request.getCurrency();
this.denominations = Collections.unmodifiableList(new ArrayList<DenominationItem>(request.getDenominations()));
this.requestTimestamp = request.getRequestTimestamp();
this.messageText = request.getMessageText();
this.digest = request.getDigest();
this.signature = request.getSignature();
this.signingKeyRef = request.getSigningKeyRef();
this.payloadJson = request.getPayloadJson();
this.status = request.getStatus().name();
this.centralBankReceiveStatus = request.getCentralBankReceiveStatus().name();
this.centralBankReceivedAt = request.getCentralBankReceivedAt();
}
public static IssuanceRequestView from(IssuanceRequest request) { return new IssuanceRequestView(request); }
public UUID getId() { return id; }
public String getRequestNo() { return requestNo; }
public String getBankCode() { return bankCode; }
public String getOrganizationId() { return organizationId; }
public BigDecimal getTotalAmount() { return totalAmount; }
public String getCurrency() { return currency; }
public List<DenominationItem> getDenominations() { return denominations; }
public String getRequestTimestamp() { return requestTimestamp; }
public String getMessageText() { return messageText; }
public String getDigest() { return digest; }
public String getSignature() { return signature; }
public String getSigningKeyRef() { return signingKeyRef; }
public String getPayloadJson() { return payloadJson; }
public String getStatus() { return status; }
public String getCentralBankReceiveStatus() { return centralBankReceiveStatus; }
public Instant getCentralBankReceivedAt() { return centralBankReceivedAt; }
}

@ -0,0 +1,34 @@
package com.yau.digitalrmb.issuance.application.service;
import com.yau.digitalrmb.issuance.application.query.IssuanceRequestView;
import com.yau.digitalrmb.issuance.domain.model.IssuanceApplicationId;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequest;
import com.yau.digitalrmb.issuance.domain.repository.IssuanceRequestRepository;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
@Service
public class CentralBankIssuanceQueryService {
private final IssuanceRequestRepository repository;
public CentralBankIssuanceQueryService(IssuanceRequestRepository repository) {
this.repository = repository;
}
@Transactional(readOnly = true)
public IssuanceRequestView getCentralBankView(UUID id) {
return IssuanceRequestView.from(requireRequest(id));
}
private IssuanceRequest requireRequest(UUID id) {
if (id == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "request id must not be null");
}
return repository.findById(new IssuanceApplicationId(id)).orElseThrow(() ->
new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "issuance request was not found: " + id));
}
}

@ -0,0 +1,213 @@
package com.yau.digitalrmb.issuance.application.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yau.digitalrmb.issuance.application.command.CreateIssuanceRequestCommand;
import com.yau.digitalrmb.issuance.application.command.UpdateIssuanceRequestCommand;
import com.yau.digitalrmb.issuance.application.query.CommercialBankInventoryView;
import com.yau.digitalrmb.issuance.application.query.IssuanceRequestView;
import com.yau.digitalrmb.issuance.domain.model.DenominationItem;
import com.yau.digitalrmb.issuance.domain.model.IssuanceApplicationId;
import com.yau.digitalrmb.issuance.domain.model.IssuanceBankInventory;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequest;
import com.yau.digitalrmb.issuance.domain.repository.IssuanceRequestRepository;
import com.yau.digitalrmb.issuance.domain.service.IssuanceMessageComposer;
import com.yau.digitalrmb.issuance.domain.service.IssuanceSignatureService;
import com.yau.digitalrmb.issuance.domain.service.SignedIssuancePayload;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
@Service
public class CommercialBankIssuanceApplicationService {
private static final String SIGNING_KEY_REF = "sm2-key-02";
private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private final IssuanceRequestRepository repository;
private final IssuanceMessageComposer messageComposer;
private final IssuanceSignatureService signatureService;
private final ObjectMapper objectMapper;
@Autowired
public CommercialBankIssuanceApplicationService(IssuanceRequestRepository repository,
IssuanceMessageComposer messageComposer,
IssuanceSignatureService signatureService) {
this(repository, messageComposer, signatureService, new ObjectMapper());
}
CommercialBankIssuanceApplicationService(IssuanceRequestRepository repository,
IssuanceMessageComposer messageComposer,
IssuanceSignatureService signatureService, ObjectMapper objectMapper) {
this.repository = repository;
this.messageComposer = messageComposer;
this.signatureService = signatureService;
this.objectMapper = objectMapper;
}
@Transactional
public IssuanceRequestView create(CreateIssuanceRequestCommand command, String authenticatedAccountName) {
try {
UUID id = UUID.randomUUID();
IssuanceRequest request = IssuanceRequest.create(new IssuanceApplicationId(id), requestNo(id),
command.getBankCode(), command.getOrganizationId(), command.getTotalAmount(), command.getCurrency(),
command.getDenominations());
repository.save(request);
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
@Transactional
public IssuanceRequestView update(UUID id, UpdateIssuanceRequestCommand command, String authenticatedAccountName) {
try {
IssuanceRequest request = requireRequest(id);
request.updateDraft(command.getTotalAmount(), command.getCurrency(), command.getDenominations());
repository.save(request);
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
@Transactional
public IssuanceRequestView prepareMessage(UUID id, String authenticatedAccountName) {
try {
IssuanceRequest request = requireRequest(id);
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
String message = messageComposer.compose(request.getBankCode(), request.getOrganizationId(),
request.getTotalAmount(), request.getDenominations(), request.getCurrency(), timestamp);
request.prepareMessage(timestamp, message);
repository.save(request);
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
@Transactional
public IssuanceRequestView digest(UUID id, String authenticatedAccountName) {
try {
IssuanceRequest request = requireRequest(id);
SignedIssuancePayload signedPayload = signatureService.sign(SIGNING_KEY_REF, request.getMessageText());
request.recordDigest(signedPayload.getDigest());
repository.save(request);
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
@Transactional
public IssuanceRequestView sign(UUID id, String authenticatedAccountName) {
try {
IssuanceRequest request = requireRequest(id);
SignedIssuancePayload signedPayload = signatureService.sign(SIGNING_KEY_REF, request.getMessageText());
request.recordSignature(signedPayload.getSigningKeyRef(), signedPayload.getSignature());
repository.save(request);
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
@Transactional
public IssuanceRequestView packagePayload(UUID id, String authenticatedAccountName) {
try {
IssuanceRequest request = requireRequest(id);
request.packagePayload(payloadFor(request));
repository.save(request);
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
@Transactional
public IssuanceRequestView send(UUID id, String authenticatedAccountName) {
try {
IssuanceRequest request = requireRequest(id);
request.sendToCentralBank(Instant.now());
repository.save(request);
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
@Transactional(readOnly = true)
public IssuanceRequestView getCommercialBankView(UUID id) {
return IssuanceRequestView.from(requireRequest(id));
}
@Transactional(readOnly = true)
public CommercialBankInventoryView getInventory(String bankCode) {
if (bankCode == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "bank code must not be null");
}
IssuanceBankInventory inventory = repository.findInventoryByBankCode(bankCode).orElseThrow(() ->
new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "issuance bank inventory was not found: " + bankCode));
return new CommercialBankInventoryView(inventory.getBankCode(), inventory.getCurrentBalance(),
inventory.getWarningThreshold(), inventory.getSuggestedSupplementAmount());
}
private IssuanceRequest requireRequest(UUID id) {
if (id == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "request id must not be null");
}
return repository.findById(new IssuanceApplicationId(id)).orElseThrow(() ->
new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "issuance request was not found: " + id));
}
private String payloadFor(IssuanceRequest request) {
Map<String, Object> payload = new LinkedHashMap<String, Object>();
payload.put("requestId", request.getId().value().toString());
payload.put("requestNo", request.getRequestNo());
payload.put("orgCode", request.getBankCode());
payload.put("bankCode", request.getBankCode());
payload.put("orgId", request.getOrganizationId());
payload.put("totalAmount", request.getTotalAmount());
payload.put("denominations", denominationPayload(request.getDenominations()));
payload.put("currency", request.getCurrency());
payload.put("timestamp", request.getRequestTimestamp());
payload.put("digest", request.getDigest());
payload.put("signature", request.getSignature());
try {
return objectMapper.writeValueAsString(payload);
} catch (JsonProcessingException exception) {
throw new IllegalStateException("unable to package issuance payload", exception);
}
}
private List<Map<String, Integer>> denominationPayload(List<DenominationItem> denominations) {
List<Map<String, Integer>> payload = new ArrayList<Map<String, Integer>>();
for (DenominationItem denomination : denominations) {
Map<String, Integer> item = new LinkedHashMap<String, Integer>();
item.put("denomination", denomination.getDenomination());
item.put("quantity", denomination.getQuantity());
payload.add(item);
}
return payload;
}
private static String requestNo(UUID id) {
return "ISSUE_REQ_" + id.toString().replace("-", "").toUpperCase(Locale.ROOT);
}
private static BusinessException validationError(RuntimeException exception) {
return new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage());
}
}

@ -0,0 +1,75 @@
package com.yau.digitalrmb.issuance.application.service;
import com.yau.digitalrmb.issuance.application.query.IssuanceRequestView;
import com.yau.digitalrmb.issuance.domain.model.DenominationItem;
import com.yau.digitalrmb.issuance.domain.model.IssuanceApplicationId;
import com.yau.digitalrmb.issuance.domain.model.IssuanceBankInventory;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequest;
import com.yau.digitalrmb.issuance.domain.repository.IssuanceRequestRepository;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Arrays;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class CentralBankIssuanceQueryServiceTest {
@Test
void exposesReceiveStatusTimeAndPayloadForSentRequest() {
UUID requestId = UUID.randomUUID();
IssuanceRequest request = IssuanceRequest.create(new IssuanceApplicationId(requestId), "ISSUE_REQ_ABCDEF",
"BKCHCNBJ00001", "ORG_3A4B5C6D7E8F", new BigDecimal("100.00"), "DC",
Arrays.asList(new DenominationItem(100, 1)));
request.prepareMessage("20260803120000", "ISSUE|...");
request.recordDigest("ABC123");
request.recordSignature("sm2-key-02", "signature");
request.packagePayload("{\"requestId\":\"" + requestId + "\"}");
Instant receivedAt = Instant.parse("2026-08-03T12:00:00Z");
request.sendToCentralBank(receivedAt);
CentralBankIssuanceQueryService service = new CentralBankIssuanceQueryService(new SingleRequestRepository(request));
IssuanceRequestView view = service.getCentralBankView(requestId);
assertThat(view.getCentralBankReceiveStatus()).isEqualTo("RECEIVED");
assertThat(view.getCentralBankReceivedAt()).isEqualTo(receivedAt);
assertThat(view.getPayloadJson()).contains(requestId.toString());
}
@Test
void mapsUnknownRequestToResourceNotFound() {
CentralBankIssuanceQueryService service = new CentralBankIssuanceQueryService(new SingleRequestRepository(null));
assertThatThrownBy(() -> service.getCentralBankView(UUID.randomUUID()))
.isInstanceOf(BusinessException.class)
.extracting(exception -> ((BusinessException) exception).getErrorCode())
.isEqualTo(ErrorCode.RESOURCE_NOT_FOUND);
}
private static final class SingleRequestRepository implements IssuanceRequestRepository {
private final IssuanceRequest request;
private SingleRequestRepository(IssuanceRequest request) {
this.request = request;
}
@Override
public void save(IssuanceRequest request) { }
@Override
public Optional<IssuanceRequest> findById(IssuanceApplicationId id) {
return request == null || !request.getId().equals(id) ? Optional.<IssuanceRequest>empty() : Optional.of(request);
}
@Override
public Optional<IssuanceBankInventory> findInventoryByBankCode(String bankCode) {
return Optional.empty();
}
}
}

@ -0,0 +1,108 @@
package com.yau.digitalrmb.issuance.application.service;
import com.yau.digitalrmb.issuance.application.command.CreateIssuanceRequestCommand;
import com.yau.digitalrmb.issuance.application.command.UpdateIssuanceRequestCommand;
import com.yau.digitalrmb.issuance.application.query.IssuanceRequestView;
import com.yau.digitalrmb.issuance.domain.model.DenominationItem;
import com.yau.digitalrmb.issuance.domain.model.IssuanceApplicationId;
import com.yau.digitalrmb.issuance.domain.model.IssuanceBankInventory;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequest;
import com.yau.digitalrmb.issuance.domain.repository.IssuanceRequestRepository;
import com.yau.digitalrmb.issuance.domain.service.IssuanceMessageComposer;
import com.yau.digitalrmb.issuance.infrastructure.crypto.BouncyCastleIssuanceSignatureService;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class CommercialBankIssuanceApplicationServiceTest {
private final InMemoryIssuanceRequestRepository repository = new InMemoryIssuanceRequestRepository();
private final CommercialBankIssuanceApplicationService service = new CommercialBankIssuanceApplicationService(
repository, new IssuanceMessageComposer(), new BouncyCastleIssuanceSignatureService());
@Test
void sendsCommercialBankRequestAndMakesCentralBankViewReceived() {
UUID requestId = service.create(createCommand(), "tzs001").getId();
service.prepareMessage(requestId, "tzs001");
service.digest(requestId, "tzs001");
service.sign(requestId, "tzs001");
service.packagePayload(requestId, "tzs001");
IssuanceRequestView sent = service.send(requestId, "tzs001");
IssuanceRequestView centralBankView = new CentralBankIssuanceQueryService(repository)
.getCentralBankView(requestId);
assertThat(sent.getStatus()).isEqualTo("SENT");
assertThat(sent.getCentralBankReceiveStatus()).isEqualTo("RECEIVED");
assertThat(centralBankView.getCentralBankReceiveStatus()).isEqualTo("RECEIVED");
assertThat(sent.getRequestNo()).matches("ISSUE_REQ_[0-9A-F]{32}");
assertThat(sent.getRequestTimestamp()).matches("\\d{14}");
assertThat(sent.getPayloadJson()).contains("\"requestId\":\"" + requestId + "\"");
assertThat(sent.getPayloadJson()).contains("\"orgCode\":\"BKCHCNBJ00001\"")
.contains("\"orgId\":\"ORG_3A4B5C6D7E8F\"")
.contains("\"denominations\"").contains("\"digest\"").contains("\"signature\"");
}
@Test
void retainsReceivedRequestWhenSendIsRepeated() {
UUID requestId = service.create(createCommand(), "tzs001").getId();
service.prepareMessage(requestId, "tzs001");
service.digest(requestId, "tzs001");
service.sign(requestId, "tzs001");
service.packagePayload(requestId, "tzs001");
IssuanceRequestView firstSend = service.send(requestId, "tzs001");
IssuanceRequestView repeatedSend = service.send(requestId, "tzs001");
assertThat(repeatedSend.getStatus()).isEqualTo("SENT");
assertThat(repeatedSend.getCentralBankReceiveStatus()).isEqualTo("RECEIVED");
assertThat(repeatedSend.getCentralBankReceivedAt()).isEqualTo(firstSend.getCentralBankReceivedAt());
}
@Test
void rejectsUpdateAfterMessagePreparation() {
UUID requestId = service.create(createCommand(), "tzs001").getId();
service.prepareMessage(requestId, "tzs001");
assertThatThrownBy(() -> service.update(requestId, new UpdateIssuanceRequestCommand(
new BigDecimal("200.00"), "DC", Arrays.asList(new DenominationItem(100, 2))), "tzs001"))
.isInstanceOf(BusinessException.class)
.extracting(exception -> ((BusinessException) exception).getErrorCode())
.isEqualTo(ErrorCode.VALIDATION_ERROR);
}
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 static final class InMemoryIssuanceRequestRepository implements IssuanceRequestRepository {
private final Map<UUID, IssuanceRequest> requests = new HashMap<UUID, IssuanceRequest>();
@Override
public void save(IssuanceRequest request) {
requests.put(request.getId().value(), request);
}
@Override
public Optional<IssuanceRequest> findById(IssuanceApplicationId id) {
return Optional.ofNullable(requests.get(id.value()));
}
@Override
public Optional<IssuanceBankInventory> findInventoryByBankCode(String bankCode) {
return Optional.empty();
}
}
}
Loading…
Cancel
Save