feat: audit issuance operations by user

master
chenyuan 4 weeks ago
parent bda520215c
commit a0d227342b

@ -8,6 +8,7 @@ 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.IssuanceAuditActor;
import com.yau.digitalrmb.issuance.domain.model.IssuanceBankInventory;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequest;
import com.yau.digitalrmb.issuance.domain.repository.IssuanceRequestRepository;
@ -58,96 +59,124 @@ public class CommercialBankIssuanceApplicationService {
}
@Transactional
public IssuanceRequestView create(CreateIssuanceRequestCommand command, String authenticatedAccountName) {
public IssuanceRequestView create(CreateIssuanceRequestCommand command, IssuanceAuditActor auditActor) {
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, auditActor(authenticatedAccountName));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
public IssuanceRequestView create(CreateIssuanceRequestCommand command, String username) {
return create(command, legacyAuditActor(username));
}
@Transactional
public IssuanceRequestView update(UUID id, UpdateIssuanceRequestCommand command, String authenticatedAccountName) {
public IssuanceRequestView update(UUID id, UpdateIssuanceRequestCommand command, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireRequest(id);
request.updateDraft(command.getTotalAmount(), command.getCurrency(), command.getDenominations());
repository.save(request, auditActor(authenticatedAccountName));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
public IssuanceRequestView update(UUID id, UpdateIssuanceRequestCommand command, String username) {
return update(id, command, legacyAuditActor(username));
}
@Transactional
public IssuanceRequestView prepareMessage(UUID id, String authenticatedAccountName) {
public IssuanceRequestView prepareMessage(UUID id, IssuanceAuditActor auditActor) {
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, auditActor(authenticatedAccountName));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
public IssuanceRequestView prepareMessage(UUID id, String username) {
return prepareMessage(id, legacyAuditActor(username));
}
@Transactional
public IssuanceRequestView digest(UUID id, String authenticatedAccountName) {
public IssuanceRequestView digest(UUID id, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireRequest(id);
SignedIssuancePayload signedPayload = signatureService.sign(SIGNING_KEY_REF, request.getMessageText());
request.recordDigest(signedPayload.getDigest());
repository.save(request, auditActor(authenticatedAccountName));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
public IssuanceRequestView digest(UUID id, String username) {
return digest(id, legacyAuditActor(username));
}
@Transactional
public IssuanceRequestView sign(UUID id, String authenticatedAccountName) {
public IssuanceRequestView sign(UUID id, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireRequest(id);
SignedIssuancePayload signedPayload = signatureService.sign(SIGNING_KEY_REF, request.getMessageText());
request.recordSignature(signedPayload.getSigningKeyRef(), signedPayload.getSignature());
repository.save(request, auditActor(authenticatedAccountName));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
public IssuanceRequestView sign(UUID id, String username) {
return sign(id, legacyAuditActor(username));
}
@Transactional
public IssuanceRequestView packagePayload(UUID id, String authenticatedAccountName) {
public IssuanceRequestView packagePayload(UUID id, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireRequest(id);
request.packagePayload(payloadFor(request));
repository.save(request, auditActor(authenticatedAccountName));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
public IssuanceRequestView packagePayload(UUID id, String username) {
return packagePayload(id, legacyAuditActor(username));
}
@Transactional
public IssuanceRequestView send(UUID id, String authenticatedAccountName) {
public IssuanceRequestView send(UUID id, IssuanceAuditActor auditActor) {
try {
IssuanceRequest request = requireRequest(id);
request.sendToCentralBank(Instant.now());
repository.save(request, auditActor(authenticatedAccountName));
repository.save(request, auditActor(auditActor));
return IssuanceRequestView.from(request);
} catch (IllegalArgumentException | IllegalStateException | NullPointerException exception) {
throw validationError(exception);
}
}
public IssuanceRequestView send(UUID id, String username) {
return send(id, legacyAuditActor(username));
}
@Transactional(readOnly = true)
public IssuanceRequestView getCommercialBankView(UUID id) {
return IssuanceRequestView.from(requireRequest(id));
@ -204,11 +233,15 @@ 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");
private static IssuanceAuditActor auditActor(IssuanceAuditActor auditActor) {
if (auditActor == null) {
throw new IllegalArgumentException("audit actor must not be null");
}
return authenticatedAccountName;
return auditActor;
}
private static IssuanceAuditActor legacyAuditActor(String username) {
return new IssuanceAuditActor(0L, username);
}
private static BusinessException validationError(RuntimeException exception) {

@ -0,0 +1,30 @@
package com.yau.digitalrmb.issuance.domain.model;
import java.util.Objects;
/** The authenticated user responsible for an issuance operation. */
public final class IssuanceAuditActor {
public static final IssuanceAuditActor SYSTEM = new IssuanceAuditActor(0L, "SYSTEM");
private final long userId;
private final String username;
public IssuanceAuditActor(long userId, String username) {
if (userId < 0L) {
throw new IllegalArgumentException("audit user id must not be negative");
}
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("audit username must not be blank");
}
this.userId = userId;
this.username = username.trim();
}
public long getUserId() {
return userId;
}
public String getUsername() {
return username;
}
}

@ -1,6 +1,7 @@
package com.yau.digitalrmb.issuance.domain.repository;
import com.yau.digitalrmb.issuance.domain.model.IssuanceApplicationId;
import com.yau.digitalrmb.issuance.domain.model.IssuanceAuditActor;
import com.yau.digitalrmb.issuance.domain.model.IssuanceBankInventory;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequest;
@ -9,7 +10,7 @@ import java.util.Optional;
public interface IssuanceRequestRepository {
void save(IssuanceRequest request);
default void save(IssuanceRequest request, String auditActor) {
default void save(IssuanceRequest request, IssuanceAuditActor auditActor) {
save(request);
}

@ -1,5 +1,6 @@
package com.yau.digitalrmb.issuance.infrastructure.persistence.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
@ -8,6 +9,7 @@ import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Getter
@Setter
@ -19,4 +21,20 @@ public class IssuanceRequestDenominationEntity {
private BigDecimal denomination;
private Integer quantity;
@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;
private Long createdByUserId;
private Long updatedByUserId;
}

@ -46,6 +46,10 @@ public class IssuanceRequestEntity {
@TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
private String updatedBy;
private Long createdByUserId;
private Long updatedByUserId;
@TableLogic
private Boolean deleted;
}

@ -4,6 +4,7 @@ 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.IssuanceAuditActor;
import com.yau.digitalrmb.issuance.domain.model.IssuanceBankInventory;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequest;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequestStatus;
@ -48,40 +49,40 @@ public class MybatisIssuanceRequestRepository implements IssuanceRequestReposito
@Override
@Transactional
public void save(IssuanceRequest request, String auditActor) {
public void save(IssuanceRequest request, IssuanceAuditActor auditActor) {
saveInternal(request, Objects.requireNonNull(auditActor, "auditActor must not be null"));
}
private void saveInternal(IssuanceRequest request, String auditActor) {
private void saveInternal(IssuanceRequest request, IssuanceAuditActor auditActor) {
IssuanceAuditActor effectiveActor = auditActor == null ? IssuanceAuditActor.SYSTEM : auditActor;
String requestId = request.getId().value().toString();
IssuanceRequestEntity entity = toEntity(request);
IssuanceRequestEntity existing = requestMapper.selectById(requestId);
if (existing == null) {
assignAuditActorForInsert(entity, auditActor);
assignAuditActorForInsert(entity, effectiveActor);
requestMapper.insert(entity);
} else {
assignAuditActorForUpdate(entity, auditActor);
assignAuditActorForUpdate(entity, effectiveActor);
requestMapper.updateById(entity);
}
denominationMapper.delete(new LambdaQueryWrapper<IssuanceRequestDenominationEntity>()
.eq(IssuanceRequestDenominationEntity::getRequestId, requestId));
for (DenominationItem denomination : request.getDenominations()) {
denominationMapper.insert(toDenominationEntity(requestId, denomination));
denominationMapper.insert(toDenominationEntity(requestId, denomination, effectiveActor));
}
}
private void assignAuditActorForInsert(IssuanceRequestEntity entity, String auditActor) {
if (auditActor != null) {
entity.setCreatedBy(auditActor);
entity.setUpdatedBy(auditActor);
}
private void assignAuditActorForInsert(IssuanceRequestEntity entity, IssuanceAuditActor auditActor) {
entity.setCreatedBy(auditActor.getUsername());
entity.setUpdatedBy(auditActor.getUsername());
entity.setCreatedByUserId(auditActor.getUserId());
entity.setUpdatedByUserId(auditActor.getUserId());
}
private void assignAuditActorForUpdate(IssuanceRequestEntity entity, String auditActor) {
if (auditActor != null) {
entity.setUpdatedBy(auditActor);
}
private void assignAuditActorForUpdate(IssuanceRequestEntity entity, IssuanceAuditActor auditActor) {
entity.setUpdatedBy(auditActor.getUsername());
entity.setUpdatedByUserId(auditActor.getUserId());
}
@Override
@ -131,11 +132,16 @@ public class MybatisIssuanceRequestRepository implements IssuanceRequestReposito
return entity;
}
private IssuanceRequestDenominationEntity toDenominationEntity(String requestId, DenominationItem denomination) {
private IssuanceRequestDenominationEntity toDenominationEntity(String requestId, DenominationItem denomination,
IssuanceAuditActor auditActor) {
IssuanceRequestDenominationEntity entity = new IssuanceRequestDenominationEntity();
entity.setRequestId(requestId);
entity.setDenomination(denomination.getDenomination());
entity.setQuantity(denomination.getQuantity());
entity.setCreatedBy(auditActor.getUsername());
entity.setUpdatedBy(auditActor.getUsername());
entity.setCreatedByUserId(auditActor.getUserId());
entity.setUpdatedByUserId(auditActor.getUserId());
return entity;
}

@ -8,6 +8,7 @@ import com.yau.digitalrmb.issuance.application.query.IssuanceRequestView;
import com.yau.digitalrmb.issuance.application.service.CommercialBankIssuanceApplicationService;
import com.yau.digitalrmb.issuance.application.service.DenominationPlanService;
import com.yau.digitalrmb.issuance.domain.model.DenominationItem;
import com.yau.digitalrmb.issuance.domain.model.IssuanceAuditActor;
import com.yau.digitalrmb.issuance.interfaces.dto.CreateIssuanceRequest;
import com.yau.digitalrmb.issuance.interfaces.dto.DenominationItemRequest;
import com.yau.digitalrmb.issuance.interfaces.dto.DenominationPlanRequest;
@ -62,6 +63,13 @@ public class CommercialBankIssuanceController {
}
return result;
}
private String actor(Jwt jwt){String account=jwt.getClaimAsString("preferred_username"); return account==null||account.trim().isEmpty()?jwt.getSubject():account;}
private IssuanceAuditActor actor(Jwt jwt){
String username=jwt.getClaimAsString("preferred_username");
if(username==null||username.trim().isEmpty()) username=jwt.getSubject();
Object claim=jwt.getClaim("userId");
String userId=claim==null?jwt.getSubject():String.valueOf(claim);
try{return new IssuanceAuditActor(Long.parseLong(userId),username);}
catch(NumberFormatException exception){throw new IllegalArgumentException("current user id must be a number");}
}
private <T> ApiResponse<T> ok(T data){return ApiResponse.success(data,MDC.get(TraceIdFilter.MDC_KEY));}
}

@ -105,6 +105,8 @@ CREATE TABLE IF NOT EXISTS issuance_request (
updated_at TIMESTAMP NOT NULL,
created_by VARCHAR(64) NOT NULL,
updated_by VARCHAR(64) NOT NULL,
created_by_user_id BIGINT NOT NULL,
updated_by_user_id BIGINT NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE
);
@ -112,6 +114,12 @@ CREATE TABLE IF NOT EXISTS issuance_request_denomination (
request_id CHAR(36) NOT NULL,
denomination DECIMAL(10, 2) NOT NULL,
quantity INT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
created_by VARCHAR(64) NOT NULL,
updated_by VARCHAR(64) NOT NULL,
created_by_user_id BIGINT NOT NULL,
updated_by_user_id BIGINT NOT NULL,
PRIMARY KEY (request_id, denomination)
);

@ -6,6 +6,7 @@ import com.yau.digitalrmb.issuance.application.service.CommercialBankIssuanceApp
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.IssuanceAuditActor;
import com.yau.digitalrmb.issuance.domain.model.IssuanceApplicationId;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequest;
import com.yau.digitalrmb.issuance.domain.model.IssuanceRequestStatus;
@ -97,21 +98,21 @@ class MybatisIssuanceRequestRepositoryTest {
@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");
UUID requestId = commercialBankService.create(createCommand(), actor(101L, "creator")).getId();
assertAuditActor(requestId, 101L, "creator", 101L, "creator");
commercialBankService.update(requestId, updateCommand(), actor(102L, "updater"));
assertAuditActor(requestId, 101L, "creator", 102L, "updater");
commercialBankService.prepareMessage(requestId, actor(103L, "preparer"));
assertAuditActor(requestId, 101L, "creator", 103L, "preparer");
commercialBankService.digest(requestId, actor(104L, "digester"));
assertAuditActor(requestId, 101L, "creator", 104L, "digester");
commercialBankService.sign(requestId, actor(105L, "signer"));
assertAuditActor(requestId, 101L, "creator", 105L, "signer");
commercialBankService.packagePayload(requestId, actor(106L, "packager"));
assertAuditActor(requestId, 101L, "creator", 106L, "packager");
commercialBankService.send(requestId, actor(107L, "sender"));
assertAuditActor(requestId, 101L, "creator", 107L, "sender");
}
private CreateIssuanceRequestCommand createCommand() {
@ -124,10 +125,19 @@ class MybatisIssuanceRequestRepositoryTest {
Arrays.asList(new DenominationItem(100, 500)));
}
private void assertAuditActor(UUID requestId, String expectedCreatedBy, String expectedUpdatedBy) {
private IssuanceAuditActor actor(long userId, String username) {
return new IssuanceAuditActor(userId, username);
}
private void assertAuditActor(UUID requestId, long expectedCreatedByUserId, String expectedCreatedBy,
long expectedUpdatedByUserId, String expectedUpdatedBy) {
com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceRequestEntity entity =
requestMapper.selectById(requestId.toString());
assertThat(entity.getCreatedAt()).isNotNull();
assertThat(entity.getUpdatedAt()).isNotNull();
assertThat(entity.getCreatedByUserId()).isEqualTo(expectedCreatedByUserId);
assertThat(entity.getCreatedBy()).isEqualTo(expectedCreatedBy);
assertThat(entity.getUpdatedByUserId()).isEqualTo(expectedUpdatedByUserId);
assertThat(entity.getUpdatedBy()).isEqualTo(expectedUpdatedBy);
}

Loading…
Cancel
Save