Test #1
@@ -4,14 +4,12 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||
import org.springframework.jdbc.support.KeyHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -26,14 +24,12 @@ import java.util.Map;
|
||||
public class FinanceService {
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper objectMapper;
|
||||
@Autowired(required = false)
|
||||
private FinanceFileClient fileClient;
|
||||
@org.springframework.beans.factory.annotation.Value("${baiyue.security.internal-service-token:}")
|
||||
private String fileToken;
|
||||
private final FinanceVoucherValidator voucherValidator;
|
||||
|
||||
public FinanceService(JdbcTemplate jdbc, ObjectMapper objectMapper) {
|
||||
public FinanceService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceVoucherValidator voucherValidator) {
|
||||
this.jdbc = jdbc;
|
||||
this.objectMapper = objectMapper;
|
||||
this.voucherValidator = voucherValidator;
|
||||
}
|
||||
|
||||
public Map<String, Object> customerTerms(long tenantId) {
|
||||
@@ -203,20 +199,8 @@ public class FinanceService {
|
||||
}
|
||||
|
||||
private void validateVoucherFiles(long tenantId, List<String> fileIds) {
|
||||
if (fileIds == null || fileIds.isEmpty()) throw bad("财务凭证不能为空");
|
||||
if (fileClient == null) throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "File服务校验能力不可用");
|
||||
for (String raw : fileIds) {
|
||||
long id; try { id = Long.parseLong(raw); } catch (Exception ex) { throw bad("凭证文件ID无效"); }
|
||||
var response = fileClient.metadata(fileToken, tenantId, tenantId, id);
|
||||
var metadata = response == null ? null : response.data();
|
||||
if (metadata == null || metadata.fileId() == null || !tenantIdEquals(metadata.tenantId(), tenantId)
|
||||
|| !"FINANCE_VOUCHER".equals(metadata.businessType()) || metadata.contentType() == null
|
||||
|| !metadata.contentType().toLowerCase(java.util.Locale.ROOT).startsWith("image/")) {
|
||||
throw bad("凭证文件不存在、归属不匹配或不是有效图片");
|
||||
}
|
||||
}
|
||||
voucherValidator.validate(tenantId, fileIds, "财务凭证");
|
||||
}
|
||||
private static boolean tenantIdEquals(Long actual, long expected) { return actual != null && actual == expected; }
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> createCost(long tenantId, long operatorId, CostRequest request, String idempotencyKey) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.baiyue.finance.service;
|
||||
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import com.baiyue.finance.dto.FinanceSettlementDtos.CreatePayableSettlementRequest;
|
||||
import com.baiyue.finance.dto.FinanceSettlementDtos.CreateReceivableSettlementRequest;
|
||||
import com.baiyue.finance.dto.FinanceSettlementDtos.CreateSettlementPaymentRequest;
|
||||
@@ -14,7 +13,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||
import org.springframework.jdbc.support.KeyHolder;
|
||||
@@ -47,15 +45,12 @@ public class FinanceSettlementService {
|
||||
private static final Set<String> PAYMENT_METHODS = Set.of("BANK_TRANSFER", "CASH", "OTHER");
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final FinanceFileClient fileClient;
|
||||
private final FinanceVoucherValidator voucherValidator;
|
||||
|
||||
@Value("${baiyue.security.internal-service-token:}")
|
||||
private String fileToken;
|
||||
|
||||
public FinanceSettlementService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceFileClient fileClient) {
|
||||
public FinanceSettlementService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceVoucherValidator voucherValidator) {
|
||||
this.jdbc = jdbc;
|
||||
this.objectMapper = objectMapper;
|
||||
this.fileClient = fileClient;
|
||||
this.voucherValidator = voucherValidator;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -355,26 +350,7 @@ public class FinanceSettlementService {
|
||||
}
|
||||
|
||||
private void validateVoucherFiles(long customerId, List<String> fileIds) {
|
||||
if (fileClient == null || !StringUtils.hasText(fileToken)) throw error("FIN_503", "File服务校验能力不可用");
|
||||
for (String raw : fileIds) {
|
||||
long fileId;
|
||||
try { fileId = Long.parseLong(raw); } catch (RuntimeException invalid) { throw error("FIN_400", "付款凭证文件ID无效"); }
|
||||
FinanceFileClient.Metadata metadata;
|
||||
try {
|
||||
var response = fileClient.metadata(fileToken, customerId, customerId, fileId);
|
||||
metadata = response == null ? null : response.data();
|
||||
} catch (RuntimeException unavailable) {
|
||||
throw error("FIN_503", "File服务校验不可用");
|
||||
}
|
||||
if (metadata == null || metadata.fileId() == null || metadata.fileId() != fileId
|
||||
|| metadata.tenantId() == null || metadata.tenantId() != customerId
|
||||
|| !"FINANCE_VOUCHER".equals(metadata.businessType())
|
||||
|| !String.valueOf(customerId).equals(metadata.businessId())
|
||||
|| !StringUtils.hasText(metadata.contentType())
|
||||
|| !metadata.contentType().toLowerCase(Locale.ROOT).startsWith("image/")) {
|
||||
throw error("FIN_400", "付款凭证不存在、客户归属不匹配或文件类型无效");
|
||||
}
|
||||
}
|
||||
voucherValidator.validate(customerId, fileIds, "付款凭证");
|
||||
}
|
||||
|
||||
private Long reserve(long customerId, long operatorId, String operation, String key,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.baiyue.finance.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.HashSet;
|
||||
|
||||
@Component
|
||||
public class FinanceVoucherValidator {
|
||||
private final FinanceFileClient fileClient;
|
||||
private final String fileToken;
|
||||
|
||||
@Autowired
|
||||
public FinanceVoucherValidator(FinanceFileClient fileClient,
|
||||
@Value("${baiyue.security.internal-service-token:}") String fileToken) {
|
||||
this.fileClient = fileClient;
|
||||
this.fileToken = fileToken;
|
||||
}
|
||||
|
||||
public void validate(long customerId, List<String> fileIds, String message) {
|
||||
if (fileIds == null || fileIds.isEmpty()) throw bad(message + "不能为空");
|
||||
if (fileClient == null || !StringUtils.hasText(fileToken)) throw unavailable("File服务校验能力不可用");
|
||||
if (new HashSet<>(fileIds).size() != fileIds.size()) throw bad(message + "文件ID不能重复");
|
||||
for (String raw : fileIds) {
|
||||
final long fileId;
|
||||
try { fileId = Long.parseLong(raw); } catch (RuntimeException invalid) { throw bad(message + "文件ID无效"); }
|
||||
FinanceFileClient.Metadata metadata;
|
||||
try {
|
||||
var response = fileClient.metadata(fileToken, customerId, customerId, fileId);
|
||||
metadata = response == null ? null : response.data();
|
||||
} catch (RuntimeException unavailable) {
|
||||
throw unavailable("File服务校验不可用");
|
||||
}
|
||||
if (metadata == null || metadata.fileId() == null || metadata.fileId() != fileId
|
||||
|| metadata.tenantId() == null || metadata.tenantId() != customerId
|
||||
|| !String.valueOf(customerId).equals(metadata.businessId())
|
||||
|| !"FINANCE_VOUCHER".equals(metadata.businessType())
|
||||
|| !StringUtils.hasText(metadata.contentType())
|
||||
|| !metadata.contentType().toLowerCase(Locale.ROOT).startsWith("image/")) {
|
||||
throw bad(message + "不存在、归属不匹配或不是有效图片");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BizException bad(String message) { return new BizException(ResultCode.BAD_REQUEST, message); }
|
||||
private static BizException unavailable(String message) {
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE, message);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.finance.service.FinanceService;
|
||||
import com.baiyue.finance.service.FinanceVoucherValidator;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
@@ -18,7 +19,6 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
class FinanceServiceValidationTest {
|
||||
@Test
|
||||
@@ -49,7 +49,7 @@ class FinanceServiceValidationTest {
|
||||
@Test
|
||||
void rejectsUnsupportedCostTypeBeforeWrite() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
FinanceService service = service(jdbc);
|
||||
var request = new FinanceService.CostRequest("C-1", "TICKET", 1L, null, null, null, null, "MANUAL", "拖车", BigDecimal.ONE, List.of("f"), null);
|
||||
assertThrows(RuntimeException.class, () -> service.createCost(1L, 2L, request, "k1"));
|
||||
verify(jdbc, never()).update(startsWith("INSERT INTO fin_cost"), (Object[]) any());
|
||||
@@ -58,7 +58,7 @@ class FinanceServiceValidationTest {
|
||||
@Test
|
||||
void rejectsNonPositiveAllocation() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
FinanceService service = service(jdbc);
|
||||
var request = new FinanceService.ReceiptRequest("R-1", 1L, null, "BANK_TRANSFER", null, List.of("f"), null,
|
||||
List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ZERO)));
|
||||
assertThrows(RuntimeException.class, () -> service.createReceipt(1L, 2L, request, "k2"));
|
||||
@@ -67,7 +67,7 @@ class FinanceServiceValidationTest {
|
||||
@Test
|
||||
void rejectsDuplicateBillAllocationInsteadOfMerging() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
FinanceService service = service(jdbc);
|
||||
var request = new FinanceService.ReceiptRequest("R-2", 1L, null, "BANK_TRANSFER", null, List.of("f"), null,
|
||||
List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE), new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE)));
|
||||
assertThrows(RuntimeException.class, () -> service.createReceipt(1L, 2L, request, "k3"));
|
||||
@@ -76,7 +76,8 @@ class FinanceServiceValidationTest {
|
||||
@Test
|
||||
void receiptFailsClosedWhenFileValidationCapabilityIsUnavailable() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper(),
|
||||
new FinanceVoucherValidator(null, ""));
|
||||
var request = new FinanceService.ReceiptRequest("R-FILE", 1L, null, "BANK_TRANSFER", null, List.of("77"), null,
|
||||
List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE)));
|
||||
|
||||
@@ -92,8 +93,7 @@ class FinanceServiceValidationTest {
|
||||
FinanceFileClient files = mock(FinanceFileClient.class);
|
||||
when(files.metadata(anyString(), eq(9L), eq(9L), eq(77L))).thenReturn(ApiResponse.success(
|
||||
new FinanceFileClient.Metadata(77L, 10L, "FINANCE_VOUCHER", "9", "v.jpg", "sha", "image/jpeg", 10L)));
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper().findAndRegisterModules());
|
||||
ReflectionTestUtils.setField(service, "fileClient", files);
|
||||
FinanceService service = service(jdbc, files);
|
||||
var request = new FinanceService.ReceiptRequest("R-3", 1L, null, "BANK_TRANSFER", null, List.of("77"), null,
|
||||
List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE)));
|
||||
assertThrows(RuntimeException.class, () -> service.createReceipt(9L, 2L, request, "k4"));
|
||||
@@ -117,9 +117,7 @@ class FinanceServiceValidationTest {
|
||||
holder.getKeyList().add(java.util.Map.of("GENERATED_KEY", 701L));
|
||||
return 1;
|
||||
}).when(jdbc).update(any(org.springframework.jdbc.core.PreparedStatementCreator.class), any(org.springframework.jdbc.support.KeyHolder.class));
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper().findAndRegisterModules());
|
||||
ReflectionTestUtils.setField(service, "fileClient", files);
|
||||
ReflectionTestUtils.setField(service, "fileToken", "internal-token");
|
||||
FinanceService service = service(jdbc, files);
|
||||
var request = new FinanceService.ReceiptRequest("R-4", 1L, java.time.LocalDateTime.of(2026, 8, 11, 10, 0),
|
||||
"BANK_TRANSFER", null, List.of("77"), "首笔收款",
|
||||
List.of(new FinanceService.ReceiptAllocation(50L, new BigDecimal("40.00"))));
|
||||
@@ -154,9 +152,7 @@ class FinanceServiceValidationTest {
|
||||
holder.getKeyList().add(java.util.Map.of("GENERATED_KEY", 701L));
|
||||
return 1;
|
||||
}).when(jdbc).update(any(org.springframework.jdbc.core.PreparedStatementCreator.class), any(org.springframework.jdbc.support.KeyHolder.class));
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper().findAndRegisterModules());
|
||||
ReflectionTestUtils.setField(service, "fileClient", files);
|
||||
ReflectionTestUtils.setField(service, "fileToken", "internal-token");
|
||||
FinanceService service = service(jdbc, files);
|
||||
var request = new FinanceService.ReceiptRequest("R-300", 1L, java.time.LocalDateTime.of(2026, 8, 11, 10, 0),
|
||||
"BANK_TRANSFER", "BANK-300", List.of("77"), "三单收款", List.of(
|
||||
new FinanceService.ReceiptAllocation(50L, new BigDecimal("100.00")),
|
||||
@@ -176,7 +172,7 @@ class FinanceServiceValidationTest {
|
||||
void rejectsSingleOrderWhenOrderNotOwnedByCustomer() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_fee_source"), eq(Number.class), any(), any())).thenReturn(0);
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
FinanceService service = service(jdbc);
|
||||
var request = new FinanceService.CostRequest("C-2", "SINGLE_ORDER", 99L, null, null, null, null, "MANUAL", "货拉拉", BigDecimal.ONE, List.of("1"), null);
|
||||
assertThrows(RuntimeException.class, () -> service.createCost(9L, 2L, request, "k5"));
|
||||
}
|
||||
@@ -198,9 +194,7 @@ class FinanceServiceValidationTest {
|
||||
holder.getKeyList().add(java.util.Map.of("GENERATED_KEY", 701L));
|
||||
return 1;
|
||||
}).when(jdbc).update(any(org.springframework.jdbc.core.PreparedStatementCreator.class), any(org.springframework.jdbc.support.KeyHolder.class));
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
ReflectionTestUtils.setField(service, "fileClient", files);
|
||||
ReflectionTestUtils.setField(service, "fileToken", "internal-token");
|
||||
FinanceService service = service(jdbc, files);
|
||||
var request = new FinanceService.CostRequest("C-3", "CONTAINER_SHARED", null, null, 88L,
|
||||
null, null, "MANUAL", "整柜成本", new BigDecimal("300.00"), List.of("77"), null);
|
||||
var snapshot = List.of(new FinanceService.ContainerOrderVolume(
|
||||
@@ -221,7 +215,7 @@ class FinanceServiceValidationTest {
|
||||
.thenReturn(1);
|
||||
var rows = List.<java.util.Map<String, Object>>of(java.util.Map.of("version", 2));
|
||||
when(jdbc.queryForList(startsWith("SELECT id,bill_id,version"), eq(9L), eq(50L))).thenReturn(rows);
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
FinanceService service = service(jdbc);
|
||||
|
||||
assertEquals(rows, service.billRevisions(9L, 50L));
|
||||
}
|
||||
@@ -231,9 +225,19 @@ class FinanceServiceValidationTest {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_bill"), eq(Number.class), eq(50L), eq(9L)))
|
||||
.thenReturn(0);
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
FinanceService service = service(jdbc);
|
||||
|
||||
assertThrows(RuntimeException.class, () -> service.billRevisions(9L, 50L));
|
||||
verify(jdbc, never()).queryForList(startsWith("SELECT id,bill_id,version"), (Object[]) any());
|
||||
}
|
||||
|
||||
private static FinanceService service(JdbcTemplate jdbc, FinanceFileClient files) {
|
||||
return new FinanceService(jdbc, new ObjectMapper().findAndRegisterModules(),
|
||||
new FinanceVoucherValidator(files, "internal-token"));
|
||||
}
|
||||
|
||||
private static FinanceService service(JdbcTemplate jdbc) {
|
||||
return new FinanceService(jdbc, new ObjectMapper().findAndRegisterModules(),
|
||||
mock(FinanceVoucherValidator.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import com.baiyue.finance.controller.FinanceSettlementController;
|
||||
@@ -12,6 +14,7 @@ import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementSummary;
|
||||
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementActionRequest;
|
||||
import com.baiyue.finance.service.FinanceEventException;
|
||||
import com.baiyue.finance.service.FinanceSettlementService;
|
||||
import com.baiyue.finance.service.FinanceVoucherValidator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -136,8 +139,11 @@ class FinanceSettlementBehaviorTest {
|
||||
when(fileClient.metadata("internal-token", 10L, 10L, 91L)).thenThrow(new IllegalStateException("down"));
|
||||
FinanceSettlementService service = service(jdbc, fileClient);
|
||||
|
||||
assertCode(() -> service.createSettlementPayment(88L, 7L, "pay-key",
|
||||
payment(new BigDecimal("40.00"), 0)), "FIN_503");
|
||||
assertThatThrownBy(() -> service.createSettlementPayment(88L, 7L, "pay-key",
|
||||
payment(new BigDecimal("40.00"), 0)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
|
||||
|
||||
verify(jdbc, never()).update(startsWith("INSERT INTO fin_settlement_payment"), (Object[]) any());
|
||||
verify(jdbc, never()).update(startsWith("UPDATE fin_settlement SET paid_amount"), (Object[]) any());
|
||||
@@ -516,10 +522,8 @@ class FinanceSettlementBehaviorTest {
|
||||
}
|
||||
|
||||
private static FinanceSettlementService service(JdbcTemplate jdbc, FinanceFileClient fileClient) {
|
||||
FinanceSettlementService service = new FinanceSettlementService(
|
||||
jdbc, new ObjectMapper().findAndRegisterModules(), fileClient);
|
||||
ReflectionTestUtils.setField(service, "fileToken", "internal-token");
|
||||
return service;
|
||||
return new FinanceSettlementService(jdbc, new ObjectMapper().findAndRegisterModules(),
|
||||
new FinanceVoucherValidator(fileClient, "internal-token"));
|
||||
}
|
||||
|
||||
private static CreateSettlementPaymentRequest payment(BigDecimal amount, int version) {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import com.baiyue.finance.service.FinanceVoucherValidator;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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 FinanceVoucherValidatorTest {
|
||||
@Test
|
||||
void acceptsCustomerOwnedFinanceImage() {
|
||||
FinanceFileClient files = mock(FinanceFileClient.class);
|
||||
when(files.metadata("token", 9L, 9L, 77L)).thenReturn(ApiResponse.success(
|
||||
new FinanceFileClient.Metadata(77L, 9L, "FINANCE_VOUCHER", "9", "receipt.jpg", "sha", "image/jpeg", 10L)));
|
||||
|
||||
new FinanceVoucherValidator(files, "token").validate(9L, List.of("77"), "财务凭证");
|
||||
verify(files).metadata("token", 9L, 9L, 77L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMismatchedFileIdentityOwnershipTypeAndMime() {
|
||||
FinanceFileClient files = mock(FinanceFileClient.class);
|
||||
List<FinanceFileClient.Metadata> invalid = List.of(
|
||||
new FinanceFileClient.Metadata(78L, 9L, "FINANCE_VOUCHER", "9", "receipt.jpg", "sha", "image/jpeg", 10L),
|
||||
new FinanceFileClient.Metadata(77L, 10L, "FINANCE_VOUCHER", "9", "receipt.jpg", "sha", "image/jpeg", 10L),
|
||||
new FinanceFileClient.Metadata(77L, 9L, "FINANCE_VOUCHER", "10", "receipt.jpg", "sha", "image/jpeg", 10L),
|
||||
new FinanceFileClient.Metadata(77L, 9L, "OTHER", "9", "receipt.jpg", "sha", "image/jpeg", 10L),
|
||||
new FinanceFileClient.Metadata(77L, 9L, "FINANCE_VOUCHER", "9", "receipt.pdf", "sha", "application/pdf", 10L));
|
||||
|
||||
for (FinanceFileClient.Metadata metadata : invalid) {
|
||||
when(files.metadata(eq("token"), eq(9L), eq(9L), eq(77L))).thenReturn(ApiResponse.success(metadata));
|
||||
assertThatThrownBy(() -> new FinanceVoucherValidator(files, "token")
|
||||
.validate(9L, List.of("77"), "付款凭证"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsClosedWhenFileDependencyUnavailable() {
|
||||
FinanceFileClient files = mock(FinanceFileClient.class);
|
||||
when(files.metadata("token", 9L, 9L, 77L)).thenThrow(new IllegalStateException("down"));
|
||||
|
||||
assertThatThrownBy(() -> new FinanceVoucherValidator(files, "token")
|
||||
.validate(9L, List.of("77"), "付款凭证"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingInvalidAndDuplicateFileIds() {
|
||||
FinanceVoucherValidator validator = new FinanceVoucherValidator(mock(FinanceFileClient.class), "token");
|
||||
assertResult(() -> validator.validate(9L, List.of(), "财务凭证"), ResultCode.BAD_REQUEST);
|
||||
assertResult(() -> validator.validate(9L, List.of("invalid"), "财务凭证"), ResultCode.BAD_REQUEST);
|
||||
assertResult(() -> validator.validate(9L, List.of("77", "77"), "付款凭证"), ResultCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWrongFileIdAndCustomerOwnership() {
|
||||
FinanceFileClient files = mock(FinanceFileClient.class);
|
||||
when(files.metadata("token", 9L, 9L, 77L)).thenReturn(ApiResponse.success(
|
||||
new FinanceFileClient.Metadata(78L, 10L, "FINANCE_VOUCHER", "9", "receipt.jpg", "sha", "image/jpeg", 10L)));
|
||||
assertResult(() -> new FinanceVoucherValidator(files, "token")
|
||||
.validate(9L, List.of("77"), "付款凭证"), ResultCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingToken() {
|
||||
assertResult(() -> new FinanceVoucherValidator(mock(FinanceFileClient.class), "")
|
||||
.validate(9L, List.of("77"), "财务凭证"), ResultCode.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
private static void assertResult(org.assertj.core.api.ThrowableAssert.ThrowingCallable call,
|
||||
ResultCode resultCode) {
|
||||
assertThatThrownBy(call).isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(resultCode);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ test('覆盖模板生命周期、版本冲突、定时任务、幂等、查询
|
||||
]) assert.match(moduleSource, new RegExp(token))
|
||||
assert.match(moduleSource, /recipientType:\s*'PLATFORM_ROLE'/)
|
||||
assert.match(moduleSource, /roleCode:\s*'PLATFORM_ADMIN'/)
|
||||
assert.match(moduleSource, /Date\.now\(\) \+ 24 \* 60 \* 60 \* 1000/)
|
||||
assert.match(moduleSource, /Date\.now\(\) \+ 365 \* 24 \* 60 \* 60 \* 1000/)
|
||||
})
|
||||
|
||||
test('异常路径会取消定时任务并停用模板,不实际投递验收消息', () => {
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function run(context) {
|
||||
template?.status === 'ACTIVE' && Number(template?.version) === 3,
|
||||
`status=${template?.status || 'missing'}, version=${template?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const scheduledAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().slice(0, 19)
|
||||
const scheduledAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 19)
|
||||
const idempotencyKey = `${context.marker}-notification-scheduled`.slice(0, 128)
|
||||
const taskCreate = {
|
||||
tenantId: 0,
|
||||
|
||||
Reference in New Issue
Block a user