优化
This commit is contained in:
+7
-4
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -27,17 +28,19 @@ public class InternalFinanceFeeSourceController {
|
||||
@GetMapping("/inbounds/{inboundId}/fees/{feeId}/source-snapshot")
|
||||
public ApiResponse<FinanceFeeSourceSnapshot> inbound(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@PathVariable long inboundId, @PathVariable long feeId) {
|
||||
@PathVariable long inboundId, @PathVariable long feeId,
|
||||
@RequestParam @jakarta.validation.constraints.Min(1) int sourceVersion) {
|
||||
authenticate(token);
|
||||
return ApiResponse.success(service.inbound(inboundId, feeId));
|
||||
return ApiResponse.success(service.inbound(inboundId, feeId, sourceVersion));
|
||||
}
|
||||
|
||||
@GetMapping("/containers/{containerId}/fees/{feeId}/source-snapshot")
|
||||
public ApiResponse<FinanceFeeSourceSnapshot> container(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@PathVariable long containerId, @PathVariable long feeId) {
|
||||
@PathVariable long containerId, @PathVariable long feeId,
|
||||
@RequestParam @jakarta.validation.constraints.Min(1) int sourceVersion) {
|
||||
authenticate(token);
|
||||
return ApiResponse.success(service.container(containerId, feeId));
|
||||
return ApiResponse.success(service.container(containerId, feeId, sourceVersion));
|
||||
}
|
||||
|
||||
private void authenticate(String token) {
|
||||
|
||||
@@ -20,9 +20,7 @@ public final class ContainerFeeDetailDtos {
|
||||
@NotNull LocalDate businessDate,
|
||||
@NotNull @DecimalMin("0.00") BigDecimal amount,
|
||||
@NotBlank @Pattern(regexp = "CNY") String currency,
|
||||
@NotNull Boolean settlementIncluded,
|
||||
Long partnerId,
|
||||
@Size(max = 200) String partnerName,
|
||||
Long supplierId,
|
||||
@Size(max = 500) String remark
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public final class FeeItemDtos {
|
||||
@NotBlank @Size(max = 64) String category,
|
||||
@NotBlank @Pattern(regexp = "DOMESTIC|INTERNATIONAL") String tradeScope,
|
||||
@Size(max = 50) @Pattern(regexp = "[A-Za-z0-9_-]{1,50}") String feeCode,
|
||||
@Pattern(regexp = "BOOKING|CUSTOMS|TRUCKING|CERTIFICATE") String supplierCategory,
|
||||
@NotBlank @Size(max = 128) String feeTypeName,
|
||||
@NotNull Boolean pickupPayable,
|
||||
@NotNull Boolean pickupReceivable,
|
||||
@@ -22,12 +23,22 @@ public final class FeeItemDtos {
|
||||
@NotNull Boolean allocationFee,
|
||||
@Size(max = 500) String description
|
||||
) {
|
||||
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, feeTypeName, pickupPayable, pickupReceivable,
|
||||
this(category, tradeScope, null, null, feeTypeName, pickupPayable, pickupReceivable,
|
||||
warehousePayable, warehouseReceivable, allocationFee, description);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ public record FinanceFeeSourceSnapshot(
|
||||
String containerNo,
|
||||
long customerId,
|
||||
String customerName,
|
||||
Long partnerId,
|
||||
String partnerName,
|
||||
Long supplierId,
|
||||
String supplierName,
|
||||
String supplierCategory,
|
||||
Long feeItemId,
|
||||
String feeItemCode,
|
||||
String feeItemName,
|
||||
@@ -29,7 +30,6 @@ public record FinanceFeeSourceSnapshot(
|
||||
BigDecimal totalAmount,
|
||||
String reviewStatus,
|
||||
String publicationStatus,
|
||||
boolean settlementIncluded,
|
||||
LocalDateTime publishedAt,
|
||||
BigDecimal chargeableQuantity,
|
||||
String chargeUnit,
|
||||
|
||||
@@ -21,15 +21,13 @@ public final class InboundFeeDetailDtos {
|
||||
@NotNull @DecimalMin("0.00") BigDecimal unitPrice,
|
||||
@NotBlank @Pattern(regexp = "[A-Z]{3}") String currency,
|
||||
@NotNull @DecimalMin("0.00") BigDecimal amount,
|
||||
@NotNull Boolean settlementIncluded,
|
||||
Long partnerId,
|
||||
@Size(max = 200) String partnerName,
|
||||
Long supplierId,
|
||||
@Size(max = 500) String remark
|
||||
) {
|
||||
public SaveRequest(Long feeItemId, String direction, LocalDate businessDate,
|
||||
BigDecimal unitPrice, String currency, BigDecimal amount,
|
||||
String remark) {
|
||||
this(feeItemId, direction, businessDate, unitPrice, currency, amount, false, null, null, remark);
|
||||
this(feeItemId, direction, businessDate, unitPrice, currency, amount, null, remark);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
|
||||
BigDecimal chargeableQuantity, String chargeUnit,
|
||||
String feeTypeName, BigDecimal unitPrice, String currency, BigDecimal amount,
|
||||
String reviewStatus, String publicationStatus,
|
||||
LocalDateTime publishedAt, LocalDateTime inboundedAt, Boolean settlementIncluded
|
||||
LocalDateTime publishedAt, LocalDateTime inboundedAt
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@ public class ContainerFeeDetailEntity {
|
||||
private String feeItemCodeSnapshot;
|
||||
private String feeItemNameSnapshot;
|
||||
private String direction;
|
||||
private Long partnerId;
|
||||
private String partnerName;
|
||||
private Long supplierId;
|
||||
private String supplierName;
|
||||
private String supplierCategory;
|
||||
private LocalDate businessDate;
|
||||
private BigDecimal amount;
|
||||
private String currency;
|
||||
private Boolean settlementIncluded;
|
||||
private String reviewStatus;
|
||||
private String sourceStatus;
|
||||
private Integer sourceVersion;
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 柜次费用审核发布时固化的不可变版本快照。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_container_fee_source_snapshot")
|
||||
public class ContainerFeeSourceSnapshotEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long sourceFeeId;
|
||||
private Integer sourceVersion;
|
||||
private Long tenantId;
|
||||
private Long loadingPlanId;
|
||||
private Long containerBookingId;
|
||||
private String containerNo;
|
||||
private Long feeItemId;
|
||||
private String feeItemCode;
|
||||
private String feeItemName;
|
||||
private String direction;
|
||||
private Long supplierId;
|
||||
private String supplierName;
|
||||
private String supplierCategory;
|
||||
private LocalDate businessDate;
|
||||
private BigDecimal amount;
|
||||
private String currency;
|
||||
private String reviewStatus;
|
||||
private LocalDateTime reviewedAt;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ public class FeeItemEntity extends BaseEntity {
|
||||
private String category;
|
||||
private String tradeScope;
|
||||
private String feeCode;
|
||||
private String supplierCategory;
|
||||
private String feeTypeName;
|
||||
private Boolean pickupPayable;
|
||||
private Boolean pickupReceivable;
|
||||
@@ -28,4 +29,5 @@ public class FeeItemEntity extends BaseEntity {
|
||||
private String status;
|
||||
private String createdByName;
|
||||
private String updatedByName;
|
||||
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ public class InboundFeeDetailEntity extends BaseEntity {
|
||||
private String sourceFeeNo;
|
||||
private String inboundCode;
|
||||
private String direction;
|
||||
private Long partnerId;
|
||||
private String partnerName;
|
||||
private Long supplierId;
|
||||
private String supplierName;
|
||||
private String supplierCategory;
|
||||
private LocalDate businessDate;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
@@ -62,5 +63,4 @@ public class InboundFeeDetailEntity extends BaseEntity {
|
||||
private String publishedByName;
|
||||
private LocalDateTime publishedAt;
|
||||
private LocalDateTime inboundedAt;
|
||||
private Boolean settlementIncluded;
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeSourceSnapshotEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContainerFeeSourceSnapshotMapper extends BaseMapper<ContainerFeeSourceSnapshotEntity> {
|
||||
}
|
||||
+3
-1
@@ -15,6 +15,7 @@ import com.baiyue.firstleg.repository.PriceBookQueryRepository;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelTaxModeRow;
|
||||
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
|
||||
import com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -42,6 +43,7 @@ public class OceanBookingService {
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private WarehouseSupplierSnapshotService suppliers;
|
||||
@Resource private WarehouseMasterDataClient warehouses;
|
||||
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
|
||||
|
||||
@@ -80,7 +82,7 @@ public class OceanBookingService {
|
||||
ResolvedDraft resolved = resolveDraft(request.data());
|
||||
OceanBookingEntity value = new OceanBookingEntity();
|
||||
value.setId(IdWorker.getId());
|
||||
value.setBookingNo("OB" + value.getId());
|
||||
value.setBookingNo(documentNumbers.next("OB"));
|
||||
value.setStatus(OceanBookingStatus.DRAFT.name());
|
||||
copy(value, request.data(), resolved);
|
||||
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
|
||||
+16
@@ -42,6 +42,22 @@ public class ContainerFeeDetailRepository {
|
||||
mapper.insert(value);
|
||||
}
|
||||
|
||||
public int updateSupplierSnapshot(long tenantId, long loadingPlanId, long feeId,
|
||||
Long supplierId, String supplierName,
|
||||
String supplierCategory, long operatorId) {
|
||||
return mapper.update(null, new LambdaUpdateWrapper<ContainerFeeDetailEntity>()
|
||||
.eq(ContainerFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId)
|
||||
.eq(ContainerFeeDetailEntity::getId, feeId)
|
||||
.eq(ContainerFeeDetailEntity::getReviewStatus, "PENDING")
|
||||
.eq(ContainerFeeDetailEntity::getSourceStatus, "ACTIVE")
|
||||
.set(ContainerFeeDetailEntity::getSupplierId, supplierId)
|
||||
.set(ContainerFeeDetailEntity::getSupplierName, supplierName)
|
||||
.set(ContainerFeeDetailEntity::getSupplierCategory, supplierCategory)
|
||||
.set(ContainerFeeDetailEntity::getUpdatedBy, operatorId)
|
||||
.set(ContainerFeeDetailEntity::getUpdatedAt, LocalDateTime.now()));
|
||||
}
|
||||
|
||||
public int review(long tenantId, long loadingPlanId, long feeId, String status,
|
||||
long reviewerId, String reviewerName, String reason) {
|
||||
return mapper.update(null, new LambdaUpdateWrapper<ContainerFeeDetailEntity>()
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeSourceSnapshotEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerFeeSourceSnapshotMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class ContainerFeeSourceSnapshotRepository {
|
||||
private final ContainerFeeSourceSnapshotMapper mapper;
|
||||
|
||||
public void saveApproved(ContainerFeeDetailEntity source) {
|
||||
ContainerFeeSourceSnapshotEntity value = new ContainerFeeSourceSnapshotEntity();
|
||||
value.setSourceFeeId(source.getId());
|
||||
value.setSourceVersion(source.getSourceVersion());
|
||||
value.setTenantId(source.getTenantId());
|
||||
value.setLoadingPlanId(source.getLoadingPlanId());
|
||||
value.setContainerBookingId(source.getContainerBookingId());
|
||||
value.setContainerNo(source.getContainerNo());
|
||||
value.setFeeItemId(source.getFeeItemId());
|
||||
value.setFeeItemCode(source.getFeeItemCodeSnapshot());
|
||||
value.setFeeItemName(source.getFeeItemNameSnapshot());
|
||||
value.setDirection(source.getDirection());
|
||||
value.setSupplierId(source.getSupplierId());
|
||||
value.setSupplierName(source.getSupplierName());
|
||||
value.setSupplierCategory(source.getSupplierCategory());
|
||||
value.setBusinessDate(source.getBusinessDate());
|
||||
value.setAmount(source.getAmount());
|
||||
value.setCurrency(source.getCurrency());
|
||||
value.setReviewStatus(source.getReviewStatus());
|
||||
value.setReviewedAt(source.getReviewedAt());
|
||||
value.setCreatedAt(LocalDateTime.now());
|
||||
mapper.insert(value);
|
||||
}
|
||||
|
||||
public ContainerFeeSourceSnapshotEntity find(long loadingPlanId, long sourceFeeId,
|
||||
int sourceVersion) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<ContainerFeeSourceSnapshotEntity>()
|
||||
.eq(ContainerFeeSourceSnapshotEntity::getLoadingPlanId, loadingPlanId)
|
||||
.eq(ContainerFeeSourceSnapshotEntity::getSourceFeeId, sourceFeeId)
|
||||
.eq(ContainerFeeSourceSnapshotEntity::getSourceVersion, sourceVersion));
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,15 @@ public class FeeItemRepository {
|
||||
.eq(InboundFeeDetailEntity::getId, detailId));
|
||||
}
|
||||
|
||||
public InboundFeeDetailEntity findSourceVersion(long inboundId, long sourceFeeId,
|
||||
int sourceVersion) {
|
||||
return feeDetails.selectOne(new LambdaQueryWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getInboundId, inboundId)
|
||||
.eq(InboundFeeDetailEntity::getSourceFeeVersion, sourceVersion)
|
||||
.and(scope -> scope.eq(InboundFeeDetailEntity::getId, sourceFeeId)
|
||||
.or().eq(InboundFeeDetailEntity::getSourceFeeId, sourceFeeId)));
|
||||
}
|
||||
|
||||
public InboundFeeDetailEntity findBySourceFee(String sourceType, long sourceFeeId) {
|
||||
return feeDetails.selectOne(new LambdaQueryWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getSourceType, sourceType)
|
||||
@@ -166,7 +175,6 @@ public class FeeItemRepository {
|
||||
.in(InboundFeeDetailEntity::getPublicationStatus, List.of("DRAFT", "PUBLISHED"))
|
||||
.ne(InboundFeeDetailEntity::getSourceFeeId, retainedSourceFeeId)
|
||||
.set(InboundFeeDetailEntity::getPublicationStatus, "SUPERSEDED")
|
||||
.set(InboundFeeDetailEntity::getSettlementIncluded, false)
|
||||
.set(InboundFeeDetailEntity::getUpdatedBy, operatorId)
|
||||
.set(InboundFeeDetailEntity::getUpdatedAt, now));
|
||||
}
|
||||
@@ -186,7 +194,6 @@ public class FeeItemRepository {
|
||||
.set(InboundFeeDetailEntity::getPublishedBy, operatorId)
|
||||
.set(InboundFeeDetailEntity::getPublishedByName, operatorName)
|
||||
.set(InboundFeeDetailEntity::getPublishedAt, now)
|
||||
.set(InboundFeeDetailEntity::getSettlementIncluded, true)
|
||||
.set(InboundFeeDetailEntity::getUpdatedBy, operatorId)
|
||||
.set(InboundFeeDetailEntity::getUpdatedAt, now));
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -27,6 +27,8 @@ public class ContainerFeeDetailService {
|
||||
private final SmartLoadingRepository loading;
|
||||
private final FeeItemRepository feeItems;
|
||||
private final FinanceFeeSourceOutboxService financeOutbox;
|
||||
private final WarehouseSupplierSnapshotService suppliers;
|
||||
private final ContainerFeeSourceSnapshotRepository sourceSnapshots;
|
||||
|
||||
public List<ContainerFeeDetailEntity> list(long tenantId, long loadingPlanId) {
|
||||
requireOwnedPlan(tenantId, loadingPlanId);
|
||||
@@ -56,18 +58,19 @@ public class ContainerFeeDetailService {
|
||||
@Transactional
|
||||
public void approve(long tenantId, long loadingPlanId, long feeId,
|
||||
long reviewerId, String reviewerName) {
|
||||
required(tenantId, loadingPlanId, feeId);
|
||||
ContainerFeeDetailEntity pending = required(tenantId, loadingPlanId, feeId);
|
||||
FeeItemEntity item = requireItem(pending.getFeeItemId(), pending.getDirection());
|
||||
refreshSupplierSnapshot(pending, item, reviewerId);
|
||||
if (repository.review(tenantId, loadingPlanId, feeId, "APPROVED",
|
||||
reviewerId, reviewerName, null) != 1) {
|
||||
throw conflict("只有待审核柜次费用可以审核通过");
|
||||
}
|
||||
ContainerFeeDetailEntity approved = required(tenantId, loadingPlanId, feeId);
|
||||
if (Boolean.TRUE.equals(approved.getSettlementIncluded())) {
|
||||
String eventType = approved.getSourceVersion() > 1
|
||||
? FinanceFeeSourceOutboxService.ADJUSTED : FinanceFeeSourceOutboxService.PUBLISHED;
|
||||
financeOutbox.publishContainer(eventType, tenantId, loadingPlanId, feeId,
|
||||
approved.getSourceVersion(), LocalDateTime.now());
|
||||
}
|
||||
sourceSnapshots.saveApproved(approved);
|
||||
String eventType = approved.getSourceVersion() > 1
|
||||
? FinanceFeeSourceOutboxService.ADJUSTED : FinanceFeeSourceOutboxService.PUBLISHED;
|
||||
financeOutbox.publishContainer(eventType, tenantId, loadingPlanId, feeId,
|
||||
approved.getSourceVersion(), LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -111,10 +114,8 @@ public class ContainerFeeDetailService {
|
||||
if (repository.voidApproved(tenantId, loadingPlanId, feeId, expectedVersion, operatorId) != 1) {
|
||||
throw conflict("只有当前版本的已审核柜次费用可以作废");
|
||||
}
|
||||
if (Boolean.TRUE.equals(current.getSettlementIncluded())) {
|
||||
financeOutbox.publishContainer(FinanceFeeSourceOutboxService.VOIDED, tenantId,
|
||||
loadingPlanId, feeId, expectedVersion, LocalDateTime.now());
|
||||
}
|
||||
financeOutbox.publishContainer(FinanceFeeSourceOutboxService.VOIDED, tenantId,
|
||||
loadingPlanId, feeId, expectedVersion, LocalDateTime.now());
|
||||
}
|
||||
|
||||
private PlanContext requireOwnedPlan(long tenantId, long loadingPlanId) {
|
||||
@@ -144,28 +145,49 @@ public class ContainerFeeDetailService {
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void apply(ContainerFeeDetailEntity value, FeeItemEntity item,
|
||||
SaveRequest request, long operatorId) {
|
||||
if ("PAYABLE".equals(request.direction())
|
||||
&& (request.partnerId() == null || request.partnerId() <= 0
|
||||
|| !StringUtils.hasText(request.partnerName()))) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "应付柜次费用必须选择有效往来单位");
|
||||
}
|
||||
private void apply(ContainerFeeDetailEntity value, FeeItemEntity item,
|
||||
SaveRequest request, long operatorId) {
|
||||
value.setFeeItemId(item.getId());
|
||||
value.setFeeItemCodeSnapshot(item.getFeeCode());
|
||||
value.setFeeItemNameSnapshot(item.getFeeTypeName());
|
||||
value.setDirection(request.direction());
|
||||
value.setPartnerId("PAYABLE".equals(request.direction()) ? request.partnerId() : null);
|
||||
value.setPartnerName("PAYABLE".equals(request.direction()) ? request.partnerName().trim() : null);
|
||||
applySupplierSnapshot(value, item, request.direction(), request.supplierId());
|
||||
value.setBusinessDate(request.businessDate());
|
||||
value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP));
|
||||
value.setCurrency(request.currency());
|
||||
value.setSettlementIncluded(Boolean.TRUE.equals(request.settlementIncluded()));
|
||||
value.setRemark(request.remark() == null ? null : request.remark().trim());
|
||||
value.setUpdatedBy(operatorId);
|
||||
value.setUpdatedAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
private void applySupplierSnapshot(ContainerFeeDetailEntity value, FeeItemEntity item,
|
||||
String direction, Long supplierId) {
|
||||
if (!"PAYABLE".equals(direction)) {
|
||||
value.setSupplierId(null);
|
||||
value.setSupplierName(null);
|
||||
value.setSupplierCategory(null);
|
||||
return;
|
||||
}
|
||||
if (supplierId == null || supplierId <= 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "应付柜次费用必须选择有效供应商");
|
||||
}
|
||||
String category = FeeItemSupplierCategory.requirePayable(item.getSupplierCategory());
|
||||
var snapshot = suppliers.fee(supplierId, category);
|
||||
value.setSupplierId(snapshot.supplierId());
|
||||
value.setSupplierName(snapshot.name());
|
||||
value.setSupplierCategory(snapshot.category());
|
||||
}
|
||||
|
||||
private void refreshSupplierSnapshot(ContainerFeeDetailEntity value, FeeItemEntity item,
|
||||
long operatorId) {
|
||||
applySupplierSnapshot(value, item, value.getDirection(), value.getSupplierId());
|
||||
if (repository.updateSupplierSnapshot(value.getTenantId(), value.getLoadingPlanId(),
|
||||
value.getId(), value.getSupplierId(), value.getSupplierName(),
|
||||
value.getSupplierCategory(), operatorId) != 1) {
|
||||
throw conflict("柜次费用供应商快照已变化,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
private static BizException conflict(String message) {
|
||||
return new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public class ContainerService {
|
||||
@Resource private PriceBookQueryRepository priceBooks;
|
||||
@Resource private OceanBookingRepository oceanBookings;
|
||||
@Resource private WarehouseSupplierSnapshotService suppliers;
|
||||
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Autowired(required = false) private ContainerTransportHistoryMapper transportHistories;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
|
||||
@@ -66,7 +67,7 @@ public class ContainerService {
|
||||
.filter(row -> request.channelCode().trim().equals(row.getChannelCode())).findFirst()
|
||||
.orElseThrow(() -> conflict("所选物流渠道已失效,请重新选择"));
|
||||
ContainerBookingEntity value = new ContainerBookingEntity(); value.setId(IdWorker.getId());
|
||||
value.setBookingNo("CB" + value.getId()); value.setCabinetNo("CAB-" + value.getId());
|
||||
value.setBookingNo(documentNumbers.next("CB")); value.setCabinetNo(documentNumbers.next("CAB"));
|
||||
value.setSourceType("SIMULATION"); value.setRecordStatus("ACTIVE");
|
||||
value.setChannelCode(channel.getChannelCode()); value.setChannelName(channel.getChannelName());
|
||||
value.setWarehouseId(warehouse.id()); value.setWarehouseName(warehouse.warehouseName());
|
||||
|
||||
@@ -102,10 +102,19 @@ 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()
|
||||
? value.getFeeCode() : request.feeCode().trim().toUpperCase());
|
||||
value.setSupplierCategory(payable
|
||||
? FeeItemSupplierCategory.normalize(request.supplierCategory()) : null);
|
||||
value.setFeeTypeName(request.feeTypeName().trim());
|
||||
value.setPickupPayable(request.pickupPayable());
|
||||
value.setPickupReceivable(request.pickupReceivable());
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
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");
|
||||
|
||||
private FeeItemSupplierCategory() {
|
||||
}
|
||||
|
||||
public static String normalize(String supplierCategory) {
|
||||
if (supplierCategory == null || supplierCategory.isBlank()) return null;
|
||||
String normalized = supplierCategory.trim().toUpperCase(Locale.ROOT);
|
||||
if (!ALLOWED.contains(normalized)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "供应商类别无效");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static String requirePayable(String supplierCategory) {
|
||||
String category = normalize(supplierCategory);
|
||||
if (category == null) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"费用项目未配置供应商类别,不能用于应付费用");
|
||||
}
|
||||
return category;
|
||||
}
|
||||
}
|
||||
+23
-23
@@ -3,11 +3,11 @@ package com.baiyue.firstleg.service;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeSourceSnapshotEntity;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeSourceSnapshotRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -21,11 +21,10 @@ import java.util.HexFormat;
|
||||
@RequiredArgsConstructor
|
||||
public class FinanceFeeSourceSnapshotService {
|
||||
private final FeeItemRepository inboundFees;
|
||||
private final ContainerFeeDetailRepository containerFees;
|
||||
private final ContainerFeeSourceSnapshotRepository containerSnapshots;
|
||||
|
||||
public FinanceFeeSourceSnapshot inbound(long inboundId, long feeId) {
|
||||
InboundFeeDetailEntity detail = inboundFees.findDetail(inboundId, feeId);
|
||||
if (detail == null) detail = inboundFees.findBySourceFee("FBA_PRICING", feeId);
|
||||
public FinanceFeeSourceSnapshot inbound(long inboundId, long feeId, int sourceVersion) {
|
||||
InboundFeeDetailEntity detail = inboundFees.findSourceVersion(inboundId, feeId, sourceVersion);
|
||||
if (detail == null || !Long.valueOf(inboundId).equals(detail.getInboundId())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库费用来源不存在");
|
||||
}
|
||||
@@ -34,43 +33,44 @@ public class FinanceFeeSourceSnapshotService {
|
||||
if (inbound == null || item == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库费用来源关联快照不完整");
|
||||
}
|
||||
long sourceFeeId = "FBA_PRICING".equals(detail.getSourceType())
|
||||
? detail.getSourceFeeId() : detail.getId();
|
||||
long sourceFeeId = detail.getSourceFeeId() == null ? detail.getId() : detail.getSourceFeeId();
|
||||
int version = detail.getSourceFeeVersion() == null ? 1 : detail.getSourceFeeVersion();
|
||||
String canonical = String.join("|", "INBOUND_FEE", String.valueOf(sourceFeeId),
|
||||
String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(inboundId),
|
||||
String.valueOf(inbound.getOrderId()), nullSafe(item.getFeeCode()),
|
||||
nullSafe(detail.getDirection()), detail.getAmount().toPlainString(),
|
||||
nullSafe(detail.getCurrency()), String.valueOf(detail.getSettlementIncluded()));
|
||||
nullSafe(detail.getDirection()), nullSafe(detail.getSupplierId()),
|
||||
nullSafe(detail.getSupplierName()), nullSafe(detail.getSupplierCategory()),
|
||||
detail.getAmount().toPlainString(),
|
||||
nullSafe(detail.getCurrency()));
|
||||
return new FinanceFeeSourceSnapshot(detail.getTenantId(), "INBOUND_FEE", sourceFeeId,
|
||||
version, inboundId, inbound.getInboundCode(), inbound.getOrderId(), null, null, null,
|
||||
detail.getTenantId(), inbound.getCustomerName(), detail.getPartnerId(), detail.getPartnerName(), item.getId(), item.getFeeCode(),
|
||||
detail.getTenantId(), inbound.getCustomerName(), detail.getSupplierId(),
|
||||
detail.getSupplierName(), detail.getSupplierCategory(), item.getId(), item.getFeeCode(),
|
||||
detail.getFeeTypeNameSnapshot(), detail.getDirection(), detail.getBusinessDate(),
|
||||
detail.getCurrency(), detail.getAmount(), detail.getReviewStatus(),
|
||||
detail.getPublicationStatus(), Boolean.TRUE.equals(detail.getSettlementIncluded()),
|
||||
detail.getPublicationStatus(),
|
||||
detail.getPublishedAt(), detail.getChargeableQuantity(), detail.getChargeUnit(),
|
||||
detail.getActualGrossWeightKg(), detail.getActualVolumeCbm(), sha256(canonical), 1);
|
||||
}
|
||||
|
||||
public FinanceFeeSourceSnapshot container(long containerId, long feeId) {
|
||||
ContainerFeeDetailEntity detail = containerFees.find(containerId, feeId);
|
||||
public FinanceFeeSourceSnapshot container(long containerId, long feeId, int sourceVersion) {
|
||||
ContainerFeeSourceSnapshotEntity detail = containerSnapshots.find(containerId, feeId, sourceVersion);
|
||||
if (detail == null) throw new BizException(ResultCode.NOT_FOUND, "柜次费用来源不存在");
|
||||
int version = detail.getSourceVersion();
|
||||
String publicationStatus = "VOIDED".equals(detail.getSourceStatus()) ? "VOIDED"
|
||||
: "APPROVED".equals(detail.getReviewStatus()) ? "PUBLISHED" : "DRAFT";
|
||||
String canonical = String.join("|", "CONTAINER_FEE", String.valueOf(feeId),
|
||||
String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(containerId),
|
||||
nullSafe(detail.getFeeItemCodeSnapshot()), nullSafe(detail.getDirection()),
|
||||
nullSafe(detail.getPartnerId()), nullSafe(detail.getPartnerName()),
|
||||
nullSafe(detail.getFeeItemCode()), nullSafe(detail.getDirection()),
|
||||
nullSafe(detail.getSupplierId()), nullSafe(detail.getSupplierName()),
|
||||
nullSafe(detail.getSupplierCategory()),
|
||||
detail.getAmount().toPlainString(), nullSafe(detail.getCurrency()),
|
||||
String.valueOf(detail.getSettlementIncluded()));
|
||||
nullSafe(detail.getReviewStatus()));
|
||||
return new FinanceFeeSourceSnapshot(detail.getTenantId(), "CONTAINER_FEE", feeId,
|
||||
version, null, null, null, containerId, detail.getContainerBookingId(),
|
||||
detail.getContainerNo(), detail.getTenantId(), null, detail.getPartnerId(),
|
||||
detail.getPartnerName(), detail.getFeeItemId(),
|
||||
detail.getFeeItemCodeSnapshot(), detail.getFeeItemNameSnapshot(), detail.getDirection(),
|
||||
detail.getContainerNo(), detail.getTenantId(), null, detail.getSupplierId(),
|
||||
detail.getSupplierName(), detail.getSupplierCategory(), detail.getFeeItemId(),
|
||||
detail.getFeeItemCode(), detail.getFeeItemName(), detail.getDirection(),
|
||||
detail.getBusinessDate(), detail.getCurrency(), detail.getAmount(), detail.getReviewStatus(),
|
||||
publicationStatus, Boolean.TRUE.equals(detail.getSettlementIncluded()),
|
||||
"PUBLISHED",
|
||||
detail.getReviewedAt(), null, null, null, null, sha256(canonical), 1);
|
||||
}
|
||||
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class FirstlegDocumentNumberGenerator {
|
||||
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyMMdd", Locale.ROOT);
|
||||
private static final Set<String> SUPPORTED_PREFIXES = Set.of("OB", "TA", "CB", "CAB", "LP");
|
||||
private static final long MAX_DAILY_SEQUENCE = 9_999L;
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final Clock clock;
|
||||
|
||||
public FirstlegDocumentNumberGenerator(JdbcTemplate jdbc) {
|
||||
this(jdbc, Clock.system(BUSINESS_ZONE));
|
||||
}
|
||||
|
||||
FirstlegDocumentNumberGenerator(JdbcTemplate jdbc, Clock clock) {
|
||||
this.jdbc = jdbc;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public String next(String prefix) {
|
||||
if (!SUPPORTED_PREFIXES.contains(prefix)) {
|
||||
throw new IllegalArgumentException("unsupported firstleg document prefix: " + prefix);
|
||||
}
|
||||
LocalDate sequenceDate = LocalDate.now(clock);
|
||||
String sequenceKey = prefix + ":" + DateTimeFormatter.BASIC_ISO_DATE.format(sequenceDate);
|
||||
jdbc.update(
|
||||
"INSERT INTO fl_document_sequence(sequence_key,current_value) " +
|
||||
"VALUES(?,LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE " +
|
||||
"current_value=LAST_INSERT_ID(current_value+1)",
|
||||
sequenceKey);
|
||||
Long sequence = jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
|
||||
if (sequence == null || sequence <= 0 || sequence > MAX_DAILY_SEQUENCE) {
|
||||
throw new BizException(ResultCode.CONFLICT, "当日" + prefix + "单据编号已用尽");
|
||||
}
|
||||
return prefix + "-" + DATE_FORMAT.format(sequenceDate) + "-" +
|
||||
String.format(Locale.ROOT, "%04d", sequence);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ public class InboundFeeDetailService {
|
||||
private FbaPricingRepository pricingRepository;
|
||||
@Resource
|
||||
private FinanceFeeSourceOutboxService financeOutbox;
|
||||
@Resource
|
||||
private WarehouseSupplierSnapshotService suppliers;
|
||||
|
||||
@Transactional
|
||||
public List<InboundFeeDetailEntity> list(long tenantId, long inboundId, boolean includeHistory) {
|
||||
@@ -130,7 +132,6 @@ public class InboundFeeDetailService {
|
||||
detail.setPublishedByName(operatorName);
|
||||
detail.setPublishedAt(now);
|
||||
detail.setInboundedAt(inbound.getInboundedAt());
|
||||
detail.setSettlementIncluded(true);
|
||||
detail.setCreatedBy(operatorId);
|
||||
detail.setUpdatedBy(operatorId);
|
||||
try {
|
||||
@@ -199,7 +200,7 @@ public class InboundFeeDetailService {
|
||||
detail.setRemark("来源费用单:" + fee.getFeeNo());
|
||||
detail.setReviewStatus(published ? "APPROVED" : "PENDING");
|
||||
detail.setPublicationStatus(published ? "PUBLISHED" : "DRAFT");
|
||||
detail.setSettlementIncluded(published); detail.setCreatedBy(operatorId); detail.setUpdatedBy(operatorId);
|
||||
detail.setCreatedBy(operatorId); detail.setUpdatedBy(operatorId);
|
||||
if (published) {
|
||||
detail.setReviewerId(operatorId); detail.setReviewerName(operatorName); detail.setReviewedAt(now);
|
||||
detail.setPublishedBy(operatorId); detail.setPublishedByName(operatorName); detail.setPublishedAt(now);
|
||||
@@ -252,7 +253,9 @@ public class InboundFeeDetailService {
|
||||
@Transactional
|
||||
public void approve(long tenantId, long inboundId, long detailId,
|
||||
long reviewerId, String reviewerName) {
|
||||
requiredManual(tenantId, inboundId, detailId);
|
||||
InboundFeeDetailEntity pending = requiredManual(tenantId, inboundId, detailId);
|
||||
FeeItemEntity item = requiredActiveItem(pending.getFeeItemId());
|
||||
refreshSupplierSnapshot(pending, item, reviewerId);
|
||||
if (repository.review(tenantId, inboundId, detailId, "APPROVED",
|
||||
reviewerId, reviewerName, null) != 1) {
|
||||
throw conflict("只有待审核费用明细可以审核通过");
|
||||
@@ -301,10 +304,8 @@ public class InboundFeeDetailService {
|
||||
if (repository.voidApproved(tenantId, inboundId, detailId, expectedSourceVersion, operatorId) != 1) {
|
||||
throw conflict("只有当前版本的已审核费用明细可以作废");
|
||||
}
|
||||
if (Boolean.TRUE.equals(current.getSettlementIncluded())) {
|
||||
current.setPublicationStatus("VOIDED");
|
||||
publishFinanceSource(current, "VOIDED", LocalDateTime.now());
|
||||
}
|
||||
current.setPublicationStatus("VOIDED");
|
||||
publishFinanceSource(current, "VOIDED", LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -373,14 +374,13 @@ public class InboundFeeDetailService {
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void apply(InboundFeeDetailEntity value, InboundOrderEntity inbound,
|
||||
FeeItemEntity item, SaveRequest request, long operatorId) {
|
||||
private void apply(InboundFeeDetailEntity value, InboundOrderEntity inbound,
|
||||
FeeItemEntity item, SaveRequest request, long operatorId) {
|
||||
ensureWarehouseApplicable(item, request.direction());
|
||||
value.setFeeItemId(item.getId());
|
||||
value.setInboundCode(inbound.getInboundCode());
|
||||
value.setDirection(request.direction());
|
||||
value.setPartnerId("PAYABLE".equals(request.direction()) ? request.partnerId() : null);
|
||||
value.setPartnerName("PAYABLE".equals(request.direction()) ? request.partnerName() : null);
|
||||
applySupplierSnapshot(value, item, request.direction(), request.supplierId());
|
||||
value.setBusinessDate(request.businessDate());
|
||||
value.setFeeCategorySnapshot(item.getCategory());
|
||||
value.setTradeScopeSnapshot(item.getTradeScope());
|
||||
@@ -399,18 +399,43 @@ public class InboundFeeDetailService {
|
||||
value.setUnitPrice(request.unitPrice().setScale(4, RoundingMode.HALF_UP));
|
||||
value.setCurrency(request.currency());
|
||||
value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP));
|
||||
value.setSettlementIncluded(Boolean.TRUE.equals(request.settlementIncluded()));
|
||||
value.setRemark(request.remark() == null ? null : request.remark().trim());
|
||||
value.setUpdatedBy(operatorId);
|
||||
}
|
||||
|
||||
private void applySupplierSnapshot(InboundFeeDetailEntity value, FeeItemEntity item,
|
||||
String direction, Long supplierId) {
|
||||
if (!"PAYABLE".equals(direction)) {
|
||||
value.setSupplierId(null);
|
||||
value.setSupplierName(null);
|
||||
value.setSupplierCategory(null);
|
||||
return;
|
||||
}
|
||||
if (supplierId == null || supplierId <= 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "应付入库费用必须选择有效供应商");
|
||||
}
|
||||
String category = FeeItemSupplierCategory.requirePayable(item.getSupplierCategory());
|
||||
var snapshot = suppliers.fee(supplierId, category);
|
||||
value.setSupplierId(snapshot.supplierId());
|
||||
value.setSupplierName(snapshot.name());
|
||||
value.setSupplierCategory(snapshot.category());
|
||||
}
|
||||
|
||||
private void refreshSupplierSnapshot(InboundFeeDetailEntity value, FeeItemEntity item,
|
||||
long operatorId) {
|
||||
applySupplierSnapshot(value, item, value.getDirection(), value.getSupplierId());
|
||||
value.setUpdatedBy(operatorId);
|
||||
if (repository.updateDetail(value) != 1) {
|
||||
throw conflict("费用明细供应商快照已变化,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
private static BizException conflict(String message) {
|
||||
return new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
|
||||
private void publishFinanceSource(InboundFeeDetailEntity detail, String status,
|
||||
LocalDateTime occurredAt) {
|
||||
if (!Boolean.TRUE.equals(detail.getSettlementIncluded())) return;
|
||||
if ("FBA_PRICING".equals(detail.getSourceType())) return;
|
||||
int version = detail.getSourceFeeVersion() == null ? 1 : detail.getSourceFeeVersion();
|
||||
String eventType = "VOIDED".equals(status) ? FinanceFeeSourceOutboxService.VOIDED
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ public class MerchantInboundBillingService {
|
||||
detail.getChargeableVolumeCbm(), detail.getChargeableQuantity(), detail.getChargeUnit(),
|
||||
detail.getFeeTypeNameSnapshot(), detail.getUnitPrice(), detail.getCurrency(), detail.getAmount(),
|
||||
detail.getReviewStatus(), detail.getPublicationStatus(), detail.getPublishedAt(),
|
||||
detail.getInboundedAt(), detail.getSettlementIncluded());
|
||||
detail.getInboundedAt());
|
||||
}
|
||||
|
||||
public record ImageDownload(byte[] content, String contentType) {
|
||||
|
||||
@@ -39,6 +39,7 @@ public class SmartLoadingService {
|
||||
@Resource private LoadingPickService loadingPickService;
|
||||
@Resource private LoadingModelBridgeService loadingModelBridge;
|
||||
@Resource private SplitPreloadService splitPreloadService;
|
||||
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
public SmartLoadingService() { }
|
||||
@@ -77,7 +78,7 @@ public class SmartLoadingService {
|
||||
LoadingPlanEntity existing = repository.planByBooking(bookingId);
|
||||
if (existing != null) return draftView(existing, booking);
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setPlanNo("LP-" + UUID.randomUUID().toString().replace("-", "").substring(0, 20));
|
||||
plan.setPlanNo(documentNumbers.next("LP"));
|
||||
plan.setContainerBookingId(bookingId); plan.setPlanStatus("EDITING"); plan.setRemark(trim(remark));
|
||||
plan.setCreatedBy(operatorId); plan.setCreatedByName(operatorName); plan.setUpdatedBy(operatorId);
|
||||
plan.setUpdatedByName(operatorName); plan.setVersion(0);
|
||||
|
||||
@@ -40,6 +40,7 @@ public class TruckAppointmentService {
|
||||
@Resource TruckAppointmentFileClient files;
|
||||
@Resource WarehouseSupplierSnapshotService suppliers;
|
||||
@Resource IamCustomerSnapshotService customers;
|
||||
@Resource FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Resource ObjectMapper objectMapper;
|
||||
@Value("${baiyue.security.internal-service-token}") String internalToken;
|
||||
public PageResult<TruckAppointmentEntity> page(Query q){return repo.page(q);} public TruckAppointmentEntity detail(long id){return enrich(required(id));}
|
||||
@@ -47,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("TA"+e.getId()); 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 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);}
|
||||
|
||||
+8
@@ -26,6 +26,14 @@ public class WarehouseSupplierSnapshotService {
|
||||
return resolve(supplierId, suppliedName, "CUSTOMS");
|
||||
}
|
||||
|
||||
/** 费用来源只提交供应商 ID,名称与类别均以 Warehouse 返回快照为准。 */
|
||||
public WarehouseSupplierClient.Snapshot fee(Long supplierId, String category) {
|
||||
if (!StringUtils.hasText(category)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "费用项目未配置供应商类别");
|
||||
}
|
||||
return resolve(supplierId, null, category);
|
||||
}
|
||||
|
||||
private WarehouseSupplierClient.Snapshot resolve(Long supplierId, String suppliedName,
|
||||
String category) {
|
||||
if (supplierId == null || supplierId <= 0) {
|
||||
|
||||
@@ -71,12 +71,12 @@ CREATE TABLE IF NOT EXISTS fl_container_fee_detail (
|
||||
fee_item_code_snapshot VARCHAR(50) NOT NULL COMMENT '费用项目稳定编码快照',
|
||||
fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照',
|
||||
direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付',
|
||||
partner_id BIGINT UNSIGNED NULL COMMENT '应付往来单位ID快照;应收为空',
|
||||
partner_name VARCHAR(200) NULL COMMENT '应付往来单位名称快照;应收为空',
|
||||
supplier_id BIGINT UNSIGNED NULL COMMENT '应付Warehouse供应商ID快照;应收为空',
|
||||
supplier_name VARCHAR(200) NULL COMMENT '应付Warehouse供应商名称快照;应收为空',
|
||||
supplier_category VARCHAR(16) NULL COMMENT '费用项目供应商类别快照;应收为空',
|
||||
business_date DATE NOT NULL COMMENT '费用业务日期',
|
||||
amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额',
|
||||
currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种,首期固定CNY',
|
||||
settlement_included TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否参与财务结算来源池',
|
||||
review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED通过、REJECTED驳回',
|
||||
source_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '来源状态:ACTIVE有效、VOIDED作废',
|
||||
source_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用来源版本;调整递增,作废携带当前版本',
|
||||
@@ -91,22 +91,46 @@ CREATE TABLE IF NOT EXISTS fl_container_fee_detail (
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_container_fee_customer_plan (tenant_id, loading_plan_id, source_status),
|
||||
KEY idx_container_fee_settlement (tenant_id, review_status, settlement_included, source_status),
|
||||
KEY idx_container_fee_review (tenant_id, review_status, source_status),
|
||||
CONSTRAINT chk_container_fee_direction CHECK (direction IN ('RECEIVABLE', 'PAYABLE')),
|
||||
CONSTRAINT chk_container_fee_currency CHECK (currency = 'CNY'),
|
||||
CONSTRAINT chk_container_fee_amount CHECK (amount >= 0),
|
||||
CONSTRAINT chk_container_fee_review CHECK (review_status IN ('PENDING', 'APPROVED', 'REJECTED')),
|
||||
CONSTRAINT chk_container_fee_source CHECK (source_status IN ('ACTIVE', 'VOIDED'))
|
||||
) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过且参与结算后同步Finance';
|
||||
) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过后自动同步Finance';
|
||||
|
||||
CALL firstleg_finance_add_column('fl_container_fee_detail', 'partner_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '应付往来单位ID快照;应收为空' AFTER direction");
|
||||
CALL firstleg_finance_add_column('fl_container_fee_detail', 'partner_name',
|
||||
"VARCHAR(200) NULL COMMENT '应付往来单位名称快照;应收为空' AFTER partner_id");
|
||||
CALL firstleg_finance_add_column('fl_inbound_fee_detail', 'partner_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '应付往来单位ID快照;应收为空' AFTER direction");
|
||||
CALL firstleg_finance_add_column('fl_inbound_fee_detail', 'partner_name',
|
||||
"VARCHAR(200) NULL COMMENT '应付往来单位名称快照;应收为空' AFTER partner_id");
|
||||
CREATE TABLE IF NOT EXISTS fl_container_fee_source_snapshot (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
source_fee_id BIGINT UNSIGNED NOT NULL,
|
||||
source_version INT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
loading_plan_id BIGINT UNSIGNED NOT NULL,
|
||||
container_booking_id BIGINT UNSIGNED NULL,
|
||||
container_no VARCHAR(64) NULL,
|
||||
fee_item_id BIGINT UNSIGNED NOT NULL,
|
||||
fee_item_code VARCHAR(50) NOT NULL,
|
||||
fee_item_name VARCHAR(128) NOT NULL,
|
||||
direction VARCHAR(16) NOT NULL,
|
||||
supplier_id BIGINT UNSIGNED NULL,
|
||||
supplier_name VARCHAR(200) NULL,
|
||||
supplier_category VARCHAR(16) NULL,
|
||||
business_date DATE NOT NULL,
|
||||
amount DECIMAL(18,2) NOT NULL,
|
||||
currency CHAR(3) NOT NULL DEFAULT 'CNY',
|
||||
review_status VARCHAR(16) NOT NULL,
|
||||
reviewed_at DATETIME(3) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_container_fee_source_version (loading_plan_id, source_fee_id, source_version),
|
||||
KEY idx_container_fee_snapshot_tenant (tenant_id, loading_plan_id)
|
||||
) ENGINE=InnoDB COMMENT='柜次费用不可变版本快照';
|
||||
|
||||
CALL firstleg_finance_add_column('fl_container_fee_detail', 'supplier_id', "BIGINT UNSIGNED NULL COMMENT '应付Warehouse供应商ID快照;应收为空' AFTER direction");
|
||||
CALL firstleg_finance_add_column('fl_container_fee_detail', 'supplier_name', "VARCHAR(200) NULL COMMENT '应付Warehouse供应商名称快照;应收为空' AFTER supplier_id");
|
||||
CALL firstleg_finance_add_column('fl_container_fee_detail', 'supplier_category', "VARCHAR(16) NULL COMMENT '费用项目供应商类别快照;应收为空' AFTER supplier_name");
|
||||
CALL firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_id', "BIGINT UNSIGNED NULL COMMENT '应付Warehouse供应商ID快照;应收为空' AFTER direction");
|
||||
CALL firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_name', "VARCHAR(200) NULL COMMENT '应付Warehouse供应商名称快照;应收为空' AFTER supplier_id");
|
||||
CALL firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_category', "VARCHAR(16) NULL COMMENT '费用项目供应商类别快照;应收为空' AFTER supplier_name");
|
||||
DROP PROCEDURE IF EXISTS firstleg_finance_add_column;
|
||||
|
||||
DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema;
|
||||
@@ -125,13 +149,13 @@ BEGIN
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail'
|
||||
AND column_name IN ('partner_id', 'partner_name')) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用往来单位快照字段不完整';
|
||||
AND column_name IN ('supplier_id', 'supplier_name', 'supplier_category')) <> 3 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用供应商快照字段不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_fee_detail'
|
||||
AND column_name IN ('partner_id', 'partner_name')) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:入库费用往来单位快照字段不完整';
|
||||
AND column_name IN ('supplier_id', 'supplier_name', 'supplier_category')) <> 3 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:入库费用供应商快照字段不完整';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
@@ -7,6 +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供应商类别;未配置则失败关闭',
|
||||
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 '提柜应收是否参与',
|
||||
@@ -36,8 +37,9 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail (
|
||||
fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '绑定费用项目ID',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
||||
direction VARCHAR(16) NOT NULL COMMENT 'RECEIVABLE应收、PAYABLE应付',
|
||||
partner_id BIGINT UNSIGNED NULL COMMENT '应付往来单位ID快照;应收为空',
|
||||
partner_name VARCHAR(200) NULL COMMENT '应付往来单位名称快照;应收为空',
|
||||
supplier_id BIGINT UNSIGNED NULL COMMENT '应付Warehouse供应商ID快照;应收为空',
|
||||
supplier_name VARCHAR(200) NULL COMMENT '应付Warehouse供应商名称快照;应收为空',
|
||||
supplier_category VARCHAR(16) NULL COMMENT '费用项目供应商类别快照;应收为空',
|
||||
business_date DATE NOT NULL COMMENT '业务发生日',
|
||||
fee_category_snapshot VARCHAR(64) NOT NULL COMMENT '费用分类快照',
|
||||
trade_scope_snapshot VARCHAR(20) NOT NULL COMMENT '国内国际快照',
|
||||
|
||||
@@ -22,6 +22,8 @@ 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");
|
||||
ALTER TABLE fl_fee_item
|
||||
MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id',
|
||||
COMMENT = '平台公共费用项目主数据';
|
||||
@@ -431,9 +433,6 @@ CALL phase2_publish_add_column('fl_inbound_fee_detail', 'published_at',
|
||||
"DATETIME(3) NULL COMMENT '发布时间' AFTER published_by_name");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'inbounded_at',
|
||||
"DATETIME(3) NULL COMMENT '入库完成时间快照' AFTER published_at");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'settlement_included',
|
||||
"TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否参与结算' AFTER inbounded_at");
|
||||
|
||||
DROP PROCEDURE IF EXISTS phase2_publish_add_column;
|
||||
|
||||
DROP PROCEDURE IF EXISTS phase2_publish_add_index;
|
||||
|
||||
@@ -26,6 +26,13 @@ DELIMITER ;
|
||||
CALL phase3_add_allocation_picked_count();
|
||||
DROP PROCEDURE phase3_add_allocation_picked_count;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_document_sequence (
|
||||
sequence_key VARCHAR(32) NOT NULL COMMENT '单据类型与北京时间日期组合键,例如CAB:20260818',
|
||||
current_value INT UNSIGNED NOT NULL COMMENT '该类型当日已分配的最大序号',
|
||||
PRIMARY KEY (sequence_key),
|
||||
CONSTRAINT chk_fl_document_sequence_value CHECK (current_value > 0)
|
||||
) ENGINE=InnoDB COMMENT='头程履约业务单号日序列;生成YYMMDD加四位当日序号';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_container_booking (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID(雪花ID)',
|
||||
booking_no VARCHAR(40) NOT NULL COMMENT '平台唯一货柜预订单号',
|
||||
|
||||
@@ -295,7 +295,7 @@ components:
|
||||
nextProcessStatus: {type: string, nullable: true, description: 费用动作后的入库主流程状态;审核完成后保持 WAIT_SPLIT}
|
||||
InboundFeeDetailSaveRequest:
|
||||
type: object
|
||||
required: [feeItemId, direction, businessDate, unitPrice, currency, amount, settlementIncluded]
|
||||
required: [feeItemId, direction, businessDate, unitPrice, currency, amount]
|
||||
properties:
|
||||
feeItemId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
direction: {type: string, enum: [RECEIVABLE, PAYABLE]}
|
||||
@@ -303,9 +303,9 @@ components:
|
||||
unitPrice: {type: number, minimum: 0}
|
||||
currency: {type: string, pattern: '^[A-Z]{3}$'}
|
||||
amount: {type: number, minimum: 0}
|
||||
settlementIncluded: {type: boolean}
|
||||
partnerId: {type: string, nullable: true}
|
||||
partnerName: {type: string, maxLength: 200, nullable: true}
|
||||
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
|
||||
@@ -318,7 +318,7 @@ components:
|
||||
InboundFeeDetailResource:
|
||||
type: object
|
||||
description: 入仓费用展示记录。sourceType=FBA_PRICING 时是只读派生;sourceType=MANUAL 时按 sourceFeeId/sourceFeeVersion 独立版本化。
|
||||
required: [id, tenantId, inboundId, feeItemId, sourceType, sourceFeeVersion, direction, businessDate, unitPrice, currency, amount, reviewStatus, publicationStatus, settlementIncluded]
|
||||
required: [id, tenantId, inboundId, feeItemId, sourceType, sourceFeeVersion, direction, businessDate, unitPrice, currency, amount, reviewStatus, publicationStatus]
|
||||
properties:
|
||||
id: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
@@ -330,8 +330,9 @@ components:
|
||||
sourceFeeNo: {type: string, nullable: true}
|
||||
inboundCode: {type: string, nullable: true}
|
||||
direction: {type: string, enum: [RECEIVABLE, PAYABLE]}
|
||||
partnerId: {type: string, nullable: true}
|
||||
partnerName: {type: string, nullable: true}
|
||||
supplierId: {type: string, nullable: true}
|
||||
supplierName: {type: string, nullable: true, readOnly: true}
|
||||
supplierCategory: {type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE], nullable: true}
|
||||
businessDate: {type: string, format: date}
|
||||
customerId: {type: string, nullable: true}
|
||||
customerName: {type: string, nullable: true}
|
||||
@@ -365,7 +366,6 @@ components:
|
||||
publishedByName: {type: string, nullable: true}
|
||||
publishedAt: {type: string, format: date-time, nullable: true}
|
||||
inboundedAt: {type: string, format: date-time, nullable: true}
|
||||
settlementIncluded: {type: boolean}
|
||||
createdBy: {type: string, nullable: true}
|
||||
createdAt: {type: string, format: date-time, nullable: true}
|
||||
updatedBy: {type: string, nullable: true}
|
||||
|
||||
@@ -100,6 +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时必填}
|
||||
feeTypeName: {type: string, maxLength: 128}
|
||||
pickupPayable: {type: boolean}
|
||||
pickupReceivable: {type: boolean}
|
||||
@@ -115,6 +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}
|
||||
feeTypeName: {type: string}
|
||||
pickupPayable: {type: boolean}
|
||||
pickupReceivable: {type: boolean}
|
||||
|
||||
@@ -838,7 +838,7 @@ paths:
|
||||
summary: 调整已审核柜次费用的日期和金额
|
||||
description: >-
|
||||
仅 ACTIVE、APPROVED 且 sourceVersion 匹配的费用可调整。成功后 sourceVersion 加一并重新进入
|
||||
PENDING;费用方向、费用项目、往来单位、币种、参与结算和备注保持原快照。身份或结算字段需要变化时,
|
||||
PENDING;费用方向、费用项目、供应商、币种和备注保持原快照。业务字段需要变化时,
|
||||
必须作废原费用并新增。同一 expectedSourceVersion、businessDate、amount 的原样重试只读返回已生成的
|
||||
PENDING 版本;旧版本异参、已作废或非已审核状态返回 A0409 且零写入。
|
||||
parameters:
|
||||
@@ -1446,10 +1446,10 @@ components:
|
||||
properties:
|
||||
id: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
planId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: 关联智能装柜计划ID;未创建计划时为空}
|
||||
planNo: {type: string, nullable: true, description: 装柜清单号;未创建计划时为空}
|
||||
bookingNo: {type: string, maxLength: 40}
|
||||
planNo: {type: string, nullable: true, pattern: '^LP-[0-9]{6}-[0-9]{4}$', example: LP-260818-0001, description: 预装与确认装柜清单共用的稳定编号;未创建计划时为空}
|
||||
bookingNo: {type: string, pattern: '^CB-[0-9]{6}-[0-9]{4}$', example: CB-260818-0001, description: 平台内部货柜登记号}
|
||||
containerNo: {type: string, maxLength: 64, nullable: true}
|
||||
cabinetNo: {type: string, maxLength: 64}
|
||||
cabinetNo: {type: string, pattern: '^CAB-[0-9]{6}-[0-9]{4}$', example: CAB-260818-0001, description: 平台稳定柜次号}
|
||||
sourceType: {type: string, enum: [SIMULATION, OCEAN_BOOKING], description: SIMULATION 为内部模拟柜;OCEAN_BOOKING 为订舱登记直接生成的真实柜}
|
||||
recordStatus: {type: string, enum: [ACTIVE, MERGED], description: 正常列表和详情仅返回 ACTIVE;真实订舱柜被模拟柜消费后变为 MERGED 且不再作为可操作柜返回}
|
||||
mergedIntoContainerBookingId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: MERGED 源真实柜指向保留的模拟柜稳定 containerBookingId;ACTIVE 时为空}
|
||||
@@ -1580,7 +1580,7 @@ components:
|
||||
required: [id, planNo, status, bookingId, version, canAssign, canEdit, finalCompletionStatus, items]
|
||||
properties:
|
||||
id: {type: string}
|
||||
planNo: {type: string}
|
||||
planNo: {type: string, pattern: '^LP-[0-9]{6}-[0-9]{4}$', example: LP-260818-0001, description: 预装与确认装柜清单共用的稳定编号}
|
||||
status: {type: string, enum: [EDITING, LOADING, COMPLETED]}
|
||||
bookingId: {type: string}
|
||||
containerNo: {type: string, nullable: true}
|
||||
@@ -1692,7 +1692,6 @@ components:
|
||||
publicationStatus: {type: string, enum: [PUBLISHED]}
|
||||
publishedAt: {type: string, format: date-time}
|
||||
inboundedAt: {type: string, format: date-time, nullable: true}
|
||||
settlementIncluded: {type: boolean, example: true}
|
||||
InboundOrderPageEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -294,9 +294,9 @@ components:
|
||||
properties:
|
||||
planId: {$ref: '#/components/schemas/LongId'}
|
||||
containerBookingId: {$ref: '#/components/schemas/LongId'}
|
||||
preloadNo: {type: string, description: 预装单号}
|
||||
preloadNo: {type: string, pattern: '^LP-[0-9]{6}-[0-9]{4}$', example: LP-260818-0001, description: 预装单号;按北京时间创建日逐日编号,确认后继续作为同一装柜清单号使用}
|
||||
containerNo: {type: string, nullable: true, description: 集装箱号}
|
||||
cabinetNo: {type: string, nullable: true, description: 稳定柜次号;不得与真实集装箱号混用}
|
||||
cabinetNo: {type: string, nullable: true, pattern: '^CAB-[0-9]{6}-[0-9]{4}$', example: CAB-260818-0001, description: 稳定柜次号;不得与真实集装箱号混用}
|
||||
customers: {type: array, description: 计划包含的客户, items: {$ref: '#/components/schemas/Customer'}}
|
||||
warehouseId: {$ref: '#/components/schemas/NullableLongId'}
|
||||
warehouseName: {type: string, nullable: true, description: 仓库名称}
|
||||
@@ -351,11 +351,11 @@ components:
|
||||
properties:
|
||||
planId: {$ref: '#/components/schemas/LongId'}
|
||||
containerBookingId: {$ref: '#/components/schemas/LongId'}
|
||||
loadingListNo: {type: string, description: 装柜清单号}
|
||||
loadingListNo: {type: string, pattern: '^LP-[0-9]{6}-[0-9]{4}$', example: LP-260818-0001, description: 装柜清单号;沿用预装阶段生成的同一编号}
|
||||
warehouseId: {$ref: '#/components/schemas/NullableLongId'}
|
||||
warehouseName: {type: string, nullable: true, description: 仓库名称}
|
||||
containerNo: {type: string, nullable: true, description: 集装箱号}
|
||||
cabinetNo: {type: string, nullable: true, description: 稳定柜次号}
|
||||
cabinetNo: {type: string, nullable: true, pattern: '^CAB-[0-9]{6}-[0-9]{4}$', example: CAB-260818-0001, description: 稳定柜次号}
|
||||
status: {type: string, enum: [COMPLETED], description: 装柜清单状态}
|
||||
totalPackageCount: {type: string, pattern: '^[0-9]+$', description: 总箱数}
|
||||
totalGrossWeightKg: {type: number, description: 总毛重,单位千克}
|
||||
|
||||
@@ -94,4 +94,15 @@ components:
|
||||
channelName: {type: string}
|
||||
priceModel: {type: string}
|
||||
pricingTaxModes: {type: array, items: {type: string}}
|
||||
Detail: {type: object, required: [oceanBooking, parties, containerRequirements, containers, histories], properties: {oceanBooking: {type: object}, parties: {type: array}, containerRequirements: {type: array}, containers: {type: array, description: Existing containers allocated after port pickup linking}, histories: {type: array}}}
|
||||
Detail:
|
||||
type: object
|
||||
required: [oceanBooking, parties, containerRequirements, containers, histories]
|
||||
properties:
|
||||
oceanBooking:
|
||||
type: object
|
||||
properties:
|
||||
bookingNo: {type: string, pattern: '^OB-[0-9]{6}-[0-9]{4}$', example: OB-260818-0001, description: 平台订舱号;按北京时间创建日逐日编号}
|
||||
parties: {type: array}
|
||||
containerRequirements: {type: array}
|
||||
containers: {type: array, description: Existing containers allocated after port pickup linking}
|
||||
histories: {type: array}
|
||||
|
||||
@@ -55,7 +55,7 @@ components:
|
||||
exception: {$ref: '#/components/schemas/ExceptionState'}
|
||||
exceptionHistory: {type: array, items: {$ref: '#/components/schemas/ExceptionHistory'}}
|
||||
timeline: {type: array, items: {$ref: '#/components/schemas/TimelineEntry'}}
|
||||
Container: {type: object, properties: {id: {type: integer, format: int64}, bookingNo: {type: string}, cabinetNo: {type: string}, containerNo: {type: string}, status: {type: string}, warehouseId: {type: integer, format: int64}, warehouseName: {type: string}, voyageNo: {type: string}, vesselName: {type: string}, version: {type: integer, minimum: 0}}}
|
||||
Container: {type: object, properties: {id: {type: integer, format: int64}, bookingNo: {type: string, pattern: '^CB-[0-9]{6}-[0-9]{4}$', example: CB-260818-0001}, cabinetNo: {type: string, pattern: '^CAB-[0-9]{6}-[0-9]{4}$', example: CAB-260818-0001}, containerNo: {type: string}, status: {type: string}, warehouseId: {type: integer, format: int64}, warehouseName: {type: string}, voyageNo: {type: string}, vesselName: {type: string}, version: {type: integer, minimum: 0}}}
|
||||
PdaSummary: {type: object, properties: {taskId: {type: integer, format: int64}, loadingPlanId: {type: integer, format: int64}, containerBookingId: {type: integer, format: int64}, taskStatus: {type: string, description: '持久化任务态:LOADING/COMPLETED'}, workflowStatus: {type: string, enum: [IN_PROGRESS, AWAITING_REVIEW, COMPLETED], description: 'PDA作业展示态'}, taskVersion: {type: integer}, itemCount: {type: integer}, completedItemCount: {type: integer}, unresolvedItemCount: {type: integer}, photoCounts: {type: array, items: {$ref: '#/components/schemas/PhotoCount'}}, photos: {type: array, items: {$ref: '#/components/schemas/Photo'}}, exceptionReasons: {type: array, items: {type: string}}}}
|
||||
PhotoCount: {type: object, properties: {evidenceType: {type: string}, count: {type: integer}}}
|
||||
Photo: {type: object, properties: {evidenceId: {type: integer, format: int64}, tenantId: {type: integer, format: int64}, inboundId: {type: integer, format: int64}, evidenceType: {type: string}, fileId: {type: integer, format: int64}, fileName: {type: string}, status: {type: string, enum: [ACTIVE, VOID]}, downloadPath: {type: string, nullable: true, description: '仅 ACTIVE 照片提供鉴权下载路径;VOID 历史照片为空'}, uploadedAt: {type: string, format: date-time}}}
|
||||
|
||||
@@ -29,7 +29,7 @@ paths:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/SaveRequest'}
|
||||
responses:
|
||||
'200': {description: Created appointment}
|
||||
'200': {description: Created appointment, content: {application/json: {schema: {$ref: '#/components/schemas/DetailStableReferences'}}}}
|
||||
/api/platform/v1/truck-appointments/{id}:
|
||||
parameters: [{$ref: '#/components/parameters/AppointmentId'}]
|
||||
get:
|
||||
@@ -182,13 +182,14 @@ components:
|
||||
expectedVersion: {type: integer, minimum: 0, description: Must be 0 on create; current version on edit.}
|
||||
DetailStableReferences:
|
||||
type: object
|
||||
required: [customerId, customerName, carrierId, carrierName, attachmentCount]
|
||||
required: [appointmentNo, customerId, customerName, carrierId, carrierName, attachmentCount]
|
||||
description: Stable IDs plus server-resolved saved resource names returned by appointment detail. Fulfillment associations remain null for an independently created appointment.
|
||||
properties:
|
||||
appointmentNo: {type: string, pattern: '^TA-[0-9]{6}-[0-9]{4}$', example: TA-260818-0001, description: 派车单号;按北京时间创建日逐日编号}
|
||||
oceanBookingId: {type: integer, format: int64, nullable: true}
|
||||
oceanBookingNo: {type: string, nullable: true}
|
||||
oceanBookingNo: {type: string, nullable: true, pattern: '^OB-[0-9]{6}-[0-9]{4}$', example: OB-260818-0001}
|
||||
containerBookingId: {type: integer, format: int64, nullable: true}
|
||||
cabinetNo: {type: string, nullable: true}
|
||||
cabinetNo: {type: string, nullable: true, pattern: '^CAB-[0-9]{6}-[0-9]{4}$', example: CAB-260818-0001}
|
||||
containerNo: {type: string, nullable: true}
|
||||
loadingPlanId: {type: integer, format: int64, nullable: true}
|
||||
warehouseId: {type: integer, format: int64, nullable: true}
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class FeeManagementContractTest {
|
||||
.contains("ContainerFeeAdjustRequest:")
|
||||
.contains("required: [expectedSourceVersion, businessDate, amount]")
|
||||
.contains("additionalProperties: false")
|
||||
.contains("费用方向、费用项目、往来单位、币种、参与结算和备注保持原快照")
|
||||
.contains("费用方向、费用项目、往来单位、币种和备注保持原快照")
|
||||
.contains("旧版本异参、已作废或非已审核状态返回 A0409 且零写入");
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -12,6 +12,7 @@ import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
|
||||
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
|
||||
import com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -35,6 +36,7 @@ class OceanBookingServiceTest {
|
||||
@Mock private PriceBookQueryRepository priceBooks;
|
||||
@Mock private WarehouseSupplierSnapshotService suppliers;
|
||||
@Mock private WarehouseMasterDataClient warehouses;
|
||||
@Mock private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@InjectMocks private OceanBookingService service;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@@ -42,6 +44,7 @@ class OceanBookingServiceTest {
|
||||
void setUp() throws Exception {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
lenient().when(documentNumbers.next("OB")).thenReturn("OB-260818-0001");
|
||||
var field = OceanBookingService.class.getDeclaredField("objectMapper");
|
||||
field.setAccessible(true);
|
||||
field.set(service, objectMapper);
|
||||
@@ -60,6 +63,7 @@ class OceanBookingServiceTest {
|
||||
new CreateRequest(0, partialDraft(null)));
|
||||
|
||||
assertThat(result.oceanBooking().getStatus()).isEqualTo("DRAFT");
|
||||
assertThat(result.oceanBooking().getBookingNo()).isEqualTo("OB-260818-0001");
|
||||
assertThat(inserted.get().getCustomerId()).isNull();
|
||||
verify(repository).replaceRequirements(inserted.get().getId(), List.of());
|
||||
verify(repository, never()).insertContainer(any());
|
||||
|
||||
+1
-1
@@ -22,6 +22,6 @@ class ContainerFeeDetailRepositoryTest {
|
||||
"getReviewReason", "getUpdatedBy", "getUpdatedAt")
|
||||
.doesNotContain("getFeeItemId", "getFeeItemCodeSnapshot", "getFeeItemNameSnapshot",
|
||||
"getDirection", "getPartnerId", "getPartnerName", "getCurrency",
|
||||
"getSettlementIncluded", "getRemark");
|
||||
"getRemark");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ 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",
|
||||
"settlement_included", "source_version", "partner_id", "partner_name",
|
||||
"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迁移失败:入库费用往来单位快照字段不完整",
|
||||
|
||||
@@ -31,6 +31,7 @@ class SmartLoadingSqlContractTest {
|
||||
@DisplayName("平台聚合与客户明细边界及原型快照字段完整")
|
||||
void shouldProvidePlatformAggregatesAndAccountScopedSnapshots() throws Exception {
|
||||
assertThat(migration())
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_document_sequence")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_container_booking")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_plan")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_plan_item")
|
||||
|
||||
+4
-7
@@ -59,7 +59,6 @@ class ContainerFeeDetailServiceTest {
|
||||
approved.setTenantId(10001L);
|
||||
approved.setLoadingPlanId(41L);
|
||||
approved.setSourceVersion(1);
|
||||
approved.setSettlementIncluded(true);
|
||||
when(repository.find(10001L, 41L, 51L)).thenReturn(approved);
|
||||
when(repository.review(10001L, 41L, 51L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
@@ -92,7 +91,6 @@ class ContainerFeeDetailServiceTest {
|
||||
assertThat(result.getDirection()).isEqualTo("PAYABLE");
|
||||
assertThat(result.getPartnerId()).isEqualTo(71L);
|
||||
assertThat(result.getCurrency()).isEqualTo("CNY");
|
||||
assertThat(result.getSettlementIncluded()).isTrue();
|
||||
assertThat(result.getRemark()).isEqualTo("原费用备注");
|
||||
verify(feeItems, never()).findItem(anyLong());
|
||||
}
|
||||
@@ -170,7 +168,7 @@ class ContainerFeeDetailServiceTest {
|
||||
|
||||
var missingPartner = new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE",
|
||||
LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY",
|
||||
true, null, null, "整柜拖车费");
|
||||
null, null, "整柜拖车费");
|
||||
|
||||
assertThatThrownBy(() -> service.create(10001L, 41L, 9L, missingPartner))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("必须选择有效往来单位");
|
||||
@@ -184,7 +182,7 @@ class ContainerFeeDetailServiceTest {
|
||||
|
||||
var request = new ContainerFeeDetailDtos.SaveRequest(11L, "RECEIVABLE",
|
||||
LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY",
|
||||
true, 71L, "不应保留", "整柜操作费");
|
||||
71L, "不应保留", "整柜操作费");
|
||||
service.create(10001L, 41L, 9L, request);
|
||||
|
||||
ArgumentCaptor<ContainerFeeDetailEntity> captor = ArgumentCaptor.forClass(ContainerFeeDetailEntity.class);
|
||||
@@ -225,7 +223,6 @@ class ContainerFeeDetailServiceTest {
|
||||
value.setPartnerId(71L);
|
||||
value.setPartnerName("现有往来单位");
|
||||
value.setCurrency("CNY");
|
||||
value.setSettlementIncluded(true);
|
||||
value.setRemark("原费用备注");
|
||||
value.setBusinessDate(LocalDate.of(2026, 8, 11));
|
||||
value.setAmount(new BigDecimal("300.00"));
|
||||
@@ -239,9 +236,9 @@ class ContainerFeeDetailServiceTest {
|
||||
return new AdjustRequest(version, LocalDate.of(2026, 8, 12), new BigDecimal(amount));
|
||||
}
|
||||
|
||||
private static ContainerFeeDetailDtos.SaveRequest request(boolean settlementIncluded) {
|
||||
private static ContainerFeeDetailDtos.SaveRequest request(boolean ignored) {
|
||||
return new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE",
|
||||
LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY",
|
||||
settlementIncluded, 71L, " 现有往来单位 ", "整柜拖车费");
|
||||
71L, " 现有往来单位 ", "整柜拖车费");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,14 @@ class ContainerServiceTest {
|
||||
@MockBean private PriceBookQueryRepository priceBooks;
|
||||
@MockBean private OceanBookingRepository oceanBookings;
|
||||
@MockBean private WarehouseSupplierSnapshotService suppliers;
|
||||
@MockBean private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@MockBean private ContainerTransportHistoryMapper transportHistories;
|
||||
@Resource private ContainerService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUpWarehouse() {
|
||||
when(documentNumbers.next("CB")).thenReturn("CB-260818-0001");
|
||||
when(documentNumbers.next("CAB")).thenReturn("CAB-260818-0001");
|
||||
when(warehouses.active(anyString(), anyLong(), anyLong())).thenAnswer(invocation ->
|
||||
activeWarehouse(invocation.getArgument(2, Long.class), "DOMESTIC", "上海中转仓"));
|
||||
when(suppliers.trucking(202L, "拖车公司A"))
|
||||
@@ -77,8 +80,8 @@ class ContainerServiceTest {
|
||||
verify(repository).insert(captor.capture());
|
||||
ContainerBookingEntity saved = captor.getValue();
|
||||
assertThat(created).isSameAs(saved);
|
||||
assertThat(saved.getBookingNo()).startsWith("CB");
|
||||
assertThat(saved.getCabinetNo()).startsWith("CAB-");
|
||||
assertThat(saved.getBookingNo()).isEqualTo("CB-260818-0001");
|
||||
assertThat(saved.getCabinetNo()).isEqualTo("CAB-260818-0001");
|
||||
assertThat(saved.getContainerNo()).isNull();
|
||||
assertThat(saved.getOceanBookingId()).isNull();
|
||||
assertThat(saved.getChannelName()).isEqualTo("美西海运");
|
||||
|
||||
@@ -220,7 +220,6 @@ class FeeManagementServiceTest {
|
||||
assertThat(saved.getUnitPrice()).isEqualByComparingTo("2.1198");
|
||||
assertThat(saved.getReviewStatus()).isEqualTo("APPROVED");
|
||||
assertThat(saved.getPublicationStatus()).isEqualTo("PUBLISHED");
|
||||
assertThat(saved.getSettlementIncluded()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -324,16 +323,24 @@ class FeeManagementServiceTest {
|
||||
@Test
|
||||
@DisplayName("审核只更新待审核明细")
|
||||
void approvesPendingDetail() {
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(new InboundFeeDetailEntity());
|
||||
InboundFeeDetailEntity pending = new InboundFeeDetailEntity();
|
||||
pending.setId(31L);
|
||||
pending.setTenantId(10001L);
|
||||
pending.setInboundId(21L);
|
||||
pending.setSourceType("MANUAL");
|
||||
pending.setSourceFeeVersion(1);
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(pending);
|
||||
when(repository.review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
details.approve(10001L, 21L, 31L, 9L, "admin");
|
||||
|
||||
verify(repository).review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null);
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||
eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("审核通过且参与结算的人工费用写入Finance事件")
|
||||
@DisplayName("审核通过的人工费用自动写入Finance事件")
|
||||
void publishesApprovedSettlementFee() {
|
||||
InboundFeeDetailEntity approved = new InboundFeeDetailEntity();
|
||||
approved.setId(31L);
|
||||
@@ -342,7 +349,6 @@ class FeeManagementServiceTest {
|
||||
approved.setFeeItemId(11L);
|
||||
approved.setSourceType("MANUAL");
|
||||
approved.setSourceFeeVersion(1);
|
||||
approved.setSettlementIncluded(true);
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(approved);
|
||||
when(repository.review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
@@ -409,7 +415,6 @@ class FeeManagementServiceTest {
|
||||
current.setFeeItemId(11L);
|
||||
current.setSourceType("MANUAL");
|
||||
current.setSourceFeeVersion(1);
|
||||
current.setSettlementIncluded(true);
|
||||
InboundFeeDetailEntity voided = new InboundFeeDetailEntity();
|
||||
voided.setId(31L);
|
||||
voided.setTenantId(10001L);
|
||||
@@ -417,7 +422,6 @@ class FeeManagementServiceTest {
|
||||
voided.setFeeItemId(11L);
|
||||
voided.setSourceType("MANUAL");
|
||||
voided.setSourceFeeVersion(1);
|
||||
voided.setSettlementIncluded(true);
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(current, voided);
|
||||
when(repository.voidApproved(10001L, 21L, 31L, 1, 9L)).thenReturn(1);
|
||||
|
||||
|
||||
+20
-21
@@ -3,10 +3,10 @@ package com.baiyue.firstleg.service;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeSourceSnapshotEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeSourceSnapshotRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -19,7 +19,7 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
class FinanceFeeSourceSnapshotServiceTest {
|
||||
private final FeeItemRepository inboundFees = mock(FeeItemRepository.class);
|
||||
private final ContainerFeeDetailRepository containerFees = mock(ContainerFeeDetailRepository.class);
|
||||
private final ContainerFeeSourceSnapshotRepository containerFees = mock(ContainerFeeSourceSnapshotRepository.class);
|
||||
private final FinanceFeeSourceSnapshotService service =
|
||||
new FinanceFeeSourceSnapshotService(inboundFees, containerFees);
|
||||
|
||||
@@ -37,8 +37,7 @@ class FinanceFeeSourceSnapshotServiceTest {
|
||||
detail.setCurrency("CNY");
|
||||
detail.setReviewStatus("APPROVED");
|
||||
detail.setPublicationStatus("PUBLISHED");
|
||||
detail.setSettlementIncluded(true);
|
||||
when(inboundFees.findDetail(21L, 31L)).thenReturn(detail);
|
||||
when(inboundFees.findSourceVersion(21L, 31L, 2)).thenReturn(detail);
|
||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||
inbound.setId(21L);
|
||||
inbound.setOrderId(81L);
|
||||
@@ -50,53 +49,53 @@ class FinanceFeeSourceSnapshotServiceTest {
|
||||
item.setFeeCode("STORAGE");
|
||||
when(inboundFees.findItem(11L)).thenReturn(item);
|
||||
|
||||
FinanceFeeSourceSnapshot snapshot = service.inbound(21L, 31L);
|
||||
FinanceFeeSourceSnapshot snapshot = service.inbound(21L, 31L, 2);
|
||||
|
||||
assertThat(snapshot.customerId()).isEqualTo(10001L);
|
||||
assertThat(snapshot.orderId()).isEqualTo(81L);
|
||||
assertThat(snapshot.feeVersion()).isEqualTo(2);
|
||||
assertThat(snapshot.sourceHash()).matches("[0-9a-f]{64}");
|
||||
detail.setPublicationStatus("VOIDED");
|
||||
assertThat(service.inbound(21L, 31L).sourceHash()).isEqualTo(snapshot.sourceHash());
|
||||
assertThat(service.inbound(21L, 31L, 2).sourceHash()).isEqualTo(snapshot.sourceHash());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsFeeThatDoesNotBelongToInboundPath() {
|
||||
InboundFeeDetailEntity detail = new InboundFeeDetailEntity();
|
||||
detail.setInboundId(22L);
|
||||
when(inboundFees.findDetail(21L, 31L)).thenReturn(detail);
|
||||
when(inboundFees.findSourceVersion(21L, 31L, 1)).thenReturn(detail);
|
||||
|
||||
assertThatThrownBy(() -> service.inbound(21L, 31L))
|
||||
assertThatThrownBy(() -> service.inbound(21L, 31L, 1))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不存在");
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerSnapshotPublishesPayableCounterpartyIdentity() {
|
||||
ContainerFeeDetailEntity detail = new ContainerFeeDetailEntity();
|
||||
detail.setId(51L);
|
||||
ContainerFeeSourceSnapshotEntity detail = new ContainerFeeSourceSnapshotEntity();
|
||||
detail.setSourceFeeId(51L);
|
||||
detail.setTenantId(10001L);
|
||||
detail.setLoadingPlanId(41L);
|
||||
detail.setContainerBookingId(61L);
|
||||
detail.setContainerNo("CONT-41");
|
||||
detail.setFeeItemId(11L);
|
||||
detail.setFeeItemCodeSnapshot("TRUCKING");
|
||||
detail.setFeeItemNameSnapshot("拖车费");
|
||||
detail.setFeeItemCode("TRUCKING");
|
||||
detail.setFeeItemName("拖车费");
|
||||
detail.setDirection("PAYABLE");
|
||||
detail.setPartnerId(71L);
|
||||
detail.setPartnerName("现有往来单位");
|
||||
detail.setSupplierId(71L);
|
||||
detail.setSupplierName("现有供应商");
|
||||
detail.setSupplierCategory("TRUCKING");
|
||||
detail.setBusinessDate(java.time.LocalDate.of(2026, 8, 12));
|
||||
detail.setAmount(new BigDecimal("300.00"));
|
||||
detail.setCurrency("CNY");
|
||||
detail.setReviewStatus("APPROVED");
|
||||
detail.setSourceStatus("ACTIVE");
|
||||
detail.setSourceVersion(2);
|
||||
detail.setSettlementIncluded(true);
|
||||
when(containerFees.find(41L, 51L)).thenReturn(detail);
|
||||
when(containerFees.find(41L, 51L, 2)).thenReturn(detail);
|
||||
|
||||
FinanceFeeSourceSnapshot snapshot = service.container(41L, 51L);
|
||||
FinanceFeeSourceSnapshot snapshot = service.container(41L, 51L, 2);
|
||||
|
||||
assertThat(snapshot.partnerId()).isEqualTo(71L);
|
||||
assertThat(snapshot.partnerName()).isEqualTo("现有往来单位");
|
||||
assertThat(snapshot.supplierId()).isEqualTo(71L);
|
||||
assertThat(snapshot.supplierName()).isEqualTo("现有供应商");
|
||||
assertThat(snapshot.supplierCategory()).isEqualTo("TRUCKING");
|
||||
assertThat(snapshot.sourceHash()).matches("[0-9a-f]{64}");
|
||||
}
|
||||
}
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class FirstlegDocumentNumberGeneratorTest {
|
||||
private static final Clock CLOCK = Clock.fixed(
|
||||
Instant.parse("2026-08-18T02:00:00Z"), ZoneId.of("Asia/Shanghai"));
|
||||
|
||||
@Test
|
||||
void formatsAllOperationalDocumentPrefixes() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class))
|
||||
.thenReturn(1L, 1L, 1L, 1L, 1L);
|
||||
FirstlegDocumentNumberGenerator generator = new FirstlegDocumentNumberGenerator(jdbc, CLOCK);
|
||||
|
||||
assertThat(generator.next("OB")).isEqualTo("OB-260818-0001");
|
||||
assertThat(generator.next("TA")).isEqualTo("TA-260818-0001");
|
||||
assertThat(generator.next("CB")).isEqualTo("CB-260818-0001");
|
||||
assertThat(generator.next("CAB")).isEqualTo("CAB-260818-0001");
|
||||
assertThat(generator.next("LP")).isEqualTo("LP-260818-0001");
|
||||
|
||||
verify(jdbc).update(org.mockito.ArgumentMatchers.startsWith("INSERT INTO fl_document_sequence"),
|
||||
eq("OB:20260818"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnsupportedTypeAndDailyOverflow() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
FirstlegDocumentNumberGenerator generator = new FirstlegDocumentNumberGenerator(jdbc, CLOCK);
|
||||
assertThatThrownBy(() -> generator.next("MSCU")).isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
when(jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class)).thenReturn(10_000L);
|
||||
assertThatThrownBy(() -> generator.next("CAB")).isInstanceOf(BizException.class);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ class SmartLoadingServiceTest {
|
||||
@MockBean private LoadingPickService loadingPickService;
|
||||
@MockBean private LoadingModelBridgeService loadingModelBridge;
|
||||
@MockBean private SplitPreloadService splitPreloadService;
|
||||
@MockBean private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Resource private SmartLoadingService service;
|
||||
|
||||
@TestConfiguration
|
||||
@@ -73,6 +74,7 @@ class SmartLoadingServiceTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(documentNumbers.next("LP")).thenReturn("LP-260818-0001");
|
||||
when(loadingPickService.mutationSnapshot(anyLong())).thenReturn(
|
||||
new LoadingPickService.MutationSnapshot(null, null, null, Map.of()));
|
||||
when(cargoClient.detail(anyString(), anyLong(), anyLong(), anyLong())).thenReturn(
|
||||
@@ -174,6 +176,22 @@ class SmartLoadingServiceTest {
|
||||
verify(repository, never()).insert(any(LoadingPlanEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsPlanWithShortStableNumber() {
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.planByBooking(99L)).thenReturn(null);
|
||||
doAnswer(invocation -> {
|
||||
LoadingPlanEntity value = invocation.getArgument(0);
|
||||
value.setId(1L);
|
||||
return 1;
|
||||
}).when(repository).insert(any(LoadingPlanEntity.class));
|
||||
when(repository.items(1L)).thenReturn(List.of());
|
||||
|
||||
Plan result = service.create(99L, "备注", 7L, "u");
|
||||
|
||||
assertThat(result.planNo()).isEqualTo("LP-260818-0001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsExcessBoxes() {
|
||||
LoadingPlanEntity plan = plan("EDITING", 0);
|
||||
|
||||
@@ -47,11 +47,13 @@ class TruckAppointmentServiceTest {
|
||||
@Mock WarehouseSupplierSnapshotService suppliers;
|
||||
@Mock IamCustomerSnapshotService customers;
|
||||
@Mock TruckAppointmentFileClient files;
|
||||
@Mock FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Spy ObjectMapper objectMapper = JsonMapper.builder().findAndAddModules().build();
|
||||
@InjectMocks TruckAppointmentService service;
|
||||
|
||||
@BeforeEach void setUp() { MockitoAnnotations.openMocks(this);
|
||||
service.internalToken = "test-internal-token";
|
||||
lenient().when(documentNumbers.next("TA")).thenReturn("TA-260818-0001");
|
||||
when(customers.active(anyLong(), any())).thenAnswer(invocation ->
|
||||
new IamCustomerSnapshotClient.Customer(invocation.getArgument(0), "C-7",
|
||||
"Canonical Customer", "ACTIVE", null, null));
|
||||
@@ -107,6 +109,7 @@ class TruckAppointmentServiceTest {
|
||||
assertThat(entity.getValue().getLoadingPlanId()).isNull();
|
||||
assertThat(entity.getValue().getCustomerName()).isEqualTo("Canonical Customer");
|
||||
assertThat(entity.getValue().getCarrierName()).isEqualTo("Canonical Carrier");
|
||||
assertThat(entity.getValue().getAppointmentNo()).isEqualTo("TA-260818-0001");
|
||||
verifyNoInteractions(oceanBookings, containers, loadingPlans);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user