优化
This commit is contained in:
+18
-2
@@ -2,6 +2,8 @@ package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.service.FinanceFeeSourceSnapshotService;
|
||||
import com.baiyue.firstleg.service.FinanceFeeSourceCommandService;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceCommandDtos.VoidRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@@ -15,6 +17,7 @@ import static org.mockito.Mockito.verify;
|
||||
|
||||
class InternalFinanceFeeSourceControllerTest {
|
||||
private final FinanceFeeSourceSnapshotService service = mock(FinanceFeeSourceSnapshotService.class);
|
||||
private final FinanceFeeSourceCommandService commands = mock(FinanceFeeSourceCommandService.class);
|
||||
private final InternalFinanceFeeSourceController controller = controller();
|
||||
|
||||
@Test
|
||||
@@ -34,6 +37,16 @@ class InternalFinanceFeeSourceControllerTest {
|
||||
verify(service).container(41L, 51L, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void routesAuthorizedVoidCommandWithIdempotencyAndHash() {
|
||||
VoidRequest request = new VoidRequest("INBOUND_FEE", 31L, 2, 9L,
|
||||
21L, null, 71L, "finance", "测试作废");
|
||||
|
||||
controller.voidSource("finance-token", "void-key", "a".repeat(64), request);
|
||||
|
||||
verify(commands).voidSource("void-key", "a".repeat(64), request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesFrozenInternalPaths() {
|
||||
var mapping = InternalFinanceFeeSourceController.class
|
||||
@@ -52,11 +65,14 @@ class InternalFinanceFeeSourceControllerTest {
|
||||
.contains("/internal/firstleg/v1/containers/{containerId}/fees/{feeId}/source-snapshot:")
|
||||
.contains("{name: sourceVersion, in: query, required: true")
|
||||
.contains("supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true")
|
||||
.contains("为空时仅要求供应商ACTIVE");
|
||||
.contains("为空时仅要求供应商ACTIVE")
|
||||
.contains("/internal/firstleg/v1/fee-sources/commands/void:")
|
||||
.contains("FIRSTLEG_FEE_VOIDED Outbox")
|
||||
.contains("FinanceFeeSourceVoidCommand:");
|
||||
}
|
||||
|
||||
private InternalFinanceFeeSourceController controller() {
|
||||
InternalFinanceFeeSourceController value = new InternalFinanceFeeSourceController(service);
|
||||
InternalFinanceFeeSourceController value = new InternalFinanceFeeSourceController(service, commands);
|
||||
ReflectionTestUtils.setField(value, "expectedToken", "finance-token");
|
||||
return value;
|
||||
}
|
||||
|
||||
+3
@@ -37,6 +37,9 @@ class FinanceFeeSourceMigrationContractTest {
|
||||
"source_version", "supplier_id", "supplier_name", "supplier_category",
|
||||
"CREATE TABLE IF NOT EXISTS fl_container_fee_source_snapshot",
|
||||
"uk_container_fee_source_version",
|
||||
"CREATE TABLE IF NOT EXISTS fl_finance_fee_source_command",
|
||||
"uk_finance_fee_command_idem",
|
||||
"request_hash CHAR(64) NOT NULL",
|
||||
"firstleg_finance_add_column('fl_container_fee_detail', 'supplier_id'", "RECEIVABLE", "PAYABLE",
|
||||
"firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_id'",
|
||||
"Firstleg Finance迁移失败:入库费用供应商快照字段不完整",
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceCommandDtos.VoidRequest;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.FinanceFeeSourceCommandEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
|
||||
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import com.baiyue.firstleg.repository.FinanceFeeSourceCommandRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FinanceFeeSourceCommandServiceTest {
|
||||
@Mock FinanceFeeSourceCommandRepository commands;
|
||||
@Mock FeeItemRepository inboundFees;
|
||||
@Mock FbaPricingRepository pricing;
|
||||
@Mock ContainerFeeDetailRepository containerFees;
|
||||
@Mock ContainerFeeDetailService containerFeeService;
|
||||
@Mock FinanceFeeSourceOutboxService outbox;
|
||||
private ObjectMapper objectMapper;
|
||||
private FinanceFeeSourceCommandService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
service = new FinanceFeeSourceCommandService(commands, inboundFees, pricing,
|
||||
containerFees, containerFeeService, outbox, objectMapper);
|
||||
doAnswer(invocation -> {
|
||||
FinanceFeeSourceCommandEntity value = invocation.getArgument(0);
|
||||
value.setId(901L);
|
||||
return null;
|
||||
}).when(commands).insert(any());
|
||||
lenient().when(commands.complete(eq(901L), eq("VOIDED"), any())).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void voidsManualInboundBusinessFactAndPublishesEvent() throws Exception {
|
||||
VoidRequest request = inboundRequest(700L, 2);
|
||||
InboundFeeDetailEntity detail = inboundDetail("MANUAL", 777L, 700L, 2, 9L);
|
||||
when(inboundFees.findSourceVersion(100L, 700L, 2)).thenReturn(detail);
|
||||
when(inboundFees.voidApproved(9L, 100L, 777L, 2, 71L)).thenReturn(1);
|
||||
|
||||
var result = service.voidSource("void-key", hash(request), request);
|
||||
|
||||
assertThat(result.status()).isEqualTo("VOIDED");
|
||||
assertThat(result.replayed()).isFalse();
|
||||
verify(outbox).publishInbound(org.mockito.ArgumentMatchers.eq(FinanceFeeSourceOutboxService.VOIDED),
|
||||
org.mockito.ArgumentMatchers.eq(9L), org.mockito.ArgumentMatchers.eq(100L),
|
||||
org.mockito.ArgumentMatchers.eq(700L), org.mockito.ArgumentMatchers.eq(2), any());
|
||||
verify(pricing, never()).voidSentFee(anyLong(), anyLong(), anyLong(), anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void voidsFbaPricingAndMaterializedInboundDetailAtomically() throws Exception {
|
||||
VoidRequest request = inboundRequest(700L, 3);
|
||||
when(inboundFees.findSourceVersion(100L, 700L, 3))
|
||||
.thenReturn(inboundDetail("FBA_PRICING", 778L, 700L, 3, 9L));
|
||||
when(pricing.voidSentFee(9L, 100L, 700L, 3, any())).thenReturn(1);
|
||||
when(pricing.markInboundFeeVoided(9L, 100L)).thenReturn(1);
|
||||
when(inboundFees.voidApproved(9L, 100L, 778L, 3, 71L)).thenReturn(1);
|
||||
|
||||
service.voidSource("void-fba", hash(request), request);
|
||||
|
||||
verify(pricing).voidSentFee(9L, 100L, 700L, 3, any());
|
||||
verify(pricing).markInboundFeeVoided(9L, 100L);
|
||||
verify(outbox).publishInbound(org.mockito.ArgumentMatchers.eq(FinanceFeeSourceOutboxService.VOIDED),
|
||||
org.mockito.ArgumentMatchers.eq(9L), org.mockito.ArgumentMatchers.eq(100L),
|
||||
org.mockito.ArgumentMatchers.eq(700L), org.mockito.ArgumentMatchers.eq(3), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void routesContainerVoidThroughExistingBusinessService() throws Exception {
|
||||
VoidRequest request = new VoidRequest("CONTAINER_FEE", 800L, 4, 9L,
|
||||
null, 200L, 71L, "finance", "柜次费用作废");
|
||||
ContainerFeeDetailEntity detail = new ContainerFeeDetailEntity();
|
||||
detail.setId(800L); detail.setTenantId(9L); detail.setLoadingPlanId(200L);
|
||||
detail.setSourceVersion(4); detail.setSourceStatus("ACTIVE");
|
||||
when(containerFees.find(200L, 800L)).thenReturn(detail);
|
||||
|
||||
service.voidSource("void-container", hash(request), request);
|
||||
|
||||
verify(containerFeeService).voidApproved(9L, 200L, 800L, 4, 71L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCrossCustomerSourceBeforeBusinessMutation() throws Exception {
|
||||
VoidRequest request = inboundRequest(700L, 2);
|
||||
when(inboundFees.findSourceVersion(100L, 700L, 2))
|
||||
.thenReturn(inboundDetail("MANUAL", 777L, 700L, 2, 10L));
|
||||
|
||||
assertThatThrownBy(() -> service.voidSource("void-cross", hash(request), request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("指定客户");
|
||||
|
||||
verify(inboundFees, never()).voidApproved(anyLong(), anyLong(), anyLong(), anyInt(), anyLong());
|
||||
verify(outbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(), anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaysCompletedCommandWithoutRepeatingBusinessMutation() throws Exception {
|
||||
VoidRequest request = inboundRequest(700L, 2);
|
||||
String requestHash = hash(request);
|
||||
doThrow(new DuplicateKeyException("duplicate")).when(commands).insert(any());
|
||||
FinanceFeeSourceCommandEntity existing = new FinanceFeeSourceCommandEntity();
|
||||
existing.setRequestHash(requestHash); existing.setStatus("SUCCEEDED");
|
||||
existing.setResultStatus("VOIDED");
|
||||
when(commands.find(9L, 71L, "VOID", "void-replay")).thenReturn(existing);
|
||||
|
||||
var result = service.voidSource("void-replay", requestHash, request);
|
||||
|
||||
assertThat(result.replayed()).isTrue();
|
||||
verify(inboundFees, never()).findSourceVersion(anyLong(), anyLong(), anyInt());
|
||||
}
|
||||
|
||||
private VoidRequest inboundRequest(long feeId, int version) {
|
||||
return new VoidRequest("INBOUND_FEE", feeId, version, 9L,
|
||||
100L, null, 71L, "finance", "入库费用作废");
|
||||
}
|
||||
|
||||
private static InboundFeeDetailEntity inboundDetail(String sourceType, long id,
|
||||
long sourceFeeId, int version,
|
||||
long tenantId) {
|
||||
InboundFeeDetailEntity value = new InboundFeeDetailEntity();
|
||||
value.setId(id); value.setTenantId(tenantId); value.setInboundId(100L);
|
||||
value.setSourceType(sourceType); value.setSourceFeeId(sourceFeeId);
|
||||
value.setSourceFeeVersion(version); value.setReviewStatus("APPROVED");
|
||||
value.setPublicationStatus("PUBLISHED");
|
||||
return value;
|
||||
}
|
||||
|
||||
private String hash(VoidRequest request) throws Exception {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(objectMapper.writeValueAsString(request).getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user