691 lines
36 KiB
Java
691 lines
36 KiB
Java
package com.starry.admin.api;
|
||
|
||
import static org.hamcrest.Matchers.hasSize;
|
||
import static org.mockito.ArgumentMatchers.anyList;
|
||
import static org.mockito.ArgumentMatchers.argThat;
|
||
import static org.mockito.ArgumentMatchers.eq;
|
||
import static org.mockito.Mockito.reset;
|
||
import static org.mockito.Mockito.verify;
|
||
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.starry.admin.common.apitest.ApiTestDataSeeder;
|
||
import com.starry.admin.common.task.OverdueOrderHandlerTask;
|
||
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||
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.vo.PlayCouponDetailsReturnVo;
|
||
import com.starry.admin.modules.weichat.service.NotificationSender;
|
||
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||
import com.starry.admin.modules.withdraw.enums.EarningsType;
|
||
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.math.RoundingMode;
|
||
import java.time.LocalDateTime;
|
||
import org.assertj.core.api.Assertions;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||
import org.springframework.http.MediaType;
|
||
import org.springframework.test.web.servlet.MvcResult;
|
||
|
||
class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
|
||
|
||
@MockBean
|
||
private NotificationSender notificationSender;
|
||
|
||
@MockBean
|
||
private WxCustomMpService wxCustomMpService;
|
||
|
||
@MockBean
|
||
private OverdueOrderHandlerTask overdueOrderHandlerTask;
|
||
|
||
@org.springframework.beans.factory.annotation.Autowired
|
||
private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService;
|
||
|
||
@Test
|
||
void randomOrderFailsWhenBalanceInsufficient() throws Exception {
|
||
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||
String remark = "API random insufficient " + IdUtils.getUuid();
|
||
try {
|
||
setCustomerBalance(BigDecimal.ZERO);
|
||
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||
|
||
ensureTenantContext();
|
||
long beforeCount = playOrderInfoService.lambdaQuery()
|
||
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
|
||
.count();
|
||
|
||
String payload = "{" +
|
||
"\"sex\":\"2\"," +
|
||
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||
"\"commodityQuantity\":1," +
|
||
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||
"\"excludeHistory\":\"0\"," +
|
||
"\"couponIds\":[]," +
|
||
"\"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))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.code").value(998));
|
||
|
||
ensureTenantContext();
|
||
long afterCount = playOrderInfoService.lambdaQuery()
|
||
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
|
||
.count();
|
||
Assertions.assertThat(afterCount).isEqualTo(beforeCount);
|
||
} finally {
|
||
CustomSecurityContextHolder.remove();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
// 测试用例:客户带随机下单请求命中默认等级与服务,接口应返回成功文案,
|
||
// 并新增一条处于待接单状态的随机订单,金额、商品信息与 remark 与提交参数保持一致。
|
||
void randomOrderCreatesPendingOrder() throws Exception {
|
||
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||
try {
|
||
resetCustomerBalance();
|
||
String rawToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, rawToken);
|
||
|
||
String remark = "API test random order " + IdUtils.getUuid();
|
||
ensureTenantContext();
|
||
long beforeCount = playOrderInfoService.lambdaQuery()
|
||
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
|
||
.count();
|
||
|
||
String payload = "{" +
|
||
"\"sex\":\"2\"," +
|
||
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||
"\"commodityQuantity\":1," +
|
||
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||
"\"excludeHistory\":\"0\"," +
|
||
"\"couponIds\":[]," +
|
||
"\"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 + rawToken)
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content(payload))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.code").value(200))
|
||
.andExpect(jsonPath("$.data").value("下单成功"));
|
||
|
||
ensureTenantContext();
|
||
long afterCount = playOrderInfoService.lambdaQuery()
|
||
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
|
||
.count();
|
||
|
||
Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);
|
||
|
||
PlayOrderInfoEntity latest = playOrderInfoService.lambdaQuery()
|
||
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
|
||
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||
.last("limit 1")
|
||
.one();
|
||
|
||
Assertions.assertThat(latest).isNotNull();
|
||
Assertions.assertThat(latest.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
|
||
Assertions.assertThat(latest.getCommodityId()).isEqualTo(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||
Assertions.assertThat(latest.getOrderMoney()).isNotNull();
|
||
} finally {
|
||
CustomSecurityContextHolder.remove();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
void randomOrderCancellationReleasesCoupon() throws Exception {
|
||
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||
String remark = "API random cancel coupon " + IdUtils.getUuid();
|
||
BigDecimal discount = new BigDecimal("15.00");
|
||
try {
|
||
resetCustomerBalance();
|
||
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||
|
||
String couponInfoId = createCouponId("cpn-rc-");
|
||
ensureFixedReductionCoupon(
|
||
couponInfoId,
|
||
OrderConstant.PlaceType.RANDOM,
|
||
discount,
|
||
new BigDecimal("60.00"),
|
||
"0");
|
||
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);
|
||
|
||
String payload = "{" +
|
||
"\"sex\":\"2\"," +
|
||
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||
"\"commodityQuantity\":1," +
|
||
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||
"\"excludeHistory\":\"0\"," +
|
||
"\"couponIds\":[\"" + couponDetailId + "\"]," +
|
||
"\"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))
|
||
.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();
|
||
Assertions.assertThat(order).isNotNull();
|
||
|
||
PlayCouponDetailsEntity detailBeforeCancel = couponDetailsService.getById(couponDetailId);
|
||
Assertions.assertThat(detailBeforeCancel).isNotNull();
|
||
Assertions.assertThat(detailBeforeCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
|
||
Assertions.assertThat(detailBeforeCancel.getUseTime()).isNotNull();
|
||
|
||
String cancelPayload = "{" +
|
||
"\"orderId\":\"" + order.getId() + "\"," +
|
||
"\"refundReason\":\"测试取消\"," +
|
||
"\"images\":[]" +
|
||
"}";
|
||
mockMvc.perform(post("/wx/custom/order/cancellation")
|
||
.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(cancelPayload))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.code").value(200))
|
||
.andExpect(jsonPath("$.data").value("取消成功"));
|
||
|
||
ensureTenantContext();
|
||
PlayOrderInfoEntity cancelled = playOrderInfoService.selectOrderInfoById(order.getId());
|
||
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
|
||
|
||
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
|
||
Assertions.assertThat(detail).isNotNull();
|
||
Assertions.assertThat(detail.getUseState())
|
||
.as("取消订单后优惠券应恢复为未使用")
|
||
.isEqualTo(CouponUseState.UNUSED.getCode());
|
||
Assertions.assertThat(detail.getUseTime()).isNull();
|
||
} finally {
|
||
CustomSecurityContextHolder.remove();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
// 测试用例:订单已接单后由管理员强制取消,也应释放所使用的优惠券
|
||
void randomOrderForceCancelReleasesCoupon() throws Exception {
|
||
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||
String remark = "API random force cancel " + IdUtils.getUuid();
|
||
BigDecimal discount = new BigDecimal("12.00");
|
||
try {
|
||
reset(notificationSender);
|
||
resetCustomerBalance();
|
||
|
||
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||
|
||
String couponInfoId = createCouponId("cpn-rf-");
|
||
ensureFixedReductionCoupon(
|
||
couponInfoId,
|
||
OrderConstant.PlaceType.RANDOM,
|
||
discount,
|
||
new BigDecimal("60.00"),
|
||
"0");
|
||
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);
|
||
|
||
String payload = "{" +
|
||
"\"sex\":\"2\"," +
|
||
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||
"\"commodityQuantity\":1," +
|
||
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||
"\"excludeHistory\":\"0\"," +
|
||
"\"couponIds\":[\"" + couponDetailId + "\"]," +
|
||
"\"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))
|
||
.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();
|
||
Assertions.assertThat(order).isNotNull();
|
||
|
||
mockMvc.perform(get("/wx/clerk/order/accept")
|
||
.param("id", order.getId())
|
||
.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").value("成功"));
|
||
|
||
ensureTenantContext();
|
||
PlayCouponDetailsEntity detailBeforeForceCancel = couponDetailsService.getById(couponDetailId);
|
||
Assertions.assertThat(detailBeforeForceCancel).isNotNull();
|
||
Assertions.assertThat(detailBeforeForceCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
|
||
Assertions.assertThat(detailBeforeForceCancel.getUseTime()).isNotNull();
|
||
|
||
ensureTenantContext();
|
||
orderInfoService.forceCancelOngoingOrder(
|
||
"2",
|
||
ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
|
||
order.getId(),
|
||
order.getFinalAmount(),
|
||
"管理员强制取消",
|
||
java.util.Collections.emptyList());
|
||
|
||
ensureTenantContext();
|
||
PlayOrderInfoEntity cancelled = playOrderInfoService.selectOrderInfoById(order.getId());
|
||
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
|
||
|
||
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
|
||
Assertions.assertThat(detail).isNotNull();
|
||
Assertions.assertThat(detail.getUseState())
|
||
.as("强制取消订单后优惠券应恢复为未使用")
|
||
.isEqualTo(CouponUseState.UNUSED.getCode());
|
||
Assertions.assertThat(detail.getUseTime()).isNull();
|
||
} finally {
|
||
CustomSecurityContextHolder.remove();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
// 测试用例:随机单携带店铺承担的满减券下单,需依据折后金额推送通知,
|
||
// 完整履约后优惠券应置为已使用且收益与预计工资保持一致。
|
||
void randomOrderLifecycleWithCouponAdjustsRevenueAndNotifications() throws Exception {
|
||
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||
String remark = "API random coupon " + IdUtils.getUuid();
|
||
BigDecimal discount = new BigDecimal("20.00");
|
||
try {
|
||
reset(notificationSender);
|
||
resetCustomerBalance();
|
||
|
||
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||
|
||
BigDecimal grossAmount = ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE;
|
||
String couponInfoId = createCouponId("cpn-r-");
|
||
ensureFixedReductionCoupon(
|
||
couponInfoId,
|
||
OrderConstant.PlaceType.RANDOM,
|
||
discount,
|
||
new BigDecimal("60.00"),
|
||
"0");
|
||
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);
|
||
|
||
String payload = "{" +
|
||
"\"sex\":\"2\"," +
|
||
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||
"\"commodityQuantity\":1," +
|
||
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||
"\"excludeHistory\":\"0\"," +
|
||
"\"couponIds\":[\"" + couponDetailId + "\"]," +
|
||
"\"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))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.code").value(200))
|
||
.andExpect(jsonPath("$.data").value("下单成功"));
|
||
|
||
ensureTenantContext();
|
||
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||
.last("limit 1")
|
||
.one();
|
||
|
||
Assertions.assertThat(order).isNotNull();
|
||
Assertions.assertThat(order.getCouponIds()).contains(couponDetailId);
|
||
|
||
BigDecimal expectedNet = grossAmount.subtract(discount).setScale(2, RoundingMode.HALF_UP);
|
||
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet);
|
||
Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount);
|
||
|
||
verify(wxCustomMpService).sendCreateOrderMessageBatch(
|
||
anyList(),
|
||
eq(order.getOrderNo()),
|
||
eq(expectedNet.toString()),
|
||
eq(order.getCommodityName()),
|
||
eq(order.getId()),
|
||
eq(order.getPlaceType()),
|
||
eq(order.getRewardType()));
|
||
String orderId = order.getId();
|
||
|
||
PlayCouponDetailsEntity detailAfterOrderPlaced = couponDetailsService.getById(couponDetailId);
|
||
Assertions.assertThat(detailAfterOrderPlaced).isNotNull();
|
||
Assertions.assertThat(detailAfterOrderPlaced.getUseState()).isEqualTo(CouponUseState.USED.getCode());
|
||
Assertions.assertThat(detailAfterOrderPlaced.getUseTime()).isNotNull();
|
||
|
||
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))
|
||
.andExpect(jsonPath("$.data").value("成功"));
|
||
|
||
verify(notificationSender).sendOrderMessageAsync(argThat(o -> o.getId().equals(orderId)));
|
||
|
||
|
||
mockMvc.perform(get("/wx/clerk/order/start")
|
||
.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))
|
||
.andExpect(jsonPath("$.data").value("成功"));
|
||
|
||
mockMvc.perform(get("/wx/custom/order/end")
|
||
.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))
|
||
.andExpect(jsonPath("$.data").value("成功"));
|
||
|
||
verify(notificationSender).sendOrderFinishMessageAsync(argThat(o -> orderId.equals(o.getId())));
|
||
|
||
ensureTenantContext();
|
||
PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId);
|
||
int ratio = completedOrder.getEstimatedRevenueRatio();
|
||
BigDecimal baseRevenue = grossAmount
|
||
.multiply(BigDecimal.valueOf(ratio))
|
||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||
PlayCouponDetailsReturnVo detail = couponDetailsService.selectPlayCouponDetailsById(couponDetailId);
|
||
BigDecimal clerkDiscount = BigDecimal.ZERO;
|
||
if (detail != null && "0".equals(detail.getAttributionDiscounts())) {
|
||
BigDecimal discountAmount = detail.getDiscountAmount() == null ? BigDecimal.ZERO : detail.getDiscountAmount();
|
||
clerkDiscount = discountAmount;
|
||
}
|
||
BigDecimal expectedRevenue = baseRevenue.subtract(clerkDiscount).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
|
||
Assertions.assertThat(completedOrder.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue);
|
||
|
||
EarningsLineEntity earningsLine = earningsService.lambdaQuery()
|
||
.eq(EarningsLineEntity::getOrderId, orderId)
|
||
.last("limit 1")
|
||
.one();
|
||
Assertions.assertThat(earningsLine).isNotNull();
|
||
Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(expectedRevenue);
|
||
|
||
assertCouponUsed(couponDetailId);
|
||
|
||
PlayCouponDetailsEntity detailAfterLifecycle = couponDetailsService.getById(couponDetailId);
|
||
Assertions.assertThat(detailAfterLifecycle).isNotNull();
|
||
Assertions.assertThat(detailAfterLifecycle.getUseState()).isEqualTo(CouponUseState.USED.getCode());
|
||
Assertions.assertThat(detailAfterLifecycle.getUseTime()).isNotNull();
|
||
} finally {
|
||
CustomSecurityContextHolder.remove();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
// 测试用例:模拟随机订单完整生命周期——客户下单、陪玩师接单/开局、客户完结,
|
||
// 期间验证微信通知被触发、收益记录生成、冻结解冻时间正确,并校准日终统计接口返回的订单数、GMV 与预计收益。
|
||
void randomOrderLifecycleGeneratesEarningsAndNotifications() throws Exception {
|
||
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||
String remark = "API random lifecycle " + IdUtils.getUuid();
|
||
try {
|
||
resetCustomerBalance();
|
||
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||
|
||
LocalDateTime overviewWindowStart = LocalDateTime.now().minusMinutes(5);
|
||
LocalDateTime overviewWindowEnd = LocalDateTime.now().plusMinutes(5);
|
||
OverviewSnapshot overviewBefore = fetchOverview(overviewWindowStart, overviewWindowEnd);
|
||
|
||
String orderId = placeRandomOrder(remark, customerToken);
|
||
|
||
reset(notificationSender);
|
||
|
||
ensureTenantContext();
|
||
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))
|
||
.andExpect(jsonPath("$.data").value("成功"));
|
||
|
||
verify(notificationSender).sendOrderMessageAsync(argThat(order -> order.getId().equals(orderId)));
|
||
|
||
reset(notificationSender);
|
||
|
||
ensureTenantContext();
|
||
mockMvc.perform(get("/wx/clerk/order/start")
|
||
.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))
|
||
.andExpect(jsonPath("$.data").value("成功"));
|
||
|
||
ensureTenantContext();
|
||
long earningsBefore = earningsService.lambdaQuery()
|
||
.eq(EarningsLineEntity::getOrderId, orderId)
|
||
.count();
|
||
Assertions.assertThat(earningsBefore).isZero();
|
||
|
||
ensureTenantContext();
|
||
mockMvc.perform(get("/wx/custom/order/end")
|
||
.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))
|
||
.andExpect(jsonPath("$.data").value("成功"));
|
||
|
||
verify(notificationSender).sendOrderFinishMessageAsync(argThat(order -> order.getId().equals(orderId)));
|
||
|
||
ensureTenantContext();
|
||
long earningsAfter = earningsService.lambdaQuery()
|
||
.eq(EarningsLineEntity::getOrderId, orderId)
|
||
.count();
|
||
Assertions.assertThat(earningsAfter).isEqualTo(1);
|
||
|
||
ensureTenantContext();
|
||
PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId);
|
||
Assertions.assertThat(completedOrder.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.COMPLETED.getCode());
|
||
Assertions.assertThat(completedOrder.getEstimatedRevenue()).isNotNull();
|
||
|
||
EarningsLineEntity earningsLine = earningsService.lambdaQuery()
|
||
.eq(EarningsLineEntity::getOrderId, orderId)
|
||
.one();
|
||
Assertions.assertThat(earningsLine).isNotNull();
|
||
Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(completedOrder.getEstimatedRevenue());
|
||
int freezeHours = freezePolicyService.resolveFreezeHours(
|
||
ApiTestDataSeeder.DEFAULT_TENANT_ID,
|
||
ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||
LocalDateTime expectedUnlock = completedOrder.getOrderEndTime().plusHours(freezeHours);
|
||
Assertions.assertThat(earningsLine.getUnlockTime()).isEqualTo(expectedUnlock);
|
||
Assertions.assertThat(earningsLine.getUnlockTime()).isAfter(LocalDateTime.now().minusMinutes(5));
|
||
Assertions.assertThat(earningsLine.getEarningType())
|
||
.isIn(EarningsType.ORDER, EarningsType.ADJUSTMENT);
|
||
|
||
OverviewSnapshot overviewAfter = fetchOverview(overviewWindowStart, overviewWindowEnd);
|
||
Assertions.assertThat(overviewAfter.totalOrderCount - overviewBefore.totalOrderCount)
|
||
.isEqualTo(1);
|
||
Assertions.assertThat(overviewAfter.totalGmv.subtract(overviewBefore.totalGmv).setScale(2, RoundingMode.HALF_UP))
|
||
.isEqualByComparingTo(completedOrder.getFinalAmount());
|
||
Assertions.assertThat(overviewAfter.totalEstimatedRevenue.subtract(overviewBefore.totalEstimatedRevenue).setScale(2, RoundingMode.HALF_UP))
|
||
.isEqualByComparingTo(completedOrder.getEstimatedRevenue());
|
||
Assertions.assertThat(overviewAfter.clerkOrderCount - overviewBefore.clerkOrderCount)
|
||
.isEqualTo(1);
|
||
Assertions.assertThat(overviewAfter.clerkGmv.subtract(overviewBefore.clerkGmv).setScale(2, RoundingMode.HALF_UP))
|
||
.isEqualByComparingTo(completedOrder.getFinalAmount());
|
||
} finally {
|
||
CustomSecurityContextHolder.remove();
|
||
}
|
||
}
|
||
|
||
private OverviewSnapshot fetchOverview(LocalDateTime start, LocalDateTime end) throws Exception {
|
||
String payload = "{" +
|
||
"\"includeSummary\":true," +
|
||
"\"includeRankings\":true," +
|
||
"\"limit\":5," +
|
||
"\"endOrderTime\":[\"" + start.format(DATE_TIME_FORMATTER) + "\",\""
|
||
+ end.format(DATE_TIME_FORMATTER) + "\"]" +
|
||
"}";
|
||
|
||
MvcResult result = mockMvc.perform(post("/statistics/performance/overview")
|
||
.header(USER_HEADER, DEFAULT_USER)
|
||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content(payload))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.code").value(200))
|
||
.andReturn();
|
||
|
||
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).path("data");
|
||
OverviewSnapshot snapshot = new OverviewSnapshot();
|
||
JsonNode summary = data.path("summary");
|
||
snapshot.totalOrderCount = summary.path("totalOrderCount").asInt();
|
||
snapshot.totalGmv = new BigDecimal(summary.path("totalGmv").asText("0"));
|
||
snapshot.totalEstimatedRevenue = new BigDecimal(summary.path("totalEstimatedRevenue").asText("0"));
|
||
|
||
JsonNode rankings = data.path("rankings");
|
||
if (rankings.isArray()) {
|
||
for (JsonNode node : rankings) {
|
||
if (ApiTestDataSeeder.DEFAULT_CLERK_ID.equals(node.path("clerkId").asText())) {
|
||
snapshot.clerkOrderCount = node.path("orderCount").asInt();
|
||
snapshot.clerkGmv = new BigDecimal(node.path("gmv").asText("0"));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return snapshot;
|
||
}
|
||
|
||
private static class OverviewSnapshot {
|
||
private int totalOrderCount;
|
||
private BigDecimal totalGmv = BigDecimal.ZERO;
|
||
private BigDecimal totalEstimatedRevenue = BigDecimal.ZERO;
|
||
private int clerkOrderCount;
|
||
private BigDecimal clerkGmv = BigDecimal.ZERO;
|
||
}
|
||
|
||
private String placeRandomOrder(String remark, String customerToken) throws Exception {
|
||
ensureTenantContext();
|
||
long beforeCount = playOrderInfoService.lambdaQuery()
|
||
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||
.count();
|
||
|
||
String payload = "{" +
|
||
"\"sex\":\"2\"," +
|
||
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||
"\"commodityQuantity\":1," +
|
||
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||
"\"excludeHistory\":\"0\"," +
|
||
"\"couponIds\":[]," +
|
||
"\"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))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.code").value(200))
|
||
.andExpect(jsonPath("$.data").value("下单成功"));
|
||
|
||
ensureTenantContext();
|
||
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||
.last("limit 1")
|
||
.one();
|
||
|
||
Assertions.assertThat(order).isNotNull();
|
||
Assertions.assertThat(order.getPlaceType()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
|
||
|
||
ensureTenantContext();
|
||
long afterCount = playOrderInfoService.lambdaQuery()
|
||
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||
.count();
|
||
Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);
|
||
|
||
verify(wxCustomMpService).sendCreateOrderMessageBatch(
|
||
anyList(),
|
||
eq(order.getOrderNo()),
|
||
eq(order.getFinalAmount().toString()),
|
||
eq(order.getCommodityName()),
|
||
eq(order.getId()),
|
||
eq(order.getPlaceType()),
|
||
eq(order.getRewardType()));
|
||
verify(overdueOrderHandlerTask).enqueue(order.getId() + "_" + ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||
|
||
reset(wxCustomMpService);
|
||
reset(overdueOrderHandlerTask);
|
||
|
||
return order.getId();
|
||
}
|
||
|
||
}
|