优化
This commit is contained in:
@@ -17,6 +17,8 @@ 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.jdbc.core.PreparedStatementCreator;
|
||||
import org.springframework.jdbc.support.KeyHolder;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
@@ -35,9 +37,11 @@ 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 org.mockito.ArgumentCaptor;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.RETURNS_DEFAULTS;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FinanceSettlementBehaviorTest {
|
||||
@@ -99,6 +103,25 @@ class FinanceSettlementBehaviorTest {
|
||||
verify(jdbc, never()).update(startsWith("UPDATE fin_settlement SET paid_amount"), (Object[]) any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void paymentRejectsInvalidCurrencyMissingVoucherStaleVersionAndOverage() {
|
||||
JdbcTemplate noDatabase = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
FinanceSettlementService validationService = service(noDatabase, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
assertCode(() -> validationService.createSettlementPayment(88L, 7L, "usd-key",
|
||||
new CreateSettlementPaymentRequest(LocalDate.now(), BigDecimal.ONE, "USD", "BANK_TRANSFER", "BANK-1", List.of("91"), 0, null)), "FIN_400");
|
||||
assertCode(() -> validationService.createSettlementPayment(88L, 7L, "voucher-key",
|
||||
new CreateSettlementPaymentRequest(LocalDate.now(), BigDecimal.ONE, "CNY", "BANK_TRANSFER", "BANK-1", List.of(), 0, null)), "FIN_400");
|
||||
verify(noDatabase, never()).queryForList(anyString(), (Object[]) any());
|
||||
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,direction"), eq(88L)))
|
||||
.thenReturn(List.of(settlementRow("PAYABLE", "ISSUED", BigDecimal.ZERO, new BigDecimal("100.00"), 2)));
|
||||
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
assertCode(() -> service.createSettlementPayment(88L, 7L, "stale-key", payment(new BigDecimal("40.00"), 1)), "FIN_409");
|
||||
assertCode(() -> service.createSettlementPayment(88L, 7L, "over-key", payment(new BigDecimal("100.01"), 2)), "FIN_409");
|
||||
verify(jdbc, never()).update(startsWith("INSERT INTO fin_settlement_payment"), (Object[]) any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialPaymentUpdatesOutstandingAndReturnsTypedDetail() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class, invocation ->
|
||||
@@ -136,6 +159,232 @@ class FinanceSettlementBehaviorTest {
|
||||
any(), any(), any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void issuedReconciliationCreatesUniqueReceivableSettlement() {
|
||||
Map<String, Object> settlement = settlementRow("RECEIVABLE", "ISSUED", BigDecimal.ZERO,
|
||||
new BigDecimal("300.00"), 0);
|
||||
settlement.put("reconciliation_statement_id", 501L);
|
||||
JdbcTemplate jdbc = creationJdbc(settlement);
|
||||
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,bill_no"), eq(501L))).thenReturn(List.of(Map.ofEntries(
|
||||
Map.entry("id", 501L), Map.entry("tenant_id", 10L), Map.entry("bill_no", "REC-501"),
|
||||
Map.entry("customer_name_snapshot", "客户10"), Map.entry("currency_code", "CNY"),
|
||||
Map.entry("bill_date", Date.valueOf("2026-08-31")), Map.entry("due_date", Date.valueOf("2026-09-30")),
|
||||
Map.entry("status", "ISSUED"), Map.entry("total_amount", new BigDecimal("300.00")))));
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AR_CREATE"), eq("ar-key"))).thenReturn(List.of());
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_settlement WHERE"), eq(Integer.class), eq(10L), eq(501L))).thenReturn(0);
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_bill_line"), eq(Integer.class), eq(10L), eq(501L))).thenReturn(1);
|
||||
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
|
||||
SettlementDetail result = service.createReceivableSettlement(7L, "ar-key",
|
||||
new CreateReceivableSettlementRequest(501L, "CNY"));
|
||||
|
||||
assertThat(result.summary().direction()).isEqualTo("RECEIVABLE");
|
||||
assertThat(result.summary().totalAmount()).isEqualByComparingTo("300.00");
|
||||
assertThat(result.summary().reconciliationStatementId()).isEqualTo(501L);
|
||||
verify(jdbc).update(startsWith("INSERT INTO fin_settlement_line"), eq(88L), eq(10L), eq(501L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivableCreationRejectsAlreadySettledStatement() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,bill_no"), eq(501L))).thenReturn(List.of(Map.ofEntries(
|
||||
Map.entry("id", 501L), Map.entry("tenant_id", 10L), Map.entry("bill_no", "REC-501"),
|
||||
Map.entry("customer_name_snapshot", "客户10"), Map.entry("currency_code", "CNY"),
|
||||
Map.entry("bill_date", Date.valueOf("2026-08-31")), Map.entry("due_date", Date.valueOf("2026-09-30")),
|
||||
Map.entry("status", "ISSUED"), Map.entry("total_amount", new BigDecimal("300.00")))));
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AR_CREATE"), eq("ar-duplicate"))).thenReturn(List.of());
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_settlement WHERE"), eq(Integer.class), eq(10L), eq(501L))).thenReturn(1);
|
||||
|
||||
assertCode(() -> service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class)).createReceivableSettlement(7L, "ar-duplicate",
|
||||
new CreateReceivableSettlementRequest(501L, "CNY")), "FIN_409");
|
||||
verify(jdbc, never()).update(any(PreparedStatementCreator.class), any(KeyHolder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void eligibleSameSupplierSourcesCreatePayableSettlement() {
|
||||
JdbcTemplate jdbc = creationJdbc(settlementRow("PAYABLE", "ISSUED", BigDecimal.ZERO,
|
||||
new BigDecimal("100.00"), 0));
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AP_CREATE"), eq("ap-key"))).thenReturn(List.of());
|
||||
when(jdbc.queryForList(startsWith("SELECT id,customer_id"), eq(31L))).thenReturn(List.of(payableSource(10L, 20L)));
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_settlement_line"), eq(Integer.class), eq(31L))).thenReturn(0);
|
||||
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
|
||||
SettlementDetail result = service.createPayableSettlement(7L, "ap-key",
|
||||
new CreatePayableSettlementRequest(10L, 20L, List.of(31L), LocalDate.of(2026, 9, 30), "CNY"));
|
||||
|
||||
assertThat(result.summary().direction()).isEqualTo("PAYABLE");
|
||||
assertThat(result.summary().supplierId()).isEqualTo(20L);
|
||||
assertThat(result.summary().totalAmount()).isEqualByComparingTo("100.00");
|
||||
verify(jdbc).update(startsWith("INSERT INTO fin_settlement_line"), eq(10L), eq(88L), eq(31L),
|
||||
eq(new BigDecimal("100.00")), eq("CNY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void payableCreationHidesCrossCustomerSource() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AP_CREATE"), eq("ap-key"))).thenReturn(List.of());
|
||||
when(jdbc.queryForList(startsWith("SELECT id,customer_id"), eq(31L))).thenReturn(List.of(payableSource(11L, 20L)));
|
||||
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
|
||||
assertCode(() -> service.createPayableSettlement(7L, "ap-key",
|
||||
new CreatePayableSettlementRequest(10L, 20L, List.of(31L), LocalDate.of(2026, 9, 30), "CNY")), "FIN_404");
|
||||
verify(jdbc, never()).update(any(PreparedStatementCreator.class), any(KeyHolder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void payableCreationRejectsMixedSupplierIneligibleAndOccupiedSources() {
|
||||
for (String condition : List.of("supplier", "ineligible", "occupied")) {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
Map<String, Object> source = payableSource(10L, "supplier".equals(condition) ? 21L : 20L);
|
||||
if ("ineligible".equals(condition)) source.put("review_status", "PENDING");
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AP_CREATE"), eq("ap-" + condition))).thenReturn(List.of());
|
||||
when(jdbc.queryForList(startsWith("SELECT id,customer_id"), eq(31L))).thenReturn(List.of(source));
|
||||
org.mockito.Mockito.lenient().when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_settlement_line"), eq(Integer.class), eq(31L)))
|
||||
.thenReturn("occupied".equals(condition) ? 1 : 0);
|
||||
|
||||
assertCode(() -> service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class)).createPayableSettlement(7L, "ap-" + condition,
|
||||
new CreatePayableSettlementRequest(10L, 20L, List.of(31L), LocalDate.of(2026, 9, 30), "CNY")), "FIN_409");
|
||||
verify(jdbc, never()).update(any(PreparedStatementCreator.class), any(KeyHolder.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void settlementDetailRejectsMissingResource() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,settlement_no"), eq(999L))).thenReturn(List.of());
|
||||
|
||||
assertCode(() -> service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class)).getSettlement(999L), "FIN_404");
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalPaymentMarksSettlementPaid() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class, invocation ->
|
||||
"update".equals(invocation.getMethod().getName()) ? 1 : RETURNS_DEFAULTS.answer(invocation));
|
||||
FinanceFileClient fileClient = org.mockito.Mockito.mock(FinanceFileClient.class);
|
||||
Map<String, Object> before = settlementRow("PAYABLE", "PARTIALLY_PAID", new BigDecimal("40.00"), new BigDecimal("60.00"), 1);
|
||||
Map<String, Object> after = settlementRow("PAYABLE", "PAID", new BigDecimal("100.00"), BigDecimal.ZERO, 2);
|
||||
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("final-key"))).thenReturn(List.of());
|
||||
stubSettlementDetail(jdbc, after);
|
||||
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, "final-key", payment(new BigDecimal("60.00"), 1));
|
||||
|
||||
assertThat(result.summary().status()).isEqualTo("PAID");
|
||||
assertThat(result.summary().outstandingAmount()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivableSettlementDetailIncludesAppliedReceiptHistory() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
Map<String, Object> settlement = settlementRow("RECEIVABLE", "PARTIALLY_PAID", new BigDecimal("40.00"),
|
||||
new BigDecimal("60.00"), 1);
|
||||
settlement.put("reconciliation_statement_id", 501L);
|
||||
stubSettlementDetail(jdbc, settlement);
|
||||
when(jdbc.queryForList(startsWith("SELECT r.id,'RECEIPT'"), eq(10L), eq(501L))).thenReturn(List.of(Map.ofEntries(
|
||||
Map.entry("id", 701L), Map.entry("transaction_type", "RECEIPT"),
|
||||
Map.entry("transaction_date", Date.valueOf("2026-08-11")), Map.entry("amount", new BigDecimal("40.00")),
|
||||
Map.entry("method", "BANK_TRANSFER"), Map.entry("reference_no", "RCPT-701"),
|
||||
Map.entry("voucher_file_ids", "[\"91\"]"), Map.entry("created_by", 7L),
|
||||
Map.entry("created_at", Timestamp.valueOf("2026-08-11 10:00:00")), Map.entry("remark", "首笔收款"))));
|
||||
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
|
||||
SettlementDetail result = service.getSettlement(88L);
|
||||
|
||||
assertThat(result.transactions()).singleElement().extracting(transaction -> transaction.transactionType())
|
||||
.isEqualTo("RECEIPT");
|
||||
assertThat(result.transactions().get(0).amount()).isEqualByComparingTo("40.00");
|
||||
}
|
||||
|
||||
@Test
|
||||
void settlementIdempotencyReplaysRejectsMismatchAndRecoversInterruptedRecords() {
|
||||
Map<String, Object> body = Map.of("customerId", 10L, "amount", "100.00");
|
||||
JdbcTemplate firstJdbc = org.mockito.Mockito.mock(JdbcTemplate.class, invocation ->
|
||||
"update".equals(invocation.getMethod().getName()) ? 1 : RETURNS_DEFAULTS.answer(invocation));
|
||||
when(firstJdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AP_CREATE"), eq("idem-key"))).thenReturn(List.of());
|
||||
FinanceSettlementService firstService = service(firstJdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
|
||||
Long first = ReflectionTestUtils.invokeMethod(firstService, "reserve", 10L, 7L, "SETTLEMENT_AP_CREATE", "idem-key", "POST", "/settlements", body);
|
||||
ArgumentCaptor<String> hash = ArgumentCaptor.forClass(String.class);
|
||||
verify(firstJdbc).update(startsWith("INSERT INTO fin_idempotency_record"), eq(10L), eq(7L),
|
||||
eq("SETTLEMENT_AP_CREATE"), eq("idem-key"), hash.capture());
|
||||
assertThat(first).isNull();
|
||||
|
||||
JdbcTemplate replayJdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
when(replayJdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AP_CREATE"), eq("idem-key")))
|
||||
.thenReturn(List.of(Map.of("request_hash", hash.getValue(), "status", "SUCCEEDED", "result_resource_id", 88L,
|
||||
"updated_at", LocalDateTime.now())));
|
||||
FinanceSettlementService replayService = service(replayJdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
assertThat((Long) ReflectionTestUtils.invokeMethod(replayService, "reserve", 10L, 7L, "SETTLEMENT_AP_CREATE", "idem-key", "POST", "/settlements", body)).isEqualTo(88L);
|
||||
assertCode(() -> ReflectionTestUtils.invokeMethod(replayService, "reserve", 10L, 7L, "SETTLEMENT_AP_CREATE", "idem-key", "POST", "/settlements", Map.of("customerId", 10L, "amount", "101.00")), "FIN_409");
|
||||
|
||||
JdbcTemplate processingJdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
when(processingJdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AP_CREATE"), eq("processing-key")))
|
||||
.thenReturn(List.of(Map.of("request_hash", hash.getValue(), "status", "PROCESSING", "updated_at", LocalDateTime.now())));
|
||||
assertCode(() -> ReflectionTestUtils.invokeMethod(service(processingJdbc, org.mockito.Mockito.mock(FinanceFileClient.class)),
|
||||
"reserve", 10L, 7L, "SETTLEMENT_AP_CREATE", "processing-key", "POST", "/settlements", body), "FIN_409");
|
||||
|
||||
JdbcTemplate recoveryJdbc = org.mockito.Mockito.mock(JdbcTemplate.class, invocation ->
|
||||
"update".equals(invocation.getMethod().getName()) ? 1 : RETURNS_DEFAULTS.answer(invocation));
|
||||
when(recoveryJdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AP_CREATE"), eq("failed-key")))
|
||||
.thenReturn(List.of(Map.of("request_hash", hash.getValue(), "status", "FAILED", "updated_at", LocalDateTime.now())));
|
||||
when(recoveryJdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AP_CREATE"), eq("stale-key")))
|
||||
.thenReturn(List.of(Map.of("request_hash", hash.getValue(), "status", "PROCESSING", "updated_at", LocalDateTime.now().minusMinutes(10))));
|
||||
FinanceSettlementService recoveryService = service(recoveryJdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
assertThat((Long) ReflectionTestUtils.invokeMethod(recoveryService, "reserve", 10L, 7L, "SETTLEMENT_AP_CREATE", "failed-key", "POST", "/settlements", body)).isNull();
|
||||
assertThat((Long) ReflectionTestUtils.invokeMethod(recoveryService, "reserve", 10L, 7L, "SETTLEMENT_AP_CREATE", "stale-key", "POST", "/settlements", body)).isNull();
|
||||
|
||||
JdbcTemplate conflictJdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
FinanceSettlementService conflictService = service(conflictJdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
assertCode(() -> ReflectionTestUtils.invokeMethod(conflictService, "complete", 10L, 7L, "SETTLEMENT_AP_CREATE", "idem-key", 88L), "FIN_409");
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivableCreationRejectsAnExistingActiveSettlement() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,bill_no"), eq(501L))).thenReturn(List.of(Map.ofEntries(
|
||||
Map.entry("id", 501L), Map.entry("tenant_id", 10L), Map.entry("bill_no", "REC-501"),
|
||||
Map.entry("customer_name_snapshot", "客户10"), Map.entry("currency_code", "CNY"),
|
||||
Map.entry("bill_date", Date.valueOf("2026-08-31")), Map.entry("due_date", Date.valueOf("2026-09-30")),
|
||||
Map.entry("status", "ISSUED"), Map.entry("total_amount", new BigDecimal("300.00")))));
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AR_CREATE"), eq("ar-key"))).thenReturn(List.of());
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_settlement WHERE"), eq(Integer.class), eq(10L), eq(501L))).thenReturn(1);
|
||||
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
|
||||
assertCode(() -> service.createReceivableSettlement(7L, "ar-key", new CreateReceivableSettlementRequest(501L, "CNY")), "FIN_409");
|
||||
verify(jdbc, never()).update(any(PreparedStatementCreator.class), any(KeyHolder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void payableCreationRejectsMixedSupplierAndOccupiedSources() {
|
||||
JdbcTemplate mixedJdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
when(mixedJdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AP_CREATE"), eq("mixed-key"))).thenReturn(List.of());
|
||||
when(mixedJdbc.queryForList(startsWith("SELECT id,customer_id"), eq(31L))).thenReturn(List.of(payableSource(10L, 21L)));
|
||||
FinanceSettlementService mixedService = service(mixedJdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
CreatePayableSettlementRequest request = new CreatePayableSettlementRequest(10L, 20L, List.of(31L), LocalDate.of(2026, 9, 30), "CNY");
|
||||
assertCode(() -> mixedService.createPayableSettlement(7L, "mixed-key", request), "FIN_409");
|
||||
|
||||
JdbcTemplate occupiedJdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
when(occupiedJdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L), eq("SETTLEMENT_AP_CREATE"), eq("occupied-key"))).thenReturn(List.of());
|
||||
when(occupiedJdbc.queryForList(startsWith("SELECT id,customer_id"), eq(31L))).thenReturn(List.of(payableSource(10L, 20L)));
|
||||
when(occupiedJdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_settlement_line"), eq(Integer.class), eq(31L))).thenReturn(1);
|
||||
FinanceSettlementService occupiedService = service(occupiedJdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
assertCode(() -> occupiedService.createPayableSettlement(7L, "occupied-key", request), "FIN_409");
|
||||
}
|
||||
|
||||
@Test
|
||||
void paymentRejectsAmountAboveOutstandingBeforeVoucherWrite() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.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)));
|
||||
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
|
||||
assertCode(() -> service.createSettlementPayment(88L, 7L, "pay-key", payment(new BigDecimal("100.01"), 0)), "FIN_409");
|
||||
verify(jdbc, never()).update(startsWith("INSERT INTO fin_settlement_payment"), (Object[]) any());
|
||||
}
|
||||
|
||||
private static FinanceSettlementService service(JdbcTemplate jdbc, FinanceFileClient fileClient) {
|
||||
FinanceSettlementService service = new FinanceSettlementService(
|
||||
jdbc, new ObjectMapper().findAndRegisterModules(), fileClient);
|
||||
@@ -148,6 +397,37 @@ class FinanceSettlementBehaviorTest {
|
||||
"BANK-1", List.of("91"), version, "首笔付款");
|
||||
}
|
||||
|
||||
private static JdbcTemplate creationJdbc(Map<String, Object> settlement) {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class,
|
||||
org.mockito.Mockito.withSettings().lenient().defaultAnswer(invocation ->
|
||||
"update".equals(invocation.getMethod().getName()) ? 1 : RETURNS_DEFAULTS.answer(invocation)));
|
||||
org.mockito.Mockito.lenient().doAnswer(invocation -> {
|
||||
KeyHolder holder = invocation.getArgument(1);
|
||||
holder.getKeyList().add(Map.of("GENERATED_KEY", 88L));
|
||||
return 1;
|
||||
}).when(jdbc).update(any(PreparedStatementCreator.class), any(KeyHolder.class));
|
||||
stubSettlementDetail(jdbc, settlement);
|
||||
return jdbc;
|
||||
}
|
||||
|
||||
private static void stubSettlementDetail(JdbcTemplate jdbc, Map<String, Object> settlement) {
|
||||
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,settlement_no"), eq(88L))).thenReturn(List.of(settlement));
|
||||
long customerId = ((Number) settlement.get("tenant_id")).longValue();
|
||||
when(jdbc.queryForList(startsWith("SELECT f.id fee_source_id"), eq(customerId), eq(88L))).thenReturn(List.of());
|
||||
when(jdbc.queryForList(startsWith("SELECT id,transaction_type"), eq(customerId), eq(88L))).thenReturn(List.of());
|
||||
when(jdbc.queryForList(startsWith("SELECT action,reason"), eq(customerId), eq(88L))).thenReturn(List.of());
|
||||
}
|
||||
|
||||
private static Map<String, Object> payableSource(long customerId, long supplierId) {
|
||||
return new java.util.HashMap<>(Map.ofEntries(
|
||||
Map.entry("id", 31L), Map.entry("customer_id", customerId), Map.entry("direction", "PAYABLE"),
|
||||
Map.entry("currency_code", "CNY"), Map.entry("status", "AVAILABLE"),
|
||||
Map.entry("amount", new BigDecimal("100.00")), Map.entry("review_status", "APPROVED"),
|
||||
Map.entry("settlement_included", "true"), Map.entry("supplier_id", supplierId),
|
||||
Map.entry("supplier_name", "供应商20"), Map.entry("source_snapshot",
|
||||
"{\"customerName\":\"客户10\",\"supplierId\":20,\"supplierName\":\"供应商20\"}")));
|
||||
}
|
||||
|
||||
private static Map<String, Object> settlementRow(String direction, String status, BigDecimal paid,
|
||||
BigDecimal outstanding, int version) {
|
||||
return new java.util.HashMap<>(Map.ofEntries(
|
||||
@@ -155,7 +435,7 @@ class FinanceSettlementBehaviorTest {
|
||||
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("total_amount", paid.add(outstanding)), 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),
|
||||
|
||||
Reference in New Issue
Block a user