| | |
| | | package com.ruoyi.account.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.MybatisConfiguration; |
| | | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.api.dto.ChargingCardBalanceDTO; |
| | | import com.ruoyi.account.api.dto.ChargingCardAllocateDTO; |
| | | import com.ruoyi.account.api.dto.ChargingCardAllocateItemDTO; |
| | | import com.ruoyi.account.api.dto.ChargingCardAuthDTO; |
| | | import com.ruoyi.account.api.dto.ChargingCardDeductDTO; |
| | | import com.ruoyi.account.api.dto.ChargingCardExcelDTO; |
| | | import com.ruoyi.account.api.dto.ChargingCardSaveDTO; |
| | | import com.ruoyi.account.api.dto.ChargingCardStatusDTO; |
| | | import com.ruoyi.account.api.model.TChargingCard; |
| | | import com.ruoyi.account.api.vo.ChargingCardAllocateVO; |
| | | import com.ruoyi.account.api.vo.ChargingCardDeductVO; |
| | | import com.ruoyi.account.mapper.TChargingCardMapper; |
| | | import com.ruoyi.common.core.context.SecurityContextHolder; |
| | | import com.ruoyi.common.core.exception.ServiceException; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.security.service.TokenService; |
| | | import com.ruoyi.system.api.domain.SysUser; |
| | | import com.ruoyi.system.api.feignClient.SysUserClient; |
| | | import com.ruoyi.system.api.model.LoginUser; |
| | | import com.ruoyi.common.security.utils.SecurityUtils; |
| | | import com.ruoyi.other.api.domain.TFleet; |
| | | import com.ruoyi.other.api.domain.TGroupCompany; |
| | | import com.ruoyi.other.api.feignClient.GroupCompanyClient; |
| | | import org.apache.ibatis.builder.MapperBuilderAssistant; |
| | | import org.junit.jupiter.api.AfterEach; |
| | | import org.junit.jupiter.api.BeforeAll; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.junit.jupiter.api.extension.ExtendWith; |
| | | import org.mockito.ArgumentCaptor; |
| | | import org.mockito.Mock; |
| | | import org.mockito.junit.jupiter.MockitoExtension; |
| | | import org.redisson.api.RLock; |
| | | import org.redisson.api.RedissonClient; |
| | | import org.springframework.dao.DuplicateKeyException; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | import org.springframework.transaction.support.TransactionCallback; |
| | | import org.springframework.transaction.support.TransactionTemplate; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertNull; |
| | | import static org.junit.jupiter.api.Assertions.assertSame; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.ArgumentMatchers.anyCollection; |
| | | import static org.mockito.ArgumentMatchers.anyList; |
| | | import static org.mockito.ArgumentMatchers.anyLong; |
| | | import static org.mockito.ArgumentMatchers.anyString; |
| | | import static org.mockito.ArgumentMatchers.isNull; |
| | | import static org.mockito.Mockito.doReturn; |
| | | import static org.mockito.Mockito.lenient; |
| | | import static org.mockito.Mockito.never; |
| | | import static org.mockito.Mockito.spy; |
| | | import static org.mockito.Mockito.verify; |
| | |
| | | class TChargingCardServiceImplTest { |
| | | |
| | | @Mock private TChargingCardMapper mapper; |
| | | @Mock private TokenService tokenService; |
| | | @Mock private SysUserClient sysUserClient; |
| | | @Mock private TransactionTemplate transactionTemplate; |
| | | @Mock private GroupCompanyClient groupCompanyClient; |
| | | @Mock private RedissonClient redissonClient; |
| | | @Mock private RLock quotaLock; |
| | | private TChargingCardServiceImpl service; |
| | | |
| | | @BeforeAll |
| | | static void initTableInfo() { |
| | | // MP 的 lambdaUpdate 会通过 TableInfoHelper.getTableInfo 取表元数据, |
| | | // 单元测试不启动 Spring,需要手动初始化 TableInfo,否则抛 MybatisPlusException |
| | | TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TChargingCard.class); |
| | | } |
| | | |
| | | @BeforeEach |
| | | void setUp() { |
| | | void setUp() throws InterruptedException { |
| | | service = spy(new TChargingCardServiceImpl()); |
| | | ReflectionTestUtils.setField(service, "baseMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "tokenService", tokenService); |
| | | ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient); |
| | | ReflectionTestUtils.setField(service, "transactionTemplate", transactionTemplate); |
| | | ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient); |
| | | ReflectionTestUtils.setField(service, "redissonClient", redissonClient); |
| | | lenient().when(redissonClient.getLock(anyString())).thenReturn(quotaLock); |
| | | lenient().when(quotaLock.tryLock(anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(true); |
| | | lenient().when(quotaLock.isHeldByCurrentThread()).thenReturn(true); |
| | | SecurityContextHolder.setUserId("123"); |
| | | } |
| | | |
| | |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setId(7L); |
| | | dto.setCardNo(" CARD-7 "); |
| | | dto.setBalance(new BigDecimal("999.99")); |
| | | when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0); |
| | | TChargingCard existing = new TChargingCard(); |
| | | existing.setId(7L); |
| | |
| | | } |
| | | |
| | | @Test |
| | | void updateBalanceAdjustsAndRecordsForAllCards() { |
| | | LoginUser loginUser = new LoginUser(); |
| | | loginUser.setUserid(123L); |
| | | SysUser user = new SysUser(); |
| | | user.setOperationPassword(com.ruoyi.common.security.utils.SecurityUtils.encryptPassword("pwd")); |
| | | when(tokenService.getLoginUser()).thenReturn(loginUser); |
| | | when(sysUserClient.getSysUser(123L)).thenReturn(R.ok(user)); |
| | | TChargingCard first = new TChargingCard(); |
| | | first.setId(1L); |
| | | TChargingCard second = new TChargingCard(); |
| | | second.setId(2L); |
| | | when(mapper.selectBatchIds(anyList())).thenReturn(Arrays.asList(first, second)); |
| | | when(mapper.adjustBalance(anyList(), any(BigDecimal.class), any(Integer.class), any(BigDecimal.class), any(Long.class))) |
| | | .thenReturn(2); |
| | | ChargingCardBalanceDTO dto = new ChargingCardBalanceDTO(); |
| | | dto.setIds(Arrays.asList(1L, 2L)); |
| | | dto.setType(1); |
| | | dto.setAmount(new BigDecimal("10.00")); |
| | | dto.setOperationPassword("pwd"); |
| | | dto.setRemark(" test "); |
| | | |
| | | assertTrue(service.updateBalance(dto)); |
| | | |
| | | verify(mapper).insertBalanceRecord(1L, 1, new BigDecimal("10.00"), "test", 123L); |
| | | verify(mapper).insertBalanceRecord(2L, 1, new BigDecimal("10.00"), "test", 123L); |
| | | } |
| | | |
| | | @Test |
| | | void updateBalanceRejectsMissingPasswordBeforeDatabaseUpdate() { |
| | | ChargingCardBalanceDTO dto = new ChargingCardBalanceDTO(); |
| | | dto.setIds(Collections.singletonList(1L)); |
| | | dto.setType(1); |
| | | dto.setAmount(BigDecimal.ONE); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, () -> service.updateBalance(dto)); |
| | | |
| | | assertEquals("请输入操作密码", exception.getMessage()); |
| | | verify(mapper, never()).adjustBalance(anyList(), any(), any(), any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void importCardsRejectsDuplicateRows() { |
| | | ChargingCardExcelDTO first = new ChargingCardExcelDTO(); |
| | | first.setCardNo("CARD-1"); |
| | |
| | | assertTrue(service.selectExportList(null).isEmpty()); |
| | | verify(mapper).selectExportList(any()); |
| | | } |
| | | |
| | | // ==================== V0.2 定稿:卡密码配置(仅配置密码的卡刷卡时才校验) ==================== |
| | | |
| | | @Test |
| | | void saveCardEncodesPasswordWhenProvided() { |
| | | when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0); |
| | | when(mapper.insert(any(TChargingCard.class))).thenAnswer(invocation -> 1); |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setCardNo("CARD-PWD"); |
| | | dto.setPassword(" 123456 "); |
| | | |
| | | assertTrue(service.saveCard(dto)); |
| | | |
| | | ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class); |
| | | verify(mapper).insert(card.capture()); |
| | | assertNotNull(card.getValue().getPassword()); |
| | | assertTrue(SecurityUtils.matchesPassword("123456", card.getValue().getPassword())); |
| | | assertEquals(Integer.valueOf(0), card.getValue().getChargingFlag()); |
| | | } |
| | | |
| | | @Test |
| | | void saveCardKeepsPasswordNullWhenNotConfigured() { |
| | | when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0); |
| | | when(mapper.insert(any(TChargingCard.class))).thenAnswer(invocation -> 1); |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setCardNo("CARD-NOPWD"); |
| | | |
| | | assertTrue(service.saveCard(dto)); |
| | | |
| | | ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class); |
| | | verify(mapper).insert(card.capture()); |
| | | assertEquals(null, card.getValue().getPassword()); |
| | | } |
| | | |
| | | @Test |
| | | void updateCardRejectsCardInChargingLock() { |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setId(7L); |
| | | dto.setCardNo("CARD-7"); |
| | | when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0); |
| | | TChargingCard locked = new TChargingCard(); |
| | | locked.setId(7L); |
| | | locked.setCardNo("CARD-7"); |
| | | locked.setChargingFlag(1); |
| | | when(mapper.selectById(7L)).thenReturn(locked); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, () -> service.updateCard(dto)); |
| | | |
| | | assertTrue(exception.getMessage().contains("正在充电中")); |
| | | verify(mapper, never()).updateById(any()); |
| | | } |
| | | |
| | | @Test |
| | | void updateCardResetsPasswordOnlyWhenProvided() { |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setId(7L); |
| | | dto.setCardNo("CARD-7"); |
| | | dto.setPassword("new-pwd"); |
| | | when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0); |
| | | TChargingCard existing = new TChargingCard(); |
| | | existing.setId(7L); |
| | | existing.setCardNo("CARD-7"); |
| | | when(mapper.selectById(7L)).thenReturn(existing); |
| | | when(mapper.updateById(any(TChargingCard.class))).thenReturn(1); |
| | | |
| | | assertTrue(service.updateCard(dto)); |
| | | |
| | | ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class); |
| | | verify(mapper).updateById(card.capture()); |
| | | assertNotNull(card.getValue().getPassword()); |
| | | assertTrue(SecurityUtils.matchesPassword("new-pwd", card.getValue().getPassword())); |
| | | } |
| | | |
| | | @Test |
| | | void updateCardKeepsPasswordWhenNotProvided() { |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setId(7L); |
| | | dto.setCardNo("CARD-7"); |
| | | when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0); |
| | | TChargingCard existing = new TChargingCard(); |
| | | existing.setId(7L); |
| | | existing.setCardNo("CARD-7"); |
| | | when(mapper.selectById(7L)).thenReturn(existing); |
| | | when(mapper.updateById(any(TChargingCard.class))).thenReturn(1); |
| | | |
| | | assertTrue(service.updateCard(dto)); |
| | | |
| | | ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class); |
| | | verify(mapper).updateById(card.capture()); |
| | | assertEquals(null, card.getValue().getPassword()); |
| | | } |
| | | |
| | | // ==================== V0.2 定稿:充电期间卡片管理操作互斥 ==================== |
| | | |
| | | @Test |
| | | void deleteCardsRejectsCardInChargingLock() { |
| | | TChargingCard first = new TChargingCard(); |
| | | first.setId(1L); |
| | | TChargingCard locked = new TChargingCard(); |
| | | locked.setId(2L); |
| | | locked.setCardNo("CARD-2"); |
| | | locked.setChargingFlag(1); |
| | | doReturn(Arrays.asList(first, locked)).when(service).listByIds(anyList()); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.deleteCards(Arrays.asList(1L, 2L))); |
| | | |
| | | assertTrue(exception.getMessage().contains("正在充电中")); |
| | | verify(service, never()).removeByIds(anyList()); |
| | | } |
| | | |
| | | // ==================== 批量解绑车队 ==================== |
| | | |
| | | @Test |
| | | void unbindFleetRejectsNullOrEmptyIds() { |
| | | ServiceException nullEx = assertThrows(ServiceException.class, () -> service.unbindFleet(null)); |
| | | assertEquals("请选择需要解绑车队的卡片", nullEx.getMessage()); |
| | | |
| | | ServiceException emptyEx = assertThrows(ServiceException.class, () -> service.unbindFleet(Collections.emptyList())); |
| | | assertEquals("请选择需要解绑车队的卡片", emptyEx.getMessage()); |
| | | } |
| | | |
| | | @Test |
| | | void unbindFleetRejectsMissingRecords() { |
| | | TChargingCard existing = new TChargingCard(); |
| | | existing.setId(1L); |
| | | doReturn(Collections.singletonList(existing)).when(service).listByIds(anyList()); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.unbindFleet(Arrays.asList(1L, 2L))); |
| | | |
| | | assertEquals("部分卡片不存在或已删除", exception.getMessage()); |
| | | verify(mapper, never()).update(isNull(), any(Wrapper.class)); |
| | | } |
| | | |
| | | @Test |
| | | void unbindFleetClearsFleetIdAndIgnoresUnboundRecords() { |
| | | TChargingCard bound = new TChargingCard(); |
| | | bound.setId(1L); |
| | | bound.setFleetId(5); |
| | | TChargingCard unbound = new TChargingCard(); |
| | | unbound.setId(2L); |
| | | doReturn(Arrays.asList(bound, unbound)).when(service).listByIds(anyList()); |
| | | when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1); |
| | | |
| | | int result = service.unbindFleet(Arrays.asList(1L, 2L, 2L, null)); |
| | | |
| | | assertEquals(1, result); |
| | | ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class); |
| | | verify(mapper).update(isNull(), wrapperCaptor.capture()); |
| | | String sqlSet = wrapperCaptor.getValue().getSqlSet(); |
| | | assertTrue(sqlSet.contains("fleet_id=")); |
| | | assertTrue(sqlSet.contains("update_time=")); |
| | | assertTrue(sqlSet.contains("update_by=")); |
| | | } |
| | | |
| | | @Test |
| | | void unbindFleetReturnsZeroWhenAllUnbound() { |
| | | TChargingCard unbound = new TChargingCard(); |
| | | unbound.setId(1L); |
| | | doReturn(Collections.singletonList(unbound)).when(service).listByIds(anyList()); |
| | | |
| | | int result = service.unbindFleet(Collections.singletonList(1L)); |
| | | |
| | | assertEquals(0, result); |
| | | verify(mapper, never()).update(isNull(), any(Wrapper.class)); |
| | | } |
| | | |
| | | @Test |
| | | void updateStatusRejectsCardInChargingLock() { |
| | | ChargingCardStatusDTO dto = new ChargingCardStatusDTO(); |
| | | dto.setId(1L); |
| | | dto.setStatus(2); |
| | | TChargingCard locked = new TChargingCard(); |
| | | locked.setId(1L); |
| | | locked.setCardNo("CARD-1"); |
| | | locked.setChargingFlag(1); |
| | | when(mapper.selectById(1L)).thenReturn(locked); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, () -> service.updateStatus(dto)); |
| | | |
| | | assertTrue(exception.getMessage().contains("正在充电中")); |
| | | verify(mapper, never()).updateById(any()); |
| | | } |
| | | |
| | | // ==================== 卡片所属公司/车队 + 初始分配额度 ==================== |
| | | |
| | | @Test |
| | | void saveCardDeductsCompanyBalanceWhenAllocationQuotaProvided() { |
| | | when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0); |
| | | when(mapper.insert(any(TChargingCard.class))).thenReturn(1); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | when(groupCompanyClient.getFleetById(3)).thenReturn(R.ok(fleet(3, 9, "测试车队"))); |
| | | when(groupCompanyClient.deductBalance(9, new BigDecimal("100.00"))) |
| | | .thenReturn(R.ok(new BigDecimal("900.00"))); |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setCardNo(" CARD-Q1 "); |
| | | dto.setCompanyId(9); |
| | | dto.setFleetId(3); |
| | | dto.setBalance(new BigDecimal("100")); |
| | | |
| | | assertTrue(service.saveCard(dto)); |
| | | |
| | | ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class); |
| | | verify(mapper).insert(card.capture()); |
| | | assertEquals(Integer.valueOf(9), card.getValue().getCompanyId()); |
| | | assertEquals(Integer.valueOf(3), card.getValue().getFleetId()); |
| | | assertEquals(new BigDecimal("100.00"), card.getValue().getBalance()); |
| | | verify(groupCompanyClient).deductBalance(9, new BigDecimal("100.00")); |
| | | } |
| | | |
| | | @Test |
| | | void saveCardRejectsQuotaForShareModeCompany() { |
| | | when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0); |
| | | TGroupCompany shareCompany = allocationCompany(9, "共享公司"); |
| | | shareCompany.setAccountMode(1); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(shareCompany)); |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setCardNo("CARD-Q2"); |
| | | dto.setCompanyId(9); |
| | | dto.setBalance(new BigDecimal("10.00")); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, () -> service.saveCard(dto)); |
| | | |
| | | assertEquals("仅分配模式的公司可填写初始分配额度", exception.getMessage()); |
| | | verify(groupCompanyClient, never()).deductBalance(any(), any()); |
| | | verify(mapper, never()).insert(any(TChargingCard.class)); |
| | | } |
| | | |
| | | @Test |
| | | void saveCardKeepsQuotaEmptyAndSkipsBalanceWhenCompanyMissing() { |
| | | when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0); |
| | | when(mapper.insert(any(TChargingCard.class))).thenReturn(1); |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setCardNo("CARD-Q3"); |
| | | |
| | | assertTrue(service.saveCard(dto)); |
| | | |
| | | ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class); |
| | | verify(mapper).insert(card.capture()); |
| | | assertEquals(null, card.getValue().getCompanyId()); |
| | | assertEquals(0, card.getValue().getBalance().compareTo(BigDecimal.ZERO)); |
| | | verify(groupCompanyClient, never()).deductBalance(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void saveCardRejectsFleetNotBelongingToCompany() { |
| | | when(mapper.countByCardNo(anyString(), isNull())).thenReturn(0); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | when(groupCompanyClient.getFleetById(3)).thenReturn(R.ok(fleet(3, 11, "他司车队"))); |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setCardNo("CARD-Q4"); |
| | | dto.setCompanyId(9); |
| | | dto.setFleetId(3); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, () -> service.saveCard(dto)); |
| | | |
| | | assertEquals("所属车队不属于所选公司", exception.getMessage()); |
| | | } |
| | | |
| | | @Test |
| | | void updateCardDeductsQuotaDiffWhenQuotaIncreased() { |
| | | ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 9, new BigDecimal("150.00")); |
| | | when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0); |
| | | when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("100.00"))); |
| | | when(mapper.updateById(any(TChargingCard.class))).thenReturn(1); |
| | | when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | when(groupCompanyClient.deductBalance(9, new BigDecimal("50.00"))) |
| | | .thenReturn(R.ok(new BigDecimal("50.00"))); |
| | | |
| | | assertTrue(service.updateCard(dto)); |
| | | |
| | | verify(groupCompanyClient).deductBalance(9, new BigDecimal("50.00")); |
| | | verify(groupCompanyClient, never()).refundBalance(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void updateCardRefundsQuotaDiffWhenQuotaReduced() { |
| | | ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 9, new BigDecimal("80.00")); |
| | | when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0); |
| | | when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("150.00"))); |
| | | when(mapper.updateById(any(TChargingCard.class))).thenReturn(1); |
| | | when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | when(groupCompanyClient.refundBalance(9, new BigDecimal("70.00"))) |
| | | .thenReturn(R.ok(new BigDecimal("70.00"))); |
| | | |
| | | assertTrue(service.updateCard(dto)); |
| | | |
| | | verify(groupCompanyClient).refundBalance(9, new BigDecimal("70.00")); |
| | | verify(groupCompanyClient, never()).deductBalance(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void updateCardSwitchesCompanyRefundsOldAndDeductsNew() { |
| | | ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 11, new BigDecimal("60.00")); |
| | | when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0); |
| | | when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("100.00"))); |
| | | when(mapper.updateById(any(TChargingCard.class))).thenReturn(1); |
| | | when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1); |
| | | when(groupCompanyClient.getCompanyById(11)).thenReturn(R.ok(allocationCompany(11, "新公司"))); |
| | | when(groupCompanyClient.refundBalance(9, new BigDecimal("100.00"))) |
| | | .thenReturn(R.ok(new BigDecimal("100.00"))); |
| | | when(groupCompanyClient.deductBalance(11, new BigDecimal("60.00"))) |
| | | .thenReturn(R.ok(new BigDecimal("60.00"))); |
| | | |
| | | assertTrue(service.updateCard(dto)); |
| | | |
| | | verify(groupCompanyClient).refundBalance(9, new BigDecimal("100.00")); |
| | | verify(groupCompanyClient).deductBalance(11, new BigDecimal("60.00")); |
| | | } |
| | | |
| | | @Test |
| | | void updateCardCompensatesRefundWhenUpdateFails() { |
| | | ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 9, new BigDecimal("150.00")); |
| | | when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0); |
| | | when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("100.00"))); |
| | | when(mapper.updateById(any(TChargingCard.class))).thenReturn(0); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | when(groupCompanyClient.deductBalance(9, new BigDecimal("50.00"))) |
| | | .thenReturn(R.ok(new BigDecimal("100.00"))); |
| | | when(groupCompanyClient.refundBalance(9, new BigDecimal("50.00"))) |
| | | .thenReturn(R.ok(new BigDecimal("150.00"))); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, () -> service.updateCard(dto)); |
| | | |
| | | assertEquals("修改卡片失败", exception.getMessage()); |
| | | verify(groupCompanyClient).refundBalance(9, new BigDecimal("50.00")); |
| | | } |
| | | |
| | | @Test |
| | | void updateCardRejectsWhenQuotaLockUnavailable() throws InterruptedException { |
| | | ChargingCardSaveDTO dto = quotaUpdateDto(7L, "CARD-7", 9, new BigDecimal("150.00")); |
| | | when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0); |
| | | when(mapper.selectById(7L)).thenReturn(cardWithQuota(7L, 9, new BigDecimal("100.00"))); |
| | | when(quotaLock.tryLock(anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(false); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, () -> service.updateCard(dto)); |
| | | |
| | | assertEquals("该卡片额度正在处理中,请稍后重试", exception.getMessage()); |
| | | verify(groupCompanyClient, never()).deductBalance(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void selectCardPageFillsCompanyAndFleetNames() { |
| | | TChargingCard record = cardWithQuota(1L, 9, new BigDecimal("10.00")); |
| | | record.setFleetId(3); |
| | | Page<TChargingCard> page = new Page<>(1, 10); |
| | | page.setRecords(Collections.singletonList(record)); |
| | | when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page); |
| | | when(groupCompanyClient.listCompaniesByIds(anyList())) |
| | | .thenReturn(R.ok(Collections.singletonList(allocationCompany(9, "测试公司")))); |
| | | when(groupCompanyClient.listFleetsByIds(anyList())) |
| | | .thenReturn(R.ok(Collections.singletonList(fleet(3, 9, "测试车队")))); |
| | | |
| | | IPage<TChargingCard> result = service.selectCardPage(null); |
| | | |
| | | TChargingCard filled = result.getRecords().get(0); |
| | | assertEquals("测试公司", filled.getCompanyName()); |
| | | assertEquals("测试车队", filled.getFleetName()); |
| | | // 分配模式:可用余额取卡片自身余额,非负为正常、无超额 |
| | | assertEquals(2, filled.getAccountMode()); |
| | | assertEquals(0, new BigDecimal("10.00").compareTo(filled.getAvailableBalance())); |
| | | assertNull(filled.getOverrun()); |
| | | assertEquals(1, filled.getAccountStatus()); |
| | | } |
| | | |
| | | @Test |
| | | void selectCardPageUsesCompanyBalanceForSharedModeCard() { |
| | | TChargingCard record = cardWithQuota(2L, 9, null); |
| | | Page<TChargingCard> page = new Page<>(1, 10); |
| | | page.setRecords(Collections.singletonList(record)); |
| | | when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page); |
| | | TGroupCompany shared = new TGroupCompany(); |
| | | shared.setId(9); |
| | | shared.setName("共享公司"); |
| | | shared.setAccountMode(1); |
| | | shared.setAvailableBalance(new BigDecimal("100.00")); |
| | | when(groupCompanyClient.listCompaniesByIds(anyList())) |
| | | .thenReturn(R.ok(Collections.singletonList(shared))); |
| | | |
| | | IPage<TChargingCard> result = service.selectCardPage(null); |
| | | |
| | | TChargingCard filled = result.getRecords().get(0); |
| | | assertEquals(1, filled.getAccountMode()); |
| | | assertEquals(0, new BigDecimal("100.00").compareTo(filled.getAvailableBalance())); |
| | | assertNull(filled.getOverrun()); |
| | | assertEquals(1, filled.getAccountStatus()); |
| | | } |
| | | |
| | | @Test |
| | | void selectCardPageComputesOverrunForNegativeCardBalance() { |
| | | TChargingCard record = cardWithQuota(3L, 9, new BigDecimal("-5.50")); |
| | | Page<TChargingCard> page = new Page<>(1, 10); |
| | | page.setRecords(Collections.singletonList(record)); |
| | | when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page); |
| | | when(groupCompanyClient.listCompaniesByIds(anyList())) |
| | | .thenReturn(R.ok(Collections.singletonList(allocationCompany(9, "分配公司")))); |
| | | |
| | | IPage<TChargingCard> result = service.selectCardPage(null); |
| | | |
| | | TChargingCard filled = result.getRecords().get(0); |
| | | assertEquals(0, new BigDecimal("-5.50").compareTo(filled.getAvailableBalance())); |
| | | assertEquals(0, new BigDecimal("5.50").compareTo(filled.getOverrun())); |
| | | assertEquals(2, filled.getAccountStatus()); |
| | | } |
| | | |
| | | @Test |
| | | void selectCardPageLeavesAccountFieldsEmptyWhenCompanyUnavailable() { |
| | | TChargingCard record = cardWithQuota(4L, 9, new BigDecimal("8.00")); |
| | | Page<TChargingCard> page = new Page<>(1, 10); |
| | | page.setRecords(Collections.singletonList(record)); |
| | | when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page); |
| | | when(groupCompanyClient.listCompaniesByIds(anyList())).thenReturn(R.fail("远程不可用")); |
| | | |
| | | IPage<TChargingCard> result = service.selectCardPage(null); |
| | | |
| | | TChargingCard filled = result.getRecords().get(0); |
| | | assertNull(filled.getCompanyName()); |
| | | assertNull(filled.getAccountMode()); |
| | | assertNull(filled.getAvailableBalance()); |
| | | assertNull(filled.getOverrun()); |
| | | assertNull(filled.getAccountStatus()); |
| | | } |
| | | |
| | | @Test |
| | | void selectExportListFillsCompanyFleetAndAccountInfo() { |
| | | ChargingCardExcelDTO item = new ChargingCardExcelDTO(); |
| | | item.setCardNo("CARD-9"); |
| | | item.setCompanyId(9); |
| | | item.setFleetId(3); |
| | | item.setBalance(new BigDecimal("20.00")); |
| | | when(mapper.selectExportList(any())).thenReturn(Collections.singletonList(item)); |
| | | when(groupCompanyClient.listCompaniesByIds(anyList())) |
| | | .thenReturn(R.ok(Collections.singletonList(allocationCompany(9, "测试公司")))); |
| | | when(groupCompanyClient.listFleetsByIds(anyList())) |
| | | .thenReturn(R.ok(Collections.singletonList(fleet(3, 9, "测试车队")))); |
| | | |
| | | List<ChargingCardExcelDTO> result = service.selectExportList(null); |
| | | |
| | | assertEquals("测试公司", result.get(0).getCompanyName()); |
| | | assertEquals("测试车队", result.get(0).getFleetName()); |
| | | assertEquals(2, result.get(0).getAccountMode()); |
| | | assertEquals(0, new BigDecimal("20.00").compareTo(result.get(0).getAvailableBalance())); |
| | | assertNull(result.get(0).getOverrun()); |
| | | assertEquals(1, result.get(0).getAccountStatus()); |
| | | } |
| | | |
| | | private TGroupCompany allocationCompany(Integer id, String name) { |
| | | TGroupCompany company = new TGroupCompany(); |
| | | company.setId(id); |
| | | company.setName(name); |
| | | company.setAccountMode(2); |
| | | company.setStatus(1); |
| | | return company; |
| | | } |
| | | |
| | | private TFleet fleet(Integer id, Integer companyId, String name) { |
| | | TFleet fleet = new TFleet(); |
| | | fleet.setId(id); |
| | | fleet.setCompanyId(companyId); |
| | | fleet.setFleetName(name); |
| | | return fleet; |
| | | } |
| | | |
| | | private TChargingCard cardWithQuota(Long id, Integer companyId, BigDecimal quota) { |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setId(id); |
| | | card.setCompanyId(companyId); |
| | | card.setBalance(quota); |
| | | return card; |
| | | } |
| | | |
| | | private ChargingCardSaveDTO quotaUpdateDto(Long id, String cardNo, Integer companyId, BigDecimal quota) { |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setId(id); |
| | | dto.setCardNo(cardNo); |
| | | dto.setCompanyId(companyId); |
| | | dto.setBalance(quota); |
| | | return dto; |
| | | } |
| | | |
| | | // ==================== V0.2 定稿:刷卡启动鉴权(条件密码校验) ==================== |
| | | |
| | | @Test |
| | | void verifyCardForChargingPassesWithoutPasswordConfigured() { |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setId(1L); |
| | | card.setCardNo("CARD-NOPWD"); |
| | | card.setStatus(1); |
| | | card.setPassword(null); |
| | | when(mapper.selectOne(any(Wrapper.class))).thenReturn(card); |
| | | |
| | | TChargingCard result = service.verifyCardForCharging(authDto("CARD-NOPWD", "whatever")); |
| | | |
| | | assertSame(card, result); |
| | | } |
| | | |
| | | @Test |
| | | void verifyCardForChargingPassesWithEmptyPasswordWhenNotConfigured() { |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setId(1L); |
| | | card.setCardNo("CARD-NOPWD"); |
| | | card.setStatus(1); |
| | | card.setPassword(""); |
| | | when(mapper.selectOne(any(Wrapper.class))).thenReturn(card); |
| | | |
| | | TChargingCard result = service.verifyCardForCharging(authDto("CARD-NOPWD", null)); |
| | | |
| | | assertSame(card, result); |
| | | } |
| | | |
| | | @Test |
| | | void verifyCardForChargingRequiresPasswordWhenConfigured() { |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setId(1L); |
| | | card.setCardNo("CARD-PWD"); |
| | | card.setStatus(1); |
| | | card.setPassword(SecurityUtils.encryptPassword("123456")); |
| | | when(mapper.selectOne(any(Wrapper.class))).thenReturn(card); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.verifyCardForCharging(authDto("CARD-PWD", null))); |
| | | |
| | | assertEquals("卡片已配置密码,请输入卡密码", exception.getMessage()); |
| | | } |
| | | |
| | | @Test |
| | | void verifyCardForChargingRejectsWrongPasswordWhenConfigured() { |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setId(1L); |
| | | card.setCardNo("CARD-PWD"); |
| | | card.setStatus(1); |
| | | card.setPassword(SecurityUtils.encryptPassword("123456")); |
| | | when(mapper.selectOne(any(Wrapper.class))).thenReturn(card); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.verifyCardForCharging(authDto("CARD-PWD", "bad-pwd"))); |
| | | |
| | | assertEquals("卡密码错误", exception.getMessage()); |
| | | } |
| | | |
| | | @Test |
| | | void verifyCardForChargingRejectsDisabledCard() { |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setId(1L); |
| | | card.setCardNo("CARD-1"); |
| | | card.setStatus(2); |
| | | when(mapper.selectOne(any(Wrapper.class))).thenReturn(card); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.verifyCardForCharging(authDto("CARD-1", null))); |
| | | |
| | | assertEquals("卡片已禁用,无法启动充电", exception.getMessage()); |
| | | } |
| | | |
| | | @Test |
| | | void verifyCardForChargingRejectsMissingCard() { |
| | | when(mapper.selectOne(any(Wrapper.class))).thenReturn(null); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.verifyCardForCharging(authDto("NO-SUCH", null))); |
| | | |
| | | assertEquals("卡片不存在或已删除", exception.getMessage()); |
| | | } |
| | | |
| | | // ==================== V0.2 定稿:充电锁定 / 解锁 ==================== |
| | | |
| | | @Test |
| | | void lockCardForChargingReturnsMapperResult() { |
| | | when(mapper.lockCardForCharging(1L, "SD20260909001")).thenReturn(1); |
| | | assertTrue(service.lockCardForCharging(1L, "SD20260909001")); |
| | | |
| | | when(mapper.lockCardForCharging(1L, "SD20260909001")).thenReturn(0); |
| | | assertEquals(false, service.lockCardForCharging(1L, "SD20260909001")); |
| | | } |
| | | |
| | | @Test |
| | | void unlockCardForChargingIsIdempotentWhenAlreadyUnlocked() { |
| | | when(mapper.unlockCardForCharging(1L, "SD20260909001")).thenReturn(0); |
| | | TChargingCard unlocked = new TChargingCard(); |
| | | unlocked.setId(1L); |
| | | unlocked.setChargingOrderCode(null); |
| | | when(mapper.selectById(1L)).thenReturn(unlocked); |
| | | |
| | | assertTrue(service.unlockCardForCharging(1L, "SD20260909001")); |
| | | } |
| | | |
| | | @Test |
| | | void unlockCardForChargingRefusesWhenOccupiedByAnotherOrder() { |
| | | when(mapper.unlockCardForCharging(1L, "SD-OLD")).thenReturn(0); |
| | | TChargingCard occupied = new TChargingCard(); |
| | | occupied.setId(1L); |
| | | occupied.setChargingOrderCode("SD-NEW"); |
| | | when(mapper.selectById(1L)).thenReturn(occupied); |
| | | |
| | | assertEquals(false, service.unlockCardForCharging(1L, "SD-OLD")); |
| | | } |
| | | |
| | | // ==================== V0.2 定稿:最终结算扣款(幂等 + 允许负余额) ==================== |
| | | |
| | | @Test |
| | | void deductBalanceForChargingAllowsNegativeBalance() { |
| | | stubTransaction(); |
| | | TChargingCard beforeCard = new TChargingCard(); |
| | | beforeCard.setId(1L); |
| | | beforeCard.setCardNo("CARD-1"); |
| | | beforeCard.setBalance(new BigDecimal("5.00")); |
| | | TChargingCard afterCard = new TChargingCard(); |
| | | afterCard.setId(1L); |
| | | afterCard.setCardNo("CARD-1"); |
| | | afterCard.setBalance(new BigDecimal("-5.00")); |
| | | when(mapper.selectById(1L)).thenReturn(beforeCard, afterCard); |
| | | when(mapper.insertOrderDeductRecord(1L, new BigDecimal("10.00"), "SD20260909001", 0L)).thenReturn(1); |
| | | when(mapper.deductBalanceByCardId(1L, new BigDecimal("10.00"))).thenReturn(1); |
| | | |
| | | ChargingCardDeductVO result = service.deductBalanceForCharging( |
| | | deductDto(1L, "SD20260909001", new BigDecimal("10.00"))); |
| | | |
| | | assertEquals(false, result.isDuplicated()); |
| | | assertEquals(0, result.getAfterBalance().compareTo(new BigDecimal("-5.00"))); |
| | | verify(mapper).insertOrderDeductRecord(1L, new BigDecimal("10.00"), "SD20260909001", 0L); |
| | | verify(mapper).deductBalanceByCardId(1L, new BigDecimal("10.00")); |
| | | } |
| | | |
| | | @Test |
| | | void deductBalanceForChargingDuplicateRequestDoesNotDeductTwice() { |
| | | stubTransaction(); |
| | | TChargingCard beforeCard = new TChargingCard(); |
| | | beforeCard.setId(1L); |
| | | beforeCard.setCardNo("CARD-1"); |
| | | beforeCard.setBalance(new BigDecimal("-5.00")); |
| | | when(mapper.selectById(1L)).thenReturn(beforeCard); |
| | | when(mapper.insertOrderDeductRecord(1L, new BigDecimal("10.00"), "SD20260909001", 0L)) |
| | | .thenThrow(new DuplicateKeyException("uk_order_code")); |
| | | |
| | | ChargingCardDeductVO result = service.deductBalanceForCharging( |
| | | deductDto(1L, "SD20260909001", new BigDecimal("10.00"))); |
| | | |
| | | assertEquals(true, result.isDuplicated()); |
| | | verify(mapper, never()).deductBalanceByCardId(anyLong(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void listDeductedOrderCodesNormalizesInputAndQueriesMapper() { |
| | | when(mapper.selectDeductedOrderCodes(Arrays.asList("SD1", "SD2"))) |
| | | .thenReturn(Collections.singletonList("SD1")); |
| | | |
| | | assertEquals(Collections.singletonList("SD1"), |
| | | service.listDeductedOrderCodes(Arrays.asList(" SD1 ", "", null, "SD1", "SD2"))); |
| | | verify(mapper).selectDeductedOrderCodes(Arrays.asList("SD1", "SD2")); |
| | | } |
| | | |
| | | @Test |
| | | void listDeductedOrderCodesSkipsDatabaseForEmptyInput() { |
| | | assertTrue(service.listDeductedOrderCodes(Collections.emptyList()).isEmpty()); |
| | | verify(mapper, never()).selectDeductedOrderCodes(anyList()); |
| | | } |
| | | |
| | | @Test |
| | | void deductBalanceForChargingRejectsMissingCard() { |
| | | stubTransaction(); |
| | | when(mapper.selectById(1L)).thenReturn(null); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.deductBalanceForCharging(deductDto(1L, "SD20260909001", new BigDecimal("10.00")))); |
| | | |
| | | assertEquals("卡片不存在或已删除", exception.getMessage()); |
| | | } |
| | | |
| | | @Test |
| | | void deductBalanceForChargingRejectsInvalidAmount() { |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.deductBalanceForCharging(deductDto(1L, "SD20260909001", new BigDecimal("0")))); |
| | | |
| | | assertEquals("扣款金额必须大于0", exception.getMessage()); |
| | | } |
| | | |
| | | private void stubTransaction() { |
| | | when(transactionTemplate.execute(any(TransactionCallback.class))).thenAnswer(invocation -> { |
| | | TransactionCallback<Object> callback = invocation.getArgument(0); |
| | | return callback.doInTransaction(null); |
| | | }); |
| | | } |
| | | |
| | | private ChargingCardAuthDTO authDto(String cardNo, String password) { |
| | | ChargingCardAuthDTO dto = new ChargingCardAuthDTO(); |
| | | dto.setCardNo(cardNo); |
| | | dto.setPassword(password); |
| | | return dto; |
| | | } |
| | | |
| | | private ChargingCardDeductDTO deductDto(Long cardId, String orderCode, BigDecimal amount) { |
| | | ChargingCardDeductDTO dto = new ChargingCardDeductDTO(); |
| | | dto.setCardId(cardId); |
| | | dto.setOrderCode(orderCode); |
| | | dto.setAmount(amount); |
| | | return dto; |
| | | } |
| | | |
| | | // ==================== 卡片额度分配 ==================== |
| | | |
| | | @Test |
| | | void getAllocateOverviewNormalizesAmounts() { |
| | | TGroupCompany company = allocationCompany(9, "测试公司"); |
| | | company.setAvailableBalance(new BigDecimal("8420.5")); |
| | | company.setTotalRechargeAmount(null); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(company)); |
| | | |
| | | ChargingCardAllocateVO vo = service.getAllocateOverview(9); |
| | | |
| | | assertEquals(Integer.valueOf(9), vo.getCompanyId()); |
| | | assertEquals("测试公司", vo.getCompanyName()); |
| | | assertEquals(new BigDecimal("8420.50"), vo.getAvailableBalance()); |
| | | assertEquals(new BigDecimal("0.00"), vo.getTotalRechargeAmount()); |
| | | } |
| | | |
| | | @Test |
| | | void getAllocateOverviewRejectsMissingCompanyId() { |
| | | ServiceException exception = assertThrows(ServiceException.class, () -> service.getAllocateOverview(null)); |
| | | |
| | | assertEquals("请选择公司", exception.getMessage()); |
| | | } |
| | | |
| | | @Test |
| | | void allocateQuotaAddsCardBalanceAndDeductsCompanyBalance() { |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | TChargingCard first = allocateCard(1L, 9, "CARD-1", "10.00"); |
| | | TChargingCard second = allocateCard(2L, 9, "CARD-2", "20.00"); |
| | | doReturn(Arrays.asList(first, second)).when(service).listByIds(anyCollection()); |
| | | when(mapper.adjustBalance(anyList(), any(BigDecimal.class), any(Integer.class), any(BigDecimal.class), |
| | | any(Long.class))).thenReturn(1); |
| | | when(mapper.insertBalanceRecord(anyLong(), any(Integer.class), any(BigDecimal.class), any(), any(Long.class))) |
| | | .thenReturn(1); |
| | | when(groupCompanyClient.deductBalance(9, new BigDecimal("30.00"))).thenReturn(R.ok(new BigDecimal("970.00"))); |
| | | |
| | | ChargingCardAllocateVO vo = service.allocateQuota(allocateDto(9, |
| | | allocateItem(1L, "10.00"), allocateItem(2L, "20.00"))); |
| | | |
| | | assertEquals(new BigDecimal("970.00"), vo.getAvailableBalance()); |
| | | assertEquals("测试公司", vo.getCompanyName()); |
| | | verify(groupCompanyClient).deductBalance(9, new BigDecimal("30.00")); |
| | | verify(mapper).insertBalanceRecord(1L, 1, new BigDecimal("10.00"), "额度分配(公司:测试公司)", 123L); |
| | | verify(mapper).insertBalanceRecord(2L, 1, new BigDecimal("20.00"), "额度分配(公司:测试公司)", 123L); |
| | | verify(redissonClient).getLock("CHARGING_CARD_QUOTA:COMPANY:9"); |
| | | } |
| | | |
| | | @Test |
| | | void allocateQuotaRejectsSharedModeCompany() { |
| | | TGroupCompany shared = allocationCompany(9, "共享公司"); |
| | | shared.setAccountMode(1); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(shared)); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00")))); |
| | | |
| | | assertEquals("该公司为共享模式,不支持额度分配", exception.getMessage()); |
| | | verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class)); |
| | | } |
| | | |
| | | @Test |
| | | void allocateQuotaRejectsCardOfAnotherCompany() { |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | TChargingCard foreign = allocateCard(1L, 8, "CARD-1", "10.00"); |
| | | doReturn(Collections.singletonList(foreign)).when(service).listByIds(anyCollection()); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00")))); |
| | | |
| | | assertTrue(exception.getMessage().contains("不属于所选公司")); |
| | | verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class)); |
| | | } |
| | | |
| | | @Test |
| | | void allocateQuotaRefundsCompanyBalanceWhenCardUpdateFails() { |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | TChargingCard card = allocateCard(1L, 9, "CARD-1", "10.00"); |
| | | doReturn(Collections.singletonList(card)).when(service).listByIds(anyCollection()); |
| | | when(groupCompanyClient.deductBalance(9, new BigDecimal("10.00"))) |
| | | .thenReturn(R.ok(new BigDecimal("990.00"))); |
| | | when(mapper.adjustBalance(anyList(), any(BigDecimal.class), any(Integer.class), any(BigDecimal.class), |
| | | any(Long.class))).thenReturn(0); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00")))); |
| | | |
| | | assertTrue(exception.getMessage().contains("余额分配失败")); |
| | | verify(groupCompanyClient).refundBalance(9, new BigDecimal("10.00")); |
| | | } |
| | | |
| | | @Test |
| | | void allocateQuotaReportsInsufficientCompanyBalance() { |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | TChargingCard card = allocateCard(1L, 9, "CARD-1", "0.00"); |
| | | doReturn(Collections.singletonList(card)).when(service).listByIds(anyCollection()); |
| | | when(groupCompanyClient.deductBalance(9, new BigDecimal("500.00"))).thenReturn(R.fail("公司可用余额不足")); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "500.00")))); |
| | | |
| | | assertEquals("公司可分配余额不足,无法完成本次分配", exception.getMessage()); |
| | | verify(mapper, never()).adjustBalance(anyList(), any(BigDecimal.class), any(Integer.class), |
| | | any(BigDecimal.class), any(Long.class)); |
| | | } |
| | | |
| | | @Test |
| | | void allocateQuotaRejectsInvalidAmount() { |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "0")))); |
| | | |
| | | assertEquals("分配金额必须大于0", exception.getMessage()); |
| | | verify(groupCompanyClient, never()).getCompanyById(any(Integer.class)); |
| | | } |
| | | |
| | | @Test |
| | | void allocateQuotaRejectsDuplicateCards() { |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00"), allocateItem(1L, "5.00")))); |
| | | |
| | | assertEquals("分配明细中存在重复卡片", exception.getMessage()); |
| | | } |
| | | |
| | | @Test |
| | | void allocateQuotaRejectsChargingLockedCard() { |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | TChargingCard card = allocateCard(1L, 9, "CARD-1", "10.00"); |
| | | card.setChargingFlag(1); |
| | | doReturn(Collections.singletonList(card)).when(service).listByIds(anyCollection()); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00")))); |
| | | |
| | | assertTrue(exception.getMessage().contains("充电中")); |
| | | verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class)); |
| | | } |
| | | |
| | | private TChargingCard allocateCard(Long id, Integer companyId, String cardNo, String balance) { |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setId(id); |
| | | card.setCompanyId(companyId); |
| | | card.setCardNo(cardNo); |
| | | card.setBalance(new BigDecimal(balance)); |
| | | card.setStatus(1); |
| | | card.setChargingFlag(0); |
| | | return card; |
| | | } |
| | | |
| | | private ChargingCardAllocateItemDTO allocateItem(Long cardId, String amount) { |
| | | ChargingCardAllocateItemDTO item = new ChargingCardAllocateItemDTO(); |
| | | item.setCardId(cardId); |
| | | item.setAmount(new BigDecimal(amount)); |
| | | return item; |
| | | } |
| | | |
| | | private ChargingCardAllocateDTO allocateDto(Integer companyId, ChargingCardAllocateItemDTO... items) { |
| | | ChargingCardAllocateDTO dto = new ChargingCardAllocateDTO(); |
| | | dto.setCompanyId(companyId); |
| | | dto.setItems(Arrays.asList(items)); |
| | | return dto; |
| | | } |
| | | } |