1301 lines
40 KiB
Plaintext
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()
|
|
);
|
|
|