feat(back-end and front-end): calculator improvements
This commit is contained in:
@@ -3,6 +3,8 @@ package com.printcalculator.service;
|
||||
import com.printcalculator.entity.PricingPolicy;
|
||||
import com.printcalculator.entity.QuoteLineItem;
|
||||
import com.printcalculator.entity.QuoteSession;
|
||||
import com.printcalculator.entity.NozzleOption;
|
||||
import com.printcalculator.repository.NozzleOptionRepository;
|
||||
import com.printcalculator.repository.PricingPolicyRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -20,13 +22,15 @@ import static org.mockito.Mockito.when;
|
||||
class QuoteSessionTotalsServiceTest {
|
||||
private PricingPolicyRepository pricingRepo;
|
||||
private QuoteCalculator quoteCalculator;
|
||||
private NozzleOptionRepository nozzleOptionRepo;
|
||||
private QuoteSessionTotalsService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
pricingRepo = mock(PricingPolicyRepository.class);
|
||||
quoteCalculator = mock(QuoteCalculator.class);
|
||||
service = new QuoteSessionTotalsService(pricingRepo, quoteCalculator);
|
||||
nozzleOptionRepo = mock(NozzleOptionRepository.class);
|
||||
service = new QuoteSessionTotalsService(pricingRepo, quoteCalculator, nozzleOptionRepo);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,6 +81,51 @@ class QuoteSessionTotalsServiceTest {
|
||||
assertAmountEquals("120.00", totals.grandTotalChf());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compute_WithRepeatedNozzleAcrossItems_ShouldChargeNozzleFeeOnlyOncePerType() {
|
||||
QuoteSession session = new QuoteSession();
|
||||
session.setSetupCostChf(new BigDecimal("2.00"));
|
||||
|
||||
QuoteLineItem itemA = createItem(new BigDecimal("10.00"), 3, 3600, "0.60");
|
||||
QuoteLineItem itemB = createItem(new BigDecimal("4.00"), 2, 1200, "0.60");
|
||||
QuoteLineItem itemC = createItem(new BigDecimal("5.00"), 1, 600, "0.80");
|
||||
|
||||
PricingPolicy policy = new PricingPolicy();
|
||||
when(pricingRepo.findFirstByIsActiveTrueOrderByValidFromDesc()).thenReturn(policy);
|
||||
when(quoteCalculator.calculateSessionMachineCost(eq(policy), any(BigDecimal.class))).thenReturn(BigDecimal.ZERO);
|
||||
when(nozzleOptionRepo.findFirstByNozzleDiameterMmAndIsActiveTrue(new BigDecimal("0.60")))
|
||||
.thenReturn(java.util.Optional.of(nozzleOption("0.60", "1.50")));
|
||||
when(nozzleOptionRepo.findFirstByNozzleDiameterMmAndIsActiveTrue(new BigDecimal("0.80")))
|
||||
.thenReturn(java.util.Optional.of(nozzleOption("0.80", "1.50")));
|
||||
|
||||
QuoteSessionTotalsService.QuoteSessionTotals totals = service.compute(session, List.of(itemA, itemB, itemC));
|
||||
|
||||
assertAmountEquals("43.00", totals.itemsTotalChf());
|
||||
assertAmountEquals("3.00", totals.nozzleChangeCostChf());
|
||||
assertAmountEquals("5.00", totals.setupCostChf());
|
||||
assertAmountEquals("50.00", totals.grandTotalChf());
|
||||
}
|
||||
|
||||
private QuoteLineItem createItem(BigDecimal unitPrice, int quantity, int printSeconds, String nozzleMm) {
|
||||
QuoteLineItem item = new QuoteLineItem();
|
||||
item.setQuantity(quantity);
|
||||
item.setUnitPriceChf(unitPrice);
|
||||
item.setPrintTimeSeconds(printSeconds);
|
||||
item.setNozzleDiameterMm(new BigDecimal(nozzleMm));
|
||||
item.setBoundingBoxXMm(new BigDecimal("10"));
|
||||
item.setBoundingBoxYMm(new BigDecimal("10"));
|
||||
item.setBoundingBoxZMm(new BigDecimal("10"));
|
||||
return item;
|
||||
}
|
||||
|
||||
private NozzleOption nozzleOption(String diameterMm, String feeChf) {
|
||||
NozzleOption option = new NozzleOption();
|
||||
option.setNozzleDiameterMm(new BigDecimal(diameterMm));
|
||||
option.setExtraNozzleChangeFeeChf(new BigDecimal(feeChf));
|
||||
option.setIsActive(true);
|
||||
return option;
|
||||
}
|
||||
|
||||
private void assertAmountEquals(String expected, BigDecimal actual) {
|
||||
assertTrue(new BigDecimal(expected).compareTo(actual) == 0,
|
||||
"Expected " + expected + " but got " + actual);
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.printcalculator.service.admin;
|
||||
|
||||
import com.printcalculator.dto.AdminFilamentMaterialTypeDto;
|
||||
import com.printcalculator.dto.AdminFilamentVariantDto;
|
||||
import com.printcalculator.dto.AdminUpsertFilamentMaterialTypeRequest;
|
||||
import com.printcalculator.dto.AdminUpsertFilamentVariantRequest;
|
||||
import com.printcalculator.entity.FilamentMaterialType;
|
||||
import com.printcalculator.entity.FilamentVariant;
|
||||
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
||||
import com.printcalculator.repository.FilamentVariantRepository;
|
||||
import com.printcalculator.repository.OrderItemRepository;
|
||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AdminFilamentControllerServiceTest {
|
||||
|
||||
@Mock
|
||||
private FilamentMaterialTypeRepository materialRepo;
|
||||
@Mock
|
||||
private FilamentVariantRepository variantRepo;
|
||||
@Mock
|
||||
private QuoteLineItemRepository quoteLineItemRepo;
|
||||
@Mock
|
||||
private OrderItemRepository orderItemRepo;
|
||||
|
||||
@InjectMocks
|
||||
private AdminFilamentControllerService service;
|
||||
|
||||
@Test
|
||||
void createMaterial_withDuplicateCode_shouldReturnBadRequest() {
|
||||
AdminUpsertFilamentMaterialTypeRequest payload = new AdminUpsertFilamentMaterialTypeRequest();
|
||||
payload.setMaterialCode("pla");
|
||||
|
||||
FilamentMaterialType existing = new FilamentMaterialType();
|
||||
existing.setId(1L);
|
||||
existing.setMaterialCode("PLA");
|
||||
when(materialRepo.findByMaterialCode("PLA")).thenReturn(Optional.of(existing));
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.createMaterial(payload)
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
|
||||
verify(materialRepo, never()).save(any(FilamentMaterialType.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createVariant_withInvalidColorHex_shouldReturnBadRequest() {
|
||||
FilamentMaterialType material = new FilamentMaterialType();
|
||||
material.setId(10L);
|
||||
material.setMaterialCode("PLA");
|
||||
when(materialRepo.findById(10L)).thenReturn(Optional.of(material));
|
||||
when(variantRepo.findByFilamentMaterialTypeAndVariantDisplayName(material, "Sunset Orange"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
AdminUpsertFilamentVariantRequest payload = baseVariantPayload();
|
||||
payload.setColorHex("#12");
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.createVariant(payload)
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
|
||||
verify(variantRepo, never()).save(any(FilamentVariant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createVariant_withValidPayload_shouldNormalizeDerivedFields() {
|
||||
FilamentMaterialType material = new FilamentMaterialType();
|
||||
material.setId(10L);
|
||||
material.setMaterialCode("PLA");
|
||||
when(materialRepo.findById(10L)).thenReturn(Optional.of(material));
|
||||
when(variantRepo.findByFilamentMaterialTypeAndVariantDisplayName(material, "Sunset Orange"))
|
||||
.thenReturn(Optional.empty());
|
||||
when(variantRepo.save(any(FilamentVariant.class))).thenAnswer(invocation -> {
|
||||
FilamentVariant variant = invocation.getArgument(0);
|
||||
variant.setId(42L);
|
||||
return variant;
|
||||
});
|
||||
|
||||
AdminUpsertFilamentVariantRequest payload = baseVariantPayload();
|
||||
payload.setFinishType("matte");
|
||||
payload.setIsMatte(false);
|
||||
payload.setBrand(" Prusa ");
|
||||
payload.setIsActive(null);
|
||||
|
||||
AdminFilamentVariantDto dto = service.createVariant(payload);
|
||||
|
||||
ArgumentCaptor<FilamentVariant> captor = ArgumentCaptor.forClass(FilamentVariant.class);
|
||||
verify(variantRepo).save(captor.capture());
|
||||
FilamentVariant saved = captor.getValue();
|
||||
|
||||
assertEquals(42L, dto.getId());
|
||||
assertEquals("MATTE", saved.getFinishType());
|
||||
assertTrue(saved.getIsMatte());
|
||||
assertEquals("Prusa", saved.getBrand());
|
||||
assertTrue(saved.getIsActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteVariant_whenInUse_shouldReturnConflict() {
|
||||
Long variantId = 11L;
|
||||
FilamentVariant variant = new FilamentVariant();
|
||||
variant.setId(variantId);
|
||||
|
||||
when(variantRepo.findById(variantId)).thenReturn(Optional.of(variant));
|
||||
when(quoteLineItemRepo.existsByFilamentVariant_Id(variantId)).thenReturn(true);
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.deleteVariant(variantId)
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.CONFLICT, ex.getStatusCode());
|
||||
verify(variantRepo, never()).delete(any(FilamentVariant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMaterials_shouldReturnAlphabeticalByCode() {
|
||||
FilamentMaterialType abs = new FilamentMaterialType();
|
||||
abs.setId(2L);
|
||||
abs.setMaterialCode("ABS");
|
||||
|
||||
FilamentMaterialType pla = new FilamentMaterialType();
|
||||
pla.setId(1L);
|
||||
pla.setMaterialCode("PLA");
|
||||
|
||||
when(materialRepo.findAll()).thenReturn(List.of(pla, abs));
|
||||
|
||||
List<AdminFilamentMaterialTypeDto> result = service.getMaterials();
|
||||
|
||||
assertEquals(2, result.size());
|
||||
assertEquals("ABS", result.get(0).getMaterialCode());
|
||||
assertEquals("PLA", result.get(1).getMaterialCode());
|
||||
}
|
||||
|
||||
private AdminUpsertFilamentVariantRequest baseVariantPayload() {
|
||||
AdminUpsertFilamentVariantRequest payload = new AdminUpsertFilamentVariantRequest();
|
||||
payload.setMaterialTypeId(10L);
|
||||
payload.setVariantDisplayName("Sunset Orange");
|
||||
payload.setColorName("Orange");
|
||||
payload.setColorHex("#FF8800");
|
||||
payload.setFinishType("GLOSSY");
|
||||
payload.setIsMatte(false);
|
||||
payload.setIsSpecial(false);
|
||||
payload.setCostChfPerKg(new BigDecimal("29.90"));
|
||||
payload.setStockSpools(new BigDecimal("2.000"));
|
||||
payload.setSpoolNetKg(new BigDecimal("1.000"));
|
||||
payload.setIsActive(true);
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package com.printcalculator.service.admin;
|
||||
|
||||
import com.printcalculator.dto.AdminCadInvoiceCreateRequest;
|
||||
import com.printcalculator.dto.AdminCadInvoiceDto;
|
||||
import com.printcalculator.dto.AdminContactRequestDetailDto;
|
||||
import com.printcalculator.dto.AdminUpdateContactRequestStatusRequest;
|
||||
import com.printcalculator.entity.CustomQuoteRequest;
|
||||
import com.printcalculator.entity.CustomQuoteRequestAttachment;
|
||||
import com.printcalculator.entity.PricingPolicy;
|
||||
import com.printcalculator.entity.QuoteSession;
|
||||
import com.printcalculator.repository.CustomQuoteRequestAttachmentRepository;
|
||||
import com.printcalculator.repository.CustomQuoteRequestRepository;
|
||||
import com.printcalculator.repository.FilamentVariantRepository;
|
||||
import com.printcalculator.repository.FilamentVariantStockKgRepository;
|
||||
import com.printcalculator.repository.OrderRepository;
|
||||
import com.printcalculator.repository.PricingPolicyRepository;
|
||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||
import com.printcalculator.repository.QuoteSessionRepository;
|
||||
import com.printcalculator.service.QuoteSessionTotalsService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AdminOperationsControllerServiceTest {
|
||||
|
||||
@Mock
|
||||
private FilamentVariantStockKgRepository filamentStockRepo;
|
||||
@Mock
|
||||
private FilamentVariantRepository filamentVariantRepo;
|
||||
@Mock
|
||||
private CustomQuoteRequestRepository customQuoteRequestRepo;
|
||||
@Mock
|
||||
private CustomQuoteRequestAttachmentRepository customQuoteRequestAttachmentRepo;
|
||||
@Mock
|
||||
private QuoteSessionRepository quoteSessionRepo;
|
||||
@Mock
|
||||
private QuoteLineItemRepository quoteLineItemRepo;
|
||||
@Mock
|
||||
private OrderRepository orderRepo;
|
||||
@Mock
|
||||
private PricingPolicyRepository pricingRepo;
|
||||
@Mock
|
||||
private QuoteSessionTotalsService quoteSessionTotalsService;
|
||||
|
||||
@InjectMocks
|
||||
private AdminOperationsControllerService service;
|
||||
|
||||
@Test
|
||||
void updateContactRequestStatus_withInvalidStatus_shouldReturnBadRequest() {
|
||||
UUID requestId = UUID.randomUUID();
|
||||
CustomQuoteRequest request = new CustomQuoteRequest();
|
||||
request.setId(requestId);
|
||||
request.setStatus("PENDING");
|
||||
when(customQuoteRequestRepo.findById(requestId)).thenReturn(Optional.of(request));
|
||||
|
||||
AdminUpdateContactRequestStatusRequest payload = new AdminUpdateContactRequestStatusRequest();
|
||||
payload.setStatus("wrong");
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.updateContactRequestStatus(requestId, payload)
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
|
||||
verify(customQuoteRequestRepo, never()).save(any(CustomQuoteRequest.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateContactRequestStatus_withValidStatus_shouldPersistAndReturnDetail() {
|
||||
UUID requestId = UUID.randomUUID();
|
||||
CustomQuoteRequest request = new CustomQuoteRequest();
|
||||
request.setId(requestId);
|
||||
request.setStatus("PENDING");
|
||||
request.setCreatedAt(OffsetDateTime.now());
|
||||
request.setUpdatedAt(OffsetDateTime.now());
|
||||
|
||||
CustomQuoteRequestAttachment attachment = new CustomQuoteRequestAttachment();
|
||||
attachment.setId(UUID.randomUUID());
|
||||
attachment.setOriginalFilename("drawing.stp");
|
||||
attachment.setMimeType("application/step");
|
||||
attachment.setFileSizeBytes(123L);
|
||||
attachment.setCreatedAt(OffsetDateTime.now());
|
||||
|
||||
when(customQuoteRequestRepo.findById(requestId)).thenReturn(Optional.of(request));
|
||||
when(customQuoteRequestRepo.save(any(CustomQuoteRequest.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(customQuoteRequestAttachmentRepo.findByRequest_IdOrderByCreatedAtAsc(requestId)).thenReturn(List.of(attachment));
|
||||
|
||||
AdminUpdateContactRequestStatusRequest payload = new AdminUpdateContactRequestStatusRequest();
|
||||
payload.setStatus("done");
|
||||
|
||||
AdminContactRequestDetailDto dto = service.updateContactRequestStatus(requestId, payload);
|
||||
|
||||
assertEquals("DONE", dto.getStatus());
|
||||
assertNotNull(dto.getUpdatedAt());
|
||||
assertEquals(1, dto.getAttachments().size());
|
||||
verify(customQuoteRequestRepo).save(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createOrUpdateCadInvoice_withMissingCadHours_shouldReturnBadRequest() {
|
||||
AdminCadInvoiceCreateRequest payload = new AdminCadInvoiceCreateRequest();
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.createOrUpdateCadInvoice(payload)
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createOrUpdateCadInvoice_withConvertedSession_shouldReturnConflict() {
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
QuoteSession session = new QuoteSession();
|
||||
session.setId(sessionId);
|
||||
session.setStatus("CONVERTED");
|
||||
|
||||
when(quoteSessionRepo.findById(sessionId)).thenReturn(Optional.of(session));
|
||||
|
||||
AdminCadInvoiceCreateRequest payload = new AdminCadInvoiceCreateRequest();
|
||||
payload.setSessionId(sessionId);
|
||||
payload.setCadHours(new BigDecimal("1.0"));
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.createOrUpdateCadInvoice(payload)
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.CONFLICT, ex.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createOrUpdateCadInvoice_withNewSession_shouldUsePolicyCadRate() {
|
||||
PricingPolicy policy = new PricingPolicy();
|
||||
policy.setCadCostChfPerHour(new BigDecimal("85"));
|
||||
|
||||
when(pricingRepo.findFirstByIsActiveTrueOrderByValidFromDesc()).thenReturn(policy);
|
||||
when(quoteSessionRepo.save(any(QuoteSession.class))).thenAnswer(invocation -> {
|
||||
QuoteSession session = invocation.getArgument(0);
|
||||
if (session.getId() == null) {
|
||||
session.setId(UUID.randomUUID());
|
||||
}
|
||||
return session;
|
||||
});
|
||||
when(quoteLineItemRepo.findByQuoteSessionId(any(UUID.class))).thenReturn(List.of());
|
||||
when(quoteSessionTotalsService.compute(any(QuoteSession.class), anyList()))
|
||||
.thenReturn(new QuoteSessionTotalsService.QuoteSessionTotals(
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
new BigDecimal("212.50"),
|
||||
new BigDecimal("212.50"),
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
new BigDecimal("212.50"),
|
||||
BigDecimal.ZERO
|
||||
));
|
||||
|
||||
AdminCadInvoiceCreateRequest payload = new AdminCadInvoiceCreateRequest();
|
||||
payload.setCadHours(new BigDecimal("2.5"));
|
||||
payload.setCadHourlyRateChf(null);
|
||||
payload.setNotes(" Custom CAD work ");
|
||||
|
||||
AdminCadInvoiceDto dto = service.createOrUpdateCadInvoice(payload);
|
||||
|
||||
assertEquals("CAD_ACTIVE", dto.getSessionStatus());
|
||||
assertEquals(new BigDecimal("2.50"), dto.getCadHours());
|
||||
assertEquals(new BigDecimal("85.00"), dto.getCadHourlyRateChf());
|
||||
assertEquals("Custom CAD work", dto.getNotes());
|
||||
assertEquals(new BigDecimal("212.50"), dto.getCadTotalChf());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteQuoteSession_whenLinkedToOrder_shouldReturnConflict() {
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
QuoteSession session = new QuoteSession();
|
||||
session.setId(sessionId);
|
||||
|
||||
when(quoteSessionRepo.findById(sessionId)).thenReturn(Optional.of(session));
|
||||
when(orderRepo.existsBySourceQuoteSession_Id(sessionId)).thenReturn(true);
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.deleteQuoteSession(sessionId)
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.CONFLICT, ex.getStatusCode());
|
||||
verify(quoteSessionRepo, never()).delete(any(QuoteSession.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package com.printcalculator.service.order;
|
||||
|
||||
import com.printcalculator.dto.AdminOrderStatusUpdateRequest;
|
||||
import com.printcalculator.dto.OrderDto;
|
||||
import com.printcalculator.entity.FilamentVariant;
|
||||
import com.printcalculator.entity.Order;
|
||||
import com.printcalculator.entity.OrderItem;
|
||||
import com.printcalculator.entity.Payment;
|
||||
import com.printcalculator.event.OrderShippedEvent;
|
||||
import com.printcalculator.repository.OrderItemRepository;
|
||||
import com.printcalculator.repository.OrderRepository;
|
||||
import com.printcalculator.repository.PaymentRepository;
|
||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||
import com.printcalculator.service.payment.InvoicePdfRenderingService;
|
||||
import com.printcalculator.service.payment.PaymentService;
|
||||
import com.printcalculator.service.payment.QrBillService;
|
||||
import com.printcalculator.service.storage.StorageService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AdminOrderControllerServiceTest {
|
||||
|
||||
@Mock
|
||||
private OrderRepository orderRepo;
|
||||
@Mock
|
||||
private OrderItemRepository orderItemRepo;
|
||||
@Mock
|
||||
private PaymentRepository paymentRepo;
|
||||
@Mock
|
||||
private QuoteLineItemRepository quoteLineItemRepo;
|
||||
@Mock
|
||||
private PaymentService paymentService;
|
||||
@Mock
|
||||
private StorageService storageService;
|
||||
@Mock
|
||||
private InvoicePdfRenderingService invoiceService;
|
||||
@Mock
|
||||
private QrBillService qrBillService;
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@InjectMocks
|
||||
private AdminOrderControllerService service;
|
||||
|
||||
@Test
|
||||
void updatePaymentMethod_withBlankMethod_shouldReturnBadRequest() {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
when(orderRepo.findById(orderId)).thenReturn(Optional.of(buildOrder(orderId, "PENDING_PAYMENT")));
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.updatePaymentMethod(orderId, Map.of("method", " "))
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
|
||||
verify(paymentService, never()).updatePaymentMethod(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePaymentMethod_withValidMethod_shouldDelegateAndReturnUpdatedDto() {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
Order order = buildOrder(orderId, "PENDING_PAYMENT");
|
||||
Payment payment = new Payment();
|
||||
payment.setMethod("BANK_TRANSFER");
|
||||
payment.setStatus("PENDING");
|
||||
|
||||
when(orderRepo.findById(orderId)).thenReturn(Optional.of(order));
|
||||
when(orderItemRepo.findByOrder_Id(orderId)).thenReturn(List.of());
|
||||
when(paymentRepo.findByOrder_Id(orderId)).thenReturn(Optional.of(payment));
|
||||
|
||||
OrderDto dto = service.updatePaymentMethod(orderId, Map.of("method", "BANK_TRANSFER"));
|
||||
|
||||
assertEquals("BANK_TRANSFER", dto.getPaymentMethod());
|
||||
assertEquals("PENDING", dto.getPaymentStatus());
|
||||
verify(paymentService).updatePaymentMethod(orderId, "BANK_TRANSFER");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateOrderStatus_toShipped_shouldPublishOrderShippedEvent() {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
Order order = buildOrder(orderId, "PAID");
|
||||
|
||||
when(orderRepo.findById(orderId)).thenReturn(Optional.of(order));
|
||||
when(orderRepo.save(any(Order.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(orderItemRepo.findByOrder_Id(orderId)).thenReturn(List.of());
|
||||
when(paymentRepo.findByOrder_Id(orderId)).thenReturn(Optional.empty());
|
||||
|
||||
AdminOrderStatusUpdateRequest payload = new AdminOrderStatusUpdateRequest();
|
||||
payload.setStatus("shipped");
|
||||
|
||||
OrderDto dto = service.updateOrderStatus(orderId, payload);
|
||||
|
||||
assertEquals("SHIPPED", dto.getStatus());
|
||||
verify(eventPublisher).publishEvent(any(OrderShippedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateOrderStatus_fromShippedToShipped_shouldNotPublishEvent() {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
Order order = buildOrder(orderId, "SHIPPED");
|
||||
|
||||
when(orderRepo.findById(orderId)).thenReturn(Optional.of(order));
|
||||
when(orderRepo.save(any(Order.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(orderItemRepo.findByOrder_Id(orderId)).thenReturn(List.of());
|
||||
when(paymentRepo.findByOrder_Id(orderId)).thenReturn(Optional.empty());
|
||||
|
||||
AdminOrderStatusUpdateRequest payload = new AdminOrderStatusUpdateRequest();
|
||||
payload.setStatus("SHIPPED");
|
||||
|
||||
service.updateOrderStatus(orderId, payload);
|
||||
|
||||
verify(eventPublisher, never()).publishEvent(any(OrderShippedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadOrderItemFile_withInvalidRelativePath_shouldReturnNotFound() {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
UUID orderItemId = UUID.randomUUID();
|
||||
|
||||
Order order = buildOrder(orderId, "PAID");
|
||||
OrderItem item = new OrderItem();
|
||||
item.setId(orderItemId);
|
||||
item.setOrder(order);
|
||||
item.setStoredRelativePath("../escape/path.stl");
|
||||
|
||||
when(orderItemRepo.findById(orderItemId)).thenReturn(Optional.of(item));
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.downloadOrderItemFile(orderId, orderItemId)
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrder_shouldIncludePerItemPrintSettingsAndVariantMetadata() {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
Order order = buildOrder(orderId, "PAID");
|
||||
|
||||
FilamentVariant variant = new FilamentVariant();
|
||||
variant.setId(42L);
|
||||
variant.setVariantDisplayName("PLA Arancione Opaco");
|
||||
variant.setColorName("Arancione");
|
||||
variant.setColorHex("#ff7a00");
|
||||
|
||||
OrderItem item = new OrderItem();
|
||||
item.setId(UUID.randomUUID());
|
||||
item.setOrder(order);
|
||||
item.setOriginalFilename("obj_4_Part 1.stl");
|
||||
item.setMaterialCode("PLA");
|
||||
item.setColorCode("Arancione");
|
||||
item.setFilamentVariant(variant);
|
||||
item.setQuality("standard");
|
||||
item.setNozzleDiameterMm(new BigDecimal("0.60"));
|
||||
item.setLayerHeightMm(new BigDecimal("0.24"));
|
||||
item.setInfillPercent(15);
|
||||
item.setInfillPattern("grid");
|
||||
item.setSupportsEnabled(Boolean.FALSE);
|
||||
item.setQuantity(1);
|
||||
item.setPrintTimeSeconds(2340);
|
||||
item.setMaterialGrams(new BigDecimal("22.76"));
|
||||
item.setUnitPriceChf(new BigDecimal("0.99"));
|
||||
item.setLineTotalChf(new BigDecimal("0.99"));
|
||||
|
||||
when(orderRepo.findById(orderId)).thenReturn(Optional.of(order));
|
||||
when(orderItemRepo.findByOrder_Id(orderId)).thenReturn(List.of(item));
|
||||
when(paymentRepo.findByOrder_Id(orderId)).thenReturn(Optional.empty());
|
||||
|
||||
OrderDto dto = service.getOrder(orderId);
|
||||
|
||||
assertEquals(1, dto.getItems().size());
|
||||
var itemDto = dto.getItems().get(0);
|
||||
assertEquals(new BigDecimal("0.60"), itemDto.getNozzleDiameterMm());
|
||||
assertEquals(new BigDecimal("0.24"), itemDto.getLayerHeightMm());
|
||||
assertEquals(15, itemDto.getInfillPercent());
|
||||
assertEquals("grid", itemDto.getInfillPattern());
|
||||
assertEquals(Boolean.FALSE, itemDto.getSupportsEnabled());
|
||||
assertEquals(42L, itemDto.getFilamentVariantId());
|
||||
assertEquals("PLA Arancione Opaco", itemDto.getFilamentVariantDisplayName());
|
||||
assertEquals("Arancione", itemDto.getFilamentColorName());
|
||||
assertEquals("#ff7a00", itemDto.getFilamentColorHex());
|
||||
}
|
||||
|
||||
private Order buildOrder(UUID orderId, String status) {
|
||||
Order order = new Order();
|
||||
order.setId(orderId);
|
||||
order.setStatus(status);
|
||||
order.setCustomerEmail("customer@example.com");
|
||||
order.setCustomerPhone("+41910000000");
|
||||
order.setBillingCustomerType("PRIVATE");
|
||||
order.setBillingFirstName("Mario");
|
||||
order.setBillingLastName("Rossi");
|
||||
order.setBillingAddressLine1("Via Test 1");
|
||||
order.setBillingZip("6900");
|
||||
order.setBillingCity("Lugano");
|
||||
order.setBillingCountryCode("CH");
|
||||
order.setShippingSameAsBilling(true);
|
||||
order.setCurrency("CHF");
|
||||
order.setSetupCostChf(BigDecimal.ZERO);
|
||||
order.setShippingCostChf(BigDecimal.ZERO);
|
||||
order.setDiscountChf(BigDecimal.ZERO);
|
||||
order.setSubtotalChf(BigDecimal.ZERO);
|
||||
order.setCadTotalChf(BigDecimal.ZERO);
|
||||
order.setTotalChf(BigDecimal.ZERO);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package com.printcalculator.service.order;
|
||||
|
||||
import com.printcalculator.dto.OrderDto;
|
||||
import com.printcalculator.entity.Order;
|
||||
import com.printcalculator.entity.OrderItem;
|
||||
import com.printcalculator.repository.OrderItemRepository;
|
||||
import com.printcalculator.repository.OrderRepository;
|
||||
import com.printcalculator.repository.PaymentRepository;
|
||||
import com.printcalculator.service.OrderService;
|
||||
import com.printcalculator.service.payment.InvoicePdfRenderingService;
|
||||
import com.printcalculator.service.payment.PaymentService;
|
||||
import com.printcalculator.service.payment.QrBillService;
|
||||
import com.printcalculator.service.payment.TwintPaymentService;
|
||||
import com.printcalculator.service.storage.StorageService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OrderControllerServiceTest {
|
||||
|
||||
@Mock
|
||||
private OrderService orderService;
|
||||
@Mock
|
||||
private OrderRepository orderRepo;
|
||||
@Mock
|
||||
private OrderItemRepository orderItemRepo;
|
||||
@Mock
|
||||
private StorageService storageService;
|
||||
@Mock
|
||||
private InvoicePdfRenderingService invoiceService;
|
||||
@Mock
|
||||
private QrBillService qrBillService;
|
||||
@Mock
|
||||
private TwintPaymentService twintPaymentService;
|
||||
@Mock
|
||||
private PaymentService paymentService;
|
||||
@Mock
|
||||
private PaymentRepository paymentRepo;
|
||||
|
||||
@InjectMocks
|
||||
private OrderControllerService service;
|
||||
|
||||
@Test
|
||||
void uploadOrderItemFile_withOrderMismatch_shouldReturnFalse() throws Exception {
|
||||
UUID expectedOrderId = UUID.randomUUID();
|
||||
UUID wrongOrderId = UUID.randomUUID();
|
||||
UUID orderItemId = UUID.randomUUID();
|
||||
|
||||
Order order = new Order();
|
||||
order.setId(expectedOrderId);
|
||||
|
||||
OrderItem item = new OrderItem();
|
||||
item.setId(orderItemId);
|
||||
item.setOrder(order);
|
||||
item.setStoredRelativePath("PENDING");
|
||||
|
||||
when(orderItemRepo.findById(orderItemId)).thenReturn(Optional.of(item));
|
||||
|
||||
MockMultipartFile file = new MockMultipartFile("file", "part.stl", "model/stl", "solid".getBytes());
|
||||
|
||||
boolean result = service.uploadOrderItemFile(wrongOrderId, orderItemId, file);
|
||||
|
||||
assertFalse(result);
|
||||
verify(storageService, never()).store(any(MockMultipartFile.class), any(Path.class));
|
||||
verify(orderItemRepo, never()).save(any(OrderItem.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadOrderItemFile_withPendingPath_shouldStoreAndPersistMetadata() throws Exception {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
UUID orderItemId = UUID.randomUUID();
|
||||
|
||||
Order order = new Order();
|
||||
order.setId(orderId);
|
||||
|
||||
OrderItem item = new OrderItem();
|
||||
item.setId(orderItemId);
|
||||
item.setOrder(order);
|
||||
item.setStoredRelativePath("PENDING");
|
||||
|
||||
when(orderItemRepo.findById(orderItemId)).thenReturn(Optional.of(item));
|
||||
|
||||
MockMultipartFile file = new MockMultipartFile("file", "model.STL", "model/stl", "mesh".getBytes());
|
||||
|
||||
boolean result = service.uploadOrderItemFile(orderId, orderItemId, file);
|
||||
|
||||
assertTrue(result);
|
||||
|
||||
ArgumentCaptor<Path> pathCaptor = ArgumentCaptor.forClass(Path.class);
|
||||
verify(storageService).store(eq(file), pathCaptor.capture());
|
||||
Path storedPath = pathCaptor.getValue();
|
||||
assertTrue(storedPath.startsWith(Path.of("orders", orderId.toString(), "3d-files", orderItemId.toString())));
|
||||
|
||||
assertTrue(item.getStoredFilename().endsWith(".stl"));
|
||||
assertEquals(file.getSize(), item.getFileSizeBytes());
|
||||
assertEquals("model/stl", item.getMimeType());
|
||||
verify(orderItemRepo).save(item);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrder_withShippedStatus_shouldRedactPersonalData() {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
Order order = buildOrder(orderId, "SHIPPED");
|
||||
|
||||
when(orderRepo.findById(orderId)).thenReturn(Optional.of(order));
|
||||
when(orderItemRepo.findByOrder_Id(orderId)).thenReturn(List.of());
|
||||
when(paymentRepo.findByOrder_Id(orderId)).thenReturn(Optional.empty());
|
||||
|
||||
Optional<OrderDto> result = service.getOrder(orderId);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
OrderDto dto = result.get();
|
||||
assertNull(dto.getCustomerEmail());
|
||||
assertNull(dto.getCustomerPhone());
|
||||
assertNull(dto.getBillingAddress());
|
||||
assertNull(dto.getShippingAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTwintQr_withOversizedInput_shouldClampSizeTo600() {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
Order order = buildOrder(orderId, "PENDING_PAYMENT");
|
||||
|
||||
byte[] png = new byte[]{1, 2, 3};
|
||||
when(orderRepo.findById(orderId)).thenReturn(Optional.of(order));
|
||||
when(twintPaymentService.generateQrPng(order, 600)).thenReturn(png);
|
||||
|
||||
ResponseEntity<byte[]> response = service.getTwintQr(orderId, 5000);
|
||||
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertEquals(MediaType.IMAGE_PNG, response.getHeaders().getContentType());
|
||||
assertArrayEquals(png, response.getBody());
|
||||
verify(twintPaymentService).generateQrPng(order, 600);
|
||||
}
|
||||
|
||||
private Order buildOrder(UUID orderId, String status) {
|
||||
Order order = new Order();
|
||||
order.setId(orderId);
|
||||
order.setStatus(status);
|
||||
order.setCustomerEmail("customer@example.com");
|
||||
order.setCustomerPhone("+41910000000");
|
||||
order.setBillingCustomerType("PRIVATE");
|
||||
order.setBillingFirstName("Mario");
|
||||
order.setBillingLastName("Rossi");
|
||||
order.setBillingAddressLine1("Via Test 1");
|
||||
order.setBillingZip("6900");
|
||||
order.setBillingCity("Lugano");
|
||||
order.setBillingCountryCode("CH");
|
||||
order.setShippingSameAsBilling(true);
|
||||
order.setCurrency("CHF");
|
||||
order.setSetupCostChf(BigDecimal.ZERO);
|
||||
order.setShippingCostChf(BigDecimal.ZERO);
|
||||
order.setDiscountChf(BigDecimal.ZERO);
|
||||
order.setSubtotalChf(BigDecimal.ZERO);
|
||||
order.setCadTotalChf(BigDecimal.ZERO);
|
||||
order.setTotalChf(BigDecimal.ZERO);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.printcalculator.service.request;
|
||||
|
||||
import com.printcalculator.entity.CustomQuoteRequest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class ContactRequestLocalizationServiceTest {
|
||||
|
||||
private ContactRequestLocalizationService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ContactRequestLocalizationService();
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeLanguage_shouldMapKnownPrefixes() {
|
||||
assertEquals("de", service.normalizeLanguage("de-CH"));
|
||||
assertEquals("en", service.normalizeLanguage("EN"));
|
||||
assertEquals("fr", service.normalizeLanguage("fr_CA"));
|
||||
assertEquals("it", service.normalizeLanguage(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveRecipientName_shouldUsePriorityAndFallback() {
|
||||
CustomQuoteRequest request = new CustomQuoteRequest();
|
||||
request.setName("Mario Rossi");
|
||||
assertEquals("Mario Rossi", service.resolveRecipientName(request, "it"));
|
||||
|
||||
request.setName(" ");
|
||||
request.setContactPerson("Laura Bianchi");
|
||||
assertEquals("Laura Bianchi", service.resolveRecipientName(request, "it"));
|
||||
|
||||
request.setContactPerson(" ");
|
||||
request.setCompanyName("3D Fab SA");
|
||||
assertEquals("3D Fab SA", service.resolveRecipientName(request, "it"));
|
||||
|
||||
request.setCompanyName(" ");
|
||||
assertEquals("customer", service.resolveRecipientName(request, "en"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyCustomerContactRequestTexts_shouldPopulateLocalizedLabels() {
|
||||
Map<String, Object> templateData = new HashMap<>();
|
||||
templateData.put("recipientName", "Mario");
|
||||
UUID requestId = UUID.randomUUID();
|
||||
|
||||
String subject = service.applyCustomerContactRequestTexts(templateData, "fr", requestId);
|
||||
|
||||
assertEquals("Nous avons recu votre demande de contact #" + requestId + " - 3D-Fab", subject);
|
||||
assertEquals("Date", templateData.get("labelDate"));
|
||||
assertEquals("Bonjour Mario,", templateData.get("greetingText"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void localizeRequestType_andCustomerType_shouldReturnExpectedValues() {
|
||||
assertEquals("Custom part request", service.localizeRequestType("print_service", "en"));
|
||||
assertEquals("Azienda", service.localizeCustomerType("business", "it"));
|
||||
assertEquals("-", service.localizeCustomerType(null, "de"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void localeForLanguage_shouldReturnExpectedLocale() {
|
||||
assertEquals(Locale.GERMAN, service.localeForLanguage("de"));
|
||||
assertEquals(Locale.ITALIAN, service.localeForLanguage("unknown"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.printcalculator.service.request;
|
||||
|
||||
import com.printcalculator.entity.CustomQuoteRequest;
|
||||
import com.printcalculator.entity.CustomQuoteRequestAttachment;
|
||||
import com.printcalculator.repository.CustomQuoteRequestAttachmentRepository;
|
||||
import com.printcalculator.service.storage.ClamAVService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CustomQuoteRequestAttachmentServiceTest {
|
||||
|
||||
@Mock
|
||||
private CustomQuoteRequestAttachmentRepository attachmentRepo;
|
||||
@Mock
|
||||
private ClamAVService clamAVService;
|
||||
|
||||
@InjectMocks
|
||||
private CustomQuoteRequestAttachmentService service;
|
||||
|
||||
private UUID lastRequestIdForCleanup;
|
||||
|
||||
@AfterEach
|
||||
void cleanStorageDirectory() {
|
||||
if (lastRequestIdForCleanup == null) {
|
||||
return;
|
||||
}
|
||||
Path requestDir = Paths.get("storage_requests", "quote-requests", lastRequestIdForCleanup.toString());
|
||||
if (!Files.exists(requestDir)) {
|
||||
return;
|
||||
}
|
||||
try (var walk = Files.walk(requestDir)) {
|
||||
walk.sorted(Comparator.reverseOrder()).forEach(path -> {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void storeAttachments_withNullFiles_shouldReturnZero() throws Exception {
|
||||
CustomQuoteRequest request = buildRequest();
|
||||
|
||||
int count = service.storeAttachments(request, null);
|
||||
|
||||
assertEquals(0, count);
|
||||
verifyNoInteractions(clamAVService, attachmentRepo);
|
||||
}
|
||||
|
||||
@Test
|
||||
void storeAttachments_withTooManyFiles_shouldThrowIOException() {
|
||||
CustomQuoteRequest request = buildRequest();
|
||||
List<MockMultipartFile> files = new ArrayList<>();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
files.add(new MockMultipartFile("files", "file-" + i + ".stl", "model/stl", "solid".getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
IOException ex = assertThrows(
|
||||
IOException.class,
|
||||
() -> service.storeAttachments(request, new ArrayList<>(files))
|
||||
);
|
||||
|
||||
assertTrue(ex.getMessage().contains("Too many files"));
|
||||
verifyNoInteractions(clamAVService, attachmentRepo);
|
||||
}
|
||||
|
||||
@Test
|
||||
void storeAttachments_withCompressedFile_shouldThrowBadRequest() {
|
||||
CustomQuoteRequest request = buildRequest();
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"files",
|
||||
"archive.zip",
|
||||
"application/zip",
|
||||
"dummy".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.storeAttachments(request, List.of(file))
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
|
||||
verifyNoInteractions(clamAVService, attachmentRepo);
|
||||
}
|
||||
|
||||
@Test
|
||||
void storeAttachments_withValidFile_shouldScanPersistAndWriteOnDisk() throws Exception {
|
||||
CustomQuoteRequest request = buildRequest();
|
||||
lastRequestIdForCleanup = request.getId();
|
||||
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"files",
|
||||
"part.stl",
|
||||
"model/stl",
|
||||
"solid model".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
|
||||
when(clamAVService.scan(any())).thenReturn(true);
|
||||
when(attachmentRepo.save(any(CustomQuoteRequestAttachment.class))).thenAnswer(invocation -> {
|
||||
CustomQuoteRequestAttachment attachment = invocation.getArgument(0);
|
||||
if (attachment.getId() == null) {
|
||||
attachment.setId(UUID.randomUUID());
|
||||
}
|
||||
return attachment;
|
||||
});
|
||||
|
||||
int savedCount = service.storeAttachments(request, List.of(file));
|
||||
|
||||
assertEquals(1, savedCount);
|
||||
|
||||
ArgumentCaptor<CustomQuoteRequestAttachment> captor = ArgumentCaptor.forClass(CustomQuoteRequestAttachment.class);
|
||||
verify(attachmentRepo, times(2)).save(captor.capture());
|
||||
verify(clamAVService, times(1)).scan(any());
|
||||
|
||||
CustomQuoteRequestAttachment persisted = captor.getAllValues().get(1);
|
||||
Path absolutePath = Paths.get("storage_requests").toAbsolutePath().normalize()
|
||||
.resolve(persisted.getStoredRelativePath())
|
||||
.normalize();
|
||||
|
||||
assertTrue(Files.exists(absolutePath));
|
||||
assertEquals("solid model", Files.readString(absolutePath, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private CustomQuoteRequest buildRequest() {
|
||||
CustomQuoteRequest request = new CustomQuoteRequest();
|
||||
request.setId(UUID.randomUUID());
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.printcalculator.service.request;
|
||||
|
||||
import com.printcalculator.dto.QuoteRequestDto;
|
||||
import com.printcalculator.entity.CustomQuoteRequest;
|
||||
import com.printcalculator.repository.CustomQuoteRequestRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CustomQuoteRequestControllerServiceTest {
|
||||
|
||||
@Mock
|
||||
private CustomQuoteRequestRepository requestRepo;
|
||||
@Mock
|
||||
private CustomQuoteRequestAttachmentService attachmentService;
|
||||
@Mock
|
||||
private CustomQuoteRequestNotificationService notificationService;
|
||||
|
||||
@InjectMocks
|
||||
private CustomQuoteRequestControllerService service;
|
||||
|
||||
@Test
|
||||
void createCustomQuoteRequest_withMissingConsents_shouldThrowBadRequest() throws Exception {
|
||||
QuoteRequestDto dto = buildRequest(false, true);
|
||||
|
||||
ResponseStatusException ex = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.createCustomQuoteRequest(dto, List.of())
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
|
||||
verifyNoInteractions(requestRepo, attachmentService, notificationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCustomQuoteRequest_withValidPayload_shouldPersistAndDelegate() throws Exception {
|
||||
UUID requestId = UUID.randomUUID();
|
||||
QuoteRequestDto dto = buildRequest(true, true);
|
||||
List<MultipartFile> files = List.of();
|
||||
|
||||
when(requestRepo.save(any(CustomQuoteRequest.class))).thenAnswer(invocation -> {
|
||||
CustomQuoteRequest request = invocation.getArgument(0);
|
||||
request.setId(requestId);
|
||||
return request;
|
||||
});
|
||||
when(attachmentService.storeAttachments(any(CustomQuoteRequest.class), eq(files))).thenReturn(2);
|
||||
|
||||
CustomQuoteRequest saved = service.createCustomQuoteRequest(dto, files);
|
||||
|
||||
assertNotNull(saved);
|
||||
assertEquals(requestId, saved.getId());
|
||||
assertEquals("PENDING", saved.getStatus());
|
||||
assertNotNull(saved.getCreatedAt());
|
||||
assertNotNull(saved.getUpdatedAt());
|
||||
|
||||
verify(requestRepo).save(any(CustomQuoteRequest.class));
|
||||
verify(attachmentService).storeAttachments(saved, files);
|
||||
verify(notificationService).sendNotifications(saved, 2, "de-CH");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCustomQuoteRequest_shouldDelegateToRepository() {
|
||||
UUID requestId = UUID.randomUUID();
|
||||
CustomQuoteRequest request = new CustomQuoteRequest();
|
||||
request.setId(requestId);
|
||||
when(requestRepo.findById(requestId)).thenReturn(Optional.of(request));
|
||||
|
||||
Optional<CustomQuoteRequest> result = service.getCustomQuoteRequest(requestId);
|
||||
|
||||
assertEquals(Optional.of(request), result);
|
||||
verify(requestRepo).findById(requestId);
|
||||
}
|
||||
|
||||
private QuoteRequestDto buildRequest(boolean acceptTerms, boolean acceptPrivacy) {
|
||||
QuoteRequestDto dto = new QuoteRequestDto();
|
||||
dto.setRequestType("PRINT_SERVICE");
|
||||
dto.setCustomerType("PRIVATE");
|
||||
dto.setLanguage("de-CH");
|
||||
dto.setEmail("customer@example.com");
|
||||
dto.setPhone("+41910000000");
|
||||
dto.setName("Mario Rossi");
|
||||
dto.setCompanyName("3D Fab SA");
|
||||
dto.setContactPerson("Mario Rossi");
|
||||
dto.setMessage("Vorrei una quotazione.");
|
||||
dto.setAcceptTerms(acceptTerms);
|
||||
dto.setAcceptPrivacy(acceptPrivacy);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.printcalculator.service.request;
|
||||
|
||||
import com.printcalculator.entity.CustomQuoteRequest;
|
||||
import com.printcalculator.service.email.EmailNotificationService;
|
||||
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.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CustomQuoteRequestNotificationServiceTest {
|
||||
|
||||
@Mock
|
||||
private EmailNotificationService emailNotificationService;
|
||||
|
||||
private ContactRequestLocalizationService localizationService;
|
||||
private CustomQuoteRequestNotificationService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
localizationService = new ContactRequestLocalizationService();
|
||||
service = new CustomQuoteRequestNotificationService(emailNotificationService, localizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendNotifications_withAdminAndCustomerEnabled_shouldSendBothEmails() {
|
||||
ReflectionTestUtils.setField(service, "contactRequestAdminMailEnabled", true);
|
||||
ReflectionTestUtils.setField(service, "contactRequestAdminMailAddress", "admin@3d-fab.ch");
|
||||
ReflectionTestUtils.setField(service, "contactRequestCustomerMailEnabled", true);
|
||||
|
||||
CustomQuoteRequest request = buildRequest();
|
||||
|
||||
service.sendNotifications(request, 3, "en-US");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, Object>> dataCaptor = (ArgumentCaptor<Map<String, Object>>) (ArgumentCaptor<?>) ArgumentCaptor.forClass(Map.class);
|
||||
ArgumentCaptor<String> toCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> subjectCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> templateCaptor = ArgumentCaptor.forClass(String.class);
|
||||
|
||||
verify(emailNotificationService, times(2)).sendEmail(
|
||||
toCaptor.capture(),
|
||||
subjectCaptor.capture(),
|
||||
templateCaptor.capture(),
|
||||
dataCaptor.capture()
|
||||
);
|
||||
|
||||
List<String> recipients = toCaptor.getAllValues();
|
||||
assertTrue(recipients.contains("admin@3d-fab.ch"));
|
||||
assertTrue(recipients.contains("customer@example.com"));
|
||||
|
||||
int customerIndex = recipients.indexOf("customer@example.com");
|
||||
assertEquals("contact-request-customer", templateCaptor.getAllValues().get(customerIndex));
|
||||
assertEquals("We received your contact request #" + request.getId() + " - 3D-Fab", subjectCaptor.getAllValues().get(customerIndex));
|
||||
assertEquals("Date", dataCaptor.getAllValues().get(customerIndex).get("labelDate"));
|
||||
|
||||
int adminIndex = recipients.indexOf("admin@3d-fab.ch");
|
||||
assertEquals("contact-request-admin", templateCaptor.getAllValues().get(adminIndex));
|
||||
assertEquals(3, dataCaptor.getAllValues().get(adminIndex).get("attachmentsCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendNotifications_withCustomerDisabled_shouldOnlySendAdminEmail() {
|
||||
ReflectionTestUtils.setField(service, "contactRequestAdminMailEnabled", true);
|
||||
ReflectionTestUtils.setField(service, "contactRequestAdminMailAddress", "admin@3d-fab.ch");
|
||||
ReflectionTestUtils.setField(service, "contactRequestCustomerMailEnabled", false);
|
||||
|
||||
service.sendNotifications(buildRequest(), 1, "it");
|
||||
|
||||
verify(emailNotificationService, times(1)).sendEmail(
|
||||
org.mockito.ArgumentMatchers.eq("admin@3d-fab.ch"),
|
||||
org.mockito.ArgumentMatchers.anyString(),
|
||||
org.mockito.ArgumentMatchers.eq("contact-request-admin"),
|
||||
org.mockito.ArgumentMatchers.anyMap()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendNotifications_withMissingAdminAddressAndCustomerDisabled_shouldSendNothing() {
|
||||
ReflectionTestUtils.setField(service, "contactRequestAdminMailEnabled", true);
|
||||
ReflectionTestUtils.setField(service, "contactRequestAdminMailAddress", " ");
|
||||
ReflectionTestUtils.setField(service, "contactRequestCustomerMailEnabled", false);
|
||||
|
||||
service.sendNotifications(buildRequest(), 1, "fr");
|
||||
|
||||
verify(emailNotificationService, never()).sendEmail(
|
||||
org.mockito.ArgumentMatchers.anyString(),
|
||||
org.mockito.ArgumentMatchers.anyString(),
|
||||
org.mockito.ArgumentMatchers.anyString(),
|
||||
org.mockito.ArgumentMatchers.anyMap()
|
||||
);
|
||||
}
|
||||
|
||||
private CustomQuoteRequest buildRequest() {
|
||||
CustomQuoteRequest request = new CustomQuoteRequest();
|
||||
request.setId(UUID.randomUUID());
|
||||
request.setRequestType("PRINT_SERVICE");
|
||||
request.setCustomerType("PRIVATE");
|
||||
request.setName("Mario Rossi");
|
||||
request.setCompanyName("3D Fab SA");
|
||||
request.setContactPerson("Mario Rossi");
|
||||
request.setEmail("customer@example.com");
|
||||
request.setPhone("+41910000000");
|
||||
request.setMessage("Vorrei una quotazione.");
|
||||
request.setCreatedAt(OffsetDateTime.parse("2026-03-05T10:15:30+01:00"));
|
||||
return request;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user