This commit is contained in:
2026-08-13 12:22:31 +08:00
parent 16dc03a2e2
commit e742abd1cf
49 changed files with 821 additions and 50 deletions
@@ -0,0 +1,100 @@
package com.baiyue.finance;
import com.baiyue.finance.client.FinanceFileClient;
import com.baiyue.finance.client.FirstlegCostSnapshotClient;
import com.baiyue.finance.controller.PlatformFinanceController;
import com.baiyue.finance.dto.FinanceReadModels.CustomerAccountSummary;
import com.baiyue.finance.service.FinanceQueryService;
import com.baiyue.finance.service.FinanceService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.access.prepost.PreAuthorize;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class CustomerAccountSummaryBehaviorTest {
@Test
void aggregatesFormalSettlementsInOneCustomerScopedQuery() throws Exception {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
ResultSet rs = summaryRow("300.00", "140.00", "160.00", "25.00", "80.00");
when(jdbc.queryForObject(anyString(), any(RowMapper.class), any(), any(), any())).thenAnswer(invocation ->
((RowMapper<?>) invocation.getArgument(1)).mapRow(rs, 0));
FinanceQueryService service = new FinanceQueryService(jdbc, new ObjectMapper());
CustomerAccountSummary result = service.customerAccountSummary(9L);
assertThat(result.customerId()).isEqualTo(9L);
assertThat(result.currency()).isEqualTo("CNY");
assertThat(result.totalReceivable()).isEqualByComparingTo("300.00");
assertThat(result.receivedAmount()).isEqualByComparingTo("140.00");
assertThat(result.outstandingReceivable()).isEqualByComparingTo("160.00");
assertThat(result.pendingBilling()).isEqualByComparingTo("25.00");
assertThat(result.outstandingPayable()).isEqualByComparingTo("80.00");
var sql = org.mockito.ArgumentCaptor.forClass(String.class);
verify(jdbc).queryForObject(sql.capture(), any(RowMapper.class),
org.mockito.ArgumentMatchers.eq(9L), org.mockito.ArgumentMatchers.eq(9L), org.mockito.ArgumentMatchers.eq(9L));
assertThat(sql.getValue())
.contains("FROM fin_settlement s WHERE s.tenant_id=?")
.contains("s.direction='RECEIVABLE' AND s.status<>'VOIDED'")
.contains("s.direction='PAYABLE' AND s.status<>'VOIDED'")
.contains("f.customer_id=?", "f.status='AVAILABLE'", "'$.reviewStatus'", "'$.settlementIncluded'")
.contains("b.status<>'VOIDED'")
.doesNotContain("SUM(received_amount)", "FROM fin_receipt r");
}
@Test
void emptyCustomerReturnsZeroAmounts() throws Exception {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
ResultSet rs = summaryRow("0", "0", "0", "0", "0");
when(jdbc.queryForObject(anyString(), any(RowMapper.class), any(), any(), any())).thenAnswer(invocation ->
((RowMapper<?>) invocation.getArgument(1)).mapRow(rs, 0));
CustomerAccountSummary result = new FinanceQueryService(jdbc, new ObjectMapper()).customerAccountSummary(77L);
assertThat(result.totalReceivable()).isZero();
assertThat(result.receivedAmount()).isZero();
assertThat(result.outstandingReceivable()).isZero();
assertThat(result.pendingBilling()).isZero();
assertThat(result.outstandingPayable()).isZero();
}
@Test
void controllerUsesListPermissionAndDelegatesSummary() throws Exception {
FinanceQueryService query = mock(FinanceQueryService.class);
PlatformFinanceController controller = new PlatformFinanceController(mock(FinanceService.class),
mock(FinanceFileClient.class), mock(FirstlegCostSnapshotClient.class), query);
controller.customerAccountSummary(11L);
verify(query).customerAccountSummary(11L);
PreAuthorize permission = PlatformFinanceController.class
.getMethod("customerAccountSummary", long.class).getAnnotation(PreAuthorize.class);
assertThat(permission.value()).isEqualTo("hasAuthority('platform:finance:customer-account:list')");
}
private static ResultSet summaryRow(String total, String paid, String outstanding,
String pending, String payable) throws Exception {
ResultSet rs = mock(ResultSet.class);
when(rs.getBigDecimal("total_receivable")).thenReturn(new BigDecimal(total));
when(rs.getBigDecimal("received_amount")).thenReturn(new BigDecimal(paid));
when(rs.getBigDecimal("outstanding_receivable")).thenReturn(new BigDecimal(outstanding));
when(rs.getBigDecimal("pending_billing")).thenReturn(new BigDecimal(pending));
when(rs.getBigDecimal("outstanding_payable")).thenReturn(new BigDecimal(payable));
when(rs.getTimestamp("last_changed_at")).thenReturn(Timestamp.valueOf(LocalDateTime.of(2026, 8, 13, 12, 0)));
return rs;
}
}
@@ -15,14 +15,14 @@ class FinancePhase1PermissionContractTest {
@Test
void freezesG1AndG2ControllerPermissionsAndHttpActions() throws Exception {
assertEndpoint("listDerivedCustomerAccounts", GetMapping.class,
"platform:finance:customer-account:list", "platform:finance:account:view",
"platform:finance:customer-account:list", null,
int.class, int.class, String.class, String.class);
assertEndpoint("listDerivedCustomerAccountLedger", GetMapping.class,
"platform:finance:customer-account:list", "platform:finance:account:view",
"platform:finance:customer-account:list", null,
long.class, int.class, int.class,
String.class, java.time.LocalDate.class, java.time.LocalDate.class);
assertEndpoint("getFinanceFeeSource", GetMapping.class,
"platform:finance:customer-arap:list", "platform:finance:fee-source:view", long.class);
"platform:finance:customer-arap:list", null, long.class);
assertEndpoint("reviewFinanceFeeSource", PostMapping.class,
"platform:finance:fee-source:review", null,
com.baiyue.common.security.AuthenticatedUser.class, long.class, String.class,
@@ -30,8 +30,8 @@ class FinancePlatformListContractTest {
Map<String, String> expected = Map.of(
"feePool", "platform:finance:customer-arap:list",
"statements", "platform:finance:customer-statement:list",
"receivableSettlements", "platform:finance:settlement-ar:view",
"payableSettlements", "platform:finance:settlement-ap:view");
"receivableSettlements", "platform:finance:settlement-ar:list",
"payableSettlements", "platform:finance:settlement-ap:list");
for (var entry : expected.entrySet()) {
assertThat(find(entry.getKey()).getAnnotation(PreAuthorize.class).value()).contains(entry.getValue());
}
@@ -54,7 +54,7 @@ class FinanceSettlementBehaviorTest {
FinanceSettlementController controller = new FinanceSettlementController(controllerService);
SettlementDetail payable = detail("PAYABLE", "ISSUED", BigDecimal.ZERO, new BigDecimal("100.00"), 0);
when(controllerService.getSettlement(88L)).thenReturn(payable);
AuthenticatedUser apViewer = user(Set.of("platform:finance:settlement-ap:view"));
AuthenticatedUser apViewer = user(Set.of("platform:finance:settlement-ap:list"));
var response = controller.getSettlement(apViewer, 88L);
@@ -62,7 +62,7 @@ class FinanceSettlementBehaviorTest {
assertThat(response.data()).isSameAs(payable);
assertThat(response.requestId()).isNotBlank();
assertThatThrownBy(() -> controller.getSettlement(
user(Set.of("platform:finance:settlement-ar:view")), 88L)).isInstanceOf(AccessDeniedException.class);
user(Set.of("platform:finance:settlement-ar:list")), 88L)).isInstanceOf(AccessDeniedException.class);
}
@Test