This commit is contained in:
2026-08-11 18:16:59 +08:00
parent 47c1c7922f
commit 7e52d0c025
13 changed files with 1057 additions and 4 deletions
@@ -0,0 +1,149 @@
package com.baiyue.finance;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.finance.controller.FinancePhase1OperationController;
import com.baiyue.finance.dto.FinancePhase1Dtos;
import com.baiyue.finance.dto.FinancePhase1Dtos.FeeSourceDetail;
import com.baiyue.finance.dto.FinancePhase1Dtos.PageEnvelope;
import com.baiyue.finance.service.FinanceEventException;
import com.baiyue.finance.service.FinancePhase1OperationService;
import com.fasterxml.jackson.databind.ObjectMapper;
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.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.time.LocalDate;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
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.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class FinancePhase1OperationBehaviorTest {
@Mock FinancePhase1OperationService operationService;
@Test
void controllerDelegatesFiveFrozenOperationsAndUsesAuthenticatedOperator() {
FinancePhase1OperationController controller = new FinancePhase1OperationController(operationService);
AuthenticatedUser user = new AuthenticatedUser(71L, null, null, null, "finance", "ADMIN", "WEB", Set.of());
var review = new FinancePhase1Dtos.ReviewRequest("APPROVE", "核对无误", 0);
var inclusion = new FinancePhase1Dtos.SettlementInclusionRequest(true, "进入本期结算", 1);
controller.listDerivedCustomerAccounts(1, 20, "柏悦", "ACTIVE");
controller.listDerivedCustomerAccountLedger(9L, 2, 50, "RECEIPT", LocalDate.of(2026, 8, 1), LocalDate.of(2026, 8, 31));
controller.getFinanceFeeSource(88L);
controller.reviewFinanceFeeSource(user, 88L, "review-key", review);
controller.updateFeeSourceSettlementInclusion(user, 88L, "include-key", inclusion);
verify(operationService).listDerivedCustomerAccounts(1, 20, "柏悦", "ACTIVE");
verify(operationService).listDerivedCustomerAccountLedger(eq(9L), eq(2), eq(50), eq("RECEIPT"), any(), any());
verify(operationService).getFinanceFeeSource(88L);
verify(operationService).reviewFinanceFeeSource(88L, 71L, "review-key", review);
verify(operationService).updateFeeSourceSettlementInclusion(88L, 71L, "include-key", inclusion);
}
@Test
void pageEnvelopeAlwaysContainsTraceMetadata() {
PageEnvelope<String> response = PageEnvelope.success(List.of("row"), 1, 20, 1);
assertThat(response.code()).isEqualTo("200");
assertThat(response.requestId()).isNotBlank();
assertThat(response.traceId()).isNotBlank();
assertThat(response.page().total()).isEqualTo(1);
}
@Test
void ledgerRejectsInvalidCustomerBeforeAnyQuery() {
FinancePhase1OperationService service = new FinancePhase1OperationService(mock(JdbcTemplate.class), new ObjectMapper());
assertThatThrownBy(() -> service.listDerivedCustomerAccountLedger(0, 1, 20, null, null, null))
.isInstanceOf(FinanceEventException.class)
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-REQ-400");
}
@Test
void rejectedFeeCannotBeIncludedInSettlement() throws Exception {
JdbcTemplate jdbc = feeSourceJdbc("REJECTED", false, 2);
FinancePhase1OperationService service = new FinancePhase1OperationService(jdbc, new ObjectMapper());
assertThatThrownBy(() -> service.updateFeeSourceSettlementInclusion(88L, 71L, "include-key",
new FinancePhase1Dtos.SettlementInclusionRequest(true, "进入结算", 2)))
.isInstanceOf(FinanceEventException.class)
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-FEE-409");
}
@Test
void reviewUsesExpectedVersionAndReturnsUpdatedDetail() throws Exception {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
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);
String review = reads.getAndIncrement() == 0 ? "PENDING" : "APPROVED";
int version = "PENDING".equals(review) ? 0 : 1;
return List.of(mapper.mapRow(resultSet(review, false, version), 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"), 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());
FeeSourceDetail result = service.reviewFinanceFeeSource(88L, 71L, "review-key",
new FinancePhase1Dtos.ReviewRequest("APPROVE", "核对无误", 0));
assertThat(result.reviewStatus()).isEqualTo("APPROVED");
assertThat(result.operationVersion()).isEqualTo(1);
verify(jdbc).update(startsWith("UPDATE fin_fee_source SET review_status="), eq("APPROVED"), eq("核对无误"), eq(88L), eq(0));
}
private static JdbcTemplate feeSourceJdbc(String reviewStatus, boolean included, int version) throws Exception {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
when(jdbc.query(startsWith("SELECT * FROM fin_fee_source"), any(RowMapper.class), (Object[]) any())).thenAnswer(invocation -> {
@SuppressWarnings("unchecked") RowMapper<FeeSourceDetail> mapper = invocation.getArgument(1);
return List.of(mapper.mapRow(resultSet(reviewStatus, included, version), 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(anyString(), any(), any(), any(), any())).thenReturn(List.of());
return jdbc;
}
private static ResultSet resultSet(String reviewStatus, boolean included, int version) throws Exception {
ResultSet rs = mock(ResultSet.class);
when(rs.getLong("id")).thenReturn(88L);
when(rs.getLong("source_fee_id")).thenReturn(700L);
when(rs.getLong("customer_id")).thenReturn(9L);
when(rs.getLong("inbound_id")).thenReturn(100L);
when(rs.getLong("container_id")).thenReturn(200L);
when(rs.getInt("source_fee_version")).thenReturn(3);
when(rs.getString("source_type")).thenReturn("INBOUND_FEE");
when(rs.getString("direction")).thenReturn("RECEIVABLE");
when(rs.getString("fee_item_name")).thenReturn("头程运费");
when(rs.getString("currency_code")).thenReturn("CNY");
when(rs.getString("status")).thenReturn("AVAILABLE");
when(rs.getString("source_hash")).thenReturn("a".repeat(64));
when(rs.getString("review_status")).thenReturn(reviewStatus);
when(rs.getString("review_reason")).thenReturn(null);
when(rs.getString("settlement_reason")).thenReturn(null);
when(rs.getBoolean("settlement_included")).thenReturn(included);
when(rs.getInt("operation_version")).thenReturn(version);
when(rs.getBigDecimal("amount")).thenReturn(new BigDecimal("100.00"));
when(rs.getString("source_snapshot")).thenReturn("{\"customerName\":\"客户9\",\"businessDate\":\"2026-08-11\",\"chargeableQuantity\":\"10\",\"reviewStatus\":\"" + reviewStatus + "\",\"settlementIncluded\":" + included + ",\"operationVersion\":" + version + "}");
return rs;
}
}