Merge remote-tracking branch 'origin/test' into test
This commit is contained in:
@@ -8467,7 +8467,6 @@ CREATE TABLE wh_document_sequence (
|
|||||||
USE baiyue_iam;
|
USE baiyue_iam;
|
||||||
INSERT INTO iam_permission (id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,route_path,http_method,sort_no,status,built_in,created_at,updated_at,deleted) VALUES (5117200,0,'PLATFORM','MENU',5105000,'platform:overseas-fee:list','海外费用明细','/overseas-business/fees','GET',60,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0);
|
INSERT INTO iam_permission (id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,route_path,http_method,sort_no,status,built_in,created_at,updated_at,deleted) VALUES (5117200,0,'PLATFORM','MENU',5105000,'platform:overseas-fee:list','海外费用明细','/overseas-business/fees','GET',60,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0);
|
||||||
INSERT INTO iam_role_permission (role_id,permission_id,tenant_id,created_at) VALUES (4000001,5117200,0,CURRENT_TIMESTAMP(3));
|
INSERT INTO iam_role_permission (role_id,permission_id,tenant_id,created_at) VALUES (4000001,5117200,0,CURRENT_TIMESTAMP(3));
|
||||||
SELECT 'PASS' AS initialization_status, 'CN' AS region, 229 AS initialized_tables;
|
|
||||||
USE baiyue_iam;
|
USE baiyue_iam;
|
||||||
-- 散板需求沿用既有菜单权限,补全当前名称和添加货物独立按钮。
|
-- 散板需求沿用既有菜单权限,补全当前名称和添加货物独立按钮。
|
||||||
INSERT INTO iam_permission (id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,route_path,http_method,sort_no,status,built_in,created_at,updated_at,deleted)
|
INSERT INTO iam_permission (id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,route_path,http_method,sort_no,status,built_in,created_at,updated_at,deleted)
|
||||||
@@ -8482,3 +8481,4 @@ SELECT 4000001,p.id,0,CURRENT_TIMESTAMP(3) FROM iam_permission p WHERE p.permiss
|
|||||||
-- 上门取货复用正式排车创建入口,仅补齐既有平台管理员角色的必要权限。
|
-- 上门取货复用正式排车创建入口,仅补齐既有平台管理员角色的必要权限。
|
||||||
INSERT INTO iam_role_permission (role_id,permission_id,tenant_id,created_at)
|
INSERT INTO iam_role_permission (role_id,permission_id,tenant_id,created_at)
|
||||||
SELECT 4000001,p.id,0,CURRENT_TIMESTAMP(3) FROM iam_permission p WHERE p.permission_code='platform:overseas-warehouse:dispatch:create' AND p.status='ACTIVE' AND p.deleted=0 AND NOT EXISTS (SELECT 1 FROM iam_role_permission rp WHERE rp.role_id=4000001 AND rp.permission_id=p.id);
|
SELECT 4000001,p.id,0,CURRENT_TIMESTAMP(3) FROM iam_permission p WHERE p.permission_code='platform:overseas-warehouse:dispatch:create' AND p.status='ACTIVE' AND p.deleted=0 AND NOT EXISTS (SELECT 1 FROM iam_role_permission rp WHERE rp.role_id=4000001 AND rp.permission_id=p.id);
|
||||||
|
SELECT 'PASS' AS initialization_status, 'CN' AS region, 229 AS initialized_tables;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
| 称重确认与整票库位块 | `service/MeasurementLocationOccupationService.java` 的 `ensureForConfirmedWeight/ensureForSubmission`、`InboundMeasurementService.java`、`MeasurementSubmissionService.java`、`client/WarehousePalletLocationClient.java`;Warehouse 块服务见 `../warehouse/src/main/java/com/baiyue/warehouse/service/LocationBlockService.java`,接口与上架引导见 `src/main/resources/openapi/firstleg-v1.yaml` |
|
| 称重确认与整票库位块 | `service/MeasurementLocationOccupationService.java` 的 `ensureForConfirmedWeight/ensureForSubmission`、`InboundMeasurementService.java`、`MeasurementSubmissionService.java`、`client/WarehousePalletLocationClient.java`;Warehouse 块服务见 `../warehouse/src/main/java/com/baiyue/warehouse/service/LocationBlockService.java`,接口与上架引导见 `src/main/resources/openapi/firstleg-v1.yaml` |
|
||||||
| 审核与初始计价 | `controller/PlatformInboundMeasurementReviewController.java`、`service/MeasurementReviewService.java`、`service/InitialInboundPricingService.java`、`dto/MeasurementApprovalResponse.java` |
|
| 审核与初始计价 | `controller/PlatformInboundMeasurementReviewController.java`、`service/MeasurementReviewService.java`、`service/InitialInboundPricingService.java`、`dto/MeasurementApprovalResponse.java` |
|
||||||
| 客户四档箱规与默认算法 | `controller/PlatformCustomerDimensionConfigController.java`、`service/CustomerDimensionConfigService.java`、`repository/CustomerDimensionConfigRepository.java`;固定 `SMALL/MEDIUM/LARGE/EXTRA_LARGE`,默认 `ACTUAL` 不调整尺寸,可启用的调整算法为 `ADD/MULTIPLY` |
|
| 客户四档箱规与默认算法 | `controller/PlatformCustomerDimensionConfigController.java`、`service/CustomerDimensionConfigService.java`、`repository/CustomerDimensionConfigRepository.java`;固定 `SMALL/MEDIUM/LARGE/EXTRA_LARGE`,默认 `ACTUAL` 不调整尺寸,可启用的调整算法为 `ADD/MULTIPLY` |
|
||||||
| FBA 主数据/计费与客户规则 | `controller/PlatformFbaMasterDataController.java`、`PlatformFbaPricingController.java`、`service/FbaPricingService.java`、`repository/FbaPricingRepository.java`;客户规则可保存多个版本,但每个客户仅一条默认规则,计算请求可提交已确认的重量/体积选择值;`POST /fees/preview` 与正式计价共用算法,只读返回当前选择值的计费量、参考单价和合计,不生成费用版本或改变履约、财务状态 |
|
| FBA 主数据/计费与客户规则 | `controller/PlatformFbaMasterDataController.java`、`PlatformFbaPricingController.java`、`service/FbaPricingService.java`、`repository/FbaPricingRepository.java`;客户规则可保存多个版本,但每个客户仅一条默认规则,计算请求可提交已确认的重量/体积选择值;私人地址 `PRIVATE` 使用 `/fees/manual-quote` 生成人工头程报价,按客户计费方式计算计费量且附加费为零,预览返回 `priceMatched=false`,报价后沿用费用版本、幂等与预装前初始费用校验;`POST /fees/preview` 与正式计价共用算法,只读返回当前选择值的计费量、参考单价和合计,不生成费用版本或改变履约、财务状态 |
|
||||||
| 客户专属价格簿、柜级阶梯运费与调价边界 | `controller/PlatformPriceBookController.java`、`service/PlatformPriceBookService.java`、`pricing/ActivePriceBookEstimator.java`、`service/FbaPricingService.java`、`pricing/ContainerTierAmountAllocator.java`;公共价格簿固定 `customerId=0`,客户专属版本按客户独立发布与替换,计价优先专属 ACTIVE、缺失才回退公共 ACTIVE。`REGION_WEIGHT_TIER` 只对同柜同客户中当前费用快照同为区域重量阶梯的有效成员聚合实际计费量命中一个阶梯,再按这些成员的计费量守恒分摊;同柜允许与 `FBA_WAREHOUSE_RATE` 等固定模型混装,固定模型成员不得参与阶梯聚合或被重算。装柜成员变化调用 `recalculateUnlockedContainerCustomerFees`,已确认/已发送或客户已确认的版本不得改写。人工调整以系统基准量为中心限制上下 10%(含边界),保存版本、原因和二次确认;一件代发 `WAIT_PALLETIZE` 可在预装确认前试算,`markInboundCalculated` 只更新费用并保留该托盘化状态 |
|
| 客户专属价格簿、柜级阶梯运费与调价边界 | `controller/PlatformPriceBookController.java`、`service/PlatformPriceBookService.java`、`pricing/ActivePriceBookEstimator.java`、`service/FbaPricingService.java`、`pricing/ContainerTierAmountAllocator.java`;公共价格簿固定 `customerId=0`,客户专属版本按客户独立发布与替换,计价优先专属 ACTIVE、缺失才回退公共 ACTIVE。`REGION_WEIGHT_TIER` 只对同柜同客户中当前费用快照同为区域重量阶梯的有效成员聚合实际计费量命中一个阶梯,再按这些成员的计费量守恒分摊;同柜允许与 `FBA_WAREHOUSE_RATE` 等固定模型混装,固定模型成员不得参与阶梯聚合或被重算。装柜成员变化调用 `recalculateUnlockedContainerCustomerFees`,已确认/已发送或客户已确认的版本不得改写。人工调整以系统基准量为中心限制上下 10%(含边界),保存版本、原因和二次确认;一件代发 `WAIT_PALLETIZE` 可在预装确认前试算,`markInboundCalculated` 只更新费用并保留该托盘化状态 |
|
||||||
| 入库费用明细 | `controller/PlatformInboundFeeDetailController.java`、`service/InboundFeeDetailService.java`、`dto/InboundFeeDetailDtos.java`;`PRIVATE_LAST_MILE_DELIVERY` 是独立私人仓尾程派送费,只允许 `destinationType=PRIVATE`,按稳定费用代码校验而不按中文名判断;Admin 消费端见 `../../apps/admin-web/src/modules/unified-orders/api/inboundFeeDetails.ts` |
|
| 入库费用明细 | `controller/PlatformInboundFeeDetailController.java`、`service/InboundFeeDetailService.java`、`dto/InboundFeeDetailDtos.java`;`PRIVATE_LAST_MILE_DELIVERY` 是独立私人仓尾程派送费,只允许 `destinationType=PRIVATE`,按稳定费用代码校验而不按中文名判断;Admin 消费端见 `../../apps/admin-web/src/modules/unified-orders/api/inboundFeeDetails.ts` |
|
||||||
| 入库/柜次直接已付快照与凭证校验 | `service/FeePaymentSnapshotValidator.java`、`client/FeePaymentVoucherFileClient.java`;写入入口分别为 `InboundFeeDetailService.java`、`ContainerFeeDetailService.java`,File 业务类型固定 `FINANCE_VOUCHER` |
|
| 入库/柜次直接已付快照与凭证校验 | `service/FeePaymentSnapshotValidator.java`、`client/FeePaymentVoucherFileClient.java`;写入入口分别为 `InboundFeeDetailService.java`、`ContainerFeeDetailService.java`,File 业务类型固定 `FINANCE_VOUCHER` |
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ public class FbaPricingService {
|
|||||||
BigDecimal volume = selectedVolume(inbound, request.selectedVolumeCbm());
|
BigDecimal volume = selectedVolume(inbound, request.selectedVolumeCbm());
|
||||||
PricingPolicy policy = pricingPolicy(inbound,
|
PricingPolicy policy = pricingPolicy(inbound,
|
||||||
request.pricingAt() == null ? LocalDateTime.now(clock) : request.pricingAt());
|
request.pricingAt() == null ? LocalDateTime.now(clock) : request.pricingAt());
|
||||||
|
if ("PRIVATE".equals(inbound.getDestinationType())) {
|
||||||
|
return manualPreview(weight, volume, policy, request.selectedUnitPrice());
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
ActivePriceBookEstimator.Estimate estimate = estimateFee(tenantId, inbound, policy, weight, volume);
|
ActivePriceBookEstimator.Estimate estimate = estimateFee(tenantId, inbound, policy, weight, volume);
|
||||||
BigDecimal additional = estimate.rate().getAdditionalFee() == null ? BigDecimal.ZERO
|
BigDecimal additional = estimate.rate().getAdditionalFee() == null ? BigDecimal.ZERO
|
||||||
@@ -91,15 +94,20 @@ public class FbaPricingService {
|
|||||||
if (exception.getResultCode() != ResultCode.NOT_FOUND || !"FBA".equals(inbound.getDestinationType())) {
|
if (exception.getResultCode() != ResultCode.NOT_FOUND || !"FBA".equals(inbound.getDestinationType())) {
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
BigDecimal factor = new BigDecimal("KG".equals(policy.chargeMode()) ? "167" : "363");
|
return manualPreview(weight, volume, policy, request.selectedUnitPrice());
|
||||||
return new FeePreviewResponse(weight, volume, manualChargeableQuantity(policy.chargeMode(), weight, volume),
|
|
||||||
policy.chargeMode(), factor, 6, "HALF_UP", null,
|
|
||||||
BigDecimal.ZERO.setScale(MONEY_SCALE), request.selectedUnitPrice() == null ? null
|
|
||||||
: manualChargeableQuantity(policy.chargeMode(), weight, volume).multiply(request.selectedUnitPrice())
|
|
||||||
.setScale(MONEY_SCALE, RoundingMode.HALF_UP), "CNY", false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FeePreviewResponse manualPreview(BigDecimal weight, BigDecimal volume, PricingPolicy policy,
|
||||||
|
BigDecimal selectedUnitPrice) {
|
||||||
|
BigDecimal quantity = manualChargeableQuantity(policy.chargeMode(), weight, volume);
|
||||||
|
BigDecimal amount = selectedUnitPrice == null ? null
|
||||||
|
: quantity.multiply(selectedUnitPrice).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal factor = new BigDecimal("KG".equals(policy.chargeMode()) ? "167" : "363");
|
||||||
|
return new FeePreviewResponse(weight, volume, quantity, policy.chargeMode(), factor, 6, "HALF_UP",
|
||||||
|
null, BigDecimal.ZERO.setScale(MONEY_SCALE), amount, "CNY", false);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
|
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
|
||||||
LocalDateTime requestedAt, String idempotencyKey) {
|
LocalDateTime requestedAt, String idempotencyKey) {
|
||||||
@@ -133,6 +141,9 @@ public class FbaPricingService {
|
|||||||
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
|
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
|
||||||
|
|
||||||
InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId);
|
InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId);
|
||||||
|
if ("PRIVATE".equals(inbound.getDestinationType())) {
|
||||||
|
throw conflict("私人地址请使用人工头程报价");
|
||||||
|
}
|
||||||
InboundFeeEntity published = null;
|
InboundFeeEntity published = null;
|
||||||
if ("SENT".equals(inbound.getFeeStatus())) {
|
if ("SENT".equals(inbound.getFeeStatus())) {
|
||||||
published = repository.findLatestFeeForUpdate(tenantId, inboundId);
|
published = repository.findLatestFeeForUpdate(tenantId, inboundId);
|
||||||
@@ -206,7 +217,9 @@ public class FbaPricingService {
|
|||||||
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
|
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
|
||||||
|
|
||||||
InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId);
|
InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId);
|
||||||
if (!"FBA".equals(inbound.getDestinationType())) throw conflict("仅FBA头程运费支持未命中人工定价");
|
if (!"FBA".equals(inbound.getDestinationType()) && !"PRIVATE".equals(inbound.getDestinationType())) {
|
||||||
|
throw conflict("仅FBA未命中报价或私人地址支持人工头程定价");
|
||||||
|
}
|
||||||
var measurement = measurementRepository.findLatestMeasurement(tenantId, inboundId);
|
var measurement = measurementRepository.findLatestMeasurement(tenantId, inboundId);
|
||||||
if (measurement == null || measurementRepository.listItems(tenantId, measurement.getId()).isEmpty()) {
|
if (measurement == null || measurementRepository.listItems(tenantId, measurement.getId()).isEmpty()) {
|
||||||
throw conflict("实际测量明细不存在,不能生成人工头程费用");
|
throw conflict("实际测量明细不存在,不能生成人工头程费用");
|
||||||
@@ -218,14 +231,17 @@ public class FbaPricingService {
|
|||||||
}
|
}
|
||||||
LocalDateTime now = LocalDateTime.now(clock);
|
LocalDateTime now = LocalDateTime.now(clock);
|
||||||
PricingPolicy policy = pricingPolicy(inbound, now);
|
PricingPolicy policy = pricingPolicy(inbound, now);
|
||||||
try {
|
// 私人地址使用人工报价;FBA仍须先证明价格表未命中,避免绕开有效报价。
|
||||||
priceBookEstimator.estimate(tenantId, new EstimateRequest(
|
if ("FBA".equals(inbound.getDestinationType())) {
|
||||||
inbound.getWarehouseId(), inbound.getChannelCode(), inbound.getDestinationCode(),
|
try {
|
||||||
destinationType(inbound), inbound.getDestinationWarehouseId(), inbound.getDestinationName(), null,
|
priceBookEstimator.estimate(tenantId, new EstimateRequest(
|
||||||
pricingTaxMode(inbound), policy.chargeMode(), request.selectedWeightKg(), request.selectedVolumeCbm()));
|
inbound.getWarehouseId(), inbound.getChannelCode(), inbound.getDestinationCode(),
|
||||||
throw conflict("当前条件已有匹配价格,请使用正常试算并调整单价");
|
destinationType(inbound), inbound.getDestinationWarehouseId(), inbound.getDestinationName(), null,
|
||||||
} catch (BizException exception) {
|
pricingTaxMode(inbound), policy.chargeMode(), request.selectedWeightKg(), request.selectedVolumeCbm()));
|
||||||
if (exception.getResultCode() != ResultCode.NOT_FOUND) throw exception;
|
throw conflict("当前条件已有匹配价格,请使用正常试算并调整单价");
|
||||||
|
} catch (BizException exception) {
|
||||||
|
if (exception.getResultCode() != ResultCode.NOT_FOUND) throw exception;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BigDecimal weight = request.selectedWeightKg().setScale(3, RoundingMode.HALF_UP);
|
BigDecimal weight = request.selectedWeightKg().setScale(3, RoundingMode.HALF_UP);
|
||||||
BigDecimal volume = request.selectedVolumeCbm().setScale(4, RoundingMode.HALF_UP);
|
BigDecimal volume = request.selectedVolumeCbm().setScale(4, RoundingMode.HALF_UP);
|
||||||
@@ -264,7 +280,8 @@ public class FbaPricingService {
|
|||||||
throw conflict("费用已被其他操作员修改,请刷新后重试");
|
throw conflict("费用已被其他操作员修改,请刷新后重试");
|
||||||
}
|
}
|
||||||
repository.insertFee(fee);
|
repository.insertFee(fee);
|
||||||
InboundFeeItemEntity item = item(tenantId, fee, 1, "FBA_BASE", "FBA基础运费", subtotal, now);
|
String feeName = "PRIVATE".equals(inbound.getDestinationType()) ? "头程基础运费" : "FBA基础运费";
|
||||||
|
InboundFeeItemEntity item = item(tenantId, fee, 1, "FBA_BASE", feeName, subtotal, now);
|
||||||
item.setQuantity(quantity); item.setUnit(unit); item.setUnitPrice(request.unitPrice());
|
item.setQuantity(quantity); item.setUnit(unit); item.setUnitPrice(request.unitPrice());
|
||||||
item.setAutomaticFlag(false); item.setRemark(reason);
|
item.setAutomaticFlag(false); item.setRemark(reason);
|
||||||
repository.insertItem(item);
|
repository.insertItem(item);
|
||||||
@@ -784,8 +801,9 @@ public class FbaPricingService {
|
|||||||
private InboundPricingEntity requireApprovedInbound(long tenantId, long inboundId) {
|
private InboundPricingEntity requireApprovedInbound(long tenantId, long inboundId) {
|
||||||
InboundPricingEntity inbound = repository.findInbound(tenantId, inboundId);
|
InboundPricingEntity inbound = repository.findInbound(tenantId, inboundId);
|
||||||
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||||
if (!"FBA".equals(inbound.getDestinationType()) && !"OVERSEAS".equals(inbound.getDestinationType())) {
|
if (!"FBA".equals(inbound.getDestinationType()) && !"OVERSEAS".equals(inbound.getDestinationType())
|
||||||
throw conflict("当前目的地类型不支持自动计费");
|
&& !"PRIVATE".equals(inbound.getDestinationType())) {
|
||||||
|
throw conflict("当前目的地类型不支持头程计费");
|
||||||
}
|
}
|
||||||
if (inbound.getWarehouseId() == null || !StringUtils.hasText(inbound.getChannelCode())) {
|
if (inbound.getWarehouseId() == null || !StringUtils.hasText(inbound.getChannelCode())) {
|
||||||
throw conflict("入库单缺少起运仓或渠道匹配维度");
|
throw conflict("入库单缺少起运仓或渠道匹配维度");
|
||||||
|
|||||||
@@ -163,8 +163,8 @@ paths:
|
|||||||
responses: {'200': {description: 成功或业务失败, content: {application/json: {schema: {$ref: '#/components/schemas/FeeSnapshotApiResponse'}}}}, '403': {description: 无权限}}
|
responses: {'200': {description: 成功或业务失败, content: {application/json: {schema: {$ref: '#/components/schemas/FeeSnapshotApiResponse'}}}}, '403': {description: 无权限}}
|
||||||
/api/platform/v1/inbounds/{inboundId}/fees/manual-quote:
|
/api/platform/v1/inbounds/{inboundId}/fees/manual-quote:
|
||||||
post:
|
post:
|
||||||
summary: FBA价格未命中时人工确定头程单价
|
summary: 人工确定FBA未命中报价或私人地址头程单价
|
||||||
description: 仅FBA且价格查询返回未命中时可用;按客户计费方式与选定测量重量、体积计算计费量,附加费为零。生成正式头程费用快照,后续复用确认与发送;需要 firstleg:inbound:confirm-send-fee 权限。
|
description: FBA须价格查询返回未命中;私人地址PRIVATE按人工单价报价,预览返回计费量且priceMatched=false。按客户计费方式与选定测量重量、体积计算计费量,附加费为零。生成CALCULATED正式头程费用快照并保留WAIT_SPLIT,后续复用版本、确认、发送与幂等流程,预装确认继续校验初始费用已完成;需要 firstleg:inbound:confirm-send-fee 权限。
|
||||||
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ManualQuoteRequest'}}}}
|
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ManualQuoteRequest'}}}}
|
||||||
responses: {'200': {description: 成功或业务失败, content: {application/json: {schema: {$ref: '#/components/schemas/FeeSnapshotApiResponse'}}}}, '403': {description: 无权限}}
|
responses: {'200': {description: 成功或业务失败, content: {application/json: {schema: {$ref: '#/components/schemas/FeeSnapshotApiResponse'}}}}, '403': {description: 无权限}}
|
||||||
|
|||||||
@@ -483,6 +483,83 @@ class FbaPricingServiceTest {
|
|||||||
assertThat(feeItem.getValue().getRemark()).isEqualTo("FBA价格未维护");
|
assertThat(feeItem.getValue().getRemark()).isEqualTo("FBA价格未维护");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void previewsPrivateAddressManualQuantityWithoutLookingUpWarehouseRates() {
|
||||||
|
InboundPricingEntity inbound = inbound("62.500", "0.3840");
|
||||||
|
inbound.setDestinationType("PRIVATE");
|
||||||
|
inbound.setDestinationCode(null);
|
||||||
|
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||||
|
|
||||||
|
var result = service.preview(TENANT, INBOUND, new FeePreviewRequest(NOW,
|
||||||
|
new BigDecimal("62.500"), new BigDecimal("0.3840"), null, null, new BigDecimal("12.2")));
|
||||||
|
|
||||||
|
assertThat(result.priceMatched()).isFalse();
|
||||||
|
assertThat(result.chargeableQuantity()).isEqualByComparingTo("64.128000");
|
||||||
|
assertThat(result.totalAmount()).isEqualByComparingTo("782.36");
|
||||||
|
org.mockito.Mockito.verifyNoInteractions(priceBookEstimator, feeDetails, financeOutbox);
|
||||||
|
verify(repository, never()).insertFee(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsPrivateAddressManualQuoteAndPreservesWaitSplit() {
|
||||||
|
readyRepository();
|
||||||
|
InboundPricingEntity inbound = inbound("62.500", "0.3840");
|
||||||
|
inbound.setDestinationType("PRIVATE");
|
||||||
|
inbound.setDestinationCode(null);
|
||||||
|
inbound.setProcessStatus("WAIT_SPLIT");
|
||||||
|
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||||
|
measurement.setId(301L);
|
||||||
|
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||||
|
when(measurementRepository.findLatestMeasurement(TENANT, INBOUND)).thenReturn(measurement);
|
||||||
|
when(measurementRepository.listItems(TENANT, 301L)).thenReturn(List.of(new InboundMeasurementItemEntity()));
|
||||||
|
|
||||||
|
var request = new ManualQuoteRequest(new BigDecimal("62.500"), new BigDecimal("0.3840"),
|
||||||
|
new BigDecimal("12.2"), "私人地址人工报价");
|
||||||
|
var result = service.manualQuote(TENANT, OPERATOR, INBOUND, request, "private-manual");
|
||||||
|
|
||||||
|
assertThat(result.rateCode()).isEqualTo("MANUAL_QUOTE");
|
||||||
|
assertThat(result.feeStatus()).isEqualTo("CALCULATED");
|
||||||
|
assertThat(result.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||||
|
assertThat(result.totalAmount()).isEqualByComparingTo("782.36");
|
||||||
|
ArgumentCaptor<InboundFeeEntity> fee = ArgumentCaptor.forClass(InboundFeeEntity.class);
|
||||||
|
verify(repository).insertFee(fee.capture());
|
||||||
|
assertThat(fee.getValue().getDestinationType()).isEqualTo("PRIVATE");
|
||||||
|
assertThat(fee.getValue().getCustomerId()).isEqualTo(TENANT);
|
||||||
|
assertThat(fee.getValue().getAdditionalAmount()).isEqualByComparingTo("0");
|
||||||
|
ArgumentCaptor<InboundFeeItemEntity> item = ArgumentCaptor.forClass(InboundFeeItemEntity.class);
|
||||||
|
verify(repository).insertItem(item.capture());
|
||||||
|
assertThat(item.getValue().getFeeName()).isEqualTo("头程基础运费");
|
||||||
|
assertThat(item.getValue().getAutomaticFlag()).isFalse();
|
||||||
|
org.mockito.Mockito.verifyNoInteractions(priceBookEstimator, financeOutbox);
|
||||||
|
|
||||||
|
ArgumentCaptor<InboundPricingIdempotencyEntity> command =
|
||||||
|
ArgumentCaptor.forClass(InboundPricingIdempotencyEntity.class);
|
||||||
|
verify(repository).insertIdempotency(command.capture());
|
||||||
|
command.getValue().setStatus("COMPLETED");
|
||||||
|
command.getValue().setResultResourceId(result.feeId());
|
||||||
|
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_MANUAL_QUOTE", "private-manual"))
|
||||||
|
.thenReturn(command.getValue());
|
||||||
|
when(repository.findFee(TENANT, result.feeId())).thenReturn(fee.getValue());
|
||||||
|
when(repository.findBaseItem(TENANT, result.feeId())).thenReturn(item.getValue());
|
||||||
|
var replay = service.manualQuote(TENANT, OPERATOR, INBOUND, request, "private-manual");
|
||||||
|
assertThat(replay.feeId()).isEqualTo(result.feeId());
|
||||||
|
verify(repository, times(1)).insertFee(any());
|
||||||
|
verify(repository, times(1)).insertIdempotency(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsAutomaticPricingForPrivateAddress() {
|
||||||
|
readyRepository();
|
||||||
|
InboundPricingEntity inbound = inbound("62.500", "0.3840");
|
||||||
|
inbound.setDestinationType("PRIVATE");
|
||||||
|
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "private-auto"))
|
||||||
|
.isInstanceOf(BizException.class).hasMessageContaining("私人地址请使用人工头程报价");
|
||||||
|
verify(repository, never()).insertFee(any());
|
||||||
|
org.mockito.Mockito.verifyNoInteractions(priceBookEstimator);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("已有匹配价格时拒绝绕过价格表创建人工报价")
|
@DisplayName("已有匹配价格时拒绝绕过价格表创建人工报价")
|
||||||
void rejectsManualQuoteWhenRateExists() {
|
void rejectsManualQuoteWhenRateExists() {
|
||||||
|
|||||||
Reference in New Issue
Block a user