package com.starry.admin.api; import static org.mockito.ArgumentMatchers.anyString; 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.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.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.WxCustomMpService; 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.util.Collections; 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; class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport { @MockBean private WxCustomMpService wxCustomMpService; @org.springframework.beans.factory.annotation.Autowired private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService; @org.springframework.beans.factory.annotation.Autowired private com.starry.admin.modules.weichat.service.WxTokenService clerkWxTokenService; @Test // 测试用例:指定单取消后优惠券应恢复为未使用 void specifiedOrderCancellationReleasesCoupon() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); String remark = "API specified cancel " + IdUtils.getUuid(); BigDecimal discount = new BigDecimal("10.00"); try { resetCustomerBalance(); String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken); String couponInfoId = createCouponId("cpn-sc-"); ensureFixedReductionCoupon( couponInfoId, OrderConstant.PlaceType.SPECIFIED, discount, new BigDecimal("60.00"), "0"); String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.SPECIFIED, customerToken); String payload = "{" + "\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," + "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," + "\"commodityQuantity\":1," + "\"weiChatCode\":\"apitest-customer-wx\"," + "\"couponIds\":[\"" + couponDetailId + "\"]," + "\"remark\":\"" + remark + "\"" + "}"; mockMvc.perform(post("/wx/custom/order/commodity") .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 detailBeforeForceCancel = couponDetailsService.getById(couponDetailId); Assertions.assertThat(detailBeforeForceCancel).isNotNull(); Assertions.assertThat(detailBeforeForceCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode()); Assertions.assertThat(detailBeforeForceCancel.getUseTime()).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 specifiedOrderWithCouponAdjustsAmountAndRevenue() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); String remark = "API specified coupon " + IdUtils.getUuid(); BigDecimal discount = new BigDecimal("15.00"); try { reset(wxCustomMpService); resetCustomerBalance(); String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken); BigDecimal grossAmount = ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE; String couponInfoId = createCouponId("cpn-s-"); ensureFixedReductionCoupon( couponInfoId, OrderConstant.PlaceType.SPECIFIED, discount, new BigDecimal("60.00"), "0"); String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.SPECIFIED, customerToken); String payload = "{" + "\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," + "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," + "\"commodityQuantity\":1," + "\"weiChatCode\":\"apitest-customer-wx\"," + "\"couponIds\":[\"" + couponDetailId + "\"]," + "\"remark\":\"" + remark + "\"" + "}"; mockMvc.perform(post("/wx/custom/order/commodity") .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); PlayCouponDetailsEntity detailAfterOrder = couponDetailsService.getById(couponDetailId); Assertions.assertThat(detailAfterOrder).isNotNull(); Assertions.assertThat(detailAfterOrder.getUseState()).isEqualTo(CouponUseState.USED.getCode()); Assertions.assertThat(detailAfterOrder.getUseTime()).isNotNull(); verify(wxCustomMpService).sendCreateOrderMessage( eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID), anyString(), eq(expectedNet.toString()), eq(order.getCommodityName()), eq(order.getId())); int ratio = order.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(order.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue); assertCouponUsed(couponDetailId); PlayCouponDetailsEntity detailAfterComplete = couponDetailsService.getById(couponDetailId); Assertions.assertThat(detailAfterComplete).isNotNull(); Assertions.assertThat(detailAfterComplete.getUseState()).isEqualTo(CouponUseState.USED.getCode()); Assertions.assertThat(detailAfterComplete.getUseTime()).isNotNull(); } finally { CustomSecurityContextHolder.remove(); } } @Test // 测试用例:指定单在接单后由管理员强制取消时,优惠券应恢复为未使用 void specifiedOrderForceCancelReleasesCoupon() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); String remark = "API specified force cancel " + IdUtils.getUuid(); BigDecimal discount = new BigDecimal("8.00"); try { reset(wxCustomMpService); 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-sf-"); ensureFixedReductionCoupon( couponInfoId, OrderConstant.PlaceType.SPECIFIED, discount, new BigDecimal("60.00"), "0"); String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.SPECIFIED, customerToken); String payload = "{" + "\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," + "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," + "\"commodityQuantity\":1," + "\"weiChatCode\":\"apitest-customer-wx\"," + "\"couponIds\":[\"" + couponDetailId + "\"]," + "\"remark\":\"" + remark + "\"" + "}"; mockMvc.perform(post("/wx/custom/order/commodity") .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(); 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 specifiedOrderCreatesPendingOrder() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); try { resetCustomerBalance(); String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken); String remark = "API specified order " + IdUtils.getUuid(); ensureTenantContext(); long beforeCount = playOrderInfoService.lambdaQuery() .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID) .eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.SPECIFIED.getCode()) .count(); String payload = "{" + "\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," + "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," + "\"commodityQuantity\":1," + "\"weiChatCode\":\"apitest-customer-wx\"," + "\"couponIds\":[]," + "\"remark\":\"" + remark + "\"" + "}"; reset(wxCustomMpService); mockMvc.perform(post("/wx/custom/order/commodity") .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.SPECIFIED.getCode()); Assertions.assertThat(order.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode()); Assertions.assertThat(order.getAcceptBy()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID); Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE); verify(wxCustomMpService).sendCreateOrderMessage( eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID), anyString(), eq(order.getFinalAmount().toString()), eq(order.getCommodityName()), eq(order.getId())); ensureTenantContext(); long afterCount = playOrderInfoService.lambdaQuery() .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID) .eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.SPECIFIED.getCode()) .count(); Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1); } finally { CustomSecurityContextHolder.remove(); } } }