This commit is contained in:
2026-09-19 17:02:04 +08:00
parent 4b69f13d76
commit 9a7b1bb2b8
9 changed files with 42 additions and 65 deletions
+1 -2
View File
@@ -5,6 +5,5 @@ spring:
password: ${FINANCE_DB_PASSWORD} password: ${FINANCE_DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
baiyue.clients.warehouse.url: ${WAREHOUSE_SERVICE_URL:} baiyue.clients.warehouse.url: ${WAREHOUSE_SERVICE_URL:}
baiyue.finance.recharge.wechat-qr-code-url: ${FINANCE_RECHARGE_WECHAT_QR_CODE_URL:} baiyue.finance.recharge.qr-code-url: ${FINANCE_RECHARGE_QR_CODE_URL:}
baiyue.finance.recharge.alipay-qr-code-url: ${FINANCE_RECHARGE_ALIPAY_QR_CODE_URL:}
baiyue.finance.wallet.default-credit-limit: ${FINANCE_WALLET_DEFAULT_CREDIT_LIMIT:0.00} baiyue.finance.wallet.default-credit-limit: ${FINANCE_WALLET_DEFAULT_CREDIT_LIMIT:0.00}
+1 -2
View File
@@ -5,6 +5,5 @@ spring:
username: ${FINANCE_DB_USERNAME:root} username: ${FINANCE_DB_USERNAME:root}
password: ${FINANCE_DB_PASSWORD:123456} password: ${FINANCE_DB_PASSWORD:123456}
baiyue.clients.warehouse.url: ${WAREHOUSE_SERVICE_URL:} baiyue.clients.warehouse.url: ${WAREHOUSE_SERVICE_URL:}
baiyue.finance.recharge.wechat-qr-code-url: ${FINANCE_RECHARGE_WECHAT_QR_CODE_URL:/customer-recharge-qr.svg} baiyue.finance.recharge.qr-code-url: ${FINANCE_RECHARGE_QR_CODE_URL:/customer-recharge-qr.svg}
baiyue.finance.recharge.alipay-qr-code-url: ${FINANCE_RECHARGE_ALIPAY_QR_CODE_URL:/customer-recharge-qr.svg}
baiyue.finance.wallet.default-credit-limit: ${FINANCE_WALLET_DEFAULT_CREDIT_LIMIT:0.00} baiyue.finance.wallet.default-credit-limit: ${FINANCE_WALLET_DEFAULT_CREDIT_LIMIT:0.00}
@@ -4908,7 +4908,6 @@ CREATE TABLE fin_recharge_request (
customer_id bigint unsigned NOT NULL COMMENT '客户ID和隔离键', customer_id bigint unsigned NOT NULL COMMENT '客户ID和隔离键',
request_no varchar(32) NOT NULL COMMENT '充值申请业务编号RC-yyMMdd-####', request_no varchar(32) NOT NULL COMMENT '充值申请业务编号RC-yyMMdd-####',
amount decimal(20,2) NOT NULL COMMENT '申请充值金额', amount decimal(20,2) NOT NULL COMMENT '申请充值金额',
payment_method varchar(16) NOT NULL COMMENT 'WECHAT或ALIPAY',
voucher_file_id bigint unsigned NOT NULL COMMENT '付款截图File ID', voucher_file_id bigint unsigned NOT NULL COMMENT '付款截图File ID',
voucher_file_name varchar(255) NOT NULL COMMENT '付款截图文件名快照', voucher_file_name varchar(255) NOT NULL COMMENT '付款截图文件名快照',
status varchar(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回', status varchar(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回',
@@ -4926,7 +4925,6 @@ CREATE TABLE fin_recharge_request (
KEY idx_fin_recharge_customer_status (customer_id,status,created_at,id), KEY idx_fin_recharge_customer_status (customer_id,status,created_at,id),
CONSTRAINT chk_fin_recharge_amount CHECK ((amount > 0)), CONSTRAINT chk_fin_recharge_amount CHECK ((amount > 0)),
CONSTRAINT chk_fin_recharge_customer CHECK ((customer_id > 0)), CONSTRAINT chk_fin_recharge_customer CHECK ((customer_id > 0)),
CONSTRAINT chk_fin_recharge_method CHECK ((payment_method in (_utf8mb4'WECHAT',_utf8mb4'ALIPAY'))),
CONSTRAINT chk_fin_recharge_status CHECK ((status in (_utf8mb4'PENDING',_utf8mb4'APPROVED',_utf8mb4'REJECTED'))) CONSTRAINT chk_fin_recharge_status CHECK ((status in (_utf8mb4'PENDING',_utf8mb4'APPROVED',_utf8mb4'REJECTED')))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='客户付款截图充值申请;平台审核通过后入账'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='客户付款截图充值申请;平台审核通过后入账';
@@ -40,19 +40,18 @@ public class ClientFinanceWalletController {
return FinanceResponses.success(service.rechargeRequests(user.tenantId(), page, size)); return FinanceResponses.success(service.rechargeRequests(user.tenantId(), page, size));
} }
@GetMapping("/recharge-channels") @GetMapping("/recharge-channel")
@PreAuthorize("hasAuthority('merchant:finance:recharge:create')") @PreAuthorize("hasAuthority('merchant:finance:recharge:create')")
public ApiResponse<?> channels() { return FinanceResponses.success(service.channels()); } public ApiResponse<?> channel() { return FinanceResponses.success(service.channel()); }
@PostMapping(value = "/recharge-requests", consumes = "multipart/form-data") @PostMapping(value = "/recharge-requests", consumes = "multipart/form-data")
@PreAuthorize("hasAuthority('merchant:finance:recharge:create')") @PreAuthorize("hasAuthority('merchant:finance:recharge:create')")
public ApiResponse<?> submit(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<?> submit(@AuthenticationPrincipal AuthenticatedUser user,
@RequestHeader("Idempotency-Key") String idempotencyKey, @RequestHeader("Idempotency-Key") String idempotencyKey,
@RequestParam BigDecimal amount, @RequestParam BigDecimal amount,
@RequestParam String paymentMethod,
@RequestPart("file") MultipartFile file) { @RequestPart("file") MultipartFile file) {
return FinanceResponses.success(service.submitRecharge(user.tenantId(), user.userId(), user.displayName(), return FinanceResponses.success(service.submitRecharge(user.tenantId(), user.userId(), user.displayName(),
idempotencyKey, amount, paymentMethod, file)); idempotencyKey, amount, file));
} }
@GetMapping("/recharge-requests/{requestId}/voucher") @GetMapping("/recharge-requests/{requestId}/voucher")
@@ -33,13 +33,12 @@ public final class FinanceWalletDtos {
String remark, LocalDateTime createdAt) { } String remark, LocalDateTime createdAt) { }
public record RechargeRequest(long requestId, String requestNo, BigDecimal amount, public record RechargeRequest(long requestId, String requestNo, BigDecimal amount,
String paymentMethod, long voucherFileId, String voucherFileName, long voucherFileId, String voucherFileName,
String status, int version, String submittedByName, String status, int version, String submittedByName,
String reviewedByName, String reviewRemark, String reviewedByName, String reviewRemark,
LocalDateTime createdAt, LocalDateTime reviewedAt) { } LocalDateTime createdAt, LocalDateTime reviewedAt) { }
public record RechargeChannel(String paymentMethod, String displayName, public record RechargeChannel(boolean enabled, String qrCodeUrl) { }
boolean enabled, String qrCodeUrl) { }
public record RechargeReviewRequest(String decision, String remark, int expectedVersion) { } public record RechargeReviewRequest(String decision, String remark, int expectedVersion) { }
@@ -42,7 +42,6 @@ import java.util.Set;
@Service @Service
public class FinanceWalletService { public class FinanceWalletService {
private static final Set<String> PAYMENT_METHODS = Set.of("WECHAT", "ALIPAY");
private static final Set<String> ADJUSTMENT_PAYMENT_METHODS = Set.of("BANK_TRANSFER", "CASH", "OTHER"); private static final Set<String> ADJUSTMENT_PAYMENT_METHODS = Set.of("BANK_TRANSFER", "CASH", "OTHER");
private static final Set<String> IMAGE_TYPES = Set.of("image/jpeg", "image/png"); private static final Set<String> IMAGE_TYPES = Set.of("image/jpeg", "image/png");
@@ -51,15 +50,13 @@ public class FinanceWalletService {
private final FinanceFileClient fileClient; private final FinanceFileClient fileClient;
private final FinanceDocumentNumberGenerator numberGenerator; private final FinanceDocumentNumberGenerator numberGenerator;
private final String internalToken; private final String internalToken;
private final String wechatQrCodeUrl; private final String rechargeQrCodeUrl;
private final String alipayQrCodeUrl;
public FinanceWalletService(JdbcTemplate jdbc, ObjectMapper objectMapper, public FinanceWalletService(JdbcTemplate jdbc, ObjectMapper objectMapper,
FinanceFileClient fileClient, FinanceFileClient fileClient,
FinanceDocumentNumberGenerator numberGenerator, FinanceDocumentNumberGenerator numberGenerator,
@Value("${baiyue.security.internal-service-token:}") String internalToken, @Value("${baiyue.security.internal-service-token:}") String internalToken,
@Value("${baiyue.finance.recharge.wechat-qr-code-url:}") String wechatQrCodeUrl, @Value("${baiyue.finance.recharge.qr-code-url:}") String rechargeQrCodeUrl,
@Value("${baiyue.finance.recharge.alipay-qr-code-url:}") String alipayQrCodeUrl,
@Value("${spring.profiles.active:}") String activeProfiles, @Value("${spring.profiles.active:}") String activeProfiles,
@Value("${DEPLOY_ENV:}") String deployEnvironment) { @Value("${DEPLOY_ENV:}") String deployEnvironment) {
this.jdbc = jdbc; this.jdbc = jdbc;
@@ -69,8 +66,7 @@ public class FinanceWalletService {
this.internalToken = internalToken; this.internalToken = internalToken;
boolean development = "development".equalsIgnoreCase(deployEnvironment.trim()) boolean development = "development".equalsIgnoreCase(deployEnvironment.trim())
|| List.of(activeProfiles.split(",")).stream().map(String::trim).anyMatch("dev"::equalsIgnoreCase); || List.of(activeProfiles.split(",")).stream().map(String::trim).anyMatch("dev"::equalsIgnoreCase);
this.wechatQrCodeUrl = rechargeQrCode(wechatQrCodeUrl, development); this.rechargeQrCodeUrl = rechargeQrCode(rechargeQrCodeUrl, development);
this.alipayQrCodeUrl = rechargeQrCode(alipayQrCodeUrl, development);
} }
private static String rechargeQrCode(String configured, boolean development) { private static String rechargeQrCode(String configured, boolean development) {
@@ -104,22 +100,20 @@ public class FinanceWalletService {
return WalletPage.of(rows, total == null ? 0 : total, page, size); return WalletPage.of(rows, total == null ? 0 : total, page, size);
} }
public List<RechargeChannel> channels() { public RechargeChannel channel() {
return List.of(new RechargeChannel("WECHAT", "微信支付", wechatQrCodeUrl != null, wechatQrCodeUrl), return new RechargeChannel(rechargeQrCodeUrl != null, rechargeQrCodeUrl);
new RechargeChannel("ALIPAY", "支付宝", alipayQrCodeUrl != null, alipayQrCodeUrl));
} }
@Transactional @Transactional
public RechargeRequest submitRecharge(long customerId, long operatorId, String operatorName, public RechargeRequest submitRecharge(long customerId, long operatorId, String operatorName,
String idempotencyKey, BigDecimal amount, String idempotencyKey, BigDecimal amount,
String paymentMethod, MultipartFile file) { MultipartFile file) {
requirePositive(customerId, "客户"); requirePositive(operatorId, "操作人"); requirePositive(customerId, "客户"); requirePositive(operatorId, "操作人");
BigDecimal normalizedAmount = money(amount); BigDecimal normalizedAmount = money(amount);
String method = paymentMethod(paymentMethod); RechargeChannel channel = channel();
RechargeChannel channel = channels().stream().filter(value -> value.paymentMethod().equals(method)).findFirst().orElseThrow(); if (!channel.enabled()) throw error("FIN-WALLET-CHANNEL-DISABLED", "收款二维码未配置");
if (!channel.enabled()) throw error("FIN-WALLET-CHANNEL-DISABLED", channel.displayName() + "收款二维码未配置");
validateImage(file); validateImage(file);
String requestHash = hash(Map.of("amount", normalizedAmount.toPlainString(), "paymentMethod", method, String requestHash = hash(Map.of("amount", normalizedAmount.toPlainString(),
"fileName", String.valueOf(file.getOriginalFilename()), "contentType", String.valueOf(file.getContentType()), "fileName", String.valueOf(file.getOriginalFilename()), "contentType", String.valueOf(file.getContentType()),
"size", file.getSize(), "fileHash", fileHash(file))); "size", file.getSize(), "fileHash", fileHash(file)));
Long replay = reserve(customerId, operatorId, "WALLET_RECHARGE_SUBMIT", idempotencyKey, requestHash); Long replay = reserve(customerId, operatorId, "WALLET_RECHARGE_SUBMIT", idempotencyKey, requestHash);
@@ -138,12 +132,12 @@ public class FinanceWalletService {
KeyHolder holder = new GeneratedKeyHolder(); KeyHolder holder = new GeneratedKeyHolder();
jdbc.update(connection -> { jdbc.update(connection -> {
PreparedStatement statement = connection.prepareStatement( PreparedStatement statement = connection.prepareStatement(
"INSERT INTO fin_recharge_request(customer_id,request_no,amount,payment_method,voucher_file_id,voucher_file_name,status,version,submitted_by,submitted_by_name) VALUES(?,?,?,?,?,?,'PENDING',0,?,?)", "INSERT INTO fin_recharge_request(customer_id,request_no,amount,voucher_file_id,voucher_file_name,status,version,submitted_by,submitted_by_name) VALUES(?,?,?,?,?,'PENDING',0,?,?)",
Statement.RETURN_GENERATED_KEYS); Statement.RETURN_GENERATED_KEYS);
statement.setLong(1, customerId); statement.setString(2, requestNo); statement.setLong(1, customerId); statement.setString(2, requestNo);
statement.setBigDecimal(3, normalizedAmount); statement.setString(4, method); statement.setBigDecimal(3, normalizedAmount);
statement.setLong(5, upload.data().fileId()); statement.setString(6, upload.data().fileName()); statement.setLong(4, upload.data().fileId()); statement.setString(5, upload.data().fileName());
statement.setLong(7, operatorId); statement.setString(8, clean(operatorName, 128)); statement.setLong(6, operatorId); statement.setString(7, clean(operatorName, 128));
return statement; return statement;
}, holder); }, holder);
long requestId = generatedId(holder); long requestId = generatedId(holder);
@@ -173,7 +167,7 @@ public class FinanceWalletService {
WalletAccount afterAccount = rechargeAccount(account, before.amount()); WalletAccount afterAccount = rechargeAccount(account, before.amount());
updateAccount(beforeCustomerId(requestId), afterAccount, account.version(), operatorId); updateAccount(beforeCustomerId(requestId), afterAccount, account.version(), operatorId);
insertLedger(beforeCustomerId(requestId), "RECHARGE", before.amount(), afterAccount, insertLedger(beforeCustomerId(requestId), "RECHARGE", before.amount(), afterAccount,
"CLIENT_RECHARGE", before.requestNo(), LocalDate.now(), before.paymentMethod(), null, "CLIENT_RECHARGE", before.requestNo(), LocalDate.now(), null, null,
operatorId, operatorName, request.remark()); operatorId, operatorName, request.remark());
} }
RechargeRequest after = requiredRecharge(requestId, null); RechargeRequest after = requiredRecharge(requestId, null);
@@ -587,7 +581,7 @@ public class FinanceWalletService {
private RechargeRequest rechargeRow(java.sql.ResultSet rs) throws java.sql.SQLException { private RechargeRequest rechargeRow(java.sql.ResultSet rs) throws java.sql.SQLException {
return new RechargeRequest(rs.getLong("id"), rs.getString("request_no"), rs.getBigDecimal("amount"), return new RechargeRequest(rs.getLong("id"), rs.getString("request_no"), rs.getBigDecimal("amount"),
rs.getString("payment_method"), rs.getLong("voucher_file_id"), rs.getString("voucher_file_name"), rs.getLong("voucher_file_id"), rs.getString("voucher_file_name"),
rs.getString("status"), rs.getInt("version"), rs.getString("submitted_by_name"), rs.getString("status"), rs.getInt("version"), rs.getString("submitted_by_name"),
rs.getString("reviewed_by_name"), rs.getString("review_remark"), timestamp(rs.getTimestamp("created_at")), rs.getString("reviewed_by_name"), rs.getString("review_remark"), timestamp(rs.getTimestamp("created_at")),
timestamp(rs.getTimestamp("reviewed_at"))); timestamp(rs.getTimestamp("reviewed_at")));
@@ -633,11 +627,6 @@ public class FinanceWalletService {
if (normalized == null) throw bad("业务单号不能为空"); if (normalized == null) throw bad("业务单号不能为空");
return normalized; return normalized;
} }
private static String paymentMethod(String value) {
String normalized = upper(value);
if (!PAYMENT_METHODS.contains(normalized)) throw bad("付款方式无效");
return normalized;
}
private static void validateImage(MultipartFile file) { private static void validateImage(MultipartFile file) {
if (file == null || file.isEmpty()) throw bad("请上传付款截图"); if (file == null || file.isEmpty()) throw bad("请上传付款截图");
if (!IMAGE_TYPES.contains(String.valueOf(file.getContentType()).toLowerCase(Locale.ROOT))) throw bad("付款截图仅支持 JPG 或 PNG"); if (!IMAGE_TYPES.contains(String.valueOf(file.getContentType()).toLowerCase(Locale.ROOT))) throw bad("付款截图仅支持 JPG 或 PNG");
@@ -8,5 +8,4 @@ spring:
baiyue: baiyue:
finance: finance:
recharge: recharge:
wechat-qr-code-url: ${FINANCE_RECHARGE_WECHAT_QR_CODE_URL:/customer-recharge-qr.svg} qr-code-url: ${FINANCE_RECHARGE_QR_CODE_URL:/customer-recharge-qr.svg}
alipay-qr-code-url: ${FINANCE_RECHARGE_ALIPAY_QR_CODE_URL:/customer-recharge-qr.svg}
@@ -467,7 +467,6 @@ CREATE TABLE IF NOT EXISTS fin_recharge_request (
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和隔离键', customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和隔离键',
request_no VARCHAR(32) NOT NULL COMMENT '充值申请业务编号RC-yyMMdd-####', request_no VARCHAR(32) NOT NULL COMMENT '充值申请业务编号RC-yyMMdd-####',
amount DECIMAL(20,2) NOT NULL COMMENT '申请充值金额', amount DECIMAL(20,2) NOT NULL COMMENT '申请充值金额',
payment_method VARCHAR(16) NOT NULL COMMENT 'WECHAT或ALIPAY',
voucher_file_id BIGINT UNSIGNED NOT NULL COMMENT '付款截图File ID', voucher_file_id BIGINT UNSIGNED NOT NULL COMMENT '付款截图File ID',
voucher_file_name VARCHAR(255) NOT NULL COMMENT '付款截图文件名快照', voucher_file_name VARCHAR(255) NOT NULL COMMENT '付款截图文件名快照',
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回', status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回',
@@ -485,7 +484,6 @@ CREATE TABLE IF NOT EXISTS fin_recharge_request (
KEY idx_fin_recharge_customer_status (customer_id, status, created_at, id), KEY idx_fin_recharge_customer_status (customer_id, status, created_at, id),
CONSTRAINT chk_fin_recharge_customer CHECK (customer_id > 0), CONSTRAINT chk_fin_recharge_customer CHECK (customer_id > 0),
CONSTRAINT chk_fin_recharge_amount CHECK (amount > 0), CONSTRAINT chk_fin_recharge_amount CHECK (amount > 0),
CONSTRAINT chk_fin_recharge_method CHECK (payment_method IN ('WECHAT','ALIPAY')),
CONSTRAINT chk_fin_recharge_status CHECK (status IN ('PENDING','APPROVED','REJECTED')) CONSTRAINT chk_fin_recharge_status CHECK (status IN ('PENDING','APPROVED','REJECTED'))
) ENGINE=InnoDB COMMENT='客户付款截图充值申请;平台审核通过后入账'; ) ENGINE=InnoDB COMMENT='客户付款截图充值申请;平台审核通过后入账';
@@ -22,7 +22,7 @@ class FinanceWalletBehaviorTest {
JdbcTemplate jdbc = mock(JdbcTemplate.class); JdbcTemplate jdbc = mock(JdbcTemplate.class);
when(jdbc.query(startsWith("SELECT currency_code"), any(RowMapper.class), eq(9L))).thenReturn(List.of()); when(jdbc.query(startsWith("SELECT currency_code"), any(RowMapper.class), eq(9L))).thenReturn(List.of());
var account = service(jdbc, "https://pay.example/wechat.png", "").account(9L); var account = service(jdbc, "https://pay.example/qr.png").account(9L);
assertThat(account.actualBalance()).isEqualByComparingTo("0.00"); assertThat(account.actualBalance()).isEqualByComparingTo("0.00");
assertThat(account.frozenBalance()).isEqualByComparingTo("0.00"); assertThat(account.frozenBalance()).isEqualByComparingTo("0.00");
@@ -34,28 +34,25 @@ class FinanceWalletBehaviorTest {
@Test @Test
void rechargeChannelsAreEnabledOnlyWhenQrCodeIsConfigured() { void rechargeChannelsAreEnabledOnlyWhenQrCodeIsConfigured() {
var channels = service(mock(JdbcTemplate.class), "https://pay.example/wechat.png", "").channels(); var channel = service(mock(JdbcTemplate.class), "https://pay.example/qr.png").channel();
assertThat(channels).satisfiesExactly( assertThat(channel.enabled()).isTrue();
wechat -> { assertThat(wechat.paymentMethod()).isEqualTo("WECHAT"); assertThat(wechat.enabled()).isTrue(); }, assertThat(channel.qrCodeUrl()).isEqualTo("https://pay.example/qr.png");
alipay -> { assertThat(alipay.paymentMethod()).isEqualTo("ALIPAY"); assertThat(alipay.enabled()).isFalse(); });
} }
@Test @Test
void developmentProfileUsesTheNonPayableSampleQrCode() { void developmentProfileUsesTheNonPayableSampleQrCode() {
var channels = new FinanceWalletService(mock(JdbcTemplate.class), new ObjectMapper().findAndRegisterModules(), var channels = new FinanceWalletService(mock(JdbcTemplate.class), new ObjectMapper().findAndRegisterModules(),
mock(FinanceFileClient.class), mock(FinanceDocumentNumberGenerator.class), "token", "", "", mock(FinanceFileClient.class), mock(FinanceDocumentNumberGenerator.class), "token", "",
"cn", "development").channels(); "cn", "development").channel();
assertThat(channels).allSatisfy(channel -> { assertThat(channels.enabled()).isTrue();
assertThat(channel.enabled()).isTrue(); assertThat(channels.qrCodeUrl()).isEqualTo("/customer-recharge-qr.svg");
assertThat(channel.qrCodeUrl()).isEqualTo("/customer-recharge-qr.svg");
});
} }
@Test @Test
void deductionMayMakeActualBalanceNegative() { void deductionMayMakeActualBalanceNegative() {
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); FinanceWalletService service = service(mock(JdbcTemplate.class), "x");
WalletAccount initial = service.accountState(new BigDecimal("10.00"), BigDecimal.ZERO, WalletAccount initial = service.accountState(new BigDecimal("10.00"), BigDecimal.ZERO,
BigDecimal.ZERO, BigDecimal.ZERO, null, 2); BigDecimal.ZERO, BigDecimal.ZERO, null, 2);
@@ -71,7 +68,7 @@ class FinanceWalletBehaviorTest {
JdbcTemplate jdbc = mock(JdbcTemplate.class); JdbcTemplate jdbc = mock(JdbcTemplate.class);
when(jdbc.query(startsWith("SELECT currency_code"), any(RowMapper.class), eq(9L))).thenReturn(List.of()); when(jdbc.query(startsWith("SELECT currency_code"), any(RowMapper.class), eq(9L))).thenReturn(List.of());
var account = service(jdbc, "x", "y").account(9L); var account = service(jdbc, "x").account(9L);
assertThat(account.creditLimit()).isZero(); assertThat(account.creditLimit()).isZero();
assertThat(account.availableCredit()).isZero(); assertThat(account.availableCredit()).isZero();
@@ -82,7 +79,7 @@ class FinanceWalletBehaviorTest {
@Test @Test
void deductionUsesCashBeforeCreditAndRechargeRepaysCreditFirst() { void deductionUsesCashBeforeCreditAndRechargeRepaysCreditFirst() {
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); FinanceWalletService service = service(mock(JdbcTemplate.class), "x");
WalletAccount initial = service.accountState(new BigDecimal("100.00"), BigDecimal.ZERO, WalletAccount initial = service.accountState(new BigDecimal("100.00"), BigDecimal.ZERO,
new BigDecimal("50.00"), BigDecimal.ZERO, LocalDate.now().plusDays(7), 3); new BigDecimal("50.00"), BigDecimal.ZERO, LocalDate.now().plusDays(7), 3);
@@ -100,7 +97,7 @@ class FinanceWalletBehaviorTest {
@Test @Test
void deductionNeverConsumesCashReservedByAnActiveHold() { void deductionNeverConsumesCashReservedByAnActiveHold() {
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); FinanceWalletService service = service(mock(JdbcTemplate.class), "x");
WalletAccount initial = service.accountState(new BigDecimal("100.00"), new BigDecimal("50.00"), WalletAccount initial = service.accountState(new BigDecimal("100.00"), new BigDecimal("50.00"),
new BigDecimal("80.00"), new BigDecimal("20.00"), LocalDate.now().plusDays(7), 4); new BigDecimal("80.00"), new BigDecimal("20.00"), LocalDate.now().plusDays(7), 4);
@@ -116,7 +113,7 @@ class FinanceWalletBehaviorTest {
@Test @Test
void rechargeRepaysUsedCreditWithoutReleasingFrozenCredit() { void rechargeRepaysUsedCreditWithoutReleasingFrozenCredit() {
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); FinanceWalletService service = service(mock(JdbcTemplate.class), "x");
WalletAccount initial = service.accountState(new BigDecimal("5.00"), new BigDecimal("25.00"), WalletAccount initial = service.accountState(new BigDecimal("5.00"), new BigDecimal("25.00"),
new BigDecimal("100.00"), new BigDecimal("40.00"), new BigDecimal("20.00"), new BigDecimal("100.00"), new BigDecimal("40.00"), new BigDecimal("20.00"),
LocalDate.now().plusDays(7), 5); LocalDate.now().plusDays(7), 5);
@@ -133,7 +130,7 @@ class FinanceWalletBehaviorTest {
@Test @Test
void accountStateRejectsCreditUsedAndFrozenAboveTheLimit() { void accountStateRejectsCreditUsedAndFrozenAboveTheLimit() {
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); FinanceWalletService service = service(mock(JdbcTemplate.class), "x");
assertThatThrownBy(() -> service.accountState(BigDecimal.ZERO, new BigDecimal("20.00"), assertThatThrownBy(() -> service.accountState(BigDecimal.ZERO, new BigDecimal("20.00"),
new BigDecimal("50.00"), new BigDecimal("31.00"), new BigDecimal("20.00"), new BigDecimal("50.00"), new BigDecimal("31.00"), new BigDecimal("20.00"),
@@ -144,7 +141,7 @@ class FinanceWalletBehaviorTest {
@Test @Test
void accountStateAllowsBalanceToBecomeNegativeWhileFundsRemainFrozen() { void accountStateAllowsBalanceToBecomeNegativeWhileFundsRemainFrozen() {
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); FinanceWalletService service = service(mock(JdbcTemplate.class), "x");
WalletAccount account = service.accountState(new BigDecimal("9.99"), new BigDecimal("10.00"), WalletAccount account = service.accountState(new BigDecimal("9.99"), new BigDecimal("10.00"),
new BigDecimal("20.00"), BigDecimal.ZERO, LocalDate.now().plusDays(7), 1); new BigDecimal("20.00"), BigDecimal.ZERO, LocalDate.now().plusDays(7), 1);
@@ -155,7 +152,7 @@ class FinanceWalletBehaviorTest {
@Test @Test
void accountStateRejectsCreditFrozenAboveTotalFrozenAmount() { void accountStateRejectsCreditFrozenAboveTotalFrozenAmount() {
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); FinanceWalletService service = service(mock(JdbcTemplate.class), "x");
assertThatThrownBy(() -> service.accountState(new BigDecimal("100.00"), new BigDecimal("10.00"), assertThatThrownBy(() -> service.accountState(new BigDecimal("100.00"), new BigDecimal("10.00"),
new BigDecimal("50.00"), new BigDecimal("10.01"), LocalDate.now().plusDays(7), 1)) new BigDecimal("50.00"), new BigDecimal("10.01"), LocalDate.now().plusDays(7), 1))
@@ -165,7 +162,7 @@ class FinanceWalletBehaviorTest {
@Test @Test
void expiredCreditNoLongerContributesToAvailableBalance() { void expiredCreditNoLongerContributesToAvailableBalance() {
FinanceWalletService service = service(mock(JdbcTemplate.class), "x", "y"); FinanceWalletService service = service(mock(JdbcTemplate.class), "x");
WalletAccount account = service.accountState(BigDecimal.ZERO, BigDecimal.ZERO, WalletAccount account = service.accountState(BigDecimal.ZERO, BigDecimal.ZERO,
new BigDecimal("50.00"), new BigDecimal("20.00"), BigDecimal.ZERO, new BigDecimal("50.00"), new BigDecimal("20.00"), BigDecimal.ZERO,
@@ -176,9 +173,9 @@ class FinanceWalletBehaviorTest {
assertThat(account.availableBalance()).isEqualByComparingTo("-20.00"); assertThat(account.availableBalance()).isEqualByComparingTo("-20.00");
} }
private static FinanceWalletService service(JdbcTemplate jdbc, String wechat, String alipay) { private static FinanceWalletService service(JdbcTemplate jdbc, String qrCodeUrl) {
return new FinanceWalletService(jdbc, new ObjectMapper().findAndRegisterModules(), return new FinanceWalletService(jdbc, new ObjectMapper().findAndRegisterModules(),
mock(FinanceFileClient.class), mock(FinanceDocumentNumberGenerator.class), "token", wechat, alipay, mock(FinanceFileClient.class), mock(FinanceDocumentNumberGenerator.class), "token", qrCodeUrl,
"test", "test"); "test", "test");
} }
} }