Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test
This commit is contained in:
@@ -4430,6 +4430,79 @@ DROP TEMPORARY TABLE IF EXISTS tmp_wh_fixed_platform_warehouse;
|
||||
|
||||
-- 全部增量完成后的最终结构强校验;只有通过此段才输出唯一 PASS
|
||||
-- ============================================================
|
||||
USE baiyue_firstleg;
|
||||
DROP PROCEDURE IF EXISTS unified_publish_fee_add_column;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE unified_publish_fee_add_column(
|
||||
IN target_table VARCHAR(64), IN target_column VARCHAR(64), IN column_definition TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = target_table AND column_name = target_column
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `', target_column, '` ', column_definition);
|
||||
PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL unified_publish_fee_add_column('fl_fee_item', 'fee_code',
|
||||
"VARCHAR(50) NULL COMMENT '稳定费用编码;系统费用使用固定编码' AFTER trade_scope");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'source_type',
|
||||
"VARCHAR(32) NOT NULL DEFAULT 'MANUAL' COMMENT 'MANUAL或FBA_PRICING' AFTER fee_item_id");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'source_fee_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '来源计费快照ID' AFTER source_type");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'source_fee_version',
|
||||
"INT UNSIGNED NULL COMMENT '来源计费快照版本' AFTER source_fee_id");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'source_fee_no',
|
||||
"VARCHAR(40) NULL COMMENT '来源费用单号' AFTER source_fee_version");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'customer_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '客户ID快照' AFTER business_date");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'customer_name',
|
||||
"VARCHAR(128) NULL COMMENT '客户公司名称快照' AFTER customer_id");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'invoice_title',
|
||||
"VARCHAR(256) NULL COMMENT '发票抬头快照' AFTER customer_name");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'chargeable_quantity',
|
||||
"DECIMAL(18,4) NULL COMMENT '本费用最终计费量' AFTER chargeable_volume_cbm");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'charge_unit',
|
||||
"VARCHAR(16) NULL COMMENT 'KG或CBM' AFTER chargeable_quantity");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'publication_status',
|
||||
"VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT 'DRAFT、PUBLISHED、SUPERSEDED' AFTER review_reason");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'published_by',
|
||||
"BIGINT NULL COMMENT '发布人ID' AFTER publication_status");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'published_by_name',
|
||||
"VARCHAR(64) NULL COMMENT '发布人名称快照' AFTER published_by");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'published_at',
|
||||
"DATETIME(3) NULL COMMENT '发布时间' AFTER published_by_name");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'inbounded_at',
|
||||
"DATETIME(3) NULL COMMENT '入库完成时间快照' AFTER published_at");
|
||||
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'settlement_included',
|
||||
"TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否参与结算' AFTER inbounded_at");
|
||||
DROP PROCEDURE IF EXISTS unified_publish_fee_add_column;
|
||||
|
||||
DROP PROCEDURE IF EXISTS unified_publish_fee_add_index;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE unified_publish_fee_add_index(
|
||||
IN target_table VARCHAR(64), IN target_index VARCHAR(64), IN index_definition TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = target_table AND index_name = target_index
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD ', index_definition);
|
||||
PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL unified_publish_fee_add_index('fl_fee_item', 'uk_fee_item_code',
|
||||
'UNIQUE KEY `uk_fee_item_code` (`tenant_id`, `fee_code`)');
|
||||
CALL unified_publish_fee_add_index('fl_inbound_fee_detail', 'uk_inbound_fee_source',
|
||||
'UNIQUE KEY `uk_inbound_fee_source` (`tenant_id`, `source_type`, `source_fee_id`)');
|
||||
CALL unified_publish_fee_add_index('fl_inbound_fee_detail', 'idx_inbound_fee_published',
|
||||
'KEY `idx_inbound_fee_published` (`tenant_id`, `inbound_id`, `publication_status`, `source_fee_version`)');
|
||||
DROP PROCEDURE IF EXISTS unified_publish_fee_add_index;
|
||||
|
||||
DROP PROCEDURE IF EXISTS unified_assert_final_extensions;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE unified_assert_final_extensions()
|
||||
@@ -4448,6 +4521,37 @@ BEGIN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '统一迁移失败:费用表 tenant_id 客户隔离语义未同步';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_firstleg'
|
||||
AND table_name = 'fl_fee_item'
|
||||
AND column_name = 'fee_code') <> 1
|
||||
OR (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_firstleg'
|
||||
AND table_name = 'fl_inbound_fee_detail'
|
||||
AND column_name IN (
|
||||
'source_type', 'source_fee_id', 'source_fee_version', 'source_fee_no',
|
||||
'customer_id', 'customer_name', 'invoice_title',
|
||||
'chargeable_quantity', 'charge_unit', 'publication_status',
|
||||
'published_by', 'published_by_name', 'published_at', 'inbounded_at',
|
||||
'settlement_included')) <> 15 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '统一迁移失败:已发布入库运费明细字段未完整创建';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg'
|
||||
AND table_name = 'fl_fee_item'
|
||||
AND index_name = 'uk_fee_item_code' AND non_unique = 0) <> 2
|
||||
OR (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg'
|
||||
AND table_name = 'fl_inbound_fee_detail'
|
||||
AND index_name = 'uk_inbound_fee_source' AND non_unique = 0) <> 3
|
||||
OR (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg'
|
||||
AND table_name = 'fl_inbound_fee_detail'
|
||||
AND index_name = 'idx_inbound_fee_published') <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '统一迁移失败:已发布入库运费明细索引未完整创建';
|
||||
END IF;
|
||||
IF (SELECT COUNT(DISTINCT CONCAT(table_name, '.', index_name))
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg'
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.baiyue.firstleg.dto;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
|
||||
@@ -12,7 +14,8 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
|
||||
List<ImageReference> measurementImages,
|
||||
List<ImageReference> weightImages,
|
||||
BillingBasis billingBasis,
|
||||
FeeSnapshotResponse latestFee) {
|
||||
FeeSnapshotResponse latestFee,
|
||||
PublishedFeeDetail publishedFeeDetail) {
|
||||
|
||||
public record MeasurementSummary(
|
||||
Long measurementId, String sourceType, String measurementStatus,
|
||||
@@ -45,4 +48,21 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
|
||||
BigDecimal convertedQuantity, BigDecimal chargeableQuantity,
|
||||
BigDecimal chargeableWeightKg, BigDecimal chargeableVolumeCbm) {
|
||||
}
|
||||
|
||||
/** 后台和客户共同读取的同一条已发布入库运费明细。 */
|
||||
public record PublishedFeeDetail(
|
||||
Long id, Long inboundId, String inboundCode,
|
||||
Long sourceFeeId, Integer sourceFeeVersion, String sourceFeeNo,
|
||||
String direction, LocalDate businessDate,
|
||||
Long customerId, String customerName, String invoiceTitle,
|
||||
String channelCode, String channelName,
|
||||
String destinationCode, String destinationName,
|
||||
Integer packageCount, BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
|
||||
BigDecimal volumeWeightKg, BigDecimal chargeableWeightKg, BigDecimal chargeableVolumeCbm,
|
||||
BigDecimal chargeableQuantity, String chargeUnit,
|
||||
String feeTypeName, BigDecimal unitPrice, String currency, BigDecimal amount,
|
||||
String reviewStatus, String publicationStatus,
|
||||
LocalDateTime publishedAt, LocalDateTime inboundedAt, Boolean settlementIncluded
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public class FeeItemEntity extends BaseEntity {
|
||||
private Long tenantId;
|
||||
private String category;
|
||||
private String tradeScope;
|
||||
private String feeCode;
|
||||
private String feeTypeName;
|
||||
private Boolean pickupPayable;
|
||||
private Boolean pickupReceivable;
|
||||
|
||||
@@ -21,9 +21,16 @@ public class InboundFeeDetailEntity extends BaseEntity {
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private Long feeItemId;
|
||||
private String sourceType;
|
||||
private Long sourceFeeId;
|
||||
private Integer sourceFeeVersion;
|
||||
private String sourceFeeNo;
|
||||
private String inboundCode;
|
||||
private String direction;
|
||||
private LocalDate businessDate;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private String invoiceTitle;
|
||||
private String feeCategorySnapshot;
|
||||
private String tradeScopeSnapshot;
|
||||
private String feeTypeNameSnapshot;
|
||||
@@ -37,6 +44,8 @@ public class InboundFeeDetailEntity extends BaseEntity {
|
||||
private BigDecimal volumeWeightKg;
|
||||
private BigDecimal chargeableWeightKg;
|
||||
private BigDecimal chargeableVolumeCbm;
|
||||
private BigDecimal chargeableQuantity;
|
||||
private String chargeUnit;
|
||||
private BigDecimal unitPrice;
|
||||
private String currency;
|
||||
private BigDecimal amount;
|
||||
@@ -46,4 +55,10 @@ public class InboundFeeDetailEntity extends BaseEntity {
|
||||
private String reviewerName;
|
||||
private LocalDateTime reviewedAt;
|
||||
private String reviewReason;
|
||||
private String publicationStatus;
|
||||
private Long publishedBy;
|
||||
private String publishedByName;
|
||||
private LocalDateTime publishedAt;
|
||||
private LocalDateTime inboundedAt;
|
||||
private Boolean settlementIncluded;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ public class FeeItemRepository {
|
||||
.eq(FeeItemEntity::getId, id));
|
||||
}
|
||||
|
||||
public FeeItemEntity findItemByCode(long tenantId, String feeCode) {
|
||||
return feeItems.selectOne(new LambdaQueryWrapper<FeeItemEntity>()
|
||||
.eq(FeeItemEntity::getTenantId, tenantId)
|
||||
.eq(FeeItemEntity::getFeeCode, feeCode));
|
||||
}
|
||||
|
||||
public boolean itemNameExists(long tenantId, String tradeScope, String feeTypeName, Long excludedId) {
|
||||
return feeItems.selectCount(new LambdaQueryWrapper<FeeItemEntity>()
|
||||
.eq(FeeItemEntity::getTenantId, tenantId)
|
||||
@@ -98,6 +104,36 @@ public class FeeItemRepository {
|
||||
.eq(InboundFeeDetailEntity::getId, detailId));
|
||||
}
|
||||
|
||||
public InboundFeeDetailEntity findBySourceFee(long tenantId, String sourceType, long sourceFeeId) {
|
||||
return feeDetails.selectOne(new LambdaQueryWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(InboundFeeDetailEntity::getSourceType, sourceType)
|
||||
.eq(InboundFeeDetailEntity::getSourceFeeId, sourceFeeId));
|
||||
}
|
||||
|
||||
public InboundFeeDetailEntity findPublishedFreight(long tenantId, long inboundId) {
|
||||
return feeDetails.selectOne(new LambdaQueryWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(InboundFeeDetailEntity::getInboundId, inboundId)
|
||||
.eq(InboundFeeDetailEntity::getSourceType, "FBA_PRICING")
|
||||
.eq(InboundFeeDetailEntity::getPublicationStatus, "PUBLISHED")
|
||||
.orderByDesc(InboundFeeDetailEntity::getSourceFeeVersion)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
public int supersedePublishedFreight(long tenantId, long inboundId, long retainedSourceFeeId,
|
||||
long operatorId, LocalDateTime now) {
|
||||
return feeDetails.update(null, new LambdaUpdateWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(InboundFeeDetailEntity::getInboundId, inboundId)
|
||||
.eq(InboundFeeDetailEntity::getSourceType, "FBA_PRICING")
|
||||
.eq(InboundFeeDetailEntity::getPublicationStatus, "PUBLISHED")
|
||||
.ne(InboundFeeDetailEntity::getSourceFeeId, retainedSourceFeeId)
|
||||
.set(InboundFeeDetailEntity::getPublicationStatus, "SUPERSEDED")
|
||||
.set(InboundFeeDetailEntity::getUpdatedBy, operatorId)
|
||||
.set(InboundFeeDetailEntity::getUpdatedAt, now));
|
||||
}
|
||||
|
||||
public void insertDetail(InboundFeeDetailEntity detail) {
|
||||
feeDetails.insert(detail);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ public class FbaPricingService {
|
||||
private FbaPricingRepository repository;
|
||||
@Resource
|
||||
private Clock clock;
|
||||
@Resource
|
||||
private InboundFeeDetailService feeDetails;
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
|
||||
@@ -120,12 +122,17 @@ public class FbaPricingService {
|
||||
String hash = hash("SEND", inboundId, current.getId().toString());
|
||||
InboundPricingIdempotencyEntity token = beginIdempotency(
|
||||
tenantId, operatorId, inboundId, "FEE_SEND", idempotencyKey, hash);
|
||||
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), "WAIT_PALLET");
|
||||
if ("COMPLETED".equals(token.getStatus())) {
|
||||
feeDetails.publishFreight(tenantId, inboundId, token.getResultResourceId(),
|
||||
operatorId, null, LocalDateTime.now(clock));
|
||||
return response(tenantId, token.getResultResourceId(), "WAIT_PALLET");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now(clock);
|
||||
if (repository.sendFee(tenantId, current.getId(), operatorId, now) != 1
|
||||
|| repository.markInboundWaitingPallet(tenantId, inboundId) != 1) {
|
||||
throw conflict("费用未确认或入库单状态已变化,不能发送");
|
||||
}
|
||||
feeDetails.publishFreight(tenantId, inboundId, current.getId(), operatorId, null, now);
|
||||
InboundOutboxEntity event = new InboundOutboxEntity();
|
||||
event.setTenantId(tenantId);
|
||||
event.setAggregateType("INBOUND");
|
||||
@@ -207,6 +214,7 @@ public class FbaPricingService {
|
||||
extra.setRemark(request.reason().trim());
|
||||
repository.insertItem(extra);
|
||||
}
|
||||
feeDetails.publishFreight(tenantId, inboundId, adjusted.getId(), operatorId, null, now);
|
||||
InboundOutboxEntity event = new InboundOutboxEntity();
|
||||
event.setTenantId(tenantId);
|
||||
event.setAggregateType("INBOUND");
|
||||
|
||||
@@ -27,6 +27,8 @@ public class InboundCompletionOrchestrator {
|
||||
private InboundWorkflowRepository repository;
|
||||
@Resource
|
||||
private InboundIdempotencyService idempotency;
|
||||
@Resource
|
||||
private InboundFeeDetailService feeDetails;
|
||||
|
||||
/**
|
||||
* 任务5对接方法一:锁定发送动作。允许前置状态仅为 FEE_CONFIRMING + CONFIRMED。
|
||||
@@ -87,6 +89,8 @@ public class InboundCompletionOrchestrator {
|
||||
if (repository.markFeeSent(tenantId, inboundId, feeId, operatorId) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "费用快照已被并发发送或状态不正确");
|
||||
}
|
||||
feeDetails.publishFreight(tenantId, inboundId, feeId, operatorId, operatorName,
|
||||
LocalDateTime.now());
|
||||
repository.insertHistory(MeasurementReviewService.history(tenantId, inboundId,
|
||||
"INBOUNDING", "INBOUNDED", "SEND_FEE_COMPLETE", null, operatorId, operatorName));
|
||||
repository.insertOutbox(completedEvent(tenantId, inboundId, feeId));
|
||||
|
||||
@@ -4,27 +4,121 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.InboundFeeDetailDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 入库单人工费用明细应用服务。 */
|
||||
@Service
|
||||
public class InboundFeeDetailService {
|
||||
private static final String FREIGHT_FEE_CODE = "FBA_HEAD_FREIGHT";
|
||||
@Resource
|
||||
private FeeItemRepository repository;
|
||||
@Resource
|
||||
private FbaPricingRepository pricingRepository;
|
||||
|
||||
public List<InboundFeeDetailEntity> list(long tenantId, long inboundId) {
|
||||
requiredInbound(tenantId, inboundId);
|
||||
return repository.listDetails(tenantId, inboundId);
|
||||
}
|
||||
|
||||
public InboundFeeDetailEntity publishedFreight(long tenantId, long inboundId) {
|
||||
requiredInbound(tenantId, inboundId);
|
||||
InboundFeeDetailEntity detail = repository.findPublishedFreight(tenantId, inboundId);
|
||||
if (detail == null) throw new BizException(ResultCode.NOT_FOUND, "入库运费明细尚未发布");
|
||||
return detail;
|
||||
}
|
||||
|
||||
/** 将计费快照发布为后台和客户共同读取的入库应收运费明细。 */
|
||||
@Transactional
|
||||
public InboundFeeDetailEntity publishFreight(long tenantId, long inboundId, long feeId,
|
||||
long operatorId, String operatorName,
|
||||
LocalDateTime publishedAt) {
|
||||
InboundFeeDetailEntity existing = repository.findBySourceFee(tenantId, "FBA_PRICING", feeId);
|
||||
if (existing != null) return existing;
|
||||
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
|
||||
InboundFeeEntity fee = pricingRepository.findFee(tenantId, feeId);
|
||||
if (fee == null || !Long.valueOf(inboundId).equals(fee.getInboundId())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "待发布的入库计费快照不存在");
|
||||
}
|
||||
if (!"SENT".equals(fee.getFeeStatus())) {
|
||||
throw conflict("只有已发送状态的计费快照才能生成客户运费明细");
|
||||
}
|
||||
LocalDateTime now = publishedAt == null ? LocalDateTime.now() : publishedAt;
|
||||
FeeItemEntity feeItem = requiredFreightItem(tenantId, operatorId, operatorName);
|
||||
InboundFeeItemEntity baseItem = pricingRepository.findBaseItem(tenantId, feeId);
|
||||
repository.supersedePublishedFreight(tenantId, inboundId, feeId, operatorId, now);
|
||||
|
||||
InboundFeeDetailEntity detail = new InboundFeeDetailEntity();
|
||||
detail.setTenantId(tenantId);
|
||||
detail.setInboundId(inboundId);
|
||||
detail.setFeeItemId(feeItem.getId());
|
||||
detail.setSourceType("FBA_PRICING");
|
||||
detail.setSourceFeeId(feeId);
|
||||
detail.setSourceFeeVersion(fee.getFeeVersion());
|
||||
detail.setSourceFeeNo(fee.getFeeNo());
|
||||
detail.setInboundCode(inbound.getInboundCode());
|
||||
detail.setDirection("RECEIVABLE");
|
||||
detail.setBusinessDate(now.toLocalDate());
|
||||
detail.setCustomerId(inbound.getCustomerId());
|
||||
detail.setCustomerName(inbound.getCustomerName());
|
||||
detail.setInvoiceTitle(inbound.getCompanyName());
|
||||
detail.setFeeCategorySnapshot(feeItem.getCategory());
|
||||
detail.setTradeScopeSnapshot(feeItem.getTradeScope());
|
||||
detail.setFeeTypeNameSnapshot(feeItem.getFeeTypeName());
|
||||
detail.setChannelCode(inbound.getChannelCode());
|
||||
detail.setChannelName(inbound.getChannelName());
|
||||
detail.setDestinationCode(inbound.getDestinationCode());
|
||||
detail.setDestinationName(inbound.getDestinationName());
|
||||
detail.setPackageCount(inbound.getTotalPackageCount());
|
||||
detail.setActualGrossWeightKg(fee.getActualWeightKg());
|
||||
detail.setActualVolumeCbm(fee.getActualVolumeCbm());
|
||||
detail.setVolumeWeightKg(inbound.getVolumeWeightKg());
|
||||
detail.setChargeableWeightKg("KG".equals(fee.getChargeUnit())
|
||||
? fee.getChargeableQuantity() : inbound.getChargeableWeightKg());
|
||||
detail.setChargeableVolumeCbm("CBM".equals(fee.getChargeUnit())
|
||||
? fee.getChargeableQuantity() : inbound.getChargeableVolumeCbm());
|
||||
detail.setChargeableQuantity(fee.getChargeableQuantity());
|
||||
detail.setChargeUnit(fee.getChargeUnit());
|
||||
detail.setUnitPrice(effectiveUnitPrice(fee));
|
||||
detail.setCurrency(fee.getCurrency());
|
||||
detail.setAmount(fee.getTotalAmount().setScale(2, RoundingMode.HALF_UP));
|
||||
detail.setRemark(baseItem == null ? "来源费用单:" + fee.getFeeNo()
|
||||
: "来源费用单:" + fee.getFeeNo() + ";基础单价:" + baseItem.getUnitPrice());
|
||||
detail.setReviewStatus("APPROVED");
|
||||
detail.setReviewerId(operatorId);
|
||||
detail.setReviewerName(operatorName);
|
||||
detail.setReviewedAt(now);
|
||||
detail.setPublicationStatus("PUBLISHED");
|
||||
detail.setPublishedBy(operatorId);
|
||||
detail.setPublishedByName(operatorName);
|
||||
detail.setPublishedAt(now);
|
||||
detail.setInboundedAt(inbound.getInboundedAt());
|
||||
detail.setSettlementIncluded(true);
|
||||
detail.setCreatedBy(operatorId);
|
||||
detail.setUpdatedBy(operatorId);
|
||||
try {
|
||||
repository.insertDetail(detail);
|
||||
return detail;
|
||||
} catch (DuplicateKeyException exception) {
|
||||
InboundFeeDetailEntity concurrent = repository.findBySourceFee(
|
||||
tenantId, "FBA_PRICING", feeId);
|
||||
if (concurrent != null) return concurrent;
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundFeeDetailEntity create(long tenantId, long inboundId, long operatorId,
|
||||
SaveRequest request) {
|
||||
@@ -36,6 +130,9 @@ public class InboundFeeDetailService {
|
||||
value.setCreatedBy(operatorId);
|
||||
apply(value, inbound, item, request, operatorId);
|
||||
value.setReviewStatus("PENDING");
|
||||
value.setSourceType("MANUAL");
|
||||
value.setPublicationStatus("DRAFT");
|
||||
value.setSettlementIncluded(false);
|
||||
repository.insertDetail(value);
|
||||
return value;
|
||||
}
|
||||
@@ -95,6 +192,46 @@ public class InboundFeeDetailService {
|
||||
return value;
|
||||
}
|
||||
|
||||
private FeeItemEntity requiredFreightItem(long tenantId, long operatorId, String operatorName) {
|
||||
FeeItemEntity item = repository.findItemByCode(tenantId, FREIGHT_FEE_CODE);
|
||||
if (item != null) {
|
||||
if (!"ACTIVE".equals(item.getStatus())) throw conflict("系统费用项目FBA头程运费已停用");
|
||||
return item;
|
||||
}
|
||||
FeeItemEntity created = new FeeItemEntity();
|
||||
created.setTenantId(tenantId);
|
||||
created.setCategory("头程运费");
|
||||
created.setTradeScope("INTERNATIONAL");
|
||||
created.setFeeCode(FREIGHT_FEE_CODE);
|
||||
created.setFeeTypeName("FBA头程运费");
|
||||
created.setPickupPayable(false);
|
||||
created.setPickupReceivable(false);
|
||||
created.setWarehousePayable(false);
|
||||
created.setWarehouseReceivable(true);
|
||||
created.setAllocationFee(false);
|
||||
created.setDescription("系统计费发布时自动维护,不允许用于应付费用");
|
||||
created.setStatus("ACTIVE");
|
||||
created.setCreatedBy(operatorId);
|
||||
created.setUpdatedBy(operatorId);
|
||||
created.setCreatedByName(operatorName);
|
||||
created.setUpdatedByName(operatorName);
|
||||
try {
|
||||
repository.insertItem(created);
|
||||
return created;
|
||||
} catch (DuplicateKeyException exception) {
|
||||
FeeItemEntity concurrent = repository.findItemByCode(tenantId, FREIGHT_FEE_CODE);
|
||||
if (concurrent != null) return concurrent;
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private static java.math.BigDecimal effectiveUnitPrice(InboundFeeEntity fee) {
|
||||
if (fee.getChargeableQuantity() == null || fee.getChargeableQuantity().signum() <= 0) {
|
||||
throw conflict("计费快照的计费量必须大于零");
|
||||
}
|
||||
return fee.getTotalAmount().divide(fee.getChargeableQuantity(), 4, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private static void ensureWarehouseApplicable(FeeItemEntity item, String direction) {
|
||||
boolean applicable = "RECEIVABLE".equals(direction)
|
||||
? Boolean.TRUE.equals(item.getWarehouseReceivable())
|
||||
|
||||
+21
-1
@@ -11,6 +11,7 @@ import com.baiyue.firstleg.dto.InboundOrderDetail;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.repository.InboundOrderRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -26,6 +27,8 @@ public class MerchantInboundBillingService {
|
||||
private InboundMeasurementService measurements;
|
||||
@Resource
|
||||
private MeasurementImageClient imageClient;
|
||||
@Resource
|
||||
private InboundFeeDetailService feeDetails;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -45,6 +48,7 @@ public class MerchantInboundBillingService {
|
||||
.toList(), List.of(), List.of());
|
||||
InboundMeasurementResponses.Detail measurement = measurements.detail(tenantId, inbound.getId());
|
||||
FeeSnapshotResponse fee = pricing.latest(tenantId, inbound.getId());
|
||||
InboundFeeDetailEntity publishedFee = feeDetails.publishedFreight(tenantId, inbound.getId());
|
||||
var summary = new MerchantInboundBillingResponse.MeasurementSummary(
|
||||
measurement.measurementId(), measurement.sourceType(), measurement.measurementStatus(),
|
||||
measurement.actualPackageCount(), measurement.actualGrossWeightKg(),
|
||||
@@ -78,7 +82,7 @@ public class MerchantInboundBillingService {
|
||||
platformDetail.summary().chargeableWeightKg(),
|
||||
platformDetail.summary().chargeableVolumeCbm());
|
||||
return new MerchantInboundBillingResponse(merchantDetail, summary, measurementItems,
|
||||
weightResults, measurementImages, weightImages, basis, fee);
|
||||
weightResults, measurementImages, weightImages, basis, fee, publishedFee(publishedFee));
|
||||
}
|
||||
|
||||
public ImageDownload downloadImage(long tenantId, long orderId, long fileId) {
|
||||
@@ -121,6 +125,22 @@ public class MerchantInboundBillingService {
|
||||
return compared > 0 ? "ACTUAL_VOLUME" : "WEIGHT_CONVERTED_VOLUME";
|
||||
}
|
||||
|
||||
private static MerchantInboundBillingResponse.PublishedFeeDetail publishedFee(
|
||||
InboundFeeDetailEntity detail) {
|
||||
return new MerchantInboundBillingResponse.PublishedFeeDetail(
|
||||
detail.getId(), detail.getInboundId(), detail.getInboundCode(),
|
||||
detail.getSourceFeeId(), detail.getSourceFeeVersion(), detail.getSourceFeeNo(),
|
||||
detail.getDirection(), detail.getBusinessDate(), detail.getCustomerId(),
|
||||
detail.getCustomerName(), detail.getInvoiceTitle(), detail.getChannelCode(),
|
||||
detail.getChannelName(), detail.getDestinationCode(), detail.getDestinationName(),
|
||||
detail.getPackageCount(), detail.getActualGrossWeightKg(), detail.getActualVolumeCbm(),
|
||||
detail.getVolumeWeightKg(), detail.getChargeableWeightKg(),
|
||||
detail.getChargeableVolumeCbm(), detail.getChargeableQuantity(), detail.getChargeUnit(),
|
||||
detail.getFeeTypeNameSnapshot(), detail.getUnitPrice(), detail.getCurrency(), detail.getAmount(),
|
||||
detail.getReviewStatus(), detail.getPublicationStatus(), detail.getPublishedAt(),
|
||||
detail.getInboundedAt(), detail.getSettlementIncluded());
|
||||
}
|
||||
|
||||
public record ImageDownload(byte[] content, String contentType) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
USE baiyue_firstleg;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 将计费快照发布为后台和客户共用的入库运费明细。
|
||||
DROP PROCEDURE IF EXISTS phase2_publish_add_column;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE phase2_publish_add_column(
|
||||
IN target_table VARCHAR(64), IN target_column VARCHAR(64), IN column_definition TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = target_table AND column_name = target_column
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `', target_column, '` ', column_definition);
|
||||
PREPARE statement FROM @ddl;
|
||||
EXECUTE statement;
|
||||
DEALLOCATE PREPARE statement;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL phase2_publish_add_column('fl_fee_item', 'fee_code',
|
||||
"VARCHAR(50) NULL COMMENT '稳定费用编码;系统费用使用固定编码' AFTER trade_scope");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'source_type',
|
||||
"VARCHAR(32) NOT NULL DEFAULT 'MANUAL' COMMENT 'MANUAL或FBA_PRICING' AFTER fee_item_id");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'source_fee_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '来源计费快照ID' AFTER source_type");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'source_fee_version',
|
||||
"INT UNSIGNED NULL COMMENT '来源计费快照版本' AFTER source_fee_id");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'source_fee_no',
|
||||
"VARCHAR(40) NULL COMMENT '来源费用单号' AFTER source_fee_version");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'customer_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '客户ID快照' AFTER business_date");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'customer_name',
|
||||
"VARCHAR(128) NULL COMMENT '客户公司名称快照' AFTER customer_id");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'invoice_title',
|
||||
"VARCHAR(256) NULL COMMENT '发票抬头快照' AFTER customer_name");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'chargeable_quantity',
|
||||
"DECIMAL(18,4) NULL COMMENT '本费用最终计费量' AFTER chargeable_volume_cbm");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'charge_unit',
|
||||
"VARCHAR(16) NULL COMMENT 'KG或CBM' AFTER chargeable_quantity");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'publication_status',
|
||||
"VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT 'DRAFT、PUBLISHED、SUPERSEDED' AFTER review_reason");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'published_by',
|
||||
"BIGINT NULL COMMENT '发布人ID' AFTER publication_status");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'published_by_name',
|
||||
"VARCHAR(64) NULL COMMENT '发布人名称快照' AFTER published_by");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'published_at',
|
||||
"DATETIME(3) NULL COMMENT '发布时间' AFTER published_by_name");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'inbounded_at',
|
||||
"DATETIME(3) NULL COMMENT '入库完成时间快照' AFTER published_at");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'settlement_included',
|
||||
"TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否参与结算' AFTER inbounded_at");
|
||||
|
||||
DROP PROCEDURE IF EXISTS phase2_publish_add_column;
|
||||
|
||||
DROP PROCEDURE IF EXISTS phase2_publish_add_index;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE phase2_publish_add_index(
|
||||
IN target_table VARCHAR(64), IN target_index VARCHAR(64), IN index_definition TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = target_table AND index_name = target_index
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD ', index_definition);
|
||||
PREPARE statement FROM @ddl;
|
||||
EXECUTE statement;
|
||||
DEALLOCATE PREPARE statement;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL phase2_publish_add_index('fl_fee_item', 'uk_fee_item_code',
|
||||
'UNIQUE KEY `uk_fee_item_code` (`tenant_id`, `fee_code`)');
|
||||
CALL phase2_publish_add_index('fl_inbound_fee_detail', 'uk_inbound_fee_source',
|
||||
'UNIQUE KEY `uk_inbound_fee_source` (`tenant_id`, `source_type`, `source_fee_id`)');
|
||||
CALL phase2_publish_add_index('fl_inbound_fee_detail', 'idx_inbound_fee_published',
|
||||
'KEY `idx_inbound_fee_published` (`tenant_id`, `inbound_id`, `publication_status`, `source_fee_version`)');
|
||||
|
||||
DROP PROCEDURE IF EXISTS phase2_publish_add_index;
|
||||
@@ -183,7 +183,11 @@ paths:
|
||||
parameters:
|
||||
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
'200':
|
||||
description: 成功;publishedFeeDetail 是后台和客户共同读取的同一条已发布入库运费明细,latestFee 仅为兼容保留的计费快照
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/MerchantInboundBillingEnvelope'}
|
||||
'403': {description: 非当前客户或无订单查看权限}
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/exception-release:
|
||||
post:
|
||||
@@ -255,6 +259,63 @@ components:
|
||||
in: query
|
||||
schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}
|
||||
schemas:
|
||||
MerchantInboundBillingEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string, example: '200'}
|
||||
message: {type: string}
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
inbound: {type: object, description: 入库单详情}
|
||||
measurementSummary: {type: object, description: 测量汇总}
|
||||
measurementItems: {type: array, items: {type: object}}
|
||||
weightResults: {type: array, items: {type: object}}
|
||||
measurementImages: {type: array, items: {type: object}}
|
||||
weightImages: {type: array, items: {type: object}}
|
||||
billingBasis: {type: object, description: 计费依据}
|
||||
latestFee: {type: object, nullable: true, description: 兼容保留的最新计费快照}
|
||||
publishedFeeDetail:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/PublishedFreightFeeDetail'}
|
||||
nullable: true
|
||||
description: 后台与客户共用的已发布FBA头程运费应收明细
|
||||
PublishedFreightFeeDetail:
|
||||
type: object
|
||||
description: 审核发布后绑定入库单的FBA头程运费应收明细
|
||||
properties:
|
||||
id: {type: integer, format: int64, description: 入库费用明细ID}
|
||||
inboundId: {type: integer, format: int64}
|
||||
inboundCode: {type: string}
|
||||
sourceFeeId: {type: integer, format: int64, description: 来源计费快照ID}
|
||||
sourceFeeVersion: {type: integer}
|
||||
sourceFeeNo: {type: string}
|
||||
direction: {type: string, enum: [RECEIVABLE]}
|
||||
businessDate: {type: string, format: date}
|
||||
customerId: {type: integer, format: int64}
|
||||
customerName: {type: string}
|
||||
invoiceTitle: {type: string, nullable: true}
|
||||
channelCode: {type: string, nullable: true}
|
||||
channelName: {type: string, nullable: true}
|
||||
destinationCode: {type: string, nullable: true}
|
||||
destinationName: {type: string, nullable: true}
|
||||
packageCount: {type: integer, nullable: true}
|
||||
actualGrossWeightKg: {type: number, format: double, nullable: true}
|
||||
actualVolumeCbm: {type: number, format: double, nullable: true}
|
||||
volumeWeightKg: {type: number, format: double, nullable: true}
|
||||
chargeableWeightKg: {type: number, format: double, nullable: true}
|
||||
chargeableVolumeCbm: {type: number, format: double, nullable: true}
|
||||
chargeableQuantity: {type: number, format: double}
|
||||
chargeUnit: {type: string, enum: [KG, CBM]}
|
||||
feeTypeName: {type: string, example: FBA头程运费}
|
||||
unitPrice: {type: number, format: double}
|
||||
currency: {type: string, example: CNY}
|
||||
amount: {type: number, format: double, description: 客户应收金额}
|
||||
reviewStatus: {type: string, enum: [APPROVED]}
|
||||
publicationStatus: {type: string, enum: [PUBLISHED]}
|
||||
publishedAt: {type: string, format: date-time}
|
||||
inboundedAt: {type: string, format: date-time, nullable: true}
|
||||
settlementIncluded: {type: boolean, example: true}
|
||||
InboundOrderPageEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -47,6 +47,7 @@ class FbaPricingServiceTest {
|
||||
private static final long INBOUND = 30001L;
|
||||
private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 3, 10, 0);
|
||||
@MockBean private FbaPricingRepository repository;
|
||||
@MockBean private InboundFeeDetailService feeDetails;
|
||||
@Resource private FbaPricingService service;
|
||||
|
||||
@Test
|
||||
@@ -172,6 +173,7 @@ class FbaPricingServiceTest {
|
||||
|
||||
verify(repository, times(1)).insertOutbox(any());
|
||||
verify(repository, times(1)).sendFee(TENANT, 501L, OPERATOR, NOW);
|
||||
verify(feeDetails, times(2)).publishFreight(TENANT, INBOUND, 501L, OPERATOR, null, NOW);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -6,22 +6,29 @@ import com.baiyue.firstleg.dto.FeeItemDtos;
|
||||
import com.baiyue.firstleg.dto.InboundFeeDetailDtos;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -31,6 +38,8 @@ import static org.mockito.Mockito.when;
|
||||
class FeeManagementServiceTest {
|
||||
@MockBean
|
||||
private FeeItemRepository repository;
|
||||
@MockBean
|
||||
private FbaPricingRepository pricingRepository;
|
||||
@Resource
|
||||
private FeeItemService feeItems;
|
||||
@Resource
|
||||
@@ -96,6 +105,92 @@ class FeeManagementServiceTest {
|
||||
assertThat(created.getReviewStatus()).isEqualTo("PENDING");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("发布计费快照生成客户与后台共用的入库运费明细")
|
||||
void publishesSharedFreightDetail() {
|
||||
InboundOrderEntity inbound = inbound();
|
||||
inbound.setCustomerId(101L);
|
||||
inbound.setCustomerName("客户A");
|
||||
inbound.setCompanyName("客户A有限公司");
|
||||
when(repository.findInbound(10001L, 21L)).thenReturn(inbound);
|
||||
when(repository.findBySourceFee(10001L, "FBA_PRICING", 501L)).thenReturn(null);
|
||||
when(repository.findItemByCode(10001L, "FBA_HEAD_FREIGHT")).thenReturn(null);
|
||||
doAnswer(invocation -> {
|
||||
FeeItemEntity value = invocation.getArgument(0);
|
||||
value.setId(11L);
|
||||
return null;
|
||||
}).when(repository).insertItem(any());
|
||||
|
||||
InboundFeeEntity fee = new InboundFeeEntity();
|
||||
fee.setId(501L);
|
||||
fee.setInboundId(21L);
|
||||
fee.setFeeNo("FBAF-21-1");
|
||||
fee.setFeeVersion(1);
|
||||
fee.setFeeStatus("SENT");
|
||||
fee.setActualWeightKg(new BigDecimal("100.0000"));
|
||||
fee.setActualVolumeCbm(new BigDecimal("1.000000"));
|
||||
fee.setChargeableQuantity(new BigDecimal("167.0000"));
|
||||
fee.setChargeUnit("KG");
|
||||
fee.setCurrency("USD");
|
||||
fee.setTotalAmount(new BigDecimal("354.00"));
|
||||
when(pricingRepository.findFee(10001L, 501L)).thenReturn(fee);
|
||||
InboundFeeItemEntity base = new InboundFeeItemEntity();
|
||||
base.setUnitPrice(new BigDecimal("2.0000"));
|
||||
when(pricingRepository.findBaseItem(10001L, 501L)).thenReturn(base);
|
||||
|
||||
LocalDateTime publishedAt = LocalDateTime.of(2026, 8, 5, 12, 0);
|
||||
InboundFeeDetailEntity result = details.publishFreight(
|
||||
10001L, 21L, 501L, 9L, "admin", publishedAt);
|
||||
|
||||
ArgumentCaptor<InboundFeeDetailEntity> captor = ArgumentCaptor.forClass(InboundFeeDetailEntity.class);
|
||||
verify(repository).insertDetail(captor.capture());
|
||||
InboundFeeDetailEntity saved = captor.getValue();
|
||||
assertThat(result).isSameAs(saved);
|
||||
assertThat(saved.getFeeItemId()).isEqualTo(11L);
|
||||
assertThat(saved.getSourceFeeId()).isEqualTo(501L);
|
||||
assertThat(saved.getDirection()).isEqualTo("RECEIVABLE");
|
||||
assertThat(saved.getAmount()).isEqualByComparingTo("354.00");
|
||||
assertThat(saved.getUnitPrice()).isEqualByComparingTo("2.1198");
|
||||
assertThat(saved.getReviewStatus()).isEqualTo("APPROVED");
|
||||
assertThat(saved.getPublicationStatus()).isEqualTo("PUBLISHED");
|
||||
assertThat(saved.getSettlementIncluded()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("并发发布同一计费快照时返回已生成的运费明细")
|
||||
void reusesConcurrentFreightDetail() {
|
||||
InboundFeeDetailEntity concurrent = new InboundFeeDetailEntity();
|
||||
concurrent.setId(99L);
|
||||
when(repository.findBySourceFee(10001L, "FBA_PRICING", 501L))
|
||||
.thenReturn(null, concurrent);
|
||||
when(repository.findInbound(10001L, 21L)).thenReturn(inbound());
|
||||
|
||||
FeeItemEntity freightItem = item(true, false);
|
||||
freightItem.setFeeCode("FBA_HEAD_FREIGHT");
|
||||
when(repository.findItemByCode(10001L, "FBA_HEAD_FREIGHT")).thenReturn(freightItem);
|
||||
|
||||
InboundFeeEntity fee = new InboundFeeEntity();
|
||||
fee.setId(501L);
|
||||
fee.setInboundId(21L);
|
||||
fee.setFeeNo("FBAF-21-1");
|
||||
fee.setFeeVersion(1);
|
||||
fee.setFeeStatus("SENT");
|
||||
fee.setActualWeightKg(new BigDecimal("100.0000"));
|
||||
fee.setActualVolumeCbm(new BigDecimal("1.000000"));
|
||||
fee.setChargeableQuantity(new BigDecimal("1.0000"));
|
||||
fee.setChargeUnit("CBM");
|
||||
fee.setCurrency("USD");
|
||||
fee.setTotalAmount(new BigDecimal("200.00"));
|
||||
when(pricingRepository.findFee(10001L, 501L)).thenReturn(fee);
|
||||
doThrow(new DuplicateKeyException("duplicate source fee"))
|
||||
.when(repository).insertDetail(any());
|
||||
|
||||
InboundFeeDetailEntity result = details.publishFreight(
|
||||
10001L, 21L, 501L, 9L, "admin", LocalDateTime.of(2026, 8, 5, 12, 0));
|
||||
|
||||
assertThat(result).isSameAs(concurrent);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("拒绝绑定未启用仓储应收的项目")
|
||||
void rejectsInapplicableItem() {
|
||||
|
||||
+5
@@ -16,6 +16,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -28,6 +29,8 @@ class InboundCompletionOrchestratorTest {
|
||||
private InboundWorkflowRepository repository;
|
||||
@MockBean
|
||||
private InboundIdempotencyService idempotency;
|
||||
@MockBean
|
||||
private InboundFeeDetailService feeDetails;
|
||||
@Resource
|
||||
private InboundCompletionOrchestrator service;
|
||||
@Test
|
||||
@@ -81,6 +84,8 @@ class InboundCompletionOrchestratorTest {
|
||||
|
||||
verify(fixture.repository).completeInbound(10001L, 10L, 9L, 5);
|
||||
verify(fixture.repository).markFeeSent(10001L, 10L, 30L, 9L);
|
||||
verify(feeDetails).publishFreight(eq(10001L), eq(10L), eq(30L), eq(9L), eq("操作员"),
|
||||
org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class));
|
||||
verify(fixture.repository).insertOutbox(any());
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.dto.InboundOrderDetail;
|
||||
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.repository.InboundOrderRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -30,6 +31,7 @@ class MerchantInboundBillingServiceTest {
|
||||
@MockBean private FbaPricingService pricing;
|
||||
@MockBean private InboundMeasurementService measurements;
|
||||
@MockBean private MeasurementImageClient imageClient;
|
||||
@MockBean private InboundFeeDetailService feeDetails;
|
||||
@Resource private MerchantInboundBillingService service;
|
||||
|
||||
@Test
|
||||
@@ -50,6 +52,15 @@ class MerchantInboundBillingServiceTest {
|
||||
when(measurement.items()).thenReturn(List.of());
|
||||
when(measurement.weightResults()).thenReturn(List.of());
|
||||
when(pricing.latest(10001L, 88L)).thenReturn(fee);
|
||||
InboundFeeDetailEntity published = new InboundFeeDetailEntity();
|
||||
published.setId(601L);
|
||||
published.setInboundId(88L);
|
||||
published.setSourceFeeId(501L);
|
||||
published.setSourceFeeVersion(1);
|
||||
published.setFeeTypeNameSnapshot("FBA头程运费");
|
||||
published.setAmount(new BigDecimal("334.00"));
|
||||
published.setPublicationStatus("PUBLISHED");
|
||||
when(feeDetails.publishedFreight(10001L, 88L)).thenReturn(published);
|
||||
when(fee.chargeMode()).thenReturn("KG");
|
||||
when(fee.chargeUnit()).thenReturn("KG");
|
||||
when(fee.actualWeightKg()).thenReturn(new BigDecimal("20"));
|
||||
@@ -65,6 +76,8 @@ class MerchantInboundBillingServiceTest {
|
||||
assertThat(result.inbound().statusHistory()).isEmpty();
|
||||
assertThat(result.billingBasis().chargeableBasis()).isEqualTo("VOLUME_WEIGHT");
|
||||
assertThat(result.latestFee()).isSameAs(fee);
|
||||
assertThat(result.publishedFeeDetail().id()).isEqualTo(601L);
|
||||
assertThat(result.publishedFeeDetail().amount()).isEqualByComparingTo("334.00");
|
||||
verify(orders).findByOrderId(10001L, 77L);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user