Files
promocion-por-whatsapp/Base de datos Supabase/Base de datos Postgre Presto.txt
T
2026-05-09 11:12:38 -04:00

1301 lines
40 KiB
Plaintext

***************************************************************************************************************************
********************************************TABLAS*************************************************************************
***************************************************************************************************************************
***************************************************************************************************************************
***************************************************************************************************************************
admin users*
create table public.admin_users (
email text not null,
is_active boolean not null default true,
created_at timestamp with time zone not null default now(),
constraint admin_users_pkey primary key (email)
) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
code_entries
create table public.code_entries (
id bigserial not null,
phone_e164 text not null,
lot_code text not null,
product_type text not null,
coupon_id bigint null,
created_at timestamp with time zone not null default now(),
constraint code_entries_pkey primary key (id),
constraint code_entries_phone_e164_fkey foreign KEY (phone_e164) references users_wa (phone_e164) on delete CASCADE,
constraint code_entries_product_type_check check (
(
product_type = any (array['sachet'::text, 'other'::text])
)
)
) TABLESPACE pg_default;
create index IF not exists code_entries_phone_idx on public.code_entries using btree (phone_e164) TABLESPACE pg_default;
create index IF not exists code_entries_phone_lot_idx on public.code_entries using btree (phone_e164, lot_code) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
coupons
create table public.coupons (
id bigserial not null,
phone_e164 text not null,
coupon_type text not null,
created_at timestamp with time zone not null default now(),
coupon_code text null,
constraint coupons_pkey primary key (id),
constraint coupons_phone_e164_fkey foreign KEY (phone_e164) references users_wa (phone_e164) on delete CASCADE,
constraint coupons_coupon_type_check check (
(
coupon_type = any (array['sachet_10'::text, 'direct_other'::text])
)
)
) TABLESPACE pg_default;
create index IF not exists coupons_phone_idx on public.coupons using btree (phone_e164) TABLESPACE pg_default;
create unique INDEX IF not exists coupons_coupon_code_key on public.coupons using btree (coupon_code) TABLESPACE pg_default;
create trigger trg_set_coupon_code BEFORE INSERT on coupons for EACH row
execute FUNCTION set_coupon_code_before_insert ();
***************************************************************************************************************************
***************************************************************************************************************************
daily_limits
create table public.daily_limits (
phone_e164 text not null,
day date not null,
coupons_created integer not null default 0,
constraint daily_limits_pkey primary key (phone_e164, day),
constraint daily_limits_phone_e164_fkey foreign KEY (phone_e164) references users_wa (phone_e164) on delete CASCADE,
constraint daily_limits_coupons_created_check check ((coupons_created >= 0))
) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
delivery_status_history*
create table public.delivery_status_history (
id bigserial not null,
prize_delivery_id bigint not null,
old_status text null,
new_status text not null,
changed_by text null,
comment text null,
created_at timestamp with time zone not null default now(),
constraint delivery_status_history_pkey primary key (id),
constraint delivery_status_history_prize_delivery_id_fkey foreign KEY (prize_delivery_id) references prize_deliveries (id) on delete CASCADE
) TABLESPACE pg_default;
create index IF not exists idx_delivery_status_history_delivery_id on public.delivery_status_history using btree (prize_delivery_id) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
draws*
create table public.draws (
id bigserial not null,
draw_number integer not null,
draw_date date not null,
notes text null,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now(),
constraint draws_pkey primary key (id),
constraint draws_draw_number_key unique (draw_number)
) TABLESPACE pg_default;
create index IF not exists idx_draws_draw_date on public.draws using btree (draw_date) TABLESPACE pg_default;
create trigger trg_draws_updated_at BEFORE
update on draws for EACH row
execute FUNCTION set_updated_at ();
***************************************************************************************************************************
***************************************************************************************************************************
fraud_signals
create table public.fraud_signals (
id bigserial not null,
signal_type text not null,
phone_e164 text null,
lot_code text null,
details jsonb null,
created_at timestamp with time zone not null default now(),
constraint fraud_signals_pkey primary key (id)
) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
lot_code_audit
create table public.lot_code_audit (
id bigserial not null,
phone_e164 text not null,
raw_input text null,
normalized_input text null,
extracted_code text null,
result_status text not null,
product_type text null,
coupons_generated integer not null default 0,
reason text null,
metadata jsonb null,
created_at timestamp with time zone not null default now(),
constraint lot_code_audit_pkey primary key (id)
) TABLESPACE pg_default;
create index IF not exists lot_code_audit_phone_idx on public.lot_code_audit using btree (phone_e164) TABLESPACE pg_default;
create index IF not exists lot_code_audit_created_idx on public.lot_code_audit using btree (created_at) TABLESPACE pg_default;
create index IF not exists lot_code_audit_code_idx on public.lot_code_audit using btree (extracted_code) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
packs_progress
create table public.packs_progress (
phone_e164 text not null,
sachet_count integer not null default 0,
status text not null default 'open'::text,
updated_at timestamp with time zone not null default now(),
constraint packs_progress_pkey primary key (phone_e164),
constraint packs_progress_phone_e164_fkey foreign KEY (phone_e164) references users_wa (phone_e164) on delete CASCADE,
constraint packs_progress_sachet_count_check check (
(
(sachet_count >= 0)
and (sachet_count <= 10)
)
),
constraint packs_progress_status_check check (
(
status = any (array['open'::text, 'completed'::text])
)
)
) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
prize_deliveries*
create table public.prize_deliveries (
id bigserial not null,
winner_prize_id bigint not null,
delivery_status text not null default 'pending'::text,
delivery_date date null,
delivered_at timestamp with time zone null,
agency text null,
delivered_by text null,
delivery_location text null,
receiver_name text null,
receiver_id text null,
notes text null,
evidence_url text null,
signed_receipt_url text null,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now(),
constraint prize_deliveries_pkey primary key (id),
constraint prize_deliveries_winner_prize_id_key unique (winner_prize_id),
constraint prize_deliveries_winner_prize_id_fkey foreign KEY (winner_prize_id) references winner_prizes (id) on delete CASCADE,
constraint prize_deliveries_delivery_status_check check (
(
delivery_status = any (
array[
'pending'::text,
'contacted'::text,
'scheduled'::text,
'delivered'::text,
'not_delivered'::text,
'cancelled'::text
]
)
)
)
) TABLESPACE pg_default;
create index IF not exists idx_prize_deliveries_status on public.prize_deliveries using btree (delivery_status) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
user_daily_attempts
create table public.user_daily_attempts (
phone_e164 text not null,
day date not null,
total_attempts integer not null default 0,
invalid_attempts integer not null default 0,
duplicate_attempts integer not null default 0,
blocked_attempts integer not null default 0,
constraint user_daily_attempts_pkey primary key (phone_e164, day)
) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
*user_global_roles*
create table public.user_global_roles (
id bigint generated always as identity not null,
user_email text not null,
role text not null,
is_active boolean not null default true,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now(),
constraint user_global_roles_pkey primary key (id),
constraint user_global_roles_user_email_key unique (user_email),
constraint user_global_roles_role_check check ((role = 'admin'::text))
) TABLESPACE pg_default;
create trigger trg_set_updated_at_user_global_roles BEFORE
update on user_global_roles for EACH row
execute FUNCTION set_updated_at_user_global_roles ();
***************************************************************************************************************************
***************************************************************************************************************************
*user_module_roles*
create table public.user_module_roles (
id bigint generated always as identity not null,
user_email text not null,
module text not null,
role text not null,
is_active boolean not null default true,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now(),
constraint user_module_roles_pkey primary key (id),
constraint user_module_roles_unique unique (user_email, module),
constraint user_module_roles_module_check check ((module = 'draws_prizes'::text)),
constraint user_module_roles_role_check check (
(
role = any (array['viewer'::text, 'editor'::text])
)
)
) TABLESPACE pg_default;
create trigger trg_set_updated_at_user_module_roles BEFORE
update on user_module_roles for EACH row
execute FUNCTION set_updated_at_user_module_roles ();
***************************************************************************************************************************
***************************************************************************************************************************
user_rate_limit
create table public.user_rate_limit (
phone_e164 text not null,
last_attempt_at timestamp with time zone null,
blocked_until timestamp with time zone null,
updated_at timestamp with time zone not null default now(),
constraint user_rate_limit_pkey primary key (phone_e164)
) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
users_wa
create table public.users_wa (
phone_e164 text not null,
state text not null default 'CONSENT_PRIVACY'::text,
name text null,
cedula text null,
email text null,
location text null,
consent_privacy boolean not null default false,
consent_marketing boolean not null default false,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now(),
purchase_place text null,
constraint users_wa_pkey primary key (phone_e164)
) TABLESPACE pg_default;
***************************************************************************************************************************
***************************************************************************************************************************
*vw_winners_detail*
create view public.vw_winners_detail as
select
wp.id as winner_prize_id,
u.name as nombre_completo,
u.cedula as id_cedula,
u.phone_e164 as telefono,
wp.prize_name,
wp.prize_type,
wp.prize_amount,
wp.currency,
COALESCE(wp.city, u.location) as ciudad_zona,
d.draw_date as fecha_sorteo,
d.draw_number as no_sorteo,
wp.status as estado_premio,
pd.delivery_status as estado_entrega,
pd.delivery_date as fecha_entrega,
pd.agency as agencia,
pd.delivered_by as entregado_por,
pd.delivery_location,
pd.receiver_name,
pd.receiver_id,
pd.evidence_url,
pd.signed_receipt_url,
wp.created_at
from
winner_prizes wp
join users_wa u on u.phone_e164 = wp.phone_e164
join draws d on d.id = wp.draw_id
left join prize_deliveries pd on pd.winner_prize_id = wp.id
order by
d.draw_date desc,
d.draw_number desc,
wp.id desc;
***************************************************************************************************************************
***************************************************************************************************************************
*winner_prizes*
create table public.winner_prizes (
id bigserial not null,
phone_e164 text not null,
draw_id bigint not null,
prize_type text not null,
prize_name text null,
prize_amount numeric(12, 2) null,
currency text not null default 'NIO'::text,
city text null,
zone text null,
status text not null default 'pending'::text,
source_coupon_id bigint null,
source_code_entry_id bigint null,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now(),
constraint winner_prizes_pkey primary key (id),
constraint winner_prizes_phone_e164_fkey foreign KEY (phone_e164) references users_wa (phone_e164) on update CASCADE,
constraint winner_prizes_draw_id_fkey foreign KEY (draw_id) references draws (id) on delete RESTRICT,
constraint winner_prizes_source_coupon_id_fkey foreign KEY (source_coupon_id) references coupons (id) on delete set null,
constraint winner_prizes_source_code_entry_id_fkey foreign KEY (source_code_entry_id) references code_entries (id) on delete set null,
constraint winner_prizes_status_check check (
(
status = any (
array[
'pending'::text,
'scheduled'::text,
'delivered'::text,
'cancelled'::text,
'expired'::text
]
)
)
)
) TABLESPACE pg_default;
create index IF not exists idx_winner_prizes_phone on public.winner_prizes using btree (phone_e164) TABLESPACE pg_default;
create index IF not exists idx_winner_prizes_draw_id on public.winner_prizes using btree (draw_id) TABLESPACE pg_default;
create index IF not exists idx_winner_prizes_status on public.winner_prizes using btree (status) TABLESPACE pg_default;
create trigger trg_winner_prizes_updated_at BEFORE
update on winner_prizes for EACH row
execute FUNCTION set_updated_at ();
***************************************************************************************************************************
********************************************FUNCIONES**********************************************************************
***************************************************************************************************************************
ensure_user_wa
begin
insert into public.users_wa(phone_e164)
values (p_phone_e164)
on conflict (phone_e164) do update
set updated_at = now();
return query
select u.phone_e164, u.state
from public.users_wa as u
where u.phone_e164 = p_phone_e164;
end;
***************************************************************************************************************************
***************************************************************************************************************************
has_module_role
select exists (
select 1
from public.user_module_roles umr
where lower(umr.user_email) = lower(coalesce(auth.jwt() ->> 'email', ''))
and umr.module = p_module
and umr.role = any(p_roles)
and umr.is_active = true
);
***************************************************************************************************************************
***************************************************************************************************************************
is_global_admin
select exists (
select 1
from public.user_global_roles ugr
where lower(ugr.user_email) = lower(coalesce((select auth.jwt() ->> 'email'), ''))
and ugr.role = 'admin'
and ugr.is_active = true
);
***************************************************************************************************************************
***************************************************************************************************************************
is_lmgomez_admin
select (auth.jwt() ->> 'email') = 'lmgomez@gomezleemarketing.com';
***************************************************************************************************************************
***************************************************************************************************************************
log_delivery_status_change
declare
v_email text;
v_uid text;
v_actor text;
v_claims jsonb;
begin
if old.delivery_status is not distinct from new.delivery_status then
return new;
end if;
-- Intentar leer claims del request JWT
begin
v_claims := nullif(current_setting('request.jwt.claims', true), '')::jsonb;
exception
when others then
v_claims := null;
end;
v_email := coalesce(
v_claims ->> 'email',
auth.jwt() ->> 'email'
);
v_uid := coalesce(
v_claims ->> 'sub',
auth.uid()::text
);
v_actor := coalesce(v_email, v_uid, session_user::text, 'unknown');
insert into public.delivery_status_history (
prize_delivery_id,
old_status,
new_status,
changed_by,
comment
)
values (
new.id,
old.delivery_status,
new.delivery_status,
v_actor,
null
);
return new;
end;
***************************************************************************************************************************
***************************************************************************************************************************
log_lot_code_event
begin
insert into public.lot_code_audit(
phone_e164,
raw_input,
normalized_input,
extracted_code,
result_status,
product_type,
coupons_generated,
reason,
metadata
)
values (
p_phone_e164,
p_raw_input,
p_normalized_input,
p_extracted_code,
p_result_status,
p_product_type,
coalesce(p_coupons_generated, 0),
p_reason,
coalesce(p_metadata, '{}'::jsonb)
);
end;
***************************************************************************************************************************
***************************************************************************************************************************
register_lot_code
declare
v_input text;
v_code text;
v_type text;
v_today date := now()::date;
v_now timestamptz := now();
v_created int;
v_new_count int;
v_coupon_id bigint;
v_same_code_count int;
v_direct_coupon_qty int := 0;
v_total_attempts int;
v_invalid_attempts int;
v_last_attempt_at timestamptz;
v_blocked_until timestamptz;
i int;
begin
out_coupon_created := false;
out_coupon_id := null;
out_coupons_generated := 0;
out_product_type := null;
out_sachet_count := 0;
out_remaining := 10;
-- asegurar fila de rate limit
insert into public.user_rate_limit(phone_e164, last_attempt_at, blocked_until)
values (p_phone_e164, null, null)
on conflict (phone_e164) do nothing;
select last_attempt_at, blocked_until
into v_last_attempt_at, v_blocked_until
from public.user_rate_limit
where phone_e164 = p_phone_e164
for update;
-- bloqueo temporal activo
if v_blocked_until is not null and v_blocked_until > v_now then
out_status := 'spam_blocked';
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
null,
null,
'spam_blocked',
null,
0,
'Usuario bloqueado temporalmente',
jsonb_build_object('blocked_until', v_blocked_until)
);
insert into public.user_daily_attempts(phone_e164, day, blocked_attempts)
values (p_phone_e164, v_today, 1)
on conflict (phone_e164, day)
do update set blocked_attempts = public.user_daily_attempts.blocked_attempts + 1;
return next;
return;
end if;
-- rate limit: 1 intento cada 3 segundos
if v_last_attempt_at is not null and v_last_attempt_at > v_now - interval '3 seconds' then
update public.user_rate_limit
set blocked_until = v_now + interval '5 minutes',
updated_at = v_now
where phone_e164 = p_phone_e164;
insert into public.fraud_signals(signal_type, phone_e164, details)
values (
'user_spam',
p_phone_e164,
jsonb_build_object('reason', 'too_fast', 'last_attempt_at', v_last_attempt_at)
);
out_status := 'spam_blocked';
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
null,
null,
'spam_blocked',
null,
0,
'Intentos demasiado rápidos',
jsonb_build_object('last_attempt_at', v_last_attempt_at)
);
return next;
return;
end if;
update public.user_rate_limit
set last_attempt_at = v_now,
updated_at = v_now
where phone_e164 = p_phone_e164;
-- asegurar contador diario
insert into public.user_daily_attempts(phone_e164, day, total_attempts, invalid_attempts, duplicate_attempts, blocked_attempts)
values (p_phone_e164, v_today, 0, 0, 0, 0)
on conflict (phone_e164, day) do nothing;
update public.user_daily_attempts
set total_attempts = total_attempts + 1
where phone_e164 = p_phone_e164 and day = v_today;
select total_attempts, invalid_attempts
into v_total_attempts, v_invalid_attempts
from public.user_daily_attempts
where phone_e164 = p_phone_e164 and day = v_today;
-- límite duro diario de intentos
if v_total_attempts > 60 then
update public.user_rate_limit
set blocked_until = v_now + interval '1 hour',
updated_at = v_now
where phone_e164 = p_phone_e164;
insert into public.fraud_signals(signal_type, phone_e164, details)
values (
'suspicious_velocity',
p_phone_e164,
jsonb_build_object('total_attempts_today', v_total_attempts)
);
out_status := 'fraud_blocked';
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
null,
null,
'fraud_blocked',
null,
0,
'Exceso de intentos por día',
jsonb_build_object('total_attempts_today', v_total_attempts)
);
return next;
return;
end if;
-- Limpieza inicial
v_input := upper(coalesce(p_lot_code, ''));
v_input := regexp_replace(v_input, '^L\s*:\s*', '', 'g');
v_input := regexp_replace(v_input, '^L\s+', '', 'g');
v_input := regexp_replace(v_input, '\s+', '', 'g');
-- Extraer solo el primer patrón válido de 10 caracteres
v_code := substring(v_input from '([0-9]{4}[A-Z][0-9]{3}[CSMT][0-9])');
if v_code is null then
update public.user_daily_attempts
set invalid_attempts = invalid_attempts + 1
where phone_e164 = p_phone_e164 and day = v_today;
select invalid_attempts
into v_invalid_attempts
from public.user_daily_attempts
where phone_e164 = p_phone_e164 and day = v_today;
if v_invalid_attempts >= 10 then
update public.user_rate_limit
set blocked_until = v_now + interval '1 hour',
updated_at = v_now
where phone_e164 = p_phone_e164;
insert into public.fraud_signals(signal_type, phone_e164, details)
values (
'too_many_invalids',
p_phone_e164,
jsonb_build_object('invalid_attempts_today', v_invalid_attempts)
);
end if;
out_status := 'invalid';
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
v_input,
null,
'invalid',
null,
0,
'No se pudo extraer un lote válido',
jsonb_build_object('invalid_attempts_today', v_invalid_attempts)
);
return next;
return;
end if;
-- Regla de negocio por terminación
if right(v_code, 2) in ('C1','C2','C3','C4') then
v_type := 'sachet';
v_direct_coupon_qty := 0;
elsif right(v_code, 2) in ('S4','M2','T1') then
v_type := 'other';
v_direct_coupon_qty := 5;
else
update public.user_daily_attempts
set invalid_attempts = invalid_attempts + 1
where phone_e164 = p_phone_e164 and day = v_today;
out_status := 'invalid';
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
v_input,
v_code,
'invalid',
null,
0,
'Terminación no participante',
'{}'::jsonb
);
return next;
return;
end if;
-- señal anti-fraude: mismo lote desde muchos teléfonos en poco tiempo
if (
select count(distinct ce.phone_e164)
from public.code_entries ce
where ce.lot_code = v_code
and ce.created_at >= v_now - interval '1 hour'
) >= 15 then
insert into public.fraud_signals(signal_type, lot_code, details)
values (
'lot_spike',
v_code,
jsonb_build_object('window', '1 hour', 'reason', 'many phones')
);
end if;
-- asegurar usuario
insert into public.users_wa(phone_e164)
values (p_phone_e164)
on conflict (phone_e164) do update
set updated_at = now();
-- asegurar progreso
insert into public.packs_progress(phone_e164)
values (p_phone_e164)
on conflict (phone_e164) do nothing;
-- mismo código máximo 20 veces por usuario por día
select count(*)::int
into v_same_code_count
from public.code_entries ce
where ce.phone_e164 = p_phone_e164
and ce.lot_code = v_code
and ce.created_at::date = v_today;
if v_same_code_count >= 20 then
update public.user_daily_attempts
set duplicate_attempts = duplicate_attempts + 1
where phone_e164 = p_phone_e164 and day = v_today;
select pp.sachet_count
into v_new_count
from public.packs_progress pp
where pp.phone_e164 = p_phone_e164;
out_status := 'duplicate';
out_product_type := v_type;
out_sachet_count := coalesce(v_new_count, 0);
out_remaining := greatest(0, 10 - coalesce(v_new_count, 0));
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
v_input,
v_code,
'duplicate',
v_type,
0,
'Máximo 20 veces por día para el mismo lote',
'{}'::jsonb
);
return next;
return;
end if;
-- inicializar límite diario de cupones
insert into public.daily_limits(phone_e164, day, coupons_created)
values (p_phone_e164, v_today, 0)
on conflict (phone_e164, day) do nothing;
-- lock fila límite diario
select dl.coupons_created
into v_created
from public.daily_limits dl
where dl.phone_e164 = p_phone_e164
and dl.day = v_today
for update;
-- registrar lote
insert into public.code_entries(phone_e164, lot_code, product_type)
values (p_phone_e164, v_code, v_type);
-- M2/S4 = 5 cupones directos
if v_type = 'other' then
if v_created + v_direct_coupon_qty > 10 then
select pp.sachet_count
into v_new_count
from public.packs_progress pp
where pp.phone_e164 = p_phone_e164;
out_status := 'limit';
out_product_type := 'other';
out_sachet_count := coalesce(v_new_count, 0);
out_remaining := greatest(0, 10 - coalesce(v_new_count, 0));
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
v_input,
v_code,
'limit',
v_type,
0,
'Límite diario de cupones alcanzado',
jsonb_build_object('current_daily_coupons', v_created)
);
return next;
return;
end if;
for i in 1..v_direct_coupon_qty loop
insert into public.coupons(phone_e164, coupon_type)
values (p_phone_e164, 'direct_other')
returning id into v_coupon_id;
end loop;
update public.daily_limits dl
set coupons_created = dl.coupons_created + v_direct_coupon_qty
where dl.phone_e164 = p_phone_e164
and dl.day = v_today;
select pp.sachet_count
into v_new_count
from public.packs_progress pp
where pp.phone_e164 = p_phone_e164;
out_status := 'ok';
out_product_type := 'other';
out_sachet_count := coalesce(v_new_count, 0);
out_remaining := greatest(0, 10 - coalesce(v_new_count, 0));
out_coupon_created := true;
out_coupon_id := v_coupon_id;
out_coupons_generated := v_direct_coupon_qty;
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
v_input,
v_code,
'ok',
v_type,
out_coupons_generated,
'Registro exitoso de lote other',
'{}'::jsonb
);
return next;
return;
end if;
-- Sachet = acumula hasta 10
select pp.sachet_count
into v_new_count
from public.packs_progress pp
where pp.phone_e164 = p_phone_e164
for update;
v_new_count := coalesce(v_new_count, 0) + 1;
if v_new_count >= 10 then
if v_created >= 10 then
out_status := 'limit';
out_product_type := 'sachet';
out_sachet_count := 10;
out_remaining := 0;
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
v_input,
v_code,
'limit',
v_type,
0,
'Límite diario de cupones alcanzado',
jsonb_build_object('current_daily_coupons', v_created)
);
return next;
return;
end if;
insert into public.coupons(phone_e164, coupon_type)
values (p_phone_e164, 'sachet_10')
returning id into v_coupon_id;
update public.daily_limits dl
set coupons_created = dl.coupons_created + 1
where dl.phone_e164 = p_phone_e164
and dl.day = v_today;
update public.packs_progress pp
set sachet_count = 0,
status = 'open',
updated_at = now()
where pp.phone_e164 = p_phone_e164;
out_status := 'ok';
out_product_type := 'sachet';
out_sachet_count := 10;
out_remaining := 0;
out_coupon_created := true;
out_coupon_id := v_coupon_id;
out_coupons_generated := 1;
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
v_input,
v_code,
'ok',
v_type,
out_coupons_generated,
'Cupón generado por completar 10 sachets',
'{}'::jsonb
);
return next;
return;
else
update public.packs_progress pp
set sachet_count = v_new_count,
status = 'open',
updated_at = now()
where pp.phone_e164 = p_phone_e164;
out_status := 'ok';
out_product_type := 'sachet';
out_sachet_count := v_new_count;
out_remaining := 10 - v_new_count;
out_coupons_generated := 0;
perform public.log_lot_code_event(
p_phone_e164,
p_lot_code,
v_input,
v_code,
'ok',
v_type,
0,
'Sachet acumulado correctamente',
jsonb_build_object('current_sachet_count', v_new_count)
);
return next;
return;
end if;
end;
***************************************************************************************************************************
***************************************************************************************************************************
reset_user_wa
begin
delete from public.code_entries where phone_e164 = p_phone;
delete from public.packs_progress where phone_e164 = p_phone;
delete from public.daily_limits where phone_e164 = p_phone;
delete from public.coupons where phone_e164 = p_phone;
delete from public.users_wa where phone_e164 = p_phone;
end;
***************************************************************************************************************************
***************************************************************************************************************************
restart_wa
declare
v_priv boolean;
begin
select u.consent_privacy into v_priv
from public.users_wa u
where u.phone_e164 = p_phone_e164;
if coalesce(v_priv,false) = true then
update public.users_wa
set state = 'ASK_CODE', updated_at = now()
where phone_e164 = p_phone_e164;
else
update public.users_wa
set state = 'CONSENT_PRIVACY', updated_at = now()
where phone_e164 = p_phone_e164;
end if;
return query
select u.state from public.users_wa u where u.phone_e164 = p_phone_e164;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_consent_marketing_wa
begin
update public.users_wa u
set consent_marketing = p_accept,
state = 'ASK_NAME',
updated_at = now()
where u.phone_e164 = p_phone_e164;
return query
select u.state
from public.users_wa u
where u.phone_e164 = p_phone_e164;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_consent_privacy_wa
begin
update public.users_wa u
set consent_privacy = p_accept,
state = case when p_accept then 'CONSENT_MARKETING' else 'DONE' end,
updated_at = now()
where u.phone_e164 = p_phone_e164;
return query
select u.state
from public.users_wa u
where u.phone_e164 = p_phone_e164;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_coupon_code_before_insert
declare
v_num bigint;
v_num_text text;
begin
if new.coupon_code is null then
v_num := nextval('public.coupons_coupon_code_seq');
v_num_text := v_num::text;
if length(v_num_text) < 4 then
v_num_text := lpad(v_num_text, 4, '0');
end if;
new.coupon_code := 'PRE-CUP-' || v_num_text;
end if;
return new;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_email_wa
declare
v_email text;
begin
v_email := nullif(lower(trim(coalesce(p_email,''))), '');
update public.users_wa u
set email = v_email,
state = 'ASK_LOCATION',
updated_at = now()
where u.phone_e164 = p_phone_e164;
return query
select u.state
from public.users_wa u
where u.phone_e164 = p_phone_e164;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_id_wa
declare
v_id text;
begin
v_id := trim(coalesce(p_cedula,''));
update public.users_wa u
set cedula = v_id,
state = 'ASK_EMAIL',
updated_at = now()
where u.phone_e164 = p_phone_e164;
return query
select u.state
from public.users_wa u
where u.phone_e164 = p_phone_e164;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_location_wa
declare
v_loc text;
begin
v_loc := trim(regexp_replace(coalesce(p_location,''), '\s+', ' ', 'g'));
update public.users_wa u
set location = v_loc,
state = 'ASK_PURCHASE_PLACE',
updated_at = now()
where u.phone_e164 = p_phone_e164;
return query
select u.state
from public.users_wa u
where u.phone_e164 = p_phone_e164;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_name_wa
declare
v_name text;
begin
-- Limpieza básica
v_name := trim(regexp_replace(coalesce(p_name,''), '\s+', ' ', 'g'));
update public.users_wa u
set name = v_name,
state = 'ASK_ID',
updated_at = now()
where u.phone_e164 = p_phone_e164;
return query
select u.state
from public.users_wa u
where u.phone_e164 = p_phone_e164;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_purchase_place_wa
declare
v_place text;
begin
v_place := trim(regexp_replace(coalesce(p_purchase_place,''), '\s+', ' ', 'g'));
update public.users_wa u
set purchase_place = v_place,
state = 'ASK_CODE',
updated_at = now()
where u.phone_e164 = p_phone_e164;
return query
select u.state
from public.users_wa u
where u.phone_e164 = p_phone_e164;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_state_wa
begin
update public.users_wa u
set state = p_state,
updated_at = now()
where u.phone_e164 = p_phone_e164;
return query select u.state from public.users_wa u where u.phone_e164 = p_phone_e164;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_updated_at
begin
new.updated_at = now();
return new;
end;
***************************************************************************************************************************
***************************************************************************************************************************
set_updated_at_user_global_roles
begin
new.updated_at = now();
return new;
end;
set_updated_at_user_module_roles
begin
new.updated_at = now();
return new;
end;
***************************************************************************************************************************
***************************************************************************************************************************
whoami
select json_build_object(
'role', auth.role(),
'email', auth.jwt() ->> 'email',
'sub', auth.uid()
);