*************************************************************************************************************************** ********************************************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() );