|
|
|
@@ -1,7 +1,6 @@
|
|
|
|
|
package com.baiyue.finance.service;
|
|
|
|
|
|
|
|
|
|
import com.baiyue.finance.client.FinanceFileClient;
|
|
|
|
|
import com.baiyue.finance.dto.FinanceWalletDtos.AccountAdjustmentRequest;
|
|
|
|
|
import com.baiyue.finance.dto.FinanceWalletDtos.WalletAccount;
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
@@ -9,8 +8,6 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
|
|
import org.springframework.jdbc.core.RowMapper;
|
|
|
|
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
|
|
import java.sql.ResultSet;
|
|
|
|
|
import java.sql.Timestamp;
|
|
|
|
|
import java.time.LocalDate;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
@@ -48,7 +45,7 @@ class FinanceWalletBehaviorTest {
|
|
|
|
|
void developmentProfileUsesTheNonPayableSampleQrCode() {
|
|
|
|
|
var channels = new FinanceWalletService(mock(JdbcTemplate.class), new ObjectMapper().findAndRegisterModules(),
|
|
|
|
|
mock(FinanceFileClient.class), mock(FinanceDocumentNumberGenerator.class), "token", "", "",
|
|
|
|
|
BigDecimal.ZERO, "cn", "development").channels();
|
|
|
|
|
"cn", "development").channels();
|
|
|
|
|
|
|
|
|
|
assertThat(channels).allSatisfy(channel -> {
|
|
|
|
|
assertThat(channel.enabled()).isTrue();
|
|
|
|
@@ -57,44 +54,29 @@ class FinanceWalletBehaviorTest {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void manualDeductionCannotExceedAvailableBalance() throws Exception {
|
|
|
|
|
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
|
|
|
|
when(jdbc.queryForList(startsWith("SELECT request_hash"), any(Object[].class))).thenReturn(List.of());
|
|
|
|
|
when(jdbc.update(startsWith("INSERT INTO fin_idempotency_record"), any(Object[].class))).thenReturn(1);
|
|
|
|
|
when(jdbc.update(startsWith("INSERT IGNORE INTO fin_wallet_account"), any(Object[].class))).thenReturn(1);
|
|
|
|
|
when(jdbc.queryForObject(startsWith("SELECT currency_code"), any(RowMapper.class), eq(9L))).thenAnswer(invocation -> {
|
|
|
|
|
@SuppressWarnings("unchecked") RowMapper<Object> mapper = invocation.getArgument(1);
|
|
|
|
|
ResultSet rs = mock(ResultSet.class);
|
|
|
|
|
when(rs.getString("currency_code")).thenReturn("CNY");
|
|
|
|
|
when(rs.getBigDecimal("actual_balance")).thenReturn(new BigDecimal("10.00"));
|
|
|
|
|
when(rs.getBigDecimal("frozen_balance")).thenReturn(BigDecimal.ZERO);
|
|
|
|
|
when(rs.getBigDecimal("available_balance")).thenReturn(new BigDecimal("10.00"));
|
|
|
|
|
when(rs.getBigDecimal("credit_limit")).thenReturn(BigDecimal.ZERO);
|
|
|
|
|
when(rs.getBigDecimal("credit_used")).thenReturn(BigDecimal.ZERO);
|
|
|
|
|
when(rs.getBigDecimal("credit_frozen")).thenReturn(BigDecimal.ZERO);
|
|
|
|
|
when(rs.getInt("version")).thenReturn(2);
|
|
|
|
|
when(rs.getTimestamp("updated_at")).thenReturn(Timestamp.valueOf("2026-09-05 08:00:00"));
|
|
|
|
|
return mapper.mapRow(rs, 0);
|
|
|
|
|
});
|
|
|
|
|
void deductionMayMakeActualBalanceNegative() {
|
|
|
|
|
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y");
|
|
|
|
|
WalletAccount initial = service.accountState(new BigDecimal("10.00"), BigDecimal.ZERO,
|
|
|
|
|
BigDecimal.ZERO, BigDecimal.ZERO, null, 2);
|
|
|
|
|
|
|
|
|
|
assertThatThrownBy(() -> service(jdbc, "x", "y").adjust(9L, 7L, "财务员", "deduct-1",
|
|
|
|
|
new AccountAdjustmentRequest("DEDUCTION", new BigDecimal("10.01"), LocalDate.now(),
|
|
|
|
|
"BANK_TRANSFER", "REF-1", "人工扣款", 2)))
|
|
|
|
|
.isInstanceOf(FinanceEventException.class)
|
|
|
|
|
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-WALLET-INSUFFICIENT");
|
|
|
|
|
verify(jdbc, never()).update(startsWith("UPDATE fin_wallet_account SET actual_balance="), any(Object[].class));
|
|
|
|
|
WalletAccount deducted = service.deductAccount(initial, new BigDecimal("10.01"));
|
|
|
|
|
|
|
|
|
|
assertThat(deducted.actualBalance()).isEqualByComparingTo("-0.01");
|
|
|
|
|
assertThat(deducted.availableBalance()).isEqualByComparingTo("-0.01");
|
|
|
|
|
assertThat(deducted.creditActive()).isFalse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void configuredCreditLimitIsUsedForAFirstAccountRead() {
|
|
|
|
|
void aMissingAccountNeverReceivesPermanentDefaultCredit() {
|
|
|
|
|
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
|
|
|
|
when(jdbc.query(startsWith("SELECT currency_code"), any(RowMapper.class), eq(9L))).thenReturn(List.of());
|
|
|
|
|
|
|
|
|
|
var account = service(jdbc, "x", "y", new BigDecimal("8000.00")).account(9L);
|
|
|
|
|
var account = service(jdbc, "x", "y").account(9L);
|
|
|
|
|
|
|
|
|
|
assertThat(account.creditLimit()).isEqualByComparingTo("8000.00");
|
|
|
|
|
assertThat(account.availableCredit()).isEqualByComparingTo("8000.00");
|
|
|
|
|
assertThat(account.availableBalance()).isEqualByComparingTo("8000.00");
|
|
|
|
|
assertThat(account.creditLimit()).isZero();
|
|
|
|
|
assertThat(account.availableCredit()).isZero();
|
|
|
|
|
assertThat(account.availableBalance()).isZero();
|
|
|
|
|
assertThat(account.creditExpiresOn()).isNull();
|
|
|
|
|
verify(jdbc, never()).update(anyString(), any(Object[].class));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -102,7 +84,7 @@ class FinanceWalletBehaviorTest {
|
|
|
|
|
void deductionUsesCashBeforeCreditAndRechargeRepaysCreditFirst() {
|
|
|
|
|
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y");
|
|
|
|
|
WalletAccount initial = service.accountState(new BigDecimal("100.00"), BigDecimal.ZERO,
|
|
|
|
|
new BigDecimal("50.00"), BigDecimal.ZERO, BigDecimal.ZERO, 3);
|
|
|
|
|
new BigDecimal("50.00"), BigDecimal.ZERO, LocalDate.now().plusDays(7), 3);
|
|
|
|
|
|
|
|
|
|
WalletAccount deducted = service.deductAccount(initial, new BigDecimal("120.00"));
|
|
|
|
|
assertThat(deducted.actualBalance()).isEqualByComparingTo("0.00");
|
|
|
|
@@ -120,7 +102,7 @@ class FinanceWalletBehaviorTest {
|
|
|
|
|
void deductionNeverConsumesCashReservedByAnActiveHold() {
|
|
|
|
|
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y");
|
|
|
|
|
WalletAccount initial = service.accountState(new BigDecimal("100.00"), new BigDecimal("50.00"),
|
|
|
|
|
new BigDecimal("80.00"), BigDecimal.ZERO, new BigDecimal("20.00"), 4);
|
|
|
|
|
new BigDecimal("80.00"), new BigDecimal("20.00"), LocalDate.now().plusDays(7), 4);
|
|
|
|
|
|
|
|
|
|
WalletAccount deducted = service.deductAccount(initial, new BigDecimal("80.00"));
|
|
|
|
|
|
|
|
|
@@ -135,37 +117,40 @@ class FinanceWalletBehaviorTest {
|
|
|
|
|
@Test
|
|
|
|
|
void rechargeRepaysUsedCreditWithoutReleasingFrozenCredit() {
|
|
|
|
|
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y");
|
|
|
|
|
WalletAccount initial = service.accountState(new BigDecimal("30.00"), new BigDecimal("25.00"),
|
|
|
|
|
new BigDecimal("100.00"), new BigDecimal("40.00"), new BigDecimal("20.00"), 5);
|
|
|
|
|
WalletAccount initial = service.accountState(new BigDecimal("5.00"), new BigDecimal("25.00"),
|
|
|
|
|
new BigDecimal("100.00"), new BigDecimal("40.00"), new BigDecimal("20.00"),
|
|
|
|
|
LocalDate.now().plusDays(7), 5);
|
|
|
|
|
|
|
|
|
|
WalletAccount recharged = service.rechargeAccount(initial, new BigDecimal("50.00"));
|
|
|
|
|
|
|
|
|
|
assertThat(recharged.actualBalance()).isEqualByComparingTo("40.00");
|
|
|
|
|
assertThat(recharged.actualBalance()).isEqualByComparingTo("15.00");
|
|
|
|
|
assertThat(recharged.creditUsed()).isEqualByComparingTo("0.00");
|
|
|
|
|
assertThat(recharged.creditFrozen()).isEqualByComparingTo("20.00");
|
|
|
|
|
assertThat(recharged.frozenBalance()).isEqualByComparingTo("25.00");
|
|
|
|
|
assertThat(recharged.availableCredit()).isEqualByComparingTo("80.00");
|
|
|
|
|
assertThat(recharged.availableBalance()).isEqualByComparingTo("115.00");
|
|
|
|
|
assertThat(recharged.availableBalance()).isEqualByComparingTo("90.00");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void accountStateRejectsCreditUsedAndFrozenAboveTheLimit() {
|
|
|
|
|
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y");
|
|
|
|
|
|
|
|
|
|
assertThatThrownBy(() -> service.accountState(new BigDecimal("10.00"), new BigDecimal("25.00"),
|
|
|
|
|
new BigDecimal("50.00"), new BigDecimal("31.00"), new BigDecimal("20.00"), 1))
|
|
|
|
|
assertThatThrownBy(() -> service.accountState(BigDecimal.ZERO, new BigDecimal("20.00"),
|
|
|
|
|
new BigDecimal("50.00"), new BigDecimal("31.00"), new BigDecimal("20.00"),
|
|
|
|
|
LocalDate.now().plusDays(7), 1))
|
|
|
|
|
.isInstanceOf(FinanceEventException.class)
|
|
|
|
|
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-WALLET-409");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void accountStateRejectsFrozenCashAboveActualCash() {
|
|
|
|
|
void accountStateAllowsBalanceToBecomeNegativeWhileFundsRemainFrozen() {
|
|
|
|
|
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y");
|
|
|
|
|
|
|
|
|
|
assertThatThrownBy(() -> service.accountState(new BigDecimal("9.99"), new BigDecimal("10.00"),
|
|
|
|
|
new BigDecimal("20.00"), BigDecimal.ZERO, BigDecimal.ZERO, 1))
|
|
|
|
|
.isInstanceOf(FinanceEventException.class)
|
|
|
|
|
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-WALLET-409");
|
|
|
|
|
WalletAccount account = service.accountState(new BigDecimal("9.99"), new BigDecimal("10.00"),
|
|
|
|
|
new BigDecimal("20.00"), BigDecimal.ZERO, LocalDate.now().plusDays(7), 1);
|
|
|
|
|
|
|
|
|
|
assertThat(account.availableBalance()).isEqualByComparingTo("19.99");
|
|
|
|
|
assertThat(account.creditActive()).isTrue();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
@@ -173,32 +158,27 @@ class FinanceWalletBehaviorTest {
|
|
|
|
|
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y");
|
|
|
|
|
|
|
|
|
|
assertThatThrownBy(() -> service.accountState(new BigDecimal("100.00"), new BigDecimal("10.00"),
|
|
|
|
|
new BigDecimal("50.00"), BigDecimal.ZERO, new BigDecimal("10.01"), 1))
|
|
|
|
|
new BigDecimal("50.00"), new BigDecimal("10.01"), LocalDate.now().plusDays(7), 1))
|
|
|
|
|
.isInstanceOf(FinanceEventException.class)
|
|
|
|
|
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-WALLET-409");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void invalidDefaultCreditConfigurationFailsClosed() {
|
|
|
|
|
assertThatThrownBy(() -> service(mock(JdbcTemplate.class), "x", "y", new BigDecimal("-0.01")))
|
|
|
|
|
.isInstanceOf(FinanceEventException.class)
|
|
|
|
|
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-REQ-400");
|
|
|
|
|
assertThatThrownBy(() -> service(mock(JdbcTemplate.class), "x", "y", new BigDecimal("1.001")))
|
|
|
|
|
.isInstanceOf(FinanceEventException.class)
|
|
|
|
|
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-REQ-400");
|
|
|
|
|
assertThatThrownBy(() -> service(mock(JdbcTemplate.class), "x", "y", new BigDecimal("1000000000000.00")))
|
|
|
|
|
.isInstanceOf(FinanceEventException.class)
|
|
|
|
|
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-REQ-400");
|
|
|
|
|
void expiredCreditNoLongerContributesToAvailableBalance() {
|
|
|
|
|
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y");
|
|
|
|
|
|
|
|
|
|
WalletAccount account = service.accountState(BigDecimal.ZERO, BigDecimal.ZERO,
|
|
|
|
|
new BigDecimal("50.00"), new BigDecimal("20.00"), BigDecimal.ZERO,
|
|
|
|
|
LocalDate.now().minusDays(1), 1);
|
|
|
|
|
|
|
|
|
|
assertThat(account.creditActive()).isFalse();
|
|
|
|
|
assertThat(account.availableCredit()).isZero();
|
|
|
|
|
assertThat(account.availableBalance()).isEqualByComparingTo("-20.00");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static FinanceWalletService service(JdbcTemplate jdbc, String wechat, String alipay) {
|
|
|
|
|
return service(jdbc, wechat, alipay, BigDecimal.ZERO);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static FinanceWalletService service(JdbcTemplate jdbc, String wechat, String alipay,
|
|
|
|
|
BigDecimal defaultCreditLimit) {
|
|
|
|
|
return new FinanceWalletService(jdbc, new ObjectMapper().findAndRegisterModules(),
|
|
|
|
|
mock(FinanceFileClient.class), mock(FinanceDocumentNumberGenerator.class), "token", wechat, alipay,
|
|
|
|
|
defaultCreditLimit, "test", "test");
|
|
|
|
|
"test", "test");
|
|
|
|
|
}
|
|
|
|
|
}
|