优化
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.firstleg.client;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FinanceFeeSourceEventClientContractTest {
|
||||
@Test
|
||||
void usesFrozenFinanceInboxPathAndIdempotencyHeaders() {
|
||||
FeignClient client = FinanceFeeSourceEventClient.class.getAnnotation(FeignClient.class);
|
||||
assertThat(client.name()).isEqualTo("finance-service");
|
||||
assertThat(client.path()).isEqualTo("/internal/finance/v1/fee-source-events");
|
||||
var method = FinanceFeeSourceEventClient.class.getDeclaredMethods()[0];
|
||||
assertThat(Arrays.stream(method.getParameterAnnotations())
|
||||
.flatMap(Arrays::stream)
|
||||
.filter(RequestHeader.class::isInstance)
|
||||
.map(RequestHeader.class::cast)
|
||||
.map(RequestHeader::value))
|
||||
.contains("X-Internal-Service-Token", "Idempotency-Key");
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.service.FinanceFeeSourceSnapshotService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class InternalFinanceFeeSourceControllerTest {
|
||||
private final FinanceFeeSourceSnapshotService service = mock(FinanceFeeSourceSnapshotService.class);
|
||||
private final InternalFinanceFeeSourceController controller = controller();
|
||||
|
||||
@Test
|
||||
void rejectsMissingOrWrongInternalTokenBeforeSnapshotLookup() {
|
||||
assertThatThrownBy(() -> controller.inbound(null, 21L, 31L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("令牌无效");
|
||||
assertThatThrownBy(() -> controller.container("wrong", 41L, 51L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("令牌无效");
|
||||
}
|
||||
|
||||
@Test
|
||||
void routesAuthorizedSnapshotLookup() {
|
||||
controller.inbound("finance-token", 21L, 31L);
|
||||
controller.container("finance-token", 41L, 51L);
|
||||
|
||||
verify(service).inbound(21L, 31L);
|
||||
verify(service).container(41L, 51L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesFrozenInternalPaths() {
|
||||
var mapping = InternalFinanceFeeSourceController.class
|
||||
.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
|
||||
assertThat(mapping.value()).containsExactly("/internal/firstleg/v1");
|
||||
assertThat(methodPath("inbound")).isEqualTo("/inbounds/{inboundId}/fees/{feeId}/source-snapshot");
|
||||
assertThat(methodPath("container")).isEqualTo("/containers/{containerId}/fees/{feeId}/source-snapshot");
|
||||
}
|
||||
|
||||
private InternalFinanceFeeSourceController controller() {
|
||||
InternalFinanceFeeSourceController value = new InternalFinanceFeeSourceController(service);
|
||||
ReflectionTestUtils.setField(value, "expectedToken", "finance-token");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String methodPath(String name) {
|
||||
return java.util.Arrays.stream(InternalFinanceFeeSourceController.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().equals(name)).findFirst().orElseThrow()
|
||||
.getAnnotation(org.springframework.web.bind.annotation.GetMapping.class).value()[0];
|
||||
}
|
||||
}
|
||||
+19
@@ -8,6 +8,8 @@ import org.apache.ibatis.annotations.Select;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -18,6 +20,9 @@ class PdaConvergenceSqlContractTest {
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_item_assignment")
|
||||
.contains("assignment_type VARCHAR(16) NOT NULL COMMENT '分配类型:CLAIMED作业员领取、ASSIGNED管理员分配'")
|
||||
.contains("CONSTRAINT fk_loading_pick_item_assignment_item")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation")
|
||||
.contains("UNIQUE KEY uk_loading_pick_operation_id")
|
||||
@@ -30,6 +35,11 @@ class PdaConvergenceSqlContractTest {
|
||||
.contains("platform:firstleg:pda:assign")
|
||||
.contains("platform:firstleg:pda:work")
|
||||
.contains("platform:firstleg:pda:review");
|
||||
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
assertThat(createBlock(unified, "fl_loading_pick_item_assignment"))
|
||||
.isEqualTo(createBlock(sql, "fl_loading_pick_item_assignment"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -51,4 +61,13 @@ class PdaConvergenceSqlContractTest {
|
||||
.contains("ON DUPLICATE KEY UPDATE")
|
||||
.contains("柜次PDA自动汇总");
|
||||
}
|
||||
|
||||
private static String createBlock(String sql, String table) {
|
||||
String marker = "CREATE TABLE IF NOT EXISTS " + table + " (";
|
||||
int start = sql.indexOf(marker);
|
||||
int end = sql.indexOf(";", start);
|
||||
assertThat(start).as("missing table %s", table).isGreaterThanOrEqualTo(0);
|
||||
assertThat(end).as("missing table terminator %s", table).isGreaterThan(start);
|
||||
return sql.substring(start, end + 1).replace("\r\n", "\n");
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FinanceFeeSourceMigrationContractTest {
|
||||
@Test
|
||||
void migrationDefinesIdempotentOutboxEnvelopeAndContainerFeeModel() throws Exception {
|
||||
String sql = new ClassPathResource("db/finance-fee-source-migration.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql).contains("information_schema.columns", "event_key", "payload_hash",
|
||||
"uk_inbound_outbox_event_key", "next_retry_at", "locked_at", "locked_by",
|
||||
"idx_inbound_outbox_finance_relay", "CREATE TABLE IF NOT EXISTS fl_container_fee_detail",
|
||||
"settlement_included", "source_version", "RECEIVABLE", "PAYABLE",
|
||||
"chk_container_fee_currency", "currency = 'CNY'", "firstleg_finance_assert_schema");
|
||||
}
|
||||
}
|
||||
+141
@@ -5,11 +5,21 @@ import org.junit.jupiter.api.Test;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class Phase3FinalUnifiedSqlContractTest {
|
||||
private static final Path UNIFIED = Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
private static final Pattern CREATE_TABLE = Pattern.compile(
|
||||
"(?i)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+(?:`[^`]+`\\.)?`?([a-zA-Z0-9_]+)`?\\s*\\(");
|
||||
private static final Pattern COLUMN = Pattern.compile(
|
||||
"(?i)^\\s*`?([a-zA-Z0-9_]+)`?\\s+(bigint|binary|bit|blob|boolean|char|date|datetime|decimal|double|enum|float|int|integer|json|longblob|longtext|mediumblob|mediumint|mediumtext|numeric|real|set|smallint|text|time|timestamp|tinyblob|tinyint|tinytext|varbinary|varchar|year)\\b");
|
||||
private static final Pattern CHINESE_COMMENT = Pattern.compile(
|
||||
"(?i)\\bCOMMENT\\s*=?\\s*'(?:(?:''|\\\\'|[^'])*[\\u3400-\\u9fff](?:''|\\\\'|[^'])*)'");
|
||||
|
||||
@Test
|
||||
void unifiedEntryOwnsPhase3OutboundCustomsPortAndEvidenceHardening() throws Exception {
|
||||
@@ -27,6 +37,10 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_split_detail")
|
||||
.contains("trg_inbound_split_detail_before_insert")
|
||||
.contains("trg_inbound_split_detail_before_update")
|
||||
.contains("trg_inbound_split_detail_before_delete")
|
||||
.contains("CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH'))")
|
||||
.contains("phase3_loading_evidence_add_column")
|
||||
.contains("DROP INDEX uk_loading_evidence_type")
|
||||
@@ -74,6 +88,86 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
"iam/src/main/resources/db/phase3-warehouse-roles.sql");
|
||||
}
|
||||
|
||||
@Test
|
||||
void financeFeeSourceMigrationsAreContiguousAndSynchronised() throws Exception {
|
||||
String unified = normalise(Files.readString(resolveUnified()));
|
||||
assertBlock(unified, "-- 来源:firstleg/src/main/resources/db/finance-fee-source-migration.sql",
|
||||
"-- 来源:finance/src/main/resources/db/fee-source-event-migration.sql",
|
||||
"firstleg/src/main/resources/db/finance-fee-source-migration.sql");
|
||||
assertBlock(unified, "-- 来源:finance/src/main/resources/db/fee-source-event-migration.sql",
|
||||
"-- END FINANCE FEE SOURCE MODULE MIGRATIONS",
|
||||
"finance/src/main/resources/db/fee-source-event-migration.sql");
|
||||
|
||||
assertThat(unified)
|
||||
.contains("column_name IN ('source_type','source_no','direction','container_id','fee_item_code',")
|
||||
.contains("column_name IN ('payload_hash','result_resource_id','error_code')")
|
||||
.contains("column_name IN ('event_key', 'event_version', 'aggregate_sequence', 'payload_hash',")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_container_fee_detail");
|
||||
}
|
||||
|
||||
@Test
|
||||
void commentUpgradeIsIdempotentAndGuardsNineSchemaStructure() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE PROCEDURE queue_table_comment")
|
||||
.contains("CREATE PROCEDURE queue_column_comment")
|
||||
.contains("CREATE PROCEDURE apply_one_table_comments")
|
||||
.contains("CREATE PROCEDURE apply_queued_comments")
|
||||
.contains("NOT (BINARY v_current_comment=BINARY p_column_comment)")
|
||||
.contains("SET v_signature_before=SHA2")
|
||||
.contains("BINARY target.signature_before")
|
||||
.contains("字段注释升级遇到未知数据类型")
|
||||
.contains("字段注释升级遇到未知EXTRA定义")
|
||||
.contains("字段注释升级遇到未知生成列结构")
|
||||
.contains("MySQL DDL 会隐式提交")
|
||||
.contains("不使用 GROUP_CONCAT")
|
||||
.contains("DECLARE table_cursor CURSOR FOR")
|
||||
.contains("CALL apply_one_table_comments(v_schema_name,v_table_name)")
|
||||
.contains("CALL apply_queued_comments();")
|
||||
.contains("PREPARE table_comment_batch_stmt FROM @table_comment_batch_sql")
|
||||
.contains("九个业务数据库未完整创建")
|
||||
.contains("存在缺失中文注释的业务表")
|
||||
.contains("存在缺失中文注释的业务字段")
|
||||
.doesNotContain("CREATE PROCEDURE upgrade_column_comment");
|
||||
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(150);
|
||||
assertThat(count(sql, "CALL queue_column_comment(")).isEqualTo(2456);
|
||||
assertThat(sql.indexOf("CALL apply_queued_comments();",
|
||||
sql.indexOf("-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS")))
|
||||
.isGreaterThan(sql.indexOf("-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedCreateDefinitionsHaveChineseTableAndColumnComments() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
List<String> issues = new ArrayList<>();
|
||||
Matcher tableMatcher = CREATE_TABLE.matcher(sql);
|
||||
int tableCount = 0;
|
||||
int columnCount = 0;
|
||||
while (tableMatcher.find()) {
|
||||
tableCount++;
|
||||
String table = tableMatcher.group(1);
|
||||
int open = tableMatcher.end() - 1;
|
||||
int close = matchingParen(sql, open);
|
||||
int semicolon = sql.indexOf(';', close);
|
||||
assertThat(semicolon).as("missing semicolon for %s", table).isGreaterThan(close);
|
||||
if (!CHINESE_COMMENT.matcher(sql.substring(close + 1, semicolon)).find()) {
|
||||
issues.add(table + ": table comment");
|
||||
}
|
||||
for (String definition : splitDefinitions(sql.substring(open + 1, close))) {
|
||||
Matcher columnMatcher = COLUMN.matcher(definition);
|
||||
if (!columnMatcher.find()) continue;
|
||||
columnCount++;
|
||||
if (!CHINESE_COMMENT.matcher(definition).find()) {
|
||||
issues.add(table + "." + columnMatcher.group(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
assertThat(tableCount).isGreaterThan(100);
|
||||
assertThat(columnCount).isGreaterThan(2000);
|
||||
assertThat(issues).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void passIsUniqueAndOnlyTheFinalExecutableStatement() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
@@ -153,4 +247,51 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
private static int count(String value, String token) {
|
||||
return value.split(java.util.regex.Pattern.quote(token), -1).length - 1;
|
||||
}
|
||||
|
||||
private static int matchingParen(String sql, int open) {
|
||||
int depth = 0;
|
||||
char quote = 0;
|
||||
for (int index = open; index < sql.length(); index++) {
|
||||
char current = sql.charAt(index);
|
||||
if (quote != 0) {
|
||||
if (current == '\\') index++;
|
||||
else if (current == quote) {
|
||||
if (index + 1 < sql.length() && sql.charAt(index + 1) == quote) index++;
|
||||
else quote = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (current == '\'' || current == '"' || current == '`') quote = current;
|
||||
else if (current == '(') depth++;
|
||||
else if (current == ')' && --depth == 0) return index;
|
||||
}
|
||||
throw new AssertionError("unclosed CREATE TABLE at " + open);
|
||||
}
|
||||
|
||||
private static List<String> splitDefinitions(String body) {
|
||||
List<String> definitions = new ArrayList<>();
|
||||
int start = 0;
|
||||
int depth = 0;
|
||||
char quote = 0;
|
||||
for (int index = 0; index < body.length(); index++) {
|
||||
char current = body.charAt(index);
|
||||
if (quote != 0) {
|
||||
if (current == '\\') index++;
|
||||
else if (current == quote) {
|
||||
if (index + 1 < body.length() && body.charAt(index + 1) == quote) index++;
|
||||
else quote = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (current == '\'' || current == '"' || current == '`') quote = current;
|
||||
else if (current == '(') depth++;
|
||||
else if (current == ')') depth--;
|
||||
else if (current == ',' && depth == 0) {
|
||||
definitions.add(body.substring(start, index));
|
||||
start = index + 1;
|
||||
}
|
||||
}
|
||||
definitions.add(body.substring(start));
|
||||
return definitions;
|
||||
}
|
||||
}
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.ContainerFeeDetailDtos;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
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.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ContainerFeeDetailServiceTest {
|
||||
private final ContainerFeeDetailRepository repository = mock(ContainerFeeDetailRepository.class);
|
||||
private final SmartLoadingRepository loading = mock(SmartLoadingRepository.class);
|
||||
private final FeeItemRepository feeItems = mock(FeeItemRepository.class);
|
||||
private final FinanceFeeSourceOutboxService outbox = mock(FinanceFeeSourceOutboxService.class);
|
||||
private final ContainerFeeDetailService service =
|
||||
new ContainerFeeDetailService(repository, loading, feeItems, outbox);
|
||||
|
||||
@Test
|
||||
void rejectsContainerThatDoesNotContainCustomerOrders() {
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setId(41L);
|
||||
when(loading.plan(41L)).thenReturn(plan);
|
||||
LoadingPlanItemEntity other = new LoadingPlanItemEntity();
|
||||
other.setAccountId(20002L);
|
||||
when(loading.items(41L)).thenReturn(List.of(other));
|
||||
|
||||
assertThatThrownBy(() -> service.create(10001L, 41L, 9L, request(true)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不属于该客户");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedSettlementFeePublishesContainerSource() {
|
||||
ownedPlan();
|
||||
ContainerFeeDetailEntity approved = new ContainerFeeDetailEntity();
|
||||
approved.setId(51L);
|
||||
approved.setTenantId(10001L);
|
||||
approved.setLoadingPlanId(41L);
|
||||
approved.setSourceVersion(1);
|
||||
approved.setSettlementIncluded(true);
|
||||
when(repository.find(10001L, 41L, 51L)).thenReturn(approved);
|
||||
when(repository.review(10001L, 41L, 51L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
service.approve(10001L, 41L, 51L, 9L, "admin");
|
||||
|
||||
verify(outbox).publishContainer(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||
eq(10001L), eq(41L), eq(51L), eq(1), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adjustmentIncrementsSourceVersionAndReturnsToReview() {
|
||||
ownedPlan();
|
||||
FeeItemEntity item = item();
|
||||
when(feeItems.findItem(11L)).thenReturn(item);
|
||||
ContainerFeeDetailEntity current = new ContainerFeeDetailEntity();
|
||||
current.setId(51L);
|
||||
current.setTenantId(10001L);
|
||||
current.setLoadingPlanId(41L);
|
||||
current.setSourceVersion(1);
|
||||
when(repository.find(10001L, 41L, 51L)).thenReturn(current);
|
||||
when(repository.adjust(any(), eq(1))).thenReturn(1);
|
||||
|
||||
service.adjust(10001L, 41L, 51L, 1, 9L, request(true));
|
||||
|
||||
ArgumentCaptor<ContainerFeeDetailEntity> captor = ArgumentCaptor.forClass(ContainerFeeDetailEntity.class);
|
||||
verify(repository).adjust(captor.capture(), eq(1));
|
||||
assertThat(captor.getValue().getSourceVersion()).isEqualTo(2);
|
||||
assertThat(captor.getValue().getReviewStatus()).isEqualTo("PENDING");
|
||||
}
|
||||
|
||||
private void ownedPlan() {
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setId(41L);
|
||||
when(loading.plan(41L)).thenReturn(plan);
|
||||
LoadingPlanItemEntity item = new LoadingPlanItemEntity();
|
||||
item.setAccountId(10001L);
|
||||
when(loading.items(41L)).thenReturn(List.of(item));
|
||||
}
|
||||
|
||||
private static FeeItemEntity item() {
|
||||
FeeItemEntity value = new FeeItemEntity();
|
||||
value.setId(11L);
|
||||
value.setFeeCode("TRUCKING");
|
||||
value.setFeeTypeName("拖车费");
|
||||
value.setWarehousePayable(true);
|
||||
value.setStatus("ACTIVE");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static ContainerFeeDetailDtos.SaveRequest request(boolean settlementIncluded) {
|
||||
return new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE",
|
||||
LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY",
|
||||
settlementIncluded, "整柜拖车费");
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import static org.mockito.Mockito.when;
|
||||
@DisplayName("FBA费用状态机")
|
||||
@SpringJUnitConfig(classes = {FbaPricingService.class, FbaPricingServiceTest.TestConfig.class})
|
||||
class FbaPricingServiceTest {
|
||||
@MockBean private FinanceFeeSourceOutboxService financeOutbox;
|
||||
private static final long TENANT = 10001L;
|
||||
private static final long OPERATOR = 20001L;
|
||||
private static final long INBOUND = 30001L;
|
||||
@@ -224,7 +225,9 @@ class FbaPricingServiceTest {
|
||||
|
||||
assertThat(first.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
||||
assertThat(repeated.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
||||
verify(repository, times(1)).insertOutbox(any());
|
||||
verify(financeOutbox, times(1)).publishInbound(
|
||||
eq(FinanceFeeSourceOutboxService.PUBLISHED), eq(TENANT), eq(INBOUND),
|
||||
eq(501L), eq(1), eq(NOW));
|
||||
verify(repository, times(1)).sendFee(TENANT, 501L, OPERATOR, NOW);
|
||||
verify(feeDetails, times(2)).publishFreight(TENANT, INBOUND, 501L, OPERATOR, null, NOW);
|
||||
}
|
||||
@@ -250,7 +253,8 @@ class FbaPricingServiceTest {
|
||||
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
||||
verify(repository).markInboundFeeSent(TENANT, INBOUND, "INBOUNDED");
|
||||
verify(repository).insertOutbox(any());
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||
eq(TENANT), eq(INBOUND), eq(501L), eq(1), eq(NOW));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -300,7 +304,8 @@ class FbaPricingServiceTest {
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(inbound.getProcessStatus()).isEqualTo("INBOUNDED");
|
||||
verify(repository).supersedeSent(TENANT, 501L);
|
||||
verify(repository).insertOutbox(any());
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.ADJUSTED),
|
||||
eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW));
|
||||
}
|
||||
|
||||
private void readyRepository() {
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.util.List;
|
||||
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.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -40,6 +41,8 @@ class FeeManagementServiceTest {
|
||||
private FeeItemRepository repository;
|
||||
@MockBean
|
||||
private FbaPricingRepository pricingRepository;
|
||||
@MockBean
|
||||
private FinanceFeeSourceOutboxService financeOutbox;
|
||||
@Resource
|
||||
private FeeItemService feeItems;
|
||||
@Resource
|
||||
@@ -268,6 +271,54 @@ class FeeManagementServiceTest {
|
||||
verify(repository).review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("审核通过且参与结算的人工费用写入Finance事件")
|
||||
void publishesApprovedSettlementFee() {
|
||||
InboundFeeDetailEntity approved = new InboundFeeDetailEntity();
|
||||
approved.setId(31L);
|
||||
approved.setTenantId(10001L);
|
||||
approved.setInboundId(21L);
|
||||
approved.setFeeItemId(11L);
|
||||
approved.setSourceType("MANUAL");
|
||||
approved.setSourceFeeVersion(1);
|
||||
approved.setSettlementIncluded(true);
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(approved);
|
||||
when(repository.review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
details.approve(10001L, 21L, 31L, 9L, "admin");
|
||||
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||
eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("作废已入池人工费用引用当前版本并发布VOIDED")
|
||||
void voidsPublishedSettlementFee() {
|
||||
InboundFeeDetailEntity current = new InboundFeeDetailEntity();
|
||||
current.setId(31L);
|
||||
current.setTenantId(10001L);
|
||||
current.setInboundId(21L);
|
||||
current.setFeeItemId(11L);
|
||||
current.setSourceType("MANUAL");
|
||||
current.setSourceFeeVersion(1);
|
||||
current.setSettlementIncluded(true);
|
||||
InboundFeeDetailEntity voided = new InboundFeeDetailEntity();
|
||||
voided.setId(31L);
|
||||
voided.setTenantId(10001L);
|
||||
voided.setInboundId(21L);
|
||||
voided.setFeeItemId(11L);
|
||||
voided.setSourceType("MANUAL");
|
||||
voided.setSourceFeeVersion(1);
|
||||
voided.setSettlementIncluded(true);
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(current, voided);
|
||||
when(repository.voidApproved(10001L, 21L, 31L, 1, 9L)).thenReturn(1);
|
||||
|
||||
details.voidApproved(10001L, 21L, 31L, 1, 9L);
|
||||
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.VOIDED),
|
||||
eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
private static FeeItemDtos.SaveRequest itemRequest() {
|
||||
return new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", "仓库租赁费",
|
||||
false, false, true, true, true, "应付仓库租赁费");
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.client.FinanceFeeSourceEventClient;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceEvent;
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
|
||||
import com.baiyue.firstleg.repository.FinanceFeeSourceOutboxRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class FinanceFeeSourceOutboxRelayTest {
|
||||
private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 11, 15, 0);
|
||||
private final ObjectMapper json = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
private final FinanceFeeSourceOutboxRepository repository = mock(FinanceFeeSourceOutboxRepository.class);
|
||||
private final FinanceFeeSourceEventClient client = mock(FinanceFeeSourceEventClient.class);
|
||||
private final FinanceFeeSourceOutboxRelay relay = relay();
|
||||
|
||||
@Test
|
||||
void claimsThenPublishesOnlyAfterFinanceAcknowledgesSameEvent() throws Exception {
|
||||
InboundOutboxEntity row = event();
|
||||
ready(row);
|
||||
FinanceFeeSourceEvent envelope = json.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class);
|
||||
when(client.receive("finance-token", envelope.idempotencyKey(), envelope)).thenReturn(
|
||||
ApiResponse.success(new FinanceFeeSourceEventClient.Acknowledgement(
|
||||
901L)));
|
||||
when(repository.published(row.getId(), "worker-1", NOW)).thenReturn(true);
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).recoverStale(NOW.minusMinutes(5), NOW.plusSeconds(10), 12);
|
||||
verify(repository).claim(row.getId(), "worker-1", NOW);
|
||||
verify(client).receive("finance-token", envelope.idempotencyKey(), envelope);
|
||||
verify(repository).published(row.getId(), "worker-1", NOW);
|
||||
verify(repository, never()).retry(any(Long.class), any(), any(), any(), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentWorkerThatLosesClaimDoesNotDeliver() {
|
||||
InboundOutboxEntity row = event();
|
||||
when(repository.pending(50, NOW)).thenReturn(List.of(row));
|
||||
when(repository.claim(row.getId(), "worker-1", NOW)).thenReturn(false);
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(client, never()).receive(any(), any(), any());
|
||||
verify(repository, never()).published(any(Long.class), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transientFailureReturnsClaimToPendingWithBackoff() throws Exception {
|
||||
InboundOutboxEntity row = event();
|
||||
ready(row);
|
||||
FinanceFeeSourceEvent envelope = json.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class);
|
||||
when(client.receive("finance-token", envelope.idempotencyKey(), envelope))
|
||||
.thenThrow(new IllegalStateException("finance unavailable"));
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).retry(row.getId(), "worker-1", NOW.plusSeconds(10),
|
||||
"finance unavailable", false);
|
||||
verify(repository, never()).published(any(Long.class), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidEventHashFailsPermanentlyWithoutCallingFinance() {
|
||||
InboundOutboxEntity row = event();
|
||||
row.setPayloadHash("0".repeat(64));
|
||||
ready(row);
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).retry(eq(row.getId()), eq("worker-1"), eq(NOW.plusSeconds(10)),
|
||||
any(), eq(true));
|
||||
verify(client, never()).receive(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void financeIdempotencyConflictFailsPermanently() throws Exception {
|
||||
InboundOutboxEntity row = event();
|
||||
ready(row);
|
||||
FinanceFeeSourceEvent envelope = json.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class);
|
||||
when(client.receive("finance-token", envelope.idempotencyKey(), envelope)).thenReturn(
|
||||
new ApiResponse<>("FIN-EVT-002", "同一事件ID对应不同请求内容", null, "r", "t", Instant.now()));
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).retry(eq(row.getId()), eq("worker-1"), eq(NOW.plusSeconds(10)),
|
||||
any(), eq(true));
|
||||
verify(repository, never()).published(any(Long.class), any(), any());
|
||||
}
|
||||
|
||||
private void ready(InboundOutboxEntity row) {
|
||||
when(repository.pending(50, NOW)).thenReturn(List.of(row));
|
||||
when(repository.claim(row.getId(), "worker-1", NOW)).thenReturn(true);
|
||||
when(repository.findClaimed(row.getId(), "worker-1")).thenReturn(row);
|
||||
}
|
||||
|
||||
private InboundOutboxEntity event() {
|
||||
InboundOutboxMapper mapper = mock(InboundOutboxMapper.class);
|
||||
FinanceFeeSourceOutboxService producer = new FinanceFeeSourceOutboxService(mapper, json);
|
||||
producer.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED,
|
||||
10001L, 21L, 31L, 1, NOW);
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(mapper).insert(captor.capture());
|
||||
InboundOutboxEntity row = captor.getValue();
|
||||
row.setId(71L);
|
||||
return row;
|
||||
}
|
||||
|
||||
private FinanceFeeSourceOutboxRelay relay() {
|
||||
Clock clock = Clock.fixed(Instant.parse("2026-08-11T07:00:00Z"), ZoneId.of("Asia/Shanghai"));
|
||||
FinanceFeeSourceOutboxRelay value =
|
||||
new FinanceFeeSourceOutboxRelay(repository, client, json, clock, "worker-1");
|
||||
ReflectionTestUtils.setField(value, "internalToken", "finance-token");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class FinanceFeeSourceOutboxServiceTest {
|
||||
private final InboundOutboxMapper mapper = mock(InboundOutboxMapper.class);
|
||||
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
private final FinanceFeeSourceOutboxService service =
|
||||
new FinanceFeeSourceOutboxService(mapper, objectMapper);
|
||||
|
||||
@Test
|
||||
void writesContractEnvelopeWithShortPayloadAndStableHash() {
|
||||
LocalDateTime occurredAt = LocalDateTime.of(2026, 8, 11, 12, 0);
|
||||
|
||||
service.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED,
|
||||
10001L, 21L, 31L, 1, occurredAt);
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(mapper).insert(captor.capture());
|
||||
InboundOutboxEntity event = captor.getValue();
|
||||
assertThat(event.getEventType()).isEqualTo("FIRSTLEG_FEE_PUBLISHED");
|
||||
assertThat(event.getEventVersion()).isEqualTo(1);
|
||||
assertThat(event.getAggregateSequence()).isEqualTo(1);
|
||||
assertThat(event.getPayloadHash()).matches("[0-9a-f]{64}");
|
||||
assertThat(event.getPayloadJson()).contains("\"producer\":\"firstleg-service\"",
|
||||
"\"inboundId\":\"21\"", "\"feeId\":\"31\"", "\"feeVersion\":1")
|
||||
.doesNotContain("amount", "currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
void treatsDuplicateEventKeyAsSuccessfulReplay() {
|
||||
doThrow(new DuplicateKeyException("duplicate event_key")).when(mapper)
|
||||
.insert((InboundOutboxEntity) org.mockito.ArgumentMatchers.any());
|
||||
|
||||
assertThatCode(() -> service.publishContainer(FinanceFeeSourceOutboxService.VOIDED,
|
||||
10001L, 41L, 51L, 2, LocalDateTime.now())).doesNotThrowAnyException();
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class FinanceFeeSourceSnapshotServiceTest {
|
||||
private final FeeItemRepository inboundFees = mock(FeeItemRepository.class);
|
||||
private final ContainerFeeDetailRepository containerFees = mock(ContainerFeeDetailRepository.class);
|
||||
private final FinanceFeeSourceSnapshotService service =
|
||||
new FinanceFeeSourceSnapshotService(inboundFees, containerFees);
|
||||
|
||||
@Test
|
||||
void returnsReviewedSettlementSnapshotWithStableSourceHash() {
|
||||
InboundFeeDetailEntity detail = new InboundFeeDetailEntity();
|
||||
detail.setId(31L);
|
||||
detail.setTenantId(10001L);
|
||||
detail.setInboundId(21L);
|
||||
detail.setFeeItemId(11L);
|
||||
detail.setSourceType("MANUAL");
|
||||
detail.setSourceFeeVersion(2);
|
||||
detail.setDirection("RECEIVABLE");
|
||||
detail.setAmount(new BigDecimal("100.00"));
|
||||
detail.setCurrency("CNY");
|
||||
detail.setReviewStatus("APPROVED");
|
||||
detail.setPublicationStatus("PUBLISHED");
|
||||
detail.setSettlementIncluded(true);
|
||||
when(inboundFees.findDetail(21L, 31L)).thenReturn(detail);
|
||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||
inbound.setId(21L);
|
||||
inbound.setOrderId(81L);
|
||||
inbound.setInboundCode("RK-21");
|
||||
inbound.setCustomerName("客户A");
|
||||
when(inboundFees.findInbound(10001L, 21L)).thenReturn(inbound);
|
||||
FeeItemEntity item = new FeeItemEntity();
|
||||
item.setId(11L);
|
||||
item.setFeeCode("STORAGE");
|
||||
when(inboundFees.findItem(11L)).thenReturn(item);
|
||||
|
||||
FinanceFeeSourceSnapshot snapshot = service.inbound(21L, 31L);
|
||||
|
||||
assertThat(snapshot.customerId()).isEqualTo(10001L);
|
||||
assertThat(snapshot.orderId()).isEqualTo(81L);
|
||||
assertThat(snapshot.feeVersion()).isEqualTo(2);
|
||||
assertThat(snapshot.sourceHash()).matches("[0-9a-f]{64}");
|
||||
detail.setPublicationStatus("VOIDED");
|
||||
assertThat(service.inbound(21L, 31L).sourceHash()).isEqualTo(snapshot.sourceHash());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsFeeThatDoesNotBelongToInboundPath() {
|
||||
InboundFeeDetailEntity detail = new InboundFeeDetailEntity();
|
||||
detail.setInboundId(22L);
|
||||
when(inboundFees.findDetail(21L, 31L)).thenReturn(detail);
|
||||
|
||||
assertThatThrownBy(() -> service.inbound(21L, 31L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不存在");
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.when;
|
||||
@DisplayName("发送费用并入库状态编排")
|
||||
@SpringJUnitConfig(InboundCompletionOrchestrator.class)
|
||||
class InboundCompletionOrchestratorTest {
|
||||
@MockBean private FinanceFeeSourceOutboxService financeOutbox;
|
||||
@MockBean
|
||||
private InboundWorkflowRepository repository;
|
||||
@MockBean
|
||||
@@ -78,6 +79,11 @@ class InboundCompletionOrchestratorTest {
|
||||
fixture.order.setProcessStatus("INBOUNDING");
|
||||
when(fixture.repository.completeInbound(10001L, 10L, 9L, 5)).thenReturn(1);
|
||||
when(fixture.repository.markFeeSent(10001L, 10L, 30L, 9L)).thenReturn(1);
|
||||
com.baiyue.firstleg.entity.InboundFeeDetailEntity published =
|
||||
new com.baiyue.firstleg.entity.InboundFeeDetailEntity();
|
||||
published.setSourceFeeVersion(1);
|
||||
when(feeDetails.publishFreight(eq(10001L), eq(10L), eq(30L), eq(9L), eq("操作员"),
|
||||
org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class))).thenReturn(published);
|
||||
|
||||
fixture.service.completeSendFeeAndInbound(
|
||||
10001L, 9L, "操作员", 10L, 30L, 5, "complete-1");
|
||||
@@ -86,7 +92,9 @@ class InboundCompletionOrchestratorTest {
|
||||
verify(fixture.repository).markFeeSent(10001L, 10L, 30L, 9L);
|
||||
verify(feeDetails).publishFreight(eq(10001L), eq(10L), eq(30L), eq(9L), eq("操作员"),
|
||||
org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class));
|
||||
verify(fixture.repository).insertOutbox(any());
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||
eq(10001L), eq(10L), eq(30L), eq(1),
|
||||
org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -102,7 +110,9 @@ class InboundCompletionOrchestratorTest {
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.CONFLICT);
|
||||
verify(fixture.repository, never()).markFeeSent(anyLong(), anyLong(), anyLong(), anyLong());
|
||||
verify(fixture.repository, never()).insertOutbox(any());
|
||||
verify(financeOutbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(),
|
||||
org.mockito.ArgumentMatchers.anyInt(),
|
||||
org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user