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 couponIds = new ArrayList<>(); private final List 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 obtainCouponReturnsNonEligibilityReasonWhenNonWhitelisted__covers_CP_002() 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__covers_CP_004() 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__covers_CP_006() 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(java.nio.charset.StandardCharsets.UTF_8)) .path("data"); assertThat(data).isNotNull(); assertThat(data.size()).isGreaterThanOrEqualTo(2); boolean foundEligibleDetail = false; boolean foundIneligibleDetail = false; for (JsonNode node : data) { if (eligibleDetail.getId().equals(node.path("id").asText())) { foundEligibleDetail = true; assertThat(node.path("available").asText()).isEqualTo("1"); assertThat(node.path("reasonForUnavailableUse").asText("")).isEmpty(); } if (ineligibleDetail.getId().equals(node.path("id").asText())) { foundIneligibleDetail = true; assertThat(node.path("available").asText()).isEqualTo("0"); assertThat(node.path("reasonForUnavailableUse").asText()).isEqualTo("订单类型不符合"); } } assertThat(foundEligibleDetail).isTrue(); assertThat(foundIneligibleDetail).isTrue(); } @Test void obtainCouponSucceedsWhenEligible__covers_CP_003() 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__covers_CP_002() 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("优惠券已达到领取上限")); } @Test void obtainCouponRejectsEmptyId__covers_CP_001() throws Exception { ensureTenantContext(); mockMvc.perform(get("/wx/coupon/custom/obtainCoupon") .param("id", "") .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(500)) .andExpect(jsonPath("$.message").value("请求参数异常,优惠券ID不能为")); } @Test void queryAllHidesWhitelistCouponFromNonWhitelistCustomer__covers_CP_004() throws Exception { ensureTenantContext(); String couponId = newCouponId("qall-wl"); PlayCouponInfoEntity coupon = createBaseCoupon(couponId); coupon.setClaimConditionType(CouponClaimConditionType.WHITELIST.code()); coupon.setCustomWhitelist(List.of("other-customer")); couponInfoService.save(coupon); couponIds.add(coupon.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(java.nio.charset.StandardCharsets.UTF_8)) .path("data"); assertThat(data).isNotNull(); assertThat(data.isArray()).isTrue(); for (JsonNode node : data) { assertThat(node.path("id").asText()).isNotEqualTo(coupon.getId()); } } @Test void queryByOrderRejectsWhenClerkIdAndLevelIdBothEmpty__covers_CP_005() throws Exception { ensureTenantContext(); ObjectNode payload = mapper.createObjectNode(); payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID); payload.put("levelId", ""); payload.put("clerkId", ""); payload.put("placeType", OrderConstant.PlaceType.RANDOM.getCode()); payload.put("commodityQuantity", 1); 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(500)) .andExpect(jsonPath("$.message").value("请求参数异常,店员ID不能为空,等级ID不能为空")); } @Test void queryByOrderSwallowsBrokenCouponAndReturnsRemaining__covers_CP_007() throws Exception { ensureTenantContext(); PlayCouponInfoEntity eligible = createBaseCoupon(newCouponId("swv")); eligible.setUseMinAmount(BigDecimal.ZERO); couponInfoService.save(eligible); couponIds.add(eligible.getId()); PlayCouponDetailsEntity eligibleDetail = createCouponDetail(eligible.getId(), CouponUseState.UNUSED); couponDetailsService.save(eligibleDetail); couponDetailIds.add(eligibleDetail.getId()); PlayCouponDetailsEntity brokenDetail = createCouponDetail(newCouponId("missing"), CouponUseState.UNUSED); couponDetailsService.save(brokenDetail); couponDetailIds.add(brokenDetail.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(); JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data"); assertThat(data.isArray()).isTrue(); boolean foundEligibleDetail = false; boolean foundBrokenDetail = false; for (JsonNode node : data) { String returnedId = node.path("id").asText(); if (eligibleDetail.getId().equals(returnedId)) { foundEligibleDetail = true; } if (brokenDetail.getId().equals(returnedId)) { foundBrokenDetail = true; } } assertThat(foundEligibleDetail).isTrue(); assertThat(foundBrokenDetail).isFalse(); } 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()); } }