produzione 1 #9
47
GEMINI.md
47
GEMINI.md
@@ -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).
|
||||
|
||||
93
README.md
93
README.md
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
126
backend/src/main/java/com/printcalculator/entity/Customer.java
Normal file
126
backend/src/main/java/com/printcalculator/entity/Customer.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
413
backend/src/main/java/com/printcalculator/entity/Order.java
Normal file
413
backend/src/main/java/com/printcalculator/entity/Order.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
198
backend/src/main/java/com/printcalculator/entity/OrderItem.java
Normal file
198
backend/src/main/java/com/printcalculator/entity/OrderItem.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
146
backend/src/main/java/com/printcalculator/entity/Payment.java
Normal file
146
backend/src/main/java/com/printcalculator/entity/Payment.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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
558
db.sql
@@ -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), -- 0–10 h
|
||||
(10.00::numeric, 20.00::numeric, 1.40::numeric), -- 10–20 h
|
||||
(20.00::numeric, null::numeric, 0.50::numeric) -- >20 h
|
||||
cross join (values (0.00::numeric, 10.00::numeric, 2.00::numeric), -- 0–10 h
|
||||
(10.00::numeric, 20.00::numeric, 1.40::numeric), -- 10–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)
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user