This commit is contained in:
2026-08-11 18:43:50 +08:00
parent e1487162a5
commit bc5d9a6215
25 changed files with 1424 additions and 28 deletions
@@ -18,6 +18,9 @@ import org.springframework.jdbc.core.RowMapper;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@@ -29,6 +32,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -67,16 +71,46 @@ class FinancePhase1OperationBehaviorTest {
@Test
void ledgerRejectsInvalidCustomerBeforeAnyQuery() {
FinancePhase1OperationService service = new FinancePhase1OperationService(mock(JdbcTemplate.class), new ObjectMapper());
FinancePhase1OperationService service = new FinancePhase1OperationService(mock(JdbcTemplate.class), objectMapper());
assertThatThrownBy(() -> service.listDerivedCustomerAccountLedger(0, 1, 20, null, null, null))
.isInstanceOf(FinanceEventException.class)
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-REQ-400");
}
@Test
void accountListReturnsFixedPagedDto() {
AccountJdbc jdbc = new AccountJdbc();
FinancePhase1OperationService service = new FinancePhase1OperationService(jdbc, objectMapper());
PageEnvelope<FinancePhase1Dtos.AccountSummary> result =
service.listDerivedCustomerAccounts(1, 20, "客户9", "ACTIVE");
assertThat(result.data()).singleElement().satisfies(account -> {
assertThat(account.customerId()).isEqualTo(9L);
assertThat(account.customerName()).isEqualTo("客户9");
assertThat(account.outstandingReceivable()).isEqualByComparingTo("100.00");
});
assertThat(result.page().total()).isEqualTo(1);
assertThat(result.requestId()).isNotBlank();
assertThat(result.traceId()).isNotBlank();
}
@Test
void ledgerQueriesEveryDerivedSourceWithinCustomerScope() {
AccountJdbc jdbc = new AccountJdbc();
FinancePhase1OperationService service = new FinancePhase1OperationService(jdbc, objectMapper());
PageEnvelope<FinancePhase1Dtos.LedgerEntry> result =
service.listDerivedCustomerAccountLedger(9L, 1, 20, null, null, null);
assertThat(result.data()).isEmpty();
assertThat(jdbc.ledgerCustomerIds).containsExactly(9L, 9L, 9L);
}
@Test
void rejectedFeeCannotBeIncludedInSettlement() throws Exception {
JdbcTemplate jdbc = feeSourceJdbc("REJECTED", false, 2);
FinancePhase1OperationService service = new FinancePhase1OperationService(jdbc, new ObjectMapper());
FinancePhase1OperationService service = new FinancePhase1OperationService(jdbc, objectMapper());
assertThatThrownBy(() -> service.updateFeeSourceSettlementInclusion(88L, 71L, "include-key",
new FinancePhase1Dtos.SettlementInclusionRequest(true, "进入结算", 2)))
@@ -84,6 +118,46 @@ class FinancePhase1OperationBehaviorTest {
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-FEE-409");
}
@Test
void feeSourceDetailUsesFixedDtoAndRealCustomerScope() throws Exception {
FinancePhase1OperationService service = new FinancePhase1OperationService(
feeSourceJdbc("APPROVED", true, 2), objectMapper());
FeeSourceDetail detail = service.getFinanceFeeSource(88L);
assertThat(detail.feeSourceId()).isEqualTo(88L);
assertThat(detail.customerId()).isEqualTo(9L);
assertThat(detail.currency()).isEqualTo("CNY");
assertThat(detail.reviewStatus()).isEqualTo("APPROVED");
assertThat(detail.settlementIncluded()).isTrue();
}
@Test
void approvedFeeCanBeIncludedWithExpectedVersion() throws Exception {
JdbcTemplate jdbc = mock(JdbcTemplate.class, invocation ->
"update".equals(invocation.getMethod().getName())
? 1 : org.mockito.Mockito.RETURNS_DEFAULTS.answer(invocation));
AtomicInteger reads = new AtomicInteger();
when(jdbc.query(startsWith("SELECT * FROM fin_fee_source"), any(RowMapper.class), (Object[]) any())).thenAnswer(invocation -> {
@SuppressWarnings("unchecked") RowMapper<FeeSourceDetail> mapper = invocation.getArgument(1);
boolean included = reads.getAndIncrement() > 0;
return List.of(mapper.mapRow(resultSet("APPROVED", included, included ? 2 : 1), 0));
});
when(jdbc.queryForObject(startsWith("SELECT COUNT(*)>0"), eq(Boolean.class), eq(700L), eq(3))).thenReturn(false);
when(jdbc.query(startsWith("SELECT created_at,action"), any(RowMapper.class), eq(9L), eq(88L))).thenReturn(List.of());
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(9L), eq(71L),
eq("FEE_SOURCE_SETTLEMENT"), eq("include-key"))).thenReturn(List.of());
FinancePhase1OperationService service = new FinancePhase1OperationService(jdbc, objectMapper());
FeeSourceDetail result = service.updateFeeSourceSettlementInclusion(88L, 71L, "include-key",
new FinancePhase1Dtos.SettlementInclusionRequest(true, "进入结算", 1));
assertThat(result.settlementIncluded()).isTrue();
assertThat(result.operationVersion()).isEqualTo(2);
verify(jdbc).update(startsWith("UPDATE fin_fee_source SET settlement_included="),
eq(true), eq("进入结算"), eq(88L), eq(1));
}
@Test
void reviewUsesExpectedVersionAndReturnsUpdatedDetail() throws Exception {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
@@ -96,12 +170,18 @@ class FinancePhase1OperationBehaviorTest {
});
when(jdbc.queryForObject(startsWith("SELECT COUNT(*)>0"), eq(Boolean.class), eq(700L), eq(3))).thenReturn(false);
when(jdbc.query(startsWith("SELECT created_at,action"), any(RowMapper.class), eq(9L), eq(88L))).thenReturn(List.of());
when(jdbc.queryForList(startsWith("SELECT request_hash"), any(), any(), any(), any())).thenReturn(List.of());
when(jdbc.update(startsWith("INSERT INTO fin_idempotency_record"), (Object[]) any())).thenReturn(1);
when(jdbc.update(startsWith("UPDATE fin_fee_source SET review_status="), (Object[]) any())).thenReturn(1);
when(jdbc.update(startsWith("INSERT INTO fin_audit_log"), (Object[]) any())).thenReturn(1);
when(jdbc.update(startsWith("UPDATE fin_idempotency_record SET status='SUCCEEDED'"), (Object[]) any())).thenReturn(1);
FinancePhase1OperationService service = new FinancePhase1OperationService(jdbc, new ObjectMapper());
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(9L), eq(71L),
eq("FEE_SOURCE_REVIEW"), eq("review-key"))).thenReturn(List.of());
when(jdbc.update(startsWith("INSERT INTO fin_idempotency_record"), eq(9L), eq(71L),
eq("FEE_SOURCE_REVIEW"), eq("review-key"), anyString())).thenReturn(1);
when(jdbc.update(startsWith("UPDATE fin_fee_source SET review_status="),
eq("APPROVED"), eq("核对无误"), eq(88L), eq(0))).thenReturn(1);
when(jdbc.update(startsWith("INSERT INTO fin_audit_log"), eq(9L), eq("FIN_FEE_SOURCE"),
eq(88L), eq("FEE_SOURCE_REVIEW_APPROVE"), eq("核对无误"), anyString(), anyString(),
eq(71L), eq("operator-71"), any(), any())).thenReturn(1);
when(jdbc.update(startsWith("UPDATE fin_idempotency_record SET status='SUCCEEDED'"),
eq(88L), eq(9L), eq(71L), eq("FEE_SOURCE_REVIEW"), eq("review-key"))).thenReturn(1);
FinancePhase1OperationService service = new FinancePhase1OperationService(jdbc, objectMapper());
FeeSourceDetail result = service.reviewFinanceFeeSource(88L, 71L, "review-key",
new FinancePhase1Dtos.ReviewRequest("APPROVE", "核对无误", 0));
@@ -119,7 +199,8 @@ class FinancePhase1OperationBehaviorTest {
});
when(jdbc.queryForObject(startsWith("SELECT COUNT(*)>0"), eq(Boolean.class), eq(700L), eq(3))).thenReturn(false);
when(jdbc.query(startsWith("SELECT created_at,action"), any(RowMapper.class), eq(9L), eq(88L))).thenReturn(List.of());
when(jdbc.queryForList(anyString(), any(), any(), any(), any())).thenReturn(List.of());
lenient().when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(9L), eq(71L),
eq("FEE_SOURCE_SETTLEMENT"), eq("include-key"))).thenReturn(List.of());
return jdbc;
}
@@ -146,4 +227,35 @@ class FinancePhase1OperationBehaviorTest {
when(rs.getString("source_snapshot")).thenReturn("{\"customerName\":\"客户9\",\"businessDate\":\"2026-08-11\",\"chargeableQuantity\":\"10\",\"reviewStatus\":\"" + reviewStatus + "\",\"settlementIncluded\":" + included + ",\"operationVersion\":" + version + "}");
return rs;
}
private static ObjectMapper objectMapper() {
return new ObjectMapper().findAndRegisterModules();
}
private static final class AccountJdbc extends JdbcTemplate {
private final List<Long> ledgerCustomerIds = new ArrayList<>();
@Override
public <T> T queryForObject(String sql, Class<T> requiredType, Object... args) {
if (sql.startsWith("SELECT COUNT(*)")) return requiredType.cast(1L);
return null;
}
@Override
@SuppressWarnings("unchecked")
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
if (sql.startsWith("SELECT c.customer_id")) {
return (List<T>) List.of(new FinancePhase1Dtos.AccountSummary(
9L, "9", "客户9", null, null, new BigDecimal("100.00"),
new BigDecimal("20.00"), new BigDecimal("30.00"), BigDecimal.ZERO,
"ACTIVE", LocalDateTime.of(2026, 8, 11, 10, 0)));
}
if (sql.startsWith("SELECT id,bill_no") || sql.startsWith("SELECT id,receipt_no")
|| sql.startsWith("SELECT id,cost_no")) {
ledgerCustomerIds.add(((Number) Arrays.asList(args).get(0)).longValue());
return List.of();
}
return List.of();
}
}
}