feat(back-end): refactor session creation
All checks were successful
Build, Test and Deploy / test-backend (push) Successful in 1m13s
Build, Test and Deploy / build-and-push (push) Successful in 56s
Build, Test and Deploy / deploy (push) Successful in 8s

This commit is contained in:
2026-02-12 17:07:26 +01:00
parent 5a84fb13c0
commit 257c60fa5e
9 changed files with 367 additions and 276 deletions

View File

@@ -29,6 +29,8 @@ dependencies {
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {

View File

@@ -17,9 +17,10 @@ public class CorsConfig implements WebMvcConfigurer {
"http://localhost:80",
"http://127.0.0.1",
"https://dev.3d-fab.ch",
"https://int.3d-fab.ch",
"https://3d-fab.ch"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
.allowedHeaders("*")
.allowCredentials(true);
}

View File

@@ -40,29 +40,20 @@ public class CustomQuoteRequestController {
@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
@RequestPart("request") com.printcalculator.dto.QuoteRequestDto requestDto,
@RequestPart(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.setRequestType(requestDto.getRequestType());
request.setCustomerType(requestDto.getCustomerType());
request.setEmail(requestDto.getEmail());
request.setPhone(requestDto.getPhone());
request.setName(requestDto.getName());
request.setCompanyName(requestDto.getCompanyName());
request.setContactPerson(requestDto.getContactPerson());
request.setMessage(requestDto.getMessage());
request.setStatus("PENDING");
request.setCreatedAt(OffsetDateTime.now());
request.setUpdatedAt(OffsetDateTime.now());

View File

@@ -28,7 +28,7 @@ import java.util.UUID;
@RestController
@RequestMapping("/api/quote-sessions")
@CrossOrigin(origins = "*") // Allow CORS for dev
public class QuoteSessionController {
private final QuoteSessionRepository sessionRepo;
@@ -36,6 +36,7 @@ public class QuoteSessionController {
private final SlicerService slicerService;
private final QuoteCalculator quoteCalculator;
private final PrinterMachineRepository machineRepo;
private final com.printcalculator.repository.PricingPolicyRepository pricingRepo;
// Defaults
private static final String DEFAULT_FILAMENT = "pla_basic";
@@ -45,49 +46,34 @@ public class QuoteSessionController {
QuoteLineItemRepository lineItemRepo,
SlicerService slicerService,
QuoteCalculator quoteCalculator,
PrinterMachineRepository machineRepo) {
PrinterMachineRepository machineRepo,
com.printcalculator.repository.PricingPolicyRepository pricingRepo) {
this.sessionRepo = sessionRepo;
this.lineItemRepo = lineItemRepo;
this.slicerService = slicerService;
this.quoteCalculator = quoteCalculator;
this.machineRepo = machineRepo;
this.pricingRepo = pricingRepo;
}
// 1. Start a new session with a file
@PostMapping(value = "/line-items", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
// 1. Start a new empty session
@PostMapping(value = "")
@Transactional
public ResponseEntity<QuoteSession> createSessionAndAddItem(
@RequestParam("file") MultipartFile file
) throws IOException {
// Create new session
public ResponseEntity<QuoteSession> createSession() {
QuoteSession session = new QuoteSession();
session.setStatus("ACTIVE");
session.setPricingVersion("v1"); // Placeholder
session.setMaterialCode(DEFAULT_FILAMENT); // Default for session
session.setPricingVersion("v1");
// Default material/settings will be set when items are added or updated?
// For now set safe defaults
session.setMaterialCode("pla_basic");
session.setSupportsEnabled(false);
session.setCreatedAt(OffsetDateTime.now());
session.setExpiresAt(OffsetDateTime.now().plusDays(30));
// Set defaults
session.setSetupCostChf(BigDecimal.ZERO);
var policy = pricingRepo.findFirstByIsActiveTrueOrderByValidFromDesc();
session.setSetupCostChf(policy != null ? policy.getFixedJobFeeChf() : 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);
}
@@ -96,17 +82,18 @@ public class QuoteSessionController {
@Transactional
public ResponseEntity<QuoteLineItem> addItemToExistingSession(
@PathVariable UUID id,
@RequestParam("file") MultipartFile file
@RequestPart("settings") com.printcalculator.dto.PrintSettingsDto settings,
@RequestPart("file") MultipartFile file
) throws IOException {
QuoteSession session = sessionRepo.findById(id)
.orElseThrow(() -> new RuntimeException("Session not found"));
QuoteLineItem item = addItemToSession(session, file);
QuoteLineItem item = addItemToSession(session, file, settings);
return ResponseEntity.ok(item);
}
// Helper to add item
private QuoteLineItem addItemToSession(QuoteSession session, MultipartFile file) throws IOException {
private QuoteLineItem addItemToSession(QuoteSession session, MultipartFile file, com.printcalculator.dto.PrintSettingsDto settings) throws IOException {
if (file.isEmpty()) throw new IOException("File is empty");
// 1. Save file temporarily
@@ -114,35 +101,87 @@ public class QuoteSessionController {
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.
// Apply Basic/Advanced Logic
applyPrintSettings(settings);
// REAL SLICING
// 1. Pick Machine (default to first active or specific)
PrinterMachine machine = machineRepo.findFirstByIsActiveTrue()
.orElseThrow(() -> new RuntimeException("No active printer found"));
// STUB CALCULATION as requested
int printTime = 3600; // 1 hour
BigDecimal materialGrams = new BigDecimal("50.00");
BigDecimal unitPrice = new BigDecimal("15.00");
// 2. Pick Profiles
String machineProfile = machine.getPrinterDisplayName(); // e.g. "Bambu Lab A1 0.4 nozzle"
// If the display name doesn't match the json profile name, we might need a mapping key in DB.
// For now assuming display name works or we use a tough default
machineProfile = "Bambu Lab A1 0.4 nozzle"; // Force known good for now? Or use DB field if exists.
// Ideally: machine.getSlicerProfileName();
// 3. Create Line Item
String filamentProfile = "Generic " + (settings.getMaterial() != null ? settings.getMaterial().toUpperCase() : "PLA");
// Mapping: "pla_basic" -> "Generic PLA", "petg_basic" -> "Generic PETG"
if (settings.getMaterial() != null) {
if (settings.getMaterial().toLowerCase().contains("pla")) filamentProfile = "Generic PLA";
else if (settings.getMaterial().toLowerCase().contains("petg")) filamentProfile = "Generic PETG";
else if (settings.getMaterial().toLowerCase().contains("tpu")) filamentProfile = "Generic TPU";
else if (settings.getMaterial().toLowerCase().contains("abs")) filamentProfile = "Generic ABS";
}
String processProfile = "0.20mm Standard @BBL A1";
// Mapping quality to process
// "standard" -> "0.20mm Standard @BBL A1"
// "draft" -> "0.28mm Extra Draft @BBL A1"
// "high" -> "0.12mm Fine @BBL A1" (approx names, need to be exact for Orca)
// Let's use robust defaults or simple overrides
if (settings.getLayerHeight() != null) {
if (settings.getLayerHeight() >= 0.28) processProfile = "0.28mm Extra Draft @BBL A1";
else if (settings.getLayerHeight() <= 0.12) processProfile = "0.12mm Fine @BBL A1";
}
// Build overrides map from settings
Map<String, String> processOverrides = new HashMap<>();
if (settings.getLayerHeight() != null) processOverrides.put("layer_height", String.valueOf(settings.getLayerHeight()));
if (settings.getInfillDensity() != null) processOverrides.put("sparse_infill_density", settings.getInfillDensity() + "%");
if (settings.getInfillPattern() != null) processOverrides.put("sparse_infill_pattern", settings.getInfillPattern());
// 3. Slice
PrintStats stats = slicerService.slice(
tempInput.toFile(),
machineProfile,
filamentProfile,
processProfile,
null, // machine overrides
processOverrides
);
// 4. Calculate Quote
QuoteResult result = quoteCalculator.calculate(stats, machine.getPrinterDisplayName(), filamentProfile);
// 5. 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.setColorCode(settings.getColor() != null ? settings.getColor() : "#FFFFFF");
item.setStatus("READY"); // or CALCULATED
item.setPrintTimeSeconds(printTime);
item.setMaterialGrams(materialGrams);
item.setUnitPriceChf(unitPrice);
item.setPricingBreakdown(Map.of("mock", true));
item.setPrintTimeSeconds((int) stats.printTimeSeconds());
item.setMaterialGrams(BigDecimal.valueOf(stats.filamentWeightGrams()));
item.setUnitPriceChf(BigDecimal.valueOf(result.getTotalPrice()));
// Set simple bounding box
item.setBoundingBoxXMm(BigDecimal.valueOf(100));
item.setBoundingBoxYMm(BigDecimal.valueOf(100));
item.setBoundingBoxZMm(BigDecimal.valueOf(20));
// Store breakdown
Map<String, Object> breakdown = new HashMap<>();
breakdown.put("machine_cost", result.getTotalPrice() - result.getSetupCost()); // Approximation?
// Better: QuoteResult could expose detailed breakdown. For now just storing what we have.
breakdown.put("setup_fee", result.getSetupCost());
item.setPricingBreakdown(breakdown);
// Dimensions
// Cannot get bb from GCodeParser yet?
// If GCodeParser doesn't return size, we might defaults or 0.
// Stats has filament used.
// Let's set dummy for now or upgrade parser later.
item.setBoundingBoxXMm(BigDecimal.ZERO);
item.setBoundingBoxYMm(BigDecimal.ZERO);
item.setBoundingBoxZMm(BigDecimal.ZERO);
item.setCreatedAt(OffsetDateTime.now());
item.setUpdatedAt(OffsetDateTime.now());
@@ -154,6 +193,37 @@ public class QuoteSessionController {
}
}
private void applyPrintSettings(com.printcalculator.dto.PrintSettingsDto settings) {
if ("BASIC".equalsIgnoreCase(settings.getComplexityMode())) {
// Set defaults based on Quality
String quality = settings.getQuality() != null ? settings.getQuality().toLowerCase() : "standard";
switch (quality) {
case "draft":
settings.setLayerHeight(0.28);
settings.setInfillDensity(15.0);
settings.setInfillPattern("grid");
break;
case "high":
settings.setLayerHeight(0.12);
settings.setInfillDensity(20.0);
settings.setInfillPattern("gyroid");
break;
case "standard":
default:
settings.setLayerHeight(0.20);
settings.setInfillDensity(20.0);
settings.setInfillPattern("grid");
break;
}
} else {
// ADVANCED Mode: Use values from Frontend, set defaults if missing
if (settings.getLayerHeight() == null) settings.setLayerHeight(0.20);
if (settings.getInfillDensity() == null) settings.setInfillDensity(20.0);
if (settings.getInfillPattern() == null) settings.setInfillPattern("grid");
}
}
// 3. Update Line Item
@PatchMapping("/line-items/{lineItemId}")
@Transactional

View File

@@ -0,0 +1,23 @@
package com.printcalculator.dto;
import lombok.Data;
@Data
public class PrintSettingsDto {
// Mode: "BASIC" or "ADVANCED"
private String complexityMode;
// Common
private String material; // e.g. "PLA", "PETG"
private String color; // e.g. "White", "#FFFFFF"
// Basic Mode
private String quality; // "draft", "standard", "high"
// Advanced Mode (Optional in Basic)
private Double layerHeight;
private Double infillDensity;
private String infillPattern;
private Boolean supportsEnabled;
private String notes;
}

View File

@@ -0,0 +1,15 @@
package com.printcalculator.dto;
import lombok.Data;
@Data
public class QuoteRequestDto {
private String requestType; // "PRINT_SERVICE" or "DESIGN_SERVICE"
private String customerType; // "PRIVATE" or "BUSINESS"
private String email;
private String phone;
private String name;
private String companyName;
private String contactPerson;
private String message;
}