From 08b9cb246813f41e50cfd395ede372f86458c395 Mon Sep 17 00:00:00 2001 From: WorkBuddy Date: Sat, 19 Sep 2026 19:30:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(p21-2e):=20=E7=BA=A2=E5=A8=98=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=8B=A6=E6=88=AA=E6=8E=A5=E7=BA=BF=EF=BC=88blocker?= =?UTF-8?q?=20=E9=A6=96=E6=AC=A1=E7=94=9F=E6=95=88=20+=20=E7=AB=9E?= =?UTF-8?q?=E6=80=81=E5=85=9C=E5=BA=95=EF=BC=89=EF=BC=9B2d=20=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=20Onboarding=20=E6=9C=AC=E5=9C=B0=E8=B5=84=E6=A0=BC?= =?UTF-8?q?=E5=B7=B2=E5=AE=9E=E7=8E=B0=EF=BC=9B=E6=96=87=E6=A1=A3=E5=9B=9E?= =?UTF-8?q?=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P16 遗留 2e(红娘服务拦截): - checkCanWithdraw 硬编码 0 → 接 MatchmakerServiceMapper.countActiveByUserId(非终态口径 submitted/assigned/in_service/waiting_user,与 findActiveByOperatorAndUser 一致),blocker 首次真正生效 - notifyMatchmaker TODO 空转 → terminateActiveMatchmakerServices 竞态兜底(残留非终态置 cancelled); 不发通知:Phase 18 D2 的 notifications.user_id→users(id) FK 教训 - 守卫 MatchmakerWithdrawBlockerGuardTest 4 用例全绿;首跑暴露测试基建坑:@Transactional Service 是 CGLIB 代理,ReflectionTestUtils 直调私有方法拿到的是代理实例的 null 字段 → 须先 AopTestUtils.getTargetObject 解包(已在测试内注释说明) - 另发现历史遗留:countInProgressByOperator 使用白名单外的 'processing'(已登记) P04-T10:复核确认 Onboarding 本地资格步骤早已实现(onboarding 消息/动作 + 路由 + /me/status 字段), JIRA ⏳ → ✅;清除首页语义错位的死状态 isVerified(miniapp tsc EXIT=0) 文档:JIRA 收尾行/计划文档 Task 3.1/3.3 勾选 --- .../service/AccountLifecycleService.java | 41 ++++-- .../mapper/MatchmakerServiceMapper.java | 10 ++ .../MatchmakerWithdrawBlockerGuardTest.java | 129 ++++++++++++++++++ docs/JIRA_BACKLOG.md | 8 +- ...9-18-phase23-closeout-and-uat-readiness.md | 21 +-- miniapp/pages/index/index.ts | 7 +- 6 files changed, 194 insertions(+), 22 deletions(-) create mode 100644 backend/src/test/java/com/danyang/match/platform/account/MatchmakerWithdrawBlockerGuardTest.java diff --git a/backend/src/main/java/com/danyang/match/platform/account/service/AccountLifecycleService.java b/backend/src/main/java/com/danyang/match/platform/account/service/AccountLifecycleService.java index dbed074..9274a50 100644 --- a/backend/src/main/java/com/danyang/match/platform/account/service/AccountLifecycleService.java +++ b/backend/src/main/java/com/danyang/match/platform/account/service/AccountLifecycleService.java @@ -85,6 +85,7 @@ public class AccountLifecycleService { private final ChatService chatService; private final UserIdentityMapper userIdentityMapper; private final CounselingFollowupMapper counselingFollowupMapper; + private final com.danyang.match.platform.matchmaker.mapper.MatchmakerServiceMapper matchmakerServiceMapper; // ==================== 退出婚恋服务(暂停推荐) ==================== @@ -166,8 +167,9 @@ public class AccountLifecycleService { Map blockers = new HashMap<>(); - // 检查是否有进行中的红娘服务 - long activeMatchmakerServices = 0; // 需要 MatchmakerServiceMapper + // 检查是否有进行中的红娘服务(2e 接线:原硬编码 0,blocker 从未生效)。 + // 状态口径 = 生产 chk_matchmaker_status 白名单的非终态值(submitted/assigned/in_service/waiting_user)。 + long activeMatchmakerServices = matchmakerServiceMapper.countActiveByUserId(userId); if (activeMatchmakerServices > 0) { blockers.put("matchmakerService", "您有进行中的红娘服务,请先完成或取消"); } @@ -270,8 +272,8 @@ public class AccountLifecycleService { // 2b. 通知所有活跃 Match 对方 notifyActiveMatches(user.getId(), "account_closed"); - // 2c. 通知红娘(如果有) - notifyMatchmaker(user.getId()); + // 2c. 终止残留红娘服务(竞态兜底,见方法注释;不能向 operator 发通知——Phase 18 D2 FK 教训) + terminateActiveMatchmakerServices(user.getId()); // 2d. 取消所有未开始的活动报名 cancelPendingActivityRegistrations(userId); @@ -425,11 +427,34 @@ public class AccountLifecycleService { } /** - * 通知红娘服务已终止 + * 注销收尾(2e 接线,原 {@code notifyMatchmaker} 为 TODO 空转):把该用户残留的非终态 + * 红娘服务置为 cancelled。 + * + *

正常路径下 {@code checkCanWithdraw} 的 blocker 会要求用户先完成/取消红娘服务, + * 走到注销时不应再有非终态记录 —— 本方法是竞态兜底(blocker 检查与注销事务之间, + * 红娘管理员恰好受理了新服务)。 + * + *

为什么不是「发通知」:Phase 18 D2 的教训 —— {@code notifications.user_id → users(id)} + * 外键下,operator 不在 users 表,向红娘发用户通知在真实 PG 上必然使注销事务回滚。 + * 因此以状态收尾 + 日志代替「通知」,效果更实质且不会炸注销事务。 */ - private void notifyMatchmaker(String userId) { - // TODO: 查询该用户的红娘服务,通知相关红娘 - log.info("通知红娘服务终止: userId={}", userId); + private void terminateActiveMatchmakerServices(String userId) { + List active = + matchmakerServiceMapper.selectList( + new LambdaQueryWrapper() + .eq(com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord::getUserId, userId) + .in(com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord::getStatus, + Constants.MATCHMAKER_STATUS_SUBMITTED, + Constants.MATCHMAKER_STATUS_ASSIGNED, + Constants.MATCHMAKER_STATUS_IN_SERVICE, + Constants.MATCHMAKER_STATUS_WAITING_USER) + .isNull(com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord::getDeletedAt)); + for (com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord svc : active) { + svc.setStatus(Constants.MATCHMAKER_STATUS_CANCELLED); + svc.setUpdatedAt(Instant.now()); + matchmakerServiceMapper.updateById(svc); + log.info("注销收尾: 残留红娘服务已终止 userId={}, serviceId={}", userId, svc.getId()); + } } /** diff --git a/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerServiceMapper.java b/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerServiceMapper.java index ae73778..2be32a3 100644 --- a/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerServiceMapper.java +++ b/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerServiceMapper.java @@ -51,4 +51,14 @@ public interface MatchmakerServiceMapper extends BaseMapper findActiveByOperatorAndUser(@Param("operatorId") String operatorId, @Param("userId") String userId); + + /** + * 注销前检查(P16 收尾 2e):该用户仍处于非终态的红娘服务数。 + * 状态口径与 {@link #findActiveByOperatorAndUser} 一致(submitted/assigned/in_service/waiting_user)。 + * ⚠️ 勿用 {@code Constants.MATCHMAKER_STATUS_PENDING} / {@code IN_PROGRESS} —— + * 这两个常量不在生产 chk_matchmaker_status 白名单(V009),该表永不出现这两个值。 + */ + @Select("SELECT COUNT(*) FROM matchmaker_services WHERE user_id = #{userId} " + + "AND status IN ('submitted','assigned','in_service','waiting_user') AND deleted_at IS NULL") + long countActiveByUserId(@Param("userId") String userId); } diff --git a/backend/src/test/java/com/danyang/match/platform/account/MatchmakerWithdrawBlockerGuardTest.java b/backend/src/test/java/com/danyang/match/platform/account/MatchmakerWithdrawBlockerGuardTest.java new file mode 100644 index 0000000..9a849ad --- /dev/null +++ b/backend/src/test/java/com/danyang/match/platform/account/MatchmakerWithdrawBlockerGuardTest.java @@ -0,0 +1,129 @@ +package com.danyang.match.platform.account; + +import com.danyang.match.platform.account.service.AccountLifecycleService; +import com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord; +import com.danyang.match.platform.matchmaker.mapper.MatchmakerServiceMapper; +import com.danyang.match.platform.shared.constant.Constants; +import com.danyang.match.platform.user.entity.User; +import com.danyang.match.platform.user.mapper.UserMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.AopTestUtils; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 2e 守卫(Phase 21 遗留清偿):注销前红娘服务拦截 + 注销竞态兜底。 + * + *

缺陷本体

+ * {@code checkCanWithdraw} 曾把进行中红娘服务数**硬编码为 0** —— blocker 从未生效, + * 有进行中红娘服务的用户可以直接注销,红娘工作台留下永不终结的孤儿服务。 + * 原因注释写「需要 MatchmakerServiceMapper」,但该 Mapper 与 {@code matchmaker_services} 表 + * (V009)其实一直都在 —— 原 STATUS「数据模型无该表」的判定已被实测推翻。 + * + *

覆盖

+ *
    + *
  1. blocker 接线:有非终态红娘服务(in_service)→ 注销被拦;
  2. + *
  3. 无红娘服务 → 不产生该 blocker;
  4. + *
  5. 竞态兜底:{@code terminateActiveMatchmakerServices} 把残留非终态服务置为 cancelled + * (方法为 private,用 ReflectionTestUtils 直调,与 {@code AuthServiceAccountClosedGuardTest} + * 同模式 —— 注销主流程被 blocker 挡住,正常路径到不了这里)。
  6. + *
+ */ +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class MatchmakerWithdrawBlockerGuardTest { + + @Autowired private AccountLifecycleService accountLifecycleService; + @Autowired private UserMapper userMapper; + @Autowired private MatchmakerServiceMapper matchmakerServiceMapper; + + private User newUser() { + User user = new User(); + user.setId(UUID.randomUUID().toString()); + user.setWechatOpenid("openid-" + UUID.randomUUID()); + user.setAccountStatus("normal"); + userMapper.insert(user); + return user; + } + + private MatchmakerServiceRecord insertService(String userId, String status) { + MatchmakerServiceRecord svc = new MatchmakerServiceRecord(); + svc.setId(UUID.randomUUID().toString()); + svc.setUserId(userId); + svc.setServiceType("recommend_partner"); + svc.setStatus(status); + matchmakerServiceMapper.insert(svc); + return svc; + } + + @Test + @DisplayName("blocker 接线:有进行中红娘服务的用户注销被拦(原硬编码 0,拦截从未生效)") + void activeMatchmakerServiceBlocksWithdraw() { + User user = newUser(); + insertService(user.getId(), Constants.MATCHMAKER_STATUS_IN_SERVICE); + + Map result = accountLifecycleService.checkCanWithdraw(user.getId()); + @SuppressWarnings("unchecked") + Map blockers = (Map) result.get("blockers"); + + assertTrue(blockers.containsKey("matchmakerService"), + "进行中红娘服务必须成为注销 blocker(P16 遗留 2e 缺陷本体)"); + assertEquals(false, result.get("canWithdraw")); + } + + @Test + @DisplayName("无红娘服务的用户:不产生 matchmakerService blocker") + void noMatchmakerServiceNoBlocker() { + User user = newUser(); + + Map result = accountLifecycleService.checkCanWithdraw(user.getId()); + @SuppressWarnings("unchecked") + Map blockers = (Map) result.get("blockers"); + + assertFalse(blockers.containsKey("matchmakerService"), + "无红娘服务时不得误报该 blocker"); + } + + @Test + @DisplayName("已完成的红娘服务不拦截注销(终态不阻塞)") + void completedServiceDoesNotBlock() { + User user = newUser(); + insertService(user.getId(), Constants.MATCHMAKER_STATUS_COMPLETED); + + Map result = accountLifecycleService.checkCanWithdraw(user.getId()); + @SuppressWarnings("unchecked") + Map blockers = (Map) result.get("blockers"); + + assertFalse(blockers.containsKey("matchmakerService"), + "终态(completed)红娘服务不应拦截注销"); + } + + @Test + @DisplayName("竞态兜底:terminateActiveMatchmakerServices 把残留非终态服务置为 cancelled") + void terminateResidualServicesOnClose() { + User user = newUser(); + MatchmakerServiceRecord svc = insertService(user.getId(), Constants.MATCHMAKER_STATUS_ASSIGNED); + + // ⚠️ 必须先解包 CGLIB 代理再反射调私有方法:该 Service 含 @Transactional 方法, + // 注入的是代理实例 —— 代理对象自身字段全为 null(委托给 target),绕过拦截器直调 + // 私有方法会拿到 null 字段 NPE。直接调代理上的 public 方法(checkCanWithdraw) + // 走拦截器委托 target,不受影响 —— 这也是本守卫首跑暴露的测试基建坑。 + Object target = AopTestUtils.getTargetObject(accountLifecycleService); + ReflectionTestUtils.invokeMethod(target, + "terminateActiveMatchmakerServices", user.getId()); + + MatchmakerServiceRecord after = matchmakerServiceMapper.selectById(svc.getId()); + assertEquals(Constants.MATCHMAKER_STATUS_CANCELLED, after.getStatus(), + "注销收尾必须终止残留的非终态红娘服务(原 notifyMatchmaker 为 TODO 空转)"); + } +} diff --git a/docs/JIRA_BACKLOG.md b/docs/JIRA_BACKLOG.md index 6100827..b9fde0b 100644 --- a/docs/JIRA_BACKLOG.md +++ b/docs/JIRA_BACKLOG.md @@ -187,7 +187,7 @@ | UI-CLEANUP-20260913 | 已完成 | 废弃 UI 代码、冲突规范、旧计划和无效验收证据已清理;`ui-document-baseline-contracts` 锁定唯一视觉入口 | | UI-ADMIN-20260913 | 已完成(待桌面端目视复验) | 管理端全局框架与工作台已按规格交付;18 modules + 22 routes,vue-tsc 与 vite build 全绿 | | UI-MINIAPP-20260913 | 已完成 | 小程序 11 个核心页面已重构;11/11 页 TS-only,2026-09-16 通过用户验收 | -| 批 2/3 收尾(2026-09-19) | 进行中 | E2E 互选 8 + 预约 8 场景真实实现(本机 65 passed)+ INV-011 显式守卫完成;隐私设置接真实落库(V047)、5 死开关 + 2 私有桶死配置移除(产品裁定)、`PUT /feature-flags` 真持久化(V048)、5 个无 SQL Mapper 方法补齐——均已处置;剩余:红娘服务拦截接线(原「无表」判定已推翻,V009 已建表)、数据保留流程(待产品决策)、Task 3.3 四项(P16-T13 / P13A-T31 / P13A-T33 / P04-T10)、CI 全绿实测(需 GitHub Actions 环境) | +| 批 2/3 收尾(2026-09-19) | 基本完成 | E2E 互选 8 + 预约 8 场景真实实现(本机 65 passed)+ INV-011 显式守卫;隐私设置接真实落库(V047)、5 死开关 + 2 死配置移除(产品裁定)、`PUT /feature-flags` 真持久化(V048)、5 个无 SQL Mapper 方法补齐;**Task 3.3 四项 + 红娘服务拦截接线全部完成**(互选 `expires_at` 写入接线、随访审计 `recordSensitiveAccess`、随访正文风险检测、Onboarding 本地资格确认已实现并回写;`checkCanWithdraw` 红娘 blocker 首次生效 + `terminateActiveMatchmakerServices` 竞态兜底);**仅余**:数据保留流程(待产品决策)、CI 全绿实测(需 GitHub Actions 环境)、E2E 场景对账表首项之外的 CI 复跑 | | V1.1 UAT / RC / Production | 进行中 | `/me/*` 契约已收敛(2026-09-16);当前停在 Phase 24(UAT)启动门前 —— 启动需用户确认,前置收尾见 `docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md` | --- @@ -305,7 +305,11 @@ ### Miniapp - P04-V11-T08 ✅ U62 本地资格说明页 - P04-V11-T09 ✅ U63 本地资格补充核验页 -- P04-V11-T10 ⏳ Onboarding增加本地资格步骤(后续Phase) +- P04-V11-T10 ✅ Onboarding 本地资格步骤 —— **2026-09-19 复核确认已实现**:首页 onboarding 消息/动作覆盖 + `not_declared` / `needs_evidence`,动作「声明本地资格」路由至 `/pages/local-eligibility/local-eligibility` + (`pages/index/index.ts` buildOnboardingMessage / buildOnboardingActions / handleQuickAction), + `eligStatus` 取自 `/me/status` 的 `localEligibilityStatus`(该字段断言守卫见 `MeStatusLocalEligibilityGuardTest`); + 同轮清除语义错位的死状态 `isVerified`(WXML/WXSS 零引用,实名判定是 `userState.verified`) ### MeStatus - P04-V11-T11 ✅ /me/status增加localEligibilityStatus diff --git a/docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md b/docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md index d536d74..fa0292c 100644 --- a/docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md +++ b/docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md @@ -343,16 +343,18 @@ - 负向注入:把桩退回「不落库」→ **恰 2 红**(落库用例 + 类型校验用例), 其余 6 个正确未红。已还原。 - 至此小程序隐私设置**真正可保存**,① ② 两层均已关闭。 -- [ ] **注销红娘服务拦截硬编码 `0`**: +- [x] **注销红娘服务拦截硬编码 `0`** → **2026-09-19 已接线清偿**(守卫 `MatchmakerWithdrawBlockerGuardTest` 4 用例): 原文断言「数据模型**没有 `matchmaker_services` 表**(全仓搜索为空),建表属扩权,需产品先确认」—— **三条均与实况不符,2026-09-19 实测推翻**: ① 表**存在**,由 `V009__add_matchmaker_tables.sql` 创建(`chk_matchmaker_status` 白名单: `submitted / assigned / in_service / waiting_user / completed / cancelled`); ② `MatchmakerServiceMapper`、`MatchmakerServiceRecord`、`MatchmakerServiceService` **均已存在**; ③ 因此**不需要扩权、不需要产品裁定**,属可直接实现的接线缺陷。 - 另:`AccountLifecycleService.notifyMatchmaker`(注销时通知红娘服务)同样是 TODO 空转。 - 位置:`AccountLifecycleService.checkCanWithdraw` 中 - `long activeMatchmakerServices = 0; // 需要 MatchmakerServiceMapper`。 + 修复:`MatchmakerServiceMapper` 新增 `countActiveByUserId`(非终态口径与 `findActiveByOperatorAndUser` 一致), + `checkCanWithdraw` 接真实计数(blocker 首次真正生效);原 `notifyMatchmaker` TODO 空转改为 + `terminateActiveMatchmakerServices`(竞态兜底:残留非终态服务置 cancelled;**不发通知**—— + Phase 18 D2 的 `notifications.user_id→users(id)` FK 教训)。 + 另发现历史遗留:`countInProgressByOperator` 用了白名单外的 `'processing'`(已登记,另行处置)。 (原文「全仓搜索为空」很可能是只在 `db/migration/` 内搜了表名的另一种写法 —— 该表在 `demo-seed-*.sql` 与 `V009` 中都有;凡「全仓无 X」这类断言必须给出**搜索命令与范围**, 否则无法复核,本项目已多次因此产生失实结论。) @@ -402,12 +404,13 @@ `startupLoadHookIsDeclared`),`FeatureFlagControllerTest` 7 例不受影响; 还原后 md5 逐字节一致、`NEG-VAL` 残留 0。 -### Task 3.3: 任务级 ⏳ 清偿 +### Task 3.3: 任务级 ⏳ 清偿 ✅(2026-09-19 全部关闭) -- [ ] **P16-T13**:互选超时接线(`expires_at` 已由 V041 提供)。 -- [ ] **P13A-T31**:随访审计(`counseling_followups` 访问需审计)。 -- [ ] **P13A-T33**:`RiskEvent source=counseling` 来源扩展。 -- [ ] **P04-T10**:Onboarding 本地资格步骤。 +- [x] **P16-T13**:互选超时接线 → **缺陷本体**:`expires_at` 全仓无人写入(48h 配置在、confirm 过滤在,唯独创建不赋值 → 过滤恒真)。修复:创建时写 `expires_at` = 活动结束时间 + `ActivityConfig.mutualDeadlineHours`(已确认口径「截止后48小时」),缺失回退创建时刻。守卫 2 用例,负向验证(摘掉写入)恰 2 红。 +- [x] **P13A-T31**:随访审计 → `getDetailForCounselor` 接线 `recordSensitiveAccess(counseling_followup)`;本人/manager 摘要视图不审计。守卫含两个不审计分支断言。 +- [x] **P13A-T33**:`RiskEvent source=counseling` 来源扩展 → `detectCounselingRisk` 此前只扫申请摘要;`addFollowup` 正文接入同一检测(INV-013 加固:不记正文只记级别)。守卫含正常内容不误报断言。 + 守卫:`CounselingFollowupAuditAndRiskGuardTest` 4 用例(真实落库回查),全绿。 +- [x] **P04-T10**:Onboarding 本地资格步骤 → **复核确认早已实现**(首页 onboarding 消息/动作覆盖 `not_declared`/`needs_evidence` 并路由本地资格页,`eligStatus` 取自 `/me/status.localEligibilityStatus`);JIRA 行滞后已回写 ✅;同轮清除语义错位的死状态 `isVerified`(`miniapp tsc` EXIT=0)。 --- diff --git a/miniapp/pages/index/index.ts b/miniapp/pages/index/index.ts index bfded39..d6a715b 100644 --- a/miniapp/pages/index/index.ts +++ b/miniapp/pages/index/index.ts @@ -34,7 +34,6 @@ Page({ loadError: false, userState: null, isOnboarding: false, - isVerified: false, isPaused: false, isSuccessful: false, onboardingMessage: '', @@ -112,7 +111,9 @@ Page({ const isOnboarding = !userState || !userState.verified || (profileStatus !== 'approved' && profileStatus !== 'completed') const isPaused = relStatus === 'paused' const isSuccessful = relStatus === 'matched_successfully' - const isVerified = eligStatus === 'verified' + // 注意:本地资格核验通过与否由 checks 里的「本地资格」项表达(eligStatus === 'verified')。 + // 此前曾有一个 isVerified = eligStatus === 'verified' 的死状态 —— 名字像「实名」实为本地资格, + // WXML/WXSS 零引用且语义错位(实名判定是 userState.verified),已移除以防未来误用。 const checks = [ { label: '已实名', done: userState?.verified || false }, @@ -123,7 +124,7 @@ Page({ this.setData({ loading: false, loadError: false, userState, - isOnboarding, isVerified, isPaused, isSuccessful, + isOnboarding, isPaused, isSuccessful, onboardingMessage: this.buildOnboardingMessage(userState, profileStatus, eligStatus), onboardingActions: this.buildOnboardingActions(userState, profileStatus, eligStatus), statusChecks: checks,