feat(lastleg): add overseas container fee sources
This commit is contained in:
@@ -1267,6 +1267,7 @@ CREATE TABLE IF NOT EXISTS ll_finance_fee_source (
|
|||||||
created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间',
|
created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间',
|
||||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_finance_fee_version(source_fee_id,source_version),
|
PRIMARY KEY(id), UNIQUE KEY uk_ll_finance_fee_version(source_fee_id,source_version),
|
||||||
KEY idx_ll_finance_fee_customer(customer_id,source_status,created_at), KEY idx_ll_finance_fee_business(business_type,business_no,created_at),
|
KEY idx_ll_finance_fee_customer(customer_id,source_status,created_at), KEY idx_ll_finance_fee_business(business_type,business_no,created_at),
|
||||||
|
KEY idx_ll_finance_fee_container_customer(container_booking_id,customer_id,source_status,created_at),
|
||||||
CONSTRAINT chk_ll_finance_fee_amount CHECK(amount>0), CONSTRAINT chk_ll_finance_fee_currency CHECK(currency='CNY'),
|
CONSTRAINT chk_ll_finance_fee_amount CHECK(amount>0), CONSTRAINT chk_ll_finance_fee_currency CHECK(currency='CNY'),
|
||||||
CONSTRAINT chk_ll_finance_fee_direction CHECK(direction IN('RECEIVABLE','PAYABLE')),
|
CONSTRAINT chk_ll_finance_fee_direction CHECK(direction IN('RECEIVABLE','PAYABLE')),
|
||||||
CONSTRAINT chk_ll_finance_fee_review CHECK(review_status IN('DRAFT','PENDING','APPROVED','REJECTED')),
|
CONSTRAINT chk_ll_finance_fee_review CHECK(review_status IN('DRAFT','PENDING','APPROVED','REJECTED')),
|
||||||
@@ -1274,6 +1275,9 @@ CREATE TABLE IF NOT EXISTS ll_finance_fee_source (
|
|||||||
CONSTRAINT chk_ll_finance_fee_status CHECK(source_status IN('ACTIVE','SUPERSEDED','VOIDED'))
|
CONSTRAINT chk_ll_finance_fee_status CHECK(source_status IN('ACTIVE','SUPERSEDED','VOIDED'))
|
||||||
) ENGINE=InnoDB COMMENT='海外业务不可变费用来源版本;发布后经跨区事件进入Finance';
|
) ENGINE=InnoDB COMMENT='海外业务不可变费用来源版本;发布后经跨区事件进入Finance';
|
||||||
|
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_finance_fee_source' AND index_name='idx_ll_finance_fee_container_customer'), 'SELECT 1', 'ALTER TABLE ll_finance_fee_source ADD KEY idx_ll_finance_fee_container_customer(container_booking_id,customer_id,source_status,created_at)'));
|
||||||
|
PREPARE ll_finance_fee_ddl FROM @ddl; EXECUTE ll_finance_fee_ddl; DEALLOCATE PREPARE ll_finance_fee_ddl;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_finance_fee_command (
|
CREATE TABLE IF NOT EXISTS ll_finance_fee_command (
|
||||||
id BIGINT NOT NULL COMMENT '幂等命令ID', operator_id VARCHAR(128) NOT NULL COMMENT '操作人ID',
|
id BIGINT NOT NULL COMMENT '幂等命令ID', operator_id VARCHAR(128) NOT NULL COMMENT '操作人ID',
|
||||||
action VARCHAR(16) NOT NULL COMMENT 'CREATE、SUBMIT、REVIEW、ADJUST或VOID', idempotency_key VARCHAR(128) NOT NULL COMMENT '操作幂等键',
|
action VARCHAR(16) NOT NULL COMMENT 'CREATE、SUBMIT、REVIEW、ADJUST或VOID', idempotency_key VARCHAR(128) NOT NULL COMMENT '操作幂等键',
|
||||||
@@ -1689,6 +1693,9 @@ CREATE TABLE IF NOT EXISTS ll_finance_fee_source (
|
|||||||
CONSTRAINT chk_ll_finance_fee_status CHECK(source_status IN('ACTIVE','SUPERSEDED','VOIDED'))
|
CONSTRAINT chk_ll_finance_fee_status CHECK(source_status IN('ACTIVE','SUPERSEDED','VOIDED'))
|
||||||
) ENGINE=InnoDB COMMENT='海外业务不可变费用来源版本;发布后经跨区事件进入Finance';
|
) ENGINE=InnoDB COMMENT='海外业务不可变费用来源版本;发布后经跨区事件进入Finance';
|
||||||
|
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_finance_fee_source' AND index_name='idx_ll_finance_fee_container_customer'), 'SELECT 1', 'ALTER TABLE ll_finance_fee_source ADD KEY idx_ll_finance_fee_container_customer(container_booking_id,customer_id,source_status,created_at)'));
|
||||||
|
PREPARE ll_finance_fee_ddl FROM @ddl; EXECUTE ll_finance_fee_ddl; DEALLOCATE PREPARE ll_finance_fee_ddl;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_finance_fee_command (
|
CREATE TABLE IF NOT EXISTS ll_finance_fee_command (
|
||||||
id BIGINT NOT NULL COMMENT '幂等命令ID', operator_id VARCHAR(128) NOT NULL COMMENT '操作人ID',
|
id BIGINT NOT NULL COMMENT '幂等命令ID', operator_id VARCHAR(128) NOT NULL COMMENT '操作人ID',
|
||||||
action VARCHAR(16) NOT NULL COMMENT 'CREATE、SUBMIT、REVIEW、ADJUST或VOID', idempotency_key VARCHAR(128) NOT NULL COMMENT '操作幂等键',
|
action VARCHAR(16) NOT NULL COMMENT 'CREATE、SUBMIT、REVIEW、ADJUST或VOID', idempotency_key VARCHAR(128) NOT NULL COMMENT '操作幂等键',
|
||||||
|
|||||||
+14
-2
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ApiResponse;
|
|||||||
import com.baiyue.common.security.AuthenticatedUser;
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ActionRequest;
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ActionRequest;
|
||||||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.AdjustRequest;
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.AdjustRequest;
|
||||||
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ContainerCustomerOption;
|
||||||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ReviewRequest;
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ReviewRequest;
|
||||||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
|
||||||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.VoidRequest;
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.VoidRequest;
|
||||||
@@ -37,8 +38,19 @@ public class PlatformOverseasFeeSourceController {
|
|||||||
public ApiResponse<List<OverseasFeeSource>> list(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<List<OverseasFeeSource>> list(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@RequestParam(required = false) String customerId,
|
@RequestParam(required = false) String customerId,
|
||||||
@RequestParam(required = false) String businessType,
|
@RequestParam(required = false) String businessType,
|
||||||
@RequestParam(required = false) String businessNo) {
|
@RequestParam(required = false) String businessNo,
|
||||||
requirePlatform(user); return ApiResponse.success(service.list(customerId, businessType, businessNo));
|
@RequestParam(required = false) Long containerBookingId) {
|
||||||
|
requirePlatform(user);
|
||||||
|
return ApiResponse.success(service.list(customerId, businessType, businessNo, containerBookingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/container-customers")
|
||||||
|
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
|
||||||
|
public ApiResponse<List<ContainerCustomerOption>> listContainerCustomers(
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@RequestParam long containerBookingId) {
|
||||||
|
requirePlatform(user);
|
||||||
|
return ApiResponse.success(service.listContainerCustomers(containerBookingId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
|
|||||||
+4
-2
@@ -15,11 +15,13 @@ import java.util.List;
|
|||||||
public final class OverseasFeeSourceDtos {
|
public final class OverseasFeeSourceDtos {
|
||||||
private OverseasFeeSourceDtos() { }
|
private OverseasFeeSourceDtos() { }
|
||||||
|
|
||||||
|
public record ContainerCustomerOption(String customerId, String customerName) { }
|
||||||
|
|
||||||
public record SaveRequest(
|
public record SaveRequest(
|
||||||
@NotBlank @Size(max = 128) String customerId,
|
@NotBlank @Size(max = 128) String customerId,
|
||||||
@Size(max = 200) String customerName,
|
@Size(max = 200) String customerName,
|
||||||
@NotBlank @Pattern(regexp = "OVERSEAS_INBOUND_FEE|OVERSEAS_PROCUREMENT_FEE|OVERSEAS_TRUCK_FEE|OVERSEAS_LOOSEBOARD_FEE|OVERSEAS_WAREHOUSE_FEE") String sourceType,
|
@NotBlank @Pattern(regexp = "OVERSEAS_INBOUND_FEE|OVERSEAS_PROCUREMENT_FEE|OVERSEAS_TRUCK_FEE|OVERSEAS_LOOSEBOARD_FEE|OVERSEAS_WAREHOUSE_FEE|OVERSEAS_CONTAINER_FEE") String sourceType,
|
||||||
@NotBlank @Pattern(regexp = "OVERSEAS_INBOUND|OVERSEAS_PROCUREMENT|US_TRUCK|LOCAL_LOOSEBOARD|WAREHOUSE_SERVICE") String businessType,
|
@NotBlank @Pattern(regexp = "OVERSEAS_INBOUND|OVERSEAS_PROCUREMENT|US_TRUCK|LOCAL_LOOSEBOARD|WAREHOUSE_SERVICE|OVERSEAS_CONTAINER") String businessType,
|
||||||
@Min(1) Long businessId,
|
@Min(1) Long businessId,
|
||||||
@NotBlank @Size(max = 128) String businessNo,
|
@NotBlank @Size(max = 128) String businessNo,
|
||||||
@Min(1) Long orderId,
|
@Min(1) Long orderId,
|
||||||
|
|||||||
+45
@@ -1,18 +1,37 @@
|
|||||||
package com.baiyue.lastleg.financefee.service;
|
package com.baiyue.lastleg.financefee.service;
|
||||||
|
|
||||||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
|
||||||
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ContainerCustomerOption;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class OverseasFeeBusinessGuard {
|
public class OverseasFeeBusinessGuard {
|
||||||
private final JdbcTemplate jdbc;
|
private final JdbcTemplate jdbc;
|
||||||
|
|
||||||
|
public List<ContainerCustomerOption> listContainerCustomers(long containerBookingId) {
|
||||||
|
List<ContainerCustomerOption> customers = jdbc.query(
|
||||||
|
"SELECT customer_id,CASE WHEN COUNT(DISTINCT NULLIF(TRIM(customer_name),''))=1 "
|
||||||
|
+ "THEN MAX(NULLIF(TRIM(customer_name),'')) ELSE NULL END AS customer_name "
|
||||||
|
+ "FROM ll_order WHERE source_container_booking_id=? GROUP BY customer_id ORDER BY customer_id",
|
||||||
|
(result, rowNum) -> new ContainerCustomerOption(
|
||||||
|
result.getString("customer_id"), result.getString("customer_name")),
|
||||||
|
String.valueOf(containerBookingId));
|
||||||
|
if (customers.isEmpty()) throw bad("真实货柜不存在或尚无尾程订单");
|
||||||
|
return customers;
|
||||||
|
}
|
||||||
|
|
||||||
public void requireOwned(SaveRequest fee) {
|
public void requireOwned(SaveRequest fee) {
|
||||||
|
if ("OVERSEAS_CONTAINER".equals(fee.businessType())) {
|
||||||
|
requireOwnedContainer(fee);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (fee.businessId() == null) {
|
if (fee.businessId() == null) {
|
||||||
if (!"WAREHOUSE_SERVICE".equals(fee.businessType()))
|
if (!"WAREHOUSE_SERVICE".equals(fee.businessType()))
|
||||||
throw bad("业务记录ID不能为空");
|
throw bad("业务记录ID不能为空");
|
||||||
@@ -38,6 +57,32 @@ public class OverseasFeeBusinessGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void requireOwnedContainer(SaveRequest fee) {
|
||||||
|
if (fee.businessId() == null || fee.containerBookingId() == null
|
||||||
|
|| !fee.businessId().equals(fee.containerBookingId()))
|
||||||
|
throw bad("海外货柜费用必须以真实来源柜ID同时关联业务和订舱柜");
|
||||||
|
if (blank(fee.containerNo()) || !fee.businessNo().trim().equals(fee.containerNo().trim()))
|
||||||
|
throw bad("海外货柜费用的业务编号必须与真实集装箱号一致");
|
||||||
|
|
||||||
|
String bookingId = String.valueOf(fee.containerBookingId());
|
||||||
|
Integer total = jdbc.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM ll_order WHERE source_container_booking_id=?",
|
||||||
|
Integer.class, bookingId);
|
||||||
|
if (total == null || total < 1) throw bad("费用关联的真实货柜不存在");
|
||||||
|
|
||||||
|
Integer inconsistent = jdbc.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM ll_order WHERE source_container_booking_id=? "
|
||||||
|
+ "AND (container_no IS NULL OR TRIM(container_no)='' OR BINARY TRIM(container_no)<>BINARY ?)",
|
||||||
|
Integer.class, bookingId, fee.containerNo().trim());
|
||||||
|
if (inconsistent == null || inconsistent > 0)
|
||||||
|
throw bad("真实货柜的集装箱号缺失或不一致");
|
||||||
|
|
||||||
|
Integer owned = jdbc.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM ll_order WHERE source_container_booking_id=? AND customer_id=?",
|
||||||
|
Integer.class, bookingId, fee.customerId().trim());
|
||||||
|
if (owned == null || owned < 1) throw bad("该客户不属于费用关联的真实货柜");
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean blank(String value) { return value == null || value.isBlank(); }
|
private static boolean blank(String value) { return value == null || value.isBlank(); }
|
||||||
private static ResponseStatusException bad(String message) {
|
private static ResponseStatusException bad(String message) {
|
||||||
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||||
|
|||||||
+23
-1
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.financefee.service;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.AdjustRequest;
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.AdjustRequest;
|
||||||
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ContainerCustomerOption;
|
||||||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ReviewRequest;
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ReviewRequest;
|
||||||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
|
||||||
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.VoidRequest;
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.VoidRequest;
|
||||||
@@ -26,26 +27,36 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.HexFormat;
|
import java.util.HexFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class OverseasFeeSourceService {
|
public class OverseasFeeSourceService {
|
||||||
|
static final Set<String> OVERSEAS_CONTAINER_FEE_ITEMS = Set.of(
|
||||||
|
"PORT_FEE", "STORAGE_FEE", "CONTAINER_REPAIR_FEE", "CRANE_FEE", "TERMINAL_DRAYAGE_FEE", "OTHER");
|
||||||
|
|
||||||
private final OverseasFeeSourceMapper mapper;
|
private final OverseasFeeSourceMapper mapper;
|
||||||
private final OverseasFeeCommandMapper commands;
|
private final OverseasFeeCommandMapper commands;
|
||||||
private final OverseasFeeBusinessGuard businessGuard;
|
private final OverseasFeeBusinessGuard businessGuard;
|
||||||
private final OverseasFeeOutboxService outbox;
|
private final OverseasFeeOutboxService outbox;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public List<OverseasFeeSource> list(String customerId, String businessType, String businessNo) {
|
public List<OverseasFeeSource> list(String customerId, String businessType, String businessNo,
|
||||||
|
Long containerBookingId) {
|
||||||
return mapper.selectList(new LambdaQueryWrapper<OverseasFeeSource>()
|
return mapper.selectList(new LambdaQueryWrapper<OverseasFeeSource>()
|
||||||
.eq(text(customerId) != null, OverseasFeeSource::getCustomerId, text(customerId))
|
.eq(text(customerId) != null, OverseasFeeSource::getCustomerId, text(customerId))
|
||||||
.eq(text(businessType) != null, OverseasFeeSource::getBusinessType, text(businessType))
|
.eq(text(businessType) != null, OverseasFeeSource::getBusinessType, text(businessType))
|
||||||
.like(text(businessNo) != null, OverseasFeeSource::getBusinessNo, text(businessNo))
|
.like(text(businessNo) != null, OverseasFeeSource::getBusinessNo, text(businessNo))
|
||||||
|
.eq(containerBookingId != null, OverseasFeeSource::getContainerBookingId, containerBookingId)
|
||||||
.inSql(OverseasFeeSource::getId, "SELECT MAX(v.id) FROM ll_finance_fee_source v GROUP BY v.source_fee_id")
|
.inSql(OverseasFeeSource::getId, "SELECT MAX(v.id) FROM ll_finance_fee_source v GROUP BY v.source_fee_id")
|
||||||
.orderByDesc(OverseasFeeSource::getCreatedAt).orderByDesc(OverseasFeeSource::getId));
|
.orderByDesc(OverseasFeeSource::getCreatedAt).orderByDesc(OverseasFeeSource::getId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ContainerCustomerOption> listContainerCustomers(long containerBookingId) {
|
||||||
|
return businessGuard.listContainerCustomers(containerBookingId);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public OverseasFeeSource create(String operatorId, String key, SaveRequest request) {
|
public OverseasFeeSource create(String operatorId, String key, SaveRequest request) {
|
||||||
OverseasFeeCommand replay = replay(operatorId, "CREATE", key, request);
|
OverseasFeeCommand replay = replay(operatorId, "CREATE", key, request);
|
||||||
@@ -99,6 +110,13 @@ public class OverseasFeeSourceService {
|
|||||||
validate(request.fee()); businessGuard.requireOwned(request.fee());
|
validate(request.fee()); businessGuard.requireOwned(request.fee());
|
||||||
if (!old.getCustomerId().equals(request.fee().customerId().trim()))
|
if (!old.getCustomerId().equals(request.fee().customerId().trim()))
|
||||||
throw conflict("调整不得改变费用所属客户");
|
throw conflict("调整不得改变费用所属客户");
|
||||||
|
if (!old.getSourceType().equals(request.fee().sourceType())
|
||||||
|
|| !old.getBusinessType().equals(request.fee().businessType())
|
||||||
|
|| !Objects.equals(old.getBusinessId(), request.fee().businessId())
|
||||||
|
|| !old.getBusinessNo().equals(request.fee().businessNo().trim())
|
||||||
|
|| !Objects.equals(old.getContainerBookingId(), request.fee().containerBookingId())
|
||||||
|
|| !Objects.equals(old.getContainerNo(), text(request.fee().containerNo())))
|
||||||
|
throw conflict("调整不得改变费用来源或业务归属");
|
||||||
old.setSourceStatus("SUPERSEDED"); old.setUpdatedBy(operatorId); old.setUpdatedAt(now()); mapper.updateById(old);
|
old.setSourceStatus("SUPERSEDED"); old.setUpdatedBy(operatorId); old.setUpdatedAt(now()); mapper.updateById(old);
|
||||||
OverseasFeeSource next = build(IdWorker.getId(), sourceFeeId, old.getSourceVersion() + 1, operatorId, request.fee());
|
OverseasFeeSource next = build(IdWorker.getId(), sourceFeeId, old.getSourceVersion() + 1, operatorId, request.fee());
|
||||||
next.setReviewStatus("PENDING");
|
next.setReviewStatus("PENDING");
|
||||||
@@ -147,9 +165,13 @@ public class OverseasFeeSourceService {
|
|||||||
case "US_TRUCK" -> "OVERSEAS_TRUCK_FEE";
|
case "US_TRUCK" -> "OVERSEAS_TRUCK_FEE";
|
||||||
case "LOCAL_LOOSEBOARD" -> "OVERSEAS_LOOSEBOARD_FEE";
|
case "LOCAL_LOOSEBOARD" -> "OVERSEAS_LOOSEBOARD_FEE";
|
||||||
case "WAREHOUSE_SERVICE" -> "OVERSEAS_WAREHOUSE_FEE";
|
case "WAREHOUSE_SERVICE" -> "OVERSEAS_WAREHOUSE_FEE";
|
||||||
|
case "OVERSEAS_CONTAINER" -> "OVERSEAS_CONTAINER_FEE";
|
||||||
default -> "";
|
default -> "";
|
||||||
};
|
};
|
||||||
if (!expected.equals(value.sourceType())) throw bad("来源类型与业务类型不匹配");
|
if (!expected.equals(value.sourceType())) throw bad("来源类型与业务类型不匹配");
|
||||||
|
if ("OVERSEAS_CONTAINER".equals(value.businessType())
|
||||||
|
&& !OVERSEAS_CONTAINER_FEE_ITEMS.contains(value.feeItemCode().trim()))
|
||||||
|
throw bad("海外货柜费用项目不受支持");
|
||||||
boolean payable = "PAYABLE".equals(value.direction());
|
boolean payable = "PAYABLE".equals(value.direction());
|
||||||
boolean files = value.paymentVoucherFileIds() != null && !value.paymentVoucherFileIds().isEmpty();
|
boolean files = value.paymentVoucherFileIds() != null && !value.paymentVoucherFileIds().isEmpty();
|
||||||
if (!payable) {
|
if (!payable) {
|
||||||
|
|||||||
@@ -799,6 +799,7 @@ CREATE TABLE IF NOT EXISTS ll_finance_fee_source (
|
|||||||
created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间',
|
created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间',
|
||||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_finance_fee_version(source_fee_id,source_version),
|
PRIMARY KEY(id), UNIQUE KEY uk_ll_finance_fee_version(source_fee_id,source_version),
|
||||||
KEY idx_ll_finance_fee_customer(customer_id,source_status,created_at), KEY idx_ll_finance_fee_business(business_type,business_no,created_at),
|
KEY idx_ll_finance_fee_customer(customer_id,source_status,created_at), KEY idx_ll_finance_fee_business(business_type,business_no,created_at),
|
||||||
|
KEY idx_ll_finance_fee_container_customer(container_booking_id,customer_id,source_status,created_at),
|
||||||
CONSTRAINT chk_ll_finance_fee_amount CHECK(amount>0), CONSTRAINT chk_ll_finance_fee_currency CHECK(currency='CNY'),
|
CONSTRAINT chk_ll_finance_fee_amount CHECK(amount>0), CONSTRAINT chk_ll_finance_fee_currency CHECK(currency='CNY'),
|
||||||
CONSTRAINT chk_ll_finance_fee_direction CHECK(direction IN('RECEIVABLE','PAYABLE')),
|
CONSTRAINT chk_ll_finance_fee_direction CHECK(direction IN('RECEIVABLE','PAYABLE')),
|
||||||
CONSTRAINT chk_ll_finance_fee_review CHECK(review_status IN('DRAFT','PENDING','APPROVED','REJECTED')),
|
CONSTRAINT chk_ll_finance_fee_review CHECK(review_status IN('DRAFT','PENDING','APPROVED','REJECTED')),
|
||||||
@@ -806,6 +807,9 @@ CREATE TABLE IF NOT EXISTS ll_finance_fee_source (
|
|||||||
CONSTRAINT chk_ll_finance_fee_status CHECK(source_status IN('ACTIVE','SUPERSEDED','VOIDED'))
|
CONSTRAINT chk_ll_finance_fee_status CHECK(source_status IN('ACTIVE','SUPERSEDED','VOIDED'))
|
||||||
) ENGINE=InnoDB COMMENT='海外业务不可变费用来源版本;发布后经跨区事件进入Finance';
|
) ENGINE=InnoDB COMMENT='海外业务不可变费用来源版本;发布后经跨区事件进入Finance';
|
||||||
|
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_finance_fee_source' AND index_name='idx_ll_finance_fee_container_customer'), 'SELECT 1', 'ALTER TABLE ll_finance_fee_source ADD KEY idx_ll_finance_fee_container_customer(container_booking_id,customer_id,source_status,created_at)'));
|
||||||
|
PREPARE ll_finance_fee_ddl FROM @ddl; EXECUTE ll_finance_fee_ddl; DEALLOCATE PREPARE ll_finance_fee_ddl;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_finance_fee_command (
|
CREATE TABLE IF NOT EXISTS ll_finance_fee_command (
|
||||||
id BIGINT NOT NULL COMMENT '幂等命令ID', operator_id VARCHAR(128) NOT NULL COMMENT '操作人ID',
|
id BIGINT NOT NULL COMMENT '幂等命令ID', operator_id VARCHAR(128) NOT NULL COMMENT '操作人ID',
|
||||||
action VARCHAR(16) NOT NULL COMMENT 'CREATE、SUBMIT、REVIEW、ADJUST或VOID', idempotency_key VARCHAR(128) NOT NULL COMMENT '操作幂等键',
|
action VARCHAR(16) NOT NULL COMMENT 'CREATE、SUBMIT、REVIEW、ADJUST或VOID', idempotency_key VARCHAR(128) NOT NULL COMMENT '操作幂等键',
|
||||||
|
|||||||
@@ -9,14 +9,28 @@ paths:
|
|||||||
- { in: query, name: customerId, schema: { type: string } }
|
- { in: query, name: customerId, schema: { type: string } }
|
||||||
- { in: query, name: businessType, schema: { $ref: '#/components/schemas/BusinessType' } }
|
- { in: query, name: businessType, schema: { $ref: '#/components/schemas/BusinessType' } }
|
||||||
- { in: query, name: businessNo, schema: { type: string } }
|
- { in: query, name: businessNo, schema: { type: string } }
|
||||||
|
- { in: query, name: containerBookingId, description: 精确查询真实来源柜下全部客户的费用;可再传customerId收窄到单个客户, schema: { type: string, pattern: '^[1-9][0-9]*$' } }
|
||||||
responses: { '200': { description: 当前版本列表 } }
|
responses: { '200': { description: 当前版本列表 } }
|
||||||
post:
|
post:
|
||||||
summary: 创建海外业务费用来源草稿
|
summary: 创建海外业务费用来源草稿
|
||||||
description: 客户和业务编号必填;入仓ID、入仓码均按业务事实选填,不得为补字段伪造。
|
description: 客户和业务编号必填;入仓ID、入仓码均按业务事实选填,不得为补字段伪造。海外货柜费用必须以相同的businessId和containerBookingId引用真实来源柜,且businessNo与containerNo均为该柜真实集装箱号。
|
||||||
security: [{ bearerAuth: [] }]
|
security: [{ bearerAuth: [] }]
|
||||||
parameters: [{ in: header, name: Idempotency-Key, required: true, schema: { type: string, maxLength: 128 } }]
|
parameters: [{ in: header, name: Idempotency-Key, required: true, schema: { type: string, maxLength: 128 } }]
|
||||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/SaveRequest' } } } }
|
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/SaveRequest' } } } }
|
||||||
responses: { '200': { description: 草稿费用来源 }, '400': { description: 业务归属或付款事实无效 } }
|
responses: { '200': { description: 草稿费用来源 }, '400': { description: 业务归属或付款事实无效 } }
|
||||||
|
/api/platform/v1/last-leg/finance-fee-sources/container-customers:
|
||||||
|
get:
|
||||||
|
summary: 查询真实来源柜内客户选项
|
||||||
|
description: 单次按真实来源柜聚合全部客户,供柜详情维护费用时选择,不逐票查询。
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
parameters:
|
||||||
|
- { in: query, name: containerBookingId, required: true, schema: { type: string, pattern: '^[1-9][0-9]*$' } }
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 柜内客户选项
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: '#/components/schemas/ContainerCustomerListResponse' }
|
||||||
/api/platform/v1/last-leg/finance-fee-sources/{sourceFeeId}/{action}:
|
/api/platform/v1/last-leg/finance-fee-sources/{sourceFeeId}/{action}:
|
||||||
post:
|
post:
|
||||||
summary: 提交、审核、调整或作废海外业务费用来源
|
summary: 提交、审核、调整或作废海外业务费用来源
|
||||||
@@ -28,23 +42,49 @@ paths:
|
|||||||
responses: { '200': { description: 当前费用来源版本 }, '409': { description: 状态、版本或幂等冲突 } }
|
responses: { '200': { description: 当前费用来源版本 }, '409': { description: 状态、版本或幂等冲突 } }
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
BusinessType: { type: string, enum: [OVERSEAS_INBOUND, OVERSEAS_PROCUREMENT, US_TRUCK, LOCAL_LOOSEBOARD, WAREHOUSE_SERVICE] }
|
BusinessType: { type: string, enum: [OVERSEAS_INBOUND, OVERSEAS_PROCUREMENT, US_TRUCK, LOCAL_LOOSEBOARD, WAREHOUSE_SERVICE, OVERSEAS_CONTAINER] }
|
||||||
|
OverseasContainerFeeItem:
|
||||||
|
type: string
|
||||||
|
description: 海外阶段柜级费用项目:港口费、堆存费、修箱费、吊机费、码头拖车费、其他。
|
||||||
|
enum: [PORT_FEE, STORAGE_FEE, CONTAINER_REPAIR_FEE, CRANE_FEE, TERMINAL_DRAYAGE_FEE, OTHER]
|
||||||
|
ContainerCustomerOption:
|
||||||
|
type: object
|
||||||
|
required: [customerId]
|
||||||
|
properties:
|
||||||
|
customerId: { type: string }
|
||||||
|
customerName: { type: string, nullable: true }
|
||||||
|
ContainerCustomerListResponse:
|
||||||
|
type: object
|
||||||
|
required: [data]
|
||||||
|
properties:
|
||||||
|
code: { type: string }
|
||||||
|
message: { type: string }
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items: { $ref: '#/components/schemas/ContainerCustomerOption' }
|
||||||
|
requestId: { type: string, nullable: true }
|
||||||
|
traceId: { type: string, nullable: true }
|
||||||
|
timestamp: { type: string, format: date-time }
|
||||||
SaveRequest:
|
SaveRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [customerId, sourceType, businessType, businessNo, feeItemCode, feeItemName, direction, businessDate, currency, amount]
|
required: [customerId, sourceType, businessType, businessNo, feeItemCode, feeItemName, direction, businessDate, currency, amount]
|
||||||
properties:
|
properties:
|
||||||
customerId: { type: string }
|
customerId: { type: string }
|
||||||
sourceType: { type: string, enum: [OVERSEAS_INBOUND_FEE, OVERSEAS_PROCUREMENT_FEE, OVERSEAS_TRUCK_FEE, OVERSEAS_LOOSEBOARD_FEE, OVERSEAS_WAREHOUSE_FEE] }
|
sourceType: { type: string, enum: [OVERSEAS_INBOUND_FEE, OVERSEAS_PROCUREMENT_FEE, OVERSEAS_TRUCK_FEE, OVERSEAS_LOOSEBOARD_FEE, OVERSEAS_WAREHOUSE_FEE, OVERSEAS_CONTAINER_FEE] }
|
||||||
businessType: { $ref: '#/components/schemas/BusinessType' }
|
businessType: { $ref: '#/components/schemas/BusinessType' }
|
||||||
businessId: { type: string, nullable: true }
|
businessId: { type: string, pattern: '^[1-9][0-9]*$', nullable: true }
|
||||||
businessNo: { type: string, maxLength: 128 }
|
businessNo: { type: string, maxLength: 128 }
|
||||||
|
containerBookingId: { type: string, pattern: '^[1-9][0-9]*$', nullable: true }
|
||||||
|
containerNo: { type: string, maxLength: 64, nullable: true }
|
||||||
warehouseId: { type: string, nullable: true }
|
warehouseId: { type: string, nullable: true }
|
||||||
inboundId: { type: string, nullable: true }
|
inboundId: { type: string, nullable: true }
|
||||||
inboundCode: { type: string, maxLength: 64, nullable: true }
|
inboundCode: { type: string, maxLength: 64, nullable: true }
|
||||||
feeItemCode: { type: string }
|
feeItemCode:
|
||||||
|
type: string
|
||||||
|
description: OVERSEAS_CONTAINER时必须取OverseasContainerFeeItem枚举;其他业务类型沿用各自费用项目编码。
|
||||||
feeItemName: { type: string }
|
feeItemName: { type: string }
|
||||||
direction: { type: string, enum: [RECEIVABLE, PAYABLE] }
|
direction: { type: string, enum: [RECEIVABLE, PAYABLE] }
|
||||||
businessDate: { type: string, format: date }
|
businessDate: { type: string, format: date }
|
||||||
currency: { type: string, enum: [CNY] }
|
currency: { type: string, enum: [CNY] }
|
||||||
amount: { type: number, format: decimal, exclusiveMinimum: 0 }
|
amount: { type: number, format: decimal, minimum: 0, exclusiveMinimum: true }
|
||||||
securitySchemes: { bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT } }
|
securitySchemes: { bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT } }
|
||||||
|
|||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
package com.baiyue.lastleg.financefee.service;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
|
||||||
|
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
|
||||||
|
import com.baiyue.lastleg.financefee.mapper.OverseasFeeCommandMapper;
|
||||||
|
import com.baiyue.lastleg.financefee.mapper.OverseasFeeSourceMapper;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
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.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class OverseasContainerFeeSourceServiceTest {
|
||||||
|
private final OverseasFeeSourceMapper mapper = mock(OverseasFeeSourceMapper.class);
|
||||||
|
private final OverseasFeeCommandMapper commands = mock(OverseasFeeCommandMapper.class);
|
||||||
|
private final OverseasFeeBusinessGuard guard = mock(OverseasFeeBusinessGuard.class);
|
||||||
|
private final OverseasFeeOutboxService outbox = mock(OverseasFeeOutboxService.class);
|
||||||
|
private final OverseasFeeSourceService service = new OverseasFeeSourceService(
|
||||||
|
mapper, commands, guard, outbox, new ObjectMapper().findAndRegisterModules());
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsContainerFeeUsingTheRealContainerAsItsStableBusinessReference() {
|
||||||
|
SaveRequest request = OverseasFeeBusinessGuardTest.containerFee("customer-a", 701L, "MSCU1234567");
|
||||||
|
when(commands.find("operator-a", "CREATE", "create-container-fee")).thenReturn(null);
|
||||||
|
OverseasFeeSource result = service.create("operator-a", "create-container-fee", request);
|
||||||
|
|
||||||
|
verify(guard).requireOwned(request);
|
||||||
|
ArgumentCaptor<OverseasFeeSource> inserted = ArgumentCaptor.forClass(OverseasFeeSource.class);
|
||||||
|
verify(mapper).insert(inserted.capture());
|
||||||
|
assertThat(result).isSameAs(inserted.getValue());
|
||||||
|
assertThat(result.getSourceType()).isEqualTo("OVERSEAS_CONTAINER_FEE");
|
||||||
|
assertThat(result.getBusinessType()).isEqualTo("OVERSEAS_CONTAINER");
|
||||||
|
assertThat(result.getBusinessId()).isEqualTo(701L);
|
||||||
|
assertThat(result.getContainerBookingId()).isEqualTo(701L);
|
||||||
|
assertThat(result.getSourceNo()).isEqualTo("MSCU1234567");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsUnsupportedContainerFeeItems() {
|
||||||
|
SaveRequest source = OverseasFeeBusinessGuardTest.containerFee("customer-a", 701L, "MSCU1234567");
|
||||||
|
SaveRequest request = new SaveRequest(source.customerId(), source.customerName(), source.sourceType(),
|
||||||
|
source.businessType(), source.businessId(), source.businessNo(), source.orderId(), source.inboundId(),
|
||||||
|
source.inboundCode(), source.containerId(), source.containerBookingId(), source.containerNo(),
|
||||||
|
source.warehouseId(), "CUSTOM_FEE", "自定义费", source.direction(), source.supplierId(),
|
||||||
|
source.supplierName(), source.supplierCategory(), source.settlementMode(), source.paidAt(),
|
||||||
|
source.paymentMethod(), source.paymentReferenceNo(), source.paymentVoucherFileIds(),
|
||||||
|
source.businessDate(), source.currency(), source.amount());
|
||||||
|
when(commands.find("operator-a", "CREATE", "bad-container-fee")).thenReturn(null);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.create("operator-a", "bad-container-fee", request))
|
||||||
|
.isInstanceOf(ResponseStatusException.class)
|
||||||
|
.hasMessageContaining("海外货柜费用项目不受支持");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void canListAllCustomerFeesForAContainerInOneQuery() {
|
||||||
|
service.list(null, "OVERSEAS_CONTAINER", null, 701L);
|
||||||
|
|
||||||
|
verify(mapper).selectList(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
package com.baiyue.lastleg.financefee.service;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
class OverseasFeeBusinessGuardTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void acceptsOnlyARealContainerOwnedByTheCustomer() {
|
||||||
|
StubJdbc jdbc = new StubJdbc(3, 0, 1);
|
||||||
|
new OverseasFeeBusinessGuard(jdbc).requireOwned(containerFee("customer-a", 701L, "MSCU1234567"));
|
||||||
|
|
||||||
|
assertThat(jdbc.sql()).hasSize(3);
|
||||||
|
assertThat(jdbc.sql().get(0)).contains("source_container_booking_id");
|
||||||
|
assertThat(jdbc.sql().get(1)).contains("container_no");
|
||||||
|
assertThat(jdbc.sql().get(2)).contains("customer_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsAContainerThatDoesNotContainTheClaimedCustomer() {
|
||||||
|
StubJdbc jdbc = new StubJdbc(3, 0, 0);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> new OverseasFeeBusinessGuard(jdbc)
|
||||||
|
.requireOwned(containerFee("other-customer", 701L, "MSCU1234567")))
|
||||||
|
.isInstanceOf(ResponseStatusException.class)
|
||||||
|
.hasMessageContaining("该客户不属于费用关联的真实货柜");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsAContainerWhoseOrdersDoNotShareTheClaimedContainerNumber() {
|
||||||
|
StubJdbc jdbc = new StubJdbc(3, 1);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> new OverseasFeeBusinessGuard(jdbc)
|
||||||
|
.requireOwned(containerFee("customer-a", 701L, "MSCU1234567")))
|
||||||
|
.isInstanceOf(ResponseStatusException.class)
|
||||||
|
.hasMessageContaining("真实货柜的集装箱号缺失或不一致");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsDifferentBusinessAndContainerBookingIdentifiersBeforeQuerying() {
|
||||||
|
SaveRequest source = containerFee("customer-a", 701L, "MSCU1234567");
|
||||||
|
SaveRequest inconsistent = new SaveRequest(source.customerId(), source.customerName(), source.sourceType(),
|
||||||
|
source.businessType(), 702L, source.businessNo(), source.orderId(), source.inboundId(),
|
||||||
|
source.inboundCode(), source.containerId(), source.containerBookingId(), source.containerNo(),
|
||||||
|
source.warehouseId(), source.feeItemCode(), source.feeItemName(), source.direction(), source.supplierId(),
|
||||||
|
source.supplierName(), source.supplierCategory(), source.settlementMode(), source.paidAt(),
|
||||||
|
source.paymentMethod(), source.paymentReferenceNo(), source.paymentVoucherFileIds(),
|
||||||
|
source.businessDate(), source.currency(), source.amount());
|
||||||
|
StubJdbc jdbc = new StubJdbc();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> new OverseasFeeBusinessGuard(jdbc).requireOwned(inconsistent))
|
||||||
|
.isInstanceOf(ResponseStatusException.class)
|
||||||
|
.hasMessageContaining("真实来源柜ID");
|
||||||
|
assertThat(jdbc.sql()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listsAllContainerCustomersWithOneGroupedQuery() {
|
||||||
|
StubJdbc jdbc = new StubJdbc();
|
||||||
|
jdbc.queryResults = List.of(
|
||||||
|
new com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ContainerCustomerOption("customer-a", "客户A"),
|
||||||
|
new com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ContainerCustomerOption("customer-b", "客户B"));
|
||||||
|
|
||||||
|
var result = new OverseasFeeBusinessGuard(jdbc).listContainerCustomers(701L);
|
||||||
|
|
||||||
|
assertThat(result).extracting("customerId").containsExactly("customer-a", "customer-b");
|
||||||
|
assertThat(jdbc.sql()).singleElement().asString().contains("GROUP BY customer_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
static SaveRequest containerFee(String customerId, Long bookingId, String containerNo) {
|
||||||
|
return new SaveRequest(customerId, "客户A", "OVERSEAS_CONTAINER_FEE", "OVERSEAS_CONTAINER",
|
||||||
|
bookingId, containerNo, null, null, null, null, bookingId, containerNo, 91L,
|
||||||
|
"PORT_FEE", "港口费", "RECEIVABLE", null, null, null, null, null, null, null,
|
||||||
|
List.of(), LocalDate.of(2026, 9, 8), "CNY", new BigDecimal("88.00"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StubJdbc extends JdbcTemplate {
|
||||||
|
private final ArrayDeque<Integer> results = new ArrayDeque<>();
|
||||||
|
private final List<String> sql = new ArrayList<>();
|
||||||
|
private List<?> queryResults = List.of();
|
||||||
|
|
||||||
|
private StubJdbc(Integer... values) {
|
||||||
|
results.addAll(List.of(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T queryForObject(String statement, Class<T> requiredType, Object... args) {
|
||||||
|
sql.add(statement);
|
||||||
|
return requiredType.cast(results.removeFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> List<T> query(String statement, RowMapper<T> rowMapper, Object... args) {
|
||||||
|
sql.add(statement);
|
||||||
|
return (List<T>) queryResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> sql() {
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user