优化
This commit is contained in:
@@ -47,7 +47,6 @@ public final class ContainerDtos {
|
||||
@Schema(name = "ContainerSaveRequest", description = "货柜基础资料;智能装柜草稿阶段可完整编辑")
|
||||
public record SaveRequest(
|
||||
@Size(max = 64) String containerNo,
|
||||
@NotBlank @Size(max = 64) String cabinetNo,
|
||||
@NotNull @Positive Long warehouseId,
|
||||
@Size(max = 128) String warehouseName,
|
||||
@Size(max = 128) String billOfLadingNo,
|
||||
|
||||
@@ -171,11 +171,11 @@ public class ContainerFeeDetailService {
|
||||
if (supplierId == null || supplierId <= 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "应付柜次费用必须选择有效供应商");
|
||||
}
|
||||
String category = FeeItemSupplierCategory.requirePayable(item.getSupplierCategory());
|
||||
String category = FeeItemSupplierCategory.normalize(item.getSupplierCategory());
|
||||
var snapshot = suppliers.fee(supplierId, category);
|
||||
value.setSupplierId(snapshot.supplierId());
|
||||
value.setSupplierName(snapshot.name());
|
||||
value.setSupplierCategory(snapshot.category());
|
||||
value.setSupplierCategory(category);
|
||||
}
|
||||
|
||||
private void refreshSupplierSnapshot(ContainerFeeDetailEntity value, FeeItemEntity item,
|
||||
|
||||
@@ -139,7 +139,7 @@ public class ContainerService {
|
||||
ContainerSpecificationCatalog.requireMatch(request.data().containerType(), request.data().capacityCbm(), request.data().maxWeightKg());
|
||||
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(request.data().warehouseId());
|
||||
var trucking = suppliers.trucking(request.data().terminalTruckingCompanyId(), request.data().terminalTruckingCompany());
|
||||
ensureUnique(trim(request.data().containerNo()), request.data().cabinetNo().trim(), id);
|
||||
ensureUnique(trim(request.data().containerNo()), current.getCabinetNo(), id);
|
||||
copy(current, request.data());
|
||||
current.setWarehouseId(warehouse.id()); current.setWarehouseName(warehouse.warehouseName());
|
||||
current.setTerminalTruckingCompanyId(trucking == null ? null : trucking.supplierId());
|
||||
@@ -222,7 +222,7 @@ public class ContainerService {
|
||||
}
|
||||
private static void copy(ContainerBookingEntity v, SaveRequest r) {
|
||||
var specification = ContainerSpecificationCatalog.requireMatch(r.containerType(), r.capacityCbm(), r.maxWeightKg());
|
||||
v.setContainerNo(trim(r.containerNo())); v.setCabinetNo(r.cabinetNo().trim());
|
||||
v.setContainerNo(trim(r.containerNo()));
|
||||
v.setWarehouseId(r.warehouseId()); v.setWarehouseName(trim(r.warehouseName()));
|
||||
v.setBillOfLadingNo(trim(r.billOfLadingNo())); v.setVesselName(trim(r.vesselName()));
|
||||
v.setVoyageNo(trim(r.voyageNo())); v.setContainerType(specification.containerType());
|
||||
|
||||
@@ -104,11 +104,6 @@ public class FeeItemService {
|
||||
private static void copy(FeeItemEntity value, SaveRequest request) {
|
||||
boolean payable = Boolean.TRUE.equals(request.pickupPayable())
|
||||
|| Boolean.TRUE.equals(request.warehousePayable());
|
||||
if (payable && (request.supplierCategory() == null
|
||||
|| request.supplierCategory().isBlank())) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"启用应付方向时必须配置供应商类别");
|
||||
}
|
||||
value.setCategory(request.category().trim());
|
||||
value.setTradeScope(request.tradeScope());
|
||||
value.setFeeCode(request.feeCode() == null || request.feeCode().isBlank()
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/** 费用项目显式供应商类别校验;未配置的应付费用失败关闭。 */
|
||||
/** 费用项目可选供应商类别约束校验。 */
|
||||
public final class FeeItemSupplierCategory {
|
||||
private static final Set<String> ALLOWED = Set.of(
|
||||
"BOOKING", "CUSTOMS", "TRUCKING", "CERTIFICATE");
|
||||
@@ -23,12 +23,4 @@ public final class FeeItemSupplierCategory {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static String requirePayable(String supplierCategory) {
|
||||
String category = normalize(supplierCategory);
|
||||
if (category == null) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"费用项目未配置供应商类别,不能用于应付费用");
|
||||
}
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,11 +414,11 @@ public class InboundFeeDetailService {
|
||||
if (supplierId == null || supplierId <= 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "应付入库费用必须选择有效供应商");
|
||||
}
|
||||
String category = FeeItemSupplierCategory.requirePayable(item.getSupplierCategory());
|
||||
String category = FeeItemSupplierCategory.normalize(item.getSupplierCategory());
|
||||
var snapshot = suppliers.fee(supplierId, category);
|
||||
value.setSupplierId(snapshot.supplierId());
|
||||
value.setSupplierName(snapshot.name());
|
||||
value.setSupplierCategory(snapshot.category());
|
||||
value.setSupplierCategory(category);
|
||||
}
|
||||
|
||||
private void refreshSupplierSnapshot(InboundFeeDetailEntity value, FeeItemEntity item,
|
||||
|
||||
@@ -48,7 +48,7 @@ public class TruckAppointmentService {
|
||||
@Transactional public TruckAppointmentAttachmentEntity upload(long id,String type,MultipartFile file,long uid){String attachmentType=attachmentType(type);TruckAppointmentEntity appointment=required(id);if(appointment.getCustomerId()==null||appointment.getCustomerId()<=0)throw bad("预约缺少客户归属,无法上传附件");try{var response=files.upload(internalToken,uid,appointment.getCustomerId(),"TRUCK_APPOINTMENT_ATTACHMENT",Long.toString(id),file);if(response==null||!response.successful()||response.data()==null)throw bad("附件上传失败");var m=response.data();TruckAppointmentAttachmentEntity a=new TruckAppointmentAttachmentEntity();a.setId(IdWorker.getId());a.setAppointmentId(id);a.setFileId(m.fileId());a.setAttachmentType(attachmentType);a.setFileName(m.fileName());a.setFileHash(m.sha256());a.setVisibility("PLATFORM");a.setUploadedBy(uid);a.setUploadedAt(LocalDateTime.now());repo.attachment(a);return a;}catch(FeignException ex){throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"文件服务暂不可用");}}
|
||||
@Transactional public TruckAppointmentAttachmentEntity attach(long id,AttachmentRequest r,long uid){String attachmentType=attachmentType(r.attachmentType());TruckAppointmentEntity appointment=required(id); if(appointment.getCustomerId()==null||appointment.getCustomerId()<=0)throw bad("预约缺少客户归属,无法引用附件"); TruckAppointmentFileClient.Metadata metadata;try{var response=files.metadata(internalToken,appointment.getCustomerId(),"TRUCK_APPOINTMENT_ATTACHMENT",Long.toString(id),r.fileId());if(response==null||!response.successful()||response.data()==null)throw bad("附件不存在或归属不匹配");metadata=response.data();}catch(FeignException ex){throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"文件服务暂不可用");} TruckAppointmentAttachmentEntity a=new TruckAppointmentAttachmentEntity();a.setId(IdWorker.getId());a.setAppointmentId(id);a.setFileId(metadata.fileId());a.setAttachmentType(attachmentType);a.setFileName(metadata.fileName());a.setFileHash(metadata.sha256());a.setVisibility("PLATFORM");a.setUploadedBy(uid);a.setUploadedAt(LocalDateTime.now());repo.attachment(a);return a;}
|
||||
@Transactional public void deleteAttachment(long id,long attachmentId){required(id);if(repo.deleteAttachment(id,attachmentId)!=1)throw new BizException(ResultCode.NOT_FOUND,"附件引用不存在");}
|
||||
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()==null||r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0");validateRequired(r); var customer=customers.active(r.customerId(),r.customerName()); AssociationContext links=validateAssociations(r,customer.id()); var carrier=suppliers.trucking(r.carrierId(),r.carrierName()); TruckAppointmentEntity e=new TruckAppointmentEntity(); e.setId(IdWorker.getId()); e.setAppointmentNo(documentNumbers.next("TA")); e.setStatus(TruckAppointmentStatus.DRAFT); e.setRevision(0); e.setVersion(0); e.setCreatedAt(LocalDateTime.now()); e.setUpdatedAt(e.getCreatedAt()); copy(e,r,links,customer.tenantName(),carrier.name()); repo.insert(e); record(e,null,e.getStatus().name(),"CREATE",null,uid,name); return e;}
|
||||
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()==null||r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0");validateRequired(r); var customer=customers.active(r.customerId(),r.customerName()); AssociationContext links=validateAssociations(r,customer.id()); var carrier=suppliers.trucking(r.carrierId(),r.carrierName()); TruckAppointmentEntity e=new TruckAppointmentEntity(); e.setId(IdWorker.getId()); e.setAppointmentNo(documentNumbers.next("TA")); e.setStatus(TruckAppointmentStatus.DRAFT); e.setRevision(0); e.setVersion(0); e.setCreatedAt(LocalDateTime.now()); e.setUpdatedAt(e.getCreatedAt()); copy(e,r,links,customer.tenantName(),carrier.name()); repo.insert(e); record(e,null,e.getStatus().name(),"CREATE",null,uid,name); return enrich(e);}
|
||||
@Transactional public TruckAppointmentEntity edit(long id,long uid,String name,SaveRequest r){validateRequired(r);TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); if(!(e.getStatus()==TruckAppointmentStatus.DRAFT||e.getStatus()==TruckAppointmentStatus.PENDING_DISPATCH||e.getStatus()==TruckAppointmentStatus.RESCHEDULED))throw conflict("当前状态不可编辑"); var customer=customers.active(r.customerId(),r.customerName()); AssociationContext links=validateAssociations(r,customer.id()); var carrier=suppliers.trucking(r.carrierId(),r.carrierName()); copy(e,r,links,customer.tenantName(),carrier.name()); if(repo.updateEdit(e,r.expectedVersion())!=1)throw conflict("版本已变化,请刷新后重试"); record(e,e.getStatus().name(),e.getStatus().name(),"EDIT",null,uid,name); return required(id);}
|
||||
@Transactional public TruckAppointmentEntity action(long id,String action,ActionRequest r,String idem,String clientHash,long uid,String name){String requestHash=hash(action,id,r);verifyClientHash(clientHash,requestHash);TruckAppointmentEntity replay=claimOrReplay(id,action,idem,requestHash);if(replay!=null)return replay;TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); if("RESCHEDULE".equals(action))throw conflict("改约请使用专用接口"); TruckAppointmentStatus from=e.getStatus(),to=next(from,action); String dispatchBill="DISPATCH".equals(action)?requireDispatchReady(e):null; int changed="DISPATCH".equals(action)?repo.dispatch(id,from.name(),dispatchBill,e.getVersion()):repo.transition(id,from.name(),to.name(),e.getVersion()); if(changed!=1)throw conflict("状态或版本已变化,请刷新后重试"); TruckAppointmentEntity after=required(id);record(after,from.name(),to.name(),action,r.reason(),uid,name);return completeOperation(id,action,idem,after);}
|
||||
@Transactional public TruckAppointmentEntity reschedule(long id,LocalDateTime at,ActionRequest r,String idem,String clientHash,long uid,String name){String action="RESCHEDULE";String requestHash=hash(action,id,new ReschedulePayload(at,r));verifyClientHash(clientHash,requestHash);TruckAppointmentEntity replay=claimOrReplay(id,action,idem,requestHash);if(replay!=null)return replay;TruckAppointmentEntity e=required(id);checkVersion(e,r.expectedVersion()); if(e.getStatus()==TruckAppointmentStatus.COMPLETED||e.getStatus()==TruckAppointmentStatus.CANCELLED)throw conflict("当前状态不可改约"); String from=e.getStatus().name(); if(repo.reschedule(id,at,r.reason(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试");TruckAppointmentEntity after=required(id);record(after,from,"RESCHEDULED",action,r.reason(),uid,name);return completeOperation(id,action,idem,after);}
|
||||
|
||||
+3
-6
@@ -26,12 +26,9 @@ public class WarehouseSupplierSnapshotService {
|
||||
return resolve(supplierId, suppliedName, "CUSTOMS");
|
||||
}
|
||||
|
||||
/** 费用来源只提交供应商 ID,名称与类别均以 Warehouse 返回快照为准。 */
|
||||
/** 费用来源只提交供应商 ID;类别为空时仅校验 ACTIVE,名称始终取 Warehouse 快照。 */
|
||||
public WarehouseSupplierClient.Snapshot fee(Long supplierId, String category) {
|
||||
if (!StringUtils.hasText(category)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "费用项目未配置供应商类别");
|
||||
}
|
||||
return resolve(supplierId, null, category);
|
||||
return resolve(supplierId, null, FeeItemSupplierCategory.normalize(category));
|
||||
}
|
||||
|
||||
private WarehouseSupplierClient.Snapshot resolve(Long supplierId, String suppliedName,
|
||||
@@ -46,7 +43,7 @@ public class WarehouseSupplierSnapshotService {
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| response.data().supplierId() != supplierId
|
||||
|| !"ACTIVE".equals(response.data().status())
|
||||
|| !category.equals(response.data().category())
|
||||
|| (StringUtils.hasText(category) && !category.equals(response.data().category()))
|
||||
|| !StringUtils.hasText(response.data().name())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "所选供应商不存在、未启用或类别不匹配");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS fl_fee_item (
|
||||
category VARCHAR(64) NOT NULL COMMENT '费用分类',
|
||||
trade_scope VARCHAR(20) NOT NULL COMMENT 'DOMESTIC国内、INTERNATIONAL国际',
|
||||
fee_code VARCHAR(50) NOT NULL COMMENT '稳定费用编码;平台公共主数据固定编码',
|
||||
supplier_category VARCHAR(16) NULL COMMENT 'PAYABLE供应商类别;未配置则失败关闭',
|
||||
supplier_category VARCHAR(16) NULL COMMENT 'PAYABLE供应商类别约束;为空时可选任意ACTIVE供应商',
|
||||
fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称',
|
||||
pickup_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应付是否参与',
|
||||
pickup_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应收是否参与',
|
||||
|
||||
@@ -23,7 +23,7 @@ DELIMITER ;
|
||||
CALL phase2_publish_add_column('fl_fee_item', 'fee_code',
|
||||
"VARCHAR(50) NULL COMMENT '稳定费用编码;系统费用使用固定编码' AFTER trade_scope");
|
||||
CALL phase2_publish_add_column('fl_fee_item', 'supplier_category',
|
||||
"VARCHAR(16) NULL COMMENT 'PAYABLE供应商类别;未配置则失败关闭' AFTER fee_code");
|
||||
"VARCHAR(16) NULL COMMENT 'PAYABLE供应商类别约束;为空时可选任意ACTIVE供应商' AFTER fee_code");
|
||||
ALTER TABLE fl_fee_item
|
||||
MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id',
|
||||
COMMENT = '平台公共费用项目主数据';
|
||||
|
||||
@@ -304,8 +304,6 @@ components:
|
||||
currency: {type: string, pattern: '^[A-Z]{3}$'}
|
||||
amount: {type: number, minimum: 0}
|
||||
supplierId: {type: string, nullable: true}
|
||||
supplierName: {type: string, maxLength: 200, nullable: true, readOnly: true}
|
||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true}
|
||||
remark: {type: string, maxLength: 500, nullable: true}
|
||||
InboundFeeDetailVersionRequest:
|
||||
type: object
|
||||
@@ -332,7 +330,7 @@ components:
|
||||
direction: {type: string, enum: [RECEIVABLE, PAYABLE]}
|
||||
supplierId: {type: string, nullable: true}
|
||||
supplierName: {type: string, nullable: true, readOnly: true}
|
||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true}
|
||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: 费用项目供应商类别约束快照;为空表示仅要求供应商ACTIVE}
|
||||
businessDate: {type: string, format: date}
|
||||
customerId: {type: string, nullable: true}
|
||||
customerName: {type: string, nullable: true}
|
||||
|
||||
@@ -100,7 +100,7 @@ components:
|
||||
category: {type: string, maxLength: 64}
|
||||
tradeScope: {type: string, enum: [DOMESTIC, INTERNATIONAL]}
|
||||
feeCode: {type: string, maxLength: 50, pattern: '^[A-Za-z0-9_-]+$'}
|
||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: pickupPayable或warehousePayable为true时必填}
|
||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: PAYABLE供应商类别约束;为空时可选任意ACTIVE Warehouse Supplier}
|
||||
feeTypeName: {type: string, maxLength: 128}
|
||||
pickupPayable: {type: boolean}
|
||||
pickupReceivable: {type: boolean}
|
||||
@@ -116,7 +116,7 @@ components:
|
||||
category: {type: string}
|
||||
tradeScope: {type: string, enum: [DOMESTIC, INTERNATIONAL]}
|
||||
feeCode: {type: string}
|
||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true}
|
||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: PAYABLE供应商类别约束;为空时可选任意ACTIVE Warehouse Supplier}
|
||||
feeTypeName: {type: string}
|
||||
pickupPayable: {type: boolean}
|
||||
pickupReceivable: {type: boolean}
|
||||
|
||||
@@ -70,6 +70,30 @@ paths:
|
||||
requireInboundCodePrefix: {type: boolean}
|
||||
responses:
|
||||
'200': {description: 成功时 data 返回 channelCode;requireInboundCodePrefix=true 时同时返回 inboundCodePrefix}
|
||||
/internal/firstleg/v1/inbounds/{inboundId}/fees/{feeId}/source-snapshot:
|
||||
get:
|
||||
summary: Finance 按入库费用来源版本精确回查不可变快照
|
||||
description: sourceVersion 必填;只返回 sourceFeeId 与该版本精确匹配的已发布费用,不回退到其他版本。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
parameters:
|
||||
- {name: inboundId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
- {name: feeId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
- {name: sourceVersion, in: query, required: true, schema: {type: integer, minimum: 1}}
|
||||
responses:
|
||||
'200': {description: 成功时 data 为 FinanceFeeSourceSnapshot;不存在精确版本时返回业务错误}
|
||||
/internal/firstleg/v1/containers/{containerId}/fees/{feeId}/source-snapshot:
|
||||
get:
|
||||
summary: Finance 按柜次费用来源版本精确回查不可变快照
|
||||
description: sourceVersion 必填;只返回 sourceFeeId 与该版本精确匹配的已发布费用,不回退到当前可变版本。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
parameters:
|
||||
- {name: containerId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
- {name: feeId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
- {name: sourceVersion, in: query, required: true, schema: {type: integer, minimum: 1}}
|
||||
responses:
|
||||
'200': {description: 成功时 data 为 FinanceFeeSourceSnapshot;不存在精确版本时返回业务错误}
|
||||
/api/platform/v1/first-leg/customer-dimension-config:
|
||||
get:
|
||||
summary: 查询客户固定四箱规尺寸配置
|
||||
@@ -1495,10 +1519,9 @@ components:
|
||||
description: 单证聚合状态;DO/PU 无完成动作,因此只返回 MISSING 或 UPLOADED
|
||||
ContainerSaveRequest:
|
||||
type: object
|
||||
required: [cabinetNo, warehouseId, warehouseName, containerType, capacityCbm, maxWeightKg]
|
||||
required: [warehouseId, warehouseName, containerType, capacityCbm, maxWeightKg]
|
||||
properties:
|
||||
containerNo: {type: string, maxLength: 64, nullable: true}
|
||||
cabinetNo: {type: string, maxLength: 64}
|
||||
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
warehouseName: {type: string, maxLength: 128}
|
||||
billOfLadingNo: {type: string, maxLength: 128, nullable: true}
|
||||
@@ -1820,3 +1843,39 @@ components:
|
||||
expectedSourceVersion: {type: integer, minimum: 1}
|
||||
businessDate: {type: string, format: date}
|
||||
amount: {type: number, minimum: 0}
|
||||
FinanceFeeSourceSnapshot:
|
||||
type: object
|
||||
description: Finance 消费的版本化费用来源快照;supplierCategory 为空表示仅约束供应商 ACTIVE,非空才约束明确类别。
|
||||
required: [tenantId, sourceType, feeId, feeVersion, customerId, customerName, feeItemName, direction, businessDate, currency, totalAmount, reviewStatus, publicationStatus, sourceHash, snapshotVersion]
|
||||
properties:
|
||||
tenantId: {type: integer, format: int64}
|
||||
sourceType: {type: string, enum: [INBOUND_FEE, CONTAINER_FEE]}
|
||||
feeId: {type: integer, format: int64}
|
||||
feeVersion: {type: integer, minimum: 1}
|
||||
inboundId: {type: integer, format: int64, nullable: true}
|
||||
inboundCode: {type: string, nullable: true}
|
||||
orderId: {type: integer, format: int64, nullable: true}
|
||||
containerId: {type: integer, format: int64, nullable: true}
|
||||
containerBookingId: {type: integer, format: int64, nullable: true}
|
||||
containerNo: {type: string, nullable: true}
|
||||
customerId: {type: integer, format: int64}
|
||||
customerName: {type: string}
|
||||
supplierId: {type: integer, format: int64, nullable: true}
|
||||
supplierName: {type: string, nullable: true}
|
||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true, description: PAYABLE供应商类别约束;为空时仅要求供应商ACTIVE}
|
||||
feeItemId: {type: integer, format: int64, nullable: true}
|
||||
feeItemCode: {type: string, nullable: true}
|
||||
feeItemName: {type: string}
|
||||
direction: {type: string, enum: [RECEIVABLE, PAYABLE]}
|
||||
businessDate: {type: string, format: date}
|
||||
currency: {type: string, enum: [CNY]}
|
||||
totalAmount: {type: number, minimum: 0}
|
||||
reviewStatus: {type: string, enum: [APPROVED]}
|
||||
publicationStatus: {type: string, enum: [PUBLISHED]}
|
||||
publishedAt: {type: string, format: date-time, nullable: true}
|
||||
chargeableQuantity: {type: number, nullable: true}
|
||||
chargeUnit: {type: string, nullable: true}
|
||||
actualGrossWeightKg: {type: number, nullable: true}
|
||||
actualVolumeCbm: {type: number, nullable: true}
|
||||
sourceHash: {type: string}
|
||||
snapshotVersion: {type: integer, minimum: 1}
|
||||
|
||||
@@ -29,7 +29,7 @@ paths:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/SaveRequest'}
|
||||
responses:
|
||||
'200': {description: Created appointment, content: {application/json: {schema: {$ref: '#/components/schemas/DetailStableReferences'}}}}
|
||||
'200': {description: Created appointment, content: {application/json: {schema: {$ref: '#/components/schemas/DetailEnvelope'}}}}
|
||||
/api/platform/v1/truck-appointments/{id}:
|
||||
parameters: [{$ref: '#/components/parameters/AppointmentId'}]
|
||||
get:
|
||||
@@ -138,6 +138,18 @@ components:
|
||||
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
||||
RequestHash: {name: X-Request-Hash, in: header, required: false, schema: {type: string, pattern: '^[0-9a-fA-F]{64}$'}}
|
||||
schemas:
|
||||
DetailEnvelope:
|
||||
type: object
|
||||
required: [code, message, data, timestamp]
|
||||
properties:
|
||||
code: {type: string, example: '200'}
|
||||
message: {type: string}
|
||||
data:
|
||||
allOf: [{$ref: '#/components/schemas/DetailStableReferences'}]
|
||||
nullable: true
|
||||
requestId: {type: string, nullable: true}
|
||||
traceId: {type: string, nullable: true}
|
||||
timestamp: {type: string, format: date-time}
|
||||
AttachmentRequest:
|
||||
type: object
|
||||
required: [fileId, attachmentType]
|
||||
|
||||
+6
-6
@@ -16,19 +16,19 @@ class InternalFinanceFeeSourceControllerTest {
|
||||
|
||||
@Test
|
||||
void rejectsMissingOrWrongInternalTokenBeforeSnapshotLookup() {
|
||||
assertThatThrownBy(() -> controller.inbound(null, 21L, 31L))
|
||||
assertThatThrownBy(() -> controller.inbound(null, 21L, 31L, 2))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("令牌无效");
|
||||
assertThatThrownBy(() -> controller.container("wrong", 41L, 51L))
|
||||
assertThatThrownBy(() -> controller.container("wrong", 41L, 51L, 2))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("令牌无效");
|
||||
}
|
||||
|
||||
@Test
|
||||
void routesAuthorizedSnapshotLookup() {
|
||||
controller.inbound("finance-token", 21L, 31L);
|
||||
controller.container("finance-token", 41L, 51L);
|
||||
controller.inbound("finance-token", 21L, 31L, 2);
|
||||
controller.container("finance-token", 41L, 51L, 3);
|
||||
|
||||
verify(service).inbound(21L, 31L);
|
||||
verify(service).container(41L, 51L);
|
||||
verify(service).inbound(21L, 31L, 2);
|
||||
verify(service).container(41L, 51L, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ class TruckAppointmentControllerContractTest {
|
||||
}
|
||||
}
|
||||
@Test void openApiDocumentsLifecycleIdempotencyAndActualTimes() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("/api/platform/v1/truck-appointments/{id}/{action}:","/api/platform/v1/truck-appointments/{id}/reschedule:","name: Idempotency-Key","required: true","actual event timestamps");}
|
||||
@Test void openApiFreezesIndependentCreateAndOptionalAssociations() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("required: [customerId, carrierId, plannedLoadingAt, expectedVersion]","independent operational records","no RELEASED-state prerequisite","It may be supplied only with containerBookingId","ACTIVE IAM customer","ACTIVE Warehouse supplier ID validated as TRUCKING","Fulfillment associations remain null");}
|
||||
@Test void openApiFreezesIndependentCreateAndOptionalAssociations() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("required: [customerId, carrierId, plannedLoadingAt, expectedVersion]","independent operational records","no RELEASED-state prerequisite","It may be supplied only with containerBookingId","ACTIVE IAM customer","ACTIVE Warehouse supplier ID validated as TRUCKING","Fulfillment associations remain null","schema: {$ref: '#/components/schemas/DetailEnvelope'}","required: [code, message, data, timestamp]","allOf: [{$ref: '#/components/schemas/DetailStableReferences'}]","nullable: true");}
|
||||
@Test void openApiFreezesAttachmentRequestAndOwnershipContract() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("AttachmentRequest:","required: [fileId, attachmentType]","required: [attachmentType, file]","BILL_OF_LADING","TRUCK_APPOINTMENT_ATTACHMENT","Deletes the appointment reference only","DetailStableReferences:","oceanBookingNo:","shippingCompanyName:","bookingAgentName:","cabinetNo:","billOfLadingNo:");}
|
||||
@Test void methodsHavePlatformPermissionsAndAuthenticatedScope() {
|
||||
Map<String,String> expected=Map.ofEntries(
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ class ContainerFeeDetailRepositoryTest {
|
||||
"getReviewStatus", "getReviewerId", "getReviewerName", "getReviewedAt",
|
||||
"getReviewReason", "getUpdatedBy", "getUpdatedAt")
|
||||
.doesNotContain("getFeeItemId", "getFeeItemCodeSnapshot", "getFeeItemNameSnapshot",
|
||||
"getDirection", "getPartnerId", "getPartnerName", "getCurrency",
|
||||
"getDirection", "getSupplierId", "getSupplierName", "getSupplierCategory", "getCurrency",
|
||||
"getRemark");
|
||||
}
|
||||
}
|
||||
|
||||
+14
-10
@@ -9,19 +9,21 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FinanceFeeSourceMigrationContractTest {
|
||||
@Test
|
||||
void freshSchemaAndMigrationExposeInboundFeePartnerSnapshotsUsedByAllFeeFlows() throws Exception {
|
||||
void freshSchemaAndMigrationExposeInboundFeeSupplierSnapshotsUsedByAllFeeFlows() throws Exception {
|
||||
String baseline = new ClassPathResource("db/phase2-fee-item-and-inbound-detail.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String migration = new ClassPathResource("db/finance-fee-source-migration.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(baseline).contains(
|
||||
"partner_id BIGINT UNSIGNED NULL",
|
||||
"partner_name VARCHAR(200) NULL");
|
||||
"supplier_id BIGINT UNSIGNED NULL",
|
||||
"supplier_name VARCHAR(200) NULL",
|
||||
"supplier_category VARCHAR(16) NULL");
|
||||
assertThat(migration).contains(
|
||||
"firstleg_finance_add_column('fl_inbound_fee_detail', 'partner_id'",
|
||||
"firstleg_finance_add_column('fl_inbound_fee_detail', 'partner_name'",
|
||||
"Firstleg Finance迁移失败:入库费用往来单位快照字段不完整");
|
||||
"firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_id'",
|
||||
"firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_name'",
|
||||
"firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_category'",
|
||||
"Firstleg Finance迁移失败:入库费用供应商快照字段不完整");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -32,10 +34,12 @@ class FinanceFeeSourceMigrationContractTest {
|
||||
assertThat(sql).contains("information_schema.columns", "event_key", "payload_hash",
|
||||
"uk_inbound_outbox_event_key", "next_retry_at", "locked_at", "locked_by",
|
||||
"idx_inbound_outbox_finance_relay", "CREATE TABLE IF NOT EXISTS fl_container_fee_detail",
|
||||
"source_version", "partner_id", "partner_name",
|
||||
"firstleg_finance_add_column('fl_container_fee_detail', 'partner_id'", "RECEIVABLE", "PAYABLE",
|
||||
"firstleg_finance_add_column('fl_inbound_fee_detail', 'partner_id'",
|
||||
"Firstleg Finance迁移失败:入库费用往来单位快照字段不完整",
|
||||
"source_version", "supplier_id", "supplier_name", "supplier_category",
|
||||
"CREATE TABLE IF NOT EXISTS fl_container_fee_source_snapshot",
|
||||
"uk_container_fee_source_version",
|
||||
"firstleg_finance_add_column('fl_container_fee_detail', 'supplier_id'", "RECEIVABLE", "PAYABLE",
|
||||
"firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_id'",
|
||||
"Firstleg Finance迁移失败:入库费用供应商快照字段不完整",
|
||||
"chk_container_fee_currency", "currency = 'CNY'", "firstleg_finance_assert_schema");
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -152,8 +152,9 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(150);
|
||||
assertThat(count(sql, "CALL queue_column_comment(")).isGreaterThanOrEqualTo(2459);
|
||||
assertThat(sql)
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'partner_id'")
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'partner_name'");
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_id'")
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_name'")
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_category'");
|
||||
assertThat(sql.indexOf("CALL apply_queued_comments();",
|
||||
sql.indexOf("-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS")))
|
||||
.isGreaterThan(sql.indexOf("-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS"));
|
||||
|
||||
+21
-13
@@ -8,6 +8,7 @@ import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeSourceSnapshotRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -34,8 +35,10 @@ class ContainerFeeDetailServiceTest {
|
||||
private final SmartLoadingRepository loading = mock(SmartLoadingRepository.class);
|
||||
private final FeeItemRepository feeItems = mock(FeeItemRepository.class);
|
||||
private final FinanceFeeSourceOutboxService outbox = mock(FinanceFeeSourceOutboxService.class);
|
||||
private final WarehouseSupplierSnapshotService suppliers = mock(WarehouseSupplierSnapshotService.class);
|
||||
private final ContainerFeeSourceSnapshotRepository sourceSnapshots = mock(ContainerFeeSourceSnapshotRepository.class);
|
||||
private final ContainerFeeDetailService service =
|
||||
new ContainerFeeDetailService(repository, loading, feeItems, outbox);
|
||||
new ContainerFeeDetailService(repository, loading, feeItems, outbox, suppliers, sourceSnapshots);
|
||||
|
||||
@Test
|
||||
void rejectsContainerThatDoesNotContainCustomerOrders() {
|
||||
@@ -58,8 +61,13 @@ class ContainerFeeDetailServiceTest {
|
||||
approved.setId(51L);
|
||||
approved.setTenantId(10001L);
|
||||
approved.setLoadingPlanId(41L);
|
||||
approved.setFeeItemId(11L);
|
||||
approved.setDirection("RECEIVABLE");
|
||||
approved.setSourceVersion(1);
|
||||
when(repository.find(10001L, 41L, 51L)).thenReturn(approved);
|
||||
when(feeItems.findItem(11L)).thenReturn(item());
|
||||
when(repository.updateSupplierSnapshot(eq(10001L), eq(41L), eq(51L),
|
||||
eq(null), eq(null), eq(null), eq(9L))).thenReturn(1);
|
||||
when(repository.review(10001L, 41L, 51L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
service.approve(10001L, 41L, 51L, 9L, "admin");
|
||||
@@ -89,7 +97,7 @@ class ContainerFeeDetailServiceTest {
|
||||
assertThat(result.getReviewStatus()).isEqualTo("PENDING");
|
||||
assertThat(result.getFeeItemId()).isEqualTo(11L);
|
||||
assertThat(result.getDirection()).isEqualTo("PAYABLE");
|
||||
assertThat(result.getPartnerId()).isEqualTo(71L);
|
||||
assertThat(result.getSupplierId()).isEqualTo(71L);
|
||||
assertThat(result.getCurrency()).isEqualTo("CNY");
|
||||
assertThat(result.getRemark()).isEqualTo("原费用备注");
|
||||
verify(feeItems, never()).findItem(anyLong());
|
||||
@@ -162,16 +170,16 @@ class ContainerFeeDetailServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void payableRequiresCounterpartySnapshot() {
|
||||
void payableRequiresSupplierSnapshot() {
|
||||
ownedPlan();
|
||||
when(feeItems.findItem(11L)).thenReturn(item());
|
||||
|
||||
var missingPartner = new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE",
|
||||
var missingSupplier = new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE",
|
||||
LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY",
|
||||
null, null, "整柜拖车费");
|
||||
null, "整柜拖车费");
|
||||
|
||||
assertThatThrownBy(() -> service.create(10001L, 41L, 9L, missingPartner))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("必须选择有效往来单位");
|
||||
assertThatThrownBy(() -> service.create(10001L, 41L, 9L, missingSupplier))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("必须选择有效供应商");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@@ -182,13 +190,13 @@ class ContainerFeeDetailServiceTest {
|
||||
|
||||
var request = new ContainerFeeDetailDtos.SaveRequest(11L, "RECEIVABLE",
|
||||
LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY",
|
||||
71L, "不应保留", "整柜操作费");
|
||||
71L, "整柜操作费");
|
||||
service.create(10001L, 41L, 9L, request);
|
||||
|
||||
ArgumentCaptor<ContainerFeeDetailEntity> captor = ArgumentCaptor.forClass(ContainerFeeDetailEntity.class);
|
||||
verify(repository).insert(captor.capture());
|
||||
assertThat(captor.getValue().getPartnerId()).isNull();
|
||||
assertThat(captor.getValue().getPartnerName()).isNull();
|
||||
assertThat(captor.getValue().getSupplierId()).isNull();
|
||||
assertThat(captor.getValue().getSupplierName()).isNull();
|
||||
}
|
||||
|
||||
private void ownedPlan() {
|
||||
@@ -220,8 +228,8 @@ class ContainerFeeDetailServiceTest {
|
||||
value.setFeeItemCodeSnapshot("TRUCKING");
|
||||
value.setFeeItemNameSnapshot("拖车费");
|
||||
value.setDirection("PAYABLE");
|
||||
value.setPartnerId(71L);
|
||||
value.setPartnerName("现有往来单位");
|
||||
value.setSupplierId(71L);
|
||||
value.setSupplierName("现有供应商");
|
||||
value.setCurrency("CNY");
|
||||
value.setRemark("原费用备注");
|
||||
value.setBusinessDate(LocalDate.of(2026, 8, 11));
|
||||
@@ -239,6 +247,6 @@ class ContainerFeeDetailServiceTest {
|
||||
private static ContainerFeeDetailDtos.SaveRequest request(boolean ignored) {
|
||||
return new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE",
|
||||
LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY",
|
||||
71L, " 现有往来单位 ", "整柜拖车费");
|
||||
71L, "整柜拖车费");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,30 +104,30 @@ class ContainerServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("智能装柜草稿阶段允许完整编辑")
|
||||
@DisplayName("智能装柜草稿阶段完整编辑时保留稳定柜次号")
|
||||
void updatesAllFieldsWhileEditing() {
|
||||
ContainerBookingEntity current = booking("EDITING", 2);
|
||||
when(repository.find(11L)).thenReturn(current);
|
||||
when(repository.updateDraft(any(), org.mockito.ArgumentMatchers.eq(2),
|
||||
org.mockito.ArgumentMatchers.eq(9L), org.mockito.ArgumentMatchers.eq("admin"))).thenReturn(1);
|
||||
|
||||
service.updateDraft(11L, 9L, "admin", new FullUpdateRequest(2, request("柜-NEW")));
|
||||
service.updateDraft(11L, 9L, "admin", new FullUpdateRequest(2, request()));
|
||||
|
||||
assertThat(current.getCabinetNo()).isEqualTo("柜-NEW");
|
||||
assertThat(current.getCabinetNo()).isEqualTo("柜-001");
|
||||
verify(repository).updateDraft(current, 2, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待装柜阶段允许完整编辑")
|
||||
@DisplayName("待装柜阶段完整编辑时保留稳定柜次号")
|
||||
void updatesAllFieldsWhileWaitingForLoading() {
|
||||
ContainerBookingEntity current = booking("WAIT_LOADING", 2);
|
||||
when(repository.find(11L)).thenReturn(current);
|
||||
when(repository.updateDraft(any(), org.mockito.ArgumentMatchers.eq(2),
|
||||
org.mockito.ArgumentMatchers.eq(9L), org.mockito.ArgumentMatchers.eq("admin"))).thenReturn(1);
|
||||
|
||||
service.updateDraft(11L, 9L, "admin", new FullUpdateRequest(2, request("柜 NEW")));
|
||||
service.updateDraft(11L, 9L, "admin", new FullUpdateRequest(2, request()));
|
||||
|
||||
assertThat(current.getCabinetNo()).isEqualTo("柜 NEW");
|
||||
assertThat(current.getCabinetNo()).isEqualTo("柜-001");
|
||||
verify(repository).updateDraft(current, 2, 9L, "admin");
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ class ContainerServiceTest {
|
||||
when(repository.find(11L)).thenReturn(booking("LOADING", 2));
|
||||
|
||||
assertThatThrownBy(() -> service.updateDraft(
|
||||
11L, 9L, "admin", new FullUpdateRequest(2, request("柜-NEW"))))
|
||||
11L, 9L, "admin", new FullUpdateRequest(2, request())))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("只能修改集装箱号和航次");
|
||||
|
||||
@@ -240,7 +240,7 @@ class ContainerServiceTest {
|
||||
when(repository.find(11L)).thenReturn(booking("EDITING", 3));
|
||||
|
||||
assertThatThrownBy(() -> service.updateDraft(
|
||||
11L, 9L, "admin", new FullUpdateRequest(2, request("柜-NEW"))))
|
||||
11L, 9L, "admin", new FullUpdateRequest(2, request())))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("刷新后重试");
|
||||
}
|
||||
@@ -394,8 +394,8 @@ class ContainerServiceTest {
|
||||
return value;
|
||||
}
|
||||
|
||||
private static SaveRequest request(String cabinetNo) {
|
||||
return new SaveRequest("MSCU1234567", cabinetNo, 101L, "上海仓", "BOL-001", "柏悦轮",
|
||||
private static SaveRequest request() {
|
||||
return new SaveRequest("MSCU1234567", 101L, "上海仓", "BOL-001", "柏悦轮",
|
||||
"V001", "40HC", 201L, "船司A", "CNSHA", "上海港", "USLAX", "洛杉矶港",
|
||||
"ONT8", "ONT8", new BigDecimal("76.0000"), new BigDecimal("28000.000"), 202L, "拖车公司A",
|
||||
LocalDateTime.of(2026, 8, 10, 8, 0), LocalDateTime.of(2026, 8, 12, 8, 0), null,
|
||||
|
||||
@@ -43,6 +43,8 @@ class FeeManagementServiceTest {
|
||||
private FbaPricingRepository pricingRepository;
|
||||
@MockBean
|
||||
private FinanceFeeSourceOutboxService financeOutbox;
|
||||
@MockBean
|
||||
private WarehouseSupplierSnapshotService suppliers;
|
||||
@Resource
|
||||
private FeeItemService feeItems;
|
||||
@Resource
|
||||
@@ -96,6 +98,8 @@ class FeeManagementServiceTest {
|
||||
assertThat(created.getId()).isEqualTo(11L);
|
||||
assertThat(created.getAllocationFee()).isTrue();
|
||||
assertThat(created.getWarehouseReceivable()).isTrue();
|
||||
assertThat(created.getWarehousePayable()).isTrue();
|
||||
assertThat(created.getSupplierCategory()).isNull();
|
||||
assertThat(created.getFeeCode()).startsWith("MANUAL_");
|
||||
assertThat(created.getTenantId()).isZero();
|
||||
assertThat(created.getCreatedByName()).isEqualTo("admin");
|
||||
@@ -327,9 +331,13 @@ class FeeManagementServiceTest {
|
||||
pending.setId(31L);
|
||||
pending.setTenantId(10001L);
|
||||
pending.setInboundId(21L);
|
||||
pending.setFeeItemId(11L);
|
||||
pending.setDirection("RECEIVABLE");
|
||||
pending.setSourceType("MANUAL");
|
||||
pending.setSourceFeeVersion(1);
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(pending);
|
||||
when(repository.findItem(11L)).thenReturn(item(true, true));
|
||||
when(repository.updateDetail(pending)).thenReturn(1);
|
||||
when(repository.review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
details.approve(10001L, 21L, 31L, 9L, "admin");
|
||||
@@ -347,9 +355,12 @@ class FeeManagementServiceTest {
|
||||
approved.setTenantId(10001L);
|
||||
approved.setInboundId(21L);
|
||||
approved.setFeeItemId(11L);
|
||||
approved.setDirection("RECEIVABLE");
|
||||
approved.setSourceType("MANUAL");
|
||||
approved.setSourceFeeVersion(1);
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(approved);
|
||||
when(repository.findItem(11L)).thenReturn(item(true, true));
|
||||
when(repository.updateDetail(approved)).thenReturn(1);
|
||||
when(repository.review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
details.approve(10001L, 21L, 31L, 9L, "admin");
|
||||
|
||||
@@ -59,6 +59,7 @@ class TruckAppointmentServiceTest {
|
||||
"Canonical Customer", "ACTIVE", null, null));
|
||||
when(suppliers.trucking(any(), any())).thenReturn(
|
||||
new WarehouseSupplierClient.Snapshot(8L, "Canonical Carrier", "TRUCKING", "ACTIVE"));
|
||||
lenient().when(repository.attachments(anyLong())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test void detailResolvesStableNamesAndIdentifiersFromLinkedResources() {
|
||||
@@ -100,7 +101,7 @@ class TruckAppointmentServiceTest {
|
||||
when(request.loadingPlanId()).thenReturn(null);
|
||||
when(request.warehouseId()).thenReturn(null);
|
||||
|
||||
service.create(7L, "admin", request);
|
||||
TruckAppointmentEntity created = service.create(7L, "admin", request);
|
||||
|
||||
ArgumentCaptor<TruckAppointmentEntity> entity = ArgumentCaptor.forClass(TruckAppointmentEntity.class);
|
||||
verify(repository).insert(entity.capture());
|
||||
@@ -110,6 +111,7 @@ class TruckAppointmentServiceTest {
|
||||
assertThat(entity.getValue().getCustomerName()).isEqualTo("Canonical Customer");
|
||||
assertThat(entity.getValue().getCarrierName()).isEqualTo("Canonical Carrier");
|
||||
assertThat(entity.getValue().getAppointmentNo()).isEqualTo("TA-260818-0001");
|
||||
assertThat(created.getAttachmentCount()).isZero();
|
||||
verifyNoInteractions(oceanBookings, containers, loadingPlans);
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -42,6 +42,16 @@ class WarehouseSupplierSnapshotServiceTest {
|
||||
verify(client).get("internal-token", 7L, true, "BOOKING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void feeWithoutCategoryAcceptsAnyActiveWarehouseSupplier() {
|
||||
when(client.get("internal-token", 8L, true, null))
|
||||
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(
|
||||
8L, "任意启用供应商", "CERTIFICATE", "ACTIVE")));
|
||||
|
||||
assertThat(service.fee(8L, null).name()).isEqualTo("任意启用供应商");
|
||||
verify(client).get("internal-token", 8L, true, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsClientNameThatDiffersFromWarehouseSnapshot() {
|
||||
when(client.get(anyString(), eq(9L), eq(true), eq("CUSTOMS")))
|
||||
|
||||
Reference in New Issue
Block a user