-- 11-consolidate-km-rental.sql -- Consolidate km/rental model: new included_km_per_day, rental_type, -- rewrite calculate_price / create_lead / qualify_lead / notify_lead_qualified, -- add sales_order_set_total RPC. -- Idempotent. -- ============================================================================= -- A. Vehicles table changes -- ============================================================================= alter table public.vehicles add column if not exists included_km_per_day integer not null default 150; update public.vehicles set included_km_per_day = coalesce(max_daily_km, 150) where included_km_per_day = 150; update public.vehicles set included_km_per_day = 200 where brand = 'Ferrari' and model = '296 GTB'; alter table public.vehicles add column if not exists price_per_km_eur numeric(10,2) not null default 1.50; alter table public.vehicles drop column if exists max_daily_km; alter table public.vehicles drop column if exists max_km_weekend; -- ============================================================================= -- B. Leads table changes -- ============================================================================= alter table public.leads add column if not exists rental_type text not null default 'weekend' check (rental_type in ('weekend','individuell')); update public.leads set rental_type = 'weekend' where rental_type is null; create index if not exists leads_rental_type_idx on public.leads (rental_type); -- ============================================================================= -- C. Sales orders table changes -- ============================================================================= alter table public.sales_orders add column if not exists rental_type text not null default 'weekend' check (rental_type in ('weekend','individuell')); update public.sales_orders set rental_type = 'weekend' where rental_type is null; create index if not exists sales_orders_rental_type_idx on public.sales_orders (rental_type); -- ============================================================================= -- D. Rewrite calculate_price() RPC -- ============================================================================= drop function if exists public.calculate_price(uuid, date, date); create or replace function public.calculate_price( p_vehicle_id uuid, p_date_from date, p_date_to date ) returns jsonb language plpgsql stable security definer as $$ declare v_vehicle record; v_total_days integer; v_weekend_days integer; v_weekdays integer; v_daily_subtotal integer; v_weekend_subtotal integer; v_subtotal_eur integer; v_vat_eur integer; v_total_eur integer; v_deposit_eur integer; v_included_km_per_day integer; v_price_per_km numeric(10,2); v_total_included_km integer; v_extra_km integer; v_extra_km_eur numeric(10,2); v_cur date; v_dow integer; begin if p_vehicle_id is null or p_date_from is null or p_date_to is null then raise exception 'vehicle_id, date_from and date_to are required'; end if; if p_date_to <= p_date_from then raise exception 'date_to must be after date_from'; end if; select daily_price_eur, weekend_price_eur, kaution_eur, included_km_per_day, price_per_km_eur into v_vehicle from public.vehicles where id = p_vehicle_id; if not found then raise exception 'Vehicle not found'; end if; v_total_days := (p_date_to - p_date_from); v_weekend_days := 0; v_cur := p_date_from; while v_cur < p_date_to loop v_dow := extract(isodow from v_cur); -- 6=Sat, 7=Sun if v_dow in (6, 7) then v_weekend_days := v_weekend_days + 1; end if; v_cur := v_cur + 1; end loop; v_weekdays := v_total_days - v_weekend_days; v_daily_subtotal := v_weekdays * v_vehicle.daily_price_eur; v_weekend_subtotal := v_weekend_days * (case when v_vehicle.weekend_price_eur > 0 then v_vehicle.weekend_price_eur else v_vehicle.daily_price_eur end); v_subtotal_eur := v_daily_subtotal + v_weekend_subtotal; v_vat_eur := round(v_subtotal_eur * 0.20); v_total_eur := v_subtotal_eur + v_vat_eur; v_deposit_eur := coalesce(nullif(v_vehicle.kaution_eur, 0), 5000); v_included_km_per_day := coalesce(v_vehicle.included_km_per_day, 150); v_total_included_km := v_total_days * v_included_km_per_day; v_price_per_km := coalesce(v_vehicle.price_per_km_eur, 1.50); v_extra_km := greatest(0, 0); -- extra km is determined by caller (frontend) based on expected usage v_extra_km_eur := v_extra_km * v_price_per_km; return jsonb_build_object( 'total_days', v_total_days, 'weekday_count', v_weekdays, 'weekend_day_count', v_weekend_days, 'daily_subtotal', v_daily_subtotal, 'weekend_subtotal', v_weekend_subtotal, 'subtotal_eur', v_subtotal_eur, 'vat_eur', v_vat_eur, 'total_eur', v_total_eur, 'deposit_eur', v_deposit_eur, 'daily_price_eur', v_vehicle.daily_price_eur, 'weekend_price_eur', (case when v_vehicle.weekend_price_eur > 0 then v_vehicle.weekend_price_eur else v_vehicle.daily_price_eur end), 'included_km_per_day', v_included_km_per_day, 'total_included_km', v_total_included_km, 'price_per_km_eur', v_price_per_km, 'extra_km', v_extra_km, 'extra_km_eur', v_extra_km_eur ); end; $$; grant execute on function public.calculate_price(uuid, date, date) to anon, authenticated, service_role; -- ============================================================================= -- E. Rewrite create_lead() RPC -- ============================================================================= drop function if exists public.create_lead( text, text, text, uuid, text, date, date, text, text ); drop function if exists public.create_lead( text, text, text, uuid, text, date, date, text, text, integer, integer, integer, integer, integer, integer, integer, integer, integer, text, text ); create or replace function public.create_lead( p_name text, p_email text, p_phone text default '', p_vehicle_id uuid default null, p_vehicle_label text default '', p_date_from date default null, p_date_to date default null, p_message text default '', p_source text default 'website', p_ip_address text default '', p_ip_country text default '' ) returns uuid language plpgsql security definer as $$ declare v_lead_id uuid; v_vehicle record; v_total_days integer := 0; v_weekend_days integer := 0; v_weekdays integer := 0; v_daily_subtotal integer := 0; v_weekend_subtotal integer := 0; v_subtotal_eur integer := 0; v_vat_eur integer := 0; v_total_eur integer := 0; v_deposit_eur integer := 0; v_rental_type text := 'weekend'; v_cur date; v_dow integer; begin if p_vehicle_id is not null and p_date_from is not null and p_date_to is not null and p_date_to > p_date_from then select daily_price_eur, weekend_price_eur, kaution_eur into v_vehicle from public.vehicles where id = p_vehicle_id; if found then v_total_days := (p_date_to - p_date_from); -- Auto-detect rental type: 2 days or less = weekend, more = individuell v_rental_type := 'weekend'; if v_total_days > 2 then v_rental_type := 'individuell'; end if; -- For individuell, set all pricing to 0 if v_rental_type = 'individuell' then v_daily_subtotal := 0; v_weekend_subtotal := 0; v_subtotal_eur := 0; v_vat_eur := 0; v_total_eur := 0; v_deposit_eur := 0; else v_cur := p_date_from; while v_cur < p_date_to loop v_dow := extract(isodow from v_cur); if v_dow in (6, 7) then v_weekend_days := v_weekend_days + 1; end if; v_cur := v_cur + 1; end loop; v_weekdays := v_total_days - v_weekend_days; v_daily_subtotal := v_weekdays * v_vehicle.daily_price_eur; v_weekend_subtotal := v_weekend_days * (case when v_vehicle.weekend_price_eur > 0 then v_vehicle.weekend_price_eur else v_vehicle.daily_price_eur end); v_subtotal_eur := v_daily_subtotal + v_weekend_subtotal; v_vat_eur := round(v_subtotal_eur * 0.20); v_total_eur := v_subtotal_eur + v_vat_eur; v_deposit_eur := coalesce(nullif(v_vehicle.kaution_eur, 0), 5000); end if; end if; end if; insert into public.leads ( name, email, phone, vehicle_id, vehicle_label, date_from, date_to, message, source, daily_subtotal, weekend_subtotal, subtotal_eur, vat_eur, total_eur, deposit_eur, total_days, weekday_count, weekend_day_count, ip_address, ip_country, rental_type ) values ( p_name, p_email, p_phone, p_vehicle_id, p_vehicle_label, p_date_from, p_date_to, p_message, p_source, v_daily_subtotal, v_weekend_subtotal, v_subtotal_eur, v_vat_eur, v_total_eur, v_deposit_eur, v_total_days, v_weekdays, v_weekend_days, p_ip_address, p_ip_country, v_rental_type ) returning id into v_lead_id; return v_lead_id; end; $$; grant execute on function public.create_lead( text, text, text, uuid, text, date, date, text, text, text, text ) to anon, authenticated, service_role; -- ============================================================================= -- F. Rewrite qualify_lead() RPC -- ============================================================================= create or replace function public.qualify_lead(p_lead_id uuid, p_notes text default '') returns public.customers language plpgsql security invoker as $$ declare v_lead public.leads; v_customer public.customers; v_sales_order public.sales_orders; v_user uuid := auth.uid(); v_order_num text; v_year integer; v_count integer; begin select * into v_lead from public.leads where id = p_lead_id for update; if not found then raise exception 'lead % not found', p_lead_id; end if; if v_lead.status = 'qualified' then select * into v_customer from public.customers where lower(email) = lower(v_lead.email) limit 1; return v_customer; end if; update public.leads set status = 'qualified', is_active = false, qualified_at = now(), qualified_by = v_user, admin_notes = coalesce(nullif(p_notes, ''), admin_notes) where id = v_lead.id; insert into public.customers (lead_id, name, email, phone, notes, created_by) values (v_lead.id, v_lead.name, v_lead.email, v_lead.phone, coalesce(p_notes,''), v_user) on conflict ((lower(email))) do update set name = excluded.name, phone = excluded.phone, notes = case when excluded.notes <> '' then excluded.notes else public.customers.notes end, updated_at = now() returning * into v_customer; v_year := extract(year from now())::integer; select coalesce(count(*), 0) + 1 into v_count from public.sales_orders where extract(year from created_at)::integer = v_year; v_order_num := 'SO-' || v_year || '-' || lpad(v_count::text, 4, '0'); insert into public.sales_orders ( customer_id, lead_id, order_number, private_notes, daily_subtotal, weekend_subtotal, subtotal_eur, vat_eur, total_eur, deposit_eur, total_days, weekday_count, weekend_day_count, date_from, date_to, vehicle_label, rental_type ) values ( v_customer.id, v_lead.id, v_order_num, coalesce(v_lead.admin_notes, ''), coalesce(v_lead.daily_subtotal, 0), coalesce(v_lead.weekend_subtotal, 0), coalesce(v_lead.subtotal_eur, 0), coalesce(v_lead.vat_eur, 0), coalesce(v_lead.total_eur, 0), coalesce(v_lead.deposit_eur, 0), coalesce(v_lead.total_days, 0), coalesce(v_lead.weekday_count, 0), coalesce(v_lead.weekend_day_count, 0), v_lead.date_from, v_lead.date_to, v_lead.vehicle_label, v_lead.rental_type ) returning * into v_sales_order; insert into public.customer_attachments (customer_id, lead_id, sales_order_id, bucket, file_path, file_name, mime_type, kind, created_at) select v_customer.id, la.lead_id, v_sales_order.id, la.bucket, la.file_path, la.file_name, la.mime_type, la.kind, la.created_at from public.lead_attachments la where la.lead_id = v_lead.id and not exists ( select 1 from public.customer_attachments ca where ca.customer_id = v_customer.id and ca.file_path = la.file_path ); insert into public.sales_order_attachments (sales_order_id, bucket, file_path, file_name, mime_type, kind, created_at) select v_sales_order.id, la.bucket, la.file_path, la.file_name, la.mime_type, la.kind, la.created_at from public.lead_attachments la where la.lead_id = v_lead.id; return v_customer; end; $$; -- ============================================================================= -- G. Rewrite notify_lead_qualified() trigger function -- ============================================================================= create or replace function public.notify_lead_qualified() returns trigger language plpgsql security definer as $$ begin -- Skip notification for 'individuell' rental type if NEW.rental_type = 'individuell' then return NEW; end if; perform pg_notify('lead_qualified', json_build_object( 'sales_order_id', NEW.id, 'customer_id', NEW.customer_id, 'lead_id', NEW.lead_id, 'order_number', NEW.order_number, 'total_eur', NEW.total_eur, 'deposit_eur', NEW.deposit_eur, 'date_from', NEW.date_from, 'date_to', NEW.date_to, 'vehicle_label', NEW.vehicle_label, 'rental_type', NEW.rental_type )::text); return NEW; end; $$; -- ============================================================================= -- H. New RPC: sales_order_set_total -- ============================================================================= create or replace function public.sales_order_set_total(p_so_id uuid, p_total_eur integer) returns void language plpgsql security invoker as $$ declare v_so public.sales_orders; begin select * into v_so from public.sales_orders where id = p_so_id for update; if not found then raise exception 'sales order % not found', p_so_id; end if; if v_so.rental_type != 'individuell' then raise exception 'can only set total for individuell orders'; end if; update public.sales_orders set total_eur = p_total_eur, updated_at = now() where id = p_so_id; end; $$; grant execute on function public.sales_order_set_total(uuid, integer) to authenticated; -- ============================================================================= -- I. Final schema reload -- ============================================================================= notify pgrst, 'reload schema';