produzione 1 #9

Merged
JoeKung merged 135 commits from dev into main 2026-03-03 09:58:04 +01:00
24 changed files with 2142 additions and 219 deletions
Showing only changes of commit 9cbd856ab6 - Show all commits

View File

@@ -4,39 +4,42 @@ Questo file serve a dare contesto all'AI (Antigravity/Gemini) sulla struttura e
## Project Overview ## Project Overview
**Nome**: Print Calculator **Nome**: Print Calculator
**Scopo**: Calcolare costi e tempi di stampa 3D da file STL. **Scopo**: Calcolare costi e tempi di stampa 3D da file STL in modo preciso tramite slicing reale.
**Stack**: **Stack**:
- **Backend**: Python (FastAPI), libreria `trimesh` per analisi geometrica. - **Backend**: Java 21 (Spring Boot 3.4), PostgreSQL, Flyway.
- **Frontend**: Angular 19 (TypeScript). - **Frontend**: Angular 19 (TypeScript), Angular Material, Three.js per visualizzazione 3D.
## Architecture ## Architecture
### Backend (`/backend`) ### Backend (`/backend`)
- **`main.py`**: Entrypoint dell'applicazione FastAPI. - **`BackendApplication.java`**: Entrypoint dell'applicazione Spring Boot.
- Definisce l'API `POST /calculate/stl`. - **`controller/`**: Espone le API REST per l'upload e il calcolo dei preventivi.
- Gestisce l'upload del file, invoca lo slicer e restituisce il preventivo. - **`service/SlicerService.java`**: Wrappa l'eseguibile di **OrcaSlicer** per effettuare lo slicing reale del modello.
- Configura CORS per permettere chiamate dal frontend. - Gestisce i profili di stampa (Macchina, Processo, Filamento) caricati da file JSON.
- **`slicer.py`**: Wrappa l'eseguibile di **OrcaSlicer** per effettuare lo slicing reale del modello. - Crea configurazioni on-the-fly e invoca OrcaSlicer in modalità headless.
- Gestisce i profili di stampa (Macchina, Processo, Filamento). - **`service/GCodeParser.java`**: Analizza il G-Code generato per estrarre tempo di stampa e peso del materiale dai metadati del file.
- Crea configurazioni on-the-fly per supportare mesh di grandi dimensioni. - **`service/QuoteCalculator.java`**: Calcola il prezzo finale basandosi su politiche di prezzo salvate nel database.
- **`calculator.py`**: Analizza il G-Code generato. - Gestisce costi macchina a scaglioni (tiered pricing).
- `GCodeParser`: Estrae tempo di stampa e materiale usato dai metadati del G-Code. - Calcola costi energetici basati sulla potenza della stampante e costo del kWh.
- `QuoteCalculator`: Applica i costi (orari, energia, materiale) per generare il prezzo finale. - Applica markup percentuali e fee fissi per job.
### Frontend (`/frontend`) ### Frontend (`/frontend`)
- Applicazione Angular standard. - Applicazione Angular 19 con architettura modulare (core, features, shared).
- Usa Angular Material. - **Three.js**: Utilizzato per il rendering dei file STL caricati dall'utente.
- Service per upload STL e visualizzazione preventivo. - **Angular Material**: Per l'interfaccia utente.
- **ngx-translate**: Per il supporto multilingua.
## Key Concepts ## Key Concepts
- **Real Slicing**: Il backend esegue un vero slicing usando OrcaSlicer in modalità headless. Questo garantisce stime di tempo e materiale estremamente precise, identiche a quelle che si otterrebbero preparando il file per la stampa. - **Real Slicing**: Il backend esegue un vero slicing usando OrcaSlicer. Questo garantisce stime di tempo e materiale estremamente precise.
- **G-Code Parsing**: Invece di stimare geometricamente, l'applicazione legge direttamene i commenti generati dallo slicer nel G-Code (es. `estimated printing time`, `filament used`). - **Database-Driven Pricing**: A differenza di versioni precedenti, il calcolo del preventivo è ora guidato da entità DB (`PricingPolicy`, `PrinterMachine`, `FilamentVariant`).
- **G-Code Metadata**: L'applicazione legge direttamene i commenti generati dallo slicer nel G-Code (es. `; estimated printing time`, `; filament used [g]`).
## Development Notes ## Development Notes
- Per eseguire il backend serve `uvicorn`. - **Backend**: Richiede JDK 21. Si avvia con `./gradlew bootRun`.
- Il frontend richiede `npm install` al primo avvio. - **Database**: Richiede PostgreSQL. Le migrazioni sono gestite da Flyway.
- Le configurazioni di stampa (layer height, wall thickness, infill) sono attualmente hardcoded o con valori di default nel backend, ma potrebbero essere esposte come parametri API in futuro. - **Frontend**: Richiede Node.js 22. Si avvia con `npm start`.
- **OrcaSlicer**: Deve essere installato sul sistema e il percorso configurato in `application.properties` o tramite variabile d'ambiente `SLICER_PATH`.
## AI Agent Rules ## AI Agent Rules
- **No Inline Code**: Tutti i componenti Angular DEVONO usare file separati per HTML (`templateUrl`) e SCSS (`styleUrl`). È vietato usare `template` o `styles` inline nel decoratore `@Component`. - **No Inline Code**: Tutti i componenti Angular DEVONO usare file separati per HTML (`templateUrl`) e SCSS (`styleUrl`). È vietato usare `template` o `styles` inline nel decoratore `@Component`.
- **Spring Boot Conventions**: Seguire i pattern standard di Spring Boot (Service-Repository-Controller).

View File

@@ -1,70 +1,67 @@
# Print Calculator (OrcaSlicer Edition) # Print Calculator (OrcaSlicer Edition)
Un'applicazione Full Stack (Angular + Python/FastAPI) per calcolare preventivi di stampa 3D precisi utilizzando **OrcaSlicer** in modalità headless. Un'applicazione Full Stack (Angular + Spring Boot) per calcolare preventivi di stampa 3D precisi utilizzando **OrcaSlicer** in modalità headless.
## Funzionalità ## Funzionalità
* **Slicing Reale**: Usa il motore di OrcaSlicer per stimare tempo e materiale, non semplici approssimazioni geometriche. * **Slicing Reale**: Usa il motore di OrcaSlicer per stimare tempo e materiale, garantendo la massima precisione.
* **Preventivazione Completa**: Calcola costo materiale, ammortamento macchina, energia e ricarico. * **Preventivazione Database-Driven**: Calcolo basato su politiche di prezzo configurabili nel database (costo materiale, ammortamento macchina a scaglioni, energia e markup).
* **Configurabile**: Prezzi e parametri macchina modificabili via variabili d'ambiente. * **Visualizzazione 3D**: Anteprima del file STL caricato tramite Three.js.
* **Docker Ready**: Tutto containerizzato per un facile deployment. * **Multi-Profilo**: Supporto per diverse stampanti, materiali e profili di processo.
## Stack Tecnologico
- **Backend**: Java 21, Spring Boot 3.4, PostgreSQL, Flyway.
- **Frontend**: Angular 19, Angular Material, Three.js.
- **Slicer**: OrcaSlicer (invocato via CLI).
## Prerequisiti ## Prerequisiti
* Docker Desktop & Docker Compose installati. * **Java 21** installato.
* **Node.js 22** e **npm** installati.
* **PostgreSQL** attivo.
* **OrcaSlicer** installato sul sistema.
## Avvio Rapido ## Avvio Rapido
1. Clona il repository. ### 1. Database
2. Esegui lo script di avvio o docker-compose: Crea un database PostgreSQL chiamato `printcalc`. Le tabelle verranno create automaticamente al primo avvio tramite Flyway.
```bash
docker-compose up --build
```
*Nota: La prima build impiegherà alcuni minuti per scaricare OrcaSlicer (~200MB) e compilare il Frontend.*
3. Accedi all'applicazione: ### 2. Backend
* **Frontend**: [http://localhost](http://localhost) Configura il percorso di OrcaSlicer in `backend/src/main/resources/application.properties` o tramite la variabile d'ambiente `SLICER_PATH`.
* **API Docs**: [http://localhost:8000/docs](http://localhost:8000/docs)
## Configurazione Prezzi
Puoi modificare i prezzi nel file `docker-compose.yml` (sezione `environment` del servizio backend):
* `FILAMENT_COST_PER_KG`: Costo filamento al kg (es. 25.0).
* `MACHINE_COST_PER_HOUR`: Costo orario macchina (ammortamento/manutenzione).
* `ENERGY_COST_PER_KWH`: Costo energia elettrica.
* `MARKUP_PERCENT`: Margine di profitto percentuale (es. 20 = +20%).
## Struttura del Progetto
* `/backend`: API Python FastAPI. Include Dockerfile che scarica OrcaSlicer AppImage.
* `/frontend`: Applicazione Angular 19+ con Material Design.
* `/backend/profiles`: Contiene i profili di slicing (.ini). Attualmente configurato per una stima generica simil-Bambu Lab A1.
## Troubleshooting
### Errore Download OrcaSlicer
Se la build del backend fallisce durante il download di `OrcaSlicer.AppImage`, verifica la tua connessione internet o aggiorna l'URL nel `backend/Dockerfile`.
### Slicing Fallito (Costo 0 o Errore)
Se l'API ritorna errore o valori nulli:
1. Controlla che il file STL sia valido (manifold).
2. Controlla i log del backend: `docker logs print-calculator-backend`.
## Sviluppo Locale (Senza Docker)
**Backend**:
Richiede Linux (o WSL2) per eseguire l'AppImage di OrcaSlicer.
```bash ```bash
cd backend cd backend
pip install -r requirements.txt ./gradlew bootRun
# Assicurati di avere OrcaSlicer installato e nel PATH o aggiorna SLICER_PATH in slicer.py
uvicorn main:app --reload
``` ```
**Frontend**: ### 3. Frontend
```bash ```bash
cd frontend cd frontend
npm install npm install
npm start npm start
``` ```
Accedi a [http://localhost:4200](http://localhost:4200).
## Configurazione Prezzi
I prezzi non sono più gestiti tramite variabili d'ambiente fisse ma tramite tabelle nel database:
- `pricing_policy`: Definisce markup, fee fissi e costi elettrici.
- `pricing_policy_machine_hour_tier`: Definisce i costi orari delle macchine in base alla durata della stampa.
- `printer_machine`: Anagrafica stampanti e consumi energetici.
- `filament_material_type` / `filament_variant`: Listino prezzi materiali.
## Struttura del Progetto
* `/backend`: API Spring Boot.
* `/frontend`: Applicazione Angular.
* `/backend/profiles`: Contiene i file di configurazione per OrcaSlicer.
## Troubleshooting
### Percorso OrcaSlicer
Assicurati che `slicer.path` punti al binario corretto. Su macOS è solitamente `/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer`. Su Linux è il percorso all'AppImage (estratta o meno).
### Database connection
Verifica le credenziali in `application.properties`. Se usi Docker, puoi passare `DB_URL`, `DB_USERNAME` e `DB_PASSWORD` come variabili d'ambiente.

View File

@@ -0,0 +1,149 @@
package com.printcalculator.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "custom_quote_requests", indexes = {@Index(name = "ix_custom_quote_requests_status",
columnList = "status")})
public class CustomQuoteRequest {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "request_id", nullable = false)
private UUID id;
@Column(name = "request_type", nullable = false, length = Integer.MAX_VALUE)
private String requestType;
@Column(name = "customer_type", nullable = false, length = Integer.MAX_VALUE)
private String customerType;
@Column(name = "email", nullable = false, length = Integer.MAX_VALUE)
private String email;
@Column(name = "phone", length = Integer.MAX_VALUE)
private String phone;
@Column(name = "name", length = Integer.MAX_VALUE)
private String name;
@Column(name = "company_name", length = Integer.MAX_VALUE)
private String companyName;
@Column(name = "contact_person", length = Integer.MAX_VALUE)
private String contactPerson;
@Column(name = "message", nullable = false, length = Integer.MAX_VALUE)
private String message;
@Column(name = "status", nullable = false, length = Integer.MAX_VALUE)
private String status;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getRequestType() {
return requestType;
}
public void setRequestType(String requestType) {
this.requestType = requestType;
}
public String getCustomerType() {
return customerType;
}
public void setCustomerType(String customerType) {
this.customerType = customerType;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
public String getContactPerson() {
return contactPerson;
}
public void setContactPerson(String contactPerson) {
this.contactPerson = contactPerson;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,119 @@
package com.printcalculator.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "custom_quote_request_attachments", indexes = {@Index(name = "ix_custom_quote_attachments_request",
columnList = "request_id")})
public class CustomQuoteRequestAttachment {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "attachment_id", nullable = false)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "request_id", nullable = false)
private CustomQuoteRequest request;
@Column(name = "original_filename", nullable = false, length = Integer.MAX_VALUE)
private String originalFilename;
@Column(name = "stored_relative_path", nullable = false, length = Integer.MAX_VALUE)
private String storedRelativePath;
@Column(name = "stored_filename", nullable = false, length = Integer.MAX_VALUE)
private String storedFilename;
@Column(name = "file_size_bytes")
private Long fileSizeBytes;
@Column(name = "mime_type", length = Integer.MAX_VALUE)
private String mimeType;
@Column(name = "sha256_hex", length = Integer.MAX_VALUE)
private String sha256Hex;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public CustomQuoteRequest getRequest() {
return request;
}
public void setRequest(CustomQuoteRequest request) {
this.request = request;
}
public String getOriginalFilename() {
return originalFilename;
}
public void setOriginalFilename(String originalFilename) {
this.originalFilename = originalFilename;
}
public String getStoredRelativePath() {
return storedRelativePath;
}
public void setStoredRelativePath(String storedRelativePath) {
this.storedRelativePath = storedRelativePath;
}
public String getStoredFilename() {
return storedFilename;
}
public void setStoredFilename(String storedFilename) {
this.storedFilename = storedFilename;
}
public Long getFileSizeBytes() {
return fileSizeBytes;
}
public void setFileSizeBytes(Long fileSizeBytes) {
this.fileSizeBytes = fileSizeBytes;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public String getSha256Hex() {
return sha256Hex;
}
public void setSha256Hex(String sha256Hex) {
this.sha256Hex = sha256Hex;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@@ -0,0 +1,126 @@
package com.printcalculator.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "customer_id", nullable = false)
private UUID id;
@Column(name = "customer_type", nullable = false, length = Integer.MAX_VALUE)
private String customerType;
@Column(name = "email", nullable = false, length = Integer.MAX_VALUE)
private String email;
@Column(name = "phone", length = Integer.MAX_VALUE)
private String phone;
@Column(name = "first_name", length = Integer.MAX_VALUE)
private String firstName;
@Column(name = "last_name", length = Integer.MAX_VALUE)
private String lastName;
@Column(name = "company_name", length = Integer.MAX_VALUE)
private String companyName;
@Column(name = "contact_person", length = Integer.MAX_VALUE)
private String contactPerson;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getCustomerType() {
return customerType;
}
public void setCustomerType(String customerType) {
this.customerType = customerType;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
public String getContactPerson() {
return contactPerson;
}
public void setContactPerson(String contactPerson) {
this.contactPerson = contactPerson;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,413 @@
package com.printcalculator.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "orders", indexes = {@Index(name = "ix_orders_status",
columnList = "status")})
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "order_id", nullable = false)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "source_quote_session_id")
private QuoteSession sourceQuoteSession;
@Column(name = "status", nullable = false, length = Integer.MAX_VALUE)
private String status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
@Column(name = "customer_email", nullable = false, length = Integer.MAX_VALUE)
private String customerEmail;
@Column(name = "customer_phone", length = Integer.MAX_VALUE)
private String customerPhone;
@Column(name = "billing_customer_type", nullable = false, length = Integer.MAX_VALUE)
private String billingCustomerType;
@Column(name = "billing_first_name", length = Integer.MAX_VALUE)
private String billingFirstName;
@Column(name = "billing_last_name", length = Integer.MAX_VALUE)
private String billingLastName;
@Column(name = "billing_company_name", length = Integer.MAX_VALUE)
private String billingCompanyName;
@Column(name = "billing_contact_person", length = Integer.MAX_VALUE)
private String billingContactPerson;
@Column(name = "billing_address_line1", nullable = false, length = Integer.MAX_VALUE)
private String billingAddressLine1;
@Column(name = "billing_address_line2", length = Integer.MAX_VALUE)
private String billingAddressLine2;
@Column(name = "billing_zip", nullable = false, length = Integer.MAX_VALUE)
private String billingZip;
@Column(name = "billing_city", nullable = false, length = Integer.MAX_VALUE)
private String billingCity;
@ColumnDefault("'CH'")
@Column(name = "billing_country_code", nullable = false, length = 2)
private String billingCountryCode;
@ColumnDefault("true")
@Column(name = "shipping_same_as_billing", nullable = false)
private Boolean shippingSameAsBilling;
@Column(name = "shipping_first_name", length = Integer.MAX_VALUE)
private String shippingFirstName;
@Column(name = "shipping_last_name", length = Integer.MAX_VALUE)
private String shippingLastName;
@Column(name = "shipping_company_name", length = Integer.MAX_VALUE)
private String shippingCompanyName;
@Column(name = "shipping_contact_person", length = Integer.MAX_VALUE)
private String shippingContactPerson;
@Column(name = "shipping_address_line1", length = Integer.MAX_VALUE)
private String shippingAddressLine1;
@Column(name = "shipping_address_line2", length = Integer.MAX_VALUE)
private String shippingAddressLine2;
@Column(name = "shipping_zip", length = Integer.MAX_VALUE)
private String shippingZip;
@Column(name = "shipping_city", length = Integer.MAX_VALUE)
private String shippingCity;
@Column(name = "shipping_country_code", length = 2)
private String shippingCountryCode;
@ColumnDefault("'CHF'")
@Column(name = "currency", nullable = false, length = 3)
private String currency;
@ColumnDefault("0.00")
@Column(name = "setup_cost_chf", nullable = false, precision = 12, scale = 2)
private BigDecimal setupCostChf;
@ColumnDefault("0.00")
@Column(name = "shipping_cost_chf", nullable = false, precision = 12, scale = 2)
private BigDecimal shippingCostChf;
@ColumnDefault("0.00")
@Column(name = "discount_chf", nullable = false, precision = 12, scale = 2)
private BigDecimal discountChf;
@ColumnDefault("0.00")
@Column(name = "subtotal_chf", nullable = false, precision = 12, scale = 2)
private BigDecimal subtotalChf;
@ColumnDefault("0.00")
@Column(name = "total_chf", nullable = false, precision = 12, scale = 2)
private BigDecimal totalChf;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
@Column(name = "paid_at")
private OffsetDateTime paidAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public QuoteSession getSourceQuoteSession() {
return sourceQuoteSession;
}
public void setSourceQuoteSession(QuoteSession sourceQuoteSession) {
this.sourceQuoteSession = sourceQuoteSession;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public String getCustomerEmail() {
return customerEmail;
}
public void setCustomerEmail(String customerEmail) {
this.customerEmail = customerEmail;
}
public String getCustomerPhone() {
return customerPhone;
}
public void setCustomerPhone(String customerPhone) {
this.customerPhone = customerPhone;
}
public String getBillingCustomerType() {
return billingCustomerType;
}
public void setBillingCustomerType(String billingCustomerType) {
this.billingCustomerType = billingCustomerType;
}
public String getBillingFirstName() {
return billingFirstName;
}
public void setBillingFirstName(String billingFirstName) {
this.billingFirstName = billingFirstName;
}
public String getBillingLastName() {
return billingLastName;
}
public void setBillingLastName(String billingLastName) {
this.billingLastName = billingLastName;
}
public String getBillingCompanyName() {
return billingCompanyName;
}
public void setBillingCompanyName(String billingCompanyName) {
this.billingCompanyName = billingCompanyName;
}
public String getBillingContactPerson() {
return billingContactPerson;
}
public void setBillingContactPerson(String billingContactPerson) {
this.billingContactPerson = billingContactPerson;
}
public String getBillingAddressLine1() {
return billingAddressLine1;
}
public void setBillingAddressLine1(String billingAddressLine1) {
this.billingAddressLine1 = billingAddressLine1;
}
public String getBillingAddressLine2() {
return billingAddressLine2;
}
public void setBillingAddressLine2(String billingAddressLine2) {
this.billingAddressLine2 = billingAddressLine2;
}
public String getBillingZip() {
return billingZip;
}
public void setBillingZip(String billingZip) {
this.billingZip = billingZip;
}
public String getBillingCity() {
return billingCity;
}
public void setBillingCity(String billingCity) {
this.billingCity = billingCity;
}
public String getBillingCountryCode() {
return billingCountryCode;
}
public void setBillingCountryCode(String billingCountryCode) {
this.billingCountryCode = billingCountryCode;
}
public Boolean getShippingSameAsBilling() {
return shippingSameAsBilling;
}
public void setShippingSameAsBilling(Boolean shippingSameAsBilling) {
this.shippingSameAsBilling = shippingSameAsBilling;
}
public String getShippingFirstName() {
return shippingFirstName;
}
public void setShippingFirstName(String shippingFirstName) {
this.shippingFirstName = shippingFirstName;
}
public String getShippingLastName() {
return shippingLastName;
}
public void setShippingLastName(String shippingLastName) {
this.shippingLastName = shippingLastName;
}
public String getShippingCompanyName() {
return shippingCompanyName;
}
public void setShippingCompanyName(String shippingCompanyName) {
this.shippingCompanyName = shippingCompanyName;
}
public String getShippingContactPerson() {
return shippingContactPerson;
}
public void setShippingContactPerson(String shippingContactPerson) {
this.shippingContactPerson = shippingContactPerson;
}
public String getShippingAddressLine1() {
return shippingAddressLine1;
}
public void setShippingAddressLine1(String shippingAddressLine1) {
this.shippingAddressLine1 = shippingAddressLine1;
}
public String getShippingAddressLine2() {
return shippingAddressLine2;
}
public void setShippingAddressLine2(String shippingAddressLine2) {
this.shippingAddressLine2 = shippingAddressLine2;
}
public String getShippingZip() {
return shippingZip;
}
public void setShippingZip(String shippingZip) {
this.shippingZip = shippingZip;
}
public String getShippingCity() {
return shippingCity;
}
public void setShippingCity(String shippingCity) {
this.shippingCity = shippingCity;
}
public String getShippingCountryCode() {
return shippingCountryCode;
}
public void setShippingCountryCode(String shippingCountryCode) {
this.shippingCountryCode = shippingCountryCode;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
public BigDecimal getSetupCostChf() {
return setupCostChf;
}
public void setSetupCostChf(BigDecimal setupCostChf) {
this.setupCostChf = setupCostChf;
}
public BigDecimal getShippingCostChf() {
return shippingCostChf;
}
public void setShippingCostChf(BigDecimal shippingCostChf) {
this.shippingCostChf = shippingCostChf;
}
public BigDecimal getDiscountChf() {
return discountChf;
}
public void setDiscountChf(BigDecimal discountChf) {
this.discountChf = discountChf;
}
public BigDecimal getSubtotalChf() {
return subtotalChf;
}
public void setSubtotalChf(BigDecimal subtotalChf) {
this.subtotalChf = subtotalChf;
}
public BigDecimal getTotalChf() {
return totalChf;
}
public void setTotalChf(BigDecimal totalChf) {
this.totalChf = totalChf;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public OffsetDateTime getPaidAt() {
return paidAt;
}
public void setPaidAt(OffsetDateTime paidAt) {
this.paidAt = paidAt;
}
}

View File

@@ -0,0 +1,198 @@
package com.printcalculator.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "order_items", indexes = {@Index(name = "ix_order_items_order",
columnList = "order_id")})
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "order_item_id", nullable = false)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
@Column(name = "original_filename", nullable = false, length = Integer.MAX_VALUE)
private String originalFilename;
@Column(name = "stored_relative_path", nullable = false, length = Integer.MAX_VALUE)
private String storedRelativePath;
@Column(name = "stored_filename", nullable = false, length = Integer.MAX_VALUE)
private String storedFilename;
@Column(name = "file_size_bytes")
private Long fileSizeBytes;
@Column(name = "mime_type", length = Integer.MAX_VALUE)
private String mimeType;
@Column(name = "sha256_hex", length = Integer.MAX_VALUE)
private String sha256Hex;
@Column(name = "material_code", nullable = false, length = Integer.MAX_VALUE)
private String materialCode;
@Column(name = "color_code", length = Integer.MAX_VALUE)
private String colorCode;
@ColumnDefault("1")
@Column(name = "quantity", nullable = false)
private Integer quantity;
@Column(name = "print_time_seconds")
private Integer printTimeSeconds;
@Column(name = "material_grams", precision = 12, scale = 2)
private BigDecimal materialGrams;
@Column(name = "unit_price_chf", nullable = false, precision = 12, scale = 2)
private BigDecimal unitPriceChf;
@Column(name = "line_total_chf", nullable = false, precision = 12, scale = 2)
private BigDecimal lineTotalChf;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
public String getOriginalFilename() {
return originalFilename;
}
public void setOriginalFilename(String originalFilename) {
this.originalFilename = originalFilename;
}
public String getStoredRelativePath() {
return storedRelativePath;
}
public void setStoredRelativePath(String storedRelativePath) {
this.storedRelativePath = storedRelativePath;
}
public String getStoredFilename() {
return storedFilename;
}
public void setStoredFilename(String storedFilename) {
this.storedFilename = storedFilename;
}
public Long getFileSizeBytes() {
return fileSizeBytes;
}
public void setFileSizeBytes(Long fileSizeBytes) {
this.fileSizeBytes = fileSizeBytes;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public String getSha256Hex() {
return sha256Hex;
}
public void setSha256Hex(String sha256Hex) {
this.sha256Hex = sha256Hex;
}
public String getMaterialCode() {
return materialCode;
}
public void setMaterialCode(String materialCode) {
this.materialCode = materialCode;
}
public String getColorCode() {
return colorCode;
}
public void setColorCode(String colorCode) {
this.colorCode = colorCode;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public Integer getPrintTimeSeconds() {
return printTimeSeconds;
}
public void setPrintTimeSeconds(Integer printTimeSeconds) {
this.printTimeSeconds = printTimeSeconds;
}
public BigDecimal getMaterialGrams() {
return materialGrams;
}
public void setMaterialGrams(BigDecimal materialGrams) {
this.materialGrams = materialGrams;
}
public BigDecimal getUnitPriceChf() {
return unitPriceChf;
}
public void setUnitPriceChf(BigDecimal unitPriceChf) {
this.unitPriceChf = unitPriceChf;
}
public BigDecimal getLineTotalChf() {
return lineTotalChf;
}
public void setLineTotalChf(BigDecimal lineTotalChf) {
this.lineTotalChf = lineTotalChf;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@@ -0,0 +1,146 @@
package com.printcalculator.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "payments", indexes = {
@Index(name = "ix_payments_order",
columnList = "order_id"),
@Index(name = "ix_payments_reference",
columnList = "payment_reference")})
public class Payment {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "payment_id", nullable = false)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
@Column(name = "method", nullable = false, length = Integer.MAX_VALUE)
private String method;
@Column(name = "status", nullable = false, length = Integer.MAX_VALUE)
private String status;
@ColumnDefault("'CHF'")
@Column(name = "currency", nullable = false, length = 3)
private String currency;
@Column(name = "amount_chf", nullable = false, precision = 12, scale = 2)
private BigDecimal amountChf;
@Column(name = "payment_reference", length = Integer.MAX_VALUE)
private String paymentReference;
@Column(name = "provider_transaction_id", length = Integer.MAX_VALUE)
private String providerTransactionId;
@Column(name = "qr_payload", length = Integer.MAX_VALUE)
private String qrPayload;
@ColumnDefault("now()")
@Column(name = "initiated_at", nullable = false)
private OffsetDateTime initiatedAt;
@Column(name = "received_at")
private OffsetDateTime receivedAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
public BigDecimal getAmountChf() {
return amountChf;
}
public void setAmountChf(BigDecimal amountChf) {
this.amountChf = amountChf;
}
public String getPaymentReference() {
return paymentReference;
}
public void setPaymentReference(String paymentReference) {
this.paymentReference = paymentReference;
}
public String getProviderTransactionId() {
return providerTransactionId;
}
public void setProviderTransactionId(String providerTransactionId) {
this.providerTransactionId = providerTransactionId;
}
public String getQrPayload() {
return qrPayload;
}
public void setQrPayload(String qrPayload) {
this.qrPayload = qrPayload;
}
public OffsetDateTime getInitiatedAt() {
return initiatedAt;
}
public void setInitiatedAt(OffsetDateTime initiatedAt) {
this.initiatedAt = initiatedAt;
}
public OffsetDateTime getReceivedAt() {
return receivedAt;
}
public void setReceivedAt(OffsetDateTime receivedAt) {
this.receivedAt = receivedAt;
}
}

View File

@@ -0,0 +1,203 @@
package com.printcalculator.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.type.SqlTypes;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.Map;
import java.util.UUID;
@Entity
@Table(name = "quote_line_items", indexes = {@Index(name = "ix_quote_line_items_session",
columnList = "quote_session_id")})
public class QuoteLineItem {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "quote_line_item_id", nullable = false)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "quote_session_id", nullable = false)
private QuoteSession quoteSession;
@Column(name = "status", nullable = false, length = Integer.MAX_VALUE)
private String status;
@Column(name = "original_filename", nullable = false, length = Integer.MAX_VALUE)
private String originalFilename;
@ColumnDefault("1")
@Column(name = "quantity", nullable = false)
private Integer quantity;
@Column(name = "color_code", length = Integer.MAX_VALUE)
private String colorCode;
@Column(name = "bounding_box_x_mm", precision = 10, scale = 3)
private BigDecimal boundingBoxXMm;
@Column(name = "bounding_box_y_mm", precision = 10, scale = 3)
private BigDecimal boundingBoxYMm;
@Column(name = "bounding_box_z_mm", precision = 10, scale = 3)
private BigDecimal boundingBoxZMm;
@Column(name = "print_time_seconds")
private Integer printTimeSeconds;
@Column(name = "material_grams", precision = 12, scale = 2)
private BigDecimal materialGrams;
@Column(name = "unit_price_chf", precision = 12, scale = 2)
private BigDecimal unitPriceChf;
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "pricing_breakdown")
private Map<String, Object> pricingBreakdown;
@Column(name = "error_message", length = Integer.MAX_VALUE)
private String errorMessage;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public QuoteSession getQuoteSession() {
return quoteSession;
}
public void setQuoteSession(QuoteSession quoteSession) {
this.quoteSession = quoteSession;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getOriginalFilename() {
return originalFilename;
}
public void setOriginalFilename(String originalFilename) {
this.originalFilename = originalFilename;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public String getColorCode() {
return colorCode;
}
public void setColorCode(String colorCode) {
this.colorCode = colorCode;
}
public BigDecimal getBoundingBoxXMm() {
return boundingBoxXMm;
}
public void setBoundingBoxXMm(BigDecimal boundingBoxXMm) {
this.boundingBoxXMm = boundingBoxXMm;
}
public BigDecimal getBoundingBoxYMm() {
return boundingBoxYMm;
}
public void setBoundingBoxYMm(BigDecimal boundingBoxYMm) {
this.boundingBoxYMm = boundingBoxYMm;
}
public BigDecimal getBoundingBoxZMm() {
return boundingBoxZMm;
}
public void setBoundingBoxZMm(BigDecimal boundingBoxZMm) {
this.boundingBoxZMm = boundingBoxZMm;
}
public Integer getPrintTimeSeconds() {
return printTimeSeconds;
}
public void setPrintTimeSeconds(Integer printTimeSeconds) {
this.printTimeSeconds = printTimeSeconds;
}
public BigDecimal getMaterialGrams() {
return materialGrams;
}
public void setMaterialGrams(BigDecimal materialGrams) {
this.materialGrams = materialGrams;
}
public BigDecimal getUnitPriceChf() {
return unitPriceChf;
}
public void setUnitPriceChf(BigDecimal unitPriceChf) {
this.unitPriceChf = unitPriceChf;
}
public Map<String, Object> getPricingBreakdown() {
return pricingBreakdown;
}
public void setPricingBreakdown(Map<String, Object> pricingBreakdown) {
this.pricingBreakdown = pricingBreakdown;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,176 @@
package com.printcalculator.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "quote_sessions", indexes = {
@Index(name = "ix_quote_sessions_status",
columnList = "status"),
@Index(name = "ix_quote_sessions_expires_at",
columnList = "expires_at")})
public class QuoteSession {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "quote_session_id", nullable = false)
private UUID id;
@Column(name = "status", nullable = false, length = Integer.MAX_VALUE)
private String status;
@Column(name = "pricing_version", nullable = false, length = Integer.MAX_VALUE)
private String pricingVersion;
@Column(name = "material_code", nullable = false, length = Integer.MAX_VALUE)
private String materialCode;
@Column(name = "nozzle_diameter_mm", precision = 5, scale = 2)
private BigDecimal nozzleDiameterMm;
@Column(name = "layer_height_mm", precision = 6, scale = 3)
private BigDecimal layerHeightMm;
@Column(name = "infill_pattern", length = Integer.MAX_VALUE)
private String infillPattern;
@Column(name = "infill_percent")
private Integer infillPercent;
@ColumnDefault("false")
@Column(name = "supports_enabled", nullable = false)
private Boolean supportsEnabled;
@Column(name = "notes", length = Integer.MAX_VALUE)
private String notes;
@ColumnDefault("0.00")
@Column(name = "setup_cost_chf", nullable = false, precision = 12, scale = 2)
private BigDecimal setupCostChf;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@Column(name = "expires_at", nullable = false)
private OffsetDateTime expiresAt;
@Column(name = "converted_order_id")
private UUID convertedOrderId;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getPricingVersion() {
return pricingVersion;
}
public void setPricingVersion(String pricingVersion) {
this.pricingVersion = pricingVersion;
}
public String getMaterialCode() {
return materialCode;
}
public void setMaterialCode(String materialCode) {
this.materialCode = materialCode;
}
public BigDecimal getNozzleDiameterMm() {
return nozzleDiameterMm;
}
public void setNozzleDiameterMm(BigDecimal nozzleDiameterMm) {
this.nozzleDiameterMm = nozzleDiameterMm;
}
public BigDecimal getLayerHeightMm() {
return layerHeightMm;
}
public void setLayerHeightMm(BigDecimal layerHeightMm) {
this.layerHeightMm = layerHeightMm;
}
public String getInfillPattern() {
return infillPattern;
}
public void setInfillPattern(String infillPattern) {
this.infillPattern = infillPattern;
}
public Integer getInfillPercent() {
return infillPercent;
}
public void setInfillPercent(Integer infillPercent) {
this.infillPercent = infillPercent;
}
public Boolean getSupportsEnabled() {
return supportsEnabled;
}
public void setSupportsEnabled(Boolean supportsEnabled) {
this.supportsEnabled = supportsEnabled;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public BigDecimal getSetupCostChf() {
return setupCostChf;
}
public void setSetupCostChf(BigDecimal setupCostChf) {
this.setupCostChf = setupCostChf;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(OffsetDateTime expiresAt) {
this.expiresAt = expiresAt;
}
public UUID getConvertedOrderId() {
return convertedOrderId;
}
public void setConvertedOrderId(UUID convertedOrderId) {
this.convertedOrderId = convertedOrderId;
}
}

View File

@@ -0,0 +1,29 @@
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;
}
}

View File

@@ -0,0 +1,9 @@
package com.printcalculator.repository;
import org.springframework.data.jpa.domain.AbstractAuditable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;
@NoRepositoryBean
public interface AbstractAuditableRepository<T extends AbstractAuditable> extends JpaRepository<T, PK> {
}

View File

@@ -0,0 +1,9 @@
package com.printcalculator.repository;
import org.springframework.data.jpa.domain.AbstractPersistable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;
@NoRepositoryBean
public interface AbstractPersistableRepository<T extends AbstractPersistable> extends JpaRepository<T, PK> {
}

View File

@@ -0,0 +1,9 @@
package com.printcalculator.repository;
import com.printcalculator.entity.CustomQuoteRequestAttachment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface CustomQuoteRequestAttachmentRepository extends JpaRepository<CustomQuoteRequestAttachment, UUID> {
}

View File

@@ -0,0 +1,9 @@
package com.printcalculator.repository;
import com.printcalculator.entity.CustomQuoteRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface CustomQuoteRequestRepository extends JpaRepository<CustomQuoteRequest, UUID> {
}

View File

@@ -0,0 +1,9 @@
package com.printcalculator.repository;
import com.printcalculator.entity.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
}

View File

@@ -0,0 +1,7 @@
package com.printcalculator.repository;
import com.printcalculator.entity.FilamentVariantStockKg;
import org.springframework.data.jpa.repository.JpaRepository;
public interface FilamentVariantStockKgRepository extends JpaRepository<FilamentVariantStockKg, Long> {
}

View File

@@ -0,0 +1,9 @@
package com.printcalculator.repository;
import com.printcalculator.entity.OrderItem;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface OrderItemRepository extends JpaRepository<OrderItem, UUID> {
}

View File

@@ -0,0 +1,9 @@
package com.printcalculator.repository;
import com.printcalculator.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface OrderRepository extends JpaRepository<Order, UUID> {
}

View File

@@ -0,0 +1,9 @@
package com.printcalculator.repository;
import com.printcalculator.entity.Payment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface PaymentRepository extends JpaRepository<Payment, UUID> {
}

View File

@@ -0,0 +1,7 @@
package com.printcalculator.repository;
import com.printcalculator.entity.PrinterFleetCurrent;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PrinterFleetCurrentRepository extends JpaRepository<PrinterFleetCurrent, Long> {
}

View File

@@ -0,0 +1,9 @@
package com.printcalculator.repository;
import com.printcalculator.entity.QuoteLineItem;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface QuoteLineItemRepository extends JpaRepository<QuoteLineItem, UUID> {
}

View File

@@ -0,0 +1,9 @@
package com.printcalculator.repository;
import com.printcalculator.entity.QuoteSession;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface QuoteSessionRepository extends JpaRepository<QuoteSession, UUID> {
}

394
db.sql
View File

@@ -16,8 +16,7 @@ create table printer_machine
); );
create view printer_fleet_current as create view printer_fleet_current as
select 1 as fleet_id, select case
case
when sum(fleet_weight) = 0 then null when sum(fleet_weight) = 0 then null
else round(sum(power_watts * fleet_weight) / sum(fleet_weight))::integer else round(sum(power_watts * fleet_weight) / sum(fleet_weight))::integer
end as weighted_average_power_watts, end as weighted_average_power_watts,
@@ -156,6 +155,23 @@ begin;
set timezone = 'Europe/Zurich'; set timezone = 'Europe/Zurich';
-- =========================================================
-- 0) (Solo se non esiste) tabella infill_pattern + seed
-- =========================================================
-- Se la tabella esiste già, commenta questo blocco.
create table if not exists infill_pattern
(
infill_pattern_id bigserial primary key,
pattern_code text not null unique, -- es: grid, gyroid
display_name text not null,
is_active boolean not null default true
);
insert into infill_pattern (pattern_code, display_name, is_active)
values ('grid', 'Grid', true),
('gyroid', 'Gyroid', true)
on conflict (pattern_code) do update
set display_name = excluded.display_name,
is_active = excluded.is_active; is_active = excluded.is_active;
@@ -163,8 +179,7 @@ set timezone = 'Europe/Zurich';
-- 1) Pricing policy (valori ESATTI da Excel) -- 1) Pricing policy (valori ESATTI da Excel)
-- Valid from: 2026-01-01, valid_to: NULL -- Valid from: 2026-01-01, valid_to: NULL
-- ========================================================= -- =========================================================
insert into pricing_policy ( insert into pricing_policy (policy_name,
policy_name,
valid_from, valid_from,
valid_to, valid_to,
electricity_cost_chf_per_kwh, electricity_cost_chf_per_kwh,
@@ -172,9 +187,8 @@ insert into pricing_policy (
fixed_job_fee_chf, fixed_job_fee_chf,
nozzle_change_base_fee_chf, nozzle_change_base_fee_chf,
cad_cost_chf_per_hour, cad_cost_chf_per_hour,
is_active is_active)
) values ( values ('Excel Tariffe 2026-01-01',
'Excel Tariffe 2026-01-01',
'2026-01-01 00:00:00+01'::timestamptz, '2026-01-01 00:00:00+01'::timestamptz,
null, null,
0.156, -- Costo elettricità CHF/kWh (Excel) 0.156, -- Costo elettricità CHF/kWh (Excel)
@@ -182,26 +196,20 @@ insert into pricing_policy (
1.00, -- Costo fisso macchina CHF (Excel) 1.00, -- Costo fisso macchina CHF (Excel)
0.00, -- Base cambio ugello: non specificato -> 0 0.00, -- Base cambio ugello: non specificato -> 0
25.00, -- Tariffa CAD CHF/h (Excel) 25.00, -- Tariffa CAD CHF/h (Excel)
true true)
)
on conflict do nothing; on conflict do nothing;
-- scaglioni tariffa stampa (Excel) -- scaglioni tariffa stampa (Excel)
insert into pricing_policy_machine_hour_tier ( insert into pricing_policy_machine_hour_tier (pricing_policy_id,
pricing_policy_id,
tier_start_hours, tier_start_hours,
tier_end_hours, tier_end_hours,
machine_cost_chf_per_hour machine_cost_chf_per_hour)
) select p.pricing_policy_id,
select
p.pricing_policy_id,
tiers.tier_start_hours, tiers.tier_start_hours,
tiers.tier_end_hours, tiers.tier_end_hours,
tiers.machine_cost_chf_per_hour tiers.machine_cost_chf_per_hour
from pricing_policy p from pricing_policy p
cross join ( cross join (values (0.00::numeric, 10.00::numeric, 2.00::numeric), -- 010 h
values
(0.00::numeric, 10.00::numeric, 2.00::numeric), -- 010 h
(10.00::numeric, 20.00::numeric, 1.40::numeric), -- 1020 h (10.00::numeric, 20.00::numeric, 1.40::numeric), -- 1020 h
(20.00::numeric, null::numeric, 0.50::numeric) -- >20 h (20.00::numeric, null::numeric, 0.50::numeric) -- >20 h
) as tiers(tier_start_hours, tier_end_hours, machine_cost_chf_per_hour) ) as tiers(tier_start_hours, tier_end_hours, machine_cost_chf_per_hour)
@@ -212,26 +220,22 @@ on conflict do nothing;
-- ========================================================= -- =========================================================
-- 2) Stampante: BambuLab A1 -- 2) Stampante: BambuLab A1
-- ========================================================= -- =========================================================
insert into printer_machine ( insert into printer_machine (printer_display_name,
printer_display_name,
build_volume_x_mm, build_volume_x_mm,
build_volume_y_mm, build_volume_y_mm,
build_volume_z_mm, build_volume_z_mm,
power_watts, power_watts,
fleet_weight, fleet_weight,
is_active is_active)
) values ( values ('BambuLab A1',
'BambuLab A1',
256, 256,
256, 256,
256, 256,
150, -- hai detto "150, 140": qui ho messo 150 150, -- hai detto "150, 140": qui ho messo 150
1.000, 1.000,
true true)
)
on conflict (printer_display_name) do update on conflict (printer_display_name) do update
set set build_volume_x_mm = excluded.build_volume_x_mm,
build_volume_x_mm = excluded.build_volume_x_mm,
build_volume_y_mm = excluded.build_volume_y_mm, build_volume_y_mm = excluded.build_volume_y_mm,
build_volume_z_mm = excluded.build_volume_z_mm, build_volume_z_mm = excluded.build_volume_z_mm,
power_watts = excluded.power_watts, power_watts = excluded.power_watts,
@@ -242,21 +246,18 @@ on conflict (printer_display_name) do update
-- ========================================================= -- =========================================================
-- 3) Material types (da Excel) - per ora niente technical -- 3) Material types (da Excel) - per ora niente technical
-- ========================================================= -- =========================================================
insert into filament_material_type ( insert into filament_material_type (material_code,
material_code,
is_flexible, is_flexible,
is_technical, is_technical,
technical_type_label technical_type_label)
) values values ('PLA', false, false, null),
('PLA', false, false, null),
('PETG', false, false, null), ('PETG', false, false, null),
('TPU', true, false, null), ('TPU', true, false, null),
('ABS', false, false, null), ('ABS', false, false, null),
('Nylon', false, false, null), ('Nylon', false, false, null),
('Carbon PLA', false, false, null) ('Carbon PLA', false, false, null)
on conflict (material_code) do update on conflict (material_code) do update
set set is_flexible = excluded.is_flexible,
is_flexible = excluded.is_flexible,
is_technical = excluded.is_technical, is_technical = excluded.is_technical,
technical_type_label = excluded.technical_type_label; technical_type_label = excluded.technical_type_label;
@@ -268,13 +269,11 @@ on conflict (material_code) do update
-- ========================================================= -- =========================================================
-- helper: ID PLA -- helper: ID PLA
with pla as ( with pla as (select filament_material_type_id
select filament_material_type_id
from filament_material_type from filament_material_type
where material_code = 'PLA' where material_code = 'PLA')
) insert
insert into filament_variant ( into filament_variant (filament_material_type_id,
filament_material_type_id,
variant_display_name, variant_display_name,
color_name, color_name,
is_matte, is_matte,
@@ -282,10 +281,8 @@ insert into filament_variant (
cost_chf_per_kg, cost_chf_per_kg,
stock_spools, stock_spools,
spool_net_kg, spool_net_kg,
is_active is_active)
) select pla.filament_material_type_id,
select
pla.filament_material_type_id,
v.variant_display_name, v.variant_display_name,
v.color_name, v.color_name,
v.is_matte, v.is_matte,
@@ -295,20 +292,17 @@ select
1.000, 1.000,
true true
from pla from pla
cross join ( cross join (values ('PLA Bianco', 'Bianco', false, false, 3.000::numeric),
values
('PLA Bianco', 'Bianco', false, false, 3.000::numeric),
('PLA Nero', 'Nero', false, false, 3.000::numeric), ('PLA Nero', 'Nero', false, false, 3.000::numeric),
('PLA Blu', 'Blu', false, false, 1.000::numeric), ('PLA Blu', 'Blu', false, false, 1.000::numeric),
('PLA Arancione', 'Arancione', false, false, 1.000::numeric), ('PLA Arancione', 'Arancione', false, false, 1.000::numeric),
('PLA Grigio', 'Grigio', false, false, 1.000::numeric), ('PLA Grigio', 'Grigio', false, false, 1.000::numeric),
('PLA Grigio Scuro', 'Grigio scuro', false, false, 1.000::numeric), ('PLA Grigio Scuro', 'Grigio scuro', false, false, 1.000::numeric),
('PLA Grigio Chiaro', 'Grigio chiaro', false, false, 1.000::numeric), ('PLA Grigio Chiaro', 'Grigio chiaro', false, false, 1.000::numeric),
('PLA Viola', 'Viola', false, false, 1.000::numeric) ('PLA Viola', 'Viola', false, false,
) as v(variant_display_name, color_name, is_matte, is_special, stock_spools) 1.000::numeric)) as v(variant_display_name, color_name, is_matte, is_special, stock_spools)
on conflict (filament_material_type_id, variant_display_name) do update on conflict (filament_material_type_id, variant_display_name) do update
set set color_name = excluded.color_name,
color_name = excluded.color_name,
is_matte = excluded.is_matte, is_matte = excluded.is_matte,
is_special = excluded.is_special, is_special = excluded.is_special,
cost_chf_per_kg = excluded.cost_chf_per_kg, cost_chf_per_kg = excluded.cost_chf_per_kg,
@@ -321,17 +315,14 @@ on conflict (filament_material_type_id, variant_display_name) do update
-- 5) Ugelli -- 5) Ugelli
-- 0.4 standard (0 extra), 0.6 con attivazione 50 CHF -- 0.4 standard (0 extra), 0.6 con attivazione 50 CHF
-- ========================================================= -- =========================================================
insert into nozzle_option ( insert into nozzle_option (nozzle_diameter_mm,
nozzle_diameter_mm,
owned_quantity, owned_quantity,
extra_nozzle_change_fee_chf, extra_nozzle_change_fee_chf,
is_active is_active)
) values values (0.40, 1, 0.00, true),
(0.40, 1, 0.00, true),
(0.60, 1, 50.00, true) (0.60, 1, 50.00, true)
on conflict (nozzle_diameter_mm) do update on conflict (nozzle_diameter_mm) do update
set set owned_quantity = excluded.owned_quantity,
owned_quantity = excluded.owned_quantity,
extra_nozzle_change_fee_chf = excluded.extra_nozzle_change_fee_chf, extra_nozzle_change_fee_chf = excluded.extra_nozzle_change_fee_chf,
is_active = excluded.is_active; is_active = excluded.is_active;
@@ -339,28 +330,297 @@ on conflict (nozzle_diameter_mm) do update
-- ========================================================= -- =========================================================
-- 6) Layer heights (opzioni) -- 6) Layer heights (opzioni)
-- ========================================================= -- =========================================================
insert into layer_height_option ( insert into layer_height_option (layer_height_mm,
layer_height_mm,
time_multiplier, time_multiplier,
is_active is_active)
) values values (0.080, 1.000, true),
(0.080, 1.000, true),
(0.120, 1.000, true), (0.120, 1.000, true),
(0.160, 1.000, true), (0.160, 1.000, true),
(0.200, 1.000, true), (0.200, 1.000, true),
(0.240, 1.000, true), (0.240, 1.000, true),
(0.280, 1.000, true) (0.280, 1.000, true)
on conflict (layer_height_mm) do update on conflict (layer_height_mm) do update
set set time_multiplier = excluded.time_multiplier,
time_multiplier = excluded.time_multiplier,
is_active = excluded.is_active; is_active = excluded.is_active;
commit; commit;
-- Sostituisci __MULTIPLIER__ con il tuo valore (es. 1.10) -- Sostituisci __MULTIPLIER__ con il tuo valore (es. 1.10)
update layer_height_option update layer_height_option
set time_multiplier = 0.1 set time_multiplier = 0.1
where layer_height_mm = 0.080; where layer_height_mm = 0.080;
-- =========================
-- CUSTOMERS (minimo indispensabile)
-- =========================
CREATE TABLE IF NOT EXISTS customers
(
customer_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
customer_type text NOT NULL CHECK (customer_type IN ('PRIVATE', 'COMPANY')),
email text NOT NULL,
phone text,
-- per PRIVATE
first_name text,
last_name text,
-- per COMPANY
company_name text,
contact_person text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS ux_customers_email
ON customers (lower(email));
-- =========================
-- QUOTE SESSIONS (carrello preventivo)
-- =========================
CREATE TABLE IF NOT EXISTS quote_sessions
(
quote_session_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
status text NOT NULL CHECK (status IN ('ACTIVE', 'EXPIRED', 'CONVERTED')),
pricing_version text NOT NULL,
-- Parametri "globali" (dalla tua UI avanzata)
material_code text NOT NULL, -- es: PLA, PETG...
nozzle_diameter_mm numeric(5, 2), -- es: 0.40
layer_height_mm numeric(6, 3), -- es: 0.20
infill_pattern text, -- es: grid
infill_percent integer CHECK (infill_percent BETWEEN 0 AND 100),
supports_enabled boolean NOT NULL DEFAULT false,
notes text,
setup_cost_chf numeric(12, 2) NOT NULL DEFAULT 0.00,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
converted_order_id uuid
);
CREATE INDEX IF NOT EXISTS ix_quote_sessions_status
ON quote_sessions (status);
CREATE INDEX IF NOT EXISTS ix_quote_sessions_expires_at
ON quote_sessions (expires_at);
-- =========================
-- QUOTE LINE ITEMS (1 file = 1 riga)
-- =========================
CREATE TABLE IF NOT EXISTS quote_line_items
(
quote_line_item_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
quote_session_id uuid NOT NULL REFERENCES quote_sessions (quote_session_id) ON DELETE CASCADE,
status text NOT NULL CHECK (status IN ('CALCULATING', 'READY', 'FAILED')),
original_filename text NOT NULL,
quantity integer NOT NULL DEFAULT 1 CHECK (quantity >= 1),
color_code text, -- es: white/black o codice interno
-- Output slicing / calcolo
bounding_box_x_mm numeric(10, 3),
bounding_box_y_mm numeric(10, 3),
bounding_box_z_mm numeric(10, 3),
print_time_seconds integer CHECK (print_time_seconds >= 0),
material_grams numeric(12, 2) CHECK (material_grams >= 0),
unit_price_chf numeric(12, 2) CHECK (unit_price_chf >= 0),
pricing_breakdown jsonb, -- opzionale: costi dettagliati senza creare tabelle
error_message text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_quote_line_items_session
ON quote_line_items (quote_session_id);
-- Vista utile per totale quote
CREATE OR REPLACE VIEW quote_session_totals AS
SELECT qs.quote_session_id,
qs.setup_cost_chf +
COALESCE(SUM(qli.unit_price_chf * qli.quantity), 0.00) AS total_chf
FROM quote_sessions qs
LEFT JOIN quote_line_items qli
ON qli.quote_session_id = qs.quote_session_id
AND qli.status = 'READY'
GROUP BY qs.quote_session_id;
-- =========================
-- ORDERS
-- =========================
CREATE TABLE IF NOT EXISTS orders
(
order_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
source_quote_session_id uuid REFERENCES quote_sessions (quote_session_id),
status text NOT NULL CHECK (status IN (
'PENDING_PAYMENT', 'PAID', 'IN_PRODUCTION',
'SHIPPED', 'COMPLETED', 'CANCELLED'
)),
customer_id uuid REFERENCES customers (customer_id),
customer_email text NOT NULL,
customer_phone text,
-- Snapshot indirizzo/fatturazione (evita tabella addresses e mantiene storico)
billing_customer_type text NOT NULL CHECK (billing_customer_type IN ('PRIVATE', 'COMPANY')),
billing_first_name text,
billing_last_name text,
billing_company_name text,
billing_contact_person text,
billing_address_line1 text NOT NULL,
billing_address_line2 text,
billing_zip text NOT NULL,
billing_city text NOT NULL,
billing_country_code char(2) NOT NULL DEFAULT 'CH',
shipping_same_as_billing boolean NOT NULL DEFAULT true,
shipping_first_name text,
shipping_last_name text,
shipping_company_name text,
shipping_contact_person text,
shipping_address_line1 text,
shipping_address_line2 text,
shipping_zip text,
shipping_city text,
shipping_country_code char(2),
currency char(3) NOT NULL DEFAULT 'CHF',
setup_cost_chf numeric(12, 2) NOT NULL DEFAULT 0.00,
shipping_cost_chf numeric(12, 2) NOT NULL DEFAULT 0.00,
discount_chf numeric(12, 2) NOT NULL DEFAULT 0.00,
subtotal_chf numeric(12, 2) NOT NULL DEFAULT 0.00,
total_chf numeric(12, 2) NOT NULL DEFAULT 0.00,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
paid_at timestamptz
);
CREATE INDEX IF NOT EXISTS ix_orders_status
ON orders (status);
CREATE INDEX IF NOT EXISTS ix_orders_customer_email
ON orders (lower(customer_email));
-- =========================
-- ORDER ITEMS (1 file 3D = 1 riga, file salvato su disco)
-- =========================
CREATE TABLE IF NOT EXISTS order_items
(
order_item_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid NOT NULL REFERENCES orders (order_id) ON DELETE CASCADE,
original_filename text NOT NULL,
stored_relative_path text NOT NULL, -- es: orders/<orderId>/3d-files/<orderItemId>/<uuid>.stl
stored_filename text NOT NULL, -- es: <uuid>.stl
file_size_bytes bigint CHECK (file_size_bytes >= 0),
mime_type text,
sha256_hex text, -- opzionale, utile anche per dedup interno
material_code text NOT NULL,
color_code text,
quantity integer NOT NULL DEFAULT 1 CHECK (quantity >= 1),
-- Snapshot output
print_time_seconds integer CHECK (print_time_seconds >= 0),
material_grams numeric(12, 2) CHECK (material_grams >= 0),
unit_price_chf numeric(12, 2) NOT NULL CHECK (unit_price_chf >= 0),
line_total_chf numeric(12, 2) NOT NULL CHECK (line_total_chf >= 0),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_order_items_order
ON order_items (order_id);
-- =========================
-- PAYMENTS (supporta più tentativi / metodi)
-- =========================
CREATE TABLE IF NOT EXISTS payments
(
payment_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid NOT NULL REFERENCES orders (order_id) ON DELETE CASCADE,
method text NOT NULL CHECK (method IN ('QR_BILL', 'BANK_TRANSFER', 'TWINT', 'CARD', 'OTHER')),
status text NOT NULL CHECK (status IN ('PENDING', 'RECEIVED', 'FAILED', 'CANCELLED', 'REFUNDED')),
currency char(3) NOT NULL DEFAULT 'CHF',
amount_chf numeric(12, 2) NOT NULL CHECK (amount_chf >= 0),
-- riferimento pagamento (molto utile per QR bill / riconciliazione)
payment_reference text,
provider_transaction_id text,
qr_payload text, -- se vuoi salvare contenuto QR/Swiss QR bill
initiated_at timestamptz NOT NULL DEFAULT now(),
received_at timestamptz
);
CREATE INDEX IF NOT EXISTS ix_payments_order
ON payments (order_id);
CREATE INDEX IF NOT EXISTS ix_payments_reference
ON payments (payment_reference);
-- =========================
-- CUSTOM QUOTE REQUESTS (preventivo personalizzato, form che hai mostrato)
-- =========================
CREATE TABLE IF NOT EXISTS custom_quote_requests
(
request_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
request_type text NOT NULL, -- es: "PREVENTIVO_PERSONALIZZATO" o come preferisci
customer_type text NOT NULL CHECK (customer_type IN ('PRIVATE', 'COMPANY')),
email text NOT NULL,
phone text,
-- PRIVATE
name text,
-- COMPANY
company_name text,
contact_person text,
message text NOT NULL,
status text NOT NULL CHECK (status IN ('NEW', 'PENDING', 'IN_PROGRESS', 'DONE', 'CLOSED')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_custom_quote_requests_status
ON custom_quote_requests (status);
CREATE INDEX IF NOT EXISTS ix_custom_quote_requests_email
ON custom_quote_requests (lower(email));
-- Allegati della richiesta (max 15 come UI)
CREATE TABLE IF NOT EXISTS custom_quote_request_attachments
(
attachment_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
request_id uuid NOT NULL REFERENCES custom_quote_requests (request_id) ON DELETE CASCADE,
original_filename text NOT NULL,
stored_relative_path text NOT NULL, -- es: quote-requests/<requestId>/attachments/<attachmentId>/<uuid>.stl
stored_filename text NOT NULL,
file_size_bytes bigint CHECK (file_size_bytes >= 0),
mime_type text,
sha256_hex text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_custom_quote_attachments_request
ON custom_quote_request_attachments (request_id);