修正外箱快照清空并提供当前计费量只读预览
This commit is contained in:
@@ -19,6 +19,7 @@ class FbaPricingContractTest {
|
||||
@Test
|
||||
@DisplayName("确认和发送费用兼容新的组合权限与原有权限")
|
||||
void allowsCombinedConfirmSendPermissionAndLegacyPermissions() {
|
||||
assertPricingAuthority("preview", "hasAnyAuthority('firstleg:inbound:confirm-send-fee', 'firstleg:fba-pricing:calculate')");
|
||||
assertPricingAuthority("confirm", "hasAnyAuthority('firstleg:inbound:confirm-send-fee',"
|
||||
+ " 'firstleg:fba-pricing:confirm')");
|
||||
assertPricingAuthority("send", "hasAnyAuthority('firstleg:inbound:confirm-send-fee',"
|
||||
@@ -58,6 +59,8 @@ class FbaPricingContractTest {
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(openapi)
|
||||
.contains("/api/platform/v1/fba-pricing/customer-rules/versions:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/fees/preview:")
|
||||
.contains("不生成费用版本")
|
||||
.contains("/api/platform/v1/fba-pricing/customer-rules/{id}/default:")
|
||||
.contains("/api/platform/v1/fba-pricing/carrier-rates/versions:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/fees/calculate:")
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundMeasurementItemMapper;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** 使用真实 MyBatis 更新语句验证清空外箱修正,不以 Mock 掩盖 NULL 被忽略的问题。 */
|
||||
class InboundMeasurementItemPersistenceTest {
|
||||
private SqlSession session;
|
||||
private JdbcTemplate sql;
|
||||
private InboundMeasurementRepository repository;
|
||||
|
||||
/** 每项用例使用隔离内存表,仅保留本轮更新涉及的列。 */
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
var ds = new JdbcDataSource();
|
||||
ds.setURL("jdbc:h2:mem:measurement-clear-" + UUID.randomUUID() + ";MODE=MySQL");
|
||||
var config = new MybatisConfiguration();
|
||||
config.setEnvironment(new Environment("test", new JdbcTransactionFactory(), ds));
|
||||
config.addMapper(InboundMeasurementItemMapper.class);
|
||||
session = new MybatisSqlSessionFactoryBuilder().build(config).openSession(true);
|
||||
session.getConnection();
|
||||
sql = new JdbcTemplate(ds);
|
||||
sql.execute("CREATE TABLE fl_inbound_measurement_item(id BIGINT PRIMARY KEY, tenant_id BIGINT, "
|
||||
+ "manual_outer_length_cm DECIMAL(12,3), manual_outer_width_cm DECIMAL(12,3), "
|
||||
+ "manual_outer_height_cm DECIMAL(12,3), charge_rule_id BIGINT)");
|
||||
sql.update("INSERT INTO fl_inbound_measurement_item VALUES (88, 10001, 63, 53, 53, 77)");
|
||||
repository = new InboundMeasurementRepository();
|
||||
ReflectionTestUtils.setField(repository, "items", session.getMapper(InboundMeasurementItemMapper.class));
|
||||
}
|
||||
|
||||
/** 关闭隔离数据库的最后一个会话,释放本项测试资源。 */
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (session != null) session.close();
|
||||
}
|
||||
|
||||
/** 单边清空与全部清空都须真正持久化,并可解除已取消的系数规则关联。 */
|
||||
@ParameterizedTest
|
||||
@CsvSource({"false,true,true", "true,false,true", "true,true,false", "false,false,false"})
|
||||
void persistsClearedWarehouseDimensions(boolean keepLength, boolean keepWidth, boolean keepHeight) {
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setId(88L);
|
||||
item.setTenantId(10001L);
|
||||
item.setManualOuterLengthCm(keepLength ? new BigDecimal("63") : null);
|
||||
item.setManualOuterWidthCm(keepWidth ? new BigDecimal("53") : null);
|
||||
item.setManualOuterHeightCm(keepHeight ? new BigDecimal("53") : null);
|
||||
item.setChargeRuleId(null);
|
||||
|
||||
assertThat(repository.updateItem(item)).isTrue();
|
||||
var stored = sql.queryForMap("SELECT * FROM fl_inbound_measurement_item WHERE id=88");
|
||||
assertThat(stored.get("MANUAL_OUTER_LENGTH_CM")).isEqualTo(keepLength ? new BigDecimal("63.000") : null);
|
||||
assertThat(stored.get("MANUAL_OUTER_WIDTH_CM")).isEqualTo(keepWidth ? new BigDecimal("53.000") : null);
|
||||
assertThat(stored.get("MANUAL_OUTER_HEIGHT_CM")).isEqualTo(keepHeight ? new BigDecimal("53.000") : null);
|
||||
assertThat(stored.get("CHARGE_RULE_ID")).isNull();
|
||||
|
||||
item.setTenantId(20002L);
|
||||
assertThat(repository.updateItem(item)).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.domain.PriceModel;
|
||||
import com.baiyue.firstleg.domain.PricingTaxMode;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.ManualQuoteRequest;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeePreviewRequest;
|
||||
import com.baiyue.firstleg.entity.CarrierFbaRateEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeItemEntity;
|
||||
@@ -70,6 +71,87 @@ class FbaPricingServiceTest {
|
||||
@MockBean private ContainerTierAmountAllocator containerTierAllocator;
|
||||
@Resource private FbaPricingService service;
|
||||
|
||||
@Test
|
||||
/** 预览只按当前选择值估价,不复用旧费用,也不产生任何计费写入。 */
|
||||
void previewsSelectedOuterVolumeWithoutWritingFeeState() {
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound("570.500", "3.6894"));
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(901L);
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setOuterLengthCm(new BigDecimal("63"));
|
||||
item.setOuterWidthCm(new BigDecimal("53"));
|
||||
item.setOuterHeightCm(new BigDecimal("53"));
|
||||
item.setCalculationRuleVersion("WAREHOUSE_OUTER_OVERRIDE_V1");
|
||||
when(measurementRepository.findLatestMeasurement(TENANT, INBOUND)).thenReturn(measurement);
|
||||
when(measurementRepository.listItems(TENANT, 901L)).thenReturn(List.of(item));
|
||||
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
|
||||
estimate("KG", "650.298000", "13.6", "8844.05", "0"));
|
||||
|
||||
var preview = service.preview(TENANT, INBOUND, new FeePreviewRequest(NOW,
|
||||
new BigDecimal("570.500"), new BigDecimal("3.8940"), "ACTUAL", "OUTER", null));
|
||||
|
||||
assertThat(preview.actualVolumeCbm()).isEqualByComparingTo("3.8940");
|
||||
assertThat(preview.chargeableQuantity()).isEqualByComparingTo("650.298000");
|
||||
assertThat(preview.totalAmount()).isEqualByComparingTo("8844.05");
|
||||
assertThat(preview.priceMatched()).isTrue();
|
||||
verify(priceBookEstimator).estimate(eq(TENANT), argThat(request ->
|
||||
request.actualWeightKg().compareTo(new BigDecimal("570.5")) == 0
|
||||
&& request.actualVolumeCbm().compareTo(new BigDecimal("3.894")) == 0));
|
||||
verify(repository, never()).insertFee(any());
|
||||
verify(repository, never()).insertIdempotency(any());
|
||||
verify(repository, never()).nextFeeVersion(anyLong(), anyLong());
|
||||
verify(repository, never()).markInboundCalculated(anyLong(), anyLong(), any(), any(), any(), any(), any());
|
||||
org.mockito.Mockito.verifyNoInteractions(financeOutbox, feeDetails);
|
||||
}
|
||||
|
||||
@Test
|
||||
/** FBA 未命中价格时仍返回服务端计费量,供人工单价预览使用。 */
|
||||
void previewsManualQuantityWhenPriceIsMissing() {
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound("570.500", "3.6894"));
|
||||
when(priceBookEstimator.estimate(anyLong(), any()))
|
||||
.thenThrow(new BizException(ResultCode.NOT_FOUND, "价格未命中"));
|
||||
|
||||
var preview = service.preview(TENANT, INBOUND, new FeePreviewRequest(NOW,
|
||||
new BigDecimal("570.5"), new BigDecimal("3.894"), null, null, null));
|
||||
|
||||
assertThat(preview.priceMatched()).isFalse();
|
||||
assertThat(preview.unitPrice()).isNull();
|
||||
assertThat(preview.chargeableQuantity()).isEqualByComparingTo("650.298000");
|
||||
assertThat(preview.additionalAmount()).isZero();
|
||||
assertThat(preview.totalAmount()).isNull();
|
||||
verify(repository, never()).insertFee(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 外箱快照缺失时拒绝预览,不把当前规则的前端推导结果视作已保存事实。 */
|
||||
void rejectsOuterPreviewWithoutSavedMeasurementSnapshot() {
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound("100", "1"));
|
||||
assertThatThrownBy(() -> service.preview(TENANT, INBOUND,
|
||||
new FeePreviewRequest(NOW, new BigDecimal("100"), BigDecimal.ONE, "ACTUAL", "OUTER", null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("测量数据不存在");
|
||||
org.mockito.Mockito.verifyNoInteractions(priceBookEstimator, financeOutbox, feeDetails);
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 预览不能读取其他客户的入库单。 */
|
||||
void rejectsCrossCustomerPreview() {
|
||||
assertThatThrownBy(() -> service.preview(10002L, INBOUND, new FeePreviewRequest(NOW, null, null, null, null, null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("入库单不存在");
|
||||
org.mockito.Mockito.verifyNoInteractions(priceBookEstimator, financeOutbox, feeDetails);
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 人工单价的金额也由服务端精确取整,不在页面重复实现舍入。 */
|
||||
void previewsManualUnitPriceUsingExactMoneyRounding() {
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound("1", "0.001"));
|
||||
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(estimate("KG", "1", "2", "2", "0"));
|
||||
var preview = service.preview(TENANT, INBOUND, new FeePreviewRequest(NOW,
|
||||
BigDecimal.ONE, new BigDecimal("0.001"), null, null, new BigDecimal("1.005")));
|
||||
assertThat(preview.unitPrice()).isEqualByComparingTo("2");
|
||||
assertThat(preview.totalAmount()).isEqualByComparingTo("1.01");
|
||||
verify(repository, never()).insertFee(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("客户B不能读取客户A的费用快照")
|
||||
void rejectsCrossCustomerFeeLookup() {
|
||||
|
||||
@@ -174,6 +174,41 @@ class MeasurementBatchServiceTest {
|
||||
verify(repository, never()).replaceItems(anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
/** 全部清空人工修正后,计算与批次汇总须恢复设备实测尺寸。 */
|
||||
@Test
|
||||
void clearsWarehouseOverridesAndRecalculatesFromDeviceDimensions() {
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(100L);
|
||||
measurement.setTenantId(1L);
|
||||
measurement.setInboundId(10L);
|
||||
measurement.setVersion(3);
|
||||
measurement.setCreatedBy(9L);
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setId(200L);
|
||||
item.setTenantId(1L);
|
||||
item.setItemNo("B1");
|
||||
item.setPackageCount(22);
|
||||
item.setLengthCm(new BigDecimal("62"));
|
||||
item.setWidthCm(new BigDecimal("52"));
|
||||
item.setHeightCm(new BigDecimal("52"));
|
||||
item.setManualOuterLengthCm(new BigDecimal("63"));
|
||||
item.setManualOuterWidthCm(new BigDecimal("53"));
|
||||
item.setManualOuterHeightCm(new BigDecimal("53"));
|
||||
item.setCoefficientMode("ACTUAL");
|
||||
when(repository.updateItem(item)).thenReturn(true);
|
||||
|
||||
service.recalculateOuterDimensions(1L, measurement, List.of(item), 200L, null, null, null);
|
||||
|
||||
assertThat(item.getManualOuterLengthCm()).isNull();
|
||||
assertThat(item.getManualOuterWidthCm()).isNull();
|
||||
assertThat(item.getManualOuterHeightCm()).isNull();
|
||||
assertThat(item.getOuterLengthCm()).isEqualByComparingTo("62.000");
|
||||
assertThat(item.getOuterWidthCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(item.getOuterHeightCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(measurement.getTotalVolumeCbm()).isEqualByComparingTo("3.6894");
|
||||
verify(repository).updateItem(item);
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 客户未启用算法时,预置系数不能放大仓库修正的实际外箱体积。 */
|
||||
void savedCoefficientDoesNotOverrideActualDefaultMode() {
|
||||
|
||||
Reference in New Issue
Block a user