feat(merchant): 未完成微信实名与授权前不允许收款
收款就绪原来只看进件审核通过、结算卡 token、通道账户状态、自动结算与取现能力,
完全没看微信侧——一个微信支付权限没生效、甚至微信业务没开通的商户也会被判成可收款。
用户已明确:未完成实名前不允许商户收款。
- MerchantPaymentReadinessService 补上「微信支付业务开通」与「微信实名与授权」两项,
缺哪项报哪项,不再只回一句「暂时无法收款」。证据取自分阶段进度(由通道查询落库),
不靠请求参数推断——「请求里带了参数」不等于通道侧生效。
- 没有进度记录时两项都算缺:查不到通道证据就不能认为能收款。这与「未完成实名不允许
收款」一致,也意味着存量商户必须先刷新过进度才可能通过判定。
- 测试:四个走下单路径的测试类(PaymentSettlementTests、AppOrderPortalServiceTests、
PaymentFundConfirmationTests、MerchantVerificationServiceTests)的 fixture 里补上进度
记录——它们测的是支付结算与核销,商户建模成已办完微信开通与实名。收款就绪自己的测试
新增两项:实名未确认时拒绝、完全没有证据时拒绝。
验证:全量 216 项,仅 docs/agents/testing.md 所列 3 项基线失败,无新增。
| | |
| | | if (!isMerchantNormal(application.merchantStatus())) { |
| | | return Readiness.blocked("斗拱子商户状态异常(" + application.merchantStatus() + "),暂时无法收款"); |
| | | } |
| | | // 微信支付权限的前置:业务开通生效 + 实名与授权完成。只开通不实名,微信侧并没有把收款 |
| | | // 权限给到商户——实名认证的适用对象就是「需要开通微信支付权限的商户」。证据取自分阶段 |
| | | // 进度(由通道查询落库),不靠请求参数推断:「请求里带了参数」不等于通道侧生效。 |
| | | List<String> missingWechat = missingWechatCapabilities(application.id()); |
| | | if (!missingWechat.isEmpty()) { |
| | | return Readiness.blocked("微信收款能力未就绪:" + String.join("、", missingWechat) + ",暂时无法收款"); |
| | | } |
| | | return Readiness.allowed(new CollectingMerchant(application.id(), application.storeId(), application.huifuId())); |
| | | } |
| | | |
| | |
| | | return huifuPaymentProperties.isMerchantCapabilityRequired(); |
| | | } |
| | | |
| | | /** |
| | | * 分阶段进度里代表微信收款权限的两个阶段,缺哪个报哪个。 |
| | | * |
| | | * <p>没有进度记录时两个都算缺——查不到证据就不能认为能收款。后续配置(延迟入账、自动结算、 |
| | | * 银行卡)另有各自的阶段与快照,这里只管微信侧这两项。 |
| | | */ |
| | | private List<String> missingWechatCapabilities(long applicationId) { |
| | | List<Map<String, Object>> saved = rows("select stages_json from merchant_onboarding_progress where application_id=?", applicationId); |
| | | if (saved.isEmpty()) return List.of("微信支付业务开通", "微信实名与授权"); |
| | | Map<String, Object> stages = com.alibaba.fastjson.JSON.parseObject(clean(saved.get(0).get("stagesJson")), Map.class); |
| | | List<String> missing = new java.util.ArrayList<>(); |
| | | if (!"confirmed".equals(clean(stages.get("wechat")))) missing.add("微信支付业务开通"); |
| | | if (!"confirmed".equals(clean(stages.get("realname")))) missing.add("微信实名与授权"); |
| | | return missing; |
| | | } |
| | | |
| | | private boolean isAutoSettleOff(String value) { return "0".equals(value) || "N".equalsIgnoreCase(value); } |
| | | |
| | | private boolean isCashAvailable(String value) { return "1".equals(value) || "Y".equalsIgnoreCase(value); } |
| | |
| | | """, merchantId, storeId, "hash-readiness-" + suffix, "readiness-card-" + suffix, shortReference("T", suffix)); |
| | | bankCardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class); |
| | | jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where id=?", bankCardId, applicationId); |
| | | confirmWechatCapabilities(); |
| | | } |
| | | |
| | | /** 微信支付业务开通与实名/授权都有通道证据——收款能力的前置。 */ |
| | | private void confirmWechatCapabilities() { |
| | | jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", applicationId); |
| | | } |
| | | |
| | | /** |
| | | * 微信实名未完成时不允许收款:只开通微信业务、不做实名,微信侧并没有把收款权限给到商户。 |
| | | * 缺哪一项要说清楚,不能只回一句「暂时无法收款」。 |
| | | */ |
| | | @Test |
| | | void rejectsANewPaidOrderWhenWechatRealnameIsNotConfirmed() { |
| | | confirmSnapshot("NORMAL", "0", "1"); |
| | | jdbc.update("update merchant_onboarding_progress set stages_json='{\"wechat\":\"confirmed\",\"realname\":\"missing\"}' where application_id=?", applicationId); |
| | | |
| | | assertThatThrownBy(() -> readinessService.requireCollectingMerchant(merchantId, storeId)) |
| | | .isInstanceOf(BusinessException.class) |
| | | .hasMessageContaining("微信实名与授权") |
| | | .hasMessageContaining("微信收款能力未就绪"); |
| | | } |
| | | |
| | | /** 没有进度记录时两项都算缺——查不到通道证据就不能认为能收款。 */ |
| | | @Test |
| | | void rejectsANewPaidOrderWhenThereIsNoCapabilityEvidenceAtAll() { |
| | | confirmSnapshot("NORMAL", "0", "1"); |
| | | jdbc.update("delete from merchant_onboarding_progress where application_id=?", applicationId); |
| | | |
| | | assertThatThrownBy(() -> readinessService.requireCollectingMerchant(merchantId, storeId)) |
| | | .isInstanceOf(BusinessException.class) |
| | | .hasMessageContaining("微信支付业务开通") |
| | | .hasMessageContaining("微信实名与授权"); |
| | | } |
| | | |
| | | @Test |
| | |
| | | values (?, ?, '中国建设银行', '攀枝花东区支行', '510400', '8888', '**** **** **** 8888', ?, 'approved', ?, ?) |
| | | """, merchantId, storeId, "hash-verification-" + suffix, "verification-card-" + suffix, "T" + reference); |
| | | jdbc.update("update merchant_onboarding_application set initial_bank_card_id = (select max(id) from merchant_bank_card) where id = ?", applicationId); |
| | | // 收款就绪要求微信支付业务开通与实名/授权都有通道证据,这里的商户建模成已办完这两步。 |
| | | jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", applicationId); |
| | | jdbc.update(""" |
| | | insert into talent_user(openid, name, mobile, talent_level, status, deleted) |
| | | values (?, ?, ?, ?, 1, 0) |
| | |
| | | """, merchantId, storeId, "hash-delay-" + suffix, "delay-card-" + suffix, "T" + suffix); |
| | | Long cardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class); |
| | | jdbc.update("update merchant_onboarding_application set initial_bank_card_id = ? where id = ?", cardId, onboardingId); |
| | | // 收款就绪要求微信支付业务开通与实名/授权都有通道证据,这里的商户建模成已办完这两步。 |
| | | jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", onboardingId); |
| | | jdbc.update(""" |
| | | insert into merchant_product(merchant_id, product_no, product_type, product_name, original_price, |
| | | sale_price, stock, audit_status, status, deleted) |
| | |
| | | """, merchantId, storeId, "hash-payment-" + suffix, "payment-card-" + suffix, shortReference("T", suffix)); |
| | | Long cardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class); |
| | | jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where id=?", cardId, onboardingId); |
| | | // 收款就绪要求微信支付业务开通与实名/授权都有通道证据。这里的商户建模成已经办完这两步, |
| | | // 否则下单会被「微信收款能力未就绪」挡住——那些是开通流程的测试,不是支付结算的测试。 |
| | | jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", onboardingId); |
| | | jdbc.update(""" |
| | | insert into merchant_product(product_no, merchant_id, product_type, product_name, original_price, sale_price, |
| | | subsidy_amount, stock, audit_status, status, deleted) |
| | |
| | | """, merchant, storeId, "hash-app-order-" + suffix, "app-order-card-" + suffix, shortReference("T", suffix)); |
| | | Long cardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class); |
| | | jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where id=?", cardId, applicationId); |
| | | // 收款就绪要求微信支付业务开通与实名/授权都有通道证据。这里的商户建模成已经办完这两步, |
| | | // 否则下单会被「微信收款能力未就绪」挡住——那些是开通流程的测试,不是下单的测试。 |
| | | jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", applicationId); |
| | | return applicationId; |
| | | } |
| | | |