feat: Add manual email sending workflow and related database changes

- Implemented a new n8n workflow for manual email sending, including webhook trigger, order data fetching, email building, and sending.
- Added logic to format email content with customer and order details.
- Introduced new columns in the sales_orders table to track email sending status.
- Updated database functions to handle new rental types and email status.
- Created new RPCs for updating email status and retrieving email details for sales orders.
This commit is contained in:
2026-05-17 18:04:36 +02:00
parent e24bc743e2
commit e34d56e36a
13 changed files with 1127 additions and 106 deletions
@@ -0,0 +1,209 @@
-- 12-email-sent-and-more.sql
-- Add email_sent column to sales_orders, update notify_lead_qualified() to include
-- rental_type and email_sent, update qualify_lead() to set email_sent=0,
-- add sales_order_update_email_sent and sales_order_get_email_details RPCs.
-- Idempotent.
-- =============================================================================
-- A. Add email_sent to sales_orders
-- =============================================================================
alter table public.sales_orders add column if not exists email_sent integer not null default 0;
create index if not exists sales_orders_email_sent_idx on public.sales_orders (email_sent);
-- =============================================================================
-- B. Update notify_lead_qualified() trigger function
-- (defined in 10-mietvertrag-workflow.sql, overridden by 11-consolidate-km-rental.sql)
-- Since migration 12 runs after 11, this is the final version.
-- =============================================================================
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,
'email_sent', NEW.email_sent
)::text);
return NEW;
end;
$$;
-- =============================================================================
-- C. Update qualify_lead() RPC
-- Add email_sent = 0 to the sales_orders insert.
-- =============================================================================
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, email_sent
) 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, 0
) 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;
$$;
-- =============================================================================
-- D. New RPC: sales_order_update_email_sent
-- =============================================================================
create or replace function public.sales_order_update_email_sent(p_so_id uuid, p_status 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 p_status not in (0, 1, 2) then
raise exception 'invalid email_sent status: %', p_status;
end if;
update public.sales_orders
set email_sent = p_status, updated_at = now()
where id = p_so_id;
end;
$$;
grant execute on function public.sales_order_update_email_sent(uuid, integer) to authenticated;
-- =============================================================================
-- E. New RPC: sales_order_get_email_details
-- =============================================================================
create or replace function public.sales_order_get_email_details(p_so_id uuid)
returns jsonb
language plpgsql
security definer
as $$
declare
v_result jsonb;
begin
select jsonb_build_object(
'order_number', so.order_number,
'total_eur', so.total_eur,
'deposit_eur', so.deposit_eur,
'date_from', so.date_from,
'date_to', so.date_to,
'vehicle_label', so.vehicle_label,
'customer_name', c.name,
'customer_email', c.email,
'customer_phone', c.phone,
'daily_subtotal', so.daily_subtotal,
'weekend_subtotal', so.weekend_subtotal,
'subtotal_eur', so.subtotal_eur,
'vat_eur', so.vat_eur,
'total_days', so.total_days,
'weekday_count', so.weekday_count,
'weekend_day_count', so.weekend_day_count
) into v_result
from public.sales_orders so
join public.customers c on c.id = so.customer_id
where so.id = p_so_id;
if v_result is null then
raise exception 'sales order % not found', p_so_id;
end if;
return v_result;
end;
$$;
grant execute on function public.sales_order_get_email_details(uuid) to authenticated;
-- =============================================================================
-- F. Final schema reload
-- =============================================================================
notify pgrst, 'reload schema';