实现手工发票生成后端接口
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import cn.nianxx.thhotel.ThHotelApplication;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectStoragePutRequest;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStoragePutResult;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.ObjectStorageService;
|
||||
import cn.nianxx.thhotel.platform.documentconversion.common.dto.ExcelToPdfConversionInput;
|
||||
import cn.nianxx.thhotel.platform.documentconversion.common.dto.ExcelToPdfConvertedDocument;
|
||||
import cn.nianxx.thhotel.platform.documentconversion.service.DocumentConversionException;
|
||||
import cn.nianxx.thhotel.platform.documentconversion.service.ExcelToPdfConverter;
|
||||
import cn.nianxx.thhotel.platform.hotel.repository.PlatformHotelRepository;
|
||||
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
|
||||
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
|
||||
import cn.nianxx.thhotel.platform.identity.repository.PlatformIdentityRepository;
|
||||
import cn.nianxx.thhotel.platform.identity.service.impl.AuthPasswordService;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:reservation_invoice_generation;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
|
||||
"auth.bootstrap.admin.username=invoice-admin",
|
||||
"auth.bootstrap.admin.password=Admin@123456",
|
||||
"auth.bootstrap.admin.display-name=系统管理员",
|
||||
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
|
||||
"auth.bootstrap.default-hotel-name=测试酒店",
|
||||
"auth.bootstrap.default-hotel-time-zone=Asia/Bangkok",
|
||||
"superagent.task-result.hmac-secret=test-superagent-secret",
|
||||
"mcp.enabled=true",
|
||||
"mcp.auth-token=test-mcp-token"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class ReservationInvoiceGenerationControllerTest {
|
||||
|
||||
private static final String ENDPOINT = "/api/reservation/invoices/manual-generations";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@Autowired
|
||||
private PlatformIdentityRepository identityRepository;
|
||||
@Autowired
|
||||
private PlatformHotelRepository hotelRepository;
|
||||
@Autowired
|
||||
private AuthPasswordService passwordService;
|
||||
|
||||
@MockBean
|
||||
private ExcelToPdfConverter excelToPdfConverter;
|
||||
@MockBean
|
||||
private ObjectStorageService objectStorageService;
|
||||
|
||||
@BeforeEach
|
||||
void setUpNoPermissionUser() {
|
||||
jdbcTemplate.update("delete from workflow_reservation_invoice_generation");
|
||||
PlatformUserEntity user = identityRepository.findUserByUsername("invoice-no-permission")
|
||||
.orElseGet(() -> {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
PlatformUserEntity created = new PlatformUserEntity();
|
||||
created.setUsername("invoice-no-permission");
|
||||
created.setPasswordHash(passwordService.hash("NoPerm@123456"));
|
||||
created.setDisplayName("无权限用户");
|
||||
created.setUserStatus(PlatformUserStatus.ACTIVE.name());
|
||||
created.setSuperAdmin(false);
|
||||
created.setPasswordChangedAt(now);
|
||||
created.setCreatedAt(now);
|
||||
created.setUpdatedAt(now);
|
||||
identityRepository.insertUser(created);
|
||||
return created;
|
||||
});
|
||||
hotelRepository.ensureUserHotel(user.getId(), "HOTEL-TEST", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGenerateManualInvoicePdfAndPersistGenerationRecord() throws Exception {
|
||||
byte[] pdfBytes = "%PDF-1.7\nmanual-invoice".getBytes(StandardCharsets.UTF_8);
|
||||
when(excelToPdfConverter.convert(any())).thenReturn(new ExcelToPdfConvertedDocument(
|
||||
"proforma-invoice.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
(long) pdfBytes.length,
|
||||
pdfBytes,
|
||||
88L));
|
||||
when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
|
||||
ObjectStoragePutRequest request = invocation.getArgument(0);
|
||||
return new ObjectStoragePutResult(
|
||||
request.objectKey(),
|
||||
"https://oss.example.test/" + request.objectKey(),
|
||||
request.contentType(),
|
||||
request.sizeBytes());
|
||||
});
|
||||
String token = loginToken(mockMvc, "invoice-admin", "Admin@123456");
|
||||
|
||||
performAuthorized(mockMvc, token, post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(validRequest()))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.generation_status").value("SUCCEEDED"))
|
||||
.andExpect(jsonPath("$.source_type").value("MANUAL"))
|
||||
.andExpect(jsonPath("$.hotel_id").value("HOTEL-TEST"))
|
||||
.andExpect(jsonPath("$.template_code").value("PROFORMA_INVOICE_V1"))
|
||||
.andExpect(jsonPath("$.pdf_url", containsString("https://oss.example.test/")))
|
||||
.andExpect(jsonPath("$.pdf_object_key", containsString("reservation-invoices/HOTEL-TEST/")))
|
||||
.andExpect(jsonPath("$.generated_excel_object_key", containsString("reservation-invoices/HOTEL-TEST/")))
|
||||
.andExpect(jsonPath("$.totals.subtotal").value(16822.43))
|
||||
.andExpect(jsonPath("$.totals.vat").value(1177.57))
|
||||
.andExpect(jsonPath("$.totals.total").value(18000.00))
|
||||
.andExpect(jsonPath("$.totals.currency").value("THB"))
|
||||
.andExpect(jsonPath("$.created_at", containsString("Z")))
|
||||
.andExpect(content().string(not(containsString("op.liantaitravel@gmail.com"))));
|
||||
|
||||
ArgumentCaptor<ExcelToPdfConversionInput> converterInputCaptor =
|
||||
ArgumentCaptor.forClass(ExcelToPdfConversionInput.class);
|
||||
verify(excelToPdfConverter).convert(converterInputCaptor.capture());
|
||||
assertThat(converterInputCaptor.getValue().fileName()).endsWith(".xlsx");
|
||||
assertThat(converterInputCaptor.getValue().content()).startsWith(new byte[]{0x50, 0x4B});
|
||||
|
||||
ArgumentCaptor<ObjectStoragePutRequest> storageRequestCaptor =
|
||||
ArgumentCaptor.forClass(ObjectStoragePutRequest.class);
|
||||
verify(objectStorageService, times(2)).putObject(storageRequestCaptor.capture());
|
||||
assertThat(storageRequestCaptor.getAllValues())
|
||||
.extracting(ObjectStoragePutRequest::objectKey)
|
||||
.anySatisfy(objectKey -> assertThat(objectKey).endsWith(".xlsx"))
|
||||
.anySatisfy(objectKey -> assertThat(objectKey).endsWith(".pdf"));
|
||||
|
||||
Integer generatedRows = jdbcTemplate.queryForObject(
|
||||
"select count(*) from workflow_reservation_invoice_generation where hotel_id = 'HOTEL-TEST'",
|
||||
Integer.class);
|
||||
assertThat(generatedRows).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectManualInvoiceWhenTokenMissing() throws Exception {
|
||||
mockMvc.perform(post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(validRequest()))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
|
||||
|
||||
verifyNoInteractions(excelToPdfConverter, objectStorageService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectManualInvoiceWhenPermissionMissing() throws Exception {
|
||||
String token = loginToken(mockMvc, "invoice-no-permission", "NoPerm@123456");
|
||||
|
||||
performAuthorized(mockMvc, token, post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(validRequest()))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
|
||||
|
||||
verifyNoInteractions(excelToPdfConverter, objectStorageService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectManualInvoiceWhenChargeLinesMissing() throws Exception {
|
||||
String token = loginToken(mockMvc, "invoice-admin", "Admin@123456");
|
||||
|
||||
performAuthorized(mockMvc, token, post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(emptyChargesRequest()))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("RESERVATION_INVOICE_VALIDATION_FAILED"))
|
||||
.andExpect(jsonPath("$.details[0]").value("invoice_payload.charges: 至少需要一条费用明细。"));
|
||||
|
||||
verifyNoInteractions(excelToPdfConverter, objectStorageService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectManualInvoiceWhenTaskDoesNotBelongToOrder() throws Exception {
|
||||
insertOrder(2080010000000000001L, 2080010000000000101L, "GRP-M009-ORDER-1");
|
||||
insertOrder(2080010000000000002L, 2080010000000000102L, "GRP-M009-ORDER-2");
|
||||
insertTask(2080010000000000201L, 2080010000000000002L, 2080010000000000102L);
|
||||
String token = loginToken(mockMvc, "invoice-admin", "Admin@123456");
|
||||
|
||||
performAuthorized(mockMvc, token, post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(validRequestWithContext(2080010000000000001L, 2080010000000000201L)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("RESERVATION_INVOICE_CONTEXT_MISMATCH"));
|
||||
|
||||
verifyNoInteractions(excelToPdfConverter, objectStorageService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepGeneratedExcelObjectKeyWhenPdfConversionFails() throws Exception {
|
||||
when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
|
||||
ObjectStoragePutRequest request = invocation.getArgument(0);
|
||||
return new ObjectStoragePutResult(
|
||||
request.objectKey(),
|
||||
"https://oss.example.test/" + request.objectKey(),
|
||||
request.contentType(),
|
||||
request.sizeBytes());
|
||||
});
|
||||
when(excelToPdfConverter.convert(any())).thenThrow(new DocumentConversionException(
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
"DOCUMENT_CONVERSION_FAILED",
|
||||
"PDF 转换失败。"));
|
||||
String token = loginToken(mockMvc, "invoice-admin", "Admin@123456");
|
||||
|
||||
performAuthorized(mockMvc, token, post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(validRequest()))
|
||||
.andExpect(status().isBadGateway())
|
||||
.andExpect(jsonPath("$.error_code").value("DOCUMENT_CONVERSION_FAILED"));
|
||||
|
||||
String generationStatus = jdbcTemplate.queryForObject(
|
||||
"SELECT generation_status FROM workflow_reservation_invoice_generation",
|
||||
String.class);
|
||||
String generatedExcelObjectKey = jdbcTemplate.queryForObject(
|
||||
"SELECT generated_excel_object_key FROM workflow_reservation_invoice_generation",
|
||||
String.class);
|
||||
String safeErrorCode = jdbcTemplate.queryForObject(
|
||||
"SELECT safe_error_code FROM workflow_reservation_invoice_generation",
|
||||
String.class);
|
||||
assertThat(generationStatus).isEqualTo("FAILED");
|
||||
assertThat(generatedExcelObjectKey).endsWith(".xlsx");
|
||||
assertThat(safeErrorCode).isEqualTo("DOCUMENT_CONVERSION_FAILED");
|
||||
verify(objectStorageService).putObject(any());
|
||||
}
|
||||
|
||||
private String validRequest() {
|
||||
return """
|
||||
{
|
||||
"hotel_id": "HOTEL-TEST",
|
||||
"source_type": "MANUAL",
|
||||
"task_id": null,
|
||||
"order_id": null,
|
||||
"template_code": "PROFORMA_INVOICE_V1",
|
||||
"invoice_payload": {
|
||||
"document": {
|
||||
"invoice_date": "2026-07-17",
|
||||
"booking_date": "2026-07-12",
|
||||
"due_date": "2026-07-22"
|
||||
},
|
||||
"recipient": {
|
||||
"company_code": "LIAN_TAI",
|
||||
"contact_id": "LIAN_TAI_KHUN_ANN",
|
||||
"company": "LIAN TAI TRAVEL (THAILAND) CO., LTD.",
|
||||
"attention": "Khun Ann",
|
||||
"address": "2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240",
|
||||
"telephone": "061-397-2675",
|
||||
"email": "op.liantaitravel@gmail.com"
|
||||
},
|
||||
"booking": {
|
||||
"group_name": "GRP-DEMO-0802",
|
||||
"arrival_date": "2026-08-02",
|
||||
"departure_date": "2026-08-05",
|
||||
"room_rate_note": "includingBF",
|
||||
"extra_bed_rate": 1200
|
||||
},
|
||||
"charges": [
|
||||
{
|
||||
"description": "GRP-DEMO-0802",
|
||||
"room_type": "Deluxe Room",
|
||||
"quantity": 2,
|
||||
"rate": 3000,
|
||||
"nights": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private String validRequestWithContext(Long orderId, Long taskId) {
|
||||
return """
|
||||
{
|
||||
"hotel_id": "HOTEL-TEST",
|
||||
"source_type": "MANUAL",
|
||||
"task_id": %s,
|
||||
"order_id": %s,
|
||||
"template_code": "PROFORMA_INVOICE_V1",
|
||||
"invoice_payload": {
|
||||
"document": {
|
||||
"invoice_date": "2026-07-17",
|
||||
"booking_date": "2026-07-12",
|
||||
"due_date": "2026-07-22"
|
||||
},
|
||||
"recipient": {
|
||||
"company_code": "LIAN_TAI",
|
||||
"contact_id": "LIAN_TAI_KHUN_ANN",
|
||||
"company": "LIAN TAI TRAVEL (THAILAND) CO., LTD.",
|
||||
"attention": "Khun Ann",
|
||||
"address": "2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240",
|
||||
"telephone": "061-397-2675",
|
||||
"email": "op.liantaitravel@gmail.com"
|
||||
},
|
||||
"booking": {
|
||||
"group_name": "GRP-DEMO-0802",
|
||||
"arrival_date": "2026-08-02",
|
||||
"departure_date": "2026-08-05",
|
||||
"room_rate_note": "includingBF",
|
||||
"extra_bed_rate": 1200
|
||||
},
|
||||
"charges": [
|
||||
{
|
||||
"description": "GRP-DEMO-0802",
|
||||
"room_type": "Deluxe Room",
|
||||
"quantity": 2,
|
||||
"rate": 3000,
|
||||
"nights": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""".formatted(taskId, orderId);
|
||||
}
|
||||
|
||||
private String emptyChargesRequest() {
|
||||
return """
|
||||
{
|
||||
"hotel_id": "HOTEL-TEST",
|
||||
"source_type": "MANUAL",
|
||||
"task_id": null,
|
||||
"order_id": null,
|
||||
"template_code": "PROFORMA_INVOICE_V1",
|
||||
"invoice_payload": {
|
||||
"document": {
|
||||
"invoice_date": "2026-07-17",
|
||||
"booking_date": "2026-07-12",
|
||||
"due_date": "2026-07-22"
|
||||
},
|
||||
"recipient": {
|
||||
"company_code": "LIAN_TAI",
|
||||
"contact_id": "LIAN_TAI_KHUN_ANN",
|
||||
"company": "LIAN TAI TRAVEL (THAILAND) CO., LTD.",
|
||||
"attention": "Khun Ann",
|
||||
"address": "2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240",
|
||||
"telephone": "061-397-2675",
|
||||
"email": "op.liantaitravel@gmail.com"
|
||||
},
|
||||
"booking": {
|
||||
"group_name": "GRP-DEMO-0802",
|
||||
"arrival_date": "2026-08-02",
|
||||
"departure_date": "2026-08-05",
|
||||
"room_rate_note": "includingBF",
|
||||
"extra_bed_rate": 1200
|
||||
},
|
||||
"charges": []
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private void insertOrder(Long orderId, Long sourceMessageId, String groupCode) {
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO workflow_reservation_order (
|
||||
id, hotel_id, order_key_type, order_business_key, active_business_key,
|
||||
temporary_order_code, order_status, business_key_source, display_name,
|
||||
source_message_id, version, latest_activity_at, created_at, updated_at
|
||||
)
|
||||
VALUES (?, 'HOTEL-TEST', 'GROUP_CODE', ?, ?, ?, 'ACTIVE', 'USER_CONFIRMED', ?,
|
||||
?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""", orderId, groupCode, groupCode, "TMP-" + orderId, groupCode, sourceMessageId);
|
||||
}
|
||||
|
||||
private void insertTask(Long taskId, Long orderId, Long sourceMessageId) {
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO workflow_reservation_task (
|
||||
id, hotel_id, order_id, source_message_id, ai_transition_id,
|
||||
result_type, ai_task_type, system_task_type, task_card_type, task_subtype,
|
||||
task_status, queue_participation, execution_order, blocked_until_parent_completed,
|
||||
version, created_at, updated_at
|
||||
)
|
||||
VALUES (?, 'HOTEL-TEST', ?, ?, ?, 'normal_task', 'Update Booking', 'UPDATE_BOOKING',
|
||||
'UPDATE_BOOKING', 'manual_invoice_test', 'READY', 1, 1, 0, 0,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""", taskId, orderId, sourceMessageId, taskId - 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowableOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectStoragePutRequest;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStoragePutResult;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.ObjectStorageService;
|
||||
import cn.nianxx.thhotel.platform.documentconversion.common.dto.ExcelToPdfConvertedDocument;
|
||||
import cn.nianxx.thhotel.platform.documentconversion.service.ExcelToPdfConverter;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationInvoiceBookingRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationInvoiceChargeRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationInvoiceDocumentRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationInvoiceManualGenerationRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationInvoicePayloadRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationInvoiceRecipientRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationInvoiceGenerationRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ReservationInvoiceGenerationServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private HotelContextService hotelContextService;
|
||||
@Mock
|
||||
private ReservationAiWorkflowRepository workflowRepository;
|
||||
@Mock
|
||||
private ReservationInvoiceGenerationRepository invoiceGenerationRepository;
|
||||
@Mock
|
||||
private ReservationInvoiceExcelTemplateRenderer templateRenderer;
|
||||
@Mock
|
||||
private ExcelToPdfConverter excelToPdfConverter;
|
||||
@Mock
|
||||
private ObjectStorageService objectStorageService;
|
||||
|
||||
private ReservationInvoiceGenerationServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ReservationInvoiceGenerationServiceImpl(
|
||||
new ObjectMapper().findAndRegisterModules(),
|
||||
hotelContextService,
|
||||
workflowRepository,
|
||||
invoiceGenerationRepository,
|
||||
templateRenderer,
|
||||
excelToPdfConverter,
|
||||
objectStorageService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMarkGenerationSucceededBeforeAuditIsWritten() {
|
||||
when(hotelContextService.requireAccessibleHotel("HOTEL-TEST")).thenReturn("HOTEL-TEST");
|
||||
when(invoiceGenerationRepository.insertGeneration(any())).thenReturn(2080020000000000001L);
|
||||
when(templateRenderer.render(any())).thenReturn(new byte[]{0x50, 0x4B, 0x03, 0x04});
|
||||
when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
|
||||
ObjectStoragePutRequest request = invocation.getArgument(0);
|
||||
return new ObjectStoragePutResult(
|
||||
request.objectKey(),
|
||||
"https://oss.example.test/" + request.objectKey(),
|
||||
request.contentType(),
|
||||
request.sizeBytes());
|
||||
});
|
||||
when(excelToPdfConverter.convert(any())).thenReturn(new ExcelToPdfConvertedDocument(
|
||||
"proforma-invoice.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
8L,
|
||||
"%PDF-1.7".getBytes(),
|
||||
10L));
|
||||
when(workflowRepository.insertAuditLog(any())).thenThrow(new RuntimeException("audit insert failed"));
|
||||
|
||||
ReservationInvoiceGenerationException exception = catchThrowableOfType(
|
||||
() -> service.generateManualInvoice(validRequest(), actor()),
|
||||
ReservationInvoiceGenerationException.class);
|
||||
|
||||
assertThat(exception.getErrorCode()).isEqualTo("RESERVATION_INVOICE_GENERATION_FAILED");
|
||||
verify(invoiceGenerationRepository, never()).markSucceeded(
|
||||
anyLong(), any(), any(), any(), any(), any());
|
||||
verify(invoiceGenerationRepository).markFailed(
|
||||
anyLong(), any(), any(), any());
|
||||
}
|
||||
|
||||
private AuthenticatedUserContext actor() {
|
||||
return new AuthenticatedUserContext(
|
||||
1L,
|
||||
"invoice-admin",
|
||||
"系统管理员",
|
||||
true,
|
||||
"HOTEL-TEST",
|
||||
List.of("HOTEL-TEST"),
|
||||
List.of("RESERVATION_INVOICE_GENERATE"));
|
||||
}
|
||||
|
||||
private ReservationInvoiceManualGenerationRequest validRequest() {
|
||||
return new ReservationInvoiceManualGenerationRequest(
|
||||
"HOTEL-TEST",
|
||||
"MANUAL",
|
||||
null,
|
||||
null,
|
||||
"PROFORMA_INVOICE_V1",
|
||||
new ReservationInvoicePayloadRequest(
|
||||
new ReservationInvoiceDocumentRequest(
|
||||
LocalDate.of(2026, 7, 17),
|
||||
LocalDate.of(2026, 7, 12),
|
||||
LocalDate.of(2026, 7, 22)),
|
||||
new ReservationInvoiceRecipientRequest(
|
||||
"LIAN_TAI",
|
||||
"LIAN_TAI_KHUN_ANN",
|
||||
"LIAN TAI TRAVEL (THAILAND) CO., LTD.",
|
||||
"Khun Ann",
|
||||
"2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240",
|
||||
"061-397-2675",
|
||||
"op.liantaitravel@gmail.com"),
|
||||
new ReservationInvoiceBookingRequest(
|
||||
"GRP-DEMO-0802",
|
||||
LocalDate.of(2026, 8, 2),
|
||||
LocalDate.of(2026, 8, 5),
|
||||
"includingBF",
|
||||
new BigDecimal("1200")),
|
||||
List.of(new ReservationInvoiceChargeRequest(
|
||||
"GRP-DEMO-0802",
|
||||
"Deluxe Room",
|
||||
new BigDecimal("2"),
|
||||
new BigDecimal("3000"),
|
||||
new BigDecimal("3")))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user