feat(finance): add container settlement aggregate

This commit is contained in:
2026-09-08 21:03:47 +08:00
parent fc079fcdc0
commit 2e2ca3bef4
11 changed files with 596 additions and 1 deletions
@@ -0,0 +1,27 @@
package com.baiyue.finance;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class FinanceContainerSettlementOpenApiContractTest {
@Test
void contractPublishesFixedQueryAggregateAndExactReadPermissions() throws Exception {
String openApi = Files.readString(
Path.of("src/main/resources/openapi/finance-container-settlement-v1.yaml"),
StandardCharsets.UTF_8);
assertThat(openApi)
.contains("/platform/finance/v1/container-settlement-details:")
.contains("platform:finance:cost:view")
.contains("platform:finance:settlement-ar:list")
.contains("platform:finance:settlement-ap:list")
.contains("x-query-count: 3 fixed set-based SQL statements")
.contains("settledSourceFeeIds:", "costs:", "allocations:");
}
}
@@ -0,0 +1,29 @@
package com.baiyue.finance;
import com.baiyue.finance.mapper.FinanceContainerSettlementQueryMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
class FinanceContainerSettlementQueryMapperContractTest {
@Test
void mapperUsesThreeSetBasedStatementsAndNoPerSettlementOrPerCostLookup() {
assertThat(FinanceContainerSettlementQueryMapper.class.isAnnotationPresent(Mapper.class)).isTrue();
var methods = FinanceContainerSettlementQueryMapper.class.getDeclaredMethods();
assertThat(methods).hasSize(3);
assertThat(Arrays.stream(methods).allMatch(method -> method.isAnnotationPresent(Select.class))).isTrue();
String sql = Arrays.stream(methods)
.flatMap(method -> Arrays.stream(method.getAnnotation(Select.class).value()))
.reduce("", (left, right) -> left + "\n" + right);
assertThat(sql).contains("JOIN fin_settlement_line", "JOIN fin_fee_source",
"LEFT JOIN fin_cost_allocation", "c.tenant_id = #{customerId}",
"c.container_id = #{containerId}");
assertThat(sql).doesNotContain("${");
}
}
@@ -0,0 +1,105 @@
package com.baiyue.finance;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.finance.mapper.FinanceContainerSettlementQueryMapper;
import com.baiyue.finance.mapper.FinanceContainerSettlementQueryMapper.CostAllocationRow;
import com.baiyue.finance.mapper.FinanceContainerSettlementQueryMapper.OwnershipRow;
import com.baiyue.finance.service.FinanceContainerSettlementQueryService;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
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.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class FinanceContainerSettlementQueryServiceTest {
@Test
void returnsSettledSourcesAndGroupsAllCostAllocationsWithThreeFixedMapperCalls() {
FinanceContainerSettlementQueryMapper mapper = mock(FinanceContainerSettlementQueryMapper.class);
when(mapper.ownership(101L, 301L)).thenReturn(ownership(3));
when(mapper.settledSourceFeeIds(101L, 301L)).thenReturn(List.of(901L, 902L));
when(mapper.costs(101L, 301L)).thenReturn(List.of(
row(501L, 301L, 601L, 701L, false),
row(501L, 301L, 602L, 702L, true),
row(502L, 301L, null, null, false)));
var result = new FinanceContainerSettlementQueryService(mapper).details(101L, 301L);
assertThat(result.customerId()).isEqualTo(101L);
assertThat(result.containerId()).isEqualTo(301L);
assertThat(result.settledSourceFeeIds()).containsExactly(901L, 902L);
assertThat(result.costs()).hasSize(2);
assertThat(result.costs().get(0).costId()).isEqualTo(501L);
assertThat(result.costs().get(0).allocations()).extracting(allocation -> allocation.orderId())
.containsExactly(701L, 702L);
assertThat(result.costs().get(1).allocations()).isEmpty();
verify(mapper, times(1)).ownership(101L, 301L);
verify(mapper, times(1)).settledSourceFeeIds(101L, 301L);
verify(mapper, times(1)).costs(101L, 301L);
}
@Test
void rejectsUnknownAndCrossCustomerContainerBeforeReadingFinanceDetails() {
FinanceContainerSettlementQueryMapper mapper = mock(FinanceContainerSettlementQueryMapper.class);
var service = new FinanceContainerSettlementQueryService(mapper);
when(mapper.ownership(101L, 301L)).thenReturn(ownership(0));
when(mapper.ownership(101L, 302L)).thenReturn(ownership(0));
assertThatThrownBy(() -> service.details(101L, 301L))
.isInstanceOf(BizException.class).hasMessageContaining("不属于客户");
assertThatThrownBy(() -> service.details(101L, 302L))
.isInstanceOf(BizException.class).hasMessageContaining("不属于客户");
verify(mapper, never()).settledSourceFeeIds(101L, 301L);
verify(mapper, never()).settledSourceFeeIds(101L, 302L);
verify(mapper, never()).costs(101L, 301L);
verify(mapper, never()).costs(101L, 302L);
}
@Test
void rejectsInvalidBusinessIdsWithoutQueryingMapper() {
FinanceContainerSettlementQueryMapper mapper = mock(FinanceContainerSettlementQueryMapper.class);
var service = new FinanceContainerSettlementQueryService(mapper);
assertThatThrownBy(() -> service.details(0, 301)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> service.details(101, 0)).isInstanceOf(IllegalArgumentException.class);
verify(mapper, never()).ownership(0, 301);
verify(mapper, never()).ownership(101, 0);
}
private static OwnershipRow ownership(long customerLinks) {
var row = new OwnershipRow();
row.setCustomerLinkCount(customerLinks);
return row;
}
private static CostAllocationRow row(long costId, long containerId, Long allocationId,
Long orderId, boolean roundingAdjustment) {
var row = new CostAllocationRow();
row.setCostId(costId);
row.setCostNo("COST-" + costId);
row.setCostType("CONTAINER_SHARED");
row.setCostOrigin("MANUAL");
row.setContainerId(containerId);
row.setSupplierName("供应商");
row.setFeeItemName("柜费");
row.setAmount(new BigDecimal("100.00"));
row.setCurrency("CNY");
row.setStatus("CONFIRMED");
row.setAllocationId(allocationId);
row.setOrderId(orderId);
row.setInboundId(orderId == null ? null : orderId + 1000);
row.setVolumeCbm(allocationId == null ? null : new BigDecimal("1.00"));
row.setTotalVolumeCbm(allocationId == null ? null : new BigDecimal("2.00"));
row.setAllocationRate(allocationId == null ? null : new BigDecimal("0.50"));
row.setAllocatedAmount(allocationId == null ? null : new BigDecimal("50.00"));
row.setRoundingAdjustment(roundingAdjustment);
return row;
}
}
@@ -0,0 +1,39 @@
package com.baiyue.finance;
import com.baiyue.finance.controller.PlatformFinanceContainerSettlementController;
import com.baiyue.finance.dto.FinanceContainerSettlementDtos.ContainerSettlementDetails;
import com.baiyue.finance.service.FinanceContainerSettlementQueryService;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import java.lang.reflect.Method;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class PlatformFinanceContainerSettlementControllerTest {
@Test
void delegatesOneAggregateReadAndRequiresAllThreeFinanceReadPermissions() throws Exception {
FinanceContainerSettlementQueryService service = mock(FinanceContainerSettlementQueryService.class);
var expected = new ContainerSettlementDetails(101L, 301L, List.of(901L), List.of());
when(service.details(101L, 301L)).thenReturn(expected);
var controller = new PlatformFinanceContainerSettlementController(service);
var response = controller.details(101L, 301L);
assertThat(response.data()).isSameAs(expected);
verify(service).details(101L, 301L);
Method method = PlatformFinanceContainerSettlementController.class
.getMethod("details", long.class, long.class);
assertThat(method.getAnnotation(GetMapping.class).value())
.containsExactly("/container-settlement-details");
assertThat(method.getAnnotation(PreAuthorize.class).value())
.contains("platform:finance:cost:view", "platform:finance:settlement-ar:list",
"platform:finance:settlement-ap:list");
}
}