| | |
| | | List<Map<String,Object>> records=rows("select * from merchant_onboarding_application where id=? and initial_bank_card_id is not null",applicationId); |
| | | if(records.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND,"Onboarding application not found"); |
| | | String applicationNo=clean(records.get(0).get("applicationNo")); |
| | | if (!applicationNo.isBlank()) continuePostOnboardingProvisioning(applicationNo); |
| | | // 后台纳管的进件(source=managed)只查询不下发:业务开通与微信配置都是下发类操作,而外部商户的 |
| | | // 口径已由业务要求保持一致,纳管只需核实;连锁多店共用同一个汇付商户号时各自下发还会重复下发。 |
| | | // 判定只认 MerchantOnboardingProgressService.isManaged 那一处定义。 |
| | | if (!applicationNo.isBlank() && !MerchantOnboardingProgressService.isManaged(records.get(0))) continuePostOnboardingProvisioning(applicationNo); |
| | | return onboardingProgressService.refresh(applicationId,retry); |
| | | } |
| | | |
| | |
| | | requireCertificatePeriod(request.subjectCertificateValidityType(), request.subjectCertificateBeginDate(), request.subjectCertificateEndDate(), "Identity document"); |
| | | if (blank(request.legalCertificateAddress())) throw new BusinessException(ErrorCode.BAD_REQUEST, enterprise ? "请输入法人证件地址" : "请输入负责人证件地址"); |
| | | if (blank(request.administratorEmail()) || !request.administratorEmail().trim().matches("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入正确的联系人邮箱"); |
| | | // 线下报备的经营场所材料是一组**三张**:门头照(F22)、内景照(F24)、收银台(F105)。 |
| | | // 通道的「商户材料校验服务」在进件这一步就按 F-code 逐张核对,缺一张整笔驳回——不是等到 |
| | | // 业务开通或实名才要,所以必须在这里拦住。**企业(含个体工商户)主体同样要交**:线下按 |
| | | // 「有实体经营场所」报备,通道不区分主体类型向门店索要这三张(2026-09-29 实测驳回原文: |
| | | // 「缺失图片:F22-店铺门头照图片;F24-店铺内景图片;F105-店铺收银台」)。 |
| | | if (blank(request.storeHeaderUrl()) || blank(request.storeIndoorUrl()) || blank(request.storeCashierUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传店铺门头照、店内环境照与收银台照片,线下报备需要这三张"); |
| | | if (enterprise) { |
| | | requireCertificatePeriod(request.licenseValidityType(), request.licenseBeginDate(), request.licenseEndDate(), "Business licence"); |
| | | if (blank(request.registeredProvinceCode()) || blank(request.registeredCityCode()) || blank(request.registeredDistrictCode())) throw new BusinessException(ErrorCode.BAD_REQUEST, "Enterprise registration address codes are required"); |
| | |
| | | normalizeRegisteredCapital(request.companyType(), request.registeredCapital()); |
| | | } else { |
| | | if (blank(request.registeredDistrictCode()) || blank(request.registeredAddress()) || blank(request.legalIdFrontUrl()) || blank(request.legalIdBackUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请补全个人商户进件资料"); |
| | | // 汇付对个人(小微)商户的微信业务开通与微信实名认证都要门店照片(门头照 F22 + 内景照 F24)。 |
| | | // 放在提交时校验:这两张图是拿到微信收款能力的硬条件,缺了也能把进件提上去, |
| | | // 但后面微信开通和实名一定被通道驳回,那时商户得把整份进件重走一遍。 |
| | | if (blank(request.storeHeaderUrl()) || blank(request.storeIndoorUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传店铺照片与店内环境照,微信支付开通与实名认证都需要"); |
| | | // 经营详细地址(进件的 detail_addr)。线下按「有实体经营场所」口径报备后它是通道必填项, |
| | | // 而证件地址(legal_addr)语义不同、不能顶替——两者原先共用同一个值,这里必须分开采集。 |
| | | if (blank(request.businessAddress())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请填写经营详细地址"); |
| | | } |
| | | boolean privateSettlement = !enterprise || "个体工商户".equals(clean(request.companyType())); |
| | | boolean privateSettlement = isPrivateSettlement(request.subjectType(), request.companyType()); |
| | | if (privateSettlement && blank(request.settlementCardFrontUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传结算银行卡卡号面"); |
| | | if (!privateSettlement && blank(request.registrationAccountUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传开户许可证"); |
| | | // 结算凭证图一列两义:对私存银行卡卡号面、对公存开户许可证(见 V143 的注释), |
| | | // 所以落库前先按主体挑出这一次要留存的那张。挑错了这一格就是空的。 |
| | | String settlementProofUrl = privateSettlement ? request.settlementCardFrontUrl() : request.registrationAccountUrl(); |
| | | List<Map<String, Object>> branchRows = rows("select clearing_code, branch_name, bank_name, head_bank_code from bank_branch_directory where clearing_code = ? limit 1", request.bankBranchCode().trim()); |
| | | if (branchRows.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择真实开户行"); |
| | | Map<String, Object> branch = branchRows.get(0); |
| | |
| | | String no = BusinessIdGenerator.next("MO"); |
| | | String certificateValidityType = certificateValidityType(request.subjectCertificateValidityType()); |
| | | String certificateBeginDate = certificateBeginDate(request.subjectCertificateBeginDate()); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name, credit_code, license_validity_type, license_begin_date, license_end_date, subject_id_no_masked, subject_id_no_encrypted, subject_cert_validity_type, subject_cert_begin_date, subject_cert_end_date, registered_region, registered_province_code, registered_city_code, registered_district_code, registered_address, merchant_name, merchant_short_name, company_type, business_license_url, established_date, registered_capital, legal_id_front_url, legal_id_back_url, settlement_card_front_url, legal_certificate_address, legal_mobile, administrator_email, contact_name, contact_mobile, store_header_url, store_indoor_url, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), no, request.subjectType(), request.subjectName().trim(), clean(request.creditCode()), certificateValidityType(request.licenseValidityType()), certificateBeginDate(request.licenseBeginDate()), certificateEndDate(request.licenseEndDate()), maskId(request.subjectIdNumber()), sensitiveDataCrypto.encrypt(clean(request.subjectIdNumber()).toUpperCase()), certificateValidityType, certificateBeginDate, certificateEndDate(request.subjectCertificateEndDate()), clean(request.registeredRegion()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), clean(request.registeredAddress()), clean(request.merchantName()), clean(request.merchantShortName()), clean(request.companyType()), clean(request.businessLicenseUrl()), clean(request.establishedDate()), clean(request.registeredCapital()), clean(request.legalIdFrontUrl()), clean(request.legalIdBackUrl()), clean(request.settlementCardFrontUrl()), clean(request.legalCertificateAddress()), clean(request.legalMobile()), clean(request.administratorEmail()), request.contactName().trim(), request.contactMobile().trim(), clean(request.storeHeaderUrl()), clean(request.storeIndoorUrl()), key.trim()); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name, credit_code, license_validity_type, license_begin_date, license_end_date, subject_id_no_masked, subject_id_no_encrypted, subject_cert_validity_type, subject_cert_begin_date, subject_cert_end_date, registered_region, registered_province_code, registered_city_code, registered_district_code, registered_address, merchant_name, merchant_short_name, company_type, business_license_url, established_date, registered_capital, legal_id_front_url, legal_id_back_url, settlement_card_front_url, legal_certificate_address, legal_mobile, administrator_email, contact_name, contact_mobile, store_header_url, store_indoor_url, store_cashier_url, business_address, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), no, request.subjectType(), request.subjectName().trim(), clean(request.creditCode()), certificateValidityType(request.licenseValidityType()), certificateBeginDate(request.licenseBeginDate()), certificateEndDate(request.licenseEndDate()), maskId(request.subjectIdNumber()), sensitiveDataCrypto.encrypt(clean(request.subjectIdNumber()).toUpperCase()), certificateValidityType, certificateBeginDate, certificateEndDate(request.subjectCertificateEndDate()), clean(request.registeredRegion()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), clean(request.registeredAddress()), clean(request.merchantName()), clean(request.merchantShortName()), clean(request.companyType()), clean(request.businessLicenseUrl()), clean(request.establishedDate()), clean(request.registeredCapital()), clean(request.legalIdFrontUrl()), clean(request.legalIdBackUrl()), clean(settlementProofUrl), clean(request.legalCertificateAddress()), clean(request.legalMobile()), clean(request.administratorEmail()), request.contactName().trim(), request.contactMobile().trim(), clean(request.storeHeaderUrl()), clean(request.storeIndoorUrl()), clean(request.storeCashierUrl()), clean(request.businessAddress()), key.trim()); |
| | | String cardNumber = request.bankCardNumber().trim(); |
| | | String cardKey = key.trim() + ":initial"; |
| | | if (cardKey.length() > 128) cardKey = cardKey.substring(0, 128); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 存量个人进件缺门店照片、或缺结算银行卡卡号面时,允许商户在重新提交里补上。 |
| | | * 存量进件缺门店照片、或缺结算银行卡卡号面时,允许商户在重新提交里补上。 |
| | | * |
| | | * <p>门店照片是个人(小微)主体开通微信支付与发起微信实名的硬材料,而分阶段模型下重复提交 |
| | | * <p>门店照片是开通微信支付与发起微信实名的硬材料,而分阶段模型下重复提交 |
| | | * **不会新建进件**(也就不会在通道上多出一家商户),商户端因此没有别的补图入口——真实开发库里 |
| | | * 已经有拿到汇付商户号的个人进件,就是这个形态。 |
| | | * 已经有拿到汇付商户号的进件,就是这个形态。三张图(门头照/内景照/收银台)**不分主体类型**都补: |
| | | * 线下报备下企业主体同样被通道索要这三张。 |
| | | * |
| | | * <p>结算卡卡号面在这一列补进 INSERT 之前**从来没落过库**(2026-09-18 工单),历史记录全是空值, |
| | | * 库里也没有别的地方能还原它。所以这里一并补上——这是已提交进件唯一的补救入口。 |
| | |
| | | private void supplyMissingOnboardingMaterials(Map<String, Object> application, MerchantOnboardingRequest request) { |
| | | if (request == null) return; |
| | | Object id = application.get("id"); |
| | | // 门店照片只有个人主体才有,企业主体不提交这两项,沿用原来的判定。 |
| | | if ("individual".equalsIgnoreCase(clean(application.get("subjectType")))) { |
| | | if (clean(application.get("storeHeaderUrl")).isBlank() && !blank(request.storeHeaderUrl())) { |
| | | jdbc.update("update merchant_onboarding_application set store_header_url=? where id=?", request.storeHeaderUrl().trim(), id); |
| | | } |
| | | if (clean(application.get("storeIndoorUrl")).isBlank() && !blank(request.storeIndoorUrl())) { |
| | | jdbc.update("update merchant_onboarding_application set store_indoor_url=? where id=?", request.storeIndoorUrl().trim(), id); |
| | | } |
| | | if (clean(application.get("storeHeaderUrl")).isBlank() && !blank(request.storeHeaderUrl())) { |
| | | jdbc.update("update merchant_onboarding_application set store_header_url=? where id=?", request.storeHeaderUrl().trim(), id); |
| | | } |
| | | // 这里不另判主体类型:企业主体(非个体工商户)客户端本来就把这一项传空串,blank 会挡住。 |
| | | if (clean(application.get("settlementCardFrontUrl")).isBlank() && !blank(request.settlementCardFrontUrl())) { |
| | | jdbc.update("update merchant_onboarding_application set settlement_card_front_url=? where id=?", request.settlementCardFrontUrl().trim(), id); |
| | | if (clean(application.get("storeIndoorUrl")).isBlank() && !blank(request.storeIndoorUrl())) { |
| | | jdbc.update("update merchant_onboarding_application set store_indoor_url=? where id=?", request.storeIndoorUrl().trim(), id); |
| | | } |
| | | if (clean(application.get("storeCashierUrl")).isBlank() && !blank(request.storeCashierUrl())) { |
| | | jdbc.update("update merchant_onboarding_application set store_cashier_url=? where id=?", request.storeCashierUrl().trim(), id); |
| | | } |
| | | // 结算凭证图一列两义,补齐时同样要按主体挑对那一张:对私取卡号面、对公取开户许可证。 |
| | | // 主体按**库里那条进件**判,不按本次请求——进件已经定性了,不能因为重填了表单就换一张图。 |
| | | String proofUrl = isPrivateSettlement(application.get("subjectType"), application.get("companyType")) |
| | | ? request.settlementCardFrontUrl() : request.registrationAccountUrl(); |
| | | if (clean(application.get("settlementCardFrontUrl")).isBlank() && !blank(proofUrl)) { |
| | | jdbc.update("update merchant_onboarding_application set settlement_card_front_url=? where id=?", proofUrl.trim(), id); |
| | | } |
| | | } |
| | | |
| | |
| | | String settlementCardPictureId = privateSettlement ? pictureUploadGateway.uploadUrl(request.settlementCardFrontUrl(), "F13") : ""; |
| | | String settlementCertFrontPictureId = privateSettlement ? pictureUploadGateway.uploadUrl(request.legalIdFrontUrl(), "F55") : ""; |
| | | String settlementCertBackPictureId = privateSettlement ? pictureUploadGateway.uploadUrl(request.legalIdBackUrl(), "F56") : ""; |
| | | // 线下经营的门店材料三张(门头照 F22、内景照 F24、收银台 F105):进件这一步就要带上, |
| | | // 通道的材料校验服务逐张按 F-code 核对,缺一张整笔驳回。企业主体原先一张都不发。 |
| | | String storeHeaderPictureId = pictureUploadGateway.uploadUrl(request.storeHeaderUrl(), "F22"); |
| | | String storeIndoorPictureId = pictureUploadGateway.uploadUrl(request.storeIndoorUrl(), "F24"); |
| | | String storeCashierPictureId = pictureUploadGateway.uploadUrl(request.storeCashierUrl(), "F105"); |
| | | String shortName = blank(request.merchantShortName()) ? request.merchantName().trim() : request.merchantShortName().trim(); |
| | | HuifuEnterpriseOnboardingGateway.Result result = enterpriseOnboardingGateway.create(new HuifuEnterpriseOnboardingCommand( |
| | | applicationNo, clean(request.merchantName()), shortName, request.creditCode().trim(), |
| | |
| | | privateSettlement ? "1" : "0", privateSettlement ? request.subjectName().trim() : request.merchantName().trim(), request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), clean(branch.get("clearingCode")), clean(branch.get("branchName")), |
| | | privateSettlement ? request.subjectIdNumber().trim().toUpperCase() : "", privateSettlement ? certificateValidityType(request.subjectCertificateValidityType()) : "", privateSettlement ? certificateBeginDate(request.subjectCertificateBeginDate()) : "", privateSettlement ? certificateEndDate(request.subjectCertificateEndDate()) : "", privateSettlement ? request.contactMobile().trim() : "", |
| | | registrationAccountPictureId, settlementCardPictureId, settlementCertFrontPictureId, settlementCertBackPictureId, |
| | | certificateBeginDate(request.establishedDate()), normalizeRegisteredCapital(request.companyType(), request.registeredCapital()))); |
| | | certificateBeginDate(request.establishedDate()), normalizeRegisteredCapital(request.companyType(), request.registeredCapital()), |
| | | storeHeaderPictureId, storeIndoorPictureId, storeCashierPictureId)); |
| | | applyOnboardingResult(applicationNo, bankCardId, result.accepted(), result.huifuId(), result.applyNo(), result.tokenNo(), result.code(), result.message()); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu submission outcome is pending confirmation", applicationNo); |
| | |
| | | String settlementCardPictureId = pictureUploadGateway.uploadUrl(request.settlementCardFrontUrl(), "F13"); |
| | | // 店铺照片:个人(小微)商户进件的必填项,三种经营类型都要传,文件类型固定 F22。 |
| | | String storeHeaderPictureId = pictureUploadGateway.uploadUrl(request.storeHeaderUrl(), "F22"); |
| | | // 线下经营的门店材料是**一组三张**,与门头照一起在进件这一步交上去(见企业路径的注释)。 |
| | | String storeIndoorPictureId = pictureUploadGateway.uploadUrl(request.storeIndoorUrl(), "F24"); |
| | | String storeCashierPictureId = pictureUploadGateway.uploadUrl(request.storeCashierUrl(), "F105"); |
| | | HuifuIndividualOnboardingGateway.Result result = individualOnboardingGateway.create(new HuifuIndividualOnboardingCommand( |
| | | applicationNo, request.subjectName().trim(), request.subjectIdNumber().trim().toUpperCase(), certificateValidityType(request.subjectCertificateValidityType()), certificateBeginDate(request.subjectCertificateBeginDate()), certificateEndDate(request.subjectCertificateEndDate()), request.contactMobile().trim(), clean(request.registeredAddress()), request.administratorEmail().trim(), |
| | | mcc(principal.merchantId(), "individual", request.companyType()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), legalFrontPictureId, legalBackPictureId, |
| | | request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), settlementCardPictureId, storeHeaderPictureId)); |
| | | request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), settlementCardPictureId, storeHeaderPictureId, clean(request.businessAddress()), |
| | | storeIndoorPictureId, storeCashierPictureId)); |
| | | applyOnboardingResult(applicationNo, bankCardId, result.accepted(), result.huifuId(), result.applyNo(), result.tokenNo(), result.code(), result.message()); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu submission outcome is pending confirmation", applicationNo); |
| | |
| | | // 个体工商户走的是企业进件接口,所以不是 individual,但同样要带 mcc。 |
| | | // 注意:个人主体的 company_type 存的是进件时选的经营行业,不会等于「个体工商户」。 |
| | | boolean mccRequired = individual || "个体工商户".equals(clean(application.get("companyType"))); |
| | | // 个人(小微)商户的门店照片必须随业务开通一起交上去。缺图就直接收口成失败、 |
| | | // 不发这次请求:发出去也只会被通道驳回,还会在通道侧留下一条缺材料的申请。 |
| | | // 线下经营的门店材料三张(门头照 F22、内景照 F24、收银台 F105)必须随业务开通一起交上去, |
| | | // **与主体类型无关**:线下按「有实体经营场所」报备,通道对企业和个人都索要这一组。 |
| | | // 缺图就直接收口成失败、不发这次请求:发出去也只会被通道驳回,还会在通道侧留下一条缺材料的申请。 |
| | | // 商户在进件页补上照片重新提交即可(见 supplyMissingOnboardingMaterials),下次刷新会带材料重发。 |
| | | String storeHeaderFileId = ""; |
| | | String storeIndoorFileId = ""; |
| | | if (individual) { |
| | | String storeHeaderUrl = clean(application.get("storeHeaderUrl")); |
| | | String storeIndoorUrl = clean(application.get("storeIndoorUrl")); |
| | | if (storeHeaderUrl.isBlank() || storeIndoorUrl.isBlank()) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', reject_reason=? where id=?", "商户业务开通失败:个人商户开通微信支付需要店铺照片与店内环境照,请在进件页补上这两张图后重新提交", application.get("id")); |
| | | return; |
| | | } |
| | | try { |
| | | storeHeaderFileId = pictureUploadGateway.uploadUrl(storeHeaderUrl, "F22"); |
| | | storeIndoorFileId = pictureUploadGateway.uploadUrl(storeIndoorUrl, "F24"); |
| | | } catch (BusinessException | HuifuPaymentGatewayException exception) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', reject_reason=? where id=?", "门店照片上传失败:" + exception.getMessage(), application.get("id")); |
| | | return; |
| | | } |
| | | String storeHeaderFileId; |
| | | String storeIndoorFileId; |
| | | String storeCashierFileId; |
| | | String storeHeaderUrl = clean(application.get("storeHeaderUrl")); |
| | | String storeIndoorUrl = clean(application.get("storeIndoorUrl")); |
| | | String storeCashierUrl = clean(application.get("storeCashierUrl")); |
| | | if (storeHeaderUrl.isBlank() || storeIndoorUrl.isBlank() || storeCashierUrl.isBlank()) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', reject_reason=? where id=?", "商户业务开通失败:线下报备需要店铺门头照、店内环境照与收银台照片,请在进件页补上这三张图后重新提交", application.get("id")); |
| | | return; |
| | | } |
| | | try { |
| | | storeHeaderFileId = pictureUploadGateway.uploadUrl(storeHeaderUrl, "F22"); |
| | | storeIndoorFileId = pictureUploadGateway.uploadUrl(storeIndoorUrl, "F24"); |
| | | storeCashierFileId = pictureUploadGateway.uploadUrl(storeCashierUrl, "F105"); |
| | | } catch (BusinessException | HuifuPaymentGatewayException exception) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', reject_reason=? where id=?", "门店照片上传失败:" + exception.getMessage(), application.get("id")); |
| | | return; |
| | | } |
| | | jdbc.update("update merchant_onboarding_application set status='pending', business_open_status='processing', business_open_request_no=?, reject_reason=null, reviewed_at=null where id=?", requestNo, application.get("id")); |
| | | try { |
| | | HuifuMerchantBusinessOpenGateway.Result result = merchantBusinessOpenGateway.open(requestNo, huifuId, shortName, mcc(merchantId, subjectType, clean(application.get("companyType"))), individual, mccRequired, storeHeaderFileId, storeIndoorFileId); |
| | | HuifuMerchantBusinessOpenGateway.Result result = merchantBusinessOpenGateway.open(requestNo, huifuId, shortName, mcc(merchantId, subjectType, clean(application.get("companyType"))), individual, mccRequired, storeHeaderFileId, storeIndoorFileId, storeCashierFileId); |
| | | if (!result.accepted()) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', business_open_apply_no=nullif(?,''), business_open_response_code=?, business_open_response_message=?, reject_reason=? where id=?", clean(result.applyNo()), result.code(), result.message(), "商户业务开通失败:" + providerReason(result.code(), result.message()), application.get("id")); |
| | | return; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 微信小程序支付配置是否已经在通道生效:人才端 AppID 是否绑定在 {@code fee_type=01} 的配置项下。 |
| | | * 微信小程序支付配置是否已经在通道生效:人才端 AppID 是否绑定在配置的业务开通类型 |
| | | * (线下 {@code 02}、线上 {@code 01},取 {@code merchant-wechat-fee-type})下的配置项里。 |
| | | * 与分阶段进度用的是同一份证据。查询失败按「未生效」处理——宁可多提一次也不要漏配。 |
| | | */ |
| | | private boolean wechatConfigEffective(String huifuId) { |
| | |
| | | Map<String, Object> config = onboardingQueryGateway.queryWechatConfig(huifuId); |
| | | if (config == null || !"00000000".equals(clean(config.get("resp_code")))) return false; |
| | | String appId = huifuPaymentProperties.getMiniProgramAppId().trim(); |
| | | String feeType = huifuPaymentProperties.getMerchantWechatFeeType(); |
| | | for (Map<String, Object> entry : providerList(config.get("atu_config_bo_list"))) { |
| | | if (!"01".equals(clean(entry.get("fee_type")))) continue; |
| | | if (!feeType.equals(clean(entry.get("fee_type")))) continue; |
| | | for (Map<String, Object> item : providerList(entry.get("wx_config_list"))) { |
| | | if ("1".equals(clean(item.get("config_type"))) && appId.equals(clean(item.get("config_value")))) return true; |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 微信业务是否已经在通道开通:商户详情的 {@code qry_wx_conf_list} 里出现线上三场景之一即视为已开通。 |
| | | * 微信业务是否已经在通道开通:商户详情的 {@code qry_wx_conf_list} 里出现配置的支付场景之一 |
| | | * (线下 {@code 1/2/3}、线上 {@code 4/5/12},取 {@code merchant-wechat-scenes})即视为已开通。 |
| | | * 业务开通是申请型接口,重复提交会给同一商户开出第二份申请,所以提交前必须先查。 |
| | | */ |
| | | private boolean wechatBusinessEffective(String huifuId) { |
| | | try { |
| | | Map<String, Object> detail = onboardingQueryGateway.queryCapabilities(huifuId); |
| | | if (detail == null || !"00000000".equals(clean(detail.get("resp_code")))) return false; |
| | | List<String> scenes = huifuPaymentProperties.merchantWechatSceneList(); |
| | | return providerList(detail.get("qry_wx_conf_list")).stream() |
| | | .anyMatch(item -> List.of("4", "5", "12").contains(clean(item.get("pay_scene")))); |
| | | .anyMatch(item -> scenes.contains(clean(item.get("pay_scene")))); |
| | | } catch (RuntimeException exception) { |
| | | log.warn("Huifu merchant business open probe failed: huifuId={}", huifuId, exception); |
| | | return false; |
| | |
| | | */ |
| | | private boolean retryable(Map<String, Object> row) { return isRetryable(row); } |
| | | |
| | | /** |
| | | * 汇付把结算主体分成对公与对私,两者要上报的「结算凭证图」不是同一张:对私(含个体工商户) |
| | | * 是个体经营者的银行卡卡号面,对公是企业的开户许可证。 |
| | | * |
| | | * <p>这个判定同时决定必填校验、上报给通道的图片用途,以及后台详情该把它们显示成哪个名字, |
| | | * 所以只留这一处定义——商户端也按同一条规则把图放进两个不同的请求字段。后台详情在另一个包, |
| | | * 所以是 public。 |
| | | */ |
| | | public static boolean isPrivateSettlement(Object subjectType, Object companyType) { |
| | | String subject = subjectType == null ? "" : String.valueOf(subjectType).trim(); |
| | | String company = companyType == null ? "" : String.valueOf(companyType).trim(); |
| | | return !"enterprise".equals(subject) || "个体工商户".equals(company); |
| | | } |
| | | |
| | | /** 取值为静态形式,便于直接断言这条规则本身(见 MerchantOnboardingRetryTests)。 */ |
| | | static boolean isRetryable(Map<String, Object> row) { |
| | | return "failed".equals(cleanStatus(row.get("wechatConfigStatus"))) || "failed".equals(cleanStatus(row.get("businessOpenStatus"))); |