dev #10

Merged
JoeKung merged 2 commits from dev into main 2026-03-03 10:26:24 +01:00
13 changed files with 84 additions and 9 deletions

View File

@@ -2,12 +2,12 @@ package com.printcalculator;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication @SpringBootApplication(exclude = {UserDetailsServiceAutoConfiguration.class})
@EnableTransactionManagement @EnableTransactionManagement
@EnableScheduling @EnableScheduling
@EnableAsync @EnableAsync

View File

@@ -29,6 +29,8 @@ public class SecurityConfig {
.logout(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/actuator/health", "/actuator/health/**").permitAll()
.requestMatchers("/actuator/**").denyAll()
.requestMatchers("/api/admin/auth/login").permitAll() .requestMatchers("/api/admin/auth/login").permitAll()
.requestMatchers("/api/admin/**").authenticated() .requestMatchers("/api/admin/**").authenticated()
.anyRequest().permitAll() .anyRequest().permitAll()

View File

@@ -97,7 +97,7 @@ public class QuoteSessionController {
session.setExpiresAt(OffsetDateTime.now().plusDays(30)); session.setExpiresAt(OffsetDateTime.now().plusDays(30));
var policy = pricingRepo.findFirstByIsActiveTrueOrderByValidFromDesc(); var policy = pricingRepo.findFirstByIsActiveTrueOrderByValidFromDesc();
session.setSetupCostChf(policy != null ? policy.getFixedJobFeeChf() : BigDecimal.ZERO); session.setSetupCostChf(quoteCalculator.calculateSessionSetupFee(policy));
session = sessionRepo.save(session); session = sessionRepo.save(session);
return ResponseEntity.ok(session); return ResponseEntity.ok(session);
@@ -296,9 +296,7 @@ public class QuoteSessionController {
return variant; return variant;
} }
String requestedMaterialCode = settings.getMaterial() != null String requestedMaterialCode = normalizeRequestedMaterialCode(settings.getMaterial());
? settings.getMaterial().trim().toUpperCase()
: "PLA";
FilamentMaterialType materialType = materialRepo.findByMaterialCode(requestedMaterialCode) FilamentMaterialType materialType = materialRepo.findByMaterialCode(requestedMaterialCode)
.orElseGet(() -> materialRepo.findByMaterialCode("PLA") .orElseGet(() -> materialRepo.findByMaterialCode("PLA")
@@ -316,6 +314,18 @@ public class QuoteSessionController {
.orElseThrow(() -> new RuntimeException("No active variant for material: " + requestedMaterialCode)); .orElseThrow(() -> new RuntimeException("No active variant for material: " + requestedMaterialCode));
} }
private String normalizeRequestedMaterialCode(String value) {
if (value == null || value.isBlank()) {
return "PLA";
}
return value.trim()
.toUpperCase(Locale.ROOT)
.replace('_', ' ')
.replace('-', ' ')
.replaceAll("\\s+", " ");
}
// 3. Update Line Item // 3. Update Line Item
@PatchMapping("/line-items/{lineItemId}") @PatchMapping("/line-items/{lineItemId}")
@Transactional @Transactional

View File

@@ -8,7 +8,7 @@ public class PrintSettingsDto {
private String complexityMode; private String complexityMode;
// Common // Common
private String material; // e.g. "PLA", "PETG" private String material; // e.g. "PLA", "PLA TOUGH", "PETG"
private String color; // e.g. "White", "#FFFFFF" private String color; // e.g. "White", "#FFFFFF"
private Long filamentVariantId; private Long filamentVariantId;
private Long printerMachineId; private Long printerMachineId;

View File

@@ -116,6 +116,7 @@ public class OrcaProfileResolver {
: "PLA"; : "PLA";
return switch (materialCode) { return switch (materialCode) {
case "PLA TOUGH" -> "Bambu PLA Tough @BBL A1";
case "PETG" -> "Generic PETG"; case "PETG" -> "Generic PETG";
case "TPU" -> "Generic TPU"; case "TPU" -> "Generic TPU";
case "PC" -> "Generic PC"; case "PC" -> "Generic PC";

View File

@@ -46,6 +46,7 @@ public class ProfileManager {
// Material Aliases // Material Aliases
profileAliases.put("pla_basic", "Bambu PLA Basic @BBL A1"); profileAliases.put("pla_basic", "Bambu PLA Basic @BBL A1");
profileAliases.put("pla_tough", "Bambu PLA Tough @BBL A1");
profileAliases.put("petg_basic", "Bambu PETG Basic @BBL A1"); profileAliases.put("petg_basic", "Bambu PETG Basic @BBL A1");
profileAliases.put("tpu_95a", "Bambu TPU 95A @BBL A1"); profileAliases.put("tpu_95a", "Bambu TPU 95A @BBL A1");

View File

@@ -21,6 +21,8 @@ import java.util.List;
@Service @Service
public class QuoteCalculator { public class QuoteCalculator {
private static final BigDecimal SETUP_FEE_DOUBLE_THRESHOLD_CHF = BigDecimal.TEN;
private static final BigDecimal SETUP_FEE_MULTIPLIER_BELOW_THRESHOLD = BigDecimal.valueOf(2);
private final PricingPolicyRepository pricingRepo; private final PricingPolicyRepository pricingRepo;
private final PricingPolicyMachineHourTierRepository tierRepo; private final PricingPolicyMachineHourTierRepository tierRepo;
@@ -111,6 +113,21 @@ public class QuoteCalculator {
return rawCost.multiply(markupFactor).setScale(2, RoundingMode.HALF_UP); return rawCost.multiply(markupFactor).setScale(2, RoundingMode.HALF_UP);
} }
public BigDecimal calculateSessionSetupFee(PricingPolicy policy) {
if (policy == null || policy.getFixedJobFeeChf() == null) {
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
BigDecimal baseSetupFee = policy.getFixedJobFeeChf();
if (baseSetupFee.compareTo(SETUP_FEE_DOUBLE_THRESHOLD_CHF) < 0) {
return baseSetupFee
.multiply(SETUP_FEE_MULTIPLIER_BELOW_THRESHOLD)
.setScale(2, RoundingMode.HALF_UP);
}
return baseSetupFee.setScale(2, RoundingMode.HALF_UP);
}
private BigDecimal calculateMachineCost(PricingPolicy policy, BigDecimal hours) { private BigDecimal calculateMachineCost(PricingPolicy policy, BigDecimal hours) {
List<PricingPolicyMachineHourTier> tiers = tierRepo.findAllByPricingPolicyOrderByTierStartHoursAsc(policy); List<PricingPolicyMachineHourTier> tiers = tierRepo.findAllByPricingPolicyOrderByTierStartHoursAsc(policy);
if (tiers.isEmpty()) { if (tiers.isEmpty()) {
@@ -158,6 +175,7 @@ public class QuoteCalculator {
private String detectMaterialCode(String profileName) { private String detectMaterialCode(String profileName) {
String lower = profileName.toLowerCase(); String lower = profileName.toLowerCase();
if (lower.contains("pla tough") || lower.contains("pla_tough")) return "PLA TOUGH";
if (lower.contains("petg")) return "PETG"; if (lower.contains("petg")) return "PETG";
if (lower.contains("tpu")) return "TPU"; if (lower.contains("tpu")) return "TPU";
if (lower.contains("abs")) return "ABS"; if (lower.contains("abs")) return "ABS";

View File

@@ -7,6 +7,7 @@ spring.datasource.username=${DB_USERNAME:printcalc}
spring.datasource.password=${DB_PASSWORD:printcalc_secret} spring.datasource.password=${DB_PASSWORD:printcalc_secret}
spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.open-in-view=false
# Slicer Configuration # Slicer Configuration
@@ -47,3 +48,6 @@ admin.password=${ADMIN_PASSWORD}
admin.session.secret=${ADMIN_SESSION_SECRET} admin.session.secret=${ADMIN_SESSION_SECRET}
admin.session.ttl-minutes=${ADMIN_SESSION_TTL_MINUTES:480} admin.session.ttl-minutes=${ADMIN_SESSION_TTL_MINUTES:480}
admin.auth.trust-proxy-headers=${ADMIN_AUTH_TRUST_PROXY_HEADERS:false} admin.auth.trust-proxy-headers=${ADMIN_AUTH_TRUST_PROXY_HEADERS:false}
# Expose only liveness endpoint by default.
management.endpoints.web.exposure.include=health

35
db.sql
View File

@@ -285,6 +285,7 @@ insert into filament_material_type (material_code,
is_technical, is_technical,
technical_type_label) technical_type_label)
values ('PLA', false, false, null), values ('PLA', false, false, null),
('PLA TOUGH', false, false, null),
('PETG', false, false, null), ('PETG', false, false, null),
('TPU', true, false, null), ('TPU', true, false, null),
('PC', false, true, 'engineering'), ('PC', false, true, 'engineering'),
@@ -355,6 +356,37 @@ on conflict (filament_material_type_id, variant_display_name) do update
is_active = excluded.is_active; is_active = excluded.is_active;
-- Varianti base per materiali principali del calcolatore -- Varianti base per materiali principali del calcolatore
with mat as (select filament_material_type_id
from filament_material_type
where material_code = 'PLA TOUGH')
insert
into filament_variant (filament_material_type_id, variant_display_name, color_name, color_hex, finish_type, brand,
is_matte, is_special, cost_chf_per_kg, stock_spools, spool_net_kg, is_active)
select mat.filament_material_type_id,
'PLA Tough Nero',
'Nero',
'#1A1A1A',
'GLOSSY',
'Bambu',
false,
false,
18.00,
1.000,
1.000,
true
from mat
on conflict (filament_material_type_id, variant_display_name) do update
set color_name = excluded.color_name,
color_hex = excluded.color_hex,
finish_type = excluded.finish_type,
brand = excluded.brand,
is_matte = excluded.is_matte,
is_special = excluded.is_special,
cost_chf_per_kg = excluded.cost_chf_per_kg,
stock_spools = excluded.stock_spools,
spool_net_kg = excluded.spool_net_kg,
is_active = excluded.is_active;
with mat as (select filament_material_type_id with mat as (select filament_material_type_id
from filament_material_type from filament_material_type
where material_code = 'PETG') where material_code = 'PETG')
@@ -491,13 +523,14 @@ with p as (select printer_machine_profile_id
and pmp.nozzle_diameter_mm = 0.40::numeric), and pmp.nozzle_diameter_mm = 0.40::numeric),
m as (select filament_material_type_id, material_code m as (select filament_material_type_id, material_code
from filament_material_type from filament_material_type
where material_code in ('PLA', 'PETG', 'TPU', 'PC')) where material_code in ('PLA', 'PLA TOUGH', 'PETG', 'TPU', 'PC'))
insert insert
into material_orca_profile_map (printer_machine_profile_id, filament_material_type_id, orca_filament_profile_name, is_active) into material_orca_profile_map (printer_machine_profile_id, filament_material_type_id, orca_filament_profile_name, is_active)
select p.printer_machine_profile_id, select p.printer_machine_profile_id,
m.filament_material_type_id, m.filament_material_type_id,
case m.material_code case m.material_code
when 'PLA' then 'Bambu PLA Basic @BBL A1' when 'PLA' then 'Bambu PLA Basic @BBL A1'
when 'PLA TOUGH' then 'Bambu PLA Tough @BBL A1'
when 'PETG' then 'Bambu PETG Basic @BBL A1' when 'PETG' then 'Bambu PETG Basic @BBL A1'
when 'TPU' then 'Bambu TPU 95A @BBL A1' when 'TPU' then 'Bambu TPU 95A @BBL A1'
when 'PC' then 'Generic PC @BBL A1' when 'PC' then 'Generic PC @BBL A1'

View File

@@ -316,7 +316,7 @@
<div class="dialog-backdrop" *ngIf="variantToDelete" (click)="closeDeleteVariantDialog()"></div> <div class="dialog-backdrop" *ngIf="variantToDelete" (click)="closeDeleteVariantDialog()"></div>
<div class="confirm-dialog" *ngIf="variantToDelete"> <div class="confirm-dialog" *ngIf="variantToDelete">
<h4>Sei sicuro?</h4> <h4>Sei sicuro?</h4>
<p>Vuoi eliminare la variante <strong>{{ variantToDelete?.variantDisplayName }}</strong>?</p> <p>Vuoi eliminare la variante <strong>{{ variantToDelete.variantDisplayName }}</strong>?</p>
<p class="muted">L'operazione non è reversibile.</p> <p class="muted">L'operazione non è reversibile.</p>
<div class="dialog-actions"> <div class="dialog-actions">
<button type="button" class="btn-secondary" (click)="closeDeleteVariantDialog()">Annulla</button> <button type="button" class="btn-secondary" (click)="closeDeleteVariantDialog()">Annulla</button>

View File

@@ -133,6 +133,8 @@
"TITLE": "Über uns", "TITLE": "Über uns",
"EYEBROW": "3D-Druck-Labor", "EYEBROW": "3D-Druck-Labor",
"SUBTITLE": "Wir sind zwei Studenten mit viel Motivation und Lernbereitschaft.", "SUBTITLE": "Wir sind zwei Studenten mit viel Motivation und Lernbereitschaft.",
"HOW_TEXT": "3D Fab ist entstanden, um das Potenzial des 3D-Drucks in alltagstaugliche Lösungen zu verwandeln. Wir haben mit technischer Neugier begonnen und sind zur Herstellung von Ersatzteilen, Produkten und maßgeschneiderten Prototypen gekommen. Um von einer Idee zu einem konkreten Projekt zu gelangen, haben wir unseren automatischen Rechner eingeführt: klare Angebote in einem Klick, damit Sie einen professionellen Service ohne Preisüberraschungen erhalten.",
"PASSIONS_TITLE": "Unsere Interessen",
"PASSION_BIKE_TRIAL": "Bike Trial", "PASSION_BIKE_TRIAL": "Bike Trial",
"PASSION_MOUNTAIN": "Berge", "PASSION_MOUNTAIN": "Berge",
"PASSION_SKI": "Ski", "PASSION_SKI": "Ski",

View File

@@ -133,6 +133,8 @@
"TITLE": "About Us", "TITLE": "About Us",
"EYEBROW": "3D Printing Lab", "EYEBROW": "3D Printing Lab",
"SUBTITLE": "We are two students with a strong desire to build and learn.", "SUBTITLE": "We are two students with a strong desire to build and learn.",
"HOW_TEXT": "3D Fab was created to turn the potential of 3D printing into everyday solutions. We started from technical curiosity and grew into producing spare parts, products, and custom prototypes. To move from an idea to a concrete project, we launched our automatic calculator: clear quotes in one click, so you get a professional service with no price surprises.",
"PASSIONS_TITLE": "Our interests",
"PASSION_BIKE_TRIAL": "Bike trial", "PASSION_BIKE_TRIAL": "Bike trial",
"PASSION_MOUNTAIN": "Mountain", "PASSION_MOUNTAIN": "Mountain",
"PASSION_SKI": "Ski", "PASSION_SKI": "Ski",

View File

@@ -190,6 +190,8 @@
"TITLE": "Qui sommes-nous", "TITLE": "Qui sommes-nous",
"EYEBROW": "Atelier d'impression 3D", "EYEBROW": "Atelier d'impression 3D",
"SUBTITLE": "Nous sommes deux étudiants avec beaucoup d'envie de faire et d'apprendre.", "SUBTITLE": "Nous sommes deux étudiants avec beaucoup d'envie de faire et d'apprendre.",
"HOW_TEXT": "3D Fab est né pour transformer le potentiel de l'impression 3D en solutions du quotidien. Nous sommes partis de la curiosité technique et nous sommes arrivés à la production de pièces de rechange, de produits et de prototypes sur mesure. Pour passer d'une idée à un projet concret, nous avons lancé notre calculateur automatique : des devis clairs en un clic pour vous garantir un service professionnel, sans surprises sur le prix.",
"PASSIONS_TITLE": "Nos centres d'intérêt",
"PASSION_BIKE_TRIAL": "Bike trial", "PASSION_BIKE_TRIAL": "Bike trial",
"PASSION_MOUNTAIN": "Montagne", "PASSION_MOUNTAIN": "Montagne",
"PASSION_SKI": "Ski", "PASSION_SKI": "Ski",