| | |
| | | import com.ruoyi.account.api.feignClient.ChargingCardClient; |
| | | import com.ruoyi.account.api.feignClient.CompanyAllocateRecordClient; |
| | | import com.ruoyi.account.api.model.TChargingCard; |
| | | import com.ruoyi.account.api.model.TCompanyAllocateRecord; |
| | | import com.ruoyi.account.api.vo.ChargingCardDeductVO; |
| | | import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyClient; |
| | | import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyDetailClient; |
| | | import com.ruoyi.chargingPile.api.feignClient.BillingTemplateClient; |
| | | import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient; |
| | | import com.ruoyi.chargingPile.api.model.TAccountingStrategy; |
| | | import com.ruoyi.chargingPile.api.model.TAccountingStrategyDetail; |
| | | import com.ruoyi.chargingPile.api.model.TChargingGun; |
| | | import com.ruoyi.chargingPile.api.vo.GetChargingGunByCode; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.order.api.dto.DeviceStartChargingDTO; |
| | | import com.ruoyi.order.api.model.TCardAccountingStrategyDetailOrder; |
| | | import com.ruoyi.order.api.model.TCardAccountingStrategyOrder; |
| | | import com.ruoyi.order.api.model.TCardChargingOrder; |
| | | import com.ruoyi.order.api.model.TCardChargingOrderAccountingStrategy; |
| | | import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery; |
| | |
| | | import com.ruoyi.order.api.vo.TransactionRecordMessageVO; |
| | | import com.ruoyi.order.mapper.TCardChargingOrderMapper; |
| | | import com.ruoyi.order.service.TCardAccountingStrategyDetailOrderService; |
| | | import com.ruoyi.order.service.TCardAccountingStrategyOrderService; |
| | | import com.ruoyi.order.service.TCardChargingOrderAccountingStrategyService; |
| | | import com.ruoyi.order.support.ChargeAccountSupport; |
| | | import com.ruoyi.other.api.domain.TGroupCompany; |
| | |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.ArgumentMatchers.anyLong; |
| | | import static org.mockito.ArgumentMatchers.anyString; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.ArgumentMatchers.isNull; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.never; |
| | | import static org.mockito.Mockito.times; |
| | |
| | | * 纯单测不启 Spring 容器,此处内联装配,远程依赖以宽松 mock 兜底(内部均有判空与兜底), |
| | | * 需要走真实分支的用例可再覆盖具体依赖字段。 |
| | | */ |
| | | /** 账户侧分配流水客户端(与 ChargeAccountSupport 内持有的是同一 mock,便于补偿双判据用例 stub 与校验) */ |
| | | private CompanyAllocateRecordClient companyAllocateRecordClient; |
| | | |
| | | private ChargeAccountSupport newChargeAccountSupport() { |
| | | ChargeAccountSupport support = new ChargeAccountSupport(); |
| | | ReflectionTestUtils.setField(support, "accountingStrategyDetailClient", mock(AccountingStrategyDetailClient.class)); |
| | | ReflectionTestUtils.setField(support, "groupCompanyClient", mock(GroupCompanyClient.class)); |
| | | ReflectionTestUtils.setField(support, "companyPreferentialClient", mock(CompanyPreferentialClient.class)); |
| | | ReflectionTestUtils.setField(support, "billingTemplateClient", mock(BillingTemplateClient.class)); |
| | | ReflectionTestUtils.setField(support, "companyAllocateRecordClient", mock(CompanyAllocateRecordClient.class)); |
| | | companyAllocateRecordClient = mock(CompanyAllocateRecordClient.class); |
| | | ReflectionTestUtils.setField(support, "companyAllocateRecordClient", companyAllocateRecordClient); |
| | | return support; |
| | | } |
| | | |
| | | /** 企业侧无消费流水(补偿双判据默认:卡侧与企业侧均可确认未结算) */ |
| | | private void mockNoCompanyConsumeRecord() { |
| | | R<java.util.List<String>> empty = okWithData(Collections.emptyList()); |
| | | when(companyAllocateRecordClient.listConsumedOrderCodes(any())).thenReturn(empty); |
| | | } |
| | | |
| | | private RLock mockedLock(boolean tryLockResult) throws InterruptedException { |
| | |
| | | ReflectionTestUtils.setField(chargeAccountSupport, "groupCompanyClient", groupCompanyClient); |
| | | ReflectionTestUtils.setField(service, "chargeAccountSupport", chargeAccountSupport); |
| | | ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); |
| | | when(mapper.insert(any(TCardChargingOrder.class))).thenReturn(1); |
| | | // 订单主键在真实库里由 MyBatis-Plus 回填,纯单测手工回填,保证策略快照与锁定额度回写能拿到订单ID |
| | | when(mapper.insert(any(TCardChargingOrder.class))).thenAnswer(invocation -> { |
| | | TCardChargingOrder entity = invocation.getArgument(0); |
| | | entity.setId(100L); |
| | | return 1; |
| | | }); |
| | | |
| | | TChargingGun gun = new TChargingGun(); |
| | | gun.setId(9); |
| | | gun.setStatus(2); |
| | | gun.setAccountingStrategyId(31); |
| | | R<TChargingGun> gunResult = okWithData(gun); |
| | | when(gunClient.getChargingGunByCode(any(GetChargingGunByCode.class))).thenReturn(gunResult); |
| | | |
| | |
| | | when(groupCompanyClient.lockBalance(any(), any())).thenReturn(lockBalanceResult); |
| | | R<Boolean> lockResult = okWithData(true); |
| | | when(cardClient.lockForCharging(anyLong(), anyString())).thenReturn(lockResult); |
| | | // 点5 改造后建单必须先成功写入计费策略快照(否则回滚删单):枪配置策略 31,策略与明细均可查且快照落库成功 |
| | | AccountingStrategyClient accountingStrategyClient = mock(AccountingStrategyClient.class); |
| | | AccountingStrategyDetailClient accountingStrategyDetailClient = mock(AccountingStrategyDetailClient.class); |
| | | TCardAccountingStrategyOrderService strategyOrderService = mock(TCardAccountingStrategyOrderService.class); |
| | | TCardAccountingStrategyDetailOrderService strategyDetailOrderService = mock(TCardAccountingStrategyDetailOrderService.class); |
| | | ReflectionTestUtils.setField(service, "accountingStrategyClient", accountingStrategyClient); |
| | | ReflectionTestUtils.setField(service, "accountingStrategyDetailClient", accountingStrategyDetailClient); |
| | | ReflectionTestUtils.setField(service, "cardAccountingStrategyOrderService", strategyOrderService); |
| | | ReflectionTestUtils.setField(service, "cardAccountingStrategyDetailOrderService", strategyDetailOrderService); |
| | | R<TAccountingStrategy> strategyResult = okWithData(new TAccountingStrategy()); |
| | | when(accountingStrategyClient.getAccountingStrategyById(31)).thenReturn(strategyResult); |
| | | TAccountingStrategyDetail strategyDetail = new TAccountingStrategyDetail(); |
| | | strategyDetail.setType(1); |
| | | R<java.util.List<TAccountingStrategyDetail>> strategyDetailResult = |
| | | okWithData(Collections.singletonList(strategyDetail)); |
| | | when(accountingStrategyDetailClient.getListByAccountingStrategyId(31)).thenReturn(strategyDetailResult); |
| | | when(strategyOrderService.save(any(TCardAccountingStrategyOrder.class))).thenReturn(true); |
| | | when(strategyDetailOrderService.saveBatch(any(Collection.class))).thenReturn(true); |
| | | |
| | | DeviceStartChargingDTO dto = new DeviceStartChargingDTO(); |
| | | dto.setChargingPileCode("P1"); |
| | |
| | | when(mapper.update(any(), any())).thenReturn(1); |
| | | R<java.util.List<String>> noDeductRecords = okWithData(Collections.emptyList()); |
| | | when(cardClient.listDeductedOrderCodes(any())).thenReturn(noDeductRecords); |
| | | mockNoCompanyConsumeRecord(); |
| | | |
| | | assertEquals(1, service.recoverFinishedOrdersWithoutDeduct()); |
| | | verify(cardClient, times(2)).listDeductedOrderCodes(any()); |
| | |
| | | when(mapper.selectList(any())).thenReturn(Collections.singletonList(candidate)); |
| | | R<java.util.List<String>> noDeductRecords = okWithData(Collections.emptyList()); |
| | | when(cardClient.listDeductedOrderCodes(any())).thenReturn(noDeductRecords); |
| | | mockNoCompanyConsumeRecord(); |
| | | |
| | | assertEquals(0, service.recoverFinishedOrdersWithoutDeduct()); |
| | | verify(cardClient, times(1)).listDeductedOrderCodes(any()); |
| | | verify(mapper, never()).update(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void compensationSkipsFinishedOrderWithCompanyConsumeRecord() { |
| | | // 共享模式订单的消费流水落在企业侧:卡侧无流水但企业侧命中时必须保持已完成状态,不得回退造成重复扣款 |
| | | TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport()); |
| | | TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); |
| | | ChargingCardClient cardClient = mock(ChargingCardClient.class); |
| | | ReflectionTestUtils.setField(service, "baseMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); |
| | | |
| | | TCardChargingOrder shared = new TCardChargingOrder(); |
| | | shared.setId(6L); |
| | | shared.setCode("SD-SHARED"); |
| | | shared.setStatus(5); |
| | | shared.setCardId(5L); |
| | | shared.setOrderAmount(BigDecimal.ONE); |
| | | when(mapper.selectList(any())).thenReturn(Collections.singletonList(shared)); |
| | | R<java.util.List<String>> noDeductRecords = okWithData(Collections.emptyList()); |
| | | when(cardClient.listDeductedOrderCodes(any())).thenReturn(noDeductRecords); |
| | | R<java.util.List<String>> consumeRecords = okWithData(Collections.singletonList("SD-SHARED")); |
| | | when(companyAllocateRecordClient.listConsumedOrderCodes(any())).thenReturn(consumeRecords); |
| | | |
| | | assertEquals(0, service.recoverFinishedOrdersWithoutDeduct()); |
| | | verify(cardClient, times(1)).listDeductedOrderCodes(any()); |
| | | verify(mapper, never()).update(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void compensationKeepsFinishedStatusWhenCompanyConsumeQueryFails() { |
| | | // 企业侧流水查询失败说明无法确认结算结果,必须保持原状态等待下轮,避免误回退后重复扣款 |
| | | TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport()); |
| | | TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); |
| | | ChargingCardClient cardClient = mock(ChargingCardClient.class); |
| | | ReflectionTestUtils.setField(service, "baseMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); |
| | | |
| | | TCardChargingOrder candidate = new TCardChargingOrder(); |
| | | candidate.setId(7L); |
| | | candidate.setCode("SD-CONSUME-UNKNOWN"); |
| | | candidate.setStatus(5); |
| | | candidate.setCardId(5L); |
| | | candidate.setOrderAmount(BigDecimal.ONE); |
| | | when(mapper.selectList(any())).thenReturn(Collections.singletonList(candidate)); |
| | | R<java.util.List<String>> noDeductRecords = okWithData(Collections.emptyList()); |
| | | when(cardClient.listDeductedOrderCodes(any())).thenReturn(noDeductRecords); |
| | | when(companyAllocateRecordClient.listConsumedOrderCodes(any())).thenReturn(R.fail("account unavailable")); |
| | | |
| | | assertEquals(0, service.recoverFinishedOrdersWithoutDeduct()); |
| | | verify(mapper, never()).update(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void rollsBackOrderWhenNoAccountingStrategyConfigured() throws InterruptedException { |
| | | // 点5:枪与站点均未配置计费策略(strategyId 为空)时必须回滚删单并回告启动失败,禁止产生无策略订单 |
| | | TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); |
| | | TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); |
| | | ChargingCardClient cardClient = mock(ChargingCardClient.class); |
| | | ChargingGunClient gunClient = mock(ChargingGunClient.class); |
| | | GroupCompanyClient groupCompanyClient = mock(GroupCompanyClient.class); |
| | | ChargeAuthorizationClient chargeAuthorizationClient = mock(ChargeAuthorizationClient.class); |
| | | ReflectionTestUtils.setField(service, "baseMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); |
| | | ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); |
| | | ReflectionTestUtils.setField(service, "chargeAuthorizationClient", chargeAuthorizationClient); |
| | | ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient); |
| | | ChargeAccountSupport chargeAccountSupport = newChargeAccountSupport(); |
| | | ReflectionTestUtils.setField(chargeAccountSupport, "groupCompanyClient", groupCompanyClient); |
| | | ReflectionTestUtils.setField(service, "chargeAccountSupport", chargeAccountSupport); |
| | | ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); |
| | | when(mapper.insert(any(TCardChargingOrder.class))).thenAnswer(invocation -> { |
| | | TCardChargingOrder entity = invocation.getArgument(0); |
| | | entity.setId(101L); |
| | | return 1; |
| | | }); |
| | | when(mapper.deleteById(anyLong())).thenReturn(1); |
| | | |
| | | TChargingGun gun = new TChargingGun(); |
| | | gun.setId(9); |
| | | gun.setStatus(2); |
| | | R<TChargingGun> gunResult = okWithData(gun); |
| | | when(gunClient.getChargingGunByCode(any(GetChargingGunByCode.class))).thenReturn(gunResult); |
| | | |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setId(5L); |
| | | card.setCardNo("C001"); |
| | | card.setCompanyId(77); |
| | | card.setBalance(new BigDecimal("100.00")); |
| | | R<TChargingCard> verifyResult = okWithData(card); |
| | | when(cardClient.verifyCharging(any(ChargingCardAuthDTO.class))).thenReturn(verifyResult); |
| | | TGroupCompany company = new TGroupCompany(); |
| | | company.setId(77); |
| | | company.setStatus(1); |
| | | company.setAccountMode(1); |
| | | company.setAvailableBalance(new BigDecimal("100.00")); |
| | | R<TGroupCompany> companyResult = okWithData(company); |
| | | when(groupCompanyClient.getCompanyById(77)).thenReturn(companyResult); |
| | | |
| | | DeviceStartChargingDTO dto = new DeviceStartChargingDTO(); |
| | | dto.setChargingPileCode("P1"); |
| | | dto.setChargingGunCode("G1"); |
| | | dto.setAccount("C001"); |
| | | DeviceStartChargingVO result = service.startCardCharging(dto); |
| | | |
| | | assertEquals(0, result.getAuthentication()); |
| | | // 订单回滚删除,且未锁定账户额度、未锁卡 |
| | | verify(mapper).deleteById(anyLong()); |
| | | verify(groupCompanyClient, never()).lockBalance(any(), any()); |
| | | verify(cardClient, never()).lockForCharging(anyLong(), anyString()); |
| | | } |
| | | |
| | | @Test |
| | | void sharedModeSettlementPassesOrderCodeAndSkipsLocalConsumeRecord() { |
| | | // 共享模式:结算传订单号启用账户侧幂等(账户侧写企业消费流水),订单侧不得再写一份导致重复记账 |
| | | TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport()); |
| | | GroupCompanyClient groupCompanyClient = mock(GroupCompanyClient.class); |
| | | ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient); |
| | | |
| | | TCardChargingOrder order = new TCardChargingOrder(); |
| | | order.setId(8L); |
| | | order.setCode("SD-SHARED-SETTLE"); |
| | | order.setCardId(5L); |
| | | order.setCardNo("C001"); |
| | | order.setCompanyId(77); |
| | | order.setAccountMode(ChargeAccountSupport.ACCOUNT_MODE_SHARED); |
| | | order.setLockedAmount(new BigDecimal("200.00")); |
| | | |
| | | R<BigDecimal> settleResult = okWithData(new BigDecimal("663.50")); |
| | | when(groupCompanyClient.settleBalance(eq(77), eq(new BigDecimal("200.00")), eq(new BigDecimal("36.50")), |
| | | eq("SD-SHARED-SETTLE"), eq("C001"), isNull(), eq("刷卡充电结算"))).thenReturn(settleResult); |
| | | |
| | | Boolean settled = ReflectionTestUtils.invokeMethod(service, "settleCardChargeAccount", order, |
| | | new BigDecimal("36.50"), "SD-SHARED-SETTLE"); |
| | | |
| | | assertTrue(settled); |
| | | verify(companyAllocateRecordClient, never()).recordConsume(any(), any(), any(), any(), any(), any(), any(), |
| | | any()); |
| | | } |
| | | |
| | | @Test |
| | | void allocatedModeSettlementWritesCardConsumeRecord() { |
| | | // 分配模式:扣的是卡账户余额,消费流水仍由订单侧记到该卡上(user_type=卡) |
| | | TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport()); |
| | | ChargingCardClient cardClient = mock(ChargingCardClient.class); |
| | | ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); |
| | | |
| | | TCardChargingOrder order = new TCardChargingOrder(); |
| | | order.setId(9L); |
| | | order.setCode("SD-ALLOC-SETTLE"); |
| | | order.setCardId(5L); |
| | | order.setCardNo("C001"); |
| | | order.setCompanyId(77); |
| | | order.setAccountMode(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED); |
| | | order.setLockedAmount(new BigDecimal("200.00")); |
| | | |
| | | ChargingCardDeductVO deductVO = new ChargingCardDeductVO(); |
| | | deductVO.setDuplicated(false); |
| | | deductVO.setAfterBalance(new BigDecimal("63.50")); |
| | | R<ChargingCardDeductVO> settleCardResult = okWithData(deductVO); |
| | | when(cardClient.settleForCharging(any(ChargingCardDeductDTO.class))).thenReturn(settleCardResult); |
| | | |
| | | Boolean settled = ReflectionTestUtils.invokeMethod(service, "settleCardChargeAccount", order, |
| | | new BigDecimal("36.50"), "SD-ALLOC-SETTLE"); |
| | | |
| | | assertTrue(settled); |
| | | verify(companyAllocateRecordClient).recordConsume(eq(77), eq(TCompanyAllocateRecord.USER_TYPE_CARD), |
| | | eq("C001"), eq("C001"), any(), eq(new BigDecimal("-36.50")), eq("SD-ALLOC-SETTLE"), |
| | | eq("刷卡充电结算")); |
| | | } |
| | | |
| | | @Test |
| | | void skipsUnlockWhenLockReleasedFlagAlreadySet() { |
| | | // 点2-D:lock_released 抢占未命中(标记已是 1)说明锁定额已释放过,禁止重复解锁 |
| | | TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); |
| | | TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); |
| | | GroupCompanyClient groupCompanyClient = mock(GroupCompanyClient.class); |
| | | ChargingCardClient cardClient = mock(ChargingCardClient.class); |
| | | ReflectionTestUtils.setField(service, "baseMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient); |
| | | ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); |
| | | when(mapper.update(any(), any())).thenReturn(0); |
| | | |
| | | TCardChargingOrder order = new TCardChargingOrder(); |
| | | order.setId(10L); |
| | | order.setCode("SD-RELEASED"); |
| | | order.setCompanyId(77); |
| | | order.setAccountMode(ChargeAccountSupport.ACCOUNT_MODE_SHARED); |
| | | order.setLockedAmount(new BigDecimal("200.00")); |
| | | |
| | | ReflectionTestUtils.invokeMethod(service, "unlockCardChargeContextQuietly", order, "启动关单"); |
| | | |
| | | verify(groupCompanyClient, never()).unlockBalance(any(), any()); |
| | | } |
| | | } |