935 lines
36 KiB
PL/PgSQL
935 lines
36 KiB
PL/PgSQL
create table printer_machine
|
||
(
|
||
printer_machine_id bigserial primary key,
|
||
printer_display_name text not null unique,
|
||
|
||
build_volume_x_mm integer not null check (build_volume_x_mm > 0),
|
||
build_volume_y_mm integer not null check (build_volume_y_mm > 0),
|
||
build_volume_z_mm integer not null check (build_volume_z_mm > 0),
|
||
|
||
power_watts integer not null check (power_watts > 0),
|
||
|
||
fleet_weight numeric(6, 3) not null default 1.000,
|
||
|
||
is_active boolean not null default true,
|
||
created_at timestamptz not null default now()
|
||
);
|
||
|
||
create view printer_fleet_current as
|
||
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,
|
||
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
|
||
from printer_machine
|
||
where is_active = true;
|
||
|
||
|
||
|
||
create table filament_material_type
|
||
(
|
||
filament_material_type_id bigserial primary key,
|
||
material_code text not null unique, -- PLA, PETG, TPU, ASA...
|
||
is_flexible boolean not null default false, -- sì/no
|
||
is_technical boolean not null default false, -- sì/no
|
||
technical_type_label text -- es: "alta temperatura", "rinforzato", ecc.
|
||
);
|
||
|
||
create table filament_variant
|
||
(
|
||
filament_variant_id bigserial primary key,
|
||
filament_material_type_id bigint not null references filament_material_type (filament_material_type_id),
|
||
|
||
variant_display_name text not null, -- es: "PLA Nero Opaco BrandX"
|
||
color_name text not null, -- Nero, Bianco, ecc.
|
||
color_hex text,
|
||
finish_type text not null default 'GLOSSY'
|
||
check (finish_type in ('GLOSSY', 'MATTE', 'MARBLE', 'SILK', 'TRANSLUCENT', 'SPECIAL')),
|
||
brand text,
|
||
is_matte boolean not null default false,
|
||
is_special boolean not null default false,
|
||
|
||
cost_chf_per_kg numeric(10, 2) not null,
|
||
|
||
-- Stock espresso in rotoli anche frazionati
|
||
stock_spools numeric(6, 3) not null default 0.000,
|
||
spool_net_kg numeric(6, 3) not null default 1.000,
|
||
|
||
is_active boolean not null default true,
|
||
created_at timestamptz not null default now(),
|
||
|
||
unique (filament_material_type_id, variant_display_name)
|
||
);
|
||
|
||
create view filament_variant_stock_kg as
|
||
select filament_variant_id,
|
||
stock_spools,
|
||
spool_net_kg,
|
||
(stock_spools * spool_net_kg) as stock_kg
|
||
from filament_variant;
|
||
|
||
create table printer_machine_profile
|
||
(
|
||
printer_machine_profile_id bigserial primary key,
|
||
printer_machine_id bigint not null references printer_machine (printer_machine_id) on delete cascade,
|
||
nozzle_diameter_mm numeric(4, 2) not null check (nozzle_diameter_mm > 0),
|
||
orca_machine_profile_name text not null,
|
||
is_default boolean not null default false,
|
||
is_active boolean not null default true,
|
||
unique (printer_machine_id, nozzle_diameter_mm)
|
||
);
|
||
|
||
create table material_orca_profile_map
|
||
(
|
||
material_orca_profile_map_id bigserial primary key,
|
||
printer_machine_profile_id bigint not null references printer_machine_profile (printer_machine_profile_id) on delete cascade,
|
||
filament_material_type_id bigint not null references filament_material_type (filament_material_type_id),
|
||
orca_filament_profile_name text not null,
|
||
is_active boolean not null default true,
|
||
unique (printer_machine_profile_id, filament_material_type_id)
|
||
);
|
||
|
||
create table filament_variant_orca_override
|
||
(
|
||
filament_variant_orca_override_id bigserial primary key,
|
||
filament_variant_id bigint not null references filament_variant (filament_variant_id) on delete cascade,
|
||
printer_machine_profile_id bigint not null references printer_machine_profile (printer_machine_profile_id) on delete cascade,
|
||
orca_filament_profile_name text not null,
|
||
is_active boolean not null default true,
|
||
unique (filament_variant_id, printer_machine_profile_id)
|
||
);
|
||
|
||
|
||
|
||
create table pricing_policy
|
||
(
|
||
pricing_policy_id bigserial primary key,
|
||
|
||
policy_name text not null, -- es: "2026 Q1", "Default", ecc.
|
||
|
||
-- validità temporale (consiglio: valid_to esclusiva)
|
||
valid_from timestamptz not null,
|
||
valid_to timestamptz,
|
||
|
||
electricity_cost_chf_per_kwh numeric(10, 6) not null,
|
||
markup_percent numeric(6, 3) not null default 20.000,
|
||
|
||
fixed_job_fee_chf numeric(10, 2) not null default 0.00, -- "costo fisso"
|
||
nozzle_change_base_fee_chf numeric(10, 2) not null default 0.00, -- base cambio ugello, se vuoi
|
||
cad_cost_chf_per_hour numeric(10, 2) not null default 0.00,
|
||
|
||
is_active boolean not null default true,
|
||
created_at timestamptz not null default now()
|
||
);
|
||
|
||
create table pricing_policy_machine_hour_tier
|
||
(
|
||
pricing_policy_machine_hour_tier_id bigserial primary key,
|
||
pricing_policy_id bigint not null references pricing_policy (pricing_policy_id),
|
||
|
||
tier_start_hours numeric(10, 2) not null,
|
||
tier_end_hours numeric(10, 2), -- null = infinito
|
||
machine_cost_chf_per_hour numeric(10, 2) not null,
|
||
|
||
constraint chk_tier_start_non_negative check (tier_start_hours >= 0),
|
||
constraint chk_tier_end_gt_start check (tier_end_hours is null or tier_end_hours > tier_start_hours)
|
||
);
|
||
|
||
create index idx_pricing_policy_validity
|
||
on pricing_policy (valid_from, valid_to);
|
||
|
||
create index idx_pricing_tier_lookup
|
||
on pricing_policy_machine_hour_tier (pricing_policy_id, tier_start_hours);
|
||
|
||
|
||
create table nozzle_option
|
||
(
|
||
nozzle_option_id bigserial primary key,
|
||
nozzle_diameter_mm numeric(4, 2) not null unique, -- 0.4, 0.6, 0.8...
|
||
|
||
owned_quantity integer not null default 0 check (owned_quantity >= 0),
|
||
|
||
-- extra costo specifico oltre ad eventuale base fee della pricing_policy
|
||
extra_nozzle_change_fee_chf numeric(10, 2) not null default 0.00,
|
||
|
||
is_active boolean not null default true,
|
||
created_at timestamptz not null default now()
|
||
);
|
||
|
||
|
||
create table layer_height_option
|
||
(
|
||
layer_height_option_id bigserial primary key,
|
||
layer_height_mm numeric(5, 3) not null unique, -- 0.12, 0.20, 0.28...
|
||
|
||
-- opzionale: moltiplicatore costo/tempo (es: 0.12 costa di più)
|
||
time_multiplier numeric(6, 3) not null default 1.000,
|
||
|
||
is_active boolean not null default true
|
||
);
|
||
|
||
create table layer_height_profile
|
||
(
|
||
layer_height_profile_id bigserial primary key,
|
||
profile_name text not null unique, -- "Standard", "Fine", ecc.
|
||
|
||
min_layer_height_mm numeric(5, 3) not null,
|
||
max_layer_height_mm numeric(5, 3) not null,
|
||
default_layer_height_mm numeric(5, 3) not null,
|
||
|
||
time_multiplier numeric(6, 3) not null default 1.000,
|
||
|
||
constraint chk_layer_range check (max_layer_height_mm >= min_layer_height_mm)
|
||
);
|
||
|
||
|
||
begin;
|
||
|
||
set timezone = 'Europe/Zurich';
|
||
|
||
-- =========================================================
|
||
-- 0) (Solo se non esiste) tabella infill_pattern + seed
|
||
-- =========================================================
|
||
-- Se la tabella esiste già, commenta questo blocco.
|
||
create table if not exists infill_pattern
|
||
(
|
||
infill_pattern_id bigserial primary key,
|
||
pattern_code text not null unique, -- es: grid, gyroid
|
||
display_name text not null,
|
||
is_active boolean not null default true
|
||
);
|
||
|
||
insert into infill_pattern (pattern_code, display_name, is_active)
|
||
values ('grid', 'Grid', true),
|
||
('gyroid', 'Gyroid', true)
|
||
on conflict (pattern_code) do update
|
||
set display_name = excluded.display_name,
|
||
is_active = excluded.is_active;
|
||
|
||
|
||
-- =========================================================
|
||
-- 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)
|
||
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
|
||
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
|
||
) 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;
|
||
|
||
|
||
-- =========================================================
|
||
-- 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)
|
||
on conflict (printer_display_name) do update
|
||
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;
|
||
|
||
|
||
-- =========================================================
|
||
-- 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),
|
||
('PLA TOUGH', false, false, null),
|
||
('PETG', false, false, null),
|
||
('TPU', true, false, null),
|
||
('PC', false, true, 'engineering'),
|
||
('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,
|
||
technical_type_label = excluded.technical_type_label;
|
||
|
||
|
||
-- =========================================================
|
||
-- 4) Filament variants (PLA colori) - costi da Excel
|
||
-- Excel: PLA = 18 CHF/kg, TPU = 42 CHF/kg (non inserito perché quantità non chiara)
|
||
-- Stock in "rotoli" (3 = 3 kg se spool_net_kg=1)
|
||
-- =========================================================
|
||
|
||
-- 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,
|
||
color_hex,
|
||
finish_type,
|
||
brand,
|
||
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.color_hex,
|
||
v.finish_type,
|
||
null::text as brand,
|
||
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', '#F5F5F5', 'GLOSSY', false, false, 3.000::numeric),
|
||
('PLA Nero', 'Nero', '#1A1A1A', 'GLOSSY', false, false, 3.000::numeric),
|
||
('PLA Blu', 'Blu', '#1976D2', 'GLOSSY', false, false, 1.000::numeric),
|
||
('PLA Arancione', 'Arancione', '#FFA726', 'GLOSSY', false, false, 1.000::numeric),
|
||
('PLA Grigio', 'Grigio', '#BDBDBD', 'GLOSSY', false, false, 1.000::numeric),
|
||
('PLA Grigio Scuro', 'Grigio scuro', '#424242', 'MATTE', true, false, 1.000::numeric),
|
||
('PLA Grigio Chiaro', 'Grigio chiaro', '#D6D6D6', 'MATTE', true, false, 1.000::numeric),
|
||
('PLA Viola', 'Viola', '#7B1FA2', 'GLOSSY', false, false,
|
||
1.000::numeric)) as v(variant_display_name, color_name, color_hex, finish_type, is_matte, is_special, stock_spools)
|
||
on conflict (filament_material_type_id, variant_display_name) do update
|
||
set color_name = excluded.color_name,
|
||
color_hex = excluded.color_hex,
|
||
finish_type = excluded.finish_type,
|
||
brand = excluded.brand,
|
||
is_matte = excluded.is_matte,
|
||
is_special = excluded.is_special,
|
||
cost_chf_per_kg = excluded.cost_chf_per_kg,
|
||
stock_spools = excluded.stock_spools,
|
||
spool_net_kg = excluded.spool_net_kg,
|
||
is_active = excluded.is_active;
|
||
|
||
-- Varianti base per materiali principali del calcolatore
|
||
with mat as (select filament_material_type_id
|
||
from filament_material_type
|
||
where material_code = 'PLA TOUGH')
|
||
insert
|
||
into filament_variant (filament_material_type_id, variant_display_name, color_name, color_hex, finish_type, brand,
|
||
is_matte, is_special, cost_chf_per_kg, stock_spools, spool_net_kg, is_active)
|
||
select mat.filament_material_type_id,
|
||
'PLA Tough Nero',
|
||
'Nero',
|
||
'#1A1A1A',
|
||
'GLOSSY',
|
||
'Bambu',
|
||
false,
|
||
false,
|
||
18.00,
|
||
1.000,
|
||
1.000,
|
||
true
|
||
from mat
|
||
on conflict (filament_material_type_id, variant_display_name) do update
|
||
set color_name = excluded.color_name,
|
||
color_hex = excluded.color_hex,
|
||
finish_type = excluded.finish_type,
|
||
brand = excluded.brand,
|
||
is_matte = excluded.is_matte,
|
||
is_special = excluded.is_special,
|
||
cost_chf_per_kg = excluded.cost_chf_per_kg,
|
||
stock_spools = excluded.stock_spools,
|
||
spool_net_kg = excluded.spool_net_kg,
|
||
is_active = excluded.is_active;
|
||
|
||
with mat as (select filament_material_type_id
|
||
from filament_material_type
|
||
where material_code = 'PETG')
|
||
insert
|
||
into filament_variant (filament_material_type_id, variant_display_name, color_name, color_hex, finish_type, brand,
|
||
is_matte, is_special, cost_chf_per_kg, stock_spools, spool_net_kg, is_active)
|
||
select mat.filament_material_type_id,
|
||
'PETG Nero',
|
||
'Nero',
|
||
'#1A1A1A',
|
||
'GLOSSY',
|
||
'Bambu',
|
||
false,
|
||
false,
|
||
24.00,
|
||
1.000,
|
||
1.000,
|
||
true
|
||
from mat
|
||
on conflict (filament_material_type_id, variant_display_name) do update
|
||
set color_name = excluded.color_name,
|
||
color_hex = excluded.color_hex,
|
||
finish_type = excluded.finish_type,
|
||
brand = excluded.brand,
|
||
is_matte = excluded.is_matte,
|
||
is_special = excluded.is_special,
|
||
cost_chf_per_kg = excluded.cost_chf_per_kg,
|
||
stock_spools = excluded.stock_spools,
|
||
spool_net_kg = excluded.spool_net_kg,
|
||
is_active = excluded.is_active;
|
||
|
||
with mat as (select filament_material_type_id
|
||
from filament_material_type
|
||
where material_code = 'TPU')
|
||
insert
|
||
into filament_variant (filament_material_type_id, variant_display_name, color_name, color_hex, finish_type, brand,
|
||
is_matte, is_special, cost_chf_per_kg, stock_spools, spool_net_kg, is_active)
|
||
select mat.filament_material_type_id,
|
||
'TPU Nero',
|
||
'Nero',
|
||
'#1A1A1A',
|
||
'GLOSSY',
|
||
'Bambu',
|
||
false,
|
||
false,
|
||
42.00,
|
||
1.000,
|
||
1.000,
|
||
true
|
||
from mat
|
||
on conflict (filament_material_type_id, variant_display_name) do update
|
||
set color_name = excluded.color_name,
|
||
color_hex = excluded.color_hex,
|
||
finish_type = excluded.finish_type,
|
||
brand = excluded.brand,
|
||
is_matte = excluded.is_matte,
|
||
is_special = excluded.is_special,
|
||
cost_chf_per_kg = excluded.cost_chf_per_kg,
|
||
stock_spools = excluded.stock_spools,
|
||
spool_net_kg = excluded.spool_net_kg,
|
||
is_active = excluded.is_active;
|
||
|
||
with mat as (select filament_material_type_id
|
||
from filament_material_type
|
||
where material_code = 'PC')
|
||
insert
|
||
into filament_variant (filament_material_type_id, variant_display_name, color_name, color_hex, finish_type, brand,
|
||
is_matte, is_special, cost_chf_per_kg, stock_spools, spool_net_kg, is_active)
|
||
select mat.filament_material_type_id,
|
||
'PC Naturale',
|
||
'Naturale',
|
||
'#D9D9D9',
|
||
'TRANSLUCENT',
|
||
'Generic',
|
||
false,
|
||
true,
|
||
48.00,
|
||
1.000,
|
||
1.000,
|
||
true
|
||
from mat
|
||
on conflict (filament_material_type_id, variant_display_name) do update
|
||
set color_name = excluded.color_name,
|
||
color_hex = excluded.color_hex,
|
||
finish_type = excluded.finish_type,
|
||
brand = excluded.brand,
|
||
is_matte = excluded.is_matte,
|
||
is_special = excluded.is_special,
|
||
cost_chf_per_kg = excluded.cost_chf_per_kg,
|
||
stock_spools = excluded.stock_spools,
|
||
spool_net_kg = excluded.spool_net_kg,
|
||
is_active = excluded.is_active;
|
||
|
||
|
||
-- =========================================================
|
||
-- 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)
|
||
on conflict (nozzle_diameter_mm) do update
|
||
set owned_quantity = excluded.owned_quantity,
|
||
extra_nozzle_change_fee_chf = excluded.extra_nozzle_change_fee_chf,
|
||
is_active = excluded.is_active;
|
||
|
||
-- =========================================================
|
||
-- 5b) Orca machine/material mapping (data-driven)
|
||
-- =========================================================
|
||
with a1 as (select printer_machine_id
|
||
from printer_machine
|
||
where printer_display_name = 'BambuLab A1')
|
||
insert
|
||
into printer_machine_profile (printer_machine_id, nozzle_diameter_mm, orca_machine_profile_name, is_default, is_active)
|
||
select a1.printer_machine_id, v.nozzle_diameter_mm, v.profile_name, v.is_default, true
|
||
from a1
|
||
cross join (values (0.40::numeric, 'Bambu Lab A1 0.4 nozzle', true),
|
||
(0.20::numeric, 'Bambu Lab A1 0.2 nozzle', false),
|
||
(0.60::numeric, 'Bambu Lab A1 0.6 nozzle', false),
|
||
(0.80::numeric, 'Bambu Lab A1 0.8 nozzle', false))
|
||
as v(nozzle_diameter_mm, profile_name, is_default)
|
||
on conflict (printer_machine_id, nozzle_diameter_mm) do update
|
||
set orca_machine_profile_name = excluded.orca_machine_profile_name,
|
||
is_default = excluded.is_default,
|
||
is_active = excluded.is_active;
|
||
|
||
with p as (select printer_machine_profile_id
|
||
from printer_machine_profile pmp
|
||
join printer_machine pm on pm.printer_machine_id = pmp.printer_machine_id
|
||
where pm.printer_display_name = 'BambuLab A1'
|
||
and pmp.nozzle_diameter_mm = 0.40::numeric),
|
||
m as (select filament_material_type_id, material_code
|
||
from filament_material_type
|
||
where material_code in ('PLA', 'PLA TOUGH', 'PETG', 'TPU', 'PC'))
|
||
insert
|
||
into material_orca_profile_map (printer_machine_profile_id, filament_material_type_id, orca_filament_profile_name, is_active)
|
||
select p.printer_machine_profile_id,
|
||
m.filament_material_type_id,
|
||
case m.material_code
|
||
when 'PLA' then 'Bambu PLA Basic @BBL A1'
|
||
when 'PLA TOUGH' then 'Bambu PLA Tough @BBL A1'
|
||
when 'PETG' then 'Bambu PETG Basic @BBL A1'
|
||
when 'TPU' then 'Bambu TPU 95A @BBL A1'
|
||
when 'PC' then 'Generic PC @BBL A1'
|
||
end,
|
||
true
|
||
from p
|
||
cross join m
|
||
on conflict (printer_machine_profile_id, filament_material_type_id) do update
|
||
set orca_filament_profile_name = excluded.orca_filament_profile_name,
|
||
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)
|
||
on conflict (layer_height_mm) do update
|
||
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', 'CAD_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,
|
||
source_request_id uuid,
|
||
cad_hours numeric(10, 2),
|
||
cad_hourly_rate_chf numeric(10, 2),
|
||
|
||
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);
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_quote_sessions_source_request
|
||
ON quote_sessions (source_request_id);
|
||
|
||
ALTER TABLE quote_sessions
|
||
ADD COLUMN IF NOT EXISTS source_request_id uuid;
|
||
|
||
ALTER TABLE quote_sessions
|
||
ADD COLUMN IF NOT EXISTS cad_hours numeric(10, 2);
|
||
|
||
ALTER TABLE quote_sessions
|
||
ADD COLUMN IF NOT EXISTS cad_hourly_rate_chf numeric(10, 2);
|
||
|
||
ALTER TABLE quote_sessions
|
||
DROP CONSTRAINT IF EXISTS quote_sessions_status_check;
|
||
|
||
ALTER TABLE quote_sessions
|
||
ADD CONSTRAINT quote_sessions_status_check
|
||
CHECK (status IN ('ACTIVE', 'CAD_ACTIVE', 'EXPIRED', 'CONVERTED'));
|
||
|
||
-- =========================
|
||
-- 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
|
||
filament_variant_id bigint REFERENCES filament_variant (filament_variant_id),
|
||
material_code text,
|
||
nozzle_diameter_mm numeric(5, 2),
|
||
layer_height_mm numeric(6, 3),
|
||
infill_pattern text,
|
||
infill_percent integer CHECK (infill_percent BETWEEN 0 AND 100),
|
||
supports_enabled boolean,
|
||
|
||
-- 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);
|
||
|
||
ALTER TABLE quote_line_items
|
||
ADD COLUMN IF NOT EXISTS material_code text;
|
||
|
||
ALTER TABLE quote_line_items
|
||
ADD COLUMN IF NOT EXISTS nozzle_diameter_mm numeric(5, 2);
|
||
|
||
ALTER TABLE quote_line_items
|
||
ADD COLUMN IF NOT EXISTS layer_height_mm numeric(6, 3);
|
||
|
||
ALTER TABLE quote_line_items
|
||
ADD COLUMN IF NOT EXISTS infill_pattern text;
|
||
|
||
ALTER TABLE quote_line_items
|
||
ADD COLUMN IF NOT EXISTS infill_percent integer;
|
||
|
||
ALTER TABLE quote_line_items
|
||
ADD COLUMN IF NOT EXISTS supports_enabled boolean;
|
||
|
||
-- 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),
|
||
source_request_id uuid,
|
||
|
||
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,
|
||
preferred_language char(2) NOT NULL DEFAULT 'it',
|
||
|
||
-- 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,
|
||
is_cad_order boolean NOT NULL DEFAULT false,
|
||
cad_hours numeric(10, 2),
|
||
cad_hourly_rate_chf numeric(10, 2),
|
||
cad_total_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));
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_orders_source_request
|
||
ON orders (source_request_id);
|
||
|
||
ALTER TABLE orders
|
||
ADD COLUMN IF NOT EXISTS source_request_id uuid;
|
||
|
||
ALTER TABLE orders
|
||
ADD COLUMN IF NOT EXISTS is_cad_order boolean NOT NULL DEFAULT false;
|
||
|
||
ALTER TABLE orders
|
||
ADD COLUMN IF NOT EXISTS cad_hours numeric(10, 2);
|
||
|
||
ALTER TABLE orders
|
||
ADD COLUMN IF NOT EXISTS cad_hourly_rate_chf numeric(10, 2);
|
||
|
||
ALTER TABLE orders
|
||
ADD COLUMN IF NOT EXISTS cad_total_chf numeric(12, 2) NOT NULL DEFAULT 0.00;
|
||
|
||
-- =========================
|
||
-- 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,
|
||
filament_variant_id bigint REFERENCES filament_variant (filament_variant_id),
|
||
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),
|
||
|
||
bounding_box_x_mm numeric(10, 3),
|
||
bounding_box_y_mm numeric(10, 3),
|
||
bounding_box_z_mm numeric(10, 3),
|
||
|
||
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', 'TWINT', 'OTHER')),
|
||
status text NOT NULL CHECK (status IN ('PENDING', 'REPORTED', '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(),
|
||
reported_at timestamptz,
|
||
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);
|
||
|
||
ALTER TABLE quote_sessions
|
||
DROP CONSTRAINT IF EXISTS fk_quote_sessions_source_request;
|
||
|
||
ALTER TABLE quote_sessions
|
||
ADD CONSTRAINT fk_quote_sessions_source_request
|
||
FOREIGN KEY (source_request_id) REFERENCES custom_quote_requests (request_id);
|
||
|
||
ALTER TABLE orders
|
||
DROP CONSTRAINT IF EXISTS fk_orders_source_request;
|
||
|
||
ALTER TABLE orders
|
||
ADD CONSTRAINT fk_orders_source_request
|
||
FOREIGN KEY (source_request_id) REFERENCES custom_quote_requests (request_id);
|