produzione 1 #9
@@ -0,0 +1,131 @@
|
||||
package com.printcalculator.controller;
|
||||
|
||||
import com.printcalculator.entity.CustomQuoteRequest;
|
||||
import com.printcalculator.entity.CustomQuoteRequestAttachment;
|
||||
import com.printcalculator.repository.CustomQuoteRequestAttachmentRepository;
|
||||
import com.printcalculator.repository.CustomQuoteRequestRepository;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/custom-quote-requests")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class CustomQuoteRequestController {
|
||||
|
||||
private final CustomQuoteRequestRepository requestRepo;
|
||||
private final CustomQuoteRequestAttachmentRepository attachmentRepo;
|
||||
|
||||
// TODO: Inject Storage Service
|
||||
private static final String STORAGE_ROOT = "storage_requests";
|
||||
|
||||
public CustomQuoteRequestController(CustomQuoteRequestRepository requestRepo,
|
||||
CustomQuoteRequestAttachmentRepository attachmentRepo) {
|
||||
this.requestRepo = requestRepo;
|
||||
this.attachmentRepo = attachmentRepo;
|
||||
}
|
||||
|
||||
// 1. Create Custom Quote Request
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Transactional
|
||||
public ResponseEntity<CustomQuoteRequest> createCustomQuoteRequest(
|
||||
// Form fields
|
||||
@RequestParam("requestType") String requestType,
|
||||
@RequestParam("customerType") String customerType,
|
||||
@RequestParam("email") String email,
|
||||
@RequestParam(value = "phone", required = false) String phone,
|
||||
@RequestParam(value = "name", required = false) String name,
|
||||
@RequestParam(value = "companyName", required = false) String companyName,
|
||||
@RequestParam(value = "contactPerson", required = false) String contactPerson,
|
||||
@RequestParam("message") String message,
|
||||
// Files (Max 15)
|
||||
@RequestParam(value = "files", required = false) List<MultipartFile> files
|
||||
) throws IOException {
|
||||
|
||||
// 1. Create Request
|
||||
CustomQuoteRequest request = new CustomQuoteRequest();
|
||||
request.setRequestType(requestType);
|
||||
request.setCustomerType(customerType);
|
||||
request.setEmail(email);
|
||||
request.setPhone(phone);
|
||||
request.setName(name);
|
||||
request.setCompanyName(companyName);
|
||||
request.setContactPerson(contactPerson);
|
||||
request.setMessage(message);
|
||||
request.setStatus("PENDING");
|
||||
request.setCreatedAt(OffsetDateTime.now());
|
||||
request.setUpdatedAt(OffsetDateTime.now());
|
||||
|
||||
request = requestRepo.save(request);
|
||||
|
||||
// 2. Handle Attachments
|
||||
if (files != null && !files.isEmpty()) {
|
||||
if (files.size() > 15) {
|
||||
throw new IOException("Too many files. Max 15 allowed.");
|
||||
}
|
||||
|
||||
for (MultipartFile file : files) {
|
||||
if (file.isEmpty()) continue;
|
||||
|
||||
CustomQuoteRequestAttachment attachment = new CustomQuoteRequestAttachment();
|
||||
attachment.setRequest(request);
|
||||
attachment.setOriginalFilename(file.getOriginalFilename());
|
||||
attachment.setMimeType(file.getContentType());
|
||||
attachment.setFileSizeBytes(file.getSize());
|
||||
attachment.setCreatedAt(OffsetDateTime.now());
|
||||
|
||||
// Generate path
|
||||
UUID fileUuid = UUID.randomUUID();
|
||||
String ext = getExtension(file.getOriginalFilename());
|
||||
String storedFilename = fileUuid.toString() + "." + ext;
|
||||
|
||||
// Note: We don't have attachment ID yet.
|
||||
// We'll save attachment first to get ID.
|
||||
attachment.setStoredFilename(storedFilename);
|
||||
attachment.setStoredRelativePath("PENDING");
|
||||
|
||||
attachment = attachmentRepo.save(attachment);
|
||||
|
||||
String relativePath = "quote-requests/" + request.getId() + "/attachments/" + attachment.getId() + "/" + storedFilename;
|
||||
attachment.setStoredRelativePath(relativePath);
|
||||
attachmentRepo.save(attachment);
|
||||
|
||||
// Save file to disk
|
||||
Path absolutePath = Paths.get(STORAGE_ROOT, relativePath);
|
||||
Files.createDirectories(absolutePath.getParent());
|
||||
Files.copy(file.getInputStream(), absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(request);
|
||||
}
|
||||
|
||||
// 2. Get Request
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<CustomQuoteRequest> getCustomQuoteRequest(@PathVariable UUID id) {
|
||||
return requestRepo.findById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// Helper
|
||||
private String getExtension(String filename) {
|
||||
if (filename == null) return "dat";
|
||||
int i = filename.lastIndexOf('.');
|
||||
if (i > 0) {
|
||||
return filename.substring(i + 1);
|
||||
}
|
||||
return "dat";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.printcalculator.controller;
|
||||
|
||||
import com.printcalculator.entity.*;
|
||||
import com.printcalculator.repository.*;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/orders")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class OrderController {
|
||||
|
||||
private final OrderRepository orderRepo;
|
||||
private final OrderItemRepository orderItemRepo;
|
||||
private final QuoteSessionRepository quoteSessionRepo;
|
||||
private final QuoteLineItemRepository quoteLineItemRepo;
|
||||
private final CustomerRepository customerRepo;
|
||||
|
||||
// TODO: Inject Storage Service or use a base path property
|
||||
private static final String STORAGE_ROOT = "storage_orders";
|
||||
|
||||
public OrderController(OrderRepository orderRepo,
|
||||
OrderItemRepository orderItemRepo,
|
||||
QuoteSessionRepository quoteSessionRepo,
|
||||
QuoteLineItemRepository quoteLineItemRepo,
|
||||
CustomerRepository customerRepo) {
|
||||
this.orderRepo = orderRepo;
|
||||
this.orderItemRepo = orderItemRepo;
|
||||
this.quoteSessionRepo = quoteSessionRepo;
|
||||
this.quoteLineItemRepo = quoteLineItemRepo;
|
||||
this.customerRepo = customerRepo;
|
||||
}
|
||||
|
||||
// DTOs
|
||||
public static class CreateOrderRequest {
|
||||
public CustomerDto customer;
|
||||
public AddressDto billingAddress;
|
||||
public AddressDto shippingAddress;
|
||||
public boolean shippingSameAsBilling;
|
||||
}
|
||||
|
||||
public static class CustomerDto {
|
||||
public String email;
|
||||
public String phone;
|
||||
public String customerType; // "PRIVATE", "BUSINESS"
|
||||
}
|
||||
|
||||
public static class AddressDto {
|
||||
public String firstName;
|
||||
public String lastName;
|
||||
public String companyName;
|
||||
public String contactPerson;
|
||||
public String addressLine1;
|
||||
public String addressLine2;
|
||||
public String zip;
|
||||
public String city;
|
||||
public String countryCode;
|
||||
}
|
||||
|
||||
// 1. Create Order from Quote
|
||||
@PostMapping("/from-quote/{quoteSessionId}")
|
||||
@Transactional
|
||||
public ResponseEntity<Order> createOrderFromQuote(
|
||||
@PathVariable UUID quoteSessionId,
|
||||
@RequestBody CreateOrderRequest request
|
||||
) {
|
||||
// 1. Fetch Quote Session
|
||||
QuoteSession session = quoteSessionRepo.findById(quoteSessionId)
|
||||
.orElseThrow(() -> new RuntimeException("Quote Session not found"));
|
||||
|
||||
if (!"ACTIVE".equals(session.getStatus())) {
|
||||
// Allow converting only active sessions? Or check if not already converted?
|
||||
// checking convertedOrderId might be better
|
||||
}
|
||||
if (session.getConvertedOrderId() != null) {
|
||||
return ResponseEntity.badRequest().body(null); // Already converted
|
||||
}
|
||||
|
||||
// 2. Handle Customer (Find or Create)
|
||||
Customer customer = customerRepo.findByEmail(request.customer.email)
|
||||
.orElseGet(() -> {
|
||||
Customer newC = new Customer();
|
||||
newC.setEmail(request.customer.email);
|
||||
newC.setCreatedAt(OffsetDateTime.now());
|
||||
return customerRepo.save(newC);
|
||||
});
|
||||
// Update customer details?
|
||||
customer.setPhone(request.customer.phone);
|
||||
customer.setCustomerType(request.customer.customerType);
|
||||
customer.setUpdatedAt(OffsetDateTime.now());
|
||||
customerRepo.save(customer);
|
||||
|
||||
// 3. Create Order
|
||||
Order order = new Order();
|
||||
order.setSourceQuoteSession(session);
|
||||
order.setCustomer(customer);
|
||||
order.setCustomerEmail(request.customer.email);
|
||||
order.setCustomerPhone(request.customer.phone);
|
||||
order.setStatus("PENDING_PAYMENT");
|
||||
order.setCreatedAt(OffsetDateTime.now());
|
||||
order.setUpdatedAt(OffsetDateTime.now());
|
||||
order.setCurrency("CHF");
|
||||
|
||||
// Billing
|
||||
order.setBillingCustomerType(request.customer.customerType);
|
||||
if (request.billingAddress != null) {
|
||||
order.setBillingFirstName(request.billingAddress.firstName);
|
||||
order.setBillingLastName(request.billingAddress.lastName);
|
||||
order.setBillingCompanyName(request.billingAddress.companyName);
|
||||
order.setBillingContactPerson(request.billingAddress.contactPerson);
|
||||
order.setBillingAddressLine1(request.billingAddress.addressLine1);
|
||||
order.setBillingAddressLine2(request.billingAddress.addressLine2);
|
||||
order.setBillingZip(request.billingAddress.zip);
|
||||
order.setBillingCity(request.billingAddress.city);
|
||||
order.setBillingCountryCode(request.billingAddress.countryCode != null ? request.billingAddress.countryCode : "CH");
|
||||
}
|
||||
|
||||
// Shipping
|
||||
order.setShippingSameAsBilling(request.shippingSameAsBilling);
|
||||
if (!request.shippingSameAsBilling && request.shippingAddress != null) {
|
||||
order.setShippingFirstName(request.shippingAddress.firstName);
|
||||
order.setShippingLastName(request.shippingAddress.lastName);
|
||||
order.setShippingCompanyName(request.shippingAddress.companyName);
|
||||
order.setShippingContactPerson(request.shippingAddress.contactPerson);
|
||||
order.setShippingAddressLine1(request.shippingAddress.addressLine1);
|
||||
order.setShippingAddressLine2(request.shippingAddress.addressLine2);
|
||||
order.setShippingZip(request.shippingAddress.zip);
|
||||
order.setShippingCity(request.shippingAddress.city);
|
||||
order.setShippingCountryCode(request.shippingAddress.countryCode != null ? request.shippingAddress.countryCode : "CH");
|
||||
} else {
|
||||
// Copy billing to shipping? Or leave empty and rely on flag?
|
||||
// Usually explicit copy is safer for queries
|
||||
order.setShippingFirstName(order.getBillingFirstName());
|
||||
order.setShippingLastName(order.getBillingLastName());
|
||||
order.setShippingCompanyName(order.getBillingCompanyName());
|
||||
order.setShippingContactPerson(order.getBillingContactPerson());
|
||||
order.setShippingAddressLine1(order.getBillingAddressLine1());
|
||||
order.setShippingAddressLine2(order.getBillingAddressLine2());
|
||||
order.setShippingZip(order.getBillingZip());
|
||||
order.setShippingCity(order.getBillingCity());
|
||||
order.setShippingCountryCode(order.getBillingCountryCode());
|
||||
}
|
||||
|
||||
// Financials from Session (Assuming mocked/calculated in session)
|
||||
// We re-calculate totals from line items to be safe
|
||||
List<QuoteLineItem> quoteItems = quoteLineItemRepo.findByQuoteSessionId(quoteSessionId);
|
||||
|
||||
BigDecimal subtotal = BigDecimal.ZERO;
|
||||
|
||||
// Save Order first to get ID
|
||||
order = orderRepo.save(order);
|
||||
|
||||
// 4. Create Order Items
|
||||
for (QuoteLineItem qItem : quoteItems) {
|
||||
OrderItem oItem = new OrderItem();
|
||||
oItem.setOrder(order);
|
||||
oItem.setOriginalFilename(qItem.getOriginalFilename());
|
||||
oItem.setQuantity(qItem.getQuantity());
|
||||
oItem.setColorCode(qItem.getColorCode());
|
||||
oItem.setMaterialCode(session.getMaterialCode()); // Or per item if supported
|
||||
|
||||
// Pricing
|
||||
oItem.setUnitPriceChf(qItem.getUnitPriceChf());
|
||||
oItem.setLineTotalChf(qItem.getUnitPriceChf().multiply(BigDecimal.valueOf(qItem.getQuantity())));
|
||||
oItem.setPrintTimeSeconds(qItem.getPrintTimeSeconds());
|
||||
oItem.setMaterialGrams(qItem.getMaterialGrams());
|
||||
|
||||
// File Handling Check
|
||||
// "orders/{orderId}/3d-files/{orderItemId}/{uuid}.{ext}"
|
||||
UUID fileUuid = UUID.randomUUID();
|
||||
String ext = getExtension(qItem.getOriginalFilename());
|
||||
String storedFilename = fileUuid.toString() + "." + ext;
|
||||
|
||||
// Note: We don't have the orderItemId yet because we haven't saved it.
|
||||
// We can pre-generate ID or save order item then update path?
|
||||
// GeneratedValue strategy AUTO might not let us set ID easily?
|
||||
// Let's save item first with temporary path, then update?
|
||||
// OR use a path structure that doesn't depend on ItemId? "orders/{orderId}/3d-files/{uuid}.ext" is also fine?
|
||||
// User requested: "orders/{orderId}/3d-files/{orderItemId}/{uuid}.{ext}"
|
||||
// So we need OrderItemId.
|
||||
|
||||
oItem.setStoredFilename(storedFilename);
|
||||
oItem.setStoredRelativePath("PENDING"); // Placeholder
|
||||
oItem.setMimeType("application/octet-stream"); // specific type if known
|
||||
|
||||
oItem = orderItemRepo.save(oItem);
|
||||
|
||||
// Update Path now that we have ID
|
||||
String relativePath = "orders/" + order.getId() + "/3d-files/" + oItem.getId() + "/" + storedFilename;
|
||||
oItem.setStoredRelativePath(relativePath);
|
||||
orderItemRepo.save(oItem);
|
||||
|
||||
subtotal = subtotal.add(oItem.getLineTotalChf());
|
||||
}
|
||||
|
||||
// Update Order Totals
|
||||
order.setSubtotalChf(subtotal);
|
||||
order.setSetupCostChf(session.getSetupCostChf());
|
||||
order.setShippingCostChf(BigDecimal.valueOf(9.00)); // Default shipping? or 0?
|
||||
// TODO: Calc implementation for shipping
|
||||
|
||||
BigDecimal total = subtotal.add(order.getSetupCostChf()).add(order.getShippingCostChf()).subtract(order.getDiscountChf() != null ? order.getDiscountChf() : BigDecimal.ZERO);
|
||||
order.setTotalChf(total);
|
||||
|
||||
// Link session
|
||||
session.setConvertedOrderId(order.getId());
|
||||
session.setStatus("CONVERTED"); // or CLOSED
|
||||
quoteSessionRepo.save(session);
|
||||
|
||||
return ResponseEntity.ok(orderRepo.save(order));
|
||||
}
|
||||
|
||||
// 2. Upload file for Order Item
|
||||
@PostMapping(value = "/{orderId}/items/{orderItemId}/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Transactional
|
||||
public ResponseEntity<Void> uploadOrderItemFile(
|
||||
@PathVariable UUID orderId,
|
||||
@PathVariable UUID orderItemId,
|
||||
@RequestParam("file") MultipartFile file
|
||||
) throws IOException {
|
||||
|
||||
OrderItem item = orderItemRepo.findById(orderItemId)
|
||||
.orElseThrow(() -> new RuntimeException("OrderItem not found"));
|
||||
|
||||
if (!item.getOrder().getId().equals(orderId)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
// Ensure path logic
|
||||
String relativePath = item.getStoredRelativePath();
|
||||
if (relativePath == null || relativePath.equals("PENDING")) {
|
||||
// Should verify consistency
|
||||
// If we used the logic above, it should have a path.
|
||||
// If it's "PENDING", regen it.
|
||||
String ext = getExtension(file.getOriginalFilename());
|
||||
String storedFilename = UUID.randomUUID().toString() + "." + ext;
|
||||
relativePath = "orders/" + orderId + "/3d-files/" + orderItemId + "/" + storedFilename;
|
||||
item.setStoredRelativePath(relativePath);
|
||||
item.setStoredFilename(storedFilename);
|
||||
// Update item
|
||||
}
|
||||
|
||||
// Save file to disk
|
||||
Path absolutePath = Paths.get(STORAGE_ROOT, relativePath);
|
||||
Files.createDirectories(absolutePath.getParent());
|
||||
|
||||
if (Files.exists(absolutePath)) {
|
||||
Files.delete(absolutePath); // Overwrite?
|
||||
}
|
||||
|
||||
Files.copy(file.getInputStream(), absolutePath);
|
||||
|
||||
item.setFileSizeBytes(file.getSize());
|
||||
item.setMimeType(file.getContentType());
|
||||
// Calculate SHA256? (Optional)
|
||||
|
||||
orderItemRepo.save(item);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private String getExtension(String filename) {
|
||||
if (filename == null) return "stl";
|
||||
int i = filename.lastIndexOf('.');
|
||||
if (i > 0) {
|
||||
return filename.substring(i + 1);
|
||||
}
|
||||
return "stl";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.printcalculator.controller;
|
||||
|
||||
import com.printcalculator.entity.PrinterMachine;
|
||||
import com.printcalculator.entity.QuoteLineItem;
|
||||
import com.printcalculator.entity.QuoteSession;
|
||||
import com.printcalculator.model.PrintStats;
|
||||
import com.printcalculator.model.QuoteResult;
|
||||
import com.printcalculator.repository.PrinterMachineRepository;
|
||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||
import com.printcalculator.repository.QuoteSessionRepository;
|
||||
import com.printcalculator.service.QuoteCalculator;
|
||||
import com.printcalculator.service.SlicerService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/quote-sessions")
|
||||
@CrossOrigin(origins = "*") // Allow CORS for dev
|
||||
public class QuoteSessionController {
|
||||
|
||||
private final QuoteSessionRepository sessionRepo;
|
||||
private final QuoteLineItemRepository lineItemRepo;
|
||||
private final SlicerService slicerService;
|
||||
private final QuoteCalculator quoteCalculator;
|
||||
private final PrinterMachineRepository machineRepo;
|
||||
|
||||
// Defaults
|
||||
private static final String DEFAULT_FILAMENT = "pla_basic";
|
||||
private static final String DEFAULT_PROCESS = "standard";
|
||||
|
||||
public QuoteSessionController(QuoteSessionRepository sessionRepo,
|
||||
QuoteLineItemRepository lineItemRepo,
|
||||
SlicerService slicerService,
|
||||
QuoteCalculator quoteCalculator,
|
||||
PrinterMachineRepository machineRepo) {
|
||||
this.sessionRepo = sessionRepo;
|
||||
this.lineItemRepo = lineItemRepo;
|
||||
this.slicerService = slicerService;
|
||||
this.quoteCalculator = quoteCalculator;
|
||||
this.machineRepo = machineRepo;
|
||||
}
|
||||
|
||||
// 1. Start a new session with a file
|
||||
@PostMapping(value = "/line-items", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Transactional
|
||||
public ResponseEntity<QuoteSession> createSessionAndAddItem(
|
||||
@RequestParam("file") MultipartFile file
|
||||
) throws IOException {
|
||||
// Create new session
|
||||
QuoteSession session = new QuoteSession();
|
||||
session.setStatus("ACTIVE");
|
||||
session.setPricingVersion("v1"); // Placeholder
|
||||
session.setMaterialCode(DEFAULT_FILAMENT); // Default for session
|
||||
session.setSupportsEnabled(false);
|
||||
session.setCreatedAt(OffsetDateTime.now());
|
||||
session.setExpiresAt(OffsetDateTime.now().plusDays(30));
|
||||
// Set defaults
|
||||
session.setSetupCostChf(BigDecimal.ZERO);
|
||||
|
||||
session = sessionRepo.save(session);
|
||||
|
||||
// Process file and add item
|
||||
addItemToSession(session, file);
|
||||
|
||||
// Refresh session to return updated data (if we added list fetching to repo, otherwise manually fetch items if needed for response)
|
||||
// For now, let's just return the session. The client might need to fetch items separately or we can return a DTO.
|
||||
// User request: "ritorna sessione + line items + total"
|
||||
// Since QuoteSession entity doesn't have a @OneToMany list of items (it has OneToMany usually but mapped by item),
|
||||
// we might need a DTO or just rely on the fact that we might add the list to the entity if valid.
|
||||
// Looking at QuoteSession.java, it does NOT have a list of items.
|
||||
// So we should probably return a DTO or just return the Session and Client calls GET /quote-sessions/{id} immediately?
|
||||
// User request: "ritorna quoteSessionId" (actually implies just ID, but likely wants full object).
|
||||
// "ritorna sessione + line items + total (usa view o calcolo service)" refers to GET /quote-sessions/{id}
|
||||
|
||||
// Let's return the full session details including items in a DTO/Map/wrapper?
|
||||
// Or just the session for now. The user said "ritorna quoteSessionId" for this specific endpoint.
|
||||
// Let's return the Session entity for now.
|
||||
return ResponseEntity.ok(session);
|
||||
}
|
||||
|
||||
// 2. Add item to existing session
|
||||
@PostMapping(value = "/{id}/line-items", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Transactional
|
||||
public ResponseEntity<QuoteLineItem> addItemToExistingSession(
|
||||
@PathVariable UUID id,
|
||||
@RequestParam("file") MultipartFile file
|
||||
) throws IOException {
|
||||
QuoteSession session = sessionRepo.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("Session not found"));
|
||||
|
||||
QuoteLineItem item = addItemToSession(session, file);
|
||||
return ResponseEntity.ok(item);
|
||||
}
|
||||
|
||||
// Helper to add item
|
||||
private QuoteLineItem addItemToSession(QuoteSession session, MultipartFile file) throws IOException {
|
||||
if (file.isEmpty()) throw new IOException("File is empty");
|
||||
|
||||
// 1. Save file temporarily
|
||||
Path tempInput = Files.createTempFile("upload_", "_" + file.getOriginalFilename());
|
||||
file.transferTo(tempInput.toFile());
|
||||
|
||||
try {
|
||||
// 2. Mock Calc or Real Calc
|
||||
// The user said: "per ora calcolo mock" (mock calculation) but we have SlicerService.
|
||||
// "Nota: il calcolo può essere stub: set print_time_seconds/material_grams/unit_price_chf a valori placeholder."
|
||||
// However, since we have the SlicerService, we CAN try to use it if we want, OR just use stub as requested to be fast?
|
||||
// "avvia calcolo (per ora calcolo mock)" -> I will use a simple Stub to satisfy the requirement immediately.
|
||||
// But I will also implement the structure to swap to Real Calc.
|
||||
|
||||
// STUB CALCULATION as requested
|
||||
int printTime = 3600; // 1 hour
|
||||
BigDecimal materialGrams = new BigDecimal("50.00");
|
||||
BigDecimal unitPrice = new BigDecimal("15.00");
|
||||
|
||||
// 3. Create Line Item
|
||||
QuoteLineItem item = new QuoteLineItem();
|
||||
item.setQuoteSession(session);
|
||||
item.setOriginalFilename(file.getOriginalFilename());
|
||||
item.setQuantity(1);
|
||||
item.setColorCode("#FFFFFF"); // Default
|
||||
item.setStatus("CALCULATED");
|
||||
|
||||
item.setPrintTimeSeconds(printTime);
|
||||
item.setMaterialGrams(materialGrams);
|
||||
item.setUnitPriceChf(unitPrice);
|
||||
item.setPricingBreakdown(Map.of("mock", true));
|
||||
|
||||
// Set simple bounding box
|
||||
item.setBoundingBoxXMm(BigDecimal.valueOf(100));
|
||||
item.setBoundingBoxYMm(BigDecimal.valueOf(100));
|
||||
item.setBoundingBoxZMm(BigDecimal.valueOf(20));
|
||||
|
||||
item.setCreatedAt(OffsetDateTime.now());
|
||||
item.setUpdatedAt(OffsetDateTime.now());
|
||||
|
||||
return lineItemRepo.save(item);
|
||||
|
||||
} finally {
|
||||
Files.deleteIfExists(tempInput);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update Line Item
|
||||
@PatchMapping("/line-items/{lineItemId}")
|
||||
@Transactional
|
||||
public ResponseEntity<QuoteLineItem> updateLineItem(
|
||||
@PathVariable UUID lineItemId,
|
||||
@RequestBody Map<String, Object> updates
|
||||
) {
|
||||
QuoteLineItem item = lineItemRepo.findById(lineItemId)
|
||||
.orElseThrow(() -> new RuntimeException("Item not found"));
|
||||
|
||||
if (updates.containsKey("quantity")) {
|
||||
item.setQuantity((Integer) updates.get("quantity"));
|
||||
}
|
||||
if (updates.containsKey("color_code")) {
|
||||
item.setColorCode((String) updates.get("color_code"));
|
||||
}
|
||||
|
||||
// Recalculate price if needed?
|
||||
// For now, unit price is fixed in mock. Total is calculated on GET.
|
||||
|
||||
item.setUpdatedAt(OffsetDateTime.now());
|
||||
return ResponseEntity.ok(lineItemRepo.save(item));
|
||||
}
|
||||
|
||||
// 4. Delete Line Item
|
||||
@DeleteMapping("/{sessionId}/line-items/{lineItemId}")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> deleteLineItem(
|
||||
@PathVariable UUID sessionId,
|
||||
@PathVariable UUID lineItemId
|
||||
) {
|
||||
// Verify item belongs to session?
|
||||
QuoteLineItem item = lineItemRepo.findById(lineItemId)
|
||||
.orElseThrow(() -> new RuntimeException("Item not found"));
|
||||
|
||||
if (!item.getQuoteSession().getId().equals(sessionId)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
lineItemRepo.delete(item);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// 5. Get Session (Session + Items + Total)
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getQuoteSession(@PathVariable UUID id) {
|
||||
QuoteSession session = sessionRepo.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("Session not found"));
|
||||
|
||||
List<QuoteLineItem> items = lineItemRepo.findByQuoteSessionId(id);
|
||||
|
||||
// Calculate Totals
|
||||
BigDecimal itemsTotal = BigDecimal.ZERO;
|
||||
for (QuoteLineItem item : items) {
|
||||
BigDecimal lineTotal = item.getUnitPriceChf().multiply(BigDecimal.valueOf(item.getQuantity()));
|
||||
itemsTotal = itemsTotal.add(lineTotal);
|
||||
}
|
||||
|
||||
BigDecimal setupFee = session.getSetupCostChf() != null ? session.getSetupCostChf() : BigDecimal.ZERO;
|
||||
BigDecimal grandTotal = itemsTotal.add(setupFee);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("session", session);
|
||||
response.put("items", items);
|
||||
response.put("itemsTotalChf", itemsTotal);
|
||||
response.put("grandTotalChf", grandTotal);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@@ -116,4 +116,6 @@ public class CustomQuoteRequestAttachment {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public void setCustomQuoteRequest(CustomQuoteRequest request) {
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.annotations.Immutable;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Immutable
|
||||
@Table(name = "quote_session_totals")
|
||||
public class QuoteSessionTotal {
|
||||
@Column(name = "quote_session_id")
|
||||
private UUID quoteSessionId;
|
||||
|
||||
@Column(name = "total_chf")
|
||||
private BigDecimal totalChf;
|
||||
|
||||
public UUID getQuoteSessionId() {
|
||||
return quoteSessionId;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalChf() {
|
||||
return totalChf;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package com.printcalculator.repository;
|
||||
import com.printcalculator.entity.Customer;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
|
||||
Optional<Customer> findByEmail(String email);
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package com.printcalculator.repository;
|
||||
import com.printcalculator.entity.QuoteLineItem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface QuoteLineItemRepository extends JpaRepository<QuoteLineItem, UUID> {
|
||||
List<QuoteLineItem> findByQuoteSessionId(UUID quoteSessionId);
|
||||
}
|
||||
@@ -28,6 +28,10 @@ export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./features/legal/legal.routes').then(m => m.LEGAL_ROUTES)
|
||||
},
|
||||
{
|
||||
path: 'payment/:orderId',
|
||||
loadComponent: () => import('./features/payment/payment.component').then(m => m.PaymentComponent)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
21
frontend/src/app/features/payment/payment.component.html
Normal file
21
frontend/src/app/features/payment/payment.component.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="payment-container">
|
||||
<mat-card class="payment-card">
|
||||
<mat-card-header>
|
||||
<mat-icon mat-card-avatar>payment</mat-icon>
|
||||
<mat-card-title>Payment Integration</mat-card-title>
|
||||
<mat-card-subtitle>Order #{{ orderId }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="coming-soon">
|
||||
<h3>Coming Soon</h3>
|
||||
<p>The online payment system is currently under development.</p>
|
||||
<p>Your order has been saved. Please contact us to arrange payment.</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="end">
|
||||
<button mat-raised-button color="primary" (click)="completeOrder()">
|
||||
Simulate Payment Completion
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
35
frontend/src/app/features/payment/payment.component.scss
Normal file
35
frontend/src/app/features/payment/payment.component.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
.payment-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 80vh;
|
||||
padding: 2rem;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.payment-card {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.coming-soon {
|
||||
text-align: center;
|
||||
padding: 2rem 0;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #777;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #3f51b5;
|
||||
}
|
||||
34
frontend/src/app/features/payment/payment.component.ts
Normal file
34
frontend/src/app/features/payment/payment.component.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'app-payment',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatButtonModule, MatCardModule, MatIconModule],
|
||||
templateUrl: './payment.component.html',
|
||||
styleUrl: './payment.component.scss'
|
||||
})
|
||||
export class PaymentComponent implements OnInit {
|
||||
orderId: string | null = null;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.orderId = this.route.snapshot.paramMap.get('orderId');
|
||||
}
|
||||
|
||||
completeOrder(): void {
|
||||
// Simulate payment completion
|
||||
alert('Payment Simulated! Order marked as PAID.');
|
||||
// Here you would call the backend to mark as paid if we had that endpoint ready
|
||||
// For now, redirect home or show success
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user