feat: complete finance and overseas receiving workflow
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.order.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@FeignClient(name = "finance-service", contextId = "orderFinanceWalletClient",
|
||||
url = "${baiyue.clients.finance.url:}", path = "/internal/finance/v1/wallet")
|
||||
public interface FinanceWalletClient {
|
||||
@GetMapping("/accounts/{customerId}")
|
||||
ApiResponse<WalletAccount> account(
|
||||
@RequestHeader("X-Internal-Service-Token") String internalToken,
|
||||
@PathVariable("customerId") long customerId);
|
||||
|
||||
record WalletAccount(BigDecimal actualBalance, BigDecimal availableBalance,
|
||||
BigDecimal creditLimit, BigDecimal availableCredit,
|
||||
LocalDate creditExpiresOn, boolean creditActive) { }
|
||||
}
|
||||
@@ -3,14 +3,18 @@ package com.baiyue.order.service;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.client.TenantEligibilityClient;
|
||||
import com.baiyue.order.client.FinanceWalletClient;
|
||||
import feign.FeignException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Service
|
||||
public class OrderEligibilityGuard {
|
||||
@Resource private TenantEligibilityClient client;
|
||||
@Resource private FinanceWalletClient financeWalletClient;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
|
||||
public void requireApproved(long tenantId) {
|
||||
@@ -32,6 +36,7 @@ public class OrderEligibilityGuard {
|
||||
throw new BizException(ResultCode.FORBIDDEN,
|
||||
response.data().reason() == null ? "Enterprise review approval is required" : response.data().reason());
|
||||
}
|
||||
requireNonNegativeActualBalance(tenantId);
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM eligibility service is unavailable");
|
||||
} catch (BizException exception) {
|
||||
@@ -41,4 +46,23 @@ public class OrderEligibilityGuard {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM 下单资格响应无效");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireNonNegativeActualBalance(long tenantId) {
|
||||
try {
|
||||
var response = financeWalletClient.account(internalToken, tenantId);
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| response.data().actualBalance() == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Unable to verify customer account balance");
|
||||
}
|
||||
if (response.data().actualBalance().compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "账户实际余额为负,请充值后再下单");
|
||||
}
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Finance account service is unavailable");
|
||||
} catch (BizException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Finance account response is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,15 @@ package com.baiyue.order.service;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.client.FinanceWalletClient;
|
||||
import com.baiyue.order.client.TenantEligibilityClient;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -48,4 +52,46 @@ class OrderEligibilityGuardTest {
|
||||
.extracting(exception -> ((BizException) exception).getResultCode())
|
||||
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void negativeActualBalanceBlocksNewOrderSubmission() {
|
||||
TenantEligibilityClient client = approvedClient();
|
||||
FinanceWalletClient finance = mock(FinanceWalletClient.class);
|
||||
when(finance.account("internal-token", 10001L)).thenReturn(ApiResponse.success(
|
||||
new FinanceWalletClient.WalletAccount(new BigDecimal("-0.01"), new BigDecimal("99.99"),
|
||||
new BigDecimal("100.00"), new BigDecimal("99.99"), LocalDate.now().plusDays(7), true)));
|
||||
OrderEligibilityGuard guard = guard(client, finance);
|
||||
|
||||
assertThatThrownBy(() -> guard.requireApproved(10001L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("账户实际余额为负,请充值后再下单")
|
||||
.extracting(exception -> ((BizException) exception).getResultCode())
|
||||
.isEqualTo(ResultCode.FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroActualBalanceAllowsOrderWhileCreditIsActive() {
|
||||
TenantEligibilityClient client = approvedClient();
|
||||
FinanceWalletClient finance = mock(FinanceWalletClient.class);
|
||||
when(finance.account("internal-token", 10001L)).thenReturn(ApiResponse.success(
|
||||
new FinanceWalletClient.WalletAccount(BigDecimal.ZERO, new BigDecimal("100.00"),
|
||||
new BigDecimal("100.00"), new BigDecimal("100.00"), LocalDate.now().plusDays(7), true)));
|
||||
|
||||
guard(client, finance).requireApproved(10001L);
|
||||
}
|
||||
|
||||
private static TenantEligibilityClient approvedClient() {
|
||||
TenantEligibilityClient client = mock(TenantEligibilityClient.class);
|
||||
when(client.check("internal-token", 10001L)).thenReturn(ApiResponse.success(
|
||||
new TenantEligibilityClient.EligibilityResponse(true, "ACTIVE", null)));
|
||||
return client;
|
||||
}
|
||||
|
||||
private static OrderEligibilityGuard guard(TenantEligibilityClient client, FinanceWalletClient finance) {
|
||||
OrderEligibilityGuard guard = new OrderEligibilityGuard();
|
||||
ReflectionTestUtils.setField(guard, "client", client);
|
||||
ReflectionTestUtils.setField(guard, "financeWalletClient", finance);
|
||||
ReflectionTestUtils.setField(guard, "internalToken", "internal-token");
|
||||
return guard;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user