| | |
| | | // 否则测试会真的打到汇付。 |
| | | @MockitoBean com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway wechatConfigGateway; |
| | | @MockitoBean com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway businessOpenGateway; |
| | | // 提交进件会真的上传图片、并向汇付递交申请,这两个也一并挡住——测试不该打网络。 |
| | | @MockitoBean com.talentcard.server.business.payment.huifu.HuifuPictureUploadGateway pictureUploadGateway; |
| | | @MockitoBean com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingGateway individualOnboardingGateway; |
| | | @MockitoBean com.talentcard.server.business.payment.huifu.HuifuEnterpriseOnboardingGateway enterpriseOnboardingGateway; |
| | | @Autowired org.springframework.web.context.WebApplicationContext context; |
| | | @Autowired com.talentcard.server.business.admin.auth.AdminAuthService adminAuth; |
| | | |
| | |
| | | MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal"); |
| | | jdbc.update("update merchant_bank_card set card_hash=? where merchant_id=?",sha256("622200001234"),principal.merchantId()); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S","bind_card_status","S")); |
| | | var scenes=java.util.List.of("4","5","12").stream().map(scene -> Map.of("pay_scene",scene,"fee_rate","0.70","fee_min_amt","0.00","fee_charge_type","1","out_fee_flag","2", |
| | | var scenes=java.util.List.of("1","2","3").stream().map(scene -> Map.of("pay_scene",scene,"fee_rate","0.30","fee_min_amt","0.00","fee_charge_type","1","out_fee_flag","2", |
| | | "wx_mer_infos",java.util.List.of(Map.of("bank_mer_code","wx-merchant","pay_channel_id","00016904","is_default","1")))).toList(); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N","qry_wx_conf_list",scenes, |
| | | "qry_settle_config_list",java.util.List.of(Map.of("settle_status","1","settle_cycle","D1","settle_pattern","P0","min_amt","0","remained_amt","0","token_no","original-card-token")), |
| | | "qry_cash_card_info_list",java.util.List.of(Map.of("card_no","622200001234","token_no","original-card-token")))); |
| | | when(gateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000","atu_config_bo_list",java.util.List.of(Map.of("fee_type","01","wx_config_list",java.util.List.of(Map.of("config_type","1","config_value","wxcfee477acb54d179")))))); |
| | | when(gateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000","atu_config_bo_list",java.util.List.of(Map.of("fee_type","02","wx_config_list",java.util.List.of(Map.of("config_type","1","config_value","wxcfee477acb54d179")))))); |
| | | when(gateway.queryRealname(anyString())).thenReturn(Map.of("resp_code","00000000","applyment_stat","APPLYMENT_STATE_PASSED","authorize_stat","1")); |
| | | assertThat(service.refreshOnboarding(principal,false)).containsEntry("status","approved"); |
| | | assertThat(service.onboardingStatus(principal)).containsEntry("status","approved"); |
| | |
| | | MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal"); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S")); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N")); |
| | | when(gateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000","atu_config_bo_list",java.util.List.of(Map.of("fee_type","01","wx_applet_app_id","wxcfee477acb54d179","wx_config_list",java.util.List.of(Map.of("config_type","1","config_value","wxcfee477acb54d179","control_flag","Y")))))); |
| | | when(gateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000","atu_config_bo_list",java.util.List.of(Map.of("fee_type","02","wx_applet_app_id","wxcfee477acb54d179","wx_config_list",java.util.List.of(Map.of("config_type","1","config_value","wxcfee477acb54d179","control_flag","Y")))))); |
| | | when(gateway.queryRealname(anyString())).thenReturn(Map.of("resp_code","00000000","applyment_stat","APPLYMENT_STATE_PASSED","authorize_stat","0")); |
| | | Map<String,Object> result=service.refreshOnboarding(principal,false); |
| | | assertThat((java.util.List<Map<String,Object>>)result.get("stages")).anySatisfy(stage -> assertThat(stage).containsEntry("key","appId").containsEntry("status","action_required")); |
| | |
| | | void configuredWechatScenesStillRequireTalentAppAndAuthorization() { |
| | | MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal"); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S")); |
| | | var scenes=java.util.List.of("4","5","12").stream().map(scene -> Map.of("pay_scene",scene,"fee_rate","0.70","fee_min_amt","0.00","fee_charge_type","1","out_fee_flag","2", |
| | | var scenes=java.util.List.of("1","2","3").stream().map(scene -> Map.of("pay_scene",scene,"fee_rate","0.30","fee_min_amt","0.00","fee_charge_type","1","out_fee_flag","2", |
| | | "wx_mer_infos",java.util.List.of(Map.of("bank_mer_code","wx-merchant","pay_channel_id","00016904","is_default","1")))).toList(); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N","qry_wx_conf_list",scenes)); |
| | | Map<String,Object> result=service.refreshOnboarding(principal,false); |
| | |
| | | MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal"); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S")); |
| | | // 与真实回执同形:三个场景都不带 fee_min_amt。 |
| | | var scenes=java.util.List.of("4","5","12").stream().map(scene -> Map.of("pay_scene",scene,"fee_rate","0.70","fee_charge_type","1","out_fee_flag","2", |
| | | var scenes=java.util.List.of("1","2","3").stream().map(scene -> Map.of("pay_scene",scene,"fee_rate","0.30","fee_charge_type","1","out_fee_flag","2", |
| | | "wx_mer_infos",java.util.List.of(Map.of("bank_mer_code","922609119","pay_channel_id","00016904","is_default","1")))).toList(); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N","qry_wx_conf_list",scenes)); |
| | | |
| | |
| | | void aNonZeroMinimumFeeKeepsTheWechatStageUnconfirmed() { |
| | | MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal"); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S")); |
| | | var scenes=java.util.List.of("4","5","12").stream().map(scene -> Map.of("pay_scene",scene,"fee_rate","0.70","fee_min_amt","1.00","fee_charge_type","1","out_fee_flag","2", |
| | | var scenes=java.util.List.of("1","2","3").stream().map(scene -> Map.of("pay_scene",scene,"fee_rate","0.30","fee_min_amt","1.00","fee_charge_type","1","out_fee_flag","2", |
| | | "wx_mer_infos",java.util.List.of(Map.of("bank_mer_code","922609119","is_default","1")))).toList(); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N","qry_wx_conf_list",scenes)); |
| | | |
| | |
| | | Map<String,Object> fixture=provisioningFixture(); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S")); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N", |
| | | "qry_wx_conf_list",java.util.List.of(Map.of("pay_scene","4","fee_rate","0.70")))); |
| | | "qry_wx_conf_list",java.util.List.of(Map.of("pay_scene","1","fee_rate","0.30")))); |
| | | when(gateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000","atu_config_bo_list", |
| | | java.util.List.of(Map.of("fee_type","01","wx_config_list",java.util.List.of(Map.of("config_type","1","config_value","wxcfee477acb54d179")))))); |
| | | java.util.List.of(Map.of("fee_type","02","wx_config_list",java.util.List.of(Map.of("config_type","1","config_value","wxcfee477acb54d179")))))); |
| | | |
| | | service.refreshOnboarding((MerchantPrincipal)fixture.get("principal"),false); |
| | | |
| | | assertThat(statusOf("wechat_config_status",String.valueOf(fixture.get("applicationNo")))).isEqualTo("approved"); |
| | | assertThat(statusOf("business_open_status",String.valueOf(fixture.get("applicationNo")))).isEqualTo("approved"); |
| | | verify(wechatConfigGateway,never()).configure(anyString(),anyString()); |
| | | verify(businessOpenGateway,never()).open(anyString(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),anyString(),anyString()); |
| | | verify(businessOpenGateway,never()).open(anyString(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),any(),any(),any()); |
| | | } |
| | | |
| | | /** |
| | |
| | | MerchantPrincipal principal=(MerchantPrincipal)fixture.get("principal"); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S")); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N")); |
| | | when(businessOpenGateway.open(anyString(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),anyString(),anyString())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway.Result(true,"BO-APPLY","90000000","审核中")); |
| | | when(businessOpenGateway.open(anyString(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),any(),any(),any())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway.Result(true,"BO-APPLY","90000000","审核中")); |
| | | |
| | | // 第一轮:通道侧还没有微信业务 → 只提交业务开通,配置这一步根本不碰。 |
| | | service.refreshOnboarding(principal,false); |
| | |
| | | |
| | | // 第二轮:通道侧已经开通线上场景 → 业务开通按已开通收口(不重复提交),配置才发出去。 |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N", |
| | | "qry_wx_conf_list",java.util.List.of(Map.of("pay_scene","4")))); |
| | | "qry_wx_conf_list",java.util.List.of(Map.of("pay_scene","1")))); |
| | | when(wechatConfigGateway.configure(anyString(),anyString())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway.Result(true,"00000000","成功")); |
| | | |
| | | service.refreshOnboarding(principal,false); |
| | | |
| | | // 业务开通只提交过一次——通道已开通时不再重复提交,避免第二份申请。 |
| | | verify(businessOpenGateway,times(1)).open(anyString(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),anyString(),anyString()); |
| | | verify(businessOpenGateway,times(1)).open(anyString(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),any(),any(),any()); |
| | | verify(wechatConfigGateway,times(1)).configure(anyString(),anyString()); |
| | | assertThat(statusOf("business_open_status",applicationNo)).isEqualTo("approved"); |
| | | assertThat(statusOf("wechat_config_status",applicationNo)).isEqualTo("approved"); |
| | |
| | | Map<String,Object> fixture=provisioningFixture(); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S")); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N", |
| | | "qry_wx_conf_list",java.util.List.of(Map.of("pay_scene","4")))); |
| | | "qry_wx_conf_list",java.util.List.of(Map.of("pay_scene","1")))); |
| | | when(wechatConfigGateway.configure(anyString(),anyString())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway.Result(false,"00000001","参数非法")); |
| | | |
| | | service.refreshOnboarding((MerchantPrincipal)fixture.get("principal"),false); |
| | |
| | | String applicationNo=String.valueOf(fixture.get("applicationNo")); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S")); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N")); |
| | | when(businessOpenGateway.open(anyString(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),anyString(),anyString())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway.Result(false,"","00000001","wx_conf_list格式错误")); |
| | | when(businessOpenGateway.open(anyString(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),any(),any(),any())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway.Result(false,"","00000001","wx_conf_list格式错误")); |
| | | |
| | | MerchantPrincipal principal=(MerchantPrincipal)fixture.get("principal"); |
| | | service.refreshOnboarding(principal,false); |
| | |
| | | service.refreshOnboarding(principal,false); |
| | | |
| | | var requests=org.mockito.ArgumentCaptor.forClass(String.class); |
| | | verify(businessOpenGateway,times(2)).open(requests.capture(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),anyString(),anyString()); |
| | | verify(businessOpenGateway,times(2)).open(requests.capture(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),any(),any(),any()); |
| | | assertThat(requests.getAllValues()).doesNotHaveDuplicates(); |
| | | assertThat(requests.getAllValues().get(0)).isEqualTo(applicationNo+"BO"); |
| | | assertThat(statusOf("business_open_request_no",applicationNo)).isEqualTo(requests.getAllValues().get(1)); |
| | | } |
| | | |
| | | /** |
| | | * 门店材料缺一张就不发业务开通请求:通道按 F-code 逐张核对,缺图发出去只会被驳回, |
| | | * 还会在通道侧留下一条缺材料的申请。进件收口成 failed,并明确告诉商户去进件页补哪三张图。 |
| | | * |
| | | * <p>2026-09-29 的真实驳回就是这条口径:线下报备要「门头照 + 内景照 + 收银台」三张, |
| | | * 企业主体原先一张都不交。这里盯住的是「缺图不要发注定被拒的请求」这道闸门,以及它给出的补图提示。 |
| | | */ |
| | | @Test |
| | | void aFailedWechatConfigIsRetriedWithANewRequestNumber() { |
| | | void aMissingStorePhotoStopsTheBusinessOpenBeforeItIsSent() { |
| | | Map<String,Object> fixture=provisioningFixture(); |
| | | String applicationNo=String.valueOf(fixture.get("applicationNo")); |
| | | jdbc.update("update merchant_onboarding_application set store_cashier_url=null where application_no=?",applicationNo); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S")); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N")); |
| | | |
| | | service.refreshOnboarding((MerchantPrincipal)fixture.get("principal"),false); |
| | | |
| | | verify(businessOpenGateway,never()).open(anyString(),anyString(),anyString(),anyString(),anyBoolean(),anyBoolean(),any(),any(),any()); |
| | | assertThat(statusOf("business_open_status",applicationNo)).isEqualTo("failed"); |
| | | assertThat(statusOf("reject_reason",applicationNo)).contains("收银台"); |
| | | } |
| | | |
| | | @Test |
| | | void aFailedWechatConfigIsRetriedWithANewRequestNumber() { Map<String,Object> fixture=provisioningFixture(); |
| | | String applicationNo=String.valueOf(fixture.get("applicationNo")); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S")); |
| | | // 通道侧已开通线上场景,配置这一步才有东西可挂(见 advanceWechatConfig 的闸门)。 |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N", |
| | | "qry_wx_conf_list",java.util.List.of(Map.of("pay_scene","4")))); |
| | | "qry_wx_conf_list",java.util.List.of(Map.of("pay_scene","1")))); |
| | | when(wechatConfigGateway.configure(anyString(),anyString())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway.Result(false,"00000001","参数非法")); |
| | | |
| | | MerchantPrincipal principal=(MerchantPrincipal)fixture.get("principal"); |
| | |
| | | |
| | | private String statusOf(String column,String applicationNo) { |
| | | return jdbc.queryForObject("select "+column+" from merchant_onboarding_application where application_no=?",String.class,applicationNo); |
| | | } |
| | | |
| | | /** |
| | | * 进件通过后**新增**的结算卡也要能从人工刷新那条路收敛为已批准。 |
| | | * |
| | | * <p>它的状态原先只有绑定回调能推进(回调会丢,开发与内网环境根本收不到),而初次进件那张卡的 |
| | | * 查询对等物只认 {@code initial_bank_card_id}。补法是把同一条查询路径推广到该门店所有待处理的卡, |
| | | * 不新增定时任务。 |
| | | */ |
| | | @Test |
| | | void anAddedBankCardConvergesThroughTheSameManualRefresh() { |
| | | Map<String,Object> fixture = fixture(); |
| | | MerchantPrincipal principal = (MerchantPrincipal) fixture.get("principal"); |
| | | String applicationNo = String.valueOf(fixture.get("applicationNo")); |
| | | String addedCardKey = "T02ADD-" + applicationNo; |
| | | jdbc.update(""" |
| | | insert into merchant_bank_card(merchant_id, store_id, bank_name, branch_name, city_code, card_tail, |
| | | card_masked, card_hash, status, idempotency_key) |
| | | values (?, ?, 'Bank', 'Branch', '510100', '5678', '****5678', ?, 'pending', ?) |
| | | """, principal.merchantId(), principal.storeId(), sha256("622200009999"), addedCardKey); |
| | | when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S","bind_card_status","S")); |
| | | when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N", |
| | | "qry_cash_card_info_list",java.util.List.of(Map.of("card_no","622200009999","token_no","added-card-token")), |
| | | "qry_settle_config_list",java.util.List.of(Map.of("settle_status","1","settle_cycle","D1","settle_pattern","P0","min_amt","0","remained_amt","0","token_no","added-card-token")))); |
| | | |
| | | service.refreshOnboarding(principal,false); |
| | | |
| | | assertThat(jdbc.queryForObject("select status from merchant_bank_card where idempotency_key=?",String.class,addedCardKey)) |
| | | .as("新增的结算卡也要能靠查询收敛,而不是永远停在处理中") |
| | | .isEqualTo("approved"); |
| | | } |
| | | |
| | | /** |
| | |
| | | assertThat(statusOf("store_indoor_url",applicationNo)).isEqualTo("https://cdn.example.com/store-indoor.jpg"); |
| | | } |
| | | |
| | | /** |
| | | * 存量进件缺「结算银行卡卡号面」时,重新提交要能补上。 |
| | | * |
| | | * <p>这一列过去从来没落过库(2026-09-18 工单),库里也没有别的地方能还原它;而分阶段模型下 |
| | | * 重复提交不会新建进件,重提就是已提交进件唯一的补救入口,所以这条必须补得住。 |
| | | */ |
| | | @Test |
| | | void resubmittingSuppliesTheMissingSettlementCardFront() { |
| | | Map<String,Object> fixture = individualProvisioningFixture(); |
| | | MerchantPrincipal principal = (MerchantPrincipal) fixture.get("principal"); |
| | | String applicationNo = String.valueOf(fixture.get("applicationNo")); |
| | | assertThat(statusOf("settlement_card_front_url",applicationNo)).isNull(); |
| | | |
| | | service.submitOnboarding(principal, request( |
| | | "https://cdn.example.com/store-header.jpg", |
| | | "https://cdn.example.com/store-indoor.jpg", |
| | | "https://cdn.example.com/card-front.jpg"), "T02-card-"+System.nanoTime()); |
| | | |
| | | assertThat(statusOf("settlement_card_front_url",applicationNo)).isEqualTo("https://cdn.example.com/card-front.jpg"); |
| | | // 没有重新进件,原汇付商户号与银行卡关系都保持。 |
| | | assertThat(jdbc.queryForObject("select count(*) from merchant_onboarding_application where application_no=?",Integer.class,applicationNo)).isEqualTo(1); |
| | | } |
| | | |
| | | /** 已经有值的卡号面不会被重新提交覆盖——存档的那张才是当时提交的。 */ |
| | | @Test |
| | | void resubmittingDoesNotOverwriteAnExistingSettlementCardFront() { |
| | | Map<String,Object> fixture = individualProvisioningFixture(); |
| | | MerchantPrincipal principal = (MerchantPrincipal) fixture.get("principal"); |
| | | String applicationNo = String.valueOf(fixture.get("applicationNo")); |
| | | jdbc.update("update merchant_onboarding_application set settlement_card_front_url='https://cdn.example.com/original-card.jpg' where application_no=?", applicationNo); |
| | | |
| | | service.submitOnboarding(principal, request( |
| | | "https://cdn.example.com/store-header.jpg", |
| | | "https://cdn.example.com/store-indoor.jpg", |
| | | "https://cdn.example.com/other-card.jpg"), "T02-card-"+System.nanoTime()); |
| | | |
| | | assertThat(statusOf("settlement_card_front_url",applicationNo)).isEqualTo("https://cdn.example.com/original-card.jpg"); |
| | | } |
| | | |
| | | private Map<String,Object> individualProvisioningFixture() { |
| | | String key="T02I-"+System.nanoTime(); |
| | | jdbc.update("insert into merchant(merchant_name,status,deleted) values (?,1,0)",key); |
| | |
| | | Long store=jdbc.queryForObject("select id from merchant_store where merchant_id=?",Long.class,merchant); |
| | | jdbc.update("insert into merchant_bank_card(merchant_id,store_id,bank_name,branch_name,city_code,card_tail,card_masked,card_hash,status,idempotency_key) values (?,?,'Bank','Branch','510100','1234','****1234',?,'approved',?)",merchant,store,key,key); |
| | | Long card=jdbc.queryForObject("select id from merchant_bank_card where idempotency_key=?",Long.class,key); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id,store_id,application_no,subject_type,subject_name,contact_name,contact_mobile,idempotency_key,status,provider_huifu_id,provider_apply_no,initial_bank_card_id,business_open_status) values (?, ?, ?, 'individual','Owner','Owner','13700000000',?,'pending','6666000241335656','original-apply',?,'failed')",merchant,store,key,key,card); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id,store_id,application_no,subject_type,subject_name,contact_name,contact_mobile,idempotency_key,status,provider_huifu_id,provider_apply_no,initial_bank_card_id,business_open_status,store_header_url,store_indoor_url,store_cashier_url) values (?, ?, ?, 'individual','Owner','Owner','13700000000',?,'pending','6666000241335656','original-apply',?,'failed','https://cdn.example.com/store-header.jpg','https://cdn.example.com/store-indoor.jpg','https://cdn.example.com/store-cashier.jpg')",merchant,store,key,key,card); |
| | | return Map.of("principal",new MerchantPrincipal(1L,merchant,store,"owner"),"applicationNo",key); |
| | | } |
| | | |
| | | /** 补齐门店照片这一步只读这两项,其余字段留空即可。 */ |
| | | private static MerchantOnboardingRequest request(String storeHeaderUrl,String storeIndoorUrl) { |
| | | return request(storeHeaderUrl, storeIndoorUrl, null); |
| | | } |
| | | |
| | | private static MerchantOnboardingRequest request(String storeHeaderUrl,String storeIndoorUrl,String settlementCardFrontUrl) { |
| | | return new MerchantOnboardingRequest("individual","Owner",null,null,null,null,null,null,null,null, |
| | | null,null,"510100",null,null,null,null,null,null,null,null,null,null,null,null,null, |
| | | "Owner","13700000000",null,null,"510100",null,null,null,storeHeaderUrl,storeIndoorUrl); |
| | | "Owner","13700000000",null,null,"510100",null,settlementCardFrontUrl,null,storeHeaderUrl,storeIndoorUrl,"四川省成都市武侯区某路1号",null); |
| | | } |
| | | |
| | | /** |
| | | * 新提交的个人进件要把「结算银行卡卡号面」真的写进库。 |
| | | * |
| | | * <p>在这之前那条 INSERT 的列清单里没有 {@code settlement_card_front_url}:商户端传了值、后端也 |
| | | * 拿它上传了汇付图片(F13),但写库时被静默丢掉,于是后台详情里这一格永远是 {@code -}。 |
| | | * 断言直接盯数据库那一列,不看接口返回值——返回值在修复前也是「成功」的。 |
| | | */ |
| | | @Test |
| | | void aNewIndividualSubmissionPersistsTheSettlementCardFront() { |
| | | String key = "T02C-" + System.nanoTime(); |
| | | Map<String,Long> ids = merchantAndStore(key); |
| | | String clearingCode = twelveDigitCode(); |
| | | insertBankBranch(clearingCode); |
| | | stubPictureUpload(); |
| | | // 通道递交刻意回「驳回」:写库发生在递交**之前**,而驳回会让流程就此收口, |
| | | // 不再触发后面的开通链路——这条测试只关心那一列有没有落库,其余都是噪音。 |
| | | when(individualOnboardingGateway.create(any())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingGateway.Result(false,"","","","00000001","参数非法")); |
| | | |
| | | service.submitOnboarding(new MerchantPrincipal(1L,ids.get("merchantId"),ids.get("storeId"),"owner"), individualRequest(clearingCode), "T02-card-"+System.nanoTime()); |
| | | |
| | | assertThat(settlementProofOf(ids.get("merchantId"))) |
| | | .as("上传了结算卡卡号面就必须落库,否则后台详情只能显示 -") |
| | | .isEqualTo("https://cdn.example.com/card-front.jpg"); |
| | | } |
| | | |
| | | /** |
| | | * 对公(企业且非个体工商户)主体的「结算凭证图」是**开户许可证**,同样必须落库。 |
| | | * |
| | | * <p>这一列一列两义:商户端对私把图放 settlementCardFrontUrl、对公放 registrationAccountUrl。 |
| | | * 只按对私那一路取值的话,对公提交出来这一格永远是空的——后台详情里就是那个刺眼的 `-`。 |
| | | */ |
| | | @Test |
| | | void aNewEnterpriseSubmissionPersistsTheRegistrationAccountProof() { |
| | | String key = "T02E-" + System.nanoTime(); |
| | | Map<String,Long> ids = merchantAndStore(key); |
| | | String clearingCode = twelveDigitCode(); |
| | | insertBankBranch(clearingCode); |
| | | stubPictureUpload(); |
| | | when(enterpriseOnboardingGateway.create(any())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuEnterpriseOnboardingGateway.Result(false,"","","","00000001","参数非法")); |
| | | |
| | | service.submitOnboarding(new MerchantPrincipal(1L,ids.get("merchantId"),ids.get("storeId"),"owner"), enterpriseRequest(clearingCode), "T02-card-"+System.nanoTime()); |
| | | |
| | | assertThat(settlementProofOf(ids.get("merchantId"))) |
| | | .as("对公主体的结算凭证图是开户许可证,不能取对私那个字段") |
| | | .isEqualTo("https://cdn.example.com/registration-account.jpg"); |
| | | } |
| | | |
| | | /** 对公主体重提时补的也是开户许可证那一张。 */ |
| | | @Test |
| | | void resubmittingAnEnterpriseApplicationSuppliesTheRegistrationAccountProof() { |
| | | Map<String,Object> fixture = provisioningFixture(); |
| | | MerchantPrincipal principal = (MerchantPrincipal) fixture.get("principal"); |
| | | String applicationNo = String.valueOf(fixture.get("applicationNo")); |
| | | assertThat(statusOf("settlement_card_front_url",applicationNo)).isNull(); |
| | | |
| | | service.submitOnboarding(principal, registrationAccountRequest("https://cdn.example.com/registration-account.jpg"), "T02-card-"+System.nanoTime()); |
| | | |
| | | assertThat(statusOf("settlement_card_front_url",applicationNo)).isEqualTo("https://cdn.example.com/registration-account.jpg"); |
| | | assertThat(jdbc.queryForObject("select count(*) from merchant_onboarding_application where application_no=?",Integer.class,applicationNo)).isEqualTo(1); |
| | | } |
| | | |
| | | private String settlementProofOf(Long merchantId) { |
| | | return jdbc.queryForObject("select settlement_card_front_url from merchant_onboarding_application where merchant_id=?",String.class,merchantId); |
| | | } |
| | | |
| | | private Map<String,Long> merchantAndStore(String key) { |
| | | jdbc.update("insert into merchant(merchant_name,merchant_type,status,deleted) values (?, '餐饮服务', 1, 0)", key); |
| | | Long merchant = jdbc.queryForObject("select id from merchant where merchant_name=?", Long.class, key); |
| | | jdbc.update("insert into merchant_store(merchant_id,store_name,status,deleted) values (?,?,1,0)", merchant, key); |
| | | Long store = jdbc.queryForObject("select id from merchant_store where merchant_id=?", Long.class, merchant); |
| | | return Map.of("merchantId", merchant, "storeId", store); |
| | | } |
| | | |
| | | /** 开户行目录里必须有这一条,且带汇付银行编码,否则提交会在选行那一步被拦。 */ |
| | | private void insertBankBranch(String clearingCode) { |
| | | jdbc.update("insert into bank_branch_directory(clearing_code,branch_name,bank_name,head_bank_code) values (?,?,?,?)", |
| | | clearingCode, "测试支行", "测试银行", "HB" + clearingCode); |
| | | } |
| | | |
| | | private static String twelveDigitCode() { |
| | | return String.format("%012d", Math.abs(System.nanoTime() % 1_000_000_000_000L)); |
| | | } |
| | | |
| | | private void stubPictureUpload() { |
| | | when(pictureUploadGateway.uploadUrl(anyString(),anyString())).thenReturn("pic-id"); |
| | | } |
| | | |
| | | /** 对公(企业且非个体工商户)的完整提交:结算凭证图走 registrationAccountUrl。 */ |
| | | private static MerchantOnboardingRequest enterpriseRequest(String clearingCode) { |
| | | return new MerchantOnboardingRequest( |
| | | "enterprise","测试法人","91110108MA01ABCDEF","511623200211245724","fixed","2022-02-14","2032-02-14", |
| | | "fixed","2020-01-01","2040-01-01","四川省 成都市 武侯区","510000","510100","510107","四川省成都市武侯区某路1号", |
| | | "测试企业有限公司",null,"私营企业","https://cdn.example.com/license.jpg","2020-01-01","100", |
| | | "https://cdn.example.com/legal-front.jpg","https://cdn.example.com/legal-back.jpg","四川省成都市武侯区某路1号",null,"owner@example.com", |
| | | "测试联系人","13700000001","6222000012345678","510000","510100",clearingCode, |
| | | null,"https://cdn.example.com/registration-account.jpg", |
| | | "https://cdn.example.com/store-header.jpg","https://cdn.example.com/store-indoor.jpg",null, |
| | | "https://cdn.example.com/store-cashier.jpg"); |
| | | } |
| | | |
| | | /** 对公主体重提时要补的是开户许可证那一张(请求里走 registrationAccountUrl)。 */ |
| | | private static MerchantOnboardingRequest registrationAccountRequest(String registrationAccountUrl) { |
| | | return new MerchantOnboardingRequest("enterprise","Owner",null,null,null,null,null,null,null,null, |
| | | null,null,"510100",null,null,null,null,null,null,null,null,null,null,null,null,null, |
| | | "Owner","13700000000",null,null,"510100",null,null,registrationAccountUrl,null,null,null,null); |
| | | } |
| | | |
| | | /** 个人进件的一次完整提交(除门店照片与结算卡卡号面外的必填项也都给上)。 */ |
| | | private static MerchantOnboardingRequest individualRequest(String clearingCode) { |
| | | return new MerchantOnboardingRequest( |
| | | "individual","测试负责人",null,"511623200211245724","fixed","2022-02-14","2032-02-14", |
| | | null,null,null,"四川省 成都市 武侯区","510000","510100","510107","四川省邻水县丰禾镇9组108号", |
| | | null,null,"其它",null,null,null, |
| | | "https://cdn.example.com/id-front.jpg","https://cdn.example.com/id-back.jpg","四川省邻水县丰禾镇9组108号","13700000000","owner@example.com", |
| | | "测试负责人","13700000000","6222000012345678","510000","510100",clearingCode, |
| | | "https://cdn.example.com/card-front.jpg",null, |
| | | "https://cdn.example.com/store-header.jpg","https://cdn.example.com/store-indoor.jpg","四川省邻水县丰禾镇9组108号", |
| | | "https://cdn.example.com/store-cashier.jpg"); |
| | | } |
| | | |
| | | /** 建一张还停在 not_started 的进件,用来观察刷新会不会去推进开通步骤。 */ |
| | |
| | | Long store=jdbc.queryForObject("select id from merchant_store where merchant_id=?",Long.class,merchant); |
| | | jdbc.update("insert into merchant_bank_card(merchant_id,store_id,bank_name,branch_name,city_code,card_tail,card_masked,card_hash,status,idempotency_key) values (?,?,'Bank','Branch','510100','1234','****1234',?,'approved',?)",merchant,store,key,key); |
| | | Long card=jdbc.queryForObject("select id from merchant_bank_card where idempotency_key=?",Long.class,key); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id,store_id,application_no,subject_type,subject_name,contact_name,contact_mobile,idempotency_key,status,provider_huifu_id,provider_apply_no,initial_bank_card_id) values (?, ?, ?, 'enterprise','Owner','Owner','13700000000',?,'pending','6666000241335656','original-apply',?)",merchant,store,key,key,card); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id,store_id,application_no,subject_type,subject_name,contact_name,contact_mobile,idempotency_key,status,provider_huifu_id,provider_apply_no,initial_bank_card_id,store_header_url,store_indoor_url,store_cashier_url) values (?, ?, ?, 'enterprise','Owner','Owner','13700000000',?,'pending','6666000241335656','original-apply',?,'https://cdn.example.com/store-header.jpg','https://cdn.example.com/store-indoor.jpg','https://cdn.example.com/store-cashier.jpg')",merchant,store,key,key,card); |
| | | return Map.of("principal",new MerchantPrincipal(1L,merchant,store,"owner"),"applicationNo",key); |
| | | } |
| | | |