Merge remote-tracking branch 'origin/test' into dev
This commit is contained in:
+2
-2
@@ -27,7 +27,7 @@ public class PlatformFinanceContainerSettlementController {
|
|||||||
+ "hasAuthority('platform:finance:settlement-ap:list')")
|
+ "hasAuthority('platform:finance:settlement-ap:list')")
|
||||||
@Operation(summary = "按客户和柜次批量查询已结算柜费及成本分摊")
|
@Operation(summary = "按客户和柜次批量查询已结算柜费及成本分摊")
|
||||||
public ApiResponse<ContainerSettlementDetails> details(@RequestParam long customerId,
|
public ApiResponse<ContainerSettlementDetails> details(@RequestParam long customerId,
|
||||||
@RequestParam long containerId) {
|
@RequestParam long containerBookingId) {
|
||||||
return FinanceResponses.success(service.details(customerId, containerId));
|
return FinanceResponses.success(service.details(customerId, containerBookingId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ public class PlatformFinanceController {
|
|||||||
@BusinessAudit(event = "FINANCE_COST_CREATE", resourceType = "FINANCE_COST")
|
@BusinessAudit(event = "FINANCE_COST_CREATE", resourceType = "FINANCE_COST")
|
||||||
public ApiResponse<?> cost(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CostRequest request) {
|
public ApiResponse<?> cost(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CostRequest request) {
|
||||||
if (request.currency() != null && !"CNY".equals(request.currency())) throw new IllegalArgumentException("首期仅支持CNY");
|
if (request.currency() != null && !"CNY".equals(request.currency())) throw new IllegalArgumentException("首期仅支持CNY");
|
||||||
var internal = new FinanceService.CostRequest(null, request.costType(), request.orderId(), null, request.containerId(), null, null, "MANUAL", request.expenseName(), request.amount(), request.voucherFileIds(), request.remark());
|
|
||||||
java.util.List<FinanceService.ContainerOrderVolume> volumes = java.util.List.of();
|
java.util.List<FinanceService.ContainerOrderVolume> volumes = java.util.List.of();
|
||||||
|
Long containerBookingId = null;
|
||||||
if ("CONTAINER_SHARED".equals(request.costType())) {
|
if ("CONTAINER_SHARED".equals(request.costType())) {
|
||||||
if (request.containerId() == null) throw new IllegalArgumentException("整柜成本必须指定柜次");
|
if (request.containerId() == null) throw new IllegalArgumentException("整柜成本必须指定柜次");
|
||||||
var response = costSnapshotClient.loadingPlan(fileToken, request.customerId(), request.containerId());
|
var response = costSnapshotClient.loadingPlan(fileToken, request.customerId(), request.containerId());
|
||||||
@@ -180,9 +180,13 @@ public class PlatformFinanceController {
|
|||||||
|| snapshot.orders() == null || snapshot.orders().isEmpty()) {
|
|| snapshot.orders() == null || snapshot.orders().isEmpty()) {
|
||||||
throw new IllegalArgumentException("Firstleg未返回有效柜内订单体积快照");
|
throw new IllegalArgumentException("Firstleg未返回有效柜内订单体积快照");
|
||||||
}
|
}
|
||||||
|
containerBookingId = snapshot.containerBookingId();
|
||||||
volumes = snapshot.orders().stream().map(v -> new FinanceService.ContainerOrderVolume(v.orderId(),
|
volumes = snapshot.orders().stream().map(v -> new FinanceService.ContainerOrderVolume(v.orderId(),
|
||||||
v.inboundId(), snapshot.loadingPlanId(), snapshot.customerId(), v.volumeCbm())).toList();
|
v.inboundId(), snapshot.containerBookingId(), snapshot.customerId(), v.volumeCbm())).toList();
|
||||||
}
|
}
|
||||||
|
var internal = new FinanceService.CostRequest(null, request.costType(), request.orderId(), null,
|
||||||
|
containerBookingId, null, null, "MANUAL", request.expenseName(), request.amount(),
|
||||||
|
request.voucherFileIds(), request.remark());
|
||||||
return FinanceResponses.success(resourceView(service.createCost(request.customerId(), user.userId(), internal, volumes, key)));
|
return FinanceResponses.success(resourceView(service.createCost(request.customerId(), user.userId(), internal, volumes, key)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public final class FinanceContainerSettlementDtos {
|
|||||||
|
|
||||||
public record ContainerSettlementDetails(
|
public record ContainerSettlementDetails(
|
||||||
long customerId,
|
long customerId,
|
||||||
long containerId,
|
long containerBookingId,
|
||||||
List<Long> settledSourceFeeIds,
|
List<Long> settledSourceFeeIds,
|
||||||
List<ContainerCostDetail> costs) {}
|
List<ContainerCostDetail> costs) {}
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ public final class FinanceContainerSettlementDtos {
|
|||||||
String costOrigin,
|
String costOrigin,
|
||||||
Long sourceFeeId,
|
Long sourceFeeId,
|
||||||
Integer sourceFeeVersion,
|
Integer sourceFeeVersion,
|
||||||
long containerId,
|
long containerBookingId,
|
||||||
String supplierName,
|
String supplierName,
|
||||||
String feeItemName,
|
String feeItemName,
|
||||||
BigDecimal amount,
|
BigDecimal amount,
|
||||||
|
|||||||
+14
-13
@@ -13,14 +13,15 @@ public interface FinanceContainerSettlementQueryMapper {
|
|||||||
@Select("""
|
@Select("""
|
||||||
SELECT COUNT(*) AS customerLinkCount
|
SELECT COUNT(*) AS customerLinkCount
|
||||||
FROM (
|
FROM (
|
||||||
SELECT customer_id, container_id FROM fin_fee_source
|
SELECT customer_id, container_booking_id AS container_id FROM fin_fee_source
|
||||||
WHERE customer_id = #{customerId} AND container_id = #{containerId}
|
WHERE customer_id = #{customerId} AND container_booking_id = #{containerBookingId}
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT tenant_id AS customer_id, container_id FROM fin_cost
|
SELECT tenant_id AS customer_id, container_id FROM fin_cost
|
||||||
WHERE tenant_id = #{customerId} AND container_id = #{containerId}
|
WHERE tenant_id = #{customerId} AND container_id = #{containerBookingId}
|
||||||
) links
|
) links
|
||||||
""")
|
""")
|
||||||
OwnershipRow ownership(@Param("customerId") long customerId, @Param("containerId") long containerId);
|
OwnershipRow ownership(@Param("customerId") long customerId,
|
||||||
|
@Param("containerBookingId") long containerBookingId);
|
||||||
|
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT DISTINCT settled.source_fee_id
|
SELECT DISTINCT settled.source_fee_id
|
||||||
@@ -32,14 +33,14 @@ public interface FinanceContainerSettlementQueryMapper {
|
|||||||
WHERE s.tenant_id = #{customerId}
|
WHERE s.tenant_id = #{customerId}
|
||||||
AND s.status = 'PAID'
|
AND s.status = 'PAID'
|
||||||
AND f.customer_id = #{customerId}
|
AND f.customer_id = #{customerId}
|
||||||
AND f.container_id = #{containerId}
|
AND f.container_booking_id = #{containerBookingId}
|
||||||
AND f.source_system = 'FIRSTLEG'
|
AND f.source_system = 'FIRSTLEG'
|
||||||
AND f.source_type = 'CONTAINER_FEE'
|
AND f.source_type = 'CONTAINER_FEE'
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT c.source_fee_id
|
SELECT c.source_fee_id
|
||||||
FROM fin_cost c
|
FROM fin_cost c
|
||||||
WHERE c.tenant_id = #{customerId}
|
WHERE c.tenant_id = #{customerId}
|
||||||
AND c.container_id = #{containerId}
|
AND c.container_id = #{containerBookingId}
|
||||||
AND c.cost_origin = 'FIRSTLEG_DIRECT_PAID'
|
AND c.cost_origin = 'FIRSTLEG_DIRECT_PAID'
|
||||||
AND c.source_system = 'FIRSTLEG'
|
AND c.source_system = 'FIRSTLEG'
|
||||||
AND c.source_type = 'CONTAINER_FEE'
|
AND c.source_type = 'CONTAINER_FEE'
|
||||||
@@ -49,12 +50,12 @@ public interface FinanceContainerSettlementQueryMapper {
|
|||||||
ORDER BY settled.source_fee_id
|
ORDER BY settled.source_fee_id
|
||||||
""")
|
""")
|
||||||
List<Long> settledSourceFeeIds(@Param("customerId") long customerId,
|
List<Long> settledSourceFeeIds(@Param("customerId") long customerId,
|
||||||
@Param("containerId") long containerId);
|
@Param("containerBookingId") long containerBookingId);
|
||||||
|
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT c.id AS costId, c.cost_no AS costNo, c.cost_type AS costType, c.cost_origin AS costOrigin,
|
SELECT c.id AS costId, c.cost_no AS costNo, c.cost_type AS costType, c.cost_origin AS costOrigin,
|
||||||
c.source_fee_id AS sourceFeeId, c.source_fee_version AS sourceFeeVersion,
|
c.source_fee_id AS sourceFeeId, c.source_fee_version AS sourceFeeVersion,
|
||||||
c.container_id AS containerId, c.supplier_name_snapshot AS supplierName,
|
c.container_id AS containerBookingId, c.supplier_name_snapshot AS supplierName,
|
||||||
c.fee_item_name AS feeItemName, c.cost_amount AS amount, c.currency_code AS currency,
|
c.fee_item_name AS feeItemName, c.cost_amount AS amount, c.currency_code AS currency,
|
||||||
c.status,
|
c.status,
|
||||||
a.id AS allocationId, a.order_id AS orderId, a.inbound_id AS inboundId,
|
a.id AS allocationId, a.order_id AS orderId, a.inbound_id AS inboundId,
|
||||||
@@ -63,11 +64,11 @@ public interface FinanceContainerSettlementQueryMapper {
|
|||||||
a.rounding_adjustment AS roundingAdjustment
|
a.rounding_adjustment AS roundingAdjustment
|
||||||
FROM fin_cost c
|
FROM fin_cost c
|
||||||
LEFT JOIN fin_cost_allocation a ON a.tenant_id = c.tenant_id AND a.cost_id = c.id
|
LEFT JOIN fin_cost_allocation a ON a.tenant_id = c.tenant_id AND a.cost_id = c.id
|
||||||
WHERE c.tenant_id = #{customerId} AND c.container_id = #{containerId}
|
WHERE c.tenant_id = #{customerId} AND c.container_id = #{containerBookingId}
|
||||||
ORDER BY c.created_at DESC, c.id DESC, a.id
|
ORDER BY c.created_at DESC, c.id DESC, a.id
|
||||||
""")
|
""")
|
||||||
List<CostAllocationRow> costs(@Param("customerId") long customerId,
|
List<CostAllocationRow> costs(@Param("customerId") long customerId,
|
||||||
@Param("containerId") long containerId);
|
@Param("containerBookingId") long containerBookingId);
|
||||||
|
|
||||||
final class OwnershipRow {
|
final class OwnershipRow {
|
||||||
private long customerLinkCount;
|
private long customerLinkCount;
|
||||||
@@ -83,7 +84,7 @@ public interface FinanceContainerSettlementQueryMapper {
|
|||||||
private String costOrigin;
|
private String costOrigin;
|
||||||
private Long sourceFeeId;
|
private Long sourceFeeId;
|
||||||
private Integer sourceFeeVersion;
|
private Integer sourceFeeVersion;
|
||||||
private long containerId;
|
private long containerBookingId;
|
||||||
private String supplierName;
|
private String supplierName;
|
||||||
private String feeItemName;
|
private String feeItemName;
|
||||||
private BigDecimal amount;
|
private BigDecimal amount;
|
||||||
@@ -110,8 +111,8 @@ public interface FinanceContainerSettlementQueryMapper {
|
|||||||
public void setSourceFeeId(Long value) { this.sourceFeeId = value; }
|
public void setSourceFeeId(Long value) { this.sourceFeeId = value; }
|
||||||
public Integer getSourceFeeVersion() { return sourceFeeVersion; }
|
public Integer getSourceFeeVersion() { return sourceFeeVersion; }
|
||||||
public void setSourceFeeVersion(Integer value) { this.sourceFeeVersion = value; }
|
public void setSourceFeeVersion(Integer value) { this.sourceFeeVersion = value; }
|
||||||
public long getContainerId() { return containerId; }
|
public long getContainerBookingId() { return containerBookingId; }
|
||||||
public void setContainerId(long value) { this.containerId = value; }
|
public void setContainerBookingId(long value) { this.containerBookingId = value; }
|
||||||
public String getSupplierName() { return supplierName; }
|
public String getSupplierName() { return supplierName; }
|
||||||
public void setSupplierName(String value) { this.supplierName = value; }
|
public void setSupplierName(String value) { this.supplierName = value; }
|
||||||
public String getFeeItemName() { return feeItemName; }
|
public String getFeeItemName() { return feeItemName; }
|
||||||
|
|||||||
+7
-7
@@ -22,21 +22,21 @@ public class FinanceContainerSettlementQueryService {
|
|||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContainerSettlementDetails details(long customerId, long containerId) {
|
public ContainerSettlementDetails details(long customerId, long containerBookingId) {
|
||||||
if (customerId <= 0) throw new IllegalArgumentException("客户范围无效");
|
if (customerId <= 0) throw new IllegalArgumentException("客户范围无效");
|
||||||
if (containerId <= 0) throw new IllegalArgumentException("柜次范围无效");
|
if (containerBookingId <= 0) throw new IllegalArgumentException("货柜范围无效");
|
||||||
|
|
||||||
var ownership = mapper.ownership(customerId, containerId);
|
var ownership = mapper.ownership(customerId, containerBookingId);
|
||||||
if (ownership == null || ownership.getCustomerLinkCount() == 0) {
|
if (ownership == null || ownership.getCustomerLinkCount() == 0) {
|
||||||
throw new BizException(ResultCode.NOT_FOUND, "柜次不存在或不属于客户");
|
throw new BizException(ResultCode.NOT_FOUND, "柜次不存在或不属于客户");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Long> settledSourceFeeIds = mapper.settledSourceFeeIds(customerId, containerId).stream()
|
List<Long> settledSourceFeeIds = mapper.settledSourceFeeIds(customerId, containerBookingId).stream()
|
||||||
.filter(value -> value != null && value > 0)
|
.filter(value -> value != null && value > 0)
|
||||||
.distinct()
|
.distinct()
|
||||||
.toList();
|
.toList();
|
||||||
List<ContainerCostDetail> costs = groupCosts(mapper.costs(customerId, containerId));
|
List<ContainerCostDetail> costs = groupCosts(mapper.costs(customerId, containerBookingId));
|
||||||
return new ContainerSettlementDetails(customerId, containerId, settledSourceFeeIds, costs);
|
return new ContainerSettlementDetails(customerId, containerBookingId, settledSourceFeeIds, costs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ContainerCostDetail> groupCosts(List<CostAllocationRow> rows) {
|
private static List<ContainerCostDetail> groupCosts(List<CostAllocationRow> rows) {
|
||||||
@@ -60,7 +60,7 @@ public class FinanceContainerSettlementQueryService {
|
|||||||
|
|
||||||
private ContainerCostDetail view() {
|
private ContainerCostDetail view() {
|
||||||
return new ContainerCostDetail(cost.getCostId(), cost.getCostNo(), cost.getCostType(),
|
return new ContainerCostDetail(cost.getCostId(), cost.getCostNo(), cost.getCostType(),
|
||||||
cost.getCostOrigin(), cost.getSourceFeeId(), cost.getSourceFeeVersion(), cost.getContainerId(),
|
cost.getCostOrigin(), cost.getSourceFeeId(), cost.getSourceFeeVersion(), cost.getContainerBookingId(),
|
||||||
cost.getSupplierName(), cost.getFeeItemName(), cost.getAmount(), cost.getCurrency(),
|
cost.getSupplierName(), cost.getFeeItemName(), cost.getAmount(), cost.getCurrency(),
|
||||||
cost.getStatus(), List.copyOf(allocations));
|
cost.getStatus(), List.copyOf(allocations));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,10 +86,11 @@ public class FinanceFeeSourceEventService {
|
|||||||
snapshot.sourceType(), snapshot.feeId());
|
snapshot.sourceType(), snapshot.feeId());
|
||||||
jdbc.update("UPDATE fin_cost SET status='VOIDED',voided_at=CURRENT_TIMESTAMP(3),void_reason='来源版本已替代',version=version+1,updated_by=0 WHERE cost_origin='FIRSTLEG_DIRECT_PAID' AND source_system='FIRSTLEG' AND source_type=? AND source_fee_id=? AND status='CONFIRMED'",
|
jdbc.update("UPDATE fin_cost SET status='VOIDED',voided_at=CURRENT_TIMESTAMP(3),void_reason='来源版本已替代',version=version+1,updated_by=0 WHERE cost_origin='FIRSTLEG_DIRECT_PAID' AND source_system='FIRSTLEG' AND source_type=? AND source_fee_id=? AND status='CONFIRMED'",
|
||||||
snapshot.sourceType(), snapshot.feeId());
|
snapshot.sourceType(), snapshot.feeId());
|
||||||
jdbc.update("INSERT INTO fin_fee_source(source_system,source_fee_id,source_fee_version,customer_id,source_type,source_no,direction,order_id,inbound_id,container_id,fee_item_code,fee_item_name,amount,currency_code,settlement_mode,paid_at,payment_method,payment_reference_no,payment_voucher_file_ids,source_hash,source_snapshot,status) VALUES ('FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'AVAILABLE')",
|
jdbc.update("INSERT INTO fin_fee_source(source_system,source_fee_id,source_fee_version,customer_id,source_type,source_no,direction,order_id,inbound_id,container_id,container_booking_id,fee_item_code,fee_item_name,amount,currency_code,settlement_mode,paid_at,payment_method,payment_reference_no,payment_voucher_file_ids,source_hash,source_snapshot,status) VALUES ('FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'AVAILABLE')",
|
||||||
snapshot.feeId(), snapshot.feeVersion(), snapshot.customerId(), snapshot.sourceType(),
|
snapshot.feeId(), snapshot.feeVersion(), snapshot.customerId(), snapshot.sourceType(),
|
||||||
snapshot.inboundCode() == null || snapshot.inboundCode().isBlank() ? snapshot.containerNo() : snapshot.inboundCode(),
|
snapshot.inboundCode() == null || snapshot.inboundCode().isBlank() ? snapshot.containerNo() : snapshot.inboundCode(),
|
||||||
snapshot.direction(), snapshot.orderId(), snapshot.inboundId(), snapshot.containerId(),
|
snapshot.direction(), snapshot.orderId(), snapshot.inboundId(), snapshot.containerId(),
|
||||||
|
financeContainerBookingId(snapshot),
|
||||||
snapshot.feeItemCode(), snapshot.feeItemName(), snapshot.totalAmount(), snapshot.currency(),
|
snapshot.feeItemCode(), snapshot.feeItemName(), snapshot.totalAmount(), snapshot.currency(),
|
||||||
snapshot.settlementMode(), snapshot.paidAt(), snapshot.paymentMethod(),
|
snapshot.settlementMode(), snapshot.paidAt(), snapshot.paymentMethod(),
|
||||||
snapshot.paymentReferenceNo(), voucherJson(snapshot.paymentVoucherFileIds()),
|
snapshot.paymentReferenceNo(), voucherJson(snapshot.paymentVoucherFileIds()),
|
||||||
@@ -116,7 +117,7 @@ public class FinanceFeeSourceEventService {
|
|||||||
String costNo = "DIRECT-" + snapshot.sourceType() + "-" + snapshot.feeId() + "-V" + snapshot.feeVersion();
|
String costNo = "DIRECT-" + snapshot.sourceType() + "-" + snapshot.feeId() + "-V" + snapshot.feeVersion();
|
||||||
jdbc.update("INSERT INTO fin_cost(tenant_id,cost_no,cost_type,cost_origin,source_system,source_type,source_fee_id,source_fee_version,source_hash,order_id,inbound_id,container_id,supplier_id,supplier_name_snapshot,fee_item_code,fee_item_name,currency_code,cost_amount,voucher_file_ids,paid_at,payment_method,payment_reference_no,status,confirmed_at,created_by,created_by_name,updated_by) VALUES(?,?,'SOURCE_DIRECT_PAID','FIRSTLEG_DIRECT_PAID','FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED',CURRENT_TIMESTAMP(3),0,'系统',0)",
|
jdbc.update("INSERT INTO fin_cost(tenant_id,cost_no,cost_type,cost_origin,source_system,source_type,source_fee_id,source_fee_version,source_hash,order_id,inbound_id,container_id,supplier_id,supplier_name_snapshot,fee_item_code,fee_item_name,currency_code,cost_amount,voucher_file_ids,paid_at,payment_method,payment_reference_no,status,confirmed_at,created_by,created_by_name,updated_by) VALUES(?,?,'SOURCE_DIRECT_PAID','FIRSTLEG_DIRECT_PAID','FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED',CURRENT_TIMESTAMP(3),0,'系统',0)",
|
||||||
snapshot.customerId(), costNo, snapshot.sourceType(), snapshot.feeId(), snapshot.feeVersion(),
|
snapshot.customerId(), costNo, snapshot.sourceType(), snapshot.feeId(), snapshot.feeVersion(),
|
||||||
snapshot.sourceHash(), snapshot.orderId(), snapshot.inboundId(), snapshot.containerId(),
|
snapshot.sourceHash(), snapshot.orderId(), snapshot.inboundId(), financeContainerBookingId(snapshot),
|
||||||
snapshot.supplierId(), snapshot.supplierName(), snapshot.feeItemCode(), snapshot.feeItemName(),
|
snapshot.supplierId(), snapshot.supplierName(), snapshot.feeItemCode(), snapshot.feeItemName(),
|
||||||
snapshot.currency(), snapshot.totalAmount(), voucherJsonOrEmpty(snapshot.paymentVoucherFileIds()),
|
snapshot.currency(), snapshot.totalAmount(), voucherJsonOrEmpty(snapshot.paymentVoucherFileIds()),
|
||||||
snapshot.paidAt(), snapshot.paymentMethod(), snapshot.paymentReferenceNo());
|
snapshot.paidAt(), snapshot.paymentMethod(), snapshot.paymentReferenceNo());
|
||||||
@@ -184,7 +185,11 @@ public class FinanceFeeSourceEventService {
|
|||||||
throw error("FIN-EVT-012", "应收费用不得携带结算方式或已付快照");
|
throw error("FIN-EVT-012", "应收费用不得携带结算方式或已付快照");
|
||||||
}
|
}
|
||||||
if ("INBOUND_FEE".equals(value.sourceType()) && (value.inboundId() == null || !String.valueOf(value.inboundId()).equals(event.payload().inboundId()))) throw error("FIN-EVT-013", "入库单归属不一致");
|
if ("INBOUND_FEE".equals(value.sourceType()) && (value.inboundId() == null || !String.valueOf(value.inboundId()).equals(event.payload().inboundId()))) throw error("FIN-EVT-013", "入库单归属不一致");
|
||||||
if ("CONTAINER_FEE".equals(value.sourceType()) && (value.containerId() == null || !String.valueOf(value.containerId()).equals(event.payload().containerId()))) throw error("FIN-EVT-014", "柜子归属不一致");
|
if ("CONTAINER_FEE".equals(value.sourceType()) && (value.containerId() == null
|
||||||
|
|| !String.valueOf(value.containerId()).equals(event.payload().containerId())
|
||||||
|
|| value.containerBookingId() == null || value.containerBookingId() <= 0)) {
|
||||||
|
throw error("FIN-EVT-014", "柜子归属不一致");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validatePayableSettlement(FirstlegFeeSourceClient.Snapshot value) {
|
private void validatePayableSettlement(FirstlegFeeSourceClient.Snapshot value) {
|
||||||
@@ -216,6 +221,10 @@ public class FinanceFeeSourceEventService {
|
|||||||
return values == null || values.isEmpty() ? "[]" : json(values);
|
return values == null || values.isEmpty() ? "[]" : json(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Long financeContainerBookingId(FirstlegFeeSourceClient.Snapshot snapshot) {
|
||||||
|
return "CONTAINER_FEE".equals(snapshot.sourceType()) ? snapshot.containerBookingId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
private String json(Object value) {
|
private String json(Object value) {
|
||||||
try { return objectMapper.writeValueAsString(value); }
|
try { return objectMapper.writeValueAsString(value); }
|
||||||
catch (JsonProcessingException e) { throw error("FIN-EVT-001", "事件序列化失败"); }
|
catch (JsonProcessingException e) { throw error("FIN-EVT-001", "事件序列化失败"); }
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ CREATE TABLE IF NOT EXISTS fin_cost (
|
|||||||
source_hash VARCHAR(71) NULL COMMENT '来源不可变快照SHA-256,兼容sha256前缀格式',
|
source_hash VARCHAR(71) NULL COMMENT '来源不可变快照SHA-256,兼容sha256前缀格式',
|
||||||
order_id BIGINT UNSIGNED NULL COMMENT '成本归属的头程订单ID,单票成本必填',
|
order_id BIGINT UNSIGNED NULL COMMENT '成本归属的头程订单ID,单票成本必填',
|
||||||
inbound_id BIGINT UNSIGNED NULL COMMENT '成本关联的入库单ID,可为空',
|
inbound_id BIGINT UNSIGNED NULL COMMENT '成本关联的入库单ID,可为空',
|
||||||
container_id BIGINT UNSIGNED NULL COMMENT 'Firstleg装柜计划ID,整柜公共成本必填',
|
container_id BIGINT UNSIGNED NULL COMMENT 'Firstleg真实货柜containerBookingId,整柜公共成本必填',
|
||||||
warehouse_id BIGINT UNSIGNED NULL COMMENT '业务直接已付成本关联仓库ID,可为空',
|
warehouse_id BIGINT UNSIGNED NULL COMMENT '业务直接已付成本关联仓库ID,可为空',
|
||||||
supplier_id BIGINT UNSIGNED NULL COMMENT 'Warehouse公共往来单位或供应商引用ID',
|
supplier_id BIGINT UNSIGNED NULL COMMENT 'Warehouse公共往来单位或供应商引用ID',
|
||||||
supplier_name_snapshot VARCHAR(200) NULL COMMENT '登记成本时供应商或成本对象名称快照',
|
supplier_name_snapshot VARCHAR(200) NULL COMMENT '登记成本时供应商或成本对象名称快照',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ paths:
|
|||||||
get:
|
get:
|
||||||
summary: 按客户和柜次查询已结算柜费及成本分摊
|
summary: 按客户和柜次查询已结算柜费及成本分摊
|
||||||
description: >-
|
description: >-
|
||||||
customerId 与 containerId 必须在 Finance 的费用来源或成本快照中存在归属关系。
|
customerId 与 containerBookingId 必须在 Finance 的费用来源或成本快照中存在归属关系。
|
||||||
服务端固定执行三条集合 SQL,不按结算单或成本逐项查询;响应不包含整张结算单、
|
服务端固定执行三条集合 SQL,不按结算单或成本逐项查询;响应不包含整张结算单、
|
||||||
其他客户数据、付款凭证或内部备注。
|
其他客户数据、付款凭证或内部备注。
|
||||||
x-permissions-all-of:
|
x-permissions-all-of:
|
||||||
@@ -25,10 +25,10 @@ paths:
|
|||||||
in: query
|
in: query
|
||||||
required: true
|
required: true
|
||||||
schema: {$ref: '#/components/schemas/Id'}
|
schema: {$ref: '#/components/schemas/Id'}
|
||||||
- name: containerId
|
- name: containerBookingId
|
||||||
in: query
|
in: query
|
||||||
required: true
|
required: true
|
||||||
description: Firstleg 装柜计划 ID。
|
description: Firstleg 真实货柜 containerBookingId,不是装柜计划 loadingPlanId。
|
||||||
schema: {$ref: '#/components/schemas/Id'}
|
schema: {$ref: '#/components/schemas/Id'}
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -63,10 +63,10 @@ components:
|
|||||||
timestamp: {type: string, format: date-time}
|
timestamp: {type: string, format: date-time}
|
||||||
ContainerSettlementDetails:
|
ContainerSettlementDetails:
|
||||||
type: object
|
type: object
|
||||||
required: [customerId, containerId, settledSourceFeeIds, costs]
|
required: [customerId, containerBookingId, settledSourceFeeIds, costs]
|
||||||
properties:
|
properties:
|
||||||
customerId: {$ref: '#/components/schemas/Id'}
|
customerId: {$ref: '#/components/schemas/Id'}
|
||||||
containerId: {$ref: '#/components/schemas/Id'}
|
containerBookingId: {$ref: '#/components/schemas/Id'}
|
||||||
settledSourceFeeIds:
|
settledSourceFeeIds:
|
||||||
type: array
|
type: array
|
||||||
uniqueItems: true
|
uniqueItems: true
|
||||||
@@ -76,7 +76,7 @@ components:
|
|||||||
items: {$ref: '#/components/schemas/ContainerCostDetail'}
|
items: {$ref: '#/components/schemas/ContainerCostDetail'}
|
||||||
ContainerCostDetail:
|
ContainerCostDetail:
|
||||||
type: object
|
type: object
|
||||||
required: [costId, costNo, costType, costOrigin, containerId, feeItemName, amount, currency, status, allocations]
|
required: [costId, costNo, costType, costOrigin, containerBookingId, feeItemName, amount, currency, status, allocations]
|
||||||
properties:
|
properties:
|
||||||
costId: {$ref: '#/components/schemas/Id'}
|
costId: {$ref: '#/components/schemas/Id'}
|
||||||
costNo: {type: string}
|
costNo: {type: string}
|
||||||
@@ -84,7 +84,7 @@ components:
|
|||||||
costOrigin: {type: string, enum: [MANUAL, FIRSTLEG_DIRECT_PAID, BUSINESS_DIRECT_PAID]}
|
costOrigin: {type: string, enum: [MANUAL, FIRSTLEG_DIRECT_PAID, BUSINESS_DIRECT_PAID]}
|
||||||
sourceFeeId: {$ref: '#/components/schemas/Id'}
|
sourceFeeId: {$ref: '#/components/schemas/Id'}
|
||||||
sourceFeeVersion: {type: integer, minimum: 1}
|
sourceFeeVersion: {type: integer, minimum: 1}
|
||||||
containerId: {$ref: '#/components/schemas/Id'}
|
containerBookingId: {$ref: '#/components/schemas/Id'}
|
||||||
supplierName: {type: string, nullable: true}
|
supplierName: {type: string, nullable: true}
|
||||||
feeItemName: {type: string}
|
feeItemName: {type: string}
|
||||||
amount: {$ref: '#/components/schemas/Money'}
|
amount: {$ref: '#/components/schemas/Money'}
|
||||||
|
|||||||
+3
-1
@@ -22,6 +22,8 @@ class FinanceContainerSettlementOpenApiContractTest {
|
|||||||
.contains("platform:finance:settlement-ar:list")
|
.contains("platform:finance:settlement-ar:list")
|
||||||
.contains("platform:finance:settlement-ap:list")
|
.contains("platform:finance:settlement-ap:list")
|
||||||
.contains("x-query-count: 3 fixed set-based SQL statements")
|
.contains("x-query-count: 3 fixed set-based SQL statements")
|
||||||
.contains("settledSourceFeeIds:", "costs:", "allocations:");
|
.contains("containerBookingId:", "不是装柜计划 loadingPlanId",
|
||||||
|
"settledSourceFeeIds:", "costs:", "allocations:")
|
||||||
|
.doesNotContain("- name: containerId");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -23,7 +23,8 @@ class FinanceContainerSettlementQueryMapperContractTest {
|
|||||||
.reduce("", (left, right) -> left + "\n" + right);
|
.reduce("", (left, right) -> left + "\n" + right);
|
||||||
assertThat(sql).contains("JOIN fin_settlement_line", "JOIN fin_fee_source",
|
assertThat(sql).contains("JOIN fin_settlement_line", "JOIN fin_fee_source",
|
||||||
"LEFT JOIN fin_cost_allocation", "c.tenant_id = #{customerId}",
|
"LEFT JOIN fin_cost_allocation", "c.tenant_id = #{customerId}",
|
||||||
"c.container_id = #{containerId}");
|
"f.container_booking_id = #{containerBookingId}",
|
||||||
|
"c.container_id = #{containerBookingId}");
|
||||||
assertThat(sql).doesNotContain("${");
|
assertThat(sql).doesNotContain("${");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -33,7 +33,7 @@ class FinanceContainerSettlementQueryServiceTest {
|
|||||||
var result = new FinanceContainerSettlementQueryService(mapper).details(101L, 301L);
|
var result = new FinanceContainerSettlementQueryService(mapper).details(101L, 301L);
|
||||||
|
|
||||||
assertThat(result.customerId()).isEqualTo(101L);
|
assertThat(result.customerId()).isEqualTo(101L);
|
||||||
assertThat(result.containerId()).isEqualTo(301L);
|
assertThat(result.containerBookingId()).isEqualTo(301L);
|
||||||
assertThat(result.settledSourceFeeIds()).containsExactly(901L, 902L);
|
assertThat(result.settledSourceFeeIds()).containsExactly(901L, 902L);
|
||||||
assertThat(result.costs()).hasSize(2);
|
assertThat(result.costs()).hasSize(2);
|
||||||
assertThat(result.costs().get(0).costId()).isEqualTo(501L);
|
assertThat(result.costs().get(0).costId()).isEqualTo(501L);
|
||||||
@@ -79,14 +79,14 @@ class FinanceContainerSettlementQueryServiceTest {
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CostAllocationRow row(long costId, long containerId, Long allocationId,
|
private static CostAllocationRow row(long costId, long containerBookingId, Long allocationId,
|
||||||
Long orderId, boolean roundingAdjustment) {
|
Long orderId, boolean roundingAdjustment) {
|
||||||
var row = new CostAllocationRow();
|
var row = new CostAllocationRow();
|
||||||
row.setCostId(costId);
|
row.setCostId(costId);
|
||||||
row.setCostNo("COST-" + costId);
|
row.setCostNo("COST-" + costId);
|
||||||
row.setCostType("CONTAINER_SHARED");
|
row.setCostType("CONTAINER_SHARED");
|
||||||
row.setCostOrigin("MANUAL");
|
row.setCostOrigin("MANUAL");
|
||||||
row.setContainerId(containerId);
|
row.setContainerBookingId(containerBookingId);
|
||||||
row.setSupplierName("供应商");
|
row.setSupplierName("供应商");
|
||||||
row.setFeeItemName("柜费");
|
row.setFeeItemName("柜费");
|
||||||
row.setAmount(new BigDecimal("100.00"));
|
row.setAmount(new BigDecimal("100.00"));
|
||||||
|
|||||||
@@ -62,6 +62,31 @@ class FinanceFeeSourceEventServiceTest {
|
|||||||
assertThat(service.consume(event)).isEqualTo(101L);
|
assertThat(service.consume(event)).isEqualTo(101L);
|
||||||
verify(firstleg).container("test-token", 41, 31, 1);
|
verify(firstleg).container("test-token", 41, 31, 1);
|
||||||
verify(jdbc).update(contains("status='SUPERSEDED'"), eq("CONTAINER_FEE"), eq(31L));
|
verify(jdbc).update(contains("status='SUPERSEDED'"), eq("CONTAINER_FEE"), eq(31L));
|
||||||
|
ArgumentCaptor<Object[]> values = ArgumentCaptor.forClass(Object[].class);
|
||||||
|
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture());
|
||||||
|
assertThat(values.getValue()[8]).as("fin_fee_source.container_id keeps loadingPlanId")
|
||||||
|
.isEqualTo(41L);
|
||||||
|
assertThat(values.getValue()[9]).as("fin_fee_source.container_booking_id stores physical container")
|
||||||
|
.isEqualTo(61L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsContainerFeeSnapshotWithoutPhysicalContainerBookingId() {
|
||||||
|
FinanceFeeSourceEvent event = containerEvent("container-missing-booking", "FIRSTLEG_FEE_PUBLISHED", 1, 7);
|
||||||
|
when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"),
|
||||||
|
eq("container-missing-booking"))).thenReturn(List.of());
|
||||||
|
var snapshot = new FirstlegFeeSourceClient.Snapshot(7, "CONTAINER_FEE", 31, 1,
|
||||||
|
null, null, null, 41L, null, "C-41", 7, "客户", null, null,
|
||||||
|
null, null, null, null, null, List.of(), 61L, "CUSTOMS", "清关费",
|
||||||
|
"RECEIVABLE", LocalDate.of(2026, 8, 11), "CNY", new BigDecimal("100.00"),
|
||||||
|
"APPROVED", "PUBLISHED", LocalDateTime.of(2026, 8, 11, 1, 0),
|
||||||
|
BigDecimal.ONE, "SHIPMENT", BigDecimal.TEN, BigDecimal.ONE, "source-hash", 1);
|
||||||
|
when(firstleg.container("test-token", 41, 31, 1)).thenReturn(ApiResponse.success(snapshot));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.consume(event))
|
||||||
|
.isInstanceOf(FinanceEventException.class)
|
||||||
|
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-EVT-014");
|
||||||
|
verify(jdbc, never()).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -83,7 +108,7 @@ class FinanceFeeSourceEventServiceTest {
|
|||||||
verify(jdbc, never()).update(startsWith("INSERT INTO fin_cost"), any(Object[].class));
|
verify(jdbc, never()).update(startsWith("INSERT INTO fin_cost"), any(Object[].class));
|
||||||
ArgumentCaptor<Object[]> values = ArgumentCaptor.forClass(Object[].class);
|
ArgumentCaptor<Object[]> values = ArgumentCaptor.forClass(Object[].class);
|
||||||
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture());
|
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture());
|
||||||
assertThat(values.getValue()[19].toString())
|
assertThat(values.getValue()[20].toString())
|
||||||
.contains("\"supplierId\":71", "\"supplierName\":\"现有供应商\"", "\"supplierCategory\":\"TRUCKING\"");
|
.contains("\"supplierId\":71", "\"supplierName\":\"现有供应商\"", "\"supplierCategory\":\"TRUCKING\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +146,9 @@ class FinanceFeeSourceEventServiceTest {
|
|||||||
|
|
||||||
ArgumentCaptor<Object[]> values = ArgumentCaptor.forClass(Object[].class);
|
ArgumentCaptor<Object[]> values = ArgumentCaptor.forClass(Object[].class);
|
||||||
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture());
|
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture());
|
||||||
assertThat(values.getValue()[13]).isEqualTo("DIRECT_PAID");
|
assertThat(values.getValue()[14]).isEqualTo("DIRECT_PAID");
|
||||||
assertThat(values.getValue()[15]).isEqualTo("BANK_TRANSFER");
|
assertThat(values.getValue()[16]).isEqualTo("BANK_TRANSFER");
|
||||||
assertThat(values.getValue()[17]).isEqualTo("[901]");
|
assertThat(values.getValue()[18]).isEqualTo("[901]");
|
||||||
ArgumentCaptor<Object[]> costValues = ArgumentCaptor.forClass(Object[].class);
|
ArgumentCaptor<Object[]> costValues = ArgumentCaptor.forClass(Object[].class);
|
||||||
verify(jdbc).update(startsWith("INSERT INTO fin_cost"), costValues.capture());
|
verify(jdbc).update(startsWith("INSERT INTO fin_cost"), costValues.capture());
|
||||||
assertThat(costValues.getValue()[0]).isEqualTo(7L);
|
assertThat(costValues.getValue()[0]).isEqualTo(7L);
|
||||||
@@ -131,6 +156,8 @@ class FinanceFeeSourceEventServiceTest {
|
|||||||
assertThat(costValues.getValue()[2]).isEqualTo("CONTAINER_FEE");
|
assertThat(costValues.getValue()[2]).isEqualTo("CONTAINER_FEE");
|
||||||
assertThat(costValues.getValue()[3]).isEqualTo(31L);
|
assertThat(costValues.getValue()[3]).isEqualTo(31L);
|
||||||
assertThat(costValues.getValue()[4]).isEqualTo(1);
|
assertThat(costValues.getValue()[4]).isEqualTo(1);
|
||||||
|
assertThat(costValues.getValue()[8]).as("fin_cost.container_id stores containerBookingId")
|
||||||
|
.isEqualTo(61L);
|
||||||
assertThat(costValues.getValue()[15]).isEqualTo("[901]");
|
assertThat(costValues.getValue()[15]).isEqualTo("[901]");
|
||||||
assertThat(costValues.getValue()[17]).isEqualTo("BANK_TRANSFER");
|
assertThat(costValues.getValue()[17]).isEqualTo("BANK_TRANSFER");
|
||||||
assertThat(costValues.getValue()[18]).isEqualTo("BANK-001");
|
assertThat(costValues.getValue()[18]).isEqualTo("BANK-001");
|
||||||
@@ -284,7 +311,7 @@ class FinanceFeeSourceEventServiceTest {
|
|||||||
Long inboundId, Long containerId,
|
Long inboundId, Long containerId,
|
||||||
String publicationStatus, BigDecimal amount) {
|
String publicationStatus, BigDecimal amount) {
|
||||||
return new FirstlegFeeSourceClient.Snapshot(customerId, sourceType, 31, version, inboundId,
|
return new FirstlegFeeSourceClient.Snapshot(customerId, sourceType, 31, version, inboundId,
|
||||||
inboundId == null ? null : "IB-21", 51L, containerId, null,
|
inboundId == null ? null : "IB-21", 51L, containerId, containerId == null ? null : 61L,
|
||||||
containerId == null ? null : "C-41", customerId, "客户", null, null, null,
|
containerId == null ? null : "C-41", customerId, "客户", null, null, null,
|
||||||
null, null, null, null, List.of(), 61L, "CUSTOMS",
|
null, null, null, null, List.of(), 61L, "CUSTOMS",
|
||||||
"清关费", "RECEIVABLE", LocalDate.of(2026, 8, 11), "CNY", amount,
|
"清关费", "RECEIVABLE", LocalDate.of(2026, 8, 11), "CNY", amount,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
@@ -55,9 +56,13 @@ class PlatformFinanceControllerBehaviorTest {
|
|||||||
controller.cost(user, "cost-key", request);
|
controller.cost(user, "cost-key", request);
|
||||||
|
|
||||||
verify(snapshotClient).loadingPlan("internal-token", 10L, 88L);
|
verify(snapshotClient).loadingPlan("internal-token", 10L, 88L);
|
||||||
verify(service).createCost(eq(10L), eq(7L), any(), eq(List.of(
|
org.mockito.ArgumentCaptor<FinanceService.CostRequest> cost =
|
||||||
new FinanceService.ContainerOrderVolume(101L, 1001L, 88L, 10L, new BigDecimal("1.25")),
|
org.mockito.ArgumentCaptor.forClass(FinanceService.CostRequest.class);
|
||||||
new FinanceService.ContainerOrderVolume(102L, 1002L, 88L, 10L, new BigDecimal("2.75")))), eq("cost-key"));
|
verify(service).createCost(eq(10L), eq(7L), cost.capture(), eq(List.of(
|
||||||
|
new FinanceService.ContainerOrderVolume(101L, 1001L, 99L, 10L, new BigDecimal("1.25")),
|
||||||
|
new FinanceService.ContainerOrderVolume(102L, 1002L, 99L, 10L, new BigDecimal("2.75")))), eq("cost-key"));
|
||||||
|
assertThat(cost.getValue().containerId()).as("Finance persists the physical containerBookingId")
|
||||||
|
.isEqualTo(99L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+2
-2
@@ -40,9 +40,9 @@ public class ContainerFeeDetailRepository {
|
|||||||
.eq(ContainerFeeDetailEntity::getId, feeId));
|
.eq(ContainerFeeDetailEntity::getId, feeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContainerFeeDetailEntity find(long loadingPlanId, long feeId) {
|
public ContainerFeeDetailEntity findByContainerBookingId(long containerBookingId, long feeId) {
|
||||||
return mapper.selectOne(new LambdaQueryWrapper<ContainerFeeDetailEntity>()
|
return mapper.selectOne(new LambdaQueryWrapper<ContainerFeeDetailEntity>()
|
||||||
.eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId)
|
.eq(ContainerFeeDetailEntity::getContainerBookingId, containerBookingId)
|
||||||
.eq(ContainerFeeDetailEntity::getId, feeId));
|
.eq(ContainerFeeDetailEntity::getId, feeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -49,10 +49,10 @@ public class ContainerFeeSourceSnapshotRepository {
|
|||||||
mapper.insert(value);
|
mapper.insert(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContainerFeeSourceSnapshotEntity find(long loadingPlanId, long sourceFeeId,
|
public ContainerFeeSourceSnapshotEntity findByContainerBookingId(long containerBookingId, long sourceFeeId,
|
||||||
int sourceVersion) {
|
int sourceVersion) {
|
||||||
return mapper.selectOne(new LambdaQueryWrapper<ContainerFeeSourceSnapshotEntity>()
|
return mapper.selectOne(new LambdaQueryWrapper<ContainerFeeSourceSnapshotEntity>()
|
||||||
.eq(ContainerFeeSourceSnapshotEntity::getLoadingPlanId, loadingPlanId)
|
.eq(ContainerFeeSourceSnapshotEntity::getContainerBookingId, containerBookingId)
|
||||||
.eq(ContainerFeeSourceSnapshotEntity::getSourceFeeId, sourceFeeId)
|
.eq(ContainerFeeSourceSnapshotEntity::getSourceFeeId, sourceFeeId)
|
||||||
.eq(ContainerFeeSourceSnapshotEntity::getSourceVersion, sourceVersion));
|
.eq(ContainerFeeSourceSnapshotEntity::getSourceVersion, sourceVersion));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ public class ContainerFeeDetailService {
|
|||||||
sourceSnapshots.saveApproved(approved);
|
sourceSnapshots.saveApproved(approved);
|
||||||
String eventType = approved.getSourceVersion() > 1
|
String eventType = approved.getSourceVersion() > 1
|
||||||
? FinanceFeeSourceOutboxService.ADJUSTED : FinanceFeeSourceOutboxService.PUBLISHED;
|
? FinanceFeeSourceOutboxService.ADJUSTED : FinanceFeeSourceOutboxService.PUBLISHED;
|
||||||
financeOutbox.publishContainer(eventType, tenantId, loadingPlanId, feeId,
|
financeOutbox.publishContainer(eventType, tenantId, containerBookingId(approved), feeId,
|
||||||
approved.getSourceVersion(), LocalDateTime.now());
|
approved.getSourceVersion(), LocalDateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ public class ContainerFeeDetailService {
|
|||||||
throw conflict("只有当前版本的已审核柜次费用可以作废");
|
throw conflict("只有当前版本的已审核柜次费用可以作废");
|
||||||
}
|
}
|
||||||
financeOutbox.publishContainer(FinanceFeeSourceOutboxService.VOIDED, tenantId,
|
financeOutbox.publishContainer(FinanceFeeSourceOutboxService.VOIDED, tenantId,
|
||||||
loadingPlanId, feeId, expectedVersion, LocalDateTime.now());
|
containerBookingId(current), feeId, expectedVersion, LocalDateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlanContext requireOwnedPlan(long tenantId, long loadingPlanId) {
|
private PlanContext requireOwnedPlan(long tenantId, long loadingPlanId) {
|
||||||
@@ -372,6 +372,13 @@ public class ContainerFeeDetailService {
|
|||||||
return new BizException(ResultCode.CONFLICT, message);
|
return new BizException(ResultCode.CONFLICT, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long containerBookingId(ContainerFeeDetailEntity value) {
|
||||||
|
if (value.getContainerBookingId() == null || value.getContainerBookingId() <= 0) {
|
||||||
|
throw conflict("柜次费用缺少真实货柜关联");
|
||||||
|
}
|
||||||
|
return value.getContainerBookingId();
|
||||||
|
}
|
||||||
|
|
||||||
private record PlanContext(LoadingPlanEntity plan, ContainerBookingEntity booking) {
|
private record PlanContext(LoadingPlanEntity plan, ContainerBookingEntity booking) {
|
||||||
}
|
}
|
||||||
private static String text(String value) { return value == null || value.isBlank() ? null : value.trim(); }
|
private static String text(String value) { return value == null || value.isBlank() ? null : value.trim(); }
|
||||||
|
|||||||
+4
-3
@@ -123,9 +123,10 @@ public class FinanceFeeSourceCommandService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void voidContainer(VoidRequest request) {
|
private void voidContainer(VoidRequest request) {
|
||||||
long containerId = positive(request.containerId(), "柜次ID");
|
long containerBookingId = positive(request.containerId(), "柜次ID");
|
||||||
if (request.inboundId() != null) throw bad("柜次费用不能携带入库单ID");
|
if (request.inboundId() != null) throw bad("柜次费用不能携带入库单ID");
|
||||||
ContainerFeeDetailEntity detail = containerFees.find(containerId, request.sourceFeeId());
|
ContainerFeeDetailEntity detail = containerFees.findByContainerBookingId(
|
||||||
|
containerBookingId, request.sourceFeeId());
|
||||||
if (detail == null || !Long.valueOf(request.customerId()).equals(detail.getTenantId())) {
|
if (detail == null || !Long.valueOf(request.customerId()).equals(detail.getTenantId())) {
|
||||||
throw new BizException(ResultCode.NOT_FOUND, "指定客户的柜次费用来源版本不存在");
|
throw new BizException(ResultCode.NOT_FOUND, "指定客户的柜次费用来源版本不存在");
|
||||||
}
|
}
|
||||||
@@ -133,7 +134,7 @@ public class FinanceFeeSourceCommandService {
|
|||||||
throw conflict("柜次费用来源版本已变化");
|
throw conflict("柜次费用来源版本已变化");
|
||||||
}
|
}
|
||||||
if ("VOIDED".equals(detail.getSourceStatus())) return;
|
if ("VOIDED".equals(detail.getSourceStatus())) return;
|
||||||
containerFeeService.voidApproved(request.customerId(), containerId,
|
containerFeeService.voidApproved(request.customerId(), detail.getLoadingPlanId(),
|
||||||
request.sourceFeeId(), request.sourceVersion(), request.operatorId());
|
request.sourceFeeId(), request.sourceVersion(), request.operatorId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -36,11 +36,11 @@ public class FinanceFeeSourceOutboxService {
|
|||||||
payload, occurredAt));
|
payload, occurredAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void publishContainer(String eventType, long tenantId, long containerId,
|
public void publishContainer(String eventType, long tenantId, long containerBookingId,
|
||||||
long feeId, int feeVersion, LocalDateTime occurredAt) {
|
long feeId, int feeVersion, LocalDateTime occurredAt) {
|
||||||
FinanceFeeSourceEvent.Payload payload = new FinanceFeeSourceEvent.Payload(
|
FinanceFeeSourceEvent.Payload payload = new FinanceFeeSourceEvent.Payload(
|
||||||
null, String.valueOf(containerId), String.valueOf(feeId), feeVersion);
|
null, String.valueOf(containerBookingId), String.valueOf(feeId), feeVersion);
|
||||||
publish(event(eventType, tenantId, "CONTAINER", containerId, feeId, feeVersion,
|
publish(event(eventType, tenantId, "CONTAINER", containerBookingId, feeId, feeVersion,
|
||||||
payload, occurredAt));
|
payload, occurredAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -58,12 +58,13 @@ public class FinanceFeeSourceSnapshotService {
|
|||||||
detail.getActualGrossWeightKg(), detail.getActualVolumeCbm(), sha256(canonical), 1);
|
detail.getActualGrossWeightKg(), detail.getActualVolumeCbm(), sha256(canonical), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FinanceFeeSourceSnapshot container(long containerId, long feeId, int sourceVersion) {
|
public FinanceFeeSourceSnapshot container(long containerBookingId, long feeId, int sourceVersion) {
|
||||||
ContainerFeeSourceSnapshotEntity detail = containerSnapshots.find(containerId, feeId, sourceVersion);
|
ContainerFeeSourceSnapshotEntity detail = containerSnapshots.findByContainerBookingId(
|
||||||
|
containerBookingId, feeId, sourceVersion);
|
||||||
if (detail == null) throw new BizException(ResultCode.NOT_FOUND, "柜次费用来源不存在");
|
if (detail == null) throw new BizException(ResultCode.NOT_FOUND, "柜次费用来源不存在");
|
||||||
int version = detail.getSourceVersion();
|
int version = detail.getSourceVersion();
|
||||||
String canonical = String.join("|", "CONTAINER_FEE", String.valueOf(feeId),
|
String canonical = String.join("|", "CONTAINER_FEE", String.valueOf(feeId),
|
||||||
String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(containerId),
|
String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(containerBookingId),
|
||||||
nullSafe(detail.getFeeItemCode()), nullSafe(detail.getDirection()),
|
nullSafe(detail.getFeeItemCode()), nullSafe(detail.getDirection()),
|
||||||
nullSafe(detail.getSupplierId()), nullSafe(detail.getSupplierName()),
|
nullSafe(detail.getSupplierId()), nullSafe(detail.getSupplierName()),
|
||||||
nullSafe(detail.getSupplierCategory()),
|
nullSafe(detail.getSupplierCategory()),
|
||||||
@@ -76,7 +77,8 @@ public class FinanceFeeSourceSnapshotService {
|
|||||||
detail.getAmount().toPlainString(), nullSafe(detail.getCurrency()),
|
detail.getAmount().toPlainString(), nullSafe(detail.getCurrency()),
|
||||||
nullSafe(detail.getReviewStatus()));
|
nullSafe(detail.getReviewStatus()));
|
||||||
return new FinanceFeeSourceSnapshot(detail.getTenantId(), "CONTAINER_FEE", feeId,
|
return new FinanceFeeSourceSnapshot(detail.getTenantId(), "CONTAINER_FEE", feeId,
|
||||||
version, detail.getAllocationInboundId(), detail.getAllocationInboundCode(), detail.getAllocationOrderId(), containerId, detail.getContainerBookingId(),
|
version, detail.getAllocationInboundId(), detail.getAllocationInboundCode(), detail.getAllocationOrderId(),
|
||||||
|
containerBookingId, containerBookingId,
|
||||||
detail.getContainerNo(), detail.getTenantId(), null, detail.getSupplierId(),
|
detail.getContainerNo(), detail.getTenantId(), null, detail.getSupplierId(),
|
||||||
detail.getSupplierName(), detail.getSupplierCategory(), detail.getSettlementMode(),
|
detail.getSupplierName(), detail.getSupplierCategory(), detail.getSettlementMode(),
|
||||||
detail.getPaidAt(), detail.getPaymentMethod(), detail.getPaymentReferenceNo(),
|
detail.getPaidAt(), detail.getPaymentMethod(), detail.getPaymentReferenceNo(),
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ paths:
|
|||||||
/internal/firstleg/v1/containers/{containerId}/fees/{feeId}/source-snapshot:
|
/internal/firstleg/v1/containers/{containerId}/fees/{feeId}/source-snapshot:
|
||||||
get:
|
get:
|
||||||
summary: Finance 按柜次费用来源版本精确回查不可变快照
|
summary: Finance 按柜次费用来源版本精确回查不可变快照
|
||||||
description: sourceVersion 必填;只返回 sourceFeeId 与该版本精确匹配的已发布费用,不回退到当前可变版本。
|
description: sourceVersion 必填;containerId 固定为真实 containerBookingId,只返回 sourceFeeId 与该版本精确匹配的已发布费用,不接受 loadingPlanId 回退。
|
||||||
security:
|
security:
|
||||||
- internalServiceToken: []
|
- internalServiceToken: []
|
||||||
parameters:
|
parameters:
|
||||||
@@ -2650,8 +2650,8 @@ components:
|
|||||||
inboundId: {type: integer, format: int64, nullable: true}
|
inboundId: {type: integer, format: int64, nullable: true}
|
||||||
inboundCode: {type: string, nullable: true}
|
inboundCode: {type: string, nullable: true}
|
||||||
orderId: {type: integer, format: int64, nullable: true}
|
orderId: {type: integer, format: int64, nullable: true}
|
||||||
containerId: {type: integer, format: int64, nullable: true}
|
containerId: {type: integer, format: int64, nullable: true, description: CONTAINER_FEE时固定为真实containerBookingId}
|
||||||
containerBookingId: {type: integer, format: int64, nullable: true}
|
containerBookingId: {type: integer, format: int64, nullable: true, description: 真实containerBookingId;CONTAINER_FEE时与containerId一致}
|
||||||
containerNo: {type: string, nullable: true}
|
containerNo: {type: string, nullable: true}
|
||||||
customerId: {type: integer, format: int64}
|
customerId: {type: integer, format: int64}
|
||||||
customerName: {type: string}
|
customerName: {type: string}
|
||||||
|
|||||||
+4
-1
@@ -75,6 +75,7 @@ class ContainerFeeDetailServiceTest {
|
|||||||
approved.setId(51L);
|
approved.setId(51L);
|
||||||
approved.setTenantId(10001L);
|
approved.setTenantId(10001L);
|
||||||
approved.setLoadingPlanId(41L);
|
approved.setLoadingPlanId(41L);
|
||||||
|
approved.setContainerBookingId(61L);
|
||||||
approved.setFeeItemId(11L);
|
approved.setFeeItemId(11L);
|
||||||
approved.setDirection("RECEIVABLE");
|
approved.setDirection("RECEIVABLE");
|
||||||
approved.setSourceVersion(1);
|
approved.setSourceVersion(1);
|
||||||
@@ -87,7 +88,7 @@ class ContainerFeeDetailServiceTest {
|
|||||||
service.approve(10001L, 41L, 51L, 9L, "admin");
|
service.approve(10001L, 41L, 51L, 9L, "admin");
|
||||||
|
|
||||||
verify(outbox).publishContainer(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
verify(outbox).publishContainer(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||||
eq(10001L), eq(41L), eq(51L), eq(1), any(LocalDateTime.class));
|
eq(10001L), eq(61L), eq(51L), eq(1), any(LocalDateTime.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -284,6 +285,7 @@ class ContainerFeeDetailServiceTest {
|
|||||||
private void ownedPlan() {
|
private void ownedPlan() {
|
||||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||||
plan.setId(41L);
|
plan.setId(41L);
|
||||||
|
plan.setContainerBookingId(61L);
|
||||||
when(loading.plan(41L)).thenReturn(plan);
|
when(loading.plan(41L)).thenReturn(plan);
|
||||||
LoadingPlanItemEntity item = new LoadingPlanItemEntity();
|
LoadingPlanItemEntity item = new LoadingPlanItemEntity();
|
||||||
item.setAccountId(10001L);
|
item.setAccountId(10001L);
|
||||||
@@ -316,6 +318,7 @@ class ContainerFeeDetailServiceTest {
|
|||||||
value.setId(51L);
|
value.setId(51L);
|
||||||
value.setTenantId(10001L);
|
value.setTenantId(10001L);
|
||||||
value.setLoadingPlanId(41L);
|
value.setLoadingPlanId(41L);
|
||||||
|
value.setContainerBookingId(61L);
|
||||||
value.setFeeItemId(11L);
|
value.setFeeItemId(11L);
|
||||||
value.setFeeItemCodeSnapshot("TRUCKING");
|
value.setFeeItemCodeSnapshot("TRUCKING");
|
||||||
value.setFeeItemNameSnapshot("拖车费");
|
value.setFeeItemNameSnapshot("拖车费");
|
||||||
|
|||||||
+3
-2
@@ -96,11 +96,12 @@ class FinanceFeeSourceCommandServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void routesContainerVoidThroughExistingBusinessService() throws Exception {
|
void routesContainerVoidThroughExistingBusinessService() throws Exception {
|
||||||
VoidRequest request = new VoidRequest("CONTAINER_FEE", 800L, 4, 9L,
|
VoidRequest request = new VoidRequest("CONTAINER_FEE", 800L, 4, 9L,
|
||||||
null, 200L, 71L, "finance", "柜次费用作废");
|
null, 61L, 71L, "finance", "柜次费用作废");
|
||||||
ContainerFeeDetailEntity detail = new ContainerFeeDetailEntity();
|
ContainerFeeDetailEntity detail = new ContainerFeeDetailEntity();
|
||||||
detail.setId(800L); detail.setTenantId(9L); detail.setLoadingPlanId(200L);
|
detail.setId(800L); detail.setTenantId(9L); detail.setLoadingPlanId(200L);
|
||||||
|
detail.setContainerBookingId(61L);
|
||||||
detail.setSourceVersion(4); detail.setSourceStatus("ACTIVE");
|
detail.setSourceVersion(4); detail.setSourceStatus("ACTIVE");
|
||||||
when(containerFees.find(200L, 800L)).thenReturn(detail);
|
when(containerFees.findByContainerBookingId(61L, 800L)).thenReturn(detail);
|
||||||
|
|
||||||
service.voidSource("void-container", hash(request), request);
|
service.voidSource("void-container", hash(request), request);
|
||||||
|
|
||||||
|
|||||||
+14
@@ -49,4 +49,18 @@ class FinanceFeeSourceOutboxServiceTest {
|
|||||||
assertThatCode(() -> service.publishContainer(FinanceFeeSourceOutboxService.VOIDED,
|
assertThatCode(() -> service.publishContainer(FinanceFeeSourceOutboxService.VOIDED,
|
||||||
10001L, 41L, 51L, 2, LocalDateTime.now())).doesNotThrowAnyException();
|
10001L, 41L, 51L, 2, LocalDateTime.now())).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void containerEventUsesTheRealContainerBookingIdAsAggregateAndPayloadIdentity() {
|
||||||
|
service.publishContainer(FinanceFeeSourceOutboxService.PUBLISHED,
|
||||||
|
10001L, 61L, 51L, 2, LocalDateTime.of(2026, 8, 11, 12, 0));
|
||||||
|
|
||||||
|
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||||
|
verify(mapper).insert(captor.capture());
|
||||||
|
InboundOutboxEntity event = captor.getValue();
|
||||||
|
assertThat(event.getAggregateType()).isEqualTo("CONTAINER");
|
||||||
|
assertThat(event.getAggregateId()).isEqualTo(61L);
|
||||||
|
assertThat(event.getPayloadJson()).contains("\"containerId\":\"61\"")
|
||||||
|
.doesNotContain("\"containerId\":\"41\"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -90,10 +90,12 @@ class FinanceFeeSourceSnapshotServiceTest {
|
|||||||
detail.setCurrency("CNY");
|
detail.setCurrency("CNY");
|
||||||
detail.setReviewStatus("APPROVED");
|
detail.setReviewStatus("APPROVED");
|
||||||
detail.setSourceVersion(2);
|
detail.setSourceVersion(2);
|
||||||
when(containerFees.find(41L, 51L, 2)).thenReturn(detail);
|
when(containerFees.findByContainerBookingId(61L, 51L, 2)).thenReturn(detail);
|
||||||
|
|
||||||
FinanceFeeSourceSnapshot snapshot = service.container(41L, 51L, 2);
|
FinanceFeeSourceSnapshot snapshot = service.container(61L, 51L, 2);
|
||||||
|
|
||||||
|
assertThat(snapshot.containerId()).isEqualTo(61L);
|
||||||
|
assertThat(snapshot.containerBookingId()).isEqualTo(61L);
|
||||||
assertThat(snapshot.supplierId()).isEqualTo(71L);
|
assertThat(snapshot.supplierId()).isEqualTo(71L);
|
||||||
assertThat(snapshot.supplierName()).isEqualTo("现有供应商");
|
assertThat(snapshot.supplierName()).isEqualTo("现有供应商");
|
||||||
assertThat(snapshot.supplierCategory()).isEqualTo("TRUCKING");
|
assertThat(snapshot.supplierCategory()).isEqualTo("TRUCKING");
|
||||||
|
|||||||
Reference in New Issue
Block a user