dev #8

Closed
JoeKung wants to merge 72 commits from dev into int
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
**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**:
- **Backend**: Python (FastAPI), libreria `trimesh` per analisi geometrica.
- **Frontend**: Angular 19 (TypeScript).
- **Backend**: Java 21 (Spring Boot 3.4), PostgreSQL, Flyway.
- **Frontend**: Angular 19 (TypeScript), Angular Material, Three.js per visualizzazione 3D.
## Architecture
### Backend (`/backend`)
- **`main.py`**: Entrypoint dell'applicazione FastAPI.
- Definisce l'API `POST /calculate/stl`.
- Gestisce l'upload del file, invoca lo slicer e restituisce il preventivo.
- Configura CORS per permettere chiamate dal frontend.
- **`slicer.py`**: Wrappa l'eseguibile di **OrcaSlicer** per effettuare lo slicing reale del modello.
- Gestisce i profili di stampa (Macchina, Processo, Filamento).
- Crea configurazioni on-the-fly per supportare mesh di grandi dimensioni.
- **`calculator.py`**: Analizza il G-Code generato.
- `GCodeParser`: Estrae tempo di stampa e materiale usato dai metadati del G-Code.
- `QuoteCalculator`: Applica i costi (orari, energia, materiale) per generare il prezzo finale.
- **`BackendApplication.java`**: Entrypoint dell'applicazione Spring Boot.
- **`controller/`**: Espone le API REST per l'upload e il calcolo dei preventivi.
- **`service/SlicerService.java`**: Wrappa l'eseguibile di **OrcaSlicer** per effettuare lo slicing reale del modello.
- Gestisce i profili di stampa (Macchina, Processo, Filamento) caricati da file JSON.
- Crea configurazioni on-the-fly e invoca OrcaSlicer in modalità headless.
- **`service/GCodeParser.java`**: Analizza il G-Code generato per estrarre tempo di stampa e peso del materiale dai metadati del file.
- **`service/QuoteCalculator.java`**: Calcola il prezzo finale basandosi su politiche di prezzo salvate nel database.
- Gestisce costi macchina a scaglioni (tiered pricing).
- Calcola costi energetici basati sulla potenza della stampante e costo del kWh.
- Applica markup percentuali e fee fissi per job.
### Frontend (`/frontend`)
- Applicazione Angular standard.
- Usa Angular Material.
- Service per upload STL e visualizzazione preventivo.
- Applicazione Angular 19 con architettura modulare (core, features, shared).
- **Three.js**: Utilizzato per il rendering dei file STL caricati dall'utente.
- **Angular Material**: Per l'interfaccia utente.
- **ngx-translate**: Per il supporto multilingua.
## 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.
- **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`).
- **Real Slicing**: Il backend esegue un vero slicing usando OrcaSlicer. Questo garantisce stime di tempo e materiale estremamente precise.
- **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
- Per eseguire il backend serve `uvicorn`.
- Il frontend richiede `npm install` al primo avvio.
- 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.
- **Backend**: Richiede JDK 21. Si avvia con `./gradlew bootRun`.
- **Database**: Richiede PostgreSQL. Le migrazioni sono gestite da Flyway.
- **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
- **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)
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à
* **Slicing Reale**: Usa il motore di OrcaSlicer per stimare tempo e materiale, non semplici approssimazioni geometriche.
* **Preventivazione Completa**: Calcola costo materiale, ammortamento macchina, energia e ricarico.
* **Configurabile**: Prezzi e parametri macchina modificabili via variabili d'ambiente.
* **Docker Ready**: Tutto containerizzato per un facile deployment.
* **Slicing Reale**: Usa il motore di OrcaSlicer per stimare tempo e materiale, garantendo la massima precisione.
* **Preventivazione Database-Driven**: Calcolo basato su politiche di prezzo configurabili nel database (costo materiale, ammortamento macchina a scaglioni, energia e markup).
* **Visualizzazione 3D**: Anteprima del file STL caricato tramite Three.js.
* **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
* Docker Desktop & Docker Compose installati.
* **Java 21** installato.
* **Node.js 22** e **npm** installati.
* **PostgreSQL** attivo.
* **OrcaSlicer** installato sul sistema.
## Avvio Rapido
1. Clona il repository.
2. Esegui lo script di avvio o docker-compose:
```bash
docker-compose up --build
```
*Nota: La prima build impiegherà alcuni minuti per scaricare OrcaSlicer (~200MB) e compilare il Frontend.*
### 1. Database
Crea un database PostgreSQL chiamato `printcalc`. Le tabelle verranno create automaticamente al primo avvio tramite Flyway.
3. Accedi all'applicazione:
* **Frontend**: [http://localhost](http://localhost)
* **API Docs**: [http://localhost:8000/docs](http://localhost:8000/docs)
### 2. Backend
Configura il percorso di OrcaSlicer in `backend/src/main/resources/application.properties` o tramite la variabile d'ambiente `SLICER_PATH`.
## 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
cd backend
pip install -r requirements.txt
# Assicurati di avere OrcaSlicer installato e nel PATH o aggiorna SLICER_PATH in slicer.py
uvicorn main:app --reload
./gradlew bootRun
```
**Frontend**:
### 3. Frontend
```bash
cd frontend
npm install
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> {
}

558
db.sql
View File

@@ -16,11 +16,10 @@ create table printer_machine
);
create view printer_fleet_current as
select 1 as fleet_id,
case
select case
when sum(fleet_weight) = 0 then null
else round(sum(power_watts * fleet_weight) / sum(fleet_weight))::integer
end as weighted_average_power_watts,
end as weighted_average_power_watts,
max(build_volume_x_mm) as fleet_max_build_x_mm,
max(build_volume_y_mm) as fleet_max_build_y_mm,
max(build_volume_z_mm) as fleet_max_build_z_mm
@@ -156,54 +155,63 @@ begin;
set timezone = 'Europe/Zurich';
is_active = excluded.is_active;
-- =========================================================
-- 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;
-- =========================================================
-- 1) Pricing policy (valori ESATTI da Excel)
-- Valid from: 2026-01-01, valid_to: NULL
-- =========================================================
insert into pricing_policy (
policy_name,
valid_from,
valid_to,
electricity_cost_chf_per_kwh,
markup_percent,
fixed_job_fee_chf,
nozzle_change_base_fee_chf,
cad_cost_chf_per_hour,
is_active
) values (
'Excel Tariffe 2026-01-01',
'2026-01-01 00:00:00+01'::timestamptz,
null,
0.156, -- Costo elettricità CHF/kWh (Excel)
0.000, -- Markup non specificato -> 0 (puoi cambiarlo dopo)
1.00, -- Costo fisso macchina CHF (Excel)
0.00, -- Base cambio ugello: non specificato -> 0
25.00, -- Tariffa CAD CHF/h (Excel)
true
)
insert into pricing_policy (policy_name,
valid_from,
valid_to,
electricity_cost_chf_per_kwh,
markup_percent,
fixed_job_fee_chf,
nozzle_change_base_fee_chf,
cad_cost_chf_per_hour,
is_active)
values ('Excel Tariffe 2026-01-01',
'2026-01-01 00:00:00+01'::timestamptz,
null,
0.156, -- Costo elettricità CHF/kWh (Excel)
0.000, -- Markup non specificato -> 0 (puoi cambiarlo dopo)
1.00, -- Costo fisso macchina CHF (Excel)
0.00, -- Base cambio ugello: non specificato -> 0
25.00, -- Tariffa CAD CHF/h (Excel)
true)
on conflict do nothing;
-- scaglioni tariffa stampa (Excel)
insert into pricing_policy_machine_hour_tier (
pricing_policy_id,
tier_start_hours,
tier_end_hours,
machine_cost_chf_per_hour
)
select
p.pricing_policy_id,
tiers.tier_start_hours,
tiers.tier_end_hours,
tiers.machine_cost_chf_per_hour
insert into pricing_policy_machine_hour_tier (pricing_policy_id,
tier_start_hours,
tier_end_hours,
machine_cost_chf_per_hour)
select p.pricing_policy_id,
tiers.tier_start_hours,
tiers.tier_end_hours,
tiers.machine_cost_chf_per_hour
from pricing_policy p
cross join (
values
(0.00::numeric, 10.00::numeric, 2.00::numeric), -- 010 h
(10.00::numeric, 20.00::numeric, 1.40::numeric), -- 1020 h
(20.00::numeric, null::numeric, 0.50::numeric) -- >20 h
cross join (values (0.00::numeric, 10.00::numeric, 2.00::numeric), -- 010 h
(10.00::numeric, 20.00::numeric, 1.40::numeric), -- 1020 h
(20.00::numeric, null::numeric, 0.50::numeric) -- >20 h
) as tiers(tier_start_hours, tier_end_hours, machine_cost_chf_per_hour)
where p.policy_name = 'Excel Tariffe 2026-01-01'
on conflict do nothing;
@@ -212,52 +220,45 @@ on conflict do nothing;
-- =========================================================
-- 2) Stampante: BambuLab A1
-- =========================================================
insert into printer_machine (
printer_display_name,
build_volume_x_mm,
build_volume_y_mm,
build_volume_z_mm,
power_watts,
fleet_weight,
is_active
) values (
'BambuLab A1',
256,
256,
256,
150, -- hai detto "150, 140": qui ho messo 150
1.000,
true
)
insert into printer_machine (printer_display_name,
build_volume_x_mm,
build_volume_y_mm,
build_volume_z_mm,
power_watts,
fleet_weight,
is_active)
values ('BambuLab A1',
256,
256,
256,
150, -- hai detto "150, 140": qui ho messo 150
1.000,
true)
on conflict (printer_display_name) do update
set
build_volume_x_mm = excluded.build_volume_x_mm,
set build_volume_x_mm = excluded.build_volume_x_mm,
build_volume_y_mm = excluded.build_volume_y_mm,
build_volume_z_mm = excluded.build_volume_z_mm,
power_watts = excluded.power_watts,
fleet_weight = excluded.fleet_weight,
is_active = excluded.is_active;
power_watts = excluded.power_watts,
fleet_weight = excluded.fleet_weight,
is_active = excluded.is_active;
-- =========================================================
-- 3) Material types (da Excel) - per ora niente technical
-- =========================================================
insert into filament_material_type (
material_code,
is_flexible,
is_technical,
technical_type_label
) values
('PLA', false, false, null),
('PETG', false, false, null),
('TPU', true, false, null),
('ABS', false, false, null),
('Nylon', false, false, null),
('Carbon PLA', false, false, null)
insert into filament_material_type (material_code,
is_flexible,
is_technical,
technical_type_label)
values ('PLA', false, false, null),
('PETG', false, false, null),
('TPU', true, false, null),
('ABS', false, false, null),
('Nylon', false, false, null),
('Carbon PLA', false, false, null)
on conflict (material_code) do update
set
is_flexible = excluded.is_flexible,
is_technical = excluded.is_technical,
set is_flexible = excluded.is_flexible,
is_technical = excluded.is_technical,
technical_type_label = excluded.technical_type_label;
@@ -268,99 +269,358 @@ on conflict (material_code) do update
-- =========================================================
-- helper: ID PLA
with pla as (
select filament_material_type_id
from filament_material_type
where material_code = 'PLA'
)
insert into filament_variant (
filament_material_type_id,
variant_display_name,
color_name,
is_matte,
is_special,
cost_chf_per_kg,
stock_spools,
spool_net_kg,
is_active
)
select
pla.filament_material_type_id,
v.variant_display_name,
v.color_name,
v.is_matte,
v.is_special,
18.00, -- PLA da Excel
v.stock_spools,
1.000,
true
with pla as (select filament_material_type_id
from filament_material_type
where material_code = 'PLA')
insert
into filament_variant (filament_material_type_id,
variant_display_name,
color_name,
is_matte,
is_special,
cost_chf_per_kg,
stock_spools,
spool_net_kg,
is_active)
select pla.filament_material_type_id,
v.variant_display_name,
v.color_name,
v.is_matte,
v.is_special,
18.00, -- PLA da Excel
v.stock_spools,
1.000,
true
from pla
cross join (
values
('PLA Bianco', 'Bianco', false, false, 3.000::numeric),
('PLA Nero', 'Nero', false, false, 3.000::numeric),
('PLA Blu', 'Blu', false, false, 1.000::numeric),
('PLA Arancione', 'Arancione', 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 Chiaro', 'Grigio chiaro', false, false, 1.000::numeric),
('PLA Viola', 'Viola', false, false, 1.000::numeric)
) as v(variant_display_name, color_name, is_matte, is_special, stock_spools)
cross join (values ('PLA Bianco', 'Bianco', false, false, 3.000::numeric),
('PLA Nero', 'Nero', false, false, 3.000::numeric),
('PLA Blu', 'Blu', false, false, 1.000::numeric),
('PLA Arancione', 'Arancione', 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 Chiaro', 'Grigio chiaro', false, false, 1.000::numeric),
('PLA Viola', 'Viola', false, false,
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
set
color_name = excluded.color_name,
is_matte = excluded.is_matte,
is_special = excluded.is_special,
set color_name = excluded.color_name,
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;
stock_spools = excluded.stock_spools,
spool_net_kg = excluded.spool_net_kg,
is_active = excluded.is_active;
-- =========================================================
-- 5) Ugelli
-- 0.4 standard (0 extra), 0.6 con attivazione 50 CHF
-- =========================================================
insert into nozzle_option (
nozzle_diameter_mm,
owned_quantity,
extra_nozzle_change_fee_chf,
is_active
) values
(0.40, 1, 0.00, true),
(0.60, 1, 50.00, true)
insert into nozzle_option (nozzle_diameter_mm,
owned_quantity,
extra_nozzle_change_fee_chf,
is_active)
values (0.40, 1, 0.00, true),
(0.60, 1, 50.00, true)
on conflict (nozzle_diameter_mm) do update
set
owned_quantity = excluded.owned_quantity,
set owned_quantity = excluded.owned_quantity,
extra_nozzle_change_fee_chf = excluded.extra_nozzle_change_fee_chf,
is_active = excluded.is_active;
is_active = excluded.is_active;
-- =========================================================
-- 6) Layer heights (opzioni)
-- =========================================================
insert into layer_height_option (
layer_height_mm,
time_multiplier,
is_active
) values
(0.080, 1.000, true),
(0.120, 1.000, true),
(0.160, 1.000, true),
(0.200, 1.000, true),
(0.240, 1.000, true),
(0.280, 1.000, true)
insert into layer_height_option (layer_height_mm,
time_multiplier,
is_active)
values (0.080, 1.000, true),
(0.120, 1.000, true),
(0.160, 1.000, true),
(0.200, 1.000, true),
(0.240, 1.000, true),
(0.280, 1.000, true)
on conflict (layer_height_mm) do update
set
time_multiplier = excluded.time_multiplier,
is_active = excluded.is_active;
set time_multiplier = excluded.time_multiplier,
is_active = excluded.is_active;
commit;
-- Sostituisci __MULTIPLIER__ con il tuo valore (es. 1.10)
update layer_height_option
set time_multiplier = 0.1
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);