feat(finance): add container settlement aggregate
This commit is contained in:
+27
@@ -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:");
|
||||
}
|
||||
}
|
||||
+29
@@ -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("${");
|
||||
}
|
||||
}
|
||||
+105
@@ -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;
|
||||
}
|
||||
}
|
||||
+39
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user