add finance cost allocation detail

This commit is contained in:
2026-08-12 14:40:05 +08:00
parent fcb7c11af4
commit c4e6c28da0
4 changed files with 41 additions and 2 deletions
@@ -162,6 +162,12 @@ public class PlatformFinanceController {
return FinanceResponses.success(queryService.payableSettlements(customerId));
}
@GetMapping("/cost-entries/{costId}")
@PreAuthorize("hasAuthority('platform:finance:cost:view')")
public ApiResponse<?> cost(@PathVariable long costId, @RequestParam long customerId) {
return FinanceResponses.success(queryService.cost(customerId, costId));
}
@PostMapping("/cost-entries")
@PreAuthorize("hasAuthority('platform:finance:cost:create')")
@BusinessAudit(event = "FINANCE_COST_CREATE", resourceType = "FINANCE_COST")
@@ -102,6 +102,19 @@ public final class FinanceReadModels {
LocalDateTime createdAt,
int version) {}
public record CostDetail(PayableSettlement cost, List<CostAllocation> allocations) {}
public record CostAllocation(
long id,
long costId,
long orderId,
Long inboundId,
BigDecimal volumeCbm,
BigDecimal totalVolumeCbm,
BigDecimal allocationRate,
BigDecimal allocatedAmount,
boolean roundingAdjustment) {}
/** Client-safe receipt view. Voucher and operator/internal fields are deliberately absent. */
public record ClientReceipt(
long id,
@@ -7,6 +7,8 @@ import com.baiyue.finance.dto.FinanceReadModels.ClientReceipt;
import com.baiyue.finance.dto.FinanceReadModels.ClientReceiptAllocation;
import com.baiyue.finance.dto.FinanceReadModels.BillRevision;
import com.baiyue.finance.dto.FinanceReadModels.CustomerAccount;
import com.baiyue.finance.dto.FinanceReadModels.CostAllocation;
import com.baiyue.finance.dto.FinanceReadModels.CostDetail;
import com.baiyue.finance.dto.FinanceReadModels.FeePoolItem;
import com.baiyue.finance.dto.FinanceReadModels.PayableSettlement;
import com.baiyue.finance.dto.FinanceReadModels.PlatformReceipt;
@@ -143,6 +145,19 @@ public class FinanceQueryService {
}, customerId);
}
public CostDetail cost(long customerId, long costId) {
PayableSettlement cost = payableSettlements(customerId).stream()
.filter(item -> item.id() == costId).findFirst()
.orElseThrow(() -> new BizException(ResultCode.NOT_FOUND, "成本不存在或不属于客户"));
List<CostAllocation> allocations = jdbc.query(
"SELECT id,cost_id,order_id,inbound_id,volume_cbm,total_volume_cbm,allocation_rate,allocated_amount,rounding_adjustment FROM fin_cost_allocation WHERE tenant_id=? AND cost_id=? ORDER BY id",
(rs, rowNum) -> new CostAllocation(rs.getLong("id"), rs.getLong("cost_id"), rs.getLong("order_id"),
nullableLong(rs, "inbound_id"), rs.getBigDecimal("volume_cbm"), rs.getBigDecimal("total_volume_cbm"),
rs.getBigDecimal("allocation_rate"), rs.getBigDecimal("allocated_amount"), rs.getBoolean("rounding_adjustment")),
customerId, costId);
return new CostDetail(cost, allocations);
}
public List<ClientReceipt> clientReceipts(long customerId) {
requireCustomer(customerId);
return jdbc.query("SELECT id,receipt_no,received_amount,received_at,payment_method,currency_code,status FROM fin_receipt WHERE tenant_id=? AND status='CONFIRMED' ORDER BY received_at DESC,id DESC",
@@ -267,7 +267,12 @@ async function createContainerCost(context, customerId, container, voucherFileId
`message=${response.body.message || 'missing'}, loadingPlanId=${container.loadingPlanId || 'missing'}, containerBookingId=${container.containerBookingId || 'missing'}, voucherFileId=${voucherFileId || 'missing'}, publicSnapshotValid=${snapshotInput?.valid === true}, validItems=${snapshotInput?.items?.length || 0}, accountId=${first?.accountId ?? 'missing'}, orderId=${first?.orderId ?? 'missing'}, volumeCbm=${first?.volumeCbm ?? 'missing'}, requestId=${response.requestId || 'missing'}, traceId=${response.traceId || 'missing'};若 publicSnapshotValid=true,说明 fixture ID 与公开装柜明细有效,阻断位于 Finance -> Firstleg 内部快照服务部署/契约`)
}
const cost = context.apiSuccess(MODULE, '创建整柜成本并触发真实体积分摊', response, '', OPTIONS)
return cost?.id ? { costId: id(cost.id), allocations: Array.isArray(cost.allocations) ? cost.allocations : null } : null
if (!cost?.id) return null
const detailResponse = await context.request('GET',
`/api/platform/finance/v1/cost-entries/${encodeURIComponent(cost.id)}?customerId=${encodeURIComponent(customerId)}`)
const detail = context.apiSuccess(MODULE, '回读整柜成本分摊结果', detailResponse,
`costId=${cost.id}`, OPTIONS)
return { costId: id(cost.id), allocations: Array.isArray(detail?.allocations) ? detail.allocations : null }
}
async function verifyContainerCostSnapshotInput(context, customerId, container) {
@@ -382,7 +387,7 @@ export async function run(context) {
const cost = snapshotInput.valid ? await createContainerCost(context, customerId, container, voucherFileId, snapshotInput) : null
if (Array.isArray(cost?.allocations)) allocations = cost.allocations
else blocker(context, '回读整柜成本分摊结果',
`costId=${cost?.costId || 'missing'};Finance POST /api/platform/finance/v1/cost-entries 仅返回 ResourceResult,GET cost-entries 仅返回成本摘要,当前没有公开接口回读 fin_cost_allocation;禁止从金额和体积自行伪造持久化结果`)
`costId=${cost?.costId || 'missing'};Finance 成本详情未返回真实 fin_cost_allocation,禁止从金额和体积自行伪造持久化结果`)
}
if (allocations.length === 0) blocker(context, '输出真实订单/运单分摊',
'缺少已完成柜次、凭证、供应商或 Finance 分摊响应;不伪造 allocation ID/金额')