优化
This commit is contained in:
+9
@@ -44,6 +44,15 @@ public record BusinessFeeSourceChangedV1(
|
|||||||
LocalDate businessDate,
|
LocalDate businessDate,
|
||||||
String currency,
|
String currency,
|
||||||
BigDecimal totalAmount,
|
BigDecimal totalAmount,
|
||||||
|
String originalCurrency,
|
||||||
|
BigDecimal originalAmount,
|
||||||
|
BigDecimal exchangeRate,
|
||||||
|
String exchangeRateMonth,
|
||||||
|
BigDecimal chargeableQuantity,
|
||||||
|
String quantityUnit,
|
||||||
|
BigDecimal unitPrice,
|
||||||
|
LocalDate periodStart,
|
||||||
|
LocalDate periodEnd,
|
||||||
String reviewStatus,
|
String reviewStatus,
|
||||||
String publicationStatus,
|
String publicationStatus,
|
||||||
String sourceStatus,
|
String sourceStatus,
|
||||||
|
|||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
package com.baiyue.finance.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
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.Value;
|
||||||
|
import org.springframework.http.CacheControl;
|
||||||
|
import org.springframework.http.ContentDisposition;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/platform/finance/v1/vouchers")
|
||||||
|
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
|
||||||
|
public class PlatformFinanceVoucherController {
|
||||||
|
private final FinanceFileClient files;
|
||||||
|
private final String token;
|
||||||
|
|
||||||
|
public PlatformFinanceVoucherController(FinanceFileClient files,
|
||||||
|
@Value("${baiyue.security.internal-service-token:}") String token) {
|
||||||
|
this.files = files;
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Voucher(String fileId, String fileName, String contentType, Long sizeBytes) {}
|
||||||
|
|
||||||
|
@GetMapping("/{fileId}")
|
||||||
|
public ApiResponse<Voucher> metadata(@PathVariable long fileId, @RequestParam long customerId) {
|
||||||
|
var file = scopedFile(customerId, fileId);
|
||||||
|
return FinanceResponses.success(new Voucher(String.valueOf(fileId), file.fileName(), file.contentType(), file.sizeBytes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{fileId}/content")
|
||||||
|
public ResponseEntity<byte[]> content(@PathVariable long fileId, @RequestParam long customerId) {
|
||||||
|
var file = scopedFile(customerId, fileId);
|
||||||
|
var response = files.download(token, customerId, customerId, fileId);
|
||||||
|
if (response == null || !response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
|
||||||
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "付款凭证读取失败,请稍后重试");
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||||
|
.header("X-Content-Type-Options", "nosniff")
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
||||||
|
.filename(file.fileName() == null ? "付款凭证" : file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM).body(response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
private FinanceFileClient.Metadata scopedFile(long customerId, long fileId) {
|
||||||
|
if (customerId <= 0 || fileId <= 0) throw new BizException(ResultCode.BAD_REQUEST, "客户或付款凭证无效");
|
||||||
|
var response = files.metadata(token, customerId, customerId, fileId);
|
||||||
|
var file = response == null ? null : response.data();
|
||||||
|
if (file == null || !Long.valueOf(fileId).equals(file.fileId())
|
||||||
|
|| !Long.valueOf(customerId).equals(file.tenantId())
|
||||||
|
|| !"FINANCE_VOUCHER".equals(file.businessType())
|
||||||
|
|| !String.valueOf(customerId).equals(file.businessId())) {
|
||||||
|
throw new BizException(ResultCode.BAD_REQUEST, "付款凭证不存在或不属于当前客户");
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -115,6 +115,13 @@ public class BusinessFeeSourceEventService {
|
|||||||
|| blank(value.feeItemName()) || !DIRECTIONS.contains(value.direction())
|
|| blank(value.feeItemName()) || !DIRECTIONS.contains(value.direction())
|
||||||
|| value.businessDate() == null || !"CNY".equals(value.currency())
|
|| value.businessDate() == null || !"CNY".equals(value.currency())
|
||||||
|| value.totalAmount() == null || value.totalAmount().signum() <= 0
|
|| value.totalAmount() == null || value.totalAmount().signum() <= 0
|
||||||
|
|| !Set.of("CNY", "USD").contains(value.originalCurrency())
|
||||||
|
|| value.originalAmount() == null || value.originalAmount().signum() <= 0
|
||||||
|
|| value.exchangeRate() == null || value.exchangeRate().signum() <= 0
|
||||||
|
|| blank(value.exchangeRateMonth())
|
||||||
|
|| (value.periodStart() == null) != (value.periodEnd() == null)
|
||||||
|
|| value.periodStart() != null && value.periodStart().isAfter(value.periodEnd())
|
||||||
|
|| (value.chargeableQuantity() == null) != blank(value.quantityUnit())
|
||||||
|| !"APPROVED".equals(value.reviewStatus()) || blank(value.sourceHash())) {
|
|| !"APPROVED".equals(value.reviewStatus()) || blank(value.sourceHash())) {
|
||||||
throw error("FIN-EVT-012", "跨区费用来源快照字段不完整");
|
throw error("FIN-EVT-012", "跨区费用来源快照字段不完整");
|
||||||
}
|
}
|
||||||
@@ -148,7 +155,9 @@ public class BusinessFeeSourceEventService {
|
|||||||
value.warehouseId(), value.feeItemCode(), value.feeItemName(), value.direction(), value.supplierId(),
|
value.warehouseId(), value.feeItemCode(), value.feeItemName(), value.direction(), value.supplierId(),
|
||||||
value.supplierName(), value.supplierCategory(), value.settlementMode(), value.paidAt(), value.paymentMethod(),
|
value.supplierName(), value.supplierCategory(), value.settlementMode(), value.paidAt(), value.paymentMethod(),
|
||||||
value.paymentReferenceNo(), value.paymentVoucherFileIds(), value.businessDate(), value.currency(),
|
value.paymentReferenceNo(), value.paymentVoucherFileIds(), value.businessDate(), value.currency(),
|
||||||
value.totalAmount(), value.reviewStatus(), "PUBLISHED", "ACTIVE", null);
|
value.totalAmount(), value.originalCurrency(), value.originalAmount(), value.exchangeRate(),
|
||||||
|
value.exchangeRateMonth(), value.chargeableQuantity(), value.quantityUnit(), value.unitPrice(),
|
||||||
|
value.periodStart(), value.periodEnd(), value.reviewStatus(), "PUBLISHED", "ACTIVE", null);
|
||||||
String expected = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(objectMapper.valueToTree(unsigned)));
|
String expected = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(objectMapper.valueToTree(unsigned)));
|
||||||
if (!expected.equals(value.sourceHash())) throw error("FIN-EVT-004", "费用来源快照哈希不一致");
|
if (!expected.equals(value.sourceHash())) throw error("FIN-EVT-004", "费用来源快照哈希不一致");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ class BusinessFeeSourceEventServiceTest {
|
|||||||
"OVERSEAS_PROCUREMENT", "801", "PO-US-001", null, null, null, null, null,
|
"OVERSEAS_PROCUREMENT", "801", "PO-US-001", null, null, null, null, null,
|
||||||
null, "91", "PROCUREMENT_SERVICE", "采购服务费", "RECEIVABLE", null, null,
|
null, "91", "PROCUREMENT_SERVICE", "采购服务费", "RECEIVABLE", null, null,
|
||||||
null, null, null, null, null, List.of(), LocalDate.of(2026, 9, 2), "CNY",
|
null, null, null, null, null, List.of(), LocalDate.of(2026, 9, 2), "CNY",
|
||||||
new BigDecimal("120.00"), "APPROVED", "PUBLISHED", "ACTIVE", null);
|
new BigDecimal("120.00"), "CNY", new BigDecimal("120.00"), BigDecimal.ONE,
|
||||||
|
"2026-09", null, null, null, null, null, "APPROVED", "PUBLISHED", "ACTIVE", null);
|
||||||
String sourceHash = EventPayloadHash.sha256(
|
String sourceHash = EventPayloadHash.sha256(
|
||||||
EventPayloadCanonicalizer.canonicalize(json.valueToTree(unsigned)));
|
EventPayloadCanonicalizer.canonicalize(json.valueToTree(unsigned)));
|
||||||
BusinessFeeSourceChangedV1 payload = new BusinessFeeSourceChangedV1("PUBLISHED", "LASTLEG",
|
BusinessFeeSourceChangedV1 payload = new BusinessFeeSourceChangedV1("PUBLISHED", "LASTLEG",
|
||||||
@@ -77,7 +78,8 @@ class BusinessFeeSourceEventServiceTest {
|
|||||||
"OVERSEAS_PROCUREMENT", "801", "PO-US-001", null, null, null, null, null,
|
"OVERSEAS_PROCUREMENT", "801", "PO-US-001", null, null, null, null, null,
|
||||||
null, "91", "PROCUREMENT_SERVICE", "采购服务费", "RECEIVABLE", null, null,
|
null, "91", "PROCUREMENT_SERVICE", "采购服务费", "RECEIVABLE", null, null,
|
||||||
null, null, null, null, null, List.of(), LocalDate.of(2026, 9, 2), "CNY",
|
null, null, null, null, null, List.of(), LocalDate.of(2026, 9, 2), "CNY",
|
||||||
new BigDecimal("120.00"), "APPROVED", "PUBLISHED", "ACTIVE", sourceHash);
|
new BigDecimal("120.00"), "CNY", new BigDecimal("120.00"), BigDecimal.ONE,
|
||||||
|
"2026-09", null, null, null, null, null, "APPROVED", "PUBLISHED", "ACTIVE", sourceHash);
|
||||||
JsonNode node = json.valueToTree(payload);
|
JsonNode node = json.valueToTree(payload);
|
||||||
String payloadHash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node));
|
String payloadHash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node));
|
||||||
return new EventEnvelope("123e4567-e89b-12d3-a456-426614174701",
|
return new EventEnvelope("123e4567-e89b-12d3-a456-426614174701",
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.baiyue.finance;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.exception.BizException;
|
||||||
|
import com.baiyue.finance.client.FinanceFileClient;
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.finance.controller.PlatformFinanceVoucherController;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
class PlatformFinanceVoucherControllerTest {
|
||||||
|
private final FinanceFileClient files = mock(FinanceFileClient.class);
|
||||||
|
private final PlatformFinanceVoucherController controller = new PlatformFinanceVoucherController(files, "test-token");
|
||||||
|
|
||||||
|
private FinanceFileClient.Metadata file(long customerId, String type, String businessId) {
|
||||||
|
return new FinanceFileClient.Metadata(91L, customerId, type, businessId, "付款.png", "sha", "image/png", 3L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void returnsFilenameAndDownloadsWithinCustomerScope() {
|
||||||
|
when(files.metadata("test-token", 11, 11, 91)).thenReturn(ApiResponse.success(file(11, "FINANCE_VOUCHER", "11"), "test", "test"));
|
||||||
|
when(files.download("test-token", 11, 11, 91)).thenReturn(ResponseEntity.ok(new byte[]{1, 2, 3}));
|
||||||
|
assertThat(controller.metadata(91, 11).data().fileName()).isEqualTo("付款.png");
|
||||||
|
var content = controller.content(91, 11);
|
||||||
|
assertThat(content.getBody()).containsExactly(1, 2, 3);
|
||||||
|
assertThat(content.getHeaders().getCacheControl()).isEqualTo("no-store");
|
||||||
|
assertThat(content.getHeaders().getFirst("Content-Disposition")).startsWith("attachment;");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void rejectsWrongCustomerBusinessTypeAndBusinessIdBeforeDownload() {
|
||||||
|
for (var invalid : new FinanceFileClient.Metadata[]{file(12, "FINANCE_VOUCHER", "11"), file(11, "OTHER", "11"), file(11, "FINANCE_VOUCHER", "12")}) {
|
||||||
|
when(files.metadata("test-token", 11, 11, 91)).thenReturn(ApiResponse.success(invalid, "test", "test"));
|
||||||
|
assertThatThrownBy(() -> controller.content(91, 11)).isInstanceOf(BizException.class);
|
||||||
|
}
|
||||||
|
verify(files, never()).download(anyString(), anyLong(), anyLong(), anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void rejectsMissingMetadataAndFailedContent() {
|
||||||
|
assertThatThrownBy(() -> controller.content(91, 11)).isInstanceOf(BizException.class);
|
||||||
|
when(files.metadata("test-token", 11, 11, 91)).thenReturn(ApiResponse.success(file(11, "FINANCE_VOUCHER", "11"), "test", "test"));
|
||||||
|
when(files.download("test-token", 11, 11, 91)).thenReturn(ResponseEntity.notFound().build());
|
||||||
|
assertThatThrownBy(() -> controller.content(91, 11)).isInstanceOf(BizException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void requiresPlatformListPermission() {
|
||||||
|
assertThat(PlatformFinanceVoucherController.class.getAnnotation(PreAuthorize.class).value())
|
||||||
|
.isEqualTo("hasAuthority('platform:finance:customer-arap:list')");
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-2
@@ -43,8 +43,13 @@ public final class OverseasFeeSourceDtos {
|
|||||||
@Size(max = 128) String paymentReferenceNo,
|
@Size(max = 128) String paymentReferenceNo,
|
||||||
@Size(max = 20) List<@NotBlank String> paymentVoucherFileIds,
|
@Size(max = 20) List<@NotBlank String> paymentVoucherFileIds,
|
||||||
@NotNull LocalDate businessDate,
|
@NotNull LocalDate businessDate,
|
||||||
@NotBlank @Pattern(regexp = "CNY") String currency,
|
@NotBlank @Pattern(regexp = "CNY|USD") String currency,
|
||||||
@NotNull @DecimalMin(value = "0.00", inclusive = false) BigDecimal amount
|
@NotNull @DecimalMin(value = "0.00", inclusive = false) BigDecimal amount,
|
||||||
|
@DecimalMin(value = "0.00", inclusive = false) BigDecimal chargeableQuantity,
|
||||||
|
@Size(max = 32) String quantityUnit,
|
||||||
|
@DecimalMin(value = "0.00", inclusive = false) BigDecimal unitPrice,
|
||||||
|
LocalDate periodStart,
|
||||||
|
LocalDate periodEnd
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public record ActionRequest(@NotBlank @Size(max = 128) String idempotencyKey) { }
|
public record ActionRequest(@NotBlank @Size(max = 128) String idempotencyKey) { }
|
||||||
|
|||||||
+9
@@ -46,6 +46,15 @@ public class OverseasFeeSource {
|
|||||||
private LocalDate businessDate;
|
private LocalDate businessDate;
|
||||||
private String currency;
|
private String currency;
|
||||||
private BigDecimal amount;
|
private BigDecimal amount;
|
||||||
|
private String originalCurrency;
|
||||||
|
private BigDecimal originalAmount;
|
||||||
|
private BigDecimal exchangeRate;
|
||||||
|
private String exchangeRateMonth;
|
||||||
|
private BigDecimal chargeableQuantity;
|
||||||
|
private String quantityUnit;
|
||||||
|
private BigDecimal unitPrice;
|
||||||
|
private LocalDate periodStart;
|
||||||
|
private LocalDate periodEnd;
|
||||||
private String reviewStatus;
|
private String reviewStatus;
|
||||||
private String publicationStatus;
|
private String publicationStatus;
|
||||||
private String sourceStatus;
|
private String sourceStatus;
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package com.baiyue.lastleg.financefee.service;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.masterdata.exchange.entity.MonthlyExchangeRate;
|
||||||
|
import com.baiyue.lastleg.masterdata.exchange.mapper.MonthlyExchangeRateMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OverseasFeeCurrencyConverter {
|
||||||
|
private static final DateTimeFormatter MONTH = DateTimeFormatter.ofPattern("yyyy-MM");
|
||||||
|
private final MonthlyExchangeRateMapper rates;
|
||||||
|
|
||||||
|
public Conversion convert(String currency, BigDecimal amount, LocalDate businessDate) {
|
||||||
|
String originalCurrency = currency == null ? null : currency.trim().toUpperCase();
|
||||||
|
BigDecimal originalAmount = amount == null ? null : amount.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
if ("CNY".equals(originalCurrency)) {
|
||||||
|
return new Conversion(originalCurrency, originalAmount, BigDecimal.ONE.setScale(6),
|
||||||
|
businessDate.format(MONTH), originalAmount);
|
||||||
|
}
|
||||||
|
if (!"USD".equals(originalCurrency)) throw bad("海外费用原币只支持CNY或USD");
|
||||||
|
String month = businessDate.format(MONTH);
|
||||||
|
MonthlyExchangeRate rate = rates.findMonth(month);
|
||||||
|
if (rate == null || !"ACTIVE".equals(rate.getStatus()) || rate.getUsdToCnyAverage() == null
|
||||||
|
|| rate.getUsdToCnyAverage().signum() <= 0) {
|
||||||
|
throw bad("业务月份缺少有效USD兑CNY汇率,不能发布财务费用");
|
||||||
|
}
|
||||||
|
BigDecimal exchangeRate = rate.getUsdToCnyAverage().setScale(6, RoundingMode.HALF_UP);
|
||||||
|
return new Conversion(originalCurrency, originalAmount, exchangeRate, month,
|
||||||
|
originalAmount.multiply(exchangeRate).setScale(2, RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResponseStatusException bad(String message) {
|
||||||
|
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Conversion(String originalCurrency, BigDecimal originalAmount,
|
||||||
|
BigDecimal exchangeRate, String exchangeRateMonth,
|
||||||
|
BigDecimal settlementAmount) { }
|
||||||
|
}
|
||||||
+4
-1
@@ -66,7 +66,10 @@ public class OverseasFeeOutboxService {
|
|||||||
text(value.getSupplierId()), value.getSupplierName(), value.getSupplierCategory(),
|
text(value.getSupplierId()), value.getSupplierName(), value.getSupplierCategory(),
|
||||||
value.getSettlementMode(), instant(value.getPaidAt()), value.getPaymentMethod(),
|
value.getSettlementMode(), instant(value.getPaidAt()), value.getPaymentMethod(),
|
||||||
value.getPaymentReferenceNo(), value.getPaymentVoucherFileIds() == null ? List.of() : value.getPaymentVoucherFileIds(),
|
value.getPaymentReferenceNo(), value.getPaymentVoucherFileIds() == null ? List.of() : value.getPaymentVoucherFileIds(),
|
||||||
value.getBusinessDate(), value.getCurrency(), value.getAmount(), value.getReviewStatus(),
|
value.getBusinessDate(), value.getCurrency(), value.getAmount(),
|
||||||
|
value.getOriginalCurrency(), value.getOriginalAmount(), value.getExchangeRate(), value.getExchangeRateMonth(),
|
||||||
|
value.getChargeableQuantity(), value.getQuantityUnit(), value.getUnitPrice(), value.getPeriodStart(), value.getPeriodEnd(),
|
||||||
|
value.getReviewStatus(),
|
||||||
"PUBLISHED", action == null && hash == null ? "ACTIVE" : value.getSourceStatus(), hash);
|
"PUBLISHED", action == null && hash == null ? "ACTIVE" : value.getSourceStatus(), hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-3
@@ -19,7 +19,6 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
@@ -40,6 +39,7 @@ public class OverseasFeeSourceService {
|
|||||||
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 OverseasFeeCurrencyConverter currencyConverter;
|
||||||
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,
|
||||||
@@ -151,8 +151,14 @@ public class OverseasFeeSourceService {
|
|||||||
value.setDirection(request.direction()); value.setSupplierId(request.supplierId()); value.setSupplierName(text(request.supplierName()));
|
value.setDirection(request.direction()); value.setSupplierId(request.supplierId()); value.setSupplierName(text(request.supplierName()));
|
||||||
value.setSupplierCategory(text(request.supplierCategory())); value.setSettlementMode(text(request.settlementMode()));
|
value.setSupplierCategory(text(request.supplierCategory())); value.setSettlementMode(text(request.settlementMode()));
|
||||||
value.setPaidAt(request.paidAt()); value.setPaymentMethod(text(request.paymentMethod())); value.setPaymentReferenceNo(text(request.paymentReferenceNo()));
|
value.setPaidAt(request.paidAt()); value.setPaymentMethod(text(request.paymentMethod())); value.setPaymentReferenceNo(text(request.paymentReferenceNo()));
|
||||||
value.setPaymentVoucherFileIds(request.paymentVoucherFileIds()); value.setBusinessDate(request.businessDate()); value.setCurrency(request.currency());
|
value.setPaymentVoucherFileIds(request.paymentVoucherFileIds()); value.setBusinessDate(request.businessDate());
|
||||||
value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP)); value.setReviewStatus("DRAFT");
|
OverseasFeeCurrencyConverter.Conversion conversion = currencyConverter.convert(request.currency(), request.amount(), request.businessDate());
|
||||||
|
value.setCurrency("CNY"); value.setAmount(conversion.settlementAmount());
|
||||||
|
value.setOriginalCurrency(conversion.originalCurrency()); value.setOriginalAmount(conversion.originalAmount());
|
||||||
|
value.setExchangeRate(conversion.exchangeRate()); value.setExchangeRateMonth(conversion.exchangeRateMonth());
|
||||||
|
value.setChargeableQuantity(request.chargeableQuantity()); value.setQuantityUnit(text(request.quantityUnit()));
|
||||||
|
value.setUnitPrice(request.unitPrice()); value.setPeriodStart(request.periodStart()); value.setPeriodEnd(request.periodEnd());
|
||||||
|
value.setReviewStatus("DRAFT");
|
||||||
value.setPublicationStatus("UNPUBLISHED"); value.setSourceStatus("ACTIVE"); value.setCreatedBy(operatorId); value.setUpdatedBy(operatorId);
|
value.setPublicationStatus("UNPUBLISHED"); value.setSourceStatus("ACTIVE"); value.setCreatedBy(operatorId); value.setUpdatedBy(operatorId);
|
||||||
value.setCreatedAt(now()); value.setUpdatedAt(now());
|
value.setCreatedAt(now()); value.setUpdatedAt(now());
|
||||||
return value;
|
return value;
|
||||||
@@ -172,6 +178,12 @@ public class OverseasFeeSourceService {
|
|||||||
if ("OVERSEAS_CONTAINER".equals(value.businessType())
|
if ("OVERSEAS_CONTAINER".equals(value.businessType())
|
||||||
&& !OVERSEAS_CONTAINER_FEE_ITEMS.contains(value.feeItemCode().trim()))
|
&& !OVERSEAS_CONTAINER_FEE_ITEMS.contains(value.feeItemCode().trim()))
|
||||||
throw bad("海外货柜费用项目不受支持");
|
throw bad("海外货柜费用项目不受支持");
|
||||||
|
if ((value.periodStart() == null) != (value.periodEnd() == null)
|
||||||
|
|| value.periodStart() != null && value.periodStart().isAfter(value.periodEnd()))
|
||||||
|
throw bad("计费期间必须同时填写且开始日期不能晚于结束日期");
|
||||||
|
if ((value.chargeableQuantity() == null) != (text(value.quantityUnit()) == null)
|
||||||
|
|| value.unitPrice() != null && value.chargeableQuantity() == null)
|
||||||
|
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) {
|
||||||
|
|||||||
+4
@@ -83,6 +83,10 @@ public class DestuffingPalletCostService {
|
|||||||
v.setBusinessId(costId); v.setBusinessNo("DESTUFF-"+bookingId); v.setContainerBookingId(parseLong(bookingId)); v.setContainerNo(containerNo);
|
v.setBusinessId(costId); v.setBusinessNo("DESTUFF-"+bookingId); v.setContainerBookingId(parseLong(bookingId)); v.setContainerNo(containerNo);
|
||||||
v.setWarehouseId(warehouseId); v.setFeeItemCode("DESTUFF_PALLET"); v.setFeeItemName("拆柜打托费"); v.setDirection("RECEIVABLE");
|
v.setWarehouseId(warehouseId); v.setFeeItemCode("DESTUFF_PALLET"); v.setFeeItemName("拆柜打托费"); v.setDirection("RECEIVABLE");
|
||||||
v.setPaymentVoucherFileIds(List.of()); v.setBusinessDate(LocalDate.now()); v.setCurrency("CNY"); v.setAmount(amount);
|
v.setPaymentVoucherFileIds(List.of()); v.setBusinessDate(LocalDate.now()); v.setCurrency("CNY"); v.setAmount(amount);
|
||||||
|
v.setOriginalCurrency("CNY"); v.setOriginalAmount(amount); v.setExchangeRate(BigDecimal.ONE.setScale(6));
|
||||||
|
v.setExchangeRateMonth(LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM")));
|
||||||
|
v.setChargeableQuantity(volume.volume()); v.setQuantityUnit("CBM");
|
||||||
|
v.setUnitPrice(volume.volume().signum() > 0 ? amount.divide(volume.volume(), 6, RoundingMode.HALF_UP) : null);
|
||||||
v.setReviewStatus("APPROVED"); v.setPublicationStatus("PUBLISHED"); v.setSourceStatus("ACTIVE"); v.setReviewerId(operator);
|
v.setReviewStatus("APPROVED"); v.setPublicationStatus("PUBLISHED"); v.setSourceStatus("ACTIVE"); v.setReviewerId(operator);
|
||||||
v.setReviewedAt(now); v.setCreatedBy(operator); v.setUpdatedBy(operator); v.setCreatedAt(now); v.setUpdatedAt(now); return v;
|
v.setReviewedAt(now); v.setCreatedBy(operator); v.setUpdatedBy(operator); v.setCreatedAt(now); v.setUpdatedAt(now); return v;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -832,8 +832,13 @@ CREATE TABLE IF NOT EXISTS ll_finance_fee_source (
|
|||||||
supplier_category VARCHAR(32) NULL COMMENT '应付供应商类别约束', settlement_mode VARCHAR(16) NULL COMMENT 'ON_ACCOUNT账期应付或DIRECT_PAID业务已付',
|
supplier_category VARCHAR(32) NULL COMMENT '应付供应商类别约束', settlement_mode VARCHAR(16) NULL COMMENT 'ON_ACCOUNT账期应付或DIRECT_PAID业务已付',
|
||||||
paid_at DATETIME(3) NULL COMMENT '直接已付时间', payment_method VARCHAR(32) NULL COMMENT '直接已付方式',
|
paid_at DATETIME(3) NULL COMMENT '直接已付时间', payment_method VARCHAR(32) NULL COMMENT '直接已付方式',
|
||||||
payment_reference_no VARCHAR(128) NULL COMMENT '直接已付流水号', payment_voucher_file_ids JSON NULL COMMENT '直接已付凭证文件ID快照',
|
payment_reference_no VARCHAR(128) NULL COMMENT '直接已付流水号', payment_voucher_file_ids JSON NULL COMMENT '直接已付凭证文件ID快照',
|
||||||
business_date DATE NOT NULL COMMENT '费用业务日期', currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '财务结算币种,当前固定CNY',
|
business_date DATE NOT NULL COMMENT '费用业务日期', currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '财务结算币种,固定CNY',
|
||||||
amount DECIMAL(20,6) NOT NULL COMMENT '费用金额', review_status VARCHAR(16) NOT NULL COMMENT 'DRAFT、PENDING、APPROVED或REJECTED',
|
amount DECIMAL(20,6) NOT NULL COMMENT '人民币结算金额', original_currency CHAR(3) NOT NULL COMMENT '业务原币种,支持CNY或USD',
|
||||||
|
original_amount DECIMAL(20,6) NOT NULL COMMENT '业务原币金额', exchange_rate DECIMAL(20,6) NOT NULL COMMENT '原币折算人民币汇率,人民币固定为1',
|
||||||
|
exchange_rate_month CHAR(7) NOT NULL COMMENT '汇率月份yyyy-MM', chargeable_quantity DECIMAL(20,6) NULL COMMENT '计费数量,如箱数、托数、体积或天数',
|
||||||
|
quantity_unit VARCHAR(32) NULL COMMENT '计费数量单位', unit_price DECIMAL(20,6) NULL COMMENT '原币计费单价',
|
||||||
|
period_start DATE NULL COMMENT '仓租等期间费用开始日期', period_end DATE NULL COMMENT '仓租等期间费用结束日期',
|
||||||
|
review_status VARCHAR(16) NOT NULL COMMENT 'DRAFT、PENDING、APPROVED或REJECTED',
|
||||||
publication_status VARCHAR(16) NOT NULL COMMENT 'UNPUBLISHED或PUBLISHED', source_status VARCHAR(16) NOT NULL COMMENT 'ACTIVE、SUPERSEDED或VOIDED',
|
publication_status VARCHAR(16) NOT NULL COMMENT 'UNPUBLISHED或PUBLISHED', source_status VARCHAR(16) NOT NULL COMMENT 'ACTIVE、SUPERSEDED或VOIDED',
|
||||||
review_reason VARCHAR(500) NULL COMMENT '审核或作废原因', reviewer_id VARCHAR(128) NULL COMMENT '审核人ID', reviewed_at DATETIME(3) NULL COMMENT '审核时间',
|
review_reason VARCHAR(500) NULL COMMENT '审核或作废原因', reviewer_id VARCHAR(128) NULL COMMENT '审核人ID', reviewed_at DATETIME(3) NULL COMMENT '审核时间',
|
||||||
created_by VARCHAR(128) NOT NULL COMMENT '创建人ID', updated_by VARCHAR(128) NOT NULL COMMENT '最后更新人ID',
|
created_by VARCHAR(128) NOT NULL COMMENT '创建人ID', updated_by VARCHAR(128) NOT NULL COMMENT '最后更新人ID',
|
||||||
@@ -842,6 +847,8 @@ CREATE TABLE IF NOT EXISTS ll_finance_fee_source (
|
|||||||
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),
|
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_original_currency CHECK(original_currency IN('CNY','USD')),
|
||||||
|
CONSTRAINT chk_ll_finance_fee_period CHECK((period_start IS NULL AND period_end IS NULL) OR (period_start IS NOT NULL AND period_end IS NOT NULL AND period_start<=period_end)),
|
||||||
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')),
|
||||||
CONSTRAINT chk_ll_finance_fee_publication CHECK(publication_status IN('UNPUBLISHED','PUBLISHED')),
|
CONSTRAINT chk_ll_finance_fee_publication CHECK(publication_status IN('UNPUBLISHED','PUBLISHED')),
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ components:
|
|||||||
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, USD], description: 业务原币;USD由服务端按业务月份有效汇率折算为CNY入池 }
|
||||||
amount: { type: number, format: decimal, minimum: 0, exclusiveMinimum: true }
|
amount: { type: number, format: decimal, minimum: 0, exclusiveMinimum: true, description: 业务原币金额 }
|
||||||
|
chargeableQuantity: { type: number, format: decimal, minimum: 0, exclusiveMinimum: true, nullable: true }
|
||||||
|
quantityUnit: { type: string, maxLength: 32, nullable: true }
|
||||||
|
unitPrice: { type: number, format: decimal, minimum: 0, exclusiveMinimum: true, nullable: true }
|
||||||
|
periodStart: { type: string, format: date, nullable: true }
|
||||||
|
periodEnd: { type: string, format: date, nullable: true }
|
||||||
securitySchemes: { bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT } }
|
securitySchemes: { bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT } }
|
||||||
|
|||||||
+6
-2
@@ -4,6 +4,7 @@ import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
|
|||||||
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
|
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
|
||||||
import com.baiyue.lastleg.financefee.mapper.OverseasFeeCommandMapper;
|
import com.baiyue.lastleg.financefee.mapper.OverseasFeeCommandMapper;
|
||||||
import com.baiyue.lastleg.financefee.mapper.OverseasFeeSourceMapper;
|
import com.baiyue.lastleg.financefee.mapper.OverseasFeeSourceMapper;
|
||||||
|
import com.baiyue.lastleg.masterdata.exchange.mapper.MonthlyExchangeRateMapper;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
@@ -22,7 +23,9 @@ class OverseasContainerFeeSourceServiceTest {
|
|||||||
private final OverseasFeeBusinessGuard guard = mock(OverseasFeeBusinessGuard.class);
|
private final OverseasFeeBusinessGuard guard = mock(OverseasFeeBusinessGuard.class);
|
||||||
private final OverseasFeeOutboxService outbox = mock(OverseasFeeOutboxService.class);
|
private final OverseasFeeOutboxService outbox = mock(OverseasFeeOutboxService.class);
|
||||||
private final OverseasFeeSourceService service = new OverseasFeeSourceService(
|
private final OverseasFeeSourceService service = new OverseasFeeSourceService(
|
||||||
mapper, commands, guard, outbox, new ObjectMapper().findAndRegisterModules());
|
mapper, commands, guard, outbox,
|
||||||
|
new OverseasFeeCurrencyConverter(mock(MonthlyExchangeRateMapper.class)),
|
||||||
|
new ObjectMapper().findAndRegisterModules());
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createsContainerFeeUsingTheRealContainerAsItsStableBusinessReference() {
|
void createsContainerFeeUsingTheRealContainerAsItsStableBusinessReference() {
|
||||||
@@ -50,7 +53,8 @@ class OverseasContainerFeeSourceServiceTest {
|
|||||||
source.warehouseId(), "CUSTOM_FEE", "自定义费", source.direction(), source.supplierId(),
|
source.warehouseId(), "CUSTOM_FEE", "自定义费", source.direction(), source.supplierId(),
|
||||||
source.supplierName(), source.supplierCategory(), source.settlementMode(), source.paidAt(),
|
source.supplierName(), source.supplierCategory(), source.settlementMode(), source.paidAt(),
|
||||||
source.paymentMethod(), source.paymentReferenceNo(), source.paymentVoucherFileIds(),
|
source.paymentMethod(), source.paymentReferenceNo(), source.paymentVoucherFileIds(),
|
||||||
source.businessDate(), source.currency(), source.amount());
|
source.businessDate(), source.currency(), source.amount(), source.chargeableQuantity(),
|
||||||
|
source.quantityUnit(), source.unitPrice(), source.periodStart(), source.periodEnd());
|
||||||
when(commands.find("operator-a", "CREATE", "bad-container-fee")).thenReturn(null);
|
when(commands.find("operator-a", "CREATE", "bad-container-fee")).thenReturn(null);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.create("operator-a", "bad-container-fee", request))
|
assertThatThrownBy(() -> service.create("operator-a", "bad-container-fee", request))
|
||||||
|
|||||||
+4
-2
@@ -57,7 +57,8 @@ class OverseasFeeBusinessGuardTest {
|
|||||||
source.warehouseId(), source.feeItemCode(), source.feeItemName(), source.direction(), source.supplierId(),
|
source.warehouseId(), source.feeItemCode(), source.feeItemName(), source.direction(), source.supplierId(),
|
||||||
source.supplierName(), source.supplierCategory(), source.settlementMode(), source.paidAt(),
|
source.supplierName(), source.supplierCategory(), source.settlementMode(), source.paidAt(),
|
||||||
source.paymentMethod(), source.paymentReferenceNo(), source.paymentVoucherFileIds(),
|
source.paymentMethod(), source.paymentReferenceNo(), source.paymentVoucherFileIds(),
|
||||||
source.businessDate(), source.currency(), source.amount());
|
source.businessDate(), source.currency(), source.amount(), source.chargeableQuantity(),
|
||||||
|
source.quantityUnit(), source.unitPrice(), source.periodStart(), source.periodEnd());
|
||||||
StubJdbc jdbc = new StubJdbc();
|
StubJdbc jdbc = new StubJdbc();
|
||||||
|
|
||||||
assertThatThrownBy(() -> new OverseasFeeBusinessGuard(jdbc).requireOwned(inconsistent))
|
assertThatThrownBy(() -> new OverseasFeeBusinessGuard(jdbc).requireOwned(inconsistent))
|
||||||
@@ -83,7 +84,8 @@ class OverseasFeeBusinessGuardTest {
|
|||||||
return new SaveRequest(customerId, "客户A", "OVERSEAS_CONTAINER_FEE", "OVERSEAS_CONTAINER",
|
return new SaveRequest(customerId, "客户A", "OVERSEAS_CONTAINER_FEE", "OVERSEAS_CONTAINER",
|
||||||
bookingId, containerNo, null, null, null, null, bookingId, containerNo, 91L,
|
bookingId, containerNo, null, null, null, null, bookingId, containerNo, 91L,
|
||||||
"PORT_FEE", "港口费", "RECEIVABLE", null, null, null, null, null, null, null,
|
"PORT_FEE", "港口费", "RECEIVABLE", null, null, null, null, null, null, null,
|
||||||
List.of(), LocalDate.of(2026, 9, 8), "CNY", new BigDecimal("88.00"));
|
List.of(), LocalDate.of(2026, 9, 8), "CNY", new BigDecimal("88.00"),
|
||||||
|
null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class StubJdbc extends JdbcTemplate {
|
private static final class StubJdbc extends JdbcTemplate {
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package com.baiyue.lastleg.financefee.service;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.masterdata.exchange.entity.MonthlyExchangeRate;
|
||||||
|
import com.baiyue.lastleg.masterdata.exchange.mapper.MonthlyExchangeRateMapper;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class OverseasFeeCurrencyConverterTest {
|
||||||
|
private final MonthlyExchangeRateMapper rates = mock(MonthlyExchangeRateMapper.class);
|
||||||
|
private final OverseasFeeCurrencyConverter converter = new OverseasFeeCurrencyConverter(rates);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void convertsUsdUsingTheActiveRateForTheBusinessMonth() {
|
||||||
|
MonthlyExchangeRate rate = new MonthlyExchangeRate();
|
||||||
|
rate.setStatus("ACTIVE"); rate.setUsdToCnyAverage(new BigDecimal("7.120000"));
|
||||||
|
when(rates.findMonth("2026-09")).thenReturn(rate);
|
||||||
|
|
||||||
|
var result = converter.convert("USD", new BigDecimal("12.34"), LocalDate.of(2026, 9, 9));
|
||||||
|
|
||||||
|
assertThat(result.originalAmount()).isEqualByComparingTo("12.34");
|
||||||
|
assertThat(result.exchangeRate()).isEqualByComparingTo("7.120000");
|
||||||
|
assertThat(result.settlementAmount()).isEqualByComparingTo("87.86");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refusesUsdWhenTheBusinessMonthHasNoActiveRate() {
|
||||||
|
assertThatThrownBy(() -> converter.convert("USD", BigDecimal.TEN, LocalDate.of(2026, 9, 9)))
|
||||||
|
.hasMessageContaining("缺少有效USD兑CNY汇率");
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -38,6 +38,8 @@ class OverseasFeeOutboxServiceTest {
|
|||||||
source.setFeeItemCode("PROCUREMENT_SERVICE"); source.setFeeItemName("采购服务费");
|
source.setFeeItemCode("PROCUREMENT_SERVICE"); source.setFeeItemName("采购服务费");
|
||||||
source.setDirection("RECEIVABLE"); source.setBusinessDate(LocalDate.of(2026, 9, 2));
|
source.setDirection("RECEIVABLE"); source.setBusinessDate(LocalDate.of(2026, 9, 2));
|
||||||
source.setCurrency("CNY"); source.setAmount(new BigDecimal("120.00"));
|
source.setCurrency("CNY"); source.setAmount(new BigDecimal("120.00"));
|
||||||
|
source.setOriginalCurrency("USD"); source.setOriginalAmount(new BigDecimal("16.85"));
|
||||||
|
source.setExchangeRate(new BigDecimal("7.122000")); source.setExchangeRateMonth("2026-09");
|
||||||
source.setReviewStatus("APPROVED"); source.setPublicationStatus("PUBLISHED");
|
source.setReviewStatus("APPROVED"); source.setPublicationStatus("PUBLISHED");
|
||||||
source.setSourceStatus("ACTIVE"); source.setPaymentVoucherFileIds(List.of());
|
source.setSourceStatus("ACTIVE"); source.setPaymentVoucherFileIds(List.of());
|
||||||
|
|
||||||
@@ -52,5 +54,7 @@ class OverseasFeeOutboxServiceTest {
|
|||||||
assertThat(payload.inboundId()).isNull();
|
assertThat(payload.inboundId()).isNull();
|
||||||
assertThat(payload.inboundCode()).isNull();
|
assertThat(payload.inboundCode()).isNull();
|
||||||
assertThat(payload.sourceHash()).startsWith("sha256:").hasSize(71);
|
assertThat(payload.sourceHash()).startsWith("sha256:").hasSize(71);
|
||||||
|
assertThat(payload.originalCurrency()).isEqualTo("USD");
|
||||||
|
assertThat(payload.exchangeRateMonth()).isEqualTo("2026-09");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user