This commit is contained in:
2026-08-07 20:07:30 +08:00
parent c698290106
commit c4b0972dc9
31 changed files with 1089 additions and 204 deletions
@@ -39,6 +39,8 @@ class PlatformInboundMeasurementControllerContractTest {
+ " and (!#request.hasAdjustment() or hasAuthority('platform:firstleg:inbound:weight:edit'))",
AuthenticatedUser.class, long.class, long.class, long.class,
InboundMeasurementRequests.ConfirmWeightResultRequest.class);
assertAuthority("deleteWeightResult", "hasAuthority('platform:firstleg:inbound:weight:edit')",
AuthenticatedUser.class, long.class, int.class, long.class, long.class);
}
private void assertAuthority(String methodName, String expected, Class<?>... parameterTypes)
@@ -1,65 +1,142 @@
package com.baiyue.firstleg.repository;
import org.junit.jupiter.api.Test;
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
import com.baiyue.firstleg.mapper.CustomerDimensionConfigMapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class CustomerDimensionConfigSqlContractTest {
@Test
void moduleMigrationDefinesUniqueCustomerConfigAndDefault() throws IOException {
String sql = new ClassPathResource("db/customer-dimension-config.sql")
.getContentAsString(StandardCharsets.UTF_8);
void allFourDatabaseEntrypointsDefineTheSameFourRowStructure() throws Exception {
List<String> sqlEntrypoints = List.of(
resource("db/customer-dimension-config.sql"),
resource("db/firstleg.sql"),
resource("db/phase2-inbound-schema.sql"),
Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
StandardCharsets.UTF_8)
);
assertThat(sql)
.contains("CREATE TABLE IF NOT EXISTS fl_customer_dimension_config")
.contains("edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000")
.contains("initialization_key VARCHAR(128) NOT NULL")
.contains("UNIQUE KEY uk_customer_dimension_config_tenant (tenant_id)");
for (String sql : sqlEntrypoints) {
assertThat(sql)
.contains("fl_customer_dimension_config")
.contains("box_type VARCHAR(24) NOT NULL")
.contains("inner_length_cm DECIMAL(9,3) NOT NULL")
.contains("inner_width_cm DECIMAL(9,3) NOT NULL")
.contains("inner_height_cm DECIMAL(9,3) NOT NULL")
.contains("edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000")
.contains("initialization_key VARCHAR(128) NOT NULL")
.contains("created_by BIGINT UNSIGNED NOT NULL")
.contains("updated_by BIGINT UNSIGNED NOT NULL")
.contains("version INT NOT NULL DEFAULT 0")
.contains("UNIQUE KEY uk_customer_dimension_config_tenant_box (tenant_id, box_type)")
.contains("CHECK (box_type IN ('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE'))")
.doesNotContain("UNIQUE KEY uk_customer_dimension_config_tenant (tenant_id)");
}
}
@Test
void unifiedMigrationIncludesConfigAndFinalAssertion() throws IOException {
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
StandardCharsets.UTF_8);
void fixedDefaultsAndBatchInitializationCoverExactlyFourBoxTypes() throws Exception {
assertThat(CustomerDimensionBoxType.values()).extracting(CustomerDimensionBoxType::name)
.containsExactly("SMALL", "MEDIUM", "LARGE", "EXTRA_LARGE");
assertThat(List.of(CustomerDimensionBoxType.values()))
.extracting(CustomerDimensionBoxType::defaultLengthCm)
.containsExactly(new java.math.BigDecimal("30.000"), new java.math.BigDecimal("40.000"),
new java.math.BigDecimal("50.000"), new java.math.BigDecimal("60.000"));
Insert annotation = CustomerDimensionConfigMapper.class
.getDeclaredMethod("insertDefaults", List.class, long.class)
.getAnnotation(Insert.class);
String sql = String.join(" ", annotation.value());
assertThat(sql)
.contains("CREATE TABLE IF NOT EXISTS baiyue_firstleg.fl_customer_dimension_config")
.contains("客户尺寸计算配置表未创建")
.contains("uk_customer_dimension_config_tenant")
.contains("CREATE PROCEDURE unified_guard_dimension_config_permissions()")
.contains("id = 5102111 AND COALESCE(permission_code, '') <> 'firstleg:dimension-config:view'")
.contains("permission_code = 'firstleg:dimension-config:view' AND id <> 5102111")
.contains("id = 5102112 AND COALESCE(permission_code, '') <> 'firstleg:dimension-config:edit'")
.contains("permission_code = 'firstleg:dimension-config:edit' AND id <> 5102112")
.contains("(5102111, 0, 'PLATFORM', 'BUTTON', 5100010")
.contains("(5102112, 0, 'PLATFORM', 'BUTTON', 5100010")
.contains("r.role_code = 'PLATFORM_ADMIN'")
.contains("r.role_code = 'ORDER_OPERATOR'")
.contains("客户尺寸计算配置权限父节点企业客户不存在")
.contains("客户尺寸计算配置权限码、固定ID或父节点不正确")
.contains("平台管理员未获得客户尺寸计算配置读写权限")
.contains("订单运营角色的客户尺寸计算配置权限不是只读");
.contains("INSERT IGNORE INTO fl_customer_dimension_config")
.contains("box_type, inner_length_cm, inner_width_cm, inner_height_cm")
.contains("<foreach collection=\"rows\"");
}
@Test
void compensationDeleteRequiresExactKeyDefaultValueAndUnmodifiedVersion() throws Exception {
Delete annotation = com.baiyue.firstleg.mapper.CustomerDimensionConfigMapper.class
.getDeclaredMethod("deleteUnchangedDefault", long.class, long.class,
String.class, java.math.BigDecimal.class)
void compensationLocksThenDeletesOnlyExactDefaultRows() throws Exception {
Select select = CustomerDimensionConfigMapper.class
.getDeclaredMethod("selectDefaultsForCompensation", long.class, String.class)
.getAnnotation(Select.class);
assertThat(String.join(" ", select.value()))
.contains("tenant_id = #{tenantId}")
.contains("initialization_key = #{initializationKey}")
.contains("FOR UPDATE");
Delete annotation = CustomerDimensionConfigMapper.class
.getDeclaredMethod("deleteUnchangedDefaults", long.class, String.class)
.getAnnotation(Delete.class);
String sql = String.join(" ", annotation.value());
assertThat(sql)
.contains("tenant_id = #{tenantId}")
.contains("initialization_key = #{initializationKey}")
.contains("edge_addition_cm = #{edgeAdditionCm}")
.contains("version = 0");
.contains("version = 0")
.contains("box_type IN ('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE')")
.contains("inner_length_cm = CASE box_type")
.contains("edge_addition_cm = 2.000")
.contains("remark = CASE box_type");
}
@Test
void unifiedMigrationKeepsPermissionGuardsAndAssertsFinalFourRowSchema() throws Exception {
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
StandardCharsets.UTF_8);
assertThat(sql)
.contains("CREATE PROCEDURE unified_guard_dimension_config_permissions()")
.contains("(5102111, 0, 'PLATFORM', 'BUTTON', 5100010")
.contains("r.role_code = 'PLATFORM_ADMIN'")
.contains("r.role_code = 'ORDER_OPERATOR'")
.contains("index_name = 'uk_customer_dimension_config_tenant_box'")
.contains("客户四箱规尺寸配置字段不完整")
.contains("客户尺寸计算配置权限码、固定ID或父节点不正确");
}
@Test
void unifiedMigrationUpgradesOnlyRecognizedLegacyTableAndBackfillsIdempotently() throws Exception {
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
StandardCharsets.UTF_8);
int upgrade = sql.indexOf("CREATE PROCEDURE unified_upgrade_legacy_customer_dimension_config()");
int create = sql.indexOf("CREATE TABLE IF NOT EXISTS baiyue_firstleg.fl_customer_dimension_config");
int backfill = sql.indexOf("INSERT INTO baiyue_firstleg.fl_customer_dimension_config (", create);
assertThat(upgrade).isGreaterThanOrEqualTo(0);
assertThat(create).isGreaterThan(upgrade);
assertThat(backfill).isGreaterThan(create);
assertThat(sql)
.contains("AND column_name = 'box_type') = 0")
.contains("AND column_name IN ('id', 'tenant_id', 'edge_addition_cm', 'remark', 'initialization_key'")
.contains("AND index_name = 'uk_customer_dimension_config_tenant'")
.contains("DROP TABLE baiyue_firstleg.fl_customer_dimension_config")
.contains("客户尺寸配置表不是可识别的旧单行结构")
.contains("SELECT id AS tenant_id, created_by")
.contains("FROM baiyue_iam.iam_tenant")
.contains("WHERE tenant_type = 'MERCHANT' AND deleted = 0 AND id > 0")
.contains("CONCAT('unified-four-box-default-v1:', tenant.tenant_id)")
.contains("ON DUPLICATE KEY UPDATE id = id")
.contains("现有企业客户未完整持有固定四箱规尺寸配置");
String repeatSafeClause = sql.substring(backfill,
sql.indexOf("DROP PROCEDURE IF EXISTS unified_assert_final_extensions", backfill));
assertThat(repeatSafeClause)
.doesNotContain("edge_addition_cm = VALUES")
.doesNotContain("inner_length_cm = VALUES")
.doesNotContain("remark = VALUES")
.doesNotContain("version = VALUES");
}
private static String resource(String path) throws Exception {
return new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8);
}
}
@@ -1,22 +1,30 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.GroupResponse;
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.InitializeResponse;
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.RuleUpdateRequest;
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.UpdateRequest;
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
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.anyLong;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -28,82 +36,167 @@ class CustomerDimensionConfigServiceTest {
private CustomerDimensionConfigService service;
@Test
void initializesTwoCentimeterDefaultExactlyOnce() {
CustomerDimensionConfigEntity persisted = config(91L, 8L, "2.000", 0, "registration-1");
when(repository.find(8L)).thenReturn(null, persisted);
when(repository.insertDefault(anyLong(), eq(8L), eq(new BigDecimal("2.000")),
eq("默认每侧加宽 2cm"), eq("registration-1"), eq(100L))).thenReturn(true);
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-1");
assertThat(result.createdByThisInitialization()).isTrue();
assertThat(result.configId()).isEqualTo(91L);
}
@Test
void retryAfterLostResponseKeepsCreationOwnershipForSameKey() {
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 0, "registration-1"));
void initializesFourDefaultsAtomicallyInFixedOrder() {
List<CustomerDimensionConfigEntity> persisted = defaultConfigs("registration-1");
when(repository.findAll(8L)).thenReturn(List.of(), persisted);
when(repository.insertDefaults(any(), eq(100L))).thenReturn(4);
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-1");
assertThat(result).isEqualTo(new InitializeResponse(91L, true));
verify(repository, never()).insertDefault(anyLong(), anyLong(),
eq(new BigDecimal("2.000")), eq("默认每侧加宽 2cm"),
eq("registration-1"), anyLong());
@SuppressWarnings("unchecked")
ArgumentCaptor<List<CustomerDimensionConfigEntity>> captor = ArgumentCaptor.forClass(List.class);
verify(repository).insertDefaults(captor.capture(), eq(100L));
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getBoxType)
.containsExactly(CustomerDimensionBoxType.values());
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getInnerLengthCm)
.containsExactly(new BigDecimal("30.000"), new BigDecimal("40.000"),
new BigDecimal("50.000"), new BigDecimal("60.000"));
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getInnerWidthCm)
.containsExactly(new BigDecimal("20.000"), new BigDecimal("30.000"),
new BigDecimal("40.000"), new BigDecimal("50.000"));
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getInnerHeightCm)
.containsExactly(new BigDecimal("20.000"), new BigDecimal("30.000"),
new BigDecimal("40.000"), new BigDecimal("50.000"));
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getEdgeAdditionCm)
.containsOnly(new BigDecimal("2.000"));
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getRemark)
.containsExactly("标准小箱", "标准中箱", "标准大箱", "特大箱");
}
@Test
void retryAfterLostResponseKeepsCreationOwnershipForSameKey() {
when(repository.findAll(8L)).thenReturn(defaultConfigs("registration-1"));
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-1");
assertThat(result).isEqualTo(new InitializeResponse(91L, true));
verify(repository, never()).insertDefaults(any(), eq(100L));
}
@Test
void differentInitializationKeyDoesNotTakeOwnershipOrOverwrite() {
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 0, "registration-1"));
when(repository.findAll(8L)).thenReturn(defaultConfigs("registration-1"));
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-2");
assertThat(result).isEqualTo(new InitializeResponse(91L, false));
verify(repository, never()).insertDefault(anyLong(), anyLong(),
eq(new BigDecimal("2.000")), eq("默认每侧加宽 2cm"),
eq("registration-2"), anyLong());
verify(repository, never()).insertDefaults(any(), eq(100L));
}
@Test
void rejectsStaleUpdateVersion() {
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 3, "registration-1"));
void returnsFixedOrderCalculatedOuterDimensionsAndThreeDecimalVolume() {
List<CustomerDimensionConfigEntity> shuffled = new ArrayList<>(defaultConfigs("registration-1"));
Collections.reverse(shuffled);
when(repository.findAll(8L)).thenReturn(shuffled);
assertThatThrownBy(() -> service.update(8L, 100L,
new UpdateRequest(new BigDecimal("2.500"), "新配置", 2)))
GroupResponse result = service.get(8L);
assertThat(result.rules()).extracting(rule -> rule.boxType())
.containsExactly(CustomerDimensionBoxType.values());
assertThat(result.rules().get(0).boxTypeName()).isEqualTo("小箱");
assertThat(result.rules().get(0).outerLengthCm()).isEqualByComparingTo("34.000");
assertThat(result.rules().get(0).outerWidthCm()).isEqualByComparingTo("24.000");
assertThat(result.rules().get(0).outerHeightCm()).isEqualByComparingTo("24.000");
assertThat(result.rules().get(0).volumeCbm()).isEqualByComparingTo("0.020");
}
@Test
void rejectsMissingOrDuplicateBoxTypes() {
List<RuleUpdateRequest> rules = new ArrayList<>(updateRequest(0).rules());
rules.set(3, rules.get(0));
assertThatThrownBy(() -> service.update(8L, 100L, new UpdateRequest(rules)))
.isInstanceOf(BizException.class)
.hasMessageContaining("重复");
verify(repository, never()).findAll(8L);
}
@Test
void rejectsAnyStaleVersionBeforeWritingRows() {
List<CustomerDimensionConfigEntity> persisted = defaultConfigs("registration-1");
persisted.forEach(value -> value.setVersion(3));
when(repository.findAll(8L)).thenReturn(persisted);
UpdateRequest request = updateRequest(3);
List<RuleUpdateRequest> rules = new ArrayList<>(request.rules());
RuleUpdateRequest stale = rules.get(2);
rules.set(2, new RuleUpdateRequest(stale.boxType(), stale.innerLengthCm(), stale.innerWidthCm(),
stale.innerHeightCm(), stale.edgeAdditionCm(), stale.remark(), 2));
assertThatThrownBy(() -> service.update(8L, 100L, new UpdateRequest(rules)))
.isInstanceOf(BizException.class)
.hasMessageContaining("刷新后重试");
verify(repository, never()).update(org.mockito.ArgumentMatchers.any());
verify(repository, never()).update(any());
}
@Test
void compensationUsesTenantKeyDefaultValueAndUnmodifiedVersion() {
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 0, "registration-1"));
void abortsWholeTransactionalUpdateWhenAnyOptimisticWriteFails() {
when(repository.findAll(8L)).thenReturn(defaultConfigs("registration-1"));
when(repository.update(any())).thenReturn(1, 0);
service.compensateDefault(8L, "registration-1");
verify(repository).deleteUnchangedDefault(
8L, 91L, "registration-1", new BigDecimal("2.000"));
assertThatThrownBy(() -> service.update(8L, 100L, updateRequest(0)))
.isInstanceOf(BizException.class)
.hasMessageContaining("刷新后重试");
verify(repository, times(2)).update(any());
}
@Test
void editedConfigIsNotCompensatedByDelayedRollback() {
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.500", 1, "registration-1"));
void compensationLocksAndDeletesExactlyFourUnchangedDefaults() {
when(repository.findDefaultsForCompensation(8L, "registration-1"))
.thenReturn(defaultConfigs("registration-1"));
when(repository.deleteUnchangedDefaults(8L, "registration-1")).thenReturn(4);
service.compensateDefault(8L, "registration-1");
verify(repository).deleteUnchangedDefault(
8L, 91L, "registration-1", new BigDecimal("2.000"));
verify(repository).deleteUnchangedDefaults(8L, "registration-1");
}
private static CustomerDimensionConfigEntity config(long id, long tenantId,
String edgeAddition, int version,
String initializationKey) {
CustomerDimensionConfigEntity value = new CustomerDimensionConfigEntity();
value.setId(id);
value.setTenantId(tenantId);
value.setEdgeAdditionCm(new BigDecimal(edgeAddition));
value.setVersion(version);
value.setInitializationKey(initializationKey);
return value;
@Test
void compensationDoesNotDeleteAnEditedGroup() {
List<CustomerDimensionConfigEntity> values = defaultConfigs("registration-1");
values.get(2).setVersion(1);
when(repository.findDefaultsForCompensation(8L, "registration-1")).thenReturn(values);
service.compensateDefault(8L, "registration-1");
verify(repository, never()).deleteUnchangedDefaults(8L, "registration-1");
}
@Test
void compensationRollsBackInsteadOfAcceptingPartialDelete() {
when(repository.findDefaultsForCompensation(8L, "registration-1"))
.thenReturn(defaultConfigs("registration-1"));
when(repository.deleteUnchangedDefaults(8L, "registration-1")).thenReturn(3);
assertThatThrownBy(() -> service.compensateDefault(8L, "registration-1"))
.isInstanceOf(BizException.class)
.hasMessageContaining("补偿删除失败");
}
private static UpdateRequest updateRequest(int version) {
return new UpdateRequest(List.of(CustomerDimensionBoxType.values()).stream()
.map(type -> new RuleUpdateRequest(type, type.defaultLengthCm(), type.defaultWidthCm(),
type.defaultHeightCm(), new BigDecimal("2.000"), type.defaultRemark(), version))
.toList());
}
private static List<CustomerDimensionConfigEntity> defaultConfigs(String initializationKey) {
List<CustomerDimensionConfigEntity> values = new ArrayList<>();
long id = 91L;
for (CustomerDimensionBoxType type : CustomerDimensionBoxType.values()) {
CustomerDimensionConfigEntity value = new CustomerDimensionConfigEntity();
value.setId(id++);
value.setTenantId(8L);
value.setBoxType(type);
value.setInnerLengthCm(type.defaultLengthCm());
value.setInnerWidthCm(type.defaultWidthCm());
value.setInnerHeightCm(type.defaultHeightCm());
value.setEdgeAdditionCm(new BigDecimal("2.000"));
value.setRemark(type.defaultRemark());
value.setVersion(0);
value.setInitializationKey(initializationKey);
values.add(value);
}
return values;
}
}
@@ -1,6 +1,8 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.client.MeasurementImageClient;
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
@@ -18,6 +20,7 @@ import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.TestPropertySource;
import java.math.BigDecimal;
import java.util.List;
@@ -27,17 +30,21 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@DisplayName("平台入库测量应用服务")
@SpringJUnitConfig(InboundMeasurementService.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class InboundMeasurementServiceTest {
@MockBean private InboundMeasurementRepository repository;
@MockBean private MeasurementBatchService batchService;
@MockBean private MeasurementImageClient imageClient;
@Resource private InboundMeasurementService service;
@Test
@@ -357,6 +364,58 @@ class InboundMeasurementServiceTest {
verify(repository, never()).updateInbound(any());
}
@Test
void deletingPendingWeightRemovesResultAndOwnedImage() {
InboundOrderEntity inbound = inbound("MEASURING");
InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "PENDING_CONFIRM", 1);
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
when(repository.deleteWeight(1L, 10L, 20L, 1)).thenReturn(true);
when(imageClient.delete("internal-token", 9L, 1L, 10L, 120L))
.thenReturn(ApiResponse.success(null));
service.deleteWeightResult(1L, 10L, 20L, 1, 9L);
verify(repository).deleteWeight(1L, 10L, 20L, 1);
verify(imageClient).delete("internal-token", 9L, 1L, 10L, 120L);
verify(repository, never()).updateInbound(any());
}
@Test
void deletingLastConfirmedWeightClearsInboundTotal() {
InboundOrderEntity inbound = inbound("MEASURING");
inbound.setActualGrossWeightKg(new BigDecimal("285.500"));
InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "CONFIRMED", 2);
weight.setFinalWeightKg(new BigDecimal("285.500"));
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
when(repository.deleteWeight(1L, 10L, 20L, 2)).thenReturn(true);
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
when(repository.updateInbound(inbound)).thenReturn(true);
when(imageClient.delete("internal-token", 9L, 1L, 10L, 120L))
.thenReturn(ApiResponse.success(null));
service.deleteWeightResult(1L, 10L, 20L, 2, 9L);
assertThat(inbound.getActualGrossWeightKg()).isNull();
verify(repository).updateInbound(inbound);
}
@Test
void deletingWeightRejectsStaleVersionBeforeCallingFileService() {
InboundOrderEntity inbound = inbound("MEASURING");
InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "PENDING_CONFIRM", 2);
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
assertThatThrownBy(() -> service.deleteWeightResult(1L, 10L, 20L, 1, 9L))
.isInstanceOf(BizException.class)
.hasMessageContaining("版本已变化");
verify(repository, never()).deleteWeight(anyLong(), anyLong(), anyLong(), anyInt());
verifyNoInteractions(imageClient);
}
@Test
void submitAdvancesOnlyToMeasurePendingAfterAllWeightsConfirmed() {
InboundOrderEntity inbound = inbound("MEASURING");