diff --git a/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java b/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java new file mode 100644 index 0000000..56cc1a7 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java @@ -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 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 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 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"; + } +} diff --git a/backend/src/main/java/com/printcalculator/controller/OrderController.java b/backend/src/main/java/com/printcalculator/controller/OrderController.java new file mode 100644 index 0000000..763ccb7 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/controller/OrderController.java @@ -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 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 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 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"; + } + +} diff --git a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java new file mode 100644 index 0000000..618338c --- /dev/null +++ b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java @@ -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 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 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 updateLineItem( + @PathVariable UUID lineItemId, + @RequestBody Map 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 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> getQuoteSession(@PathVariable UUID id) { + QuoteSession session = sessionRepo.findById(id) + .orElseThrow(() -> new RuntimeException("Session not found")); + + List 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 response = new HashMap<>(); + response.put("session", session); + response.put("items", items); + response.put("itemsTotalChf", itemsTotal); + response.put("grandTotalChf", grandTotal); + + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/com/printcalculator/entity/CustomQuoteRequestAttachment.java b/backend/src/main/java/com/printcalculator/entity/CustomQuoteRequestAttachment.java index 1c60d24..01f3406 100644 --- a/backend/src/main/java/com/printcalculator/entity/CustomQuoteRequestAttachment.java +++ b/backend/src/main/java/com/printcalculator/entity/CustomQuoteRequestAttachment.java @@ -116,4 +116,6 @@ public class CustomQuoteRequestAttachment { this.createdAt = createdAt; } + public void setCustomQuoteRequest(CustomQuoteRequest request) { + } } \ No newline at end of file diff --git a/backend/src/main/java/com/printcalculator/entity/QuoteSessionTotal.java b/backend/src/main/java/com/printcalculator/entity/QuoteSessionTotal.java deleted file mode 100644 index 9a2fce1..0000000 --- a/backend/src/main/java/com/printcalculator/entity/QuoteSessionTotal.java +++ /dev/null @@ -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; - } - -} \ No newline at end of file diff --git a/backend/src/main/java/com/printcalculator/repository/CustomerRepository.java b/backend/src/main/java/com/printcalculator/repository/CustomerRepository.java index f874743..4aa6cad 100644 --- a/backend/src/main/java/com/printcalculator/repository/CustomerRepository.java +++ b/backend/src/main/java/com/printcalculator/repository/CustomerRepository.java @@ -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 { + Optional findByEmail(String email); } \ No newline at end of file diff --git a/backend/src/main/java/com/printcalculator/repository/QuoteLineItemRepository.java b/backend/src/main/java/com/printcalculator/repository/QuoteLineItemRepository.java index 2f4d591..809f33b 100644 --- a/backend/src/main/java/com/printcalculator/repository/QuoteLineItemRepository.java +++ b/backend/src/main/java/com/printcalculator/repository/QuoteLineItemRepository.java @@ -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 { + List findByQuoteSessionId(UUID quoteSessionId); } \ No newline at end of file diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 183f658..46d2cf3 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -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) } ] } diff --git a/frontend/src/app/features/payment/payment.component.html b/frontend/src/app/features/payment/payment.component.html new file mode 100644 index 0000000..733e839 --- /dev/null +++ b/frontend/src/app/features/payment/payment.component.html @@ -0,0 +1,21 @@ +
+ + + payment + Payment Integration + Order #{{ orderId }} + + +
+

Coming Soon

+

The online payment system is currently under development.

+

Your order has been saved. Please contact us to arrange payment.

+
+
+ + + +
+
diff --git a/frontend/src/app/features/payment/payment.component.scss b/frontend/src/app/features/payment/payment.component.scss new file mode 100644 index 0000000..d4475db --- /dev/null +++ b/frontend/src/app/features/payment/payment.component.scss @@ -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; +} diff --git a/frontend/src/app/features/payment/payment.component.ts b/frontend/src/app/features/payment/payment.component.ts new file mode 100644 index 0000000..671a36e --- /dev/null +++ b/frontend/src/app/features/payment/payment.component.ts @@ -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(['/']); + } +}