feat(firstleg): support customer price books
This commit is contained in:
+4
-2
@@ -38,9 +38,10 @@ public class PlatformPriceBookController {
|
||||
@Operation(summary = "查询船期价格表版本")
|
||||
public ApiResponse<java.util.List<PriceBookDtos.PriceBookVersionSummary>> list(
|
||||
@AuthenticationPrincipal com.baiyue.common.security.AuthenticatedUser user,
|
||||
@RequestParam(defaultValue = "0") long customerId,
|
||||
@RequestParam(required = false) PriceBookStatus status) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.list(status));
|
||||
return ApiResponse.success(service.list(customerId, status));
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@@ -48,9 +49,10 @@ public class PlatformPriceBookController {
|
||||
@BusinessAudit(event = "FBA_PRICE_BOOK_UPLOAD", resourceType = "PRICE_BOOK_VERSION", description = "上传船期价格表并等待确认")
|
||||
public ApiResponse<PriceBookDtos.PriceBookVersionDetail> upload(
|
||||
@AuthenticationPrincipal com.baiyue.common.security.AuthenticatedUser user,
|
||||
@RequestParam(defaultValue = "0") long customerId,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.upload(user.userId(), file));
|
||||
return ApiResponse.success(service.upload(user.userId(), customerId, file));
|
||||
}
|
||||
|
||||
@GetMapping("/{versionId}")
|
||||
|
||||
@@ -23,6 +23,7 @@ public final class PriceBookDtos {
|
||||
|
||||
public record PriceBookVersionSummary(
|
||||
Long id,
|
||||
Long customerId,
|
||||
Integer versionNo,
|
||||
String originalFileName,
|
||||
Long sourceFileId,
|
||||
|
||||
@@ -10,12 +10,12 @@ import lombok.Setter;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 船司 FBA 目的仓价格的不可变版本。 */
|
||||
/** 公共或客户专属的头程报价不可变版本。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_carrier_fba_rate")
|
||||
public class CarrierFbaRateEntity extends BaseEntity {
|
||||
/** 船司价格是平台公共主数据,不绑定任何客户。 */
|
||||
/** 0 为平台公共报价,正数为客户专属报价作用域。 */
|
||||
public static final long PLATFORM_SCOPE = 0L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
|
||||
+15
-5
@@ -32,6 +32,16 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader {
|
||||
""")
|
||||
Long findActiveVersionId();
|
||||
|
||||
@Select("""
|
||||
SELECT id
|
||||
FROM fl_price_book_version
|
||||
WHERE tenant_id IN (0, #{customerId}) AND status = 'ACTIVE' AND deleted = 0
|
||||
ORDER BY CASE WHEN tenant_id = #{customerId} THEN 0 ELSE 1 END,
|
||||
published_at DESC, version_no DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
Long findEffectiveVersionId(@Param("customerId") long customerId);
|
||||
|
||||
@Select("""
|
||||
SELECT c.price_book_version_id AS priceBookVersionId,
|
||||
c.channel_code AS channelCode, c.channel_name AS channelName,
|
||||
@@ -48,7 +58,7 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader {
|
||||
SELECT 1 FROM fl_carrier_fba_rate r
|
||||
WHERE r.price_book_version_id = c.price_book_version_id
|
||||
AND r.channel_code = c.channel_code
|
||||
AND r.tenant_id = 0 AND r.origin_warehouse_id = #{originWarehouseId}
|
||||
AND r.origin_warehouse_id = #{originWarehouseId}
|
||||
AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
)
|
||||
ORDER BY c.id
|
||||
@@ -60,7 +70,7 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader {
|
||||
SELECT r.channel_code AS channelCode, r.pricing_tax_mode AS pricingTaxMode
|
||||
FROM fl_carrier_fba_rate r
|
||||
WHERE r.price_book_version_id = #{versionId}
|
||||
AND r.tenant_id = 0 AND r.origin_warehouse_id = #{originWarehouseId}
|
||||
AND r.origin_warehouse_id = #{originWarehouseId}
|
||||
AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
AND r.pricing_tax_mode IS NOT NULL
|
||||
GROUP BY r.channel_code, r.pricing_tax_mode
|
||||
@@ -83,7 +93,7 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader {
|
||||
r.effective_from AS effectiveFrom, r.effective_to AS effectiveTo
|
||||
FROM fl_carrier_fba_rate r
|
||||
WHERE r.price_book_version_id = #{versionId}
|
||||
AND r.tenant_id = 0 AND r.origin_warehouse_id = #{originWarehouseId}
|
||||
AND r.origin_warehouse_id = #{originWarehouseId}
|
||||
AND r.channel_code = #{channelCode} AND r.destination_type = 'FBA'
|
||||
AND UPPER(r.destination_code) = #{destinationCode}
|
||||
AND r.pricing_tax_mode = #{pricingTaxMode}
|
||||
@@ -109,7 +119,7 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader {
|
||||
r.effective_from AS effectiveFrom, r.effective_to AS effectiveTo
|
||||
FROM fl_carrier_fba_rate r
|
||||
WHERE r.price_book_version_id = #{versionId}
|
||||
AND r.tenant_id = 0 AND r.origin_warehouse_id = #{originWarehouseId}
|
||||
AND r.origin_warehouse_id = #{originWarehouseId}
|
||||
AND r.channel_code = #{channelCode} AND r.destination_type = 'OVERSEAS'
|
||||
AND r.destination_warehouse_id = #{destinationWarehouseId}
|
||||
AND r.pricing_tax_mode = #{pricingTaxMode}
|
||||
@@ -135,7 +145,7 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader {
|
||||
r.effective_from AS effectiveFrom, r.effective_to AS effectiveTo
|
||||
FROM fl_carrier_fba_rate r
|
||||
WHERE r.price_book_version_id = #{versionId}
|
||||
AND r.tenant_id = 0 AND r.origin_warehouse_id = #{originWarehouseId}
|
||||
AND r.origin_warehouse_id = #{originWarehouseId}
|
||||
AND r.channel_code = #{channelCode}
|
||||
AND r.price_model = 'REGION_WEIGHT_TIER'
|
||||
AND r.region_code = #{regionCode}
|
||||
|
||||
+3
@@ -16,6 +16,9 @@ public class PriceBookQueryService implements PriceBookQueryUseCase {
|
||||
@Resource private PriceBookQueryReader reader;
|
||||
|
||||
@Override public Long activeVersionId() { return reader.findActiveVersionId(); }
|
||||
@Override public Long effectiveVersionId(long customerId) {
|
||||
return reader.findEffectiveVersionId(customerId);
|
||||
}
|
||||
@Override public List<ChannelRow> channels(long versionId, long originWarehouseId) {
|
||||
return reader.findChannels(versionId, originWarehouseId);
|
||||
}
|
||||
|
||||
+1
@@ -9,6 +9,7 @@ import java.util.List;
|
||||
|
||||
public interface PriceBookQueryUseCase {
|
||||
Long activeVersionId();
|
||||
Long effectiveVersionId(long customerId);
|
||||
List<ChannelRow> channels(long versionId, long originWarehouseId);
|
||||
List<ChannelTaxModeRow> channelTaxModes(long versionId, long originWarehouseId);
|
||||
List<RateRow> fbaRates(long versionId, long originWarehouseId, String channelCode,
|
||||
|
||||
+1
@@ -9,6 +9,7 @@ import java.util.List;
|
||||
|
||||
public interface PriceBookQueryReader {
|
||||
Long findActiveVersionId();
|
||||
Long findEffectiveVersionId(long customerId);
|
||||
List<ChannelRow> findChannels(long versionId, long originWarehouseId);
|
||||
List<ChannelTaxModeRow> findChannelTaxModes(long versionId, long originWarehouseId);
|
||||
List<RateRow> findFbaRates(long versionId, long originWarehouseId, String channelCode,
|
||||
|
||||
@@ -27,10 +27,14 @@ public class ActivePriceBookEstimator {
|
||||
@Resource private PriceBookWarehouseRegionResolver warehouseRegionResolver;
|
||||
|
||||
public Estimate estimate(EstimateRequest request) {
|
||||
return estimate(0L, request);
|
||||
}
|
||||
|
||||
public Estimate estimate(long customerId, EstimateRequest request) {
|
||||
if (request == null || request.originWarehouseId() == null || request.originWarehouseId() <= 0) {
|
||||
throw validation("请选择有效起运仓");
|
||||
}
|
||||
Long versionId = repository.activeVersionId();
|
||||
Long versionId = repository.effectiveVersionId(customerId);
|
||||
if (versionId == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "当前没有生效的船期价格版本");
|
||||
}
|
||||
|
||||
@@ -32,39 +32,50 @@ public class PriceBookRepository {
|
||||
public int insertVersion(PriceBookVersionEntity value) { return versions.insert(value); }
|
||||
public int updateVersion(PriceBookVersionEntity value) {
|
||||
return versions.update(value, new LambdaUpdateWrapper<PriceBookVersionEntity>()
|
||||
.eq(PriceBookVersionEntity::getTenantId, PriceBookVersionEntity.PLATFORM_SCOPE)
|
||||
.eq(PriceBookVersionEntity::getTenantId, value.getTenantId())
|
||||
.eq(PriceBookVersionEntity::getId, value.getId()));
|
||||
}
|
||||
public PriceBookVersionEntity findVersion(long id) {
|
||||
return versions.selectOne(new LambdaQueryWrapper<PriceBookVersionEntity>()
|
||||
.eq(PriceBookVersionEntity::getTenantId, PriceBookVersionEntity.PLATFORM_SCOPE)
|
||||
.eq(PriceBookVersionEntity::getId, id)
|
||||
.eq(PriceBookVersionEntity::getDeleted, false));
|
||||
}
|
||||
public List<PriceBookVersionEntity> listVersions(String status) {
|
||||
return listVersions(PriceBookVersionEntity.PLATFORM_SCOPE, status);
|
||||
}
|
||||
public List<PriceBookVersionEntity> listVersions(long customerId, String status) {
|
||||
return versions.selectList(new LambdaQueryWrapper<PriceBookVersionEntity>()
|
||||
.eq(PriceBookVersionEntity::getTenantId, PriceBookVersionEntity.PLATFORM_SCOPE)
|
||||
.eq(PriceBookVersionEntity::getTenantId, customerId)
|
||||
.eq(status != null && !status.isBlank(), PriceBookVersionEntity::getStatus, status)
|
||||
.eq(PriceBookVersionEntity::getDeleted, false)
|
||||
.orderByDesc(PriceBookVersionEntity::getCreatedAt));
|
||||
}
|
||||
public PriceBookVersionEntity findActiveForUpdate() {
|
||||
return findActiveForUpdate(PriceBookVersionEntity.PLATFORM_SCOPE);
|
||||
}
|
||||
public PriceBookVersionEntity findActiveForUpdate(long customerId) {
|
||||
return versions.selectOne(new LambdaQueryWrapper<PriceBookVersionEntity>()
|
||||
.eq(PriceBookVersionEntity::getTenantId, PriceBookVersionEntity.PLATFORM_SCOPE)
|
||||
.eq(PriceBookVersionEntity::getTenantId, customerId)
|
||||
.eq(PriceBookVersionEntity::getStatus, "ACTIVE")
|
||||
.eq(PriceBookVersionEntity::getDeleted, false)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
public List<PriceBookVersionEntity> listActiveForUpdate() {
|
||||
return listActiveForUpdate(PriceBookVersionEntity.PLATFORM_SCOPE);
|
||||
}
|
||||
public List<PriceBookVersionEntity> listActiveForUpdate(long customerId) {
|
||||
return versions.selectList(new LambdaQueryWrapper<PriceBookVersionEntity>()
|
||||
.eq(PriceBookVersionEntity::getTenantId, PriceBookVersionEntity.PLATFORM_SCOPE)
|
||||
.eq(PriceBookVersionEntity::getTenantId, customerId)
|
||||
.eq(PriceBookVersionEntity::getStatus, "ACTIVE")
|
||||
.eq(PriceBookVersionEntity::getDeleted, false)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
public PriceBookVersionEntity findActive() {
|
||||
return findActive(PriceBookVersionEntity.PLATFORM_SCOPE);
|
||||
}
|
||||
public PriceBookVersionEntity findActive(long customerId) {
|
||||
return versions.selectOne(new LambdaQueryWrapper<PriceBookVersionEntity>()
|
||||
.eq(PriceBookVersionEntity::getTenantId, PriceBookVersionEntity.PLATFORM_SCOPE)
|
||||
.eq(PriceBookVersionEntity::getTenantId, customerId)
|
||||
.eq(PriceBookVersionEntity::getStatus, "ACTIVE")
|
||||
.eq(PriceBookVersionEntity::getDeleted, false));
|
||||
}
|
||||
@@ -74,14 +85,20 @@ public class PriceBookRepository {
|
||||
* ACTIVE version exists (both transactions could otherwise pass the check).
|
||||
*/
|
||||
public void lockPlatformVersions() {
|
||||
lockVersions(PriceBookVersionEntity.PLATFORM_SCOPE);
|
||||
}
|
||||
public void lockVersions(long customerId) {
|
||||
versions.selectList(new LambdaQueryWrapper<PriceBookVersionEntity>()
|
||||
.eq(PriceBookVersionEntity::getTenantId, PriceBookVersionEntity.PLATFORM_SCOPE)
|
||||
.eq(PriceBookVersionEntity::getTenantId, customerId)
|
||||
.eq(PriceBookVersionEntity::getDeleted, false)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
public int nextVersionNo() {
|
||||
return nextVersionNo(PriceBookVersionEntity.PLATFORM_SCOPE);
|
||||
}
|
||||
public int nextVersionNo(long customerId) {
|
||||
PriceBookVersionEntity latest = versions.selectOne(new LambdaQueryWrapper<PriceBookVersionEntity>()
|
||||
.eq(PriceBookVersionEntity::getTenantId, PriceBookVersionEntity.PLATFORM_SCOPE)
|
||||
.eq(PriceBookVersionEntity::getTenantId, customerId)
|
||||
.orderByDesc(PriceBookVersionEntity::getVersionNo).last("LIMIT 1 FOR UPDATE"));
|
||||
return latest == null ? 1 : latest.getVersionNo() + 1;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public class FbaPricingService {
|
||||
if (incomplete) throw conflict("外箱规数据或绑定的计算规则不完整,不能生成对客费用");
|
||||
}
|
||||
PricingPolicy policy = pricingPolicy(inbound, requestedAt == null ? LocalDateTime.now(clock) : requestedAt);
|
||||
ActivePriceBookEstimator.Estimate estimate = priceBookEstimator.estimate(new EstimateRequest(
|
||||
ActivePriceBookEstimator.Estimate estimate = priceBookEstimator.estimate(tenantId, new EstimateRequest(
|
||||
inbound.getWarehouseId(), inbound.getChannelCode(), inbound.getDestinationCode(),
|
||||
destinationType(inbound), inbound.getDestinationWarehouseId(), inbound.getDestinationName(), null,
|
||||
pricingTaxMode(inbound), policy.chargeMode(),
|
||||
|
||||
@@ -32,7 +32,7 @@ public class MerchantPriceBookService {
|
||||
public List<CurrentChannel> channels(long merchantTenantId, long originWarehouseId) {
|
||||
requireMerchant(merchantTenantId);
|
||||
if (originWarehouseId <= 0) throw validation("请选择有效起运仓");
|
||||
Long versionId = repository.activeVersionId();
|
||||
Long versionId = repository.effectiveVersionId(merchantTenantId);
|
||||
if (versionId == null) return List.of();
|
||||
List<ChannelRow> channels = repository.channels(versionId, originWarehouseId);
|
||||
if (channels.isEmpty()) return List.of();
|
||||
@@ -46,7 +46,7 @@ public class MerchantPriceBookService {
|
||||
&& request.chargeMode() != null && !"KG".equals(request.chargeMode())) {
|
||||
throw validation("包税报价必须按KG计费");
|
||||
}
|
||||
ActivePriceBookEstimator.Estimate estimate = estimator.estimate(request);
|
||||
ActivePriceBookEstimator.Estimate estimate = estimator.estimate(merchantTenantId, request);
|
||||
return new EstimateResponse(estimate.priceBookVersionId(), estimate.channelCode(), estimate.priceModel(),
|
||||
estimate.pricingTaxMode(), estimate.chargeableQuantity(), estimate.chargeUnit(), estimate.unitPrice(),
|
||||
estimate.amount(), estimate.currency(), DISCLAIMER);
|
||||
@@ -56,7 +56,7 @@ public class MerchantPriceBookService {
|
||||
String destinationType, boolean requireInboundCodePrefix) {
|
||||
requireMerchant(merchantTenantId);
|
||||
if (originWarehouseId <= 0) throw validation("请选择有效起运仓");
|
||||
Long versionId = repository.activeVersionId();
|
||||
Long versionId = repository.effectiveVersionId(merchantTenantId);
|
||||
String normalized = normalizeChannel(channelCode);
|
||||
if (versionId == null || !repository.channelAvailable(versionId, originWarehouseId, normalized)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "所选渠道已下线,请重新选择");
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ public class OrderChannelChangePricingService {
|
||||
request.channelCode(), request.destinationType(), false);
|
||||
BigDecimal weight = positive(inbound.getActualNetWeightKg(), ELIGIBILITY_WEIGHT);
|
||||
BigDecimal volume = positive(inbound.getActualVolumeCbm(), ELIGIBILITY_VOLUME);
|
||||
return estimator.estimate(new EstimateRequest(request.originWarehouseId(), request.channelCode(),
|
||||
return estimator.estimate(request.tenantId(), new EstimateRequest(request.originWarehouseId(), request.channelCode(),
|
||||
request.destinationCode(), PricingDestinationType.valueOf(request.destinationType()),
|
||||
request.destinationWarehouseId(), inbound.getDestinationName(), null,
|
||||
PricingTaxMode.valueOf(request.pricingTaxMode()), weight, volume));
|
||||
|
||||
@@ -72,13 +72,26 @@ public class PlatformPriceBookService {
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
|
||||
public List<PriceBookDtos.PriceBookVersionSummary> list(PriceBookStatus status) {
|
||||
return list(PriceBookVersionEntity.PLATFORM_SCOPE, status);
|
||||
}
|
||||
|
||||
public List<PriceBookDtos.PriceBookVersionSummary> list(long customerId, PriceBookStatus status) {
|
||||
requireScope(customerId);
|
||||
String persistedStatus = status == null ? null : status.name();
|
||||
return repository.listVersions(persistedStatus).stream().map(this::summary).toList();
|
||||
return (customerId == PriceBookVersionEntity.PLATFORM_SCOPE
|
||||
? repository.listVersions(persistedStatus) : repository.listVersions(customerId, persistedStatus))
|
||||
.stream().map(this::summary).toList();
|
||||
}
|
||||
|
||||
public PriceBookDtos.PriceBookVersionDetail upload(long operatorId, MultipartFile file) {
|
||||
return upload(operatorId, PriceBookVersionEntity.PLATFORM_SCOPE, file);
|
||||
}
|
||||
|
||||
public PriceBookDtos.PriceBookVersionDetail upload(long operatorId, long customerId, MultipartFile file) {
|
||||
requireScope(customerId);
|
||||
requireFile(file);
|
||||
PriceBookVersionEntity version = transactions.execute(status -> createVersion(operatorId, file.getOriginalFilename()));
|
||||
PriceBookVersionEntity version = transactions.execute(status -> createVersion(
|
||||
operatorId, customerId, file.getOriginalFilename()));
|
||||
if (version == null) throw internal("价格版本创建失败");
|
||||
Path temp = null;
|
||||
try {
|
||||
@@ -123,7 +136,8 @@ public class PlatformPriceBookService {
|
||||
List<PriceBookChannelEntity> channelRows = repository.listChannels(versionId);
|
||||
List<PriceBookDtos.ParseIssue> issues = new ArrayList<>(readIssues(version.getParseIssuesJson()));
|
||||
if (PriceBookStatus.PENDING_CONFIRMATION.name().equals(version.getStatus())) {
|
||||
PriceBookVersionEntity active = repository.findActive();
|
||||
PriceBookVersionEntity active = version.getTenantId() == PriceBookVersionEntity.PLATFORM_SCOPE
|
||||
? repository.findActive() : repository.findActive(version.getTenantId());
|
||||
if (active != null && !Objects.equals(active.getId(), versionId)) {
|
||||
Set<String> current = channelRows.stream().map(value -> normalizeChannel(value.getChannelCode())).collect(java.util.stream.Collectors.toSet());
|
||||
repository.listChannels(active.getId()).stream()
|
||||
@@ -140,7 +154,9 @@ public class PlatformPriceBookService {
|
||||
@Transactional(noRollbackFor = BizException.class)
|
||||
public PriceBookDtos.PriceBookVersionDetail publish(long versionId, long operatorId,
|
||||
PriceBookConfirmationRequest confirmation) {
|
||||
repository.lockPlatformVersions();
|
||||
PriceBookVersionEntity candidate = requireVersion(versionId);
|
||||
if (candidate.getTenantId() == PriceBookVersionEntity.PLATFORM_SCOPE) repository.lockPlatformVersions();
|
||||
else repository.lockVersions(candidate.getTenantId());
|
||||
PriceBookVersionEntity version = requirePendingConfirmation(versionId);
|
||||
List<PriceBookDtos.ParsedChannel> channels = repository.listChannels(versionId).stream().map(this::channelDto).toList();
|
||||
List<PriceBookDtos.ParsedRate> rates = repository.listRates(versionId).stream().map(this::rateDto).toList();
|
||||
@@ -158,7 +174,8 @@ public class PlatformPriceBookService {
|
||||
version.setChannelCount(channels.size());
|
||||
version.setRateCount(mapped.rates().size());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
repository.listActiveForUpdate().stream()
|
||||
(version.getTenantId() == PriceBookVersionEntity.PLATFORM_SCOPE
|
||||
? repository.listActiveForUpdate() : repository.listActiveForUpdate(version.getTenantId())).stream()
|
||||
.filter(active -> !Objects.equals(active.getId(), versionId))
|
||||
.forEach(active -> repository.supersede(active, now, operatorId));
|
||||
repository.activate(version, now, operatorId);
|
||||
@@ -182,10 +199,11 @@ public class PlatformPriceBookService {
|
||||
}
|
||||
}
|
||||
|
||||
private PriceBookVersionEntity createVersion(long operatorId, String originalFileName) {
|
||||
private PriceBookVersionEntity createVersion(long operatorId, long customerId, String originalFileName) {
|
||||
PriceBookVersionEntity value = new PriceBookVersionEntity();
|
||||
value.setTenantId(PriceBookVersionEntity.PLATFORM_SCOPE);
|
||||
value.setVersionNo(repository.nextVersionNo());
|
||||
value.setTenantId(customerId);
|
||||
value.setVersionNo(customerId == PriceBookVersionEntity.PLATFORM_SCOPE
|
||||
? repository.nextVersionNo() : repository.nextVersionNo(customerId));
|
||||
value.setOriginalFileName(safeFileName(originalFileName));
|
||||
value.setStatus(PriceBookStatus.PENDING_CONFIRMATION.name());
|
||||
value.setBlockingErrorCount(0); value.setWarningCount(0); value.setChannelCount(0); value.setRateCount(0);
|
||||
@@ -228,7 +246,7 @@ public class PlatformPriceBookService {
|
||||
private CarrierFbaRateEntity rateEntity(PriceBookVersionEntity version, PriceBookDtos.ParsedRate parsedRate,
|
||||
long operatorId, int number, LocalDateTime now) {
|
||||
CarrierFbaRateEntity value = new CarrierFbaRateEntity();
|
||||
value.setTenantId(CarrierFbaRateEntity.PLATFORM_SCOPE); value.setPriceBookVersionId(version.getId());
|
||||
value.setTenantId(version.getTenantId()); value.setPriceBookVersionId(version.getId());
|
||||
value.setRateCode("PB-" + version.getVersionNo() + "-" + number); value.setRateVersion(1);
|
||||
value.setOriginWarehouseId(parsedRate.originWarehouseId()); value.setOriginWarehouseCode(parsedRate.originWarehouseCode().trim());
|
||||
value.setOriginWarehouseName(parsedRate.originWarehouseName().trim()); value.setBusinessType("FBA_SEA");
|
||||
@@ -540,12 +558,16 @@ public class PlatformPriceBookService {
|
||||
|
||||
private PriceBookDtos.PriceBookVersionSummary summary(PriceBookVersionEntity value) {
|
||||
PriceBookStatus status = PriceBookStatus.valueOf(value.getStatus());
|
||||
return new PriceBookDtos.PriceBookVersionSummary(value.getId(), value.getVersionNo(), value.getOriginalFileName(),
|
||||
return new PriceBookDtos.PriceBookVersionSummary(value.getId(), value.getTenantId(), value.getVersionNo(), value.getOriginalFileName(),
|
||||
value.getSourceFileId(), value.getSourceSha256(), status,
|
||||
value.getChannelCount(), value.getRateCount(), value.getBlockingErrorCount(), value.getWarningCount(),
|
||||
value.getParseIssuesJson(), value.getCreatedBy(), value.getCreatedAt(), value.getPublishedBy(),
|
||||
value.getPublishedAt(), value.getSupersededAt());
|
||||
}
|
||||
|
||||
private static void requireScope(long customerId) {
|
||||
if (customerId < 0) throw new BizException(ResultCode.VALIDATION_FAILED, "客户ID不能为负数");
|
||||
}
|
||||
private PriceBookDtos.ParsedChannel channelDto(PriceBookChannelEntity value) {
|
||||
return new PriceBookDtos.ParsedChannel(value.getChannelCode(), value.getChannelName(), PriceModel.valueOf(value.getPriceModel()),
|
||||
value.getSourceSheetName(), value.getEstimatedDepartureDate(), value.getEstimatedDepartureText(),
|
||||
|
||||
@@ -533,7 +533,7 @@ CREATE TABLE IF NOT EXISTS fl_price_warehouse_mapping (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_price_book_version (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '整表价格版本ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共价格作用域固定为0',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '报价作用域:0平台公共,正数为客户专属',
|
||||
version_no INT UNSIGNED NOT NULL COMMENT '整表版本号',
|
||||
original_file_name VARCHAR(255) NOT NULL COMMENT '原始Excel文件名',
|
||||
source_file_id BIGINT UNSIGNED NULL COMMENT 'File服务原始文件ID',
|
||||
|
||||
@@ -72,6 +72,7 @@ paths:
|
||||
summary: 查询船期价格表版本
|
||||
description: 需要 firstleg:fba-pricing:rate:view 权限;返回待确认上传、当前生效和历史替换版本。
|
||||
parameters:
|
||||
- {name: customerId, in: query, schema: {type: string, pattern: '^(0|[1-9][0-9]*)$', default: '0'}, description: 0 表示公共报价;正数表示客户专属报价}
|
||||
- {name: status, in: query, schema: {$ref: '#/components/schemas/PriceBookStatus'}}
|
||||
responses:
|
||||
'200':
|
||||
@@ -84,7 +85,9 @@ paths:
|
||||
'403': {description: 无权限}
|
||||
post:
|
||||
summary: 上传船期价格表并生成待确认记录
|
||||
description: 仅上传和解析,生成待确认上传记录,不改变当前生效版本;需要 firstleg:fba-pricing:rate:edit 权限。
|
||||
description: 仅上传和解析,生成待确认上传记录,不改变当前生效版本;customerId=0 为公共报价,customerId>0 为该客户的专属报价;需要 firstleg:fba-pricing:rate:edit 权限。
|
||||
parameters:
|
||||
- {name: customerId, in: query, schema: {type: string, pattern: '^(0|[1-9][0-9]*)$', default: '0'}}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -105,7 +108,7 @@ paths:
|
||||
/api/platform/v1/fba-pricing/carrier-rates/price-books/{versionId}/publish:
|
||||
post:
|
||||
summary: 确认并立即生效船期价格表版本
|
||||
description: 只确认上传表中的仓库映射,不允许在线改价;阻断错误必须为零。发布事务将上一 ACTIVE 版本标记为 SUPERSEDED,始终只保留一份 ACTIVE。
|
||||
description: 只确认上传表中的仓库映射,不允许在线改价;阻断错误必须为零。发布事务只替换同 customerId 作用域的上一 ACTIVE 版本。计价时优先使用客户专属 ACTIVE 版本,不存在时回退公共 ACTIVE 版本。
|
||||
parameters: [{$ref: '#/components/parameters/VersionId'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/PriceBookConfirmationRequest'}}}}
|
||||
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
|
||||
@@ -545,6 +548,7 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
id: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
customerId: {type: string, pattern: '^(0|[1-9][0-9]*)$', description: 0 为公共报价,正数为客户专属报价}
|
||||
versionNo: {type: integer}
|
||||
originalFileName: {type: string, maxLength: 255}
|
||||
sourceFileId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
|
||||
+3
@@ -25,11 +25,14 @@ class PriceBookQueryServiceTest {
|
||||
void delegatesStableReadOperationsToTheOutputPort() {
|
||||
ChannelPrefixRow prefix = new ChannelPrefixRow("SEA", "IN", "OUT");
|
||||
when(reader.findActiveVersionId()).thenReturn(9L);
|
||||
when(reader.findEffectiveVersionId(10001L)).thenReturn(10L);
|
||||
when(reader.findChannelPrefix("SEA")).thenReturn(prefix);
|
||||
|
||||
assertThat(service.activeVersionId()).isEqualTo(9L);
|
||||
assertThat(service.effectiveVersionId(10001L)).isEqualTo(10L);
|
||||
assertThat(service.channelPrefix("SEA")).isSameAs(prefix);
|
||||
verify(reader).findActiveVersionId();
|
||||
verify(reader).findEffectiveVersionId(10001L);
|
||||
verify(reader).findChannelPrefix("SEA");
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class FbaPricingServiceTest {
|
||||
inbound.setCarrierId(null);
|
||||
inbound.setCarrierName(null);
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
|
||||
estimate("KG", "167.017", "2.345678", "391.77", "10.00"));
|
||||
|
||||
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-1");
|
||||
@@ -164,7 +164,7 @@ class FbaPricingServiceTest {
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(measurementRepository.findLatestMeasurement(TENANT, INBOUND)).thenReturn(measurement);
|
||||
when(measurementRepository.listItems(TENANT, 301L)).thenReturn(List.of(item));
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
|
||||
estimate("CBM", "1.0019", "100", "100.19", "0"));
|
||||
|
||||
service.calculate(TENANT, OPERATOR, INBOUND, NOW,
|
||||
@@ -202,7 +202,7 @@ class FbaPricingServiceTest {
|
||||
InboundPricingEntity inbound = inbound("200.000", "0.2000");
|
||||
inbound.setCustomerId(null);
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
|
||||
estimate("KG", "200.000", "2.000000", "400.00", "0.00"));
|
||||
|
||||
assertThat(service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-no-customer"))
|
||||
@@ -223,7 +223,7 @@ class FbaPricingServiceTest {
|
||||
rule.setId(601L); rule.setRuleCode("CBM-RULE"); rule.setRuleVersion(1);
|
||||
rule.setChargeMode("CBM"); rule.setDefaultRule(true);
|
||||
when(repository.matchRules(eq(inbound), any())).thenReturn(List.of(rule));
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
|
||||
estimate("CBM", "1.0001", "100.000000", "100.01", "0.00"));
|
||||
|
||||
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-2");
|
||||
@@ -245,7 +245,7 @@ class FbaPricingServiceTest {
|
||||
InboundPricingEntity inbound = inbound("100.000", "1.0000");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
|
||||
estimate("KG", "167.000", "2.000000", "334.00", "0.00"));
|
||||
|
||||
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "wait-split");
|
||||
@@ -304,11 +304,11 @@ class FbaPricingServiceTest {
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setMeasureReviewStatus("REJECTED");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
|
||||
estimate("CBM", "1.0000", "10", "10", "0"));
|
||||
|
||||
assertThat(service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3")).isNotNull();
|
||||
verify(priceBookEstimator).estimate(any());
|
||||
verify(priceBookEstimator).estimate(eq(TENANT), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -323,7 +323,7 @@ class FbaPricingServiceTest {
|
||||
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(sent);
|
||||
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(2);
|
||||
when(repository.supersedeCurrent(TENANT, 501L, "SENT")).thenReturn(1);
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
|
||||
estimate("CBM", "1.0000", "10", "10", "0"));
|
||||
|
||||
FeeSnapshotResponse response = service.calculate(
|
||||
@@ -365,7 +365,7 @@ class FbaPricingServiceTest {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(priceBookEstimator.estimate(any())).thenThrow(
|
||||
when(priceBookEstimator.estimate(anyLong(), any())).thenThrow(
|
||||
new BizException(ResultCode.NOT_FOUND, "当前条件没有匹配价格"));
|
||||
|
||||
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "none"))
|
||||
|
||||
+10
-9
@@ -29,6 +29,7 @@ import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -44,7 +45,7 @@ class MerchantPriceBookServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldResolveNormalAndOverseasInboundCodePrefixesFromChannelCatalog() {
|
||||
when(repository.activeVersionId()).thenReturn(7L);
|
||||
when(repository.effectiveVersionId(anyLong())).thenReturn(7L);
|
||||
when(repository.channelAvailable(7L, 10L, "MATSON_CLX_TRUCK")).thenReturn(true);
|
||||
when(repository.channels(7L, 10L)).thenReturn(List.of(
|
||||
channel(7L, "MATSON_CLX_TRUCK", PriceModel.FBA_WAREHOUSE_RATE)));
|
||||
@@ -59,7 +60,7 @@ class MerchantPriceBookServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldFallBackToNormalPrefixWhenChannelHasNoOverseasVariant() {
|
||||
when(repository.activeVersionId()).thenReturn(7L);
|
||||
when(repository.effectiveVersionId(anyLong())).thenReturn(7L);
|
||||
when(repository.channelAvailable(7L, 10L, "OA_HOUSTON_TRUCK")).thenReturn(true);
|
||||
when(repository.channels(7L, 10L)).thenReturn(List.of(
|
||||
channel(7L, "OA_HOUSTON_TRUCK", PriceModel.FBA_WAREHOUSE_RATE)));
|
||||
@@ -72,7 +73,7 @@ class MerchantPriceBookServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldValidateLegacyChannelWithoutRequiringAnInboundCodePrefix() {
|
||||
when(repository.activeVersionId()).thenReturn(7L);
|
||||
when(repository.effectiveVersionId(anyLong())).thenReturn(7L);
|
||||
when(repository.channelAvailable(7L, 10L, "LEGACY_CHANNEL")).thenReturn(true);
|
||||
var response = service.requireAvailableChannel(
|
||||
10001L, 10L, "LEGACY_CHANNEL", "FBA", false);
|
||||
@@ -84,7 +85,7 @@ class MerchantPriceBookServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyChannelsWithoutActiveVersion() {
|
||||
when(repository.activeVersionId()).thenReturn(null);
|
||||
when(repository.effectiveVersionId(anyLong())).thenReturn(null);
|
||||
|
||||
assertThat(service.channels(10001L, 10L)).isEmpty();
|
||||
verify(repository, never()).channels(1L, 10L);
|
||||
@@ -93,7 +94,7 @@ class MerchantPriceBookServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldExposeOnlyChannelsFromLatestActiveVersionAfterSwitch() {
|
||||
when(repository.activeVersionId()).thenReturn(1L, 2L);
|
||||
when(repository.effectiveVersionId(anyLong())).thenReturn(1L, 2L);
|
||||
when(repository.channels(1L, 10L)).thenReturn(List.of(channel(1L, "OLD", PriceModel.FBA_WAREHOUSE_RATE)));
|
||||
when(repository.channels(2L, 10L)).thenReturn(List.of(channel(2L, "NEW", PriceModel.FBA_WAREHOUSE_RATE)));
|
||||
|
||||
@@ -105,7 +106,7 @@ class MerchantPriceBookServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldExposeDistinctPricingTaxModesForActiveOriginChannels() {
|
||||
when(repository.activeVersionId()).thenReturn(7L);
|
||||
when(repository.effectiveVersionId(anyLong())).thenReturn(7L);
|
||||
when(repository.channels(7L, 10L)).thenReturn(List.of(
|
||||
channel(7L, "MATSON_CLX_TRUCK", PriceModel.FBA_WAREHOUSE_RATE),
|
||||
channel(7L, "OA_PARCEL", PriceModel.REGION_WEIGHT_TIER)));
|
||||
@@ -124,7 +125,7 @@ class MerchantPriceBookServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldKeepActiveChannelWhenItsTaxModeProjectionIsEmpty() {
|
||||
when(repository.activeVersionId()).thenReturn(7L);
|
||||
when(repository.effectiveVersionId(anyLong())).thenReturn(7L);
|
||||
when(repository.channels(7L, 10L)).thenReturn(List.of(
|
||||
channel(7L, "MATSON_CLX_TRUCK", PriceModel.FBA_WAREHOUSE_RATE)));
|
||||
when(repository.channelTaxModes(7L, 10L)).thenReturn(List.of());
|
||||
@@ -136,7 +137,7 @@ class MerchantPriceBookServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldRejectForgedOrRemovedChannel() {
|
||||
when(repository.activeVersionId()).thenReturn(2L);
|
||||
when(repository.effectiveVersionId(anyLong())).thenReturn(2L);
|
||||
when(repository.channels(2L, 10L)).thenReturn(List.of(channel(2L, "ACTIVE_CHANNEL",
|
||||
PriceModel.FBA_WAREHOUSE_RATE)));
|
||||
|
||||
@@ -282,7 +283,7 @@ class MerchantPriceBookServiceTest {
|
||||
}
|
||||
|
||||
private void stubChannel(long versionId, String code, PriceModel model) {
|
||||
when(repository.activeVersionId()).thenReturn(versionId);
|
||||
when(repository.effectiveVersionId(anyLong())).thenReturn(versionId);
|
||||
when(repository.channels(versionId, 10L)).thenReturn(List.of(channel(versionId, code, model)));
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -18,6 +18,7 @@ import java.math.BigDecimal;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -48,7 +49,7 @@ class OrderChannelChangePricingServiceTest {
|
||||
InboundPricingEntity inbound = inbound("CALCULATED");
|
||||
when(repository.findInboundByOrder(10001L, 77L)).thenReturn(inbound);
|
||||
when(repository.findLatestFee(10001L, 88L)).thenReturn(fee(91L, "CALCULATED"));
|
||||
when(estimator.estimate(any())).thenReturn(estimate());
|
||||
when(estimator.estimate(anyLong(), any())).thenReturn(estimate());
|
||||
|
||||
var response = service.validate(request(4));
|
||||
|
||||
@@ -64,7 +65,7 @@ class OrderChannelChangePricingServiceTest {
|
||||
InboundFeeEntity fee = fee(91L, "CALCULATED");
|
||||
when(repository.findInboundByOrderForUpdate(10001L, 77L)).thenReturn(inbound);
|
||||
when(repository.findLatestFeeForUpdate(10001L, 88L)).thenReturn(fee);
|
||||
when(estimator.estimate(any())).thenReturn(estimate());
|
||||
when(estimator.estimate(anyLong(), any())).thenReturn(estimate());
|
||||
when(repository.supersedeCurrent(10001L, 91L, "CALCULATED")).thenReturn(1);
|
||||
when(repository.applyOrderChannelChange(inbound, "MATSON_NEW", "新渠道")).thenReturn(1);
|
||||
when(repository.completeIdempotency(any(), org.mockito.ArgumentMatchers.eq(4L), any())).thenReturn(1);
|
||||
@@ -88,7 +89,7 @@ class OrderChannelChangePricingServiceTest {
|
||||
token.setRequestHash(requestHash(request(4)));
|
||||
when(repository.findInboundByOrderForUpdate(10001L, 77L)).thenReturn(inbound);
|
||||
when(repository.findIdempotency(10001L, 0L, "ORDER_CHANNEL_CHANGE", "77:4")).thenReturn(token);
|
||||
when(estimator.estimate(any())).thenReturn(estimate());
|
||||
when(estimator.estimate(anyLong(), any())).thenReturn(estimate());
|
||||
|
||||
assertThat(service.apply(request(4)).orderVersion()).isEqualTo(4L);
|
||||
|
||||
|
||||
+17
-1
@@ -64,6 +64,22 @@ class PlatformPriceBookServiceTest {
|
||||
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listsCustomerSpecificVersionsWithinRequestedScope() {
|
||||
PriceBookVersionEntity customerVersion = version(21L, PriceBookStatus.ACTIVE);
|
||||
customerVersion.setTenantId(10001L);
|
||||
when(repository.listVersions(10001L, PriceBookStatus.ACTIVE.name()))
|
||||
.thenReturn(List.of(customerVersion));
|
||||
|
||||
var result = service.list(10001L, PriceBookStatus.ACTIVE);
|
||||
|
||||
assertThat(result).singleElement().satisfies(summary -> {
|
||||
assertThat(summary.id()).isEqualTo(21L);
|
||||
assertThat(summary.customerId()).isEqualTo(10001L);
|
||||
});
|
||||
verify(repository).listVersions(10001L, PriceBookStatus.ACTIVE.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fileServiceFailureMarksAllocatedVersionFailed() {
|
||||
executeTransactionsImmediately();
|
||||
@@ -121,7 +137,7 @@ class PlatformPriceBookServiceTest {
|
||||
assertThat(Arrays.stream(PlatformPriceBookService.class.getDeclaredMethods())
|
||||
.filter(method -> java.lang.reflect.Modifier.isPublic(method.getModifiers()))
|
||||
.map(java.lang.reflect.Method::getName).toList())
|
||||
.containsExactlyInAnyOrder("list", "upload", "detail", "publish", "source");
|
||||
.containsOnly("list", "upload", "detail", "publish", "source");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user