|
|
@@ -0,0 +1,216 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.Activity;
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+
|
|
|
+import java.util.Calendar;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * A2 活动适用人群 + 费用模型重构(audienceScope/audienceType + 参与者身份分类结算 + 仅会员硬门禁)
|
|
|
+ *
|
|
|
+ * 覆盖验收点:
|
|
|
+ * ⑤ 老活动(audience_type 为空/默认 adult)结算金额与改造前逐分一致 —— 用
|
|
|
+ * price / parentFee / memberPrice / parentMemberFee 四个字段各非空、组合覆盖
|
|
|
+ * 4 种取值路径各测一例(新列优先 / 旧列回退 / 混合路径)。
|
|
|
+ * ④ 报名人 age 为空 → 一律按成人档计费,不得误判为儿童。
|
|
|
+ * ③ member_only 硬门禁:非会员拒绝、非登录用户引导登录。
|
|
|
+ */
|
|
|
+class ActivityOrderServiceTest {
|
|
|
+
|
|
|
+ /** 构造活动:childFee/childMemberFee 固定为 1000/800 分,四档成人价格由参数指定 */
|
|
|
+ private Activity activity(Integer price, Integer memberPrice, Integer parentFee, Integer parentMemberFee) {
|
|
|
+ return activity(price, memberPrice, parentFee, parentMemberFee, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Activity activity(Integer price, Integer memberPrice, Integer parentFee, Integer parentMemberFee,
|
|
|
+ String audienceType) {
|
|
|
+ Activity a = new Activity();
|
|
|
+ a.setPrice(price);
|
|
|
+ a.setMemberPrice(memberPrice);
|
|
|
+ a.setParentFee(parentFee);
|
|
|
+ a.setParentMemberFee(parentMemberFee);
|
|
|
+ a.setChildFee(1000);
|
|
|
+ a.setChildMemberFee(800);
|
|
|
+ a.setAudienceScope("all");
|
|
|
+ a.setAudienceType(audienceType);
|
|
|
+ return a;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 复刻改造前的结算逻辑(ActivityOrderService 旧实现,用于逐分对比):
|
|
|
+ * old = isChild ? (isMember ? childMemberFee : childFee)
|
|
|
+ * : (isMember ? parentMemberFee : parentFee)
|
|
|
+ */
|
|
|
+ private int oldFee(Activity a, boolean child, boolean member) {
|
|
|
+ if (child) {
|
|
|
+ return member ? nz(a.getChildMemberFee()) : nz(a.getChildFee());
|
|
|
+ }
|
|
|
+ return member ? nz(a.getParentMemberFee()) : nz(a.getParentFee());
|
|
|
+ }
|
|
|
+
|
|
|
+ private int nz(Integer v) {
|
|
|
+ return v == null ? 0 : v;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============ 验收⑤:4 种成人档取值路径(旧列回退路径必须逐分一致) ============
|
|
|
+
|
|
|
+ /** 路径1:非会员走新列 price(price 非空且 >0) */
|
|
|
+ @Test
|
|
|
+ void adultFee_case1_priceTakesPrecedence() {
|
|
|
+ Activity a = activity(10000, 8000, 0, 0); // price/memberPrice 承担成人价,旧列 0
|
|
|
+ assertEquals(10000, ActivityOrderService.resolveAdultNonMemberFee(a), "非会员 → 新列 price(10000)");
|
|
|
+ assertEquals(8000, ActivityOrderService.resolveAdultMemberFee(a), "会员 → 新列 memberPrice(8000)");
|
|
|
+ assertEquals(10000, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, false), "self 非会员");
|
|
|
+ assertEquals(8000, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, true), "self 会员");
|
|
|
+ // 儿童档不受影响,与改造前逐分一致
|
|
|
+ assertEquals(1000, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_CHILD, false), "child 非会员");
|
|
|
+ assertEquals(800, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_CHILD, true), "child 会员");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 路径2:新列为 0/空 → 回退旧列 parentFee / parentMemberFee(典型存量小程序活动形态) */
|
|
|
+ @Test
|
|
|
+ void adultFee_case2_legacyColumnsFallback_identicalToOld() {
|
|
|
+ Activity a = activity(0, null, 12000, 9000); // 存量活动:price=0、memberPrice=NULL、旧列承载成人价
|
|
|
+ assertNull(a.getAudienceType(), "存量活动 audience_type 可能为空(迁移 DEFAULT 前创建)");
|
|
|
+ // 非会员与会员 4 种身份 × 新旧实现逐分一致
|
|
|
+ assertEquals(oldFee(a, false, false), ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, false), "self 非会员");
|
|
|
+ assertEquals(oldFee(a, false, true), ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, true), "self 会员");
|
|
|
+ assertEquals(oldFee(a, true, false), ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_CHILD, false), "child 非会员");
|
|
|
+ assertEquals(oldFee(a, true, true), ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_CHILD, true), "child 会员");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 路径3:会员走新列 memberPrice、非会员走旧列 parentFee(混合) */
|
|
|
+ @Test
|
|
|
+ void adultFee_case3_mixed_legacyPrice_newMemberPrice() {
|
|
|
+ Activity a = activity(0, 6000, 11000, 0);
|
|
|
+ assertEquals(11000, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, false), "self 非会员 → parentFee(11000) 与旧实现一致");
|
|
|
+ assertEquals(oldFee(a, false, false), ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, false), "非会员路径逐分一致");
|
|
|
+ assertEquals(6000, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, true), "self 会员 → memberPrice(6000)");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 路径4:会员走旧列 parentMemberFee(memberPrice 未写入过 = NULL,存量别名路径,0=免费 原样保留) */
|
|
|
+ @Test
|
|
|
+ void adultFee_case4_legacyMemberPriceFallback() {
|
|
|
+ Activity a = activity(0, null, 0, 7000);
|
|
|
+ assertNull(a.getMemberPrice(), "存量活动的 memberPrice 为 NULL");
|
|
|
+ assertEquals(7000, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, true), "self 会员 → parentMemberFee(7000)");
|
|
|
+ assertEquals(oldFee(a, false, true), ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, true), "会员路径逐分一致");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 存量别名路径下 parentMemberFee=0(历史语义:会员免费)必须原样保留,不得改为按成人价收费 */
|
|
|
+ @Test
|
|
|
+ void adultFee_legacyMemberPriceZero_staysFree() {
|
|
|
+ Activity a = activity(0, null, 12000, 0);
|
|
|
+ assertEquals(12000, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, false), "非会员 → parentFee(12000)");
|
|
|
+ assertEquals(0, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, true), "会员 → parentMemberFee=0(免费)原样保留");
|
|
|
+ assertEquals(oldFee(a, false, true), ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, true), "会员路径逐分一致");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 迁移 DEFAULT='adult' 后,存量活动 audience_type 已是 adult,行为与空值等价(不分支于 audience_type 计费) */
|
|
|
+ @Test
|
|
|
+ void adultFee_audienceTypeDefaultAdult_equivalentToLegacy() {
|
|
|
+ Activity legacyNull = activity(0, null, 12000, 9000, null);
|
|
|
+ Activity defaulted = activity(0, null, 12000, 9000, "adult");
|
|
|
+ for (boolean member : new boolean[]{false, true}) {
|
|
|
+ assertEquals(
|
|
|
+ ActivityOrderService.resolveFee(legacyNull, ActivityOrderService.TYPE_SELF, member),
|
|
|
+ ActivityOrderService.resolveFee(defaulted, ActivityOrderService.TYPE_SELF, member),
|
|
|
+ "audience_type 空/默认 adult 计费结果必须一致");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 会员价与旧别名均为 0/空 → 回落成人非会员价(会员不免费) */
|
|
|
+ @Test
|
|
|
+ void adultMemberFee_memberPriceUnset_fallsBackToBasePrice() {
|
|
|
+ Activity a = activity(20000, 0, 0, 0); // 新活动形态:price=20000,memberPrice 未设置
|
|
|
+ assertEquals(20000, ActivityOrderService.resolveAdultMemberFee(a), "会员价留空 = 同非会员价");
|
|
|
+ assertEquals(20000, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, true), "self 会员按非会员价");
|
|
|
+ assertEquals(20000, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_ELDER, true), "elder 会员复用成人价");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============ 验收④:age 为空必须回落 adult(不得误判为儿童) ============
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void classify_ageMissing_neverChild() {
|
|
|
+ assertEquals(ActivityOrderService.TYPE_SELF, ActivityOrderService.classifyParticipant(null, null), "未关联成员=本人");
|
|
|
+ assertEquals(ActivityOrderService.TYPE_ADULT, ActivityOrderService.classifyParticipant(1L, null), "年龄未知→adult,绝不 child");
|
|
|
+ assertEquals(ActivityOrderService.TYPE_ADULT, ActivityOrderService.classifyParticipant(1L, 18), "18 岁不算儿童");
|
|
|
+ assertEquals(ActivityOrderService.TYPE_CHILD, ActivityOrderService.classifyParticipant(1L, 17), "17 岁儿童");
|
|
|
+ assertEquals(ActivityOrderService.TYPE_CHILD, ActivityOrderService.classifyParticipant(1L, 0), "0 岁儿童");
|
|
|
+ assertEquals(ActivityOrderService.TYPE_ADULT, ActivityOrderService.classifyParticipant(1L, 59), "59 岁成人");
|
|
|
+ assertEquals(ActivityOrderService.TYPE_ELDER, ActivityOrderService.classifyParticipant(1L, 60), "60 岁长者");
|
|
|
+ assertEquals(ActivityOrderService.TYPE_ELDER, ActivityOrderService.classifyParticipant(1L, 100), "100 岁长者");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** FamilyMember.birthday 为空 → resolveAge 返回 null(不得当 0 岁儿童) */
|
|
|
+ @Test
|
|
|
+ void resolveAge_birthdayMissing_returnsNull() {
|
|
|
+ FamilyMember m = new FamilyMember();
|
|
|
+ assertNull(ActivityOrderService.resolveAge(m), "birthday 为空 → 年龄未知");
|
|
|
+ assertNull(ActivityOrderService.resolveAge(null), "成员不存在 → 年龄未知");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void resolveAge_birthdayPresent_computesYears() {
|
|
|
+ FamilyMember m = new FamilyMember();
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.add(Calendar.YEAR, -10);
|
|
|
+ cal.add(Calendar.DAY_OF_YEAR, -5);
|
|
|
+ m.setBirthday(cal.getTime());
|
|
|
+ Integer age = ActivityOrderService.resolveAge(m);
|
|
|
+ assertNotNull(age);
|
|
|
+ assertTrue(age == 10 || age == 11, "10 岁生日前后误差 1 年,实际=" + age);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============ 验收③:仅会员硬门禁(后端拦截,与 TaskController.checkMemberOnly 同范式) ============
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void memberOnlyGate_unloggedUser() {
|
|
|
+ Activity a = activity(0, null, 0, 0);
|
|
|
+ a.setAudienceScope("member_only");
|
|
|
+ assertEquals("请先登录后再报名", ActivityOrderService.memberOnlyGateMessage(a, null, false), "非登录用户→引导登录");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void memberOnlyGate_nonMember() {
|
|
|
+ Activity a = activity(0, null, 0, 0);
|
|
|
+ a.setAudienceScope("member_only");
|
|
|
+ assertEquals("该活动仅会员可参加", ActivityOrderService.memberOnlyGateMessage(a, 1L, false), "非会员→拒绝");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void memberOnlyGate_member() {
|
|
|
+ Activity a = activity(0, null, 0, 0);
|
|
|
+ a.setAudienceScope("member_only");
|
|
|
+ assertNull(ActivityOrderService.memberOnlyGateMessage(a, 1L, true), "会员→放行");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void memberOnlyGate_allActivitiesPass() {
|
|
|
+ Activity a = activity(0, null, 0, 0);
|
|
|
+ a.setAudienceScope("all");
|
|
|
+ assertNull(ActivityOrderService.memberOnlyGateMessage(a, null, false), "all 不加会员门槛,非登录也由既有鉴权兜底");
|
|
|
+ assertNull(ActivityOrderService.memberOnlyGateMessage(a, 1L, false), "all 非会员放行");
|
|
|
+ assertNull(ActivityOrderService.memberOnlyGateMessage(null, 1L, false), "活动不存在由前置校验兜底");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 免费活动(全部档位为 0/空)→ totalAmount=0 走免报名快照分支(验收⑥:金额口径不变) */
|
|
|
+ @Test
|
|
|
+ void freeActivity_allFeesZero() {
|
|
|
+ Activity a = new Activity();
|
|
|
+ a.setPrice(0);
|
|
|
+ a.setMemberPrice(0);
|
|
|
+ a.setParentFee(0);
|
|
|
+ a.setParentMemberFee(0);
|
|
|
+ a.setChildFee(0);
|
|
|
+ a.setChildMemberFee(0);
|
|
|
+ assertEquals(0, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, false), "免费活动 self 非会员 0 分");
|
|
|
+ assertEquals(0, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_SELF, true), "免费活动 self 会员 0 分");
|
|
|
+ assertEquals(0, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_CHILD, false), "免费活动 child 0 分");
|
|
|
+ assertEquals(0, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_ELDER, true), "免费活动 elder 0 分");
|
|
|
+ assertEquals(0, ActivityOrderService.resolveFee(a, ActivityOrderService.TYPE_ADULT, false), "免费活动 adult 0 分");
|
|
|
+ }
|
|
|
+}
|