Files
peipei-backend/play-admin/src/test/java/com/starry/admin/api/AdminWithdrawalControllerApiTest.java
2025-11-07 23:41:39 -05:00

293 lines
14 KiB
Java

package com.starry.admin.api;
import static org.hamcrest.Matchers.nullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
import com.starry.admin.modules.withdraw.enums.EarningsType;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MvcResult;
class AdminWithdrawalControllerApiTest extends AbstractApiTest {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
private IEarningsService earningsService;
@Autowired
private IWithdrawalService withdrawalService;
@Autowired
private IPlayOrderInfoService orderInfoService;
private final ObjectMapper objectMapper = new ObjectMapper();
private final List<String> earningsToCleanup = new ArrayList<>();
private final List<String> withdrawalsToCleanup = new ArrayList<>();
private final List<String> ordersToCleanup = new ArrayList<>();
@AfterEach
void tearDown() {
if (!earningsToCleanup.isEmpty()) {
earningsService.removeByIds(earningsToCleanup);
earningsToCleanup.clear();
}
if (!withdrawalsToCleanup.isEmpty()) {
withdrawalService.removeByIds(withdrawalsToCleanup);
withdrawalsToCleanup.clear();
}
if (!ordersToCleanup.isEmpty()) {
orderInfoService.removeByIds(ordersToCleanup);
ordersToCleanup.clear();
}
}
@Test
void auditReturnsEarningLinesWithOrderDetails() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity order = seedOrder(LocalDateTime.now().minusHours(2));
WithdrawalRequestEntity withdrawal = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("88.60"));
LocalDateTime now = LocalDateTime.now();
seedEarningLine(withdrawal.getId(), order.getId(), new BigDecimal("50.30"), "withdrawn", now.minusMinutes(30), EarningsType.ORDER);
seedEarningLine(withdrawal.getId(), null, new BigDecimal("38.30"), "withdrawing", now.minusMinutes(10), EarningsType.ORDER);
MvcResult result = mockMvc.perform(get("/admin/withdraw/requests/" + withdrawal.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data.length()").value(2))
.andReturn();
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).get("data");
boolean foundOrder = false;
boolean foundMissing = false;
for (JsonNode node : data) {
String orderNo = node.path("orderNo").isMissingNode() ? null : node.path("orderNo").asText(null);
if (order.getOrderNo().equals(orderNo)) {
Assertions.assertThat(node.path("orderStatus").asText()).isEqualTo(order.getOrderStatus());
Assertions.assertThat(node.path("earningType").asText()).isEqualTo(EarningsType.ORDER.name());
foundOrder = true;
}
if (node.path("orderNo").isNull()) {
foundMissing = true;
}
}
Assertions.assertThat(foundOrder).isTrue();
Assertions.assertThat(foundMissing).isTrue();
}
@Test
void auditRejectsMissingRequest() throws Exception {
ensureTenantContext();
mockMvc.perform(get("/admin/withdraw/requests/missing-request/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(500))
.andExpect(jsonPath("$.message").value("提现申请不存在或无权查看"));
}
@Test
void auditRejectsCrossTenantAccess() throws Exception {
ensureTenantContext();
WithdrawalRequestEntity outsider = seedWithdrawal("tenant-other", new BigDecimal("30.00"));
mockMvc.perform(get("/admin/withdraw/requests/" + outsider.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(500))
.andExpect(jsonPath("$.message").value("提现申请不存在或无权查看"));
}
@Test
void auditReturnsEmptyListWhenNoEarningsFound() throws Exception {
ensureTenantContext();
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("22.00"));
mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.length()").value(0));
}
@Test
void auditHandlesMissingOrderRecordsGracefully() throws Exception {
ensureTenantContext();
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("40.00"));
String orphanOrderId = "order-orphan-" + IdUtils.getUuid();
seedEarningLine(request.getId(), orphanOrderId, new BigDecimal("15.00"), "withdrawn", LocalDateTime.now().minusMinutes(5), EarningsType.ORDER);
mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data[0].orderNo").value(nullValue()))
.andExpect(jsonPath("$.data[0].orderId").value(orphanOrderId));
}
@Test
void auditReturnsLinesSortedByCreatedTime() throws Exception {
ensureTenantContext();
PlayOrderInfoEntity order = seedOrder(LocalDateTime.now().minusHours(1));
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("90.00"));
LocalDateTime now = LocalDateTime.now();
String firstId = seedEarningLine(request.getId(), order.getId(), new BigDecimal("10"), "withdrawn", now.minusMinutes(20), EarningsType.ORDER);
Thread.sleep(5L);
String secondId = seedEarningLine(request.getId(), null, new BigDecimal("20"), "withdrawn", now.minusMinutes(10), EarningsType.ORDER);
Thread.sleep(5L);
String thirdId = seedEarningLine(request.getId(), null, new BigDecimal("30"), "withdrawn", now.minusMinutes(5), EarningsType.ORDER);
MvcResult result = mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.length()").value(3))
.andReturn();
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).get("data");
List<String> ids = new ArrayList<>();
LocalDateTime previous = null;
for (JsonNode node : data) {
ids.add(node.get("id").asText());
String createdText = node.path("createdTime").asText();
if (createdText != null && !createdText.isEmpty()) {
LocalDateTime created = LocalDateTime.parse(createdText, DATE_TIME_FORMATTER);
if (previous != null) {
Assertions.assertThat(created.isBefore(previous)).isFalse();
}
previous = created;
}
}
Assertions.assertThat(ids).containsExactlyInAnyOrder(firstId, secondId, thirdId);
}
@Test
void auditSupportsCommissionEarnings() throws Exception {
ensureTenantContext();
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("55.00"));
seedEarningLine(request.getId(), null, new BigDecimal("55.00"), "withdrawn", LocalDateTime.now().minusMinutes(2), EarningsType.COMMISSION);
mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data[0].earningType").value(EarningsType.COMMISSION.name()));
}
@SuppressWarnings("deprecation")
private PlayOrderInfoEntity seedOrder(LocalDateTime endTime) {
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
String id = "order-audit-" + IdUtils.getUuid();
order.setId(id);
order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
order.setOrderNo("ORD-" + System.currentTimeMillis());
order.setOrderStatus("3");
order.setOrderType("2");
order.setPlaceType("0");
order.setRewardType("0");
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
order.setOrderMoney(new BigDecimal("120.50"));
order.setFinalAmount(order.getOrderMoney());
order.setEstimatedRevenue(new BigDecimal("80.25"));
order.setOrderSettlementState("1");
order.setOrderEndTime(endTime);
order.setOrderSettlementTime(endTime);
Date nowDate = toDate(LocalDateTime.now());
order.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
order.setCreatedTime(nowDate);
order.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
order.setUpdatedTime(nowDate);
order.setDeleted(false);
orderInfoService.save(order);
ordersToCleanup.add(id);
return order;
}
private WithdrawalRequestEntity seedWithdrawal(String tenantId, BigDecimal amount) {
WithdrawalRequestEntity entity = new WithdrawalRequestEntity();
String id = "withdraw-audit-" + IdUtils.getUuid();
entity.setId(id);
entity.setTenantId(tenantId);
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
entity.setAmount(amount);
entity.setFee(BigDecimal.ZERO);
entity.setNetAmount(amount);
entity.setDestAccount("alipay:audit@test.com");
entity.setStatus("processing");
entity.setPayeeSnapshot("{\"displayName\":\"审计专用\"}");
Date nowDate = toDate(LocalDateTime.now());
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
entity.setCreatedTime(nowDate);
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
entity.setUpdatedTime(nowDate);
withdrawalService.save(entity);
withdrawalsToCleanup.add(id);
return entity;
}
private String seedEarningLine(String withdrawalId, String orderId, BigDecimal amount, String status, LocalDateTime createdAt, EarningsType earningType) {
EarningsLineEntity entity = new EarningsLineEntity();
String id = "earn-audit-" + IdUtils.getUuid();
entity.setId(id);
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
String resolvedOrderId = orderId != null ? orderId : "order-missing-" + IdUtils.getUuid();
entity.setOrderId(resolvedOrderId);
entity.setAmount(amount);
entity.setStatus(status);
entity.setEarningType(earningType);
entity.setUnlockTime(createdAt.minusHours(1));
entity.setWithdrawalId(withdrawalId);
Date createdDate = toDate(createdAt);
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
entity.setCreatedTime(createdDate);
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
entity.setUpdatedTime(createdDate);
entity.setDeleted(false);
earningsService.save(entity);
earningsToCleanup.add(id);
return id;
}
private void ensureTenantContext() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
}
private Date toDate(LocalDateTime value) {
return Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
}
}