package com.bms.receivable; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import java.math.BigDecimal; import java.util.List; import java.util.Set; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.util.ReflectionTestUtils; import com.bms.common.core.domain.entity.SysUser; import com.bms.common.core.domain.model.LoginUser; import com.bms.common.exception.ServiceException; import com.bms.receivable.domain.ArBill; import com.bms.receivable.domain.ArBillLog; import com.bms.receivable.mapper.ArBillDetailMapper; import com.bms.receivable.mapper.ArBillExportMapper; import com.bms.receivable.mapper.ArBillMapper; import com.bms.receivable.security.ArBillScopeResolver; import com.bms.receivable.service.IArBillLogService; import com.bms.receivable.service.impl.ArBillServiceImpl; class ArBillConfirmTest { private final ArBillMapper billMapper = mock(ArBillMapper.class); private final ArBillDetailMapper detailMapper = mock(ArBillDetailMapper.class); private final ArBillExportMapper exportMapper = mock(ArBillExportMapper.class); private final ArBillScopeResolver scope = mock(ArBillScopeResolver.class); private final IArBillLogService logService = mock(IArBillLogService.class); private final ArBillServiceImpl service = new ArBillServiceImpl(); @BeforeEach void setUp() { ReflectionTestUtils.setField(service, "billMapper", billMapper); ReflectionTestUtils.setField(service, "detailMapper", detailMapper); ReflectionTestUtils.setField(service, "exportMapper", exportMapper); ReflectionTestUtils.setField(service, "billScope", scope); ReflectionTestUtils.setField(service, "logService", logService); SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken( new LoginUser(7L, 1L, new SysUser(), Set.of()), null)); } @AfterEach void clearSecurity() { SecurityContextHolder.clearContext(); } @Test void readyDraftCanBeConfirmedWithoutReadingDetails() { ArBill draft = bill("DRAFT", "READY"); draft.setTotalAmount(new BigDecimal("123.45")); ArBill confirmed = bill("CONFIRMED", "READY"); when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(draft); when(billMapper.updateArBillStatus(11L, "CONFIRMED", null, "finance", "DRAFT")).thenReturn(1); when(billMapper.selectArBillByIdScoped(11L, null)).thenReturn(confirmed); when(exportMapper.selectByBillId(11L)).thenReturn(List.of()); assertSame(confirmed, service.confirmBill(11L, null, "finance", "request-1")); verify(scope).requireBillAccessible(11L); verifyNoInteractions(detailMapper); verify(logService).record(any(ArBillLog.class)); } @Test void unfinishedOrNonDraftBillStillCannotBeConfirmed() { when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn( bill("DRAFT", "GENERATING"), bill("CONFIRMED", "READY")); ServiceException unfinished = assertThrows(ServiceException.class, () -> service.confirmBill(11L, null, "finance", "request-1")); assertEquals("账单尚未生成完成,暂不能确认", unfinished.getMessage()); ServiceException notDraft = assertThrows(ServiceException.class, () -> service.confirmBill(11L, null, "finance", "request-2")); assertEquals("只有草稿账单可以确认,当前状态为 CONFIRMED", notDraft.getMessage()); verify(billMapper, never()).updateArBillStatus(eq(11L), any(), nullable(String.class), any(), any()); verifyNoInteractions(detailMapper); } @Test void inaccessibleBillStillCannotBeConfirmed() { when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(bill("DRAFT", "READY")); doThrow(new ServiceException("账单不存在或无权访问")).when(scope).requireBillAccessible(11L); assertThrows(ServiceException.class, () -> service.confirmBill(11L, null, "finance", "request-1")); verify(billMapper, never()).updateArBillStatus(eq(11L), any(), nullable(String.class), any(), any()); verifyNoInteractions(detailMapper); } @Test void concurrentStatusChangeStillPreventsConfirmation() { when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(bill("DRAFT", "READY")); ServiceException error = assertThrows(ServiceException.class, () -> service.confirmBill(11L, null, "finance", "request-1")); assertEquals("账单状态已发生变化,请刷新后重试", error.getMessage()); verify(logService, never()).record(any(ArBillLog.class)); verifyNoInteractions(detailMapper); } private static ArBill bill(String status, String generationStatus) { ArBill bill = new ArBill(); bill.setArBillId(11L); bill.setStatus(status); bill.setGenerationStatus(generationStatus); return bill; } }