优化
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import com.baiyue.finance.controller.FinanceSettlementController;
|
||||
import com.baiyue.finance.dto.FinanceSettlementDtos.CreatePayableSettlementRequest;
|
||||
import com.baiyue.finance.dto.FinanceSettlementDtos.CreateReceivableSettlementRequest;
|
||||
import com.baiyue.finance.dto.FinanceSettlementDtos.CreateSettlementPaymentRequest;
|
||||
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementDetail;
|
||||
import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementSummary;
|
||||
import com.baiyue.finance.service.FinanceEventException;
|
||||
import com.baiyue.finance.service.FinanceSettlementService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Date;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.startsWith;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FinanceSettlementBehaviorTest {
|
||||
@Mock FinanceSettlementService controllerService;
|
||||
|
||||
@Test
|
||||
void controllerReturnsFixedEnvelopeAndEnforcesDirectionPermission() {
|
||||
FinanceSettlementController controller = new FinanceSettlementController(controllerService);
|
||||
SettlementDetail payable = detail("PAYABLE", "ISSUED", BigDecimal.ZERO, new BigDecimal("100.00"), 0);
|
||||
when(controllerService.getSettlement(88L)).thenReturn(payable);
|
||||
AuthenticatedUser apViewer = user(Set.of("platform:finance:settlement-ap:view"));
|
||||
|
||||
var response = controller.getSettlement(apViewer, 88L);
|
||||
|
||||
assertThat(response.code()).isEqualTo("200");
|
||||
assertThat(response.data()).isSameAs(payable);
|
||||
assertThat(response.requestId()).isNotBlank();
|
||||
assertThatThrownBy(() -> controller.getSettlement(
|
||||
user(Set.of("platform:finance:settlement-ar:view")), 88L)).isInstanceOf(AccessDeniedException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void arCreationRejectsNonCnyBeforeDatabaseAccess() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
|
||||
assertCode(() -> service.createReceivableSettlement(7L, "ar-key",
|
||||
new CreateReceivableSettlementRequest(9L, "USD")), "FIN_400");
|
||||
|
||||
verify(jdbc, never()).queryForList(anyString(), (Object[]) any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void payableCreationRejectsDuplicateSourcesBeforeIdempotencyReservation() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
|
||||
assertCode(() -> service.createPayableSettlement(7L, "ap-key",
|
||||
new CreatePayableSettlementRequest(10L, 20L, List.of(3L, 3L), LocalDate.now(), "CNY")), "FIN_400");
|
||||
|
||||
verify(jdbc, never()).update(startsWith("INSERT INTO fin_idempotency_record"), (Object[]) any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void paymentFailsClosedWhenFileServiceIsUnavailable() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
FinanceFileClient fileClient = org.mockito.Mockito.mock(FinanceFileClient.class);
|
||||
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,direction"), eq(88L)))
|
||||
.thenReturn(List.of(settlementRow("PAYABLE", "ISSUED", BigDecimal.ZERO, new BigDecimal("100.00"), 0)));
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L),
|
||||
eq("SETTLEMENT_PAYMENT_CREATE"), eq("pay-key"))).thenReturn(List.of());
|
||||
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");
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialPaymentUpdatesOutstandingAndReturnsTypedDetail() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class, invocation ->
|
||||
"update".equals(invocation.getMethod().getName())
|
||||
? 1 : org.mockito.Mockito.RETURNS_DEFAULTS.answer(invocation));
|
||||
FinanceFileClient fileClient = org.mockito.Mockito.mock(FinanceFileClient.class);
|
||||
Map<String, Object> before = settlementRow("PAYABLE", "ISSUED", BigDecimal.ZERO,
|
||||
new BigDecimal("100.00"), 0);
|
||||
Map<String, Object> after = settlementRow("PAYABLE", "PARTIALLY_PAID", new BigDecimal("40.00"),
|
||||
new BigDecimal("60.00"), 1);
|
||||
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,direction"), eq(88L))).thenReturn(List.of(before));
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L),
|
||||
eq("SETTLEMENT_PAYMENT_CREATE"), eq("pay-key"))).thenReturn(List.of());
|
||||
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,settlement_no"), eq(88L))).thenReturn(List.of(after));
|
||||
when(jdbc.queryForList(startsWith("SELECT f.id fee_source_id"), eq(10L), eq(88L))).thenReturn(List.of());
|
||||
when(jdbc.queryForList(startsWith("SELECT id,transaction_type"), eq(10L), eq(88L))).thenReturn(List.of(Map.of(
|
||||
"id", 501L, "transaction_type", "PAYMENT", "transaction_date", Date.valueOf(LocalDate.of(2026, 8, 11)),
|
||||
"amount", new BigDecimal("40.00"), "method", "BANK_TRANSFER", "reference_no", "BANK-1",
|
||||
"voucher_file_ids", "[\"91\"]", "created_by", 7L,
|
||||
"created_at", Timestamp.valueOf(LocalDateTime.of(2026, 8, 11, 10, 0)), "remark", "首笔付款")));
|
||||
when(jdbc.queryForList(startsWith("SELECT action,reason"), eq(10L), eq(88L))).thenReturn(List.of());
|
||||
when(fileClient.metadata("internal-token", 10L, 10L, 91L)).thenReturn(ApiResponse.success(
|
||||
new FinanceFileClient.Metadata(91L, 10L, "FINANCE_VOUCHER", "10", "pay.png", "abc", "image/png", 10L)));
|
||||
FinanceSettlementService service = service(jdbc, fileClient);
|
||||
|
||||
SettlementDetail result = service.createSettlementPayment(88L, 7L, "pay-key",
|
||||
payment(new BigDecimal("40.00"), 0));
|
||||
|
||||
assertThat(result.summary().status()).isEqualTo("PARTIALLY_PAID");
|
||||
assertThat(result.summary().paidAmount()).isEqualByComparingTo("40.00");
|
||||
assertThat(result.summary().outstandingAmount()).isEqualByComparingTo("60.00");
|
||||
assertThat(result.transactions()).singleElement().extracting(transaction -> transaction.transactionType())
|
||||
.isEqualTo("PAYMENT");
|
||||
verify(jdbc).update(startsWith("UPDATE fin_settlement SET paid_amount"),
|
||||
any(), any(), any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static CreateSettlementPaymentRequest payment(BigDecimal amount, int version) {
|
||||
return new CreateSettlementPaymentRequest(LocalDate.of(2026, 8, 11), amount, "CNY", "BANK_TRANSFER",
|
||||
"BANK-1", List.of("91"), version, "首笔付款");
|
||||
}
|
||||
|
||||
private static Map<String, Object> settlementRow(String direction, String status, BigDecimal paid,
|
||||
BigDecimal outstanding, int version) {
|
||||
return new java.util.HashMap<>(Map.ofEntries(
|
||||
Map.entry("id", 88L), Map.entry("tenant_id", 10L), Map.entry("settlement_no", "AP-20260811-001"),
|
||||
Map.entry("direction", direction), Map.entry("customer_name_snapshot", "客户10"),
|
||||
Map.entry("supplier_id", 20L), Map.entry("supplier_name_snapshot", "供应商20"),
|
||||
Map.entry("business_period", "2026-08"), Map.entry("currency_code", "CNY"),
|
||||
Map.entry("total_amount", new BigDecimal("100.00")), Map.entry("paid_amount", paid),
|
||||
Map.entry("outstanding_amount", outstanding), Map.entry("generated_date", Date.valueOf("2026-08-11")),
|
||||
Map.entry("due_date", Date.valueOf("2026-09-10")),
|
||||
Map.entry("payment_status", paid.signum() == 0 ? "UNPAID" : status), Map.entry("status", status),
|
||||
Map.entry("version", version)));
|
||||
}
|
||||
|
||||
private static SettlementDetail detail(String direction, String status, BigDecimal paid,
|
||||
BigDecimal outstanding, int version) {
|
||||
return new SettlementDetail(new SettlementSummary(88L, "SET-88", direction, 10L, "客户10", 20L,
|
||||
"供应商20", null, "2026-08", "CNY", paid.add(outstanding), paid, outstanding,
|
||||
LocalDate.of(2026, 8, 11), LocalDate.of(2026, 9, 10), paid.signum() == 0 ? "UNPAID" : status,
|
||||
status, version), List.of(), List.of(), List.of());
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(Set<String> permissions) {
|
||||
return new AuthenticatedUser(7L, null, null, null, "finance", "ADMIN", "WEB", permissions);
|
||||
}
|
||||
|
||||
private static void assertCode(org.assertj.core.api.ThrowableAssert.ThrowingCallable call, String code) {
|
||||
assertThatThrownBy(call).isInstanceOf(FinanceEventException.class)
|
||||
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo(code);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user