test: 添加微信端优惠券、订单评价、订单管理和提现的API测试
Some checks failed
Build and Push Backend / docker (push) Failing after 5s
Some checks failed
Build and Push Backend / docker (push) Failing after 5s
新增四个API集成测试类: - WxCouponControllerApiTest: 测试优惠券领取、查询、使用限制和白名单逻辑 - WxCustomOrderEvaluationApiTest: 测试订单评价创建和查询功能 - WxOrderInfoControllerApiTest: 测试随机订单接单、续单申请和隐私字段处理 - WxWithdrawControllerApiTest: 测试收益余额查询、提现申请和收益明细过滤 提高微信端核心业务流程的测试覆盖率
This commit is contained in:
@@ -0,0 +1,347 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.anyOf;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
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.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponDiscountType;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
||||||
|
import com.starry.admin.modules.weichat.service.WxTokenService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class WxCouponControllerApiTest extends AbstractApiTest {
|
||||||
|
|
||||||
|
private static final String COUPON_ID_PREFIX = "apitest-cpn-";
|
||||||
|
private static final BigDecimal DEFAULT_DISCOUNT_AMOUNT = new BigDecimal("5");
|
||||||
|
private static final BigDecimal MINIMUM_USAGE_AMOUNT = new BigDecimal("60");
|
||||||
|
private static final int DEFAULT_MAX_PER_USER = 3;
|
||||||
|
private static final int DEFAULT_STOCK = 100;
|
||||||
|
private static final String DISCOUNT_BEARER_SHOP = "0";
|
||||||
|
private static final String GLOBAL_CLERK_SCOPE = "0";
|
||||||
|
private static final String CHECK_CONDITION_DISABLED = "0";
|
||||||
|
private static final String NEW_USER_FLAG_DISABLED = "0";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayCouponInfoService couponInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayCouponDetailsService couponDetailsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WxTokenService wxTokenService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayCustomUserInfoService customUserInfoService;
|
||||||
|
|
||||||
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
private final List<String> couponIds = new ArrayList<>();
|
||||||
|
private final List<String> couponDetailIds = new ArrayList<>();
|
||||||
|
private String customerToken;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ensureTenantContext();
|
||||||
|
couponInfoService.lambdaUpdate()
|
||||||
|
.eq(PlayCouponInfoEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
|
||||||
|
.like(PlayCouponInfoEntity::getId, COUPON_ID_PREFIX)
|
||||||
|
.remove();
|
||||||
|
couponDetailsService.lambdaUpdate()
|
||||||
|
.eq(PlayCouponDetailsEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
|
||||||
|
.like(PlayCouponDetailsEntity::getCouponId, COUPON_ID_PREFIX)
|
||||||
|
.remove();
|
||||||
|
customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ensureTenantContext();
|
||||||
|
if (!couponDetailIds.isEmpty()) {
|
||||||
|
couponDetailsService.removeByIds(couponDetailIds);
|
||||||
|
couponDetailIds.clear();
|
||||||
|
}
|
||||||
|
if (!couponIds.isEmpty()) {
|
||||||
|
couponInfoService.removeByIds(couponIds);
|
||||||
|
couponIds.clear();
|
||||||
|
}
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void obtainCouponRejectsNonWhitelistCustomer() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String couponId = newCouponId("whitelist");
|
||||||
|
PlayCouponInfoEntity coupon = createBaseCoupon(couponId);
|
||||||
|
coupon.setClaimConditionType(CouponClaimConditionType.WHITELIST.code());
|
||||||
|
coupon.setCustomWhitelist(List.of("other-customer"));
|
||||||
|
couponInfoService.save(coupon);
|
||||||
|
couponIds.add(coupon.getId());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
|
||||||
|
.param("id", coupon.getId())
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.success").value(false))
|
||||||
|
.andExpect(jsonPath("$.data.msg").value("非指定用户"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryAllSkipsOfflineCouponsAndMarksObtainedOnes() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCouponInfoEntity onlineCoupon = createBaseCoupon(newCouponId("online"));
|
||||||
|
onlineCoupon.setCustomWhitelist(List.of(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID));
|
||||||
|
couponInfoService.save(onlineCoupon);
|
||||||
|
couponIds.add(onlineCoupon.getId());
|
||||||
|
|
||||||
|
PlayCouponDetailsEntity obtained = createCouponDetail(onlineCoupon.getId(), CouponUseState.UNUSED);
|
||||||
|
couponDetailsService.save(obtained);
|
||||||
|
couponDetailIds.add(obtained.getId());
|
||||||
|
|
||||||
|
PlayCouponInfoEntity offlineCoupon = createBaseCoupon(newCouponId("offline"));
|
||||||
|
offlineCoupon.setCouponOnLineState(CouponOnlineState.OFFLINE.getCode());
|
||||||
|
couponInfoService.save(offlineCoupon);
|
||||||
|
couponIds.add(offlineCoupon.getId());
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(post("/wx/coupon/custom/queryAll")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data");
|
||||||
|
assertThat(data).isNotNull();
|
||||||
|
assertThat(data.isArray()).isTrue();
|
||||||
|
assertThat(data).hasSizeGreaterThanOrEqualTo(1);
|
||||||
|
boolean containsOnline = false;
|
||||||
|
for (JsonNode node : data) {
|
||||||
|
assertThat(node.path("id").asText()).isNotEqualTo(offlineCoupon.getId());
|
||||||
|
if (onlineCoupon.getId().equals(node.path("id").asText())) {
|
||||||
|
containsOnline = true;
|
||||||
|
assertThat(node.path("obtained").asText()).isEqualTo("1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(containsOnline).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByOrderFlagsOnlyEligibleCouponsAsAvailable() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCouponInfoEntity eligible = createBaseCoupon(newCouponId("eligible"));
|
||||||
|
eligible.setUseMinAmount(MINIMUM_USAGE_AMOUNT);
|
||||||
|
couponInfoService.save(eligible);
|
||||||
|
couponIds.add(eligible.getId());
|
||||||
|
|
||||||
|
PlayCouponInfoEntity ineligible = createBaseCoupon(newCouponId("ineligible"));
|
||||||
|
ineligible.setPlaceType(List.of(OrderConstant.PlaceType.SPECIFIED.getCode()));
|
||||||
|
couponInfoService.save(ineligible);
|
||||||
|
couponIds.add(ineligible.getId());
|
||||||
|
|
||||||
|
PlayCouponDetailsEntity eligibleDetail = createCouponDetail(eligible.getId(), CouponUseState.UNUSED);
|
||||||
|
PlayCouponDetailsEntity ineligibleDetail = createCouponDetail(ineligible.getId(), CouponUseState.UNUSED);
|
||||||
|
couponDetailsService.save(eligibleDetail);
|
||||||
|
couponDetailsService.save(ineligibleDetail);
|
||||||
|
couponDetailIds.add(eligibleDetail.getId());
|
||||||
|
couponDetailIds.add(ineligibleDetail.getId());
|
||||||
|
|
||||||
|
ObjectNode payload = mapper.createObjectNode();
|
||||||
|
payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
payload.put("clerkId", "");
|
||||||
|
payload.put("placeType", OrderConstant.PlaceType.RANDOM.getCode());
|
||||||
|
payload.put("commodityQuantity", 1);
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(post("/wx/coupon/custom/queryByOrder")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
ArrayNode data = (ArrayNode) mapper.readTree(result.getResponse().getContentAsString()).path("data");
|
||||||
|
assertThat(data).isNotNull();
|
||||||
|
assertThat(data.size()).isGreaterThanOrEqualTo(2);
|
||||||
|
for (JsonNode node : data) {
|
||||||
|
if (eligible.getId().equals(node.path("couponId").asText())) {
|
||||||
|
assertThat(node.path("available").asText()).isEqualTo("1");
|
||||||
|
assertThat(node.path("reasonForUnavailableUse").asText("")).isEmpty();
|
||||||
|
}
|
||||||
|
if (ineligible.getId().equals(node.path("couponId").asText())) {
|
||||||
|
assertThat(node.path("available").asText()).isEqualTo("0");
|
||||||
|
assertThat(node.path("reasonForUnavailableUse").asText()).isEqualTo("订单类型不符合");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void obtainCouponSucceedsWhenEligible() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCouponInfoEntity coupon = createBaseCoupon(newCouponId("success"));
|
||||||
|
couponInfoService.save(coupon);
|
||||||
|
couponIds.add(coupon.getId());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
|
||||||
|
.param("id", coupon.getId())
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.msg").value(anyOf(is(""), nullValue())));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCouponDetailsEntity detail = couponDetailsService.lambdaQuery()
|
||||||
|
.eq(PlayCouponDetailsEntity::getCouponId, coupon.getId())
|
||||||
|
.eq(PlayCouponDetailsEntity::getCustomId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.orderByDesc(PlayCouponDetailsEntity::getObtainingTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
assertThat(detail).isNotNull();
|
||||||
|
couponDetailIds.add(detail.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void obtainCouponHonorsPerUserLimit() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCouponInfoEntity coupon = createBaseCoupon(newCouponId("limit"));
|
||||||
|
coupon.setClerkObtainedMaxQuantity(1);
|
||||||
|
couponInfoService.save(coupon);
|
||||||
|
couponIds.add(coupon.getId());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
|
||||||
|
.param("id", coupon.getId())
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.success").value(true));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCouponDetailsEntity detail = couponDetailsService.lambdaQuery()
|
||||||
|
.eq(PlayCouponDetailsEntity::getCouponId, coupon.getId())
|
||||||
|
.eq(PlayCouponDetailsEntity::getCustomId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.one();
|
||||||
|
assertThat(detail).isNotNull();
|
||||||
|
couponDetailIds.add(detail.getId());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
|
||||||
|
.param("id", coupon.getId())
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.success").value(false))
|
||||||
|
.andExpect(jsonPath("$.data.msg").value("优惠券已达到领取上限"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayCouponInfoEntity createBaseCoupon(String id) {
|
||||||
|
PlayCouponInfoEntity coupon = new PlayCouponInfoEntity();
|
||||||
|
coupon.setId(id);
|
||||||
|
coupon.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
coupon.setCouponName("测试券-" + id);
|
||||||
|
coupon.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
|
||||||
|
coupon.setUseMinAmount(BigDecimal.ZERO);
|
||||||
|
coupon.setDiscountType(CouponDiscountType.FULL_REDUCTION.getCode());
|
||||||
|
coupon.setDiscountContent("测试优惠");
|
||||||
|
coupon.setDiscountAmount(DEFAULT_DISCOUNT_AMOUNT);
|
||||||
|
coupon.setAttributionDiscounts(DISCOUNT_BEARER_SHOP);
|
||||||
|
coupon.setPlaceType(List.of(
|
||||||
|
OrderConstant.PlaceType.RANDOM.getCode(),
|
||||||
|
OrderConstant.PlaceType.SPECIFIED.getCode()));
|
||||||
|
coupon.setClerkType(GLOBAL_CLERK_SCOPE);
|
||||||
|
coupon.setCouponQuantity(DEFAULT_STOCK);
|
||||||
|
coupon.setIssuedQuantity(0);
|
||||||
|
coupon.setRemainingQuantity(DEFAULT_STOCK);
|
||||||
|
coupon.setClerkObtainedMaxQuantity(DEFAULT_MAX_PER_USER);
|
||||||
|
coupon.setClaimConditionType(CouponClaimConditionType.ALL.code());
|
||||||
|
coupon.setCustomWhitelist(new ArrayList<>());
|
||||||
|
coupon.setCustomLevelCheckType(CHECK_CONDITION_DISABLED);
|
||||||
|
coupon.setCustomSexCheckType(CHECK_CONDITION_DISABLED);
|
||||||
|
coupon.setNewUser(NEW_USER_FLAG_DISABLED);
|
||||||
|
coupon.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
coupon.setProductiveTime(now.minusDays(1));
|
||||||
|
coupon.setExpirationTime(now.plusDays(30));
|
||||||
|
coupon.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
coupon.setCreatedTime(toDate(now));
|
||||||
|
coupon.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
coupon.setUpdatedTime(toDate(now));
|
||||||
|
return coupon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayCouponDetailsEntity createCouponDetail(String couponId, CouponUseState status) {
|
||||||
|
PlayCouponDetailsEntity entity = new PlayCouponDetailsEntity();
|
||||||
|
entity.setId("detail-" + IdUtils.getUuid());
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setCustomId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
entity.setCouponId(couponId);
|
||||||
|
entity.setCustomNickname("APITester");
|
||||||
|
entity.setCustomLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
entity.setObtainingChannels(CouponObtainChannel.SELF_SERVICE.getCode());
|
||||||
|
entity.setUseState(status.getCode());
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
entity.setObtainingTime(now);
|
||||||
|
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setCreatedTime(toDate(now));
|
||||||
|
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setUpdatedTime(toDate(now));
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String newCouponId(String suffix) {
|
||||||
|
return COUPON_ID_PREFIX + suffix + "-" + IdUtils.getUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private java.util.Date toDate(LocalDateTime value) {
|
||||||
|
return java.util.Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
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.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class WxCustomOrderEvaluationApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
private static final String EVALUATION_MESSAGE = "评价成功";
|
||||||
|
private String customerToken;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayOrderEvaluateInfoService orderEvaluateInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayOrderInfoService orderInfoService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ensureTenantContext();
|
||||||
|
resetCustomerBalance();
|
||||||
|
customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void customerCanEvaluateCompletedOrderAndRetrieveResult() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String remark = "evaluate-" + LocalDateTime.now();
|
||||||
|
String orderId = createRandomOrder(remark);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
orderInfoService.lambdaUpdate()
|
||||||
|
.eq(PlayOrderInfoEntity::getId, orderId)
|
||||||
|
.set(PlayOrderInfoEntity::getAcceptBy, ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.COMPLETED.getCode())
|
||||||
|
.update();
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", orderId);
|
||||||
|
payload.put("anonymous", "1");
|
||||||
|
payload.put("evaluateLevel", 5);
|
||||||
|
String evaluationSuffix = IdUtils.getUuid();
|
||||||
|
String evaluationText = "API评价-" + evaluationSuffix;
|
||||||
|
payload.put("evaluateCon", evaluationText);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/evaluate/add")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value(EVALUATION_MESSAGE));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
assertThat(orderEvaluateInfoService.queryCustomToOrderEvaluateInfo(
|
||||||
|
ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, orderId))
|
||||||
|
.isNotNull();
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/wx/custom/order/evaluate/queryByOrderId")
|
||||||
|
.param("id", orderId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).path("data");
|
||||||
|
assertThat(data.path("id").asText()).isNotBlank();
|
||||||
|
assertThat(data.path("evaluateLevel").asInt()).isEqualTo(5);
|
||||||
|
assertThat(data.path("anonymous").asText()).isEqualTo("1");
|
||||||
|
assertThat(data.path("evaluateCon").asText()).endsWith(evaluationSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createRandomOrder(String remark) throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("sex", OrderConstant.Gender.FEMALE.getCode());
|
||||||
|
payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
payload.put("commodityQuantity", 1);
|
||||||
|
payload.put("weiChatCode", "apitest-customer-wx");
|
||||||
|
payload.put("excludeHistory", "0");
|
||||||
|
payload.set("couponIds", objectMapper.createArrayNode());
|
||||||
|
payload.put("remark", remark);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/random")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = orderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
assertThat(order).isNotNull();
|
||||||
|
return order.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.reset;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
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.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.common.task.OverdueOrderHandlerTask;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderContinueInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderContinueInfoService;
|
||||||
|
import com.starry.admin.modules.weichat.service.NotificationSender;
|
||||||
|
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
private static final String MESSAGE_OPERATION_SUCCESS = "操作成功";
|
||||||
|
private static final String REVIEW_PENDING_STATE = "0";
|
||||||
|
private static final String EXCLUDE_HISTORY_DISABLED = "0";
|
||||||
|
private static final String OTHER_CLERK_ID = "clerk-apitest-other";
|
||||||
|
private static final int SINGLE_QUANTITY = 1;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private NotificationSender notificationSender;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private WxCustomMpService wxCustomMpService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private OverdueOrderHandlerTask overdueOrderHandlerTask;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayOrderContinueInfoService orderContinueInfoService;
|
||||||
|
|
||||||
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
private String customerToken;
|
||||||
|
private String clerkToken;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUpTokens() {
|
||||||
|
ensureTenantContext();
|
||||||
|
customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||||||
|
// Relax notifications to avoid strict verification noise
|
||||||
|
doNothing().when(notificationSender).sendOrderMessageAsync(Mockito.any());
|
||||||
|
doNothing().when(notificationSender).sendOrderFinishMessageAsync(Mockito.any());
|
||||||
|
doNothing().when(wxCustomMpService).sendCreateOrderMessageBatch(anyList(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString());
|
||||||
|
doNothing().when(overdueOrderHandlerTask).enqueue(Mockito.anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ensureTenantContext();
|
||||||
|
orderContinueInfoService.lambdaUpdate()
|
||||||
|
.eq(PlayOrderContinueInfoEntity::getClerkId, ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.remove();
|
||||||
|
reset(notificationSender, wxCustomMpService, overdueOrderHandlerTask);
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectRandomOrderByIdHidesCustomerContactWhenPending() throws Exception {
|
||||||
|
String marker = "privacy-" + LocalDateTime.now().toString();
|
||||||
|
String orderId = createRandomOrder(marker);
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById")
|
||||||
|
.param("id", orderId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data");
|
||||||
|
assertThat(data.path("weiChatCode").asText()).isEmpty();
|
||||||
|
assertThat(data.path("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
|
||||||
|
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void duplicateContinuationRequestIsRejected() throws Exception {
|
||||||
|
String marker = "continue-" + LocalDateTime.now().toString();
|
||||||
|
String orderId = createRandomOrder(marker);
|
||||||
|
|
||||||
|
acceptOrder(orderId);
|
||||||
|
|
||||||
|
ArrayNode images = mapper.createArrayNode().add("https://example.com/proof.png");
|
||||||
|
ObjectNode payload = mapper.createObjectNode()
|
||||||
|
.put("orderId", orderId)
|
||||||
|
.put("remark", "加场申请")
|
||||||
|
.set("images", images);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/order/clerk/continue")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("下单成功"))
|
||||||
|
.andExpect(jsonPath("$.message").value(MESSAGE_OPERATION_SUCCESS));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/order/clerk/continue")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(500))
|
||||||
|
.andExpect(jsonPath("$.message").value("同一场订单只能续单一次"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderContinueInfoEntity continuation = orderContinueInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderContinueInfoEntity::getOrderId, orderId)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
assertThat(continuation).isNotNull();
|
||||||
|
assertThat(continuation.getReviewedState()).isEqualTo(REVIEW_PENDING_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void randomOrderAcceptedByAnotherClerkHidesSensitiveFields() throws Exception {
|
||||||
|
String marker = "privacy-accepted-" + LocalDateTime.now();
|
||||||
|
String orderId = createRandomOrder(marker);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.lambdaUpdate()
|
||||||
|
.eq(PlayOrderInfoEntity::getId, orderId)
|
||||||
|
.set(PlayOrderInfoEntity::getAcceptBy, OTHER_CLERK_ID)
|
||||||
|
.set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.ACCEPTED.getCode())
|
||||||
|
.update();
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById")
|
||||||
|
.param("id", orderId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data");
|
||||||
|
assertThat(data.path("acceptBy").asText()).isEqualTo(OTHER_CLERK_ID);
|
||||||
|
assertThat(data.path("weiChatCode").asText()).isEmpty();
|
||||||
|
assertThat(data.path("customId").asText()).isEmpty();
|
||||||
|
String nickname = data.path("customNickname").asText();
|
||||||
|
assertThat(nickname.equals("匿名用户") || "å¿åç¨æ·".equals(nickname)).isTrue();
|
||||||
|
assertThat(data.path("customAvatar").asText()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createRandomOrder(String remark) throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
resetCustomerBalance();
|
||||||
|
|
||||||
|
ObjectNode payload = mapper.createObjectNode();
|
||||||
|
payload.put("sex", OrderConstant.Gender.FEMALE.getCode());
|
||||||
|
payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
payload.put("commodityQuantity", SINGLE_QUANTITY);
|
||||||
|
payload.put("weiChatCode", "apitest-customer-wx");
|
||||||
|
payload.put("excludeHistory", EXCLUDE_HISTORY_DISABLED);
|
||||||
|
payload.set("couponIds", mapper.createArrayNode());
|
||||||
|
payload.put("remark", remark);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/random")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
assertThat(order).isNotNull();
|
||||||
|
return order.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptOrder(String orderId) throws Exception {
|
||||||
|
mockMvc.perform(get("/wx/clerk/order/accept")
|
||||||
|
.param("id", orderId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
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.clerk.service.IPlayClerkUserInfoService;
|
||||||
|
import com.starry.admin.modules.weichat.service.WxTokenService;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.ClerkPayeeProfileEntity;
|
||||||
|
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.IClerkPayeeProfileService;
|
||||||
|
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.constant.Constants;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
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.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class WxWithdrawControllerApiTest extends AbstractApiTest {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_TIME_FORMATTER =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
private static final String ORDER_ID_PREFIX = "earn-order-";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IEarningsService earningsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IWithdrawalService withdrawalService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private IClerkPayeeProfileService clerkPayeeProfileService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WxTokenService wxTokenService;
|
||||||
|
|
||||||
|
private final List<String> earningsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> withdrawalsToCleanup = new ArrayList<>();
|
||||||
|
private String clerkToken;
|
||||||
|
private ClerkPayeeProfileEntity payeeProfile;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ensureTenantContext();
|
||||||
|
// reset seeded data to a clean state for deterministic assertions
|
||||||
|
earningsService.lambdaUpdate()
|
||||||
|
.eq(EarningsLineEntity::getClerkId, ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.remove();
|
||||||
|
withdrawalService.lambdaUpdate()
|
||||||
|
.eq(WithdrawalRequestEntity::getClerkId, ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.remove();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
payeeProfile = new ClerkPayeeProfileEntity();
|
||||||
|
payeeProfile.setId("payee-" + IdUtils.getUuid());
|
||||||
|
payeeProfile.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
payeeProfile.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
payeeProfile.setChannel("ALIPAY_QR");
|
||||||
|
payeeProfile.setQrCodeUrl("https://example.com/test-payee.png");
|
||||||
|
payeeProfile.setDisplayName("API测试收款码");
|
||||||
|
payeeProfile.setLastConfirmedAt(now);
|
||||||
|
|
||||||
|
Mockito.when(clerkPayeeProfileService.getByClerk(
|
||||||
|
ApiTestDataSeeder.DEFAULT_TENANT_ID, ApiTestDataSeeder.DEFAULT_CLERK_ID))
|
||||||
|
.thenAnswer(invocation -> payeeProfile);
|
||||||
|
Mockito.when(clerkPayeeProfileService.updateById(Mockito.any(ClerkPayeeProfileEntity.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
payeeProfile = invocation.getArgument(0);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
Mockito.when(clerkPayeeProfileService.save(Mockito.any(ClerkPayeeProfileEntity.class)))
|
||||||
|
.thenReturn(true);
|
||||||
|
Mockito.when(clerkPayeeProfileService.removeById(Mockito.any()))
|
||||||
|
.thenReturn(true);
|
||||||
|
clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ensureTenantContext();
|
||||||
|
if (!earningsToCleanup.isEmpty()) {
|
||||||
|
earningsService.removeByIds(earningsToCleanup);
|
||||||
|
earningsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!withdrawalsToCleanup.isEmpty()) {
|
||||||
|
withdrawalService.removeByIds(withdrawalsToCleanup);
|
||||||
|
withdrawalsToCleanup.clear();
|
||||||
|
}
|
||||||
|
Mockito.reset(clerkPayeeProfileService);
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void balanceEndpointAggregatesAvailableAndPendingEarnings() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime now = LocalDateTime.now().withNano(0);
|
||||||
|
String availableId = insertEarningsLine(
|
||||||
|
"available",
|
||||||
|
new BigDecimal("35.50"),
|
||||||
|
EarningsStatus.AVAILABLE,
|
||||||
|
now.minusHours(1));
|
||||||
|
String frozenId = insertEarningsLine(
|
||||||
|
"frozen",
|
||||||
|
new BigDecimal("64.40"),
|
||||||
|
EarningsStatus.FROZEN,
|
||||||
|
now.plusHours(6));
|
||||||
|
earningsToCleanup.add(availableId);
|
||||||
|
earningsToCleanup.add(frozenId);
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/wx/withdraw/balance")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode root = new ObjectMapper().readTree(result.getResponse().getContentAsString());
|
||||||
|
JsonNode data = root.get("data");
|
||||||
|
assertThat(data.get("available").decimalValue()).isEqualByComparingTo("35.50");
|
||||||
|
assertThat(data.get("pending").decimalValue()).isEqualByComparingTo("64.40");
|
||||||
|
assertThat(data.get("nextUnlockAt").asText())
|
||||||
|
.isEqualTo(now.plusHours(6).format(DATE_TIME_FORMATTER));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWithdrawRejectsNonPositiveAmount() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
mockMvc.perform(post("/wx/withdraw/requests")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"amount\":0}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(500))
|
||||||
|
.andExpect(jsonPath("$.message").value("提现金额必须大于0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWithdrawLocksEligibleEarningsLines() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
BigDecimal amount = new BigDecimal("80.00");
|
||||||
|
String firstLine = insertEarningsLine(
|
||||||
|
"available-one",
|
||||||
|
new BigDecimal("50.00"),
|
||||||
|
EarningsStatus.AVAILABLE,
|
||||||
|
LocalDateTime.now().minusDays(1));
|
||||||
|
String secondLine = insertEarningsLine(
|
||||||
|
"available-two",
|
||||||
|
new BigDecimal("30.00"),
|
||||||
|
EarningsStatus.AVAILABLE,
|
||||||
|
LocalDateTime.now().minusHours(2));
|
||||||
|
earningsToCleanup.add(firstLine);
|
||||||
|
earningsToCleanup.add(secondLine);
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(post("/wx/withdraw/requests")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"amount\":80}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.id").isNotEmpty())
|
||||||
|
.andExpect(jsonPath("$.data.amount").value(amount.doubleValue()))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode root = new ObjectMapper().readTree(result.getResponse().getContentAsString());
|
||||||
|
String withdrawalId = root.path("data").path("id").asText();
|
||||||
|
assertThat(withdrawalId).isNotBlank();
|
||||||
|
|
||||||
|
withdrawalsToCleanup.add(withdrawalId);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
WithdrawalRequestEntity request = withdrawalService.getById(withdrawalId);
|
||||||
|
assertThat(request).isNotNull();
|
||||||
|
assertThat(request.getClerkId()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
assertThat(request.getAmount()).isEqualByComparingTo(amount);
|
||||||
|
|
||||||
|
EarningsLineEntity lockedOne = earningsService.getById(firstLine);
|
||||||
|
EarningsLineEntity lockedTwo = earningsService.getById(secondLine);
|
||||||
|
assertThat(lockedOne.getStatus()).isEqualTo(EarningsStatus.WITHDRAWING.getCode());
|
||||||
|
assertThat(lockedTwo.getStatus()).isEqualTo(EarningsStatus.WITHDRAWING.getCode());
|
||||||
|
assertThat(lockedOne.getWithdrawalId()).isEqualTo(withdrawalId);
|
||||||
|
assertThat(lockedTwo.getWithdrawalId()).isEqualTo(withdrawalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void earningsEndpointFiltersByStatus() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String availableId = insertEarningsLine(
|
||||||
|
"earning-available",
|
||||||
|
new BigDecimal("20.00"),
|
||||||
|
EarningsStatus.AVAILABLE,
|
||||||
|
LocalDateTime.now().minusHours(3));
|
||||||
|
String frozenId = insertEarningsLine(
|
||||||
|
"earning-frozen",
|
||||||
|
new BigDecimal("45.00"),
|
||||||
|
EarningsStatus.FROZEN,
|
||||||
|
LocalDateTime.now().plusHours(6));
|
||||||
|
String withdrawnId = insertEarningsLine(
|
||||||
|
"earning-withdrawn",
|
||||||
|
new BigDecimal("15.00"),
|
||||||
|
EarningsStatus.WITHDRAWING,
|
||||||
|
LocalDateTime.now().minusDays(1));
|
||||||
|
earningsToCleanup.add(availableId);
|
||||||
|
earningsToCleanup.add(frozenId);
|
||||||
|
earningsToCleanup.add(withdrawnId);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/withdraw/earnings")
|
||||||
|
.param("status", EarningsStatus.AVAILABLE.getCode())
|
||||||
|
.param("pageNum", "1")
|
||||||
|
.param("pageSize", "10")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").isArray())
|
||||||
|
.andExpect(jsonPath("$.data[0].id").value(availableId))
|
||||||
|
.andExpect(jsonPath("$.data[0].status").value(EarningsStatus.AVAILABLE.getCode()))
|
||||||
|
.andExpect(jsonPath("$.data[1]").doesNotExist());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String insertEarningsLine(
|
||||||
|
String suffix, BigDecimal amount, EarningsStatus status, LocalDateTime unlockAt) {
|
||||||
|
EarningsLineEntity entity = new EarningsLineEntity();
|
||||||
|
String id = "earn-" + suffix + "-" + IdUtils.getUuid();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
String rawOrderId = ORDER_ID_PREFIX + IdUtils.getUuid();
|
||||||
|
entity.setOrderId(rawOrderId.length() <= 32 ? rawOrderId : rawOrderId.substring(0, 32));
|
||||||
|
entity.setAmount(amount);
|
||||||
|
entity.setStatus(status.getCode());
|
||||||
|
entity.setUnlockTime(unlockAt);
|
||||||
|
entity.setEarningType(EarningsType.ORDER);
|
||||||
|
Date now = toDate(LocalDateTime.now());
|
||||||
|
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setCreatedTime(now);
|
||||||
|
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setUpdatedTime(now);
|
||||||
|
earningsService.save(entity);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date toDate(LocalDateTime value) {
|
||||||
|
return Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum EarningsStatus {
|
||||||
|
AVAILABLE("available"),
|
||||||
|
FROZEN("frozen"),
|
||||||
|
WITHDRAWING("withdrawing");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
EarningsStatus(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user