feat: persist issuance requests and bank inventory
parent
c17cc29db2
commit
432081d8ce
@ -0,0 +1,36 @@
|
||||
package com.yau.digitalrmb.issuance.domain.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class IssuanceBankInventory {
|
||||
private final String bankCode;
|
||||
private final BigDecimal currentBalance;
|
||||
private final BigDecimal warningThreshold;
|
||||
private final BigDecimal suggestedSupplementAmount;
|
||||
|
||||
public IssuanceBankInventory(String bankCode, BigDecimal currentBalance, BigDecimal warningThreshold,
|
||||
BigDecimal suggestedSupplementAmount) {
|
||||
this.bankCode = Objects.requireNonNull(bankCode, "bankCode must not be null");
|
||||
this.currentBalance = Objects.requireNonNull(currentBalance, "currentBalance must not be null");
|
||||
this.warningThreshold = Objects.requireNonNull(warningThreshold, "warningThreshold must not be null");
|
||||
this.suggestedSupplementAmount = Objects.requireNonNull(suggestedSupplementAmount,
|
||||
"suggestedSupplementAmount must not be null");
|
||||
}
|
||||
|
||||
public String getBankCode() {
|
||||
return bankCode;
|
||||
}
|
||||
|
||||
public BigDecimal getCurrentBalance() {
|
||||
return currentBalance;
|
||||
}
|
||||
|
||||
public BigDecimal getWarningThreshold() {
|
||||
return warningThreshold;
|
||||
}
|
||||
|
||||
public BigDecimal getSuggestedSupplementAmount() {
|
||||
return suggestedSupplementAmount;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.yau.digitalrmb.issuance.domain.repository;
|
||||
|
||||
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 java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface IssuanceRequestRepository {
|
||||
void save(IssuanceRequest request);
|
||||
|
||||
Optional<IssuanceRequest> findById(IssuanceApplicationId id);
|
||||
|
||||
Optional<IssuanceBankInventory> findInventoryByBankCode(String bankCode);
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.yau.digitalrmb.issuance.infrastructure.persistence.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("issuance_bank_inventory")
|
||||
public class IssuanceBankInventoryEntity {
|
||||
@TableId(value = "bank_code", type = IdType.INPUT)
|
||||
private String bankCode;
|
||||
|
||||
private BigDecimal currentBalance;
|
||||
|
||||
private BigDecimal warningThreshold;
|
||||
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.yau.digitalrmb.issuance.infrastructure.persistence.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("issuance_request_denomination")
|
||||
public class IssuanceRequestDenominationEntity {
|
||||
@TableId(value = "request_id", type = IdType.INPUT)
|
||||
private String requestId;
|
||||
|
||||
private Integer denomination;
|
||||
|
||||
private Integer quantity;
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.yau.digitalrmb.issuance.infrastructure.persistence.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("issuance_request")
|
||||
public class IssuanceRequestEntity {
|
||||
@TableId(value = "id", type = IdType.INPUT)
|
||||
private String id;
|
||||
|
||||
private String requestNo;
|
||||
private String bankCode;
|
||||
private String organizationId;
|
||||
private BigDecimal totalAmount;
|
||||
private String currency;
|
||||
private String requestTimestamp;
|
||||
private String messageText;
|
||||
private String digest;
|
||||
private String signature;
|
||||
private String signingKeyRef;
|
||||
private String payloadJson;
|
||||
private String status;
|
||||
private String centralReceiveStatus;
|
||||
private LocalDateTime centralReceivedAt;
|
||||
|
||||
@TableField(value = "created_at", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@TableField(value = "created_by", fill = FieldFill.INSERT)
|
||||
private String createdBy;
|
||||
|
||||
@TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
|
||||
private String updatedBy;
|
||||
|
||||
@TableLogic
|
||||
private Boolean deleted;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.yau.digitalrmb.issuance.infrastructure.persistence.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceBankInventoryEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface IssuanceBankInventoryMapper extends BaseMapper<IssuanceBankInventoryEntity> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.yau.digitalrmb.issuance.infrastructure.persistence.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceRequestDenominationEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface IssuanceRequestDenominationMapper extends BaseMapper<IssuanceRequestDenominationEntity> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.yau.digitalrmb.issuance.infrastructure.persistence.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceRequestEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface IssuanceRequestMapper extends BaseMapper<IssuanceRequestEntity> {
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
package com.yau.digitalrmb.issuance.infrastructure.persistence.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.yau.digitalrmb.issuance.domain.model.CentralBankReceiveStatus;
|
||||
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.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.entity.IssuanceRequestDenominationEntity;
|
||||
import com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceRequestEntity;
|
||||
import com.yau.digitalrmb.issuance.infrastructure.persistence.mapper.IssuanceBankInventoryMapper;
|
||||
import com.yau.digitalrmb.issuance.infrastructure.persistence.mapper.IssuanceRequestDenominationMapper;
|
||||
import com.yau.digitalrmb.issuance.infrastructure.persistence.mapper.IssuanceRequestMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public class MybatisIssuanceRequestRepository implements IssuanceRequestRepository {
|
||||
private final IssuanceRequestMapper requestMapper;
|
||||
private final IssuanceRequestDenominationMapper denominationMapper;
|
||||
private final IssuanceBankInventoryMapper inventoryMapper;
|
||||
|
||||
public MybatisIssuanceRequestRepository(IssuanceRequestMapper requestMapper,
|
||||
IssuanceRequestDenominationMapper denominationMapper,
|
||||
IssuanceBankInventoryMapper inventoryMapper) {
|
||||
this.requestMapper = requestMapper;
|
||||
this.denominationMapper = denominationMapper;
|
||||
this.inventoryMapper = inventoryMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void save(IssuanceRequest request) {
|
||||
String requestId = request.getId().value().toString();
|
||||
IssuanceRequestEntity entity = toEntity(request);
|
||||
if (requestMapper.selectById(requestId) == null) {
|
||||
requestMapper.insert(entity);
|
||||
} else {
|
||||
requestMapper.updateById(entity);
|
||||
}
|
||||
|
||||
denominationMapper.delete(new LambdaQueryWrapper<IssuanceRequestDenominationEntity>()
|
||||
.eq(IssuanceRequestDenominationEntity::getRequestId, requestId));
|
||||
for (DenominationItem denomination : request.getDenominations()) {
|
||||
denominationMapper.insert(toDenominationEntity(requestId, denomination));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<IssuanceRequest> findById(IssuanceApplicationId id) {
|
||||
String requestId = id.value().toString();
|
||||
IssuanceRequestEntity requestEntity = requestMapper.selectById(requestId);
|
||||
if (requestEntity == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
List<IssuanceRequestDenominationEntity> denominationEntities = denominationMapper.selectList(
|
||||
new LambdaQueryWrapper<IssuanceRequestDenominationEntity>()
|
||||
.eq(IssuanceRequestDenominationEntity::getRequestId, requestId)
|
||||
.orderByDesc(IssuanceRequestDenominationEntity::getDenomination));
|
||||
return Optional.of(toAggregate(requestEntity, denominationEntities));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<IssuanceBankInventory> findInventoryByBankCode(String bankCode) {
|
||||
IssuanceBankInventoryEntity inventory = inventoryMapper.selectById(bankCode);
|
||||
if (inventory == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
BigDecimal suggested = inventory.getWarningThreshold().subtract(inventory.getCurrentBalance());
|
||||
return Optional.of(new IssuanceBankInventory(inventory.getBankCode(), inventory.getCurrentBalance(),
|
||||
inventory.getWarningThreshold(), suggested.max(BigDecimal.ZERO).setScale(2)));
|
||||
}
|
||||
|
||||
private IssuanceRequestEntity toEntity(IssuanceRequest request) {
|
||||
IssuanceRequestEntity entity = new IssuanceRequestEntity();
|
||||
entity.setId(request.getId().value().toString());
|
||||
entity.setRequestNo(request.getRequestNo());
|
||||
entity.setBankCode(request.getBankCode());
|
||||
entity.setOrganizationId(request.getOrganizationId());
|
||||
entity.setTotalAmount(request.getTotalAmount());
|
||||
entity.setCurrency(request.getCurrency());
|
||||
entity.setRequestTimestamp(request.getRequestTimestamp());
|
||||
entity.setMessageText(request.getMessageText());
|
||||
entity.setDigest(request.getDigest());
|
||||
entity.setSignature(request.getSignature());
|
||||
entity.setSigningKeyRef(request.getSigningKeyRef());
|
||||
entity.setPayloadJson(request.getPayloadJson());
|
||||
entity.setStatus(request.getStatus().name());
|
||||
entity.setCentralReceiveStatus(request.getCentralBankReceiveStatus().name());
|
||||
if (request.getCentralBankReceivedAt() != null) {
|
||||
entity.setCentralReceivedAt(request.getCentralBankReceivedAt().atOffset(ZoneOffset.UTC).toLocalDateTime());
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private IssuanceRequestDenominationEntity toDenominationEntity(String requestId, DenominationItem denomination) {
|
||||
IssuanceRequestDenominationEntity entity = new IssuanceRequestDenominationEntity();
|
||||
entity.setRequestId(requestId);
|
||||
entity.setDenomination(denomination.getDenomination());
|
||||
entity.setQuantity(denomination.getQuantity());
|
||||
return entity;
|
||||
}
|
||||
|
||||
private IssuanceRequest toAggregate(IssuanceRequestEntity requestEntity,
|
||||
List<IssuanceRequestDenominationEntity> denominationEntities) {
|
||||
List<DenominationItem> denominations = new ArrayList<DenominationItem>();
|
||||
for (IssuanceRequestDenominationEntity denomination : denominationEntities) {
|
||||
denominations.add(new DenominationItem(denomination.getDenomination(), denomination.getQuantity()));
|
||||
}
|
||||
return IssuanceRequest.rehydrate(new IssuanceApplicationId(UUID.fromString(requestEntity.getId())),
|
||||
requestEntity.getRequestNo(), requestEntity.getBankCode(), requestEntity.getOrganizationId(),
|
||||
requestEntity.getTotalAmount(), requestEntity.getCurrency(), denominations,
|
||||
requestEntity.getRequestTimestamp(), requestEntity.getMessageText(), requestEntity.getDigest(),
|
||||
requestEntity.getSignature(), requestEntity.getSigningKeyRef(), requestEntity.getPayloadJson(),
|
||||
IssuanceRequestStatus.valueOf(requestEntity.getStatus()),
|
||||
CentralBankReceiveStatus.valueOf(requestEntity.getCentralReceiveStatus()),
|
||||
requestEntity.getCentralReceivedAt() == null ? null
|
||||
: requestEntity.getCentralReceivedAt().toInstant(ZoneOffset.UTC));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package com.yau.digitalrmb.issuance.infrastructure.persistence;
|
||||
|
||||
import com.yau.digitalrmb.issuance.domain.model.CentralBankReceiveStatus;
|
||||
import com.yau.digitalrmb.issuance.domain.model.DenominationItem;
|
||||
import com.yau.digitalrmb.issuance.domain.model.IssuanceBankInventory;
|
||||
import com.yau.digitalrmb.issuance.domain.model.IssuanceApplicationId;
|
||||
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequest;
|
||||
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequestStatus;
|
||||
import com.yau.digitalrmb.issuance.domain.repository.IssuanceRequestRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class MybatisIssuanceRequestRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private IssuanceRequestRepository repository;
|
||||
|
||||
@Test
|
||||
void savesAndRestoresTheRequestWithItsDenominationsAndCentralBankStatus() {
|
||||
IssuanceRequest sentRequest = sentRequest();
|
||||
|
||||
repository.save(sentRequest);
|
||||
|
||||
IssuanceRequest restored = repository.findById(sentRequest.getId())
|
||||
.orElseThrow(() -> new AssertionError("saved request was not restored"));
|
||||
|
||||
assertThat(restored.getRequestNo()).isEqualTo(sentRequest.getRequestNo());
|
||||
assertThat(restored.getBankCode()).isEqualTo("BKCHCNBJ00001");
|
||||
assertThat(restored.getOrganizationId()).isEqualTo("ORG_3A4B5C6D7E8F");
|
||||
assertThat(restored.getTotalAmount()).isEqualByComparingTo("50000.00");
|
||||
assertThat(restored.getCurrency()).isEqualTo("DC");
|
||||
assertThat(restored.getDenominations())
|
||||
.containsExactly(new DenominationItem(100, 400), new DenominationItem(50, 200));
|
||||
assertThat(restored.getRequestTimestamp()).isEqualTo("20260801103218");
|
||||
assertThat(restored.getMessageText()).isEqualTo("ISSUE|BKCHCNBJ00001|ORG_3A4B5C6D7E8F|50000.00");
|
||||
assertThat(restored.getDigest()).isEqualTo("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF");
|
||||
assertThat(restored.getSignature()).isEqualTo("base64-signature");
|
||||
assertThat(restored.getSigningKeyRef()).isEqualTo("sm2-key-02");
|
||||
assertThat(restored.getPayloadJson()).isEqualTo("{\"requestId\":\"example\"}");
|
||||
assertThat(restored.getStatus()).isEqualTo(IssuanceRequestStatus.SENT);
|
||||
assertThat(restored.getCentralBankReceiveStatus()).isEqualTo(CentralBankReceiveStatus.RECEIVED);
|
||||
assertThat(restored.getCentralBankReceivedAt()).isEqualTo(Instant.parse("2026-08-03T12:00:00Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsSuggestedAmountAsThresholdMinusCurrentBalance() {
|
||||
IssuanceBankInventory inventory = repository.findInventoryByBankCode("BKCHCNBJ00001")
|
||||
.orElseThrow(() -> new AssertionError("seeded bank inventory was not found"));
|
||||
|
||||
assertThat(inventory.getCurrentBalance()).isEqualByComparingTo("49950000.00");
|
||||
assertThat(inventory.getWarningThreshold()).isEqualByComparingTo("50000000.00");
|
||||
assertThat(inventory.getSuggestedSupplementAmount()).isEqualByComparingTo("50000.00");
|
||||
}
|
||||
|
||||
@Test
|
||||
void savingChangedDraftReplacesRatherThanAccumulatesDenominations() {
|
||||
IssuanceRequest request = IssuanceRequest.create(new IssuanceApplicationId(UUID.randomUUID()),
|
||||
"BKCHCNBJ00001", "ORG_3A4B5C6D7E8F", new BigDecimal("50000.00"), "DC",
|
||||
Arrays.asList(new DenominationItem(100, 400), new DenominationItem(50, 200)));
|
||||
repository.save(request);
|
||||
|
||||
request.updateDraft(new BigDecimal("50000.00"), "DC", Arrays.asList(new DenominationItem(100, 500)));
|
||||
repository.save(request);
|
||||
|
||||
IssuanceRequest restored = repository.findById(request.getId())
|
||||
.orElseThrow(() -> new AssertionError("updated request was not restored"));
|
||||
assertThat(restored.getDenominations()).containsExactly(new DenominationItem(100, 500));
|
||||
}
|
||||
|
||||
private IssuanceRequest sentRequest() {
|
||||
IssuanceRequest request = IssuanceRequest.create(new IssuanceApplicationId(UUID.randomUUID()),
|
||||
"BKCHCNBJ00001", "ORG_3A4B5C6D7E8F", new BigDecimal("50000.00"), "DC",
|
||||
Arrays.asList(new DenominationItem(100, 400), new DenominationItem(50, 200)));
|
||||
request.prepareMessage("20260801103218", "ISSUE|BKCHCNBJ00001|ORG_3A4B5C6D7E8F|50000.00");
|
||||
request.recordDigest("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF");
|
||||
request.recordSignature("sm2-key-02", "base64-signature");
|
||||
request.packagePayload("{\"requestId\":\"example\"}");
|
||||
request.sendToCentralBank(Instant.parse("2026-08-03T12:00:00Z"));
|
||||
return request;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue