Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f9e027dfc |
@@ -4,6 +4,9 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.Digits;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/** 费用项目请求模型。 */
|
/** 费用项目请求模型。 */
|
||||||
public final class FeeItemDtos {
|
public final class FeeItemDtos {
|
||||||
@@ -21,26 +24,9 @@ public final class FeeItemDtos {
|
|||||||
@NotNull Boolean warehousePayable,
|
@NotNull Boolean warehousePayable,
|
||||||
@NotNull Boolean warehouseReceivable,
|
@NotNull Boolean warehouseReceivable,
|
||||||
@NotNull Boolean allocationFee,
|
@NotNull Boolean allocationFee,
|
||||||
@Size(max = 500) String description
|
@Size(max = 500) String description,
|
||||||
|
@DecimalMin("0.00") @Digits(integer = 10, fraction = 2) BigDecimal defaultPrice
|
||||||
) {
|
) {
|
||||||
public SaveRequest(String category, String tradeScope, String feeCode,
|
|
||||||
String feeTypeName, Boolean pickupPayable,
|
|
||||||
Boolean pickupReceivable, Boolean warehousePayable,
|
|
||||||
Boolean warehouseReceivable, Boolean allocationFee,
|
|
||||||
String description) {
|
|
||||||
this(category, tradeScope, feeCode, null, feeTypeName, pickupPayable,
|
|
||||||
pickupReceivable, warehousePayable, warehouseReceivable,
|
|
||||||
allocationFee, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Backward-compatible constructor for callers predating the stable fee code. */
|
|
||||||
public SaveRequest(String category, String tradeScope, String feeTypeName,
|
|
||||||
Boolean pickupPayable, Boolean pickupReceivable,
|
|
||||||
Boolean warehousePayable, Boolean warehouseReceivable,
|
|
||||||
Boolean allocationFee, String description) {
|
|
||||||
this(category, tradeScope, null, null, feeTypeName, pickupPayable, pickupReceivable,
|
|
||||||
warehousePayable, warehouseReceivable, allocationFee, description);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record StatusRequest(
|
public record StatusRequest(
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package com.baiyue.firstleg.entity;
|
package com.baiyue.firstleg.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baiyue.common.core.model.BaseEntity;
|
import com.baiyue.common.core.model.BaseEntity;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/** 费用项目主数据。 */
|
/** 费用项目主数据。 */
|
||||||
@Getter
|
@Getter
|
||||||
@@ -20,6 +23,9 @@ public class FeeItemEntity extends BaseEntity {
|
|||||||
private String feeCode;
|
private String feeCode;
|
||||||
private String supplierCategory;
|
private String supplierCategory;
|
||||||
private String feeTypeName;
|
private String feeTypeName;
|
||||||
|
/** 业务选费时带入的默认价格,可清空且不限制实际成交金额。 */
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
|
private BigDecimal defaultPrice;
|
||||||
private Boolean pickupPayable;
|
private Boolean pickupPayable;
|
||||||
private Boolean pickupReceivable;
|
private Boolean pickupReceivable;
|
||||||
private Boolean warehousePayable;
|
private Boolean warehousePayable;
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ public class FeeItemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保存项目属性及可清空的默认价格,实际费用仍以业务录入金额为准。 */
|
||||||
private static void copy(FeeItemEntity value, SaveRequest request) {
|
private static void copy(FeeItemEntity value, SaveRequest request) {
|
||||||
boolean payable = Boolean.TRUE.equals(request.pickupPayable())
|
boolean payable = Boolean.TRUE.equals(request.pickupPayable())
|
||||||
|| Boolean.TRUE.equals(request.warehousePayable());
|
|| Boolean.TRUE.equals(request.warehousePayable());
|
||||||
@@ -111,6 +112,7 @@ public class FeeItemService {
|
|||||||
value.setSupplierCategory(payable
|
value.setSupplierCategory(payable
|
||||||
? FeeItemSupplierCategory.normalize(request.supplierCategory()) : null);
|
? FeeItemSupplierCategory.normalize(request.supplierCategory()) : null);
|
||||||
value.setFeeTypeName(request.feeTypeName().trim());
|
value.setFeeTypeName(request.feeTypeName().trim());
|
||||||
|
value.setDefaultPrice(request.defaultPrice());
|
||||||
value.setPickupPayable(request.pickupPayable());
|
value.setPickupPayable(request.pickupPayable());
|
||||||
value.setPickupReceivable(request.pickupReceivable());
|
value.setPickupReceivable(request.pickupReceivable());
|
||||||
value.setWarehousePayable(request.warehousePayable());
|
value.setWarehousePayable(request.warehousePayable());
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS fl_fee_item (
|
|||||||
fee_code VARCHAR(50) NOT NULL COMMENT '稳定费用编码;平台公共主数据固定编码',
|
fee_code VARCHAR(50) NOT NULL COMMENT '稳定费用编码;平台公共主数据固定编码',
|
||||||
supplier_category VARCHAR(16) NULL COMMENT 'PAYABLE供应商类别约束;为空时可选任意ACTIVE供应商',
|
supplier_category VARCHAR(16) NULL COMMENT 'PAYABLE供应商类别约束;为空时可选任意ACTIVE供应商',
|
||||||
fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称',
|
fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称',
|
||||||
|
default_price DECIMAL(12,2) NULL COMMENT '默认价格CNY;仅选费带入,实际费用可修改',
|
||||||
pickup_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应付是否参与',
|
pickup_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应付是否参与',
|
||||||
pickup_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应收是否参与',
|
pickup_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应收是否参与',
|
||||||
warehouse_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '仓储应付是否参与',
|
warehouse_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '仓储应付是否参与',
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ components:
|
|||||||
feeCode: {type: string, maxLength: 50, pattern: '^[A-Za-z0-9_-]+$'}
|
feeCode: {type: string, maxLength: 50, pattern: '^[A-Za-z0-9_-]+$'}
|
||||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: PAYABLE供应商类别约束;为空时可选任意ACTIVE Warehouse Supplier}
|
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: PAYABLE供应商类别约束;为空时可选任意ACTIVE Warehouse Supplier}
|
||||||
feeTypeName: {type: string, maxLength: 128}
|
feeTypeName: {type: string, maxLength: 128}
|
||||||
|
defaultPrice: {type: number, nullable: true, minimum: 0, maximum: 9999999999.99, multipleOf: 0.01, description: 默认价格CNY;为空不自动带价,业务录入时可修改,不追改已有费用}
|
||||||
pickupPayable: {type: boolean}
|
pickupPayable: {type: boolean}
|
||||||
pickupReceivable: {type: boolean}
|
pickupReceivable: {type: boolean}
|
||||||
warehousePayable: {type: boolean}
|
warehousePayable: {type: boolean}
|
||||||
@@ -118,6 +119,7 @@ components:
|
|||||||
feeCode: {type: string}
|
feeCode: {type: string}
|
||||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: PAYABLE供应商类别约束;为空时可选任意ACTIVE Warehouse Supplier}
|
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: PAYABLE供应商类别约束;为空时可选任意ACTIVE Warehouse Supplier}
|
||||||
feeTypeName: {type: string}
|
feeTypeName: {type: string}
|
||||||
|
defaultPrice: {type: number, nullable: true, minimum: 0, maximum: 9999999999.99, multipleOf: 0.01, description: 默认价格CNY;仅用于业务选择费用项目时带入}
|
||||||
pickupPayable: {type: boolean}
|
pickupPayable: {type: boolean}
|
||||||
pickupReceivable: {type: boolean}
|
pickupReceivable: {type: boolean}
|
||||||
warehousePayable: {type: boolean}
|
warehousePayable: {type: boolean}
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ class FeeManagementContractTest {
|
|||||||
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
|
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
|
||||||
assertThat(schemas).containsKeys("FeeItemSaveRequest", "FeeItemResource", "FeeItemPage",
|
assertThat(schemas).containsKeys("FeeItemSaveRequest", "FeeItemResource", "FeeItemPage",
|
||||||
"FeeItemApiResponse", "FeeItemPageApiResponse", "VoidApiResponse");
|
"FeeItemApiResponse", "FeeItemPageApiResponse", "VoidApiResponse");
|
||||||
|
for (String schema : List.of("FeeItemSaveRequest", "FeeItemResource")) {
|
||||||
|
assertThat(stringMap(properties(schemas, schema).get("defaultPrice")))
|
||||||
|
.containsEntry("nullable", true).containsEntry("minimum", 0);
|
||||||
|
}
|
||||||
for (String response : List.of("FeeItemApiResponse", "FeeItemPageApiResponse", "VoidApiResponse")) {
|
for (String response : List.of("FeeItemApiResponse", "FeeItemPageApiResponse", "VoidApiResponse")) {
|
||||||
assertThat(list(stringMap(schemas.get(response)).get("required"))).contains("timestamp");
|
assertThat(list(stringMap(schemas.get(response)).get("required"))).contains("timestamp");
|
||||||
assertThat(properties(schemas, response)).containsKeys(
|
assertThat(properties(schemas, response)).containsKeys(
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class FeeManagementServiceTest {
|
|||||||
|
|
||||||
assertThat(created.getId()).isEqualTo(11L);
|
assertThat(created.getId()).isEqualTo(11L);
|
||||||
assertThat(created.getAllocationFee()).isTrue();
|
assertThat(created.getAllocationFee()).isTrue();
|
||||||
|
assertThat(created.getDefaultPrice()).isEqualByComparingTo("60.00");
|
||||||
assertThat(created.getWarehouseReceivable()).isTrue();
|
assertThat(created.getWarehouseReceivable()).isTrue();
|
||||||
assertThat(created.getWarehousePayable()).isTrue();
|
assertThat(created.getWarehousePayable()).isTrue();
|
||||||
assertThat(created.getSupplierCategory()).isNull();
|
assertThat(created.getSupplierCategory()).isNull();
|
||||||
@@ -122,7 +123,7 @@ class FeeManagementServiceTest {
|
|||||||
@DisplayName("自定义费用编码统一转为大写并检查客户内唯一性")
|
@DisplayName("自定义费用编码统一转为大写并检查客户内唯一性")
|
||||||
void normalizesCustomFeeCode() {
|
void normalizesCustomFeeCode() {
|
||||||
FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL",
|
FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL",
|
||||||
"storage_fee", "仓库租赁费", false, false, true, true, true, "应付仓库租赁费");
|
"storage_fee", null, "仓库租赁费", false, false, true, true, true, "应付仓库租赁费", null);
|
||||||
|
|
||||||
FeeItemEntity created = feeItems.create(9L, "admin", request);
|
FeeItemEntity created = feeItems.create(9L, "admin", request);
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ class FeeManagementServiceTest {
|
|||||||
@DisplayName("并发创建相同费用编码返回业务冲突")
|
@DisplayName("并发创建相同费用编码返回业务冲突")
|
||||||
void rejectsConcurrentDuplicateFeeCode() {
|
void rejectsConcurrentDuplicateFeeCode() {
|
||||||
FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL",
|
FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL",
|
||||||
"STORAGE_FEE", "仓库租赁费", false, false, true, true, true, null);
|
"STORAGE_FEE", null, "仓库租赁费", false, false, true, true, true, null, null);
|
||||||
doThrow(new DuplicateKeyException("duplicate fee code")).when(repository).insertItem(any());
|
doThrow(new DuplicateKeyException("duplicate fee code")).when(repository).insertItem(any());
|
||||||
|
|
||||||
assertThatThrownBy(() -> feeItems.create(9L, "admin", request))
|
assertThatThrownBy(() -> feeItems.create(9L, "admin", request))
|
||||||
@@ -156,6 +157,38 @@ class FeeManagementServiceTest {
|
|||||||
verify(repository, never()).itemCodeExists("STORAGE_FEE", 11L);
|
verify(repository, never()).itemCodeExists("STORAGE_FEE", 11L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 清空默认价只修改公共项目,不触碰已生成的费用。 */
|
||||||
|
@Test
|
||||||
|
void clearsDefaultPrice() {
|
||||||
|
FeeItemEntity existing = item(true, true);
|
||||||
|
existing.setDefaultPrice(new BigDecimal("60.00"));
|
||||||
|
when(repository.findItem(11L)).thenReturn(existing);
|
||||||
|
when(repository.updateItem(any())).thenReturn(1);
|
||||||
|
FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL",
|
||||||
|
null, null, "仓库租赁费", false, false, true, true, true, null, null);
|
||||||
|
|
||||||
|
assertThat(feeItems.update(11L, 9L, "admin", request).getDefaultPrice()).isNull();
|
||||||
|
verify(repository).updateItem(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 请求价格允许空值和零元,拒绝负数、过多小数和超出字段范围的金额。 */
|
||||||
|
@Test
|
||||||
|
void validatesDefaultPrice() {
|
||||||
|
try (var factory = jakarta.validation.Validation.buildDefaultValidatorFactory()) {
|
||||||
|
var validator = factory.getValidator();
|
||||||
|
for (String price : List.of("0", "60.25", "9999999999.99")) {
|
||||||
|
var request = new FeeItemDtos.SaveRequest("仓储成本", "DOMESTIC", null, null,
|
||||||
|
"操作费", false, true, false, false, false, null, new BigDecimal(price));
|
||||||
|
assertThat(validator.validate(request)).isEmpty();
|
||||||
|
}
|
||||||
|
for (String price : List.of("-1", "60.255", "10000000000")) {
|
||||||
|
var request = new FeeItemDtos.SaveRequest("仓储成本", "DOMESTIC", null, null,
|
||||||
|
"操作费", false, true, false, false, false, null, new BigDecimal(price));
|
||||||
|
assertThat(validator.validate(request)).isNotEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("任一客户引用公共费用项目后都禁止删除")
|
@DisplayName("任一客户引用公共费用项目后都禁止删除")
|
||||||
void rejectsDeletingItemBoundByAnyCustomer() {
|
void rejectsDeletingItemBoundByAnyCustomer() {
|
||||||
@@ -478,9 +511,10 @@ class FeeManagementServiceTest {
|
|||||||
eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class));
|
eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 构造带默认价格的公共费用项目。 */
|
||||||
private static FeeItemDtos.SaveRequest itemRequest() {
|
private static FeeItemDtos.SaveRequest itemRequest() {
|
||||||
return new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", "仓库租赁费",
|
return new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", null, null, "仓库租赁费",
|
||||||
false, false, true, true, true, "应付仓库租赁费");
|
false, false, true, true, true, "应付仓库租赁费", new BigDecimal("60.00"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InboundFeeDetailDtos.SaveRequest detailRequest(String direction) {
|
private static InboundFeeDetailDtos.SaveRequest detailRequest(String direction) {
|
||||||
|
|||||||
Reference in New Issue
Block a user