feat(back-end): shop ui implementation
This commit is contained in:
@@ -191,7 +191,9 @@ public class OrderService {
|
||||
oItem.setShopVariantLabel(qItem.getShopVariantLabel());
|
||||
oItem.setShopVariantColorName(qItem.getShopVariantColorName());
|
||||
oItem.setShopVariantColorHex(qItem.getShopVariantColorHex());
|
||||
if (qItem.getFilamentVariant() != null
|
||||
if (qItem.getMaterialCode() != null && !qItem.getMaterialCode().isBlank()) {
|
||||
oItem.setMaterialCode(qItem.getMaterialCode());
|
||||
} else if (qItem.getFilamentVariant() != null
|
||||
&& qItem.getFilamentVariant().getFilamentMaterialType() != null
|
||||
&& qItem.getFilamentVariant().getFilamentMaterialType().getMaterialCode() != null) {
|
||||
oItem.setMaterialCode(qItem.getFilamentVariant().getFilamentMaterialType().getMaterialCode());
|
||||
|
||||
@@ -88,14 +88,9 @@ public class InvoicePdfRenderingService {
|
||||
vars.put("shippingAddressLine2", order.getShippingZip() + " " + order.getShippingCity() + ", " + order.getShippingCountryCode());
|
||||
}
|
||||
|
||||
List<Map<String, Object>> invoiceLineItems = items.stream().map(i -> {
|
||||
Map<String, Object> line = new HashMap<>();
|
||||
line.put("description", "Stampa 3D: " + i.getOriginalFilename());
|
||||
line.put("quantity", i.getQuantity());
|
||||
line.put("unitPriceFormatted", String.format("CHF %.2f", i.getUnitPriceChf()));
|
||||
line.put("lineTotalFormatted", String.format("CHF %.2f", i.getLineTotalChf()));
|
||||
return line;
|
||||
}).collect(Collectors.toList());
|
||||
List<Map<String, Object>> invoiceLineItems = items.stream()
|
||||
.map(this::toInvoiceLineItem)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (order.getCadTotalChf() != null && order.getCadTotalChf().compareTo(BigDecimal.ZERO) > 0) {
|
||||
BigDecimal cadHours = order.getCadHours() != null ? order.getCadHours() : BigDecimal.ZERO;
|
||||
@@ -157,4 +152,45 @@ public class InvoicePdfRenderingService {
|
||||
private String formatCadHours(BigDecimal hours) {
|
||||
return hours.setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private Map<String, Object> toInvoiceLineItem(OrderItem item) {
|
||||
Map<String, Object> line = new HashMap<>();
|
||||
line.put("description", buildLineDescription(item));
|
||||
line.put("quantity", item.getQuantity());
|
||||
line.put("unitPriceFormatted", String.format("CHF %.2f", item.getUnitPriceChf()));
|
||||
line.put("lineTotalFormatted", String.format("CHF %.2f", item.getLineTotalChf()));
|
||||
return line;
|
||||
}
|
||||
|
||||
private String buildLineDescription(OrderItem item) {
|
||||
if (item == null) {
|
||||
return "Articolo";
|
||||
}
|
||||
|
||||
if ("SHOP_PRODUCT".equalsIgnoreCase(item.getItemType())) {
|
||||
String productName = firstNonBlank(
|
||||
item.getDisplayName(),
|
||||
item.getShopProductName(),
|
||||
item.getOriginalFilename(),
|
||||
"Prodotto shop"
|
||||
);
|
||||
String variantLabel = firstNonBlank(item.getShopVariantLabel(), item.getShopVariantColorName(), null);
|
||||
return variantLabel != null ? productName + " - " + variantLabel : productName;
|
||||
}
|
||||
|
||||
String fileName = firstNonBlank(item.getDisplayName(), item.getOriginalFilename(), "File 3D");
|
||||
return "Stampa 3D: " + fileName;
|
||||
}
|
||||
|
||||
private String firstNonBlank(String... values) {
|
||||
if (values == null || values.length == 0) {
|
||||
return null;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.printcalculator.controller.admin;
|
||||
|
||||
import com.printcalculator.config.SecurityConfig;
|
||||
import com.printcalculator.service.order.AdminOrderControllerService;
|
||||
import com.printcalculator.security.AdminLoginThrottleService;
|
||||
import com.printcalculator.security.AdminSessionAuthenticationFilter;
|
||||
import com.printcalculator.security.AdminSessionService;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
|
||||
import org.springframework.transaction.support.DefaultTransactionStatus;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
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.status;
|
||||
|
||||
@WebMvcTest(controllers = {AdminAuthController.class, AdminOrderController.class})
|
||||
@Import({
|
||||
SecurityConfig.class,
|
||||
AdminSessionAuthenticationFilter.class,
|
||||
AdminSessionService.class,
|
||||
AdminLoginThrottleService.class,
|
||||
AdminOrderControllerSecurityTest.TransactionTestConfig.class
|
||||
})
|
||||
@TestPropertySource(properties = {
|
||||
"admin.password=test-admin-password",
|
||||
"admin.session.secret=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"admin.session.ttl-minutes=60"
|
||||
})
|
||||
class AdminOrderControllerSecurityTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean
|
||||
private AdminOrderControllerService adminOrderControllerService;
|
||||
|
||||
@Test
|
||||
void confirmationDocument_withoutAdminCookie_shouldReturn401() throws Exception {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
|
||||
mockMvc.perform(get("/api/admin/orders/{orderId}/documents/confirmation", orderId))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmationDocument_withAdminCookie_shouldReturnPdf() throws Exception {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
when(adminOrderControllerService.downloadOrderConfirmation(orderId))
|
||||
.thenReturn(ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body("confirmation".getBytes()));
|
||||
|
||||
mockMvc.perform(get("/api/admin/orders/{orderId}/documents/confirmation", orderId)
|
||||
.cookie(loginAndExtractCookie()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().bytes("confirmation".getBytes()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void invoiceDocument_withAdminCookie_shouldReturnPdf() throws Exception {
|
||||
UUID orderId = UUID.randomUUID();
|
||||
when(adminOrderControllerService.downloadOrderInvoice(orderId))
|
||||
.thenReturn(ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body("invoice".getBytes()));
|
||||
|
||||
mockMvc.perform(get("/api/admin/orders/{orderId}/documents/invoice", orderId)
|
||||
.cookie(loginAndExtractCookie()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().bytes("invoice".getBytes()));
|
||||
}
|
||||
|
||||
private Cookie loginAndExtractCookie() throws Exception {
|
||||
MvcResult login = mockMvc.perform(post("/api/admin/auth/login")
|
||||
.with(req -> {
|
||||
req.setRemoteAddr("10.0.0.44");
|
||||
return req;
|
||||
})
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"password\":\"test-admin-password\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
String setCookie = login.getResponse().getHeader(HttpHeaders.SET_COOKIE);
|
||||
assertNotNull(setCookie);
|
||||
String[] parts = setCookie.split(";", 2);
|
||||
String[] keyValue = parts[0].split("=", 2);
|
||||
return new Cookie(keyValue[0], keyValue.length > 1 ? keyValue[1] : "");
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TransactionTestConfig {
|
||||
@Bean
|
||||
PlatformTransactionManager transactionManager() {
|
||||
return new AbstractPlatformTransactionManager() {
|
||||
@Override
|
||||
protected Object doGetTransaction() {
|
||||
return new Object();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doBegin(Object transaction, TransactionDefinition definition) {
|
||||
// No-op transaction manager for WebMvc security tests.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doCommit(DefaultTransactionStatus status) {
|
||||
// No-op transaction manager for WebMvc security tests.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doRollback(DefaultTransactionStatus status) {
|
||||
// No-op transaction manager for WebMvc security tests.
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package com.printcalculator.service;
|
||||
|
||||
import com.printcalculator.dto.AddressDto;
|
||||
import com.printcalculator.dto.CreateOrderRequest;
|
||||
import com.printcalculator.dto.CustomerDto;
|
||||
import com.printcalculator.entity.Customer;
|
||||
import com.printcalculator.entity.Order;
|
||||
import com.printcalculator.entity.OrderItem;
|
||||
import com.printcalculator.entity.Payment;
|
||||
import com.printcalculator.entity.QuoteLineItem;
|
||||
import com.printcalculator.entity.QuoteSession;
|
||||
import com.printcalculator.entity.ShopCategory;
|
||||
import com.printcalculator.entity.ShopProduct;
|
||||
import com.printcalculator.entity.ShopProductVariant;
|
||||
import com.printcalculator.event.OrderCreatedEvent;
|
||||
import com.printcalculator.repository.CustomerRepository;
|
||||
import com.printcalculator.repository.OrderItemRepository;
|
||||
import com.printcalculator.repository.OrderRepository;
|
||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||
import com.printcalculator.repository.QuoteSessionRepository;
|
||||
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.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
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.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OrderServiceTest {
|
||||
|
||||
@Mock
|
||||
private OrderRepository orderRepo;
|
||||
@Mock
|
||||
private OrderItemRepository orderItemRepo;
|
||||
@Mock
|
||||
private QuoteSessionRepository quoteSessionRepo;
|
||||
@Mock
|
||||
private QuoteLineItemRepository quoteLineItemRepo;
|
||||
@Mock
|
||||
private CustomerRepository customerRepo;
|
||||
@Mock
|
||||
private StorageService storageService;
|
||||
@Mock
|
||||
private InvoicePdfRenderingService invoiceService;
|
||||
@Mock
|
||||
private QrBillService qrBillService;
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
@Mock
|
||||
private PaymentService paymentService;
|
||||
@Mock
|
||||
private QuoteSessionTotalsService quoteSessionTotalsService;
|
||||
|
||||
@InjectMocks
|
||||
private OrderService service;
|
||||
|
||||
@Test
|
||||
void createOrderFromQuote_withShopCart_shouldPreserveShopSnapshotAndMaterialCode() throws Exception {
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
UUID orderId = UUID.randomUUID();
|
||||
UUID orderItemId = UUID.randomUUID();
|
||||
|
||||
QuoteSession session = new QuoteSession();
|
||||
session.setId(sessionId);
|
||||
session.setStatus("ACTIVE");
|
||||
session.setSessionType("SHOP_CART");
|
||||
session.setMaterialCode("SHOP");
|
||||
session.setPricingVersion("v1");
|
||||
session.setSetupCostChf(BigDecimal.ZERO);
|
||||
session.setExpiresAt(OffsetDateTime.now().plusDays(30));
|
||||
|
||||
ShopCategory category = new ShopCategory();
|
||||
category.setId(UUID.randomUUID());
|
||||
category.setSlug("cable-management");
|
||||
category.setName("Cable Management");
|
||||
|
||||
ShopProduct product = new ShopProduct();
|
||||
product.setId(UUID.randomUUID());
|
||||
product.setCategory(category);
|
||||
product.setSlug("desk-cable-clip");
|
||||
product.setName("Desk Cable Clip");
|
||||
|
||||
ShopProductVariant variant = new ShopProductVariant();
|
||||
variant.setId(UUID.randomUUID());
|
||||
variant.setProduct(product);
|
||||
variant.setVariantLabel("Coral Red");
|
||||
variant.setColorName("Coral Red");
|
||||
variant.setColorHex("#ff6b6b");
|
||||
variant.setInternalMaterialCode("PLA-MATTE");
|
||||
variant.setPriceChf(new BigDecimal("14.90"));
|
||||
|
||||
Path sourceDir = Path.of("storage_quotes").toAbsolutePath().normalize().resolve(sessionId.toString());
|
||||
Files.createDirectories(sourceDir);
|
||||
Path sourceFile = sourceDir.resolve("shop-demo.stl");
|
||||
Files.writeString(sourceFile, "solid demo\nendsolid demo\n", StandardCharsets.UTF_8);
|
||||
|
||||
QuoteLineItem qItem = new QuoteLineItem();
|
||||
qItem.setId(UUID.randomUUID());
|
||||
qItem.setQuoteSession(session);
|
||||
qItem.setStatus("READY");
|
||||
qItem.setLineItemType("SHOP_PRODUCT");
|
||||
qItem.setOriginalFilename("shop-demo.stl");
|
||||
qItem.setDisplayName("Desk Cable Clip");
|
||||
qItem.setQuantity(2);
|
||||
qItem.setColorCode("Coral Red");
|
||||
qItem.setMaterialCode("PLA-MATTE");
|
||||
qItem.setShopProduct(product);
|
||||
qItem.setShopProductVariant(variant);
|
||||
qItem.setShopProductSlug(product.getSlug());
|
||||
qItem.setShopProductName(product.getName());
|
||||
qItem.setShopVariantLabel("Coral Red");
|
||||
qItem.setShopVariantColorName("Coral Red");
|
||||
qItem.setShopVariantColorHex("#ff6b6b");
|
||||
qItem.setBoundingBoxXMm(new BigDecimal("60.000"));
|
||||
qItem.setBoundingBoxYMm(new BigDecimal("40.000"));
|
||||
qItem.setBoundingBoxZMm(new BigDecimal("20.000"));
|
||||
qItem.setUnitPriceChf(new BigDecimal("14.90"));
|
||||
qItem.setStoredPath(sourceFile.toString());
|
||||
|
||||
Customer customer = new Customer();
|
||||
customer.setId(UUID.randomUUID());
|
||||
customer.setEmail("buyer@example.com");
|
||||
|
||||
when(quoteSessionRepo.findById(sessionId)).thenReturn(Optional.of(session));
|
||||
when(customerRepo.findByEmail("buyer@example.com")).thenReturn(Optional.empty());
|
||||
when(customerRepo.save(any(Customer.class))).thenAnswer(invocation -> {
|
||||
Customer saved = invocation.getArgument(0);
|
||||
if (saved.getId() == null) {
|
||||
saved.setId(customer.getId());
|
||||
}
|
||||
return saved;
|
||||
});
|
||||
when(quoteLineItemRepo.findByQuoteSessionId(sessionId)).thenReturn(List.of(qItem));
|
||||
when(quoteSessionTotalsService.compute(eq(session), eq(List.of(qItem)))).thenReturn(
|
||||
new QuoteSessionTotalsService.QuoteSessionTotals(
|
||||
new BigDecimal("29.80"),
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
new BigDecimal("29.80"),
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
new BigDecimal("2.00"),
|
||||
new BigDecimal("31.80"),
|
||||
BigDecimal.ZERO
|
||||
)
|
||||
);
|
||||
when(orderRepo.save(any(Order.class))).thenAnswer(invocation -> {
|
||||
Order saved = invocation.getArgument(0);
|
||||
if (saved.getId() == null) {
|
||||
saved.setId(orderId);
|
||||
}
|
||||
return saved;
|
||||
});
|
||||
when(orderItemRepo.save(any(OrderItem.class))).thenAnswer(invocation -> {
|
||||
OrderItem saved = invocation.getArgument(0);
|
||||
if (saved.getId() == null) {
|
||||
saved.setId(orderItemId);
|
||||
}
|
||||
return saved;
|
||||
});
|
||||
when(qrBillService.generateQrBillSvg(any(Order.class))).thenReturn("<svg/>".getBytes(StandardCharsets.UTF_8));
|
||||
when(invoiceService.generateDocumentPdf(any(Order.class), any(List.class), eq(true), eq(qrBillService), isNull()))
|
||||
.thenReturn("pdf".getBytes(StandardCharsets.UTF_8));
|
||||
when(paymentService.getOrCreatePaymentForOrder(any(Order.class), eq("OTHER"))).thenReturn(new Payment());
|
||||
|
||||
Order order = service.createOrderFromQuote(sessionId, buildRequest());
|
||||
|
||||
assertEquals(orderId, order.getId());
|
||||
assertEquals("SHOP", order.getSourceType());
|
||||
assertEquals("CONVERTED", session.getStatus());
|
||||
assertEquals(orderId, session.getConvertedOrderId());
|
||||
assertAmountEquals("29.80", order.getSubtotalChf());
|
||||
assertAmountEquals("31.80", order.getTotalChf());
|
||||
|
||||
ArgumentCaptor<OrderItem> itemCaptor = ArgumentCaptor.forClass(OrderItem.class);
|
||||
verify(orderItemRepo, times(2)).save(itemCaptor.capture());
|
||||
OrderItem savedItem = itemCaptor.getAllValues().getLast();
|
||||
assertEquals("SHOP_PRODUCT", savedItem.getItemType());
|
||||
assertEquals("Desk Cable Clip", savedItem.getDisplayName());
|
||||
assertEquals("PLA-MATTE", savedItem.getMaterialCode());
|
||||
assertEquals("desk-cable-clip", savedItem.getShopProductSlug());
|
||||
assertEquals("Desk Cable Clip", savedItem.getShopProductName());
|
||||
assertEquals("Coral Red", savedItem.getShopVariantLabel());
|
||||
assertEquals("Coral Red", savedItem.getShopVariantColorName());
|
||||
assertEquals("#ff6b6b", savedItem.getShopVariantColorHex());
|
||||
assertAmountEquals("14.90", savedItem.getUnitPriceChf());
|
||||
assertAmountEquals("29.80", savedItem.getLineTotalChf());
|
||||
|
||||
verify(storageService).store(eq(sourceFile), eq(Path.of(
|
||||
"orders", orderId.toString(), "3d-files", orderItemId.toString(), savedItem.getStoredFilename()
|
||||
)));
|
||||
verify(paymentService).getOrCreatePaymentForOrder(order, "OTHER");
|
||||
verify(eventPublisher).publishEvent(any(OrderCreatedEvent.class));
|
||||
}
|
||||
|
||||
private CreateOrderRequest buildRequest() {
|
||||
CustomerDto customer = new CustomerDto();
|
||||
customer.setEmail("buyer@example.com");
|
||||
customer.setPhone("+41790000000");
|
||||
customer.setCustomerType("PRIVATE");
|
||||
|
||||
AddressDto billing = new AddressDto();
|
||||
billing.setFirstName("Joe");
|
||||
billing.setLastName("Buyer");
|
||||
billing.setAddressLine1("Via Test 1");
|
||||
billing.setZip("6900");
|
||||
billing.setCity("Lugano");
|
||||
billing.setCountryCode("CH");
|
||||
|
||||
CreateOrderRequest request = new CreateOrderRequest();
|
||||
request.setCustomer(customer);
|
||||
request.setBillingAddress(billing);
|
||||
request.setShippingSameAsBilling(true);
|
||||
request.setLanguage("it");
|
||||
request.setAcceptTerms(true);
|
||||
request.setAcceptPrivacy(true);
|
||||
return request;
|
||||
}
|
||||
|
||||
private void assertAmountEquals(String expected, BigDecimal actual) {
|
||||
assertTrue(new BigDecimal(expected).compareTo(actual) == 0,
|
||||
"Expected " + expected + " but got " + actual);
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,8 @@ class PublicMediaQueryServiceTest {
|
||||
MediaUsage usageDraft = buildUsage(draftAsset, "HOME_SECTION", "shop-gallery", 0, false, true);
|
||||
MediaUsage usagePrivate = buildUsage(privateAsset, "HOME_SECTION", "shop-gallery", 3, false, true);
|
||||
|
||||
when(mediaUsageRepository.findByUsageTypeAndUsageKeyAndIsActiveTrueOrderBySortOrderAscCreatedAtAsc(
|
||||
"HOME_SECTION", "shop-gallery"
|
||||
when(mediaUsageRepository.findActiveByUsageTypeAndUsageKeys(
|
||||
"HOME_SECTION", List.of("shop-gallery")
|
||||
)).thenReturn(List.of(usageSecond, usageFirst, usageDraft, usagePrivate));
|
||||
when(mediaVariantRepository.findByMediaAsset_IdIn(List.of(readyPublicAsset.getId())))
|
||||
.thenReturn(List.of(
|
||||
@@ -93,8 +93,8 @@ class PublicMediaQueryServiceTest {
|
||||
MediaAsset asset = buildAsset("READY", "PUBLIC", "Joe portrait", "Joe portrait fallback");
|
||||
MediaUsage usage = buildUsage(asset, "ABOUT_MEMBER", "joe", 0, true, true);
|
||||
|
||||
when(mediaUsageRepository.findByUsageTypeAndUsageKeyAndIsActiveTrueOrderBySortOrderAscCreatedAtAsc(
|
||||
"ABOUT_MEMBER", "joe"
|
||||
when(mediaUsageRepository.findActiveByUsageTypeAndUsageKeys(
|
||||
"ABOUT_MEMBER", List.of("joe")
|
||||
)).thenReturn(List.of(usage));
|
||||
when(mediaVariantRepository.findByMediaAsset_IdIn(List.of(asset.getId())))
|
||||
.thenReturn(List.of(buildVariant(asset, "card", "JPEG", "joe/card.jpg")));
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.printcalculator.service.payment;
|
||||
|
||||
import com.printcalculator.entity.Order;
|
||||
import com.printcalculator.entity.OrderItem;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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.assertNotNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class InvoicePdfRenderingServiceTest {
|
||||
|
||||
@Test
|
||||
void generateDocumentPdf_shouldDescribeShopItemsWithProductAndVariant() {
|
||||
CapturingInvoicePdfRenderingService service = new CapturingInvoicePdfRenderingService();
|
||||
QrBillService qrBillService = mock(QrBillService.class);
|
||||
when(qrBillService.generateQrBillSvg(org.mockito.ArgumentMatchers.any(Order.class)))
|
||||
.thenReturn("<svg/>".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Order order = new Order();
|
||||
order.setId(UUID.randomUUID());
|
||||
order.setCreatedAt(OffsetDateTime.parse("2026-03-10T10:15:30+01:00"));
|
||||
order.setBillingCustomerType("PRIVATE");
|
||||
order.setBillingFirstName("Joe");
|
||||
order.setBillingLastName("Buyer");
|
||||
order.setBillingAddressLine1("Via Test 1");
|
||||
order.setBillingZip("6900");
|
||||
order.setBillingCity("Lugano");
|
||||
order.setBillingCountryCode("CH");
|
||||
order.setSetupCostChf(BigDecimal.ZERO);
|
||||
order.setShippingCostChf(new BigDecimal("2.00"));
|
||||
order.setSubtotalChf(new BigDecimal("36.80"));
|
||||
order.setTotalChf(new BigDecimal("38.80"));
|
||||
order.setCadTotalChf(BigDecimal.ZERO);
|
||||
|
||||
OrderItem shopItem = new OrderItem();
|
||||
shopItem.setItemType("SHOP_PRODUCT");
|
||||
shopItem.setDisplayName("Desk Cable Clip");
|
||||
shopItem.setOriginalFilename("desk-cable-clip-demo.stl");
|
||||
shopItem.setShopProductName("Desk Cable Clip");
|
||||
shopItem.setShopVariantLabel("Coral Red");
|
||||
shopItem.setQuantity(2);
|
||||
shopItem.setUnitPriceChf(new BigDecimal("14.90"));
|
||||
shopItem.setLineTotalChf(new BigDecimal("29.80"));
|
||||
|
||||
OrderItem printItem = new OrderItem();
|
||||
printItem.setItemType("PRINT_FILE");
|
||||
printItem.setDisplayName("gear-cover.stl");
|
||||
printItem.setOriginalFilename("gear-cover.stl");
|
||||
printItem.setQuantity(1);
|
||||
printItem.setUnitPriceChf(new BigDecimal("7.00"));
|
||||
printItem.setLineTotalChf(new BigDecimal("7.00"));
|
||||
|
||||
byte[] pdf = service.generateDocumentPdf(order, List.of(shopItem, printItem), true, qrBillService, null);
|
||||
|
||||
assertNotNull(pdf);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> invoiceLineItems = (List<Map<String, Object>>) service.capturedVariables.get("invoiceLineItems");
|
||||
assertEquals("Desk Cable Clip - Coral Red", invoiceLineItems.getFirst().get("description"));
|
||||
assertEquals("Stampa 3D: gear-cover.stl", invoiceLineItems.get(1).get("description"));
|
||||
}
|
||||
|
||||
private static class CapturingInvoicePdfRenderingService extends InvoicePdfRenderingService {
|
||||
private Map<String, Object> capturedVariables;
|
||||
|
||||
private CapturingInvoicePdfRenderingService() {
|
||||
super(mock(TemplateEngine.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] generateInvoicePdfBytesFromTemplate(Map<String, Object> invoiceTemplateVariables, String qrBillSvg) {
|
||||
this.capturedVariables = invoiceTemplateVariables;
|
||||
return new byte[]{1, 2, 3};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package com.printcalculator.service.shop;
|
||||
|
||||
import com.printcalculator.dto.ShopCartAddItemRequest;
|
||||
import com.printcalculator.entity.QuoteLineItem;
|
||||
import com.printcalculator.entity.QuoteSession;
|
||||
import com.printcalculator.entity.ShopCategory;
|
||||
import com.printcalculator.entity.ShopProduct;
|
||||
import com.printcalculator.entity.ShopProductVariant;
|
||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||
import com.printcalculator.repository.QuoteSessionRepository;
|
||||
import com.printcalculator.repository.ShopProductModelAssetRepository;
|
||||
import com.printcalculator.repository.ShopProductVariantRepository;
|
||||
import com.printcalculator.service.QuoteSessionTotalsService;
|
||||
import com.printcalculator.service.quote.QuoteSessionResponseAssembler;
|
||||
import com.printcalculator.service.quote.QuoteStorageService;
|
||||
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.mock.web.MockHttpServletRequest;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
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.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.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ShopCartServiceTest {
|
||||
|
||||
@Mock
|
||||
private QuoteSessionRepository quoteSessionRepository;
|
||||
@Mock
|
||||
private QuoteLineItemRepository quoteLineItemRepository;
|
||||
@Mock
|
||||
private ShopProductVariantRepository shopProductVariantRepository;
|
||||
@Mock
|
||||
private ShopProductModelAssetRepository shopProductModelAssetRepository;
|
||||
@Mock
|
||||
private QuoteSessionTotalsService quoteSessionTotalsService;
|
||||
@Mock
|
||||
private QuoteSessionResponseAssembler quoteSessionResponseAssembler;
|
||||
@Mock
|
||||
private ShopStorageService shopStorageService;
|
||||
@Mock
|
||||
private ShopCartCookieService shopCartCookieService;
|
||||
|
||||
private ShopCartService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ShopCartService(
|
||||
quoteSessionRepository,
|
||||
quoteLineItemRepository,
|
||||
shopProductVariantRepository,
|
||||
shopProductModelAssetRepository,
|
||||
quoteSessionTotalsService,
|
||||
quoteSessionResponseAssembler,
|
||||
new QuoteStorageService(),
|
||||
shopStorageService,
|
||||
shopCartCookieService
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addItem_shouldCreateServerCartAndPersistVariantPricingSnapshot() {
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
UUID lineItemId = UUID.randomUUID();
|
||||
UUID variantId = UUID.randomUUID();
|
||||
List<QuoteLineItem> savedItems = new ArrayList<>();
|
||||
|
||||
ShopProductVariant variant = buildVariant(variantId);
|
||||
|
||||
when(shopCartCookieService.extractSessionId(any())).thenReturn(Optional.empty());
|
||||
when(shopCartCookieService.getCookieTtlDays()).thenReturn(30L);
|
||||
when(shopProductVariantRepository.findById(variantId)).thenReturn(Optional.of(variant));
|
||||
when(shopProductModelAssetRepository.findByProduct_Id(variant.getProduct().getId())).thenReturn(Optional.empty());
|
||||
when(quoteSessionRepository.save(any(QuoteSession.class))).thenAnswer(invocation -> {
|
||||
QuoteSession session = invocation.getArgument(0);
|
||||
if (session.getId() == null) {
|
||||
session.setId(sessionId);
|
||||
}
|
||||
return session;
|
||||
});
|
||||
when(quoteLineItemRepository.findFirstByQuoteSession_IdAndLineItemTypeAndShopProductVariant_Id(
|
||||
eq(sessionId),
|
||||
eq("SHOP_PRODUCT"),
|
||||
eq(variantId)
|
||||
)).thenReturn(Optional.empty());
|
||||
when(quoteLineItemRepository.save(any(QuoteLineItem.class))).thenAnswer(invocation -> {
|
||||
QuoteLineItem item = invocation.getArgument(0);
|
||||
if (item.getId() == null) {
|
||||
item.setId(lineItemId);
|
||||
}
|
||||
savedItems.clear();
|
||||
savedItems.add(item);
|
||||
return item;
|
||||
});
|
||||
when(quoteLineItemRepository.findByQuoteSessionIdOrderByCreatedAtAsc(sessionId)).thenAnswer(invocation -> List.copyOf(savedItems));
|
||||
when(quoteSessionTotalsService.compute(any(), any())).thenReturn(
|
||||
new QuoteSessionTotalsService.QuoteSessionTotals(
|
||||
new BigDecimal("22.80"),
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
new BigDecimal("22.80"),
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
new BigDecimal("2.00"),
|
||||
new BigDecimal("24.80"),
|
||||
BigDecimal.ZERO
|
||||
)
|
||||
);
|
||||
when(quoteSessionResponseAssembler.assemble(any(), any(), any())).thenAnswer(invocation -> {
|
||||
QuoteSession session = invocation.getArgument(0);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("session", session);
|
||||
response.put("items", List.of());
|
||||
response.put("grandTotalChf", new BigDecimal("24.80"));
|
||||
return response;
|
||||
});
|
||||
|
||||
ShopCartAddItemRequest payload = new ShopCartAddItemRequest();
|
||||
payload.setShopProductVariantId(variantId);
|
||||
payload.setQuantity(2);
|
||||
|
||||
ShopCartService.CartResult result = service.addItem(new MockHttpServletRequest(), payload);
|
||||
|
||||
assertEquals(sessionId, result.sessionId());
|
||||
assertFalse(result.clearCookie());
|
||||
assertEquals(new BigDecimal("24.80"), result.response().get("grandTotalChf"));
|
||||
|
||||
QuoteLineItem savedItem = savedItems.getFirst();
|
||||
assertEquals("SHOP_PRODUCT", savedItem.getLineItemType());
|
||||
assertEquals("Desk Cable Clip", savedItem.getDisplayName());
|
||||
assertEquals("desk-cable-clip", savedItem.getOriginalFilename());
|
||||
assertEquals(2, savedItem.getQuantity());
|
||||
assertEquals("PLA", savedItem.getMaterialCode());
|
||||
assertEquals("Coral Red", savedItem.getColorCode());
|
||||
assertEquals("Desk Cable Clip", savedItem.getShopProductName());
|
||||
assertEquals("Coral Red", savedItem.getShopVariantLabel());
|
||||
assertEquals("Coral Red", savedItem.getShopVariantColorName());
|
||||
assertAmountEquals("11.40", savedItem.getUnitPriceChf());
|
||||
assertNull(savedItem.getStoredPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadCart_withExpiredCookieSession_shouldExpireSessionAndAskCookieClear() {
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
QuoteSession session = new QuoteSession();
|
||||
session.setId(sessionId);
|
||||
session.setSessionType("SHOP_CART");
|
||||
session.setStatus("ACTIVE");
|
||||
session.setExpiresAt(OffsetDateTime.now().minusHours(1));
|
||||
|
||||
Map<String, Object> emptyResponse = new HashMap<>();
|
||||
emptyResponse.put("session", null);
|
||||
emptyResponse.put("items", List.of());
|
||||
|
||||
when(shopCartCookieService.hasCartCookie(any())).thenReturn(true);
|
||||
when(shopCartCookieService.extractSessionId(any())).thenReturn(Optional.of(sessionId));
|
||||
when(quoteSessionRepository.findByIdAndSessionType(sessionId, "SHOP_CART")).thenReturn(Optional.of(session));
|
||||
when(quoteSessionRepository.save(any(QuoteSession.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(quoteSessionResponseAssembler.emptyCart()).thenReturn(emptyResponse);
|
||||
|
||||
ShopCartService.CartResult result = service.loadCart(new MockHttpServletRequest());
|
||||
|
||||
assertTrue(result.clearCookie());
|
||||
assertNull(result.sessionId());
|
||||
assertEquals(emptyResponse, result.response());
|
||||
assertEquals("EXPIRED", session.getStatus());
|
||||
verify(quoteSessionRepository).save(session);
|
||||
}
|
||||
|
||||
private ShopProductVariant buildVariant(UUID variantId) {
|
||||
ShopCategory category = new ShopCategory();
|
||||
category.setId(UUID.randomUUID());
|
||||
category.setSlug("cable-management");
|
||||
category.setName("Cable Management");
|
||||
category.setIsActive(true);
|
||||
|
||||
ShopProduct product = new ShopProduct();
|
||||
product.setId(UUID.randomUUID());
|
||||
product.setCategory(category);
|
||||
product.setSlug("desk-cable-clip");
|
||||
product.setName("Desk Cable Clip");
|
||||
product.setIsActive(true);
|
||||
|
||||
ShopProductVariant variant = new ShopProductVariant();
|
||||
variant.setId(variantId);
|
||||
variant.setProduct(product);
|
||||
variant.setSku("DEMO-CLIP-CORAL");
|
||||
variant.setVariantLabel("Coral Red");
|
||||
variant.setColorName("Coral Red");
|
||||
variant.setColorHex("#ff6b6b");
|
||||
variant.setInternalMaterialCode("PLA");
|
||||
variant.setPriceChf(new BigDecimal("11.40"));
|
||||
variant.setIsActive(true);
|
||||
variant.setIsDefault(false);
|
||||
return variant;
|
||||
}
|
||||
|
||||
private void assertAmountEquals(String expected, BigDecimal actual) {
|
||||
assertTrue(new BigDecimal(expected).compareTo(actual) == 0,
|
||||
"Expected " + expected + " but got " + actual);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user