commit 232facf1e8e1abbed23853f602ff12b955dc94e6 Author: Eidan T. Date: Sat May 9 11:12:38 2026 -0400 first commit diff --git a/Base de datos Supabase/Base de datos Postgre Presto.txt b/Base de datos Supabase/Base de datos Postgre Presto.txt new file mode 100644 index 0000000..5497a9d --- /dev/null +++ b/Base de datos Supabase/Base de datos Postgre Presto.txt @@ -0,0 +1,1300 @@ +*************************************************************************************************************************** +********************************************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() + ); + diff --git a/Base de datos Supabase/Triggers.png b/Base de datos Supabase/Triggers.png new file mode 100644 index 0000000..a46d8c5 Binary files /dev/null and b/Base de datos Supabase/Triggers.png differ diff --git a/Base de datos Supabase/pg-dump-postgres1-1776492006.dmp b/Base de datos Supabase/pg-dump-postgres1-1776492006.dmp new file mode 100644 index 0000000..b0a1d2f Binary files /dev/null and b/Base de datos Supabase/pg-dump-postgres1-1776492006.dmp differ diff --git a/Flujo n8n/Flujo WhatsApp.docx b/Flujo n8n/Flujo WhatsApp.docx new file mode 100644 index 0000000..374e5dd Binary files /dev/null and b/Flujo n8n/Flujo WhatsApp.docx differ diff --git a/Flujo n8n/Flujo con WSP.json b/Flujo n8n/Flujo con WSP.json new file mode 100644 index 0000000..8f518cc --- /dev/null +++ b/Flujo n8n/Flujo con WSP.json @@ -0,0 +1,3020 @@ +{ + "name": "Flujo con WSP Cliente Final", + "nodes": [ + { + "parameters": { + "jsCode": "const item = $json;\n\nconst value = item.entry?.[0]?.changes?.[0]?.value ?? item.value ?? item;\nconst msg = value.messages?.[0];\nconst contact = value.contacts?.[0];\n\n// Lista de números bloqueados SIN el +\n// Ejemplo: \"18095551234\"\nconst blockedNumbers = new Set([\n '50769808472'\n]);\n\nif (!msg) {\n return [{\n json: {\n ignore: true,\n reason: 'No hay mensaje entrante'\n }\n }];\n}\n\nconst incomingNumber = String(msg.from || '').trim();\n\n// Bloqueo por número\nif (blockedNumbers.has(incomingNumber)) {\n return [{\n json: {\n ignore: true,\n blocked: true,\n reason: 'Número bloqueado',\n phone_e164: incomingNumber ? `+${incomingNumber}` : '',\n number_no_plus: incomingNumber,\n wa_message_id: msg.id || null,\n wa_profile_name: contact?.profile?.name || null,\n wa_id: contact?.wa_id || incomingNumber || null,\n raw_type: msg.type || null\n }\n }];\n}\n\nlet text = '';\n\nif (msg.type === 'text') {\n text = msg.text?.body || '';\n} else if (msg.type === 'interactive') {\n text =\n msg.interactive?.button_reply?.title ||\n msg.interactive?.list_reply?.title ||\n msg.interactive?.button_reply?.id ||\n msg.interactive?.list_reply?.id ||\n '';\n} else if (msg.type === 'button') {\n text = msg.button?.text || '';\n} else if (msg.type === 'image') {\n text = msg.image?.caption || '';\n} else if (msg.type === 'document') {\n text = msg.document?.caption || '';\n}\n\nreturn [{\n json: {\n ignore: false,\n blocked: false,\n phone_e164: incomingNumber ? `+${incomingNumber}` : '',\n number_no_plus: incomingNumber,\n text: String(text).trim(),\n wa_message_id: msg.id || null,\n wa_profile_name: contact?.profile?.name || null,\n wa_id: contact?.wa_id || incomingNumber || null,\n raw_type: msg.type || null\n }\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -1072, + 0 + ], + "id": "45ccfd7e-a6b6-4769-92de-7beac6990f37", + "name": "Code in JavaScript" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "252f7a30-e434-4174-995b-24184c251e59", + "leftValue": "={{$json.ignore}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + -656, + 0 + ], + "id": "2ab173bf-1296-4567-b7df-52a397fd1cc6", + "name": "If" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/ensure_user_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{$('If').item.json.phone_e164}}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + -96, + 16 + ], + "id": "91d38654-07b6-4208-ac5f-6b20cb48ee39", + "name": "HTTP Request — Supabase ensure_user_wa", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $('HTTP Request — Supabase ensure_user_wa').item.json.out_state }}", + "rightValue": "CONSENT_PRIVACY", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "b492a8a6-5e12-43ac-bb29-397f662344e8" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CONSENT_PRIVACY" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "3f468b94-6665-4682-92ac-69415af6a433", + "leftValue": "={{ $('HTTP Request — Supabase ensure_user_wa').item.json.out_state }}", + "rightValue": "CONSENT_MARKETING", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CONSENT_MARKETING" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dab531c7-8ce1-4152-b85d-3f85463dea61", + "leftValue": "={{ $('HTTP Request — Supabase ensure_user_wa').item.json.out_state }}", + "rightValue": "ASK_NAME", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ASK_NAME" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "b9ea1510-6e50-49be-9157-0b7c0e695e79", + "leftValue": "={{ $('HTTP Request — Supabase ensure_user_wa').item.json.out_state }}", + "rightValue": "ASK_ID", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ASK_ID" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "2f92c80f-8151-4353-9c65-d9bc1b562278", + "leftValue": "={{ $('HTTP Request — Supabase ensure_user_wa').item.json.out_state }}", + "rightValue": "ASK_EMAIL", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ASK_EMAIL" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "b41e6e03-6e10-4d5c-9f9d-2ace8c4ea829", + "leftValue": "={{ $('HTTP Request — Supabase ensure_user_wa').item.json.out_state }}", + "rightValue": "ASK_LOCATION", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ASK_LOCATION" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "e7e1410f-eff9-42b9-b237-f5bbe727dbe8", + "leftValue": "={{ $('HTTP Request — Supabase ensure_user_wa').item.json.out_state }}", + "rightValue": "ASK_PURCHASE_PLACE", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ASK_PURCHASE_PLACE" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "2787bf16-fdde-4f39-9fe8-032def79943b", + "leftValue": "={{ $('HTTP Request — Supabase ensure_user_wa').item.json.out_state }}", + "rightValue": "ASK_CODE", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ASK_CODE" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "f5fe2ce0-94e1-4120-b35f-808491a0767a", + "leftValue": "={{ $('HTTP Request — Supabase ensure_user_wa').item.json.out_state }}", + "rightValue": "MENU_AFTER_CODE", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "MENU_AFTER_CODE" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "cfef3dcb-555f-4dbb-aae4-cfe7caf7829d", + "leftValue": "={{ $('HTTP Request — Supabase ensure_user_wa').item.json.out_state }}", + "rightValue": "DONE", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "DONE" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 288, + 32 + ], + "id": "7a50baa7-6289-4721-8978-4ca25dd97eaf", + "name": "State Router" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "ef9abd6c-28ba-4432-bcbe-ff195df3ceec", + "leftValue": "={{$json.isValid}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 784, + 448 + ], + "id": "b7c25b08-e17b-449e-a86c-9fb8658c8bb8", + "name": "If1" + }, + { + "parameters": { + "jsCode": "const text = ($node[\"Code in JavaScript\"].json.text || \"\").trim().toLowerCase();\nconst t = text.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\");\n\nconst yes = [\"si\", \"s\", \"acepto\", \"ok\", \"de acuerdo\", \"✅ si\"];\nconst no = [\"no\", \"n\", \"rechazo\", \"no acepto\", \"❌ no\"];\n\nlet intent = \"prompt\";\nif (yes.includes(t)) intent = \"accept\";\nelse if (no.includes(t)) intent = \"reject\";\n\nreturn [{ intent, normalized: t, raw: text }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 672, + -96 + ], + "id": "2fc600fe-9677-4903-85aa-ea28d28626cb", + "name": "normalize_answer_privacy" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{$json.intent}}", + "rightValue": "prompt", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "0b4db2d9-865d-4fbd-b351-022a3e9a30a7" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "prompt" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "90094f76-7e7b-4f59-a119-ade3e16931ec", + "leftValue": "={{$json.intent}}", + "rightValue": "accept", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "accept" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "2e0cdd92-02ae-44c0-9bc1-06c2cdf72c93", + "leftValue": "={{$json.intent}}", + "rightValue": "reject", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "reject" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 864, + -80 + ], + "id": "1e295af6-1bbf-4eee-aaa7-98ba6b70c859", + "name": "Intent Router Privacy" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_consent_privacy_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"p_accept\": true\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1072, + 48 + ], + "id": "dfd18231-4f41-47fa-94f0-3fe0cdb0c859", + "name": "set_consent_privacy_wa", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "jsCode": "const text = ($node[\"Code in JavaScript\"].json.text || \"\").trim().toLowerCase();\nconst t = text.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\").replace(/[^\\w\\s]/g, \"\");\n\nlet intent = \"prompt\";\nif (/^(si|s|acepto|ok|de acuerdo| si)$/.test(t)) intent = \"accept\";\nelse if (/^(no|n|rechazo|no acepto| no)$/.test(t)) intent = \"reject\";\n\nreturn [{ intent, normalized: t, raw: text }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 672, + 48 + ], + "id": "8f8e80af-9d8e-4530-a910-a877e52becc6", + "name": "normalize_answer_marketing" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{$json.intent}}", + "rightValue": "prompt", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "b663102c-fb9d-43a3-b627-4e57ed5c0a5c" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "prompt" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "4859f59c-ff58-421f-aedf-618f150fab11", + "leftValue": "={{$json.intent}}", + "rightValue": "accept", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "accept" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "e81b8b3f-4370-4569-9c9a-05601d366570", + "leftValue": "={{$json.intent}}", + "rightValue": "reject", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "reject" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 832, + 240 + ], + "id": "17f15cfd-f0bd-4f14-9e02-d8b704f3aca0", + "name": "Intent Router Marketing" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_consent_marketing_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"p_accept\": true\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1104, + 240 + ], + "id": "91eaa2b5-22dc-44cb-828e-11c131fff648", + "name": "RPC set_consent_marketing_wa (true)", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f03e65d0-8615-4437-9995-bb56c60149f4", + "name": "reply", + "value": "=Ingresa tu nombre y apellido", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1312, + 288 + ], + "id": "023736c2-7b07-4fad-9c06-5d8405d8cc0c", + "name": "reply_promo01" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_consent_marketing_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"p_accept\": false\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1104, + 432 + ], + "id": "57992276-8e7d-4ecd-aafe-d89316485160", + "name": "RPC set_consent_marketing_wa (true)1", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "jsCode": "const raw = ($node[\"Code in JavaScript\"].json.text || \"\").trim();\n\n// Limpieza: quita espacios repetidos\nconst name = raw.replace(/\\s+/g, \" \").trim();\n\n// Reglas mínimas:\nconst minLen = 6;\nconst hasTwoWords = name.split(\" \").filter(Boolean).length >= 2;\n// Solo letras/espacios (acepta tildes y ñ)\nconst okChars = /^[A-Za-zÁÉÍÓÚÜÑáéíóúüñ\\s]+$/.test(name);\n\nconst isValid = name.length >= minLen && hasTwoWords && okChars;\n\nreturn [{ isValid, name, raw }];;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 656, + 304 + ], + "id": "13ec35d9-dd67-4140-9ce8-3533bf9a7c3d", + "name": "validate_name" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_name_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"p_name\": \"{{$json.name}}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1104, + 640 + ], + "id": "078fb8a1-cd3b-4697-85fd-9495173f2996", + "name": "RPC set_name_wa", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f03e65d0-8615-4437-9995-bb56c60149f4", + "name": "reply", + "value": "=Ingresa tu número de cédula o pasaporte", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1328, + 512 + ], + "id": "7a42e645-aa71-4473-abbb-00b4e67109f6", + "name": "reply_promo02" + }, + { + "parameters": { + "jsCode": "const raw = ($node[\"Code in JavaScript\"].json.text || \"\").trim();\n\n// reemplaza saltos de línea por espacio\nconst cedula = raw.replace(/\\r?\\n/g, \" \").trim();\n\nconst isValid = cedula.length > 0;\n\nreturn [{\n isValid,\n cedula,\n raw\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 528, + 560 + ], + "id": "abacdb70-9855-4885-b63e-7249c42673d6", + "name": "validate_id" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "ef9abd6c-28ba-4432-bcbe-ff195df3ceec", + "leftValue": "={{$json.isValid}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 784, + 608 + ], + "id": "bdbc1930-1cb4-4686-b6d2-bae3ed02cfdd", + "name": "If2" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_id_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"p_cedula\": \"{{$json.cedula}}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 928, + 720 + ], + "id": "181869a0-96da-4ce0-bab2-40ee7e3fb9a4", + "name": "RPC set_id_wa", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f03e65d0-8615-4437-9995-bb56c60149f4", + "name": "reply", + "value": "=Ingresa tu correo electrónico 📧 o ingresa NO para continuar.", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1472, + 800 + ], + "id": "fa7c12a2-eaf1-418f-97dd-cab1e7e1c7a5", + "name": "reply_promo03" + }, + { + "parameters": { + "jsCode": "const raw = ($node[\"Code in JavaScript\"].json.text || \"\").trim();\nconst text = raw.toLowerCase();\n\nconst skipWords = [\n \"no\",\n \"no tengo\",\n \"ninguno\",\n \"omitir\",\n \"skip\",\n \"na\",\n \"n/a\"\n];\n\nconst email = raw.toLowerCase();\nconst isSkip = skipWords.includes(text);\nconst isValidEmail = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/.test(email);\n\nreturn [{\n isValid: isSkip || isValidEmail,\n isSkip,\n email: isSkip ? null : email,\n raw\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 624, + 896 + ], + "id": "81e4f18a-2fff-4a7b-9ebd-9136fb8747de", + "name": "validate_email" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "ef9abd6c-28ba-4432-bcbe-ff195df3ceec", + "leftValue": "={{$json.isValid}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 784, + 896 + ], + "id": "072d618d-baf9-4ba5-a258-7d5bb242e042", + "name": "If3" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_email_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{$node['Code in JavaScript'].json.phone_e164}}\",\n \"p_email\": \"{{$json.email}}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1264, + 960 + ], + "id": "3574b4f8-a157-4edb-b36c-a09da79fe92f", + "name": "RPC set_id_wa1", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/register_lot_code", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{$node['Code in JavaScript'].json.phone_e164}}\",\n \"p_lot_code\": \"{{$node['validate_lot_code_format'].json.lot_code}}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1008, + 1232 + ], + "id": "c5ccc548-1143-4566-a67b-689d334ad9ce", + "name": "RPC register_lot_code", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "jsCode": "const r = Array.isArray($json) ? $json[0] : $json;\nconst qty = r.out_coupons_generated || 1;\n\nlet reply = \"\";\nlet nextState = \"ASK_CODE\"; // por defecto\n\nif (r.out_status === \"invalid\") {\n reply = `❌ Tu código es incorrecto o estás ingresando el código de un producto no participante.\n\n✔️Verifica nuestros parámetros de control y productos participantes en nuestro reglamento en:\nhttps://promos.nestlecam.com/extrarealespresto/terminos-y-condiciones\n\n¿Deseas ingresar otro código?`;\n nextState = \"ASK_CODE\";\n\n} else if (r.out_status === \"duplicate\") {\n reply = `⚠️ Ya ingresaste este código el máximo permitido de 20 veces por día. \n\n🕔Espera 24 horas para volver a ingresar este código.\n\n¿Deseas ingresar otro código?`;\n nextState = \"ASK_CODE\";\n\n} else if (r.out_status === \"limit\") {\n reply = `🚫 Has alcanzado el límite diario de participación.\nIntenta nuevamente mañana.`;\n nextState = \"ASK_CODE\";\n\n}else if (r.out_status === \"spam_blocked\") {\n reply = `🚫 Has enviado intentos demasiado rápido.\n\nPor favor espera unos minutos antes de intentar nuevamente.`;\n nextState = \"ASK_CODE\";\n\n} else if (r.out_status === \"fraud_blocked\") {\n reply = `🚫 Detectamos actividad inusual en tu participación.\n\nPor favor intenta más tarde o contacta soporte si el problema persiste.`;\n nextState = \"ASK_CODE\";\n\n\n} else if (r.out_status === \"ok\") {\n if (r.out_coupon_created) {\n reply = `🎉 ¡Excelente!\n\nHas generado ${qty} cupón${qty > 1 ? \"es\" : \"\"} de participación 🎟️.\n\nYa estás participando en los sorteos Extra Reales Presto ☕️.\n\nConsulta en nuestras redes 📱[ingresar link instagram] la fecha del próximo sorteo. Los ganadores serán contactados el día del sorteo.\n\n💭 Recuerda guardar los empaques participantes para poder redimir tu premio en caso de resultar ganador. \nSi tienes dudas 🤔adicionales por favor llámanos al 📞: 1-800-4000 o consulta nuestra página de preguntas frecuentes.\n\n¿Deseas registrar otro código?`;\n nextState = \"MENU_AFTER_CODE\";\n\n } else {\n reply = `✅ Código registrado correctamente.\n\nHas ingresado ${r.out_sachet_count} de 10 códigos de churritos/sticks ☕️.\n\n¿Deseas ingresar otro código?`;\n nextState = \"MENU_AFTER_CODE\";\n }\n\n} else {\n reply = `Ocurrió un problema. Intenta nuevamente.\n\ndebes responder si o no para registrar un nuevo código\n\n¿Deseas ingresar otro código?`;\n nextState = \"ASK_CODE\";\n}\n\nreturn [{ reply, nextState, r }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1200, + 1232 + ], + "id": "69f92ccf-f210-4dc9-86a7-d7524a9831be", + "name": "build_reply_from_register" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_state_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"p_state\": \"{{$json.nextState}}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1376, + 1232 + ], + "id": "037bcc33-ec15-4fd1-a41f-bba2c037ca57", + "name": "RPC register_lot_code1", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f03e65d0-8615-4437-9995-bb56c60149f4", + "name": "reply", + "value": "={{$node['build_reply_from_register'].json.reply}}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1760, + 1264 + ], + "id": "d5bb7dbd-3555-475a-ac9a-0efbce958bb2", + "name": "reply_code" + }, + { + "parameters": { + "jsCode": "const raw = ($node[\"Code in JavaScript\"].json.text || \"\").trim().toLowerCase();\nconst t = raw.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\").replace(/[^\\w\\s]/g, \"\");\n\nlet choice = null;\n\nif (t === \"1\" || t === \"uno\" || t === \"si\" || t === \"sí\" || t === \" si\" || t.includes(\"seguir\")) choice = 1;\nif (t === \"2\" || t === \"dos\" || t === \"no\" || t === \" no\" || t.includes(\"salir\")) choice = 2;\n\nreturn [{ choice, raw, normalized: t }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 288, + 736 + ], + "id": "a0fe1585-29f0-48eb-ae7b-121a4e9b5b9f", + "name": "parse_menu_choice" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{$json.choice}}", + "rightValue": 1, + "operator": { + "type": "number", + "operation": "equals" + }, + "id": "22a7e215-a50c-4ca3-98f9-edfa69c4ff16" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "1" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "2fc0d3df-8250-4747-a7f2-07f39bb4e89e", + "leftValue": "={{$json.choice}}", + "rightValue": 2, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "2" + } + ] + }, + "options": { + "fallbackOutput": "extra" + } + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 416, + 1280 + ], + "id": "df0098db-9d77-4a3c-8d37-3f5c5b6b4112", + "name": "Menu Router" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_state_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"p_state\": \"ASK_CODE\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 720, + 1248 + ], + "id": "8f587606-f3a4-4961-868a-a67d3d9a5126", + "name": "RPC set_state_wa (ASK_CODE)", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f03e65d0-8615-4437-9995-bb56c60149f4", + "name": "reply", + "value": "=Perfecto ✅\nIngresa tu próximo código de lote", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1248, + 1472 + ], + "id": "e33cc477-ab3a-4c65-abeb-b2b84975a872", + "name": "reply_code1" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_state_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"p_state\": \"DONE\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 736, + 1488 + ], + "id": "d723f160-8b09-45f5-8720-922fa73fcbbe", + "name": "RPC set_state_wa (DONE)", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f03e65d0-8615-4437-9995-bb56c60149f4", + "name": "reply", + "value": "=👌 Tu progreso queda guardado.\n\nPuedes continuar ingresando tus códigos escribiendo *Hola* a este mismo número. \n\n¡Te esperamos! ☕✨\n", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1248, + 1616 + ], + "id": "df0cc0be-db85-446c-928b-eb2d177e3a3a", + "name": "reply_code2" + }, + { + "parameters": { + "jsCode": "const raw = ($node[\"Code in JavaScript\"].json.text || \"\").trim().toLowerCase();\nconst t = raw.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\").replace(/[^\\w\\s]/g, \"\");\n\nconst isHello = [\"hola\", \"buenas\", \"buenos dias\", \"buenas tardes\", \"buenas noches\", \"hi\", \"hello\"]\n .includes(t);\n\nreturn [{ isHello, raw, normalized: t }];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 528, + 2016 + ], + "id": "dfdba68b-d5fa-4447-a09a-739af7418770", + "name": "detect_hello" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "558979b6-6e59-488f-adbb-dbb095c6adf3", + "leftValue": "={{$json.isHello}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 736, + 2016 + ], + "id": "3d6acb74-e513-4606-951c-7507fec1dec3", + "name": "isHello?" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/restart_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1040, + 2000 + ], + "id": "2650fa78-abbc-465d-b891-1f773aa34c9f", + "name": "RPC restart_wa", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $json.out_state }}", + "rightValue": "ASK_CODE", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "1f59bd91-2f21-4f24-82b6-dbc21e8db4b0" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ASK_CODE" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "12f763d2-5efb-4e33-b463-f3aa557af106", + "leftValue": "={{ $json.out_state }}", + "rightValue": "CONSENT_PRIVACY", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CONSENT_PRIVACY" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 1248, + 2000 + ], + "id": "b92a89a5-f40e-4a4d-b3a9-a99f680c333a", + "name": "Restart State Router" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f03e65d0-8615-4437-9995-bb56c60149f4", + "name": "reply", + "value": "=👋 ¡Bienvenido de nuevo!\nPara iniciar nuevamente, escribe: Hola", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1536, + 1472 + ], + "id": "5873147f-9c84-48e0-9e8c-23d593d3469f", + "name": "reply_done_hint" + }, + { + "parameters": { + "jsCode": "const raw = ($node[\"Code in JavaScript\"].json.text || \"\").trim();\n\n// normaliza: sin tildes, lower, espacios simples\nconst norm = raw\n .toLowerCase()\n .normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n\n// catálogo permitido (normalizado)\nconst allowed = new Map([\n [\"boaco\", \"Boaco\"],\n [\"carazo\", \"Carazo\"],\n [\"chinandega\", \"Chinandega\"],\n [\"chontales\", \"Chontales\"],\n [\"esteli\", \"Estelí\"],\n [\"granada\", \"Granada\"],\n [\"jinotega\", \"Jinotega\"],\n [\"leon\", \"León\"],\n [\"madriz\", \"Madriz\"],\n [\"managua\", \"Managua\"],\n [\"masaya\", \"Masaya\"],\n [\"matagalpa\", \"Matagalpa\"],\n [\"nueva segovia\", \"Nueva Segovia\"],\n [\"rio san juan\", \"Río San Juan\"],\n [\"rivas\", \"Rivas\"],\n [\"raccn\", \"RACCN\"],\n [\"raccs\", \"RACCS\"],\n [\"raccst\", \"RACCSt\"], // por si te lo escriben así\n]);\n\n// alias útiles\nconst aliases = new Map([\n [\"rac cn\", \"raccn\"],\n [\"rac cs\", \"raccs\"],\n [\"rac c n\", \"raccn\"],\n [\"rac c s\", \"raccs\"],\n [\"rio sanjuan\", \"rio san juan\"],\n [\"san juan\", \"rio san juan\"],\n]);\n\nconst key = aliases.get(norm) || norm;\nconst canonical = allowed.get(key) || null;\n\nreturn [{\n isValid: !!canonical,\n location: canonical,\n raw,\n normalized: norm,\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 272, + 464 + ], + "id": "aafa3aa0-f366-4630-9d68-068aa8a0aaad", + "name": "validate_location" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "cfc0d873-5f21-4dbe-a2a9-94f77ccff406", + "leftValue": "={{$json.isValid}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + -240, + 880 + ], + "id": "417a63a4-bd56-41ea-a7c6-d229397e6da9", + "name": "If4" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_location_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"p_location\": \"{{$json.location}}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1744, + 752 + ], + "id": "9cff8253-b6ca-45bb-9ec8-3a33e904fd2c", + "name": "RPC set_location_wa", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "6686534c-1dac-41b9-bf2d-9b8d82d2eef9", + "name": "reply", + "value": "=Para participar en nuestra promocion debe ser mayor de edad y estar de acuerdo con la Politica de Privacidad de Nestlé®", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1264, + -32 + ], + "id": "5c0472ed-c2e5-40aa-b489-e828de1e6ac5", + "name": "reply_reject" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f03e65d0-8615-4437-9995-bb56c60149f4", + "name": "reply", + "value": "=No podemos reconocer tu respuesta. Por favor responde solo enviando tu número de identificación.", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1472, + 624 + ], + "id": "554e18a5-aa2d-4ed0-9ae8-f41ceb88778e", + "name": "reply_ID" + }, + { + "parameters": { + "operation": "send", + "phoneNumberId": "1053542944519088", + "recipientPhoneNumber": "={{ $('Code in JavaScript').item.json.phone_e164 }}", + "textBody": "={{ $json.reply }}", + "additionalFields": {} + }, + "type": "n8n-nodes-base.whatsApp", + "typeVersion": 1.1, + "position": [ + 2576, + 176 + ], + "id": "463c3a03-f0d5-40d8-bbf2-2f53bfbf57a9", + "name": "Send message", + "webhookId": "46502e2f-9d44-464b-8459-804220a8efc4", + "credentials": { + "whatsAppApi": { + "id": "bY7IrSgWwSg0qBkX", + "name": "Oficial Nestle" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/1053542944519088/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"type\": \"interactive\",\n \"interactive\": {\n \"type\": \"button\",\n \"header\": {\n \"type\": \"image\",\n \"image\": {\n \"link\": \"https://digitalcompass.agency/wp-content/uploads/2026/03/PRESTO-EXTRA-REALES-1-e1774883338675.jpg\"\n }\n },\n \"body\": {\n \"text\": \"*Registro Promo*\\n\\n¡Bienvenid@ a la promoción Extra Reales Presto ☕🔥, donde puedes participar por más de C$1,400,000 de premios en efectivo. 💵\\n\\nVer reglamento en:\\n\\npromos.nestlecam.com/extrarealespresto/terminos-y-condiciones\\n\\nA continuación responde SI o NO, \\\"Declara\\\" que eres mayor de edad y estás de acuerdo con la Política de Privacidad de Nestlé® https://www.nestle-centroamerica.com/politica-de-privacidad\"\n },\n \"footer\": {\n \"text\": \"Presto\"\n },\n \"action\": {\n \"buttons\": [\n {\n \"type\": \"reply\",\n \"reply\": {\n \"id\": \"SI\",\n \"title\": \"✅ Si\"\n }\n },\n {\n \"type\": \"reply\",\n \"reply\": {\n \"id\": \"NO\",\n \"title\": \"❌ No\"\n }\n }\n ]\n }\n }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1904, + -240 + ], + "id": "7d34ba59-8fd4-4ee5-9ad8-16131424137c", + "name": "HTTP Request", + "credentials": { + "httpHeaderAuth": { + "id": "xwpr4wSQ8IOc6Plo", + "name": "NestleOficial" + } + } + }, + { + "parameters": { + "jsCode": "const raw = ($node[\"Code in JavaScript\"].json.text || \"\").trim().toUpperCase();\n\n// Limpieza general\nconst cleaned = raw\n .replace(/^L\\s*:\\s*/i, \"\")\n .replace(/^L\\s+/i, \"\")\n .replace(/\\s+/g, \"\");\n\n// Extrae SOLO el primer código válido de 10 caracteres\nconst match = cleaned.match(/[0-9]{4}[A-Z][0-9]{3}[CSMT][0-9]/);\n\nconst lot_code = match ? match[0] : null;\nconst isValidFormat = !!lot_code;\n\nlet productMarker = null;\nif (lot_code) {\n productMarker = lot_code.charAt(8);\n}\n\nreturn [{\n raw,\n cleaned,\n lot_code,\n isValidFormat,\n productMarker\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 592, + 1056 + ], + "id": "e7e5f008-a0dd-49b8-a9b1-e68c0b31195c", + "name": "validate_lot_code_format" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "d57db435-60e4-4b61-8f12-8523f6a387be", + "leftValue": "={{$json.isValidFormat}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 720, + 1056 + ], + "id": "89345247-bbde-46ea-8797-b0fcf90be555", + "name": "If5" + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/1053542944519088/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"type\": \"interactive\",\n \"interactive\": {\n \"type\": \"button\",\n \"body\": {\n \"text\": \"A continuacion responde SI o NO, Declara que Nestlé® y sus marcas pueden utilizar los datos de contacto, interacciones y demás datos personales provistos para enviarte comunicaciones de marketing personalizadas. El consentimiento para recibir estas comunicaciones es voluntario y puedes retirarlo en cualquier momento.\"\n },\n \"footer\": {\n \"text\": \"Presto\"\n },\n \"action\": {\n \"buttons\": [\n {\n \"type\": \"reply\",\n \"reply\": {\n \"id\": \"SI\",\n \"title\": \"✅ Si\"\n }\n },\n {\n \"type\": \"reply\",\n \"reply\": {\n \"id\": \"NO\",\n \"title\": \"❌ No\"\n }\n }\n ]\n }\n }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1824, + 144 + ], + "id": "89b5a564-8968-4e3f-850d-6b0cee274a43", + "name": "HTTP Request2", + "credentials": { + "httpHeaderAuth": { + "id": "xwpr4wSQ8IOc6Plo", + "name": "NestleOficial" + } + } + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "eec5e9c4-fb5d-4064-9492-41c6800c115f", + "leftValue": "={{ $json.raw }}", + "rightValue": "Más departamentos", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 112, + 1152 + ], + "id": "360621d4-bf0b-40f5-b513-34e85693479c", + "name": "If6" + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/1053542944519088/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"type\": \"interactive\",\n \"interactive\": {\n \"type\": \"list\",\n \"header\": {\n \"type\": \"text\",\n \"text\": \"Selecciona tu departamento\"\n },\n \"body\": {\n \"text\": \"Elige una opción de la lista.\"\n },\n \"footer\": {\n \"text\": \"Presto\"\n },\n \"action\": {\n \"button\": \"Ver más opciones\",\n \"sections\": [\n {\n \"title\": \"Departamentos 2/2\",\n \"rows\": [\n { \"id\": \"dep_esteli\", \"title\": \"Estelí\" },\n { \"id\": \"dep_carazo\", \"title\": \"Carazo\" },\n { \"id\": \"dep_jinotega\", \"title\": \"Jinotega\" },\n { \"id\": \"dep_rio_san_juan\", \"title\": \"Río San Juan\" },\n { \"id\": \"dep_boaco\", \"title\": \"Boaco\" },\n { \"id\": \"dep_raccn\", \"title\": \"RACCN\" },\n { \"id\": \"dep_madriz\", \"title\": \"Madriz\" },\n { \"id\": \"dep_nueva_segovia\", \"title\": \"Nueva Segovia\" }\n ]\n }\n ]\n }\n }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2608, + 896 + ], + "id": "c812d7c9-568d-46f2-a5bd-b04d06d1076d", + "name": "HTTP Request3", + "credentials": { + "httpHeaderAuth": { + "id": "xwpr4wSQ8IOc6Plo", + "name": "NestleOficial" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/1053542944519088/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"type\": \"interactive\",\n \"interactive\": {\n \"type\": \"list\",\n \"header\": {\n \"type\": \"text\",\n \"text\": \"Selecciona tu departamento\"\n },\n \"body\": {\n \"text\": \"Elige una opción de la lista.\"\n },\n \"footer\": {\n \"text\": \"Presto\"\n },\n \"action\": {\n \"button\": \"Ver opciones\",\n \"sections\": [\n {\n \"title\": \"Departamentos 1/2\",\n \"rows\": [\n { \"id\": \"dep_managua\", \"title\": \"Managua\" },\n { \"id\": \"dep_masaya\", \"title\": \"Masaya\" },\n { \"id\": \"dep_chinandega\", \"title\": \"Chinandega\" },\n { \"id\": \"dep_leon\", \"title\": \"León\" },\n { \"id\": \"dep_chontales\", \"title\": \"Chontales\" },\n { \"id\": \"dep_matagalpa\", \"title\": \"Matagalpa\" },\n { \"id\": \"dep_rivas\", \"title\": \"Rivas\" },\n { \"id\": \"dep_granada\", \"title\": \"Granada\" },\n { \"id\": \"dep_raccs\", \"title\": \"RACCS\" },\n { \"id\": \"dep_mas\", \"title\": \"Más departamentos\" }\n ]\n }\n ]\n }\n }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2656, + 1088 + ], + "id": "efedcd43-4453-452b-9c61-15b8eb2278f2", + "name": "HTTP Request4", + "credentials": { + "httpHeaderAuth": { + "id": "xwpr4wSQ8IOc6Plo", + "name": "NestleOficial" + } + } + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "48db3cb1-b297-40e8-8c7a-e33d09a91d11", + "leftValue": "={{ $('build_reply_from_register').item.json.r.out_status }}", + "rightValue": "limit", + "operator": { + "type": "string", + "operation": "notEquals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 2096, + 1760 + ], + "id": "e43c2083-0f92-4ab0-a6fb-32b2f96a93e3", + "name": "If7" + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/1053542944519088/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"type\": \"interactive\",\n \"interactive\": {\n \"type\": \"button\",\n \"body\": {\n \"text\": {{ JSON.stringify($node[\"build_reply_from_register\"].json.reply) }}\n },\n \"footer\": {\n \"text\": \"Presto\"\n },\n \"action\": {\n \"buttons\": [\n {\n \"type\": \"reply\",\n \"reply\": {\n \"id\": \"SI\",\n \"title\": \"✅ Si\"\n }\n },\n {\n \"type\": \"reply\",\n \"reply\": {\n \"id\": \"NO\",\n \"title\": \"❌ No\"\n }\n }\n ]\n }\n }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2304, + 1312 + ], + "id": "186cad18-361f-438f-8f8f-7bafb76e442e", + "name": "HTTP Request5", + "credentials": { + "httpHeaderAuth": { + "id": "xwpr4wSQ8IOc6Plo", + "name": "NestleOficial" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/1053542944519088/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"type\": \"interactive\",\n \"interactive\": {\n \"type\": \"button\",\n \"body\": {\n \"text\": \"❌ Tu código es incorrecto o estás ingresando el código de un producto no participante.\\n\\n✔️ Verifica nuestros parámetros de control y productos participantes en nuestro reglamento en:\\nhttp://promos.nestlecam.com/extrarealespresto/terminos-y-condiciones\\n\\n¿Deseas ingresar otro código?\"\n },\n \"footer\": {\n \"text\": \"Presto\"\n },\n \"action\": {\n \"buttons\": [\n {\n \"type\": \"reply\",\n \"reply\": {\n \"id\": \"SI\",\n \"title\": \"✅ Si\"\n }\n },\n {\n \"type\": \"reply\",\n \"reply\": {\n \"id\": \"NO\",\n \"title\": \"❌ No\"\n }\n }\n ]\n }\n }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2320, + 2032 + ], + "id": "ee5399b2-0d77-4c23-8ecb-08ff2b57cbd4", + "name": "HTTP Request6", + "credentials": { + "httpHeaderAuth": { + "id": "xwpr4wSQ8IOc6Plo", + "name": "NestleOficial" + } + } + }, + { + "parameters": { + "updates": [ + "messages" + ], + "options": {} + }, + "type": "n8n-nodes-base.whatsAppTrigger", + "typeVersion": 1, + "position": [ + -1296, + 0 + ], + "id": "91493d7f-3649-42d3-bdd0-5a3f21df5873", + "name": "WhatsApp Trigger", + "webhookId": "2cd23a95-5277-4b87-adbe-95555157078c", + "credentials": { + "whatsAppTriggerApi": { + "id": "vJ3R8sYaCMkIZN9S", + "name": "OficialNestle" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f03e65d0-8615-4437-9995-bb56c60149f4", + "name": "reply", + "value": "=👋 ¡Bienvenido de nuevo!\n\nPor favor ingresa tu siguiente código de lote.", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1728, + 1616 + ], + "id": "3de05c8e-1199-4091-93fd-8c20be306bd5", + "name": "reply_codeBienvenida" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "833ba324-ae8b-4caf-9e08-ea2241b9c44c", + "leftValue": "={{$json.isValid}}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + -656, + 592 + ], + "id": "5ac24fb9-5bbc-40ad-9098-b36a354efd1c", + "name": "If8" + }, + { + "parameters": { + "jsCode": "const raw = ($node[\"Code in JavaScript\"].json.text || \"\").trim();\n\nconst norm = raw\n .toLowerCase()\n .normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n\nlet purchase_place = null;\n\nif (norm === \"1\" || norm === \"mercado\") {\n purchase_place = \"Mercado\";\n} else if (norm === \"2\" || norm === \"supermercado\") {\n purchase_place = \"Supermercado\";\n} else if (norm === \"3\" || norm === \"pulperia\") {\n purchase_place = \"Pulperia\";\n}\n\nreturn [{\n isValid: !!purchase_place,\n purchase_place,\n raw,\n normalized: norm\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -864, + 592 + ], + "id": "2437c9f2-1cd0-4daa-b6c4-2f20dfa126b3", + "name": "validate_purchase_place" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbpromopro.digitalcompass.agency/rest/v1/rpc/set_purchase_place_wa", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone_e164\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"p_purchase_place\": \"{{$json.purchase_place}}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + -512, + 496 + ], + "id": "9214e324-b816-4e63-862c-7a39ed8eb8ec", + "name": "RPC set_purchase_place_wa", + "credentials": { + "httpCustomAuth": { + "id": "oJ6NXOprzl2uve2R", + "name": "Supabase Service Role" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/1053542944519088/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"type\": \"interactive\",\n \"interactive\": {\n \"type\": \"list\",\n \"header\": {\n \"type\": \"text\",\n \"text\": \"Ingresa donde realizaste tu compra 🛒\"\n },\n \"body\": {\n \"text\": \"Elige una opción de la lista.\"\n },\n \"footer\": {\n \"text\": \"Presto\"\n },\n \"action\": {\n \"button\": \"Ver más opciones\",\n \"sections\": [\n {\n \"title\": \"Lugar\",\n \"rows\": [\n { \"id\": \"dep_mercado\", \"title\": \"Mercado\" },\n { \"id\": \"dep_supermercado\", \"title\": \"Supermercado\" },\n { \"id\": \"dep_pulperia\", \"title\": \"Pulperia\" }\n ]\n }\n ]\n }\n }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2608, + 640 + ], + "id": "2eb7cc8f-5722-41aa-a4c6-cf86f1734dbb", + "name": "HTTP Request7", + "credentials": { + "httpHeaderAuth": { + "id": "xwpr4wSQ8IOc6Plo", + "name": "NestleOficial" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/1053542944519088/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $('Code in JavaScript').item.json.phone_e164 }}\",\n \"type\": \"image\",\n \"image\": {\n \"link\": \"https://digitalcompass.agency/wp-content/uploads/2026/03/PRESTO-EXTRA-REALES-e1774883519533.jpg\",\n \"caption\": \"⬆️Así encuentras⬆️ 🔍 el lote en los empaques de Presto ☕️.\\n\\nRecuerda‼️Para participar debes ingresar:\\n\\n✅10 códigos de churritos/sticks Presto\\n\\no\\n\\n✅1 código de cualquier otra presentación\\n\\nPresto\\n\\n*Ahora ingresa tu primer código de lote.*\"\n }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2576, + 384 + ], + "id": "d99f473e-ae3c-4bb9-993e-0ac5002d8155", + "name": "HTTP Request1", + "credentials": { + "httpHeaderAuth": { + "id": "xwpr4wSQ8IOc6Plo", + "name": "NestleOficial" + } + } + }, + { + "parameters": { + "amount": 6 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + -864, + 0 + ], + "id": "08b351dc-111c-4267-8f1d-67201e5e6fc6", + "name": "Wait", + "webhookId": "1307911c-f1bc-40c2-9895-9f6f847fbf03" + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/1053542944519088/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"status\": \"read\",\n \"message_id\": \"{{ $('WhatsApp Trigger').item.json.messages[0].id }}\",\n \"typing_indicator\": {\n \"type\": \"text\"\n }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + -832, + 848 + ], + "id": "a4028740-0a96-4884-b6f3-dd4774aae302", + "name": "HTTP Request8", + "credentials": { + "httpHeaderAuth": { + "id": "xwpr4wSQ8IOc6Plo", + "name": "NestleOficial" + } + } + }, + { + "parameters": { + "amount": 3 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + -656, + 848 + ], + "id": "e75e108f-80e3-45e1-a426-9f88f5ad6692", + "name": "Wait1", + "webhookId": "ff963181-77a2-4159-9449-5828019f4844" + }, + { + "parameters": { + "amount": 2 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 1968, + 1264 + ], + "id": "eea1f534-9fe9-4bdc-9bea-9bed56847f9d", + "name": "Wait2", + "webhookId": "885f041d-b52a-4d82-9c8e-0f89f6a29e30" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $('build_reply_from_register').item.json.r.out_status }}", + "rightValue": "limit", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "e09c1af3-8bc3-41dc-ba0e-76acc36ad1ac" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "limit" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "417f8f65-b626-4325-a89d-dc6ce1bb4d1e", + "leftValue": "={{ $('build_reply_from_register').item.json.r.out_status }}", + "rightValue": "spam_blocked", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "spam_blocked" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "9715774a-b211-42d2-86a1-cc81bc80673c", + "leftValue": "={{ $('build_reply_from_register').item.json.r.out_status }}", + "rightValue": "fraud_blocked", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "fraud_blocked" + } + ] + }, + "options": { + "fallbackOutput": "extra" + } + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 1552, + 1200 + ], + "id": "f8221e28-cf2f-448d-a13e-ace5cfeb34e0", + "name": "Switch" + } + ], + "pinData": { + "WhatsApp Trigger": [ + { + "json": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "18495060779", + "phone_number_id": "1076984602155497" + }, + "contacts": [ + { + "profile": { + "name": "Luis Matos" + }, + "wa_id": "18494105855" + } + ], + "messages": [ + { + "from": "18494105855", + "id": "wamid.HBgLMTg0OTQxMDU4NTUVAgASGCBBNUQyNDUwRjFFMjk3N0VEOEQ5RjUxREVBREQxMzlGOAA=", + "timestamp": "1774028644", + "text": { + "body": "L: 5268b502 c 1" + }, + "type": "text" + } + ], + "field": "messages" + }, + "pairedItem": { + "item": 0 + } + } + ] + }, + "connections": { + "Code in JavaScript": { + "main": [ + [ + { + "node": "Wait", + "type": "main", + "index": 0 + } + ] + ] + }, + "If": { + "main": [ + [], + [ + { + "node": "HTTP Request — Supabase ensure_user_wa", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request — Supabase ensure_user_wa": { + "main": [ + [ + { + "node": "State Router", + "type": "main", + "index": 0 + } + ] + ] + }, + "State Router": { + "main": [ + [ + { + "node": "normalize_answer_privacy", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "normalize_answer_marketing", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "validate_name", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "validate_id", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "validate_email", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "validate_location", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "validate_purchase_place", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "validate_lot_code_format", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "parse_menu_choice", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "detect_hello", + "type": "main", + "index": 0 + } + ] + ] + }, + "normalize_answer_privacy": { + "main": [ + [ + { + "node": "Intent Router Privacy", + "type": "main", + "index": 0 + } + ] + ] + }, + "Intent Router Privacy": { + "main": [ + [ + { + "node": "HTTP Request", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "set_consent_privacy_wa", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "reply_reject", + "type": "main", + "index": 0 + } + ] + ] + }, + "set_consent_privacy_wa": { + "main": [ + [ + { + "node": "HTTP Request2", + "type": "main", + "index": 0 + } + ] + ] + }, + "normalize_answer_marketing": { + "main": [ + [ + { + "node": "Intent Router Marketing", + "type": "main", + "index": 0 + } + ] + ] + }, + "Intent Router Marketing": { + "main": [ + [ + { + "node": "HTTP Request2", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "RPC set_consent_marketing_wa (true)", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "RPC set_consent_marketing_wa (true)1", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC set_consent_marketing_wa (true)": { + "main": [ + [ + { + "node": "reply_promo01", + "type": "main", + "index": 0 + } + ] + ] + }, + "reply_promo01": { + "main": [ + [ + { + "node": "Send message", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC set_consent_marketing_wa (true)1": { + "main": [ + [ + { + "node": "reply_promo01", + "type": "main", + "index": 0 + } + ] + ] + }, + "validate_name": { + "main": [ + [ + { + "node": "If1", + "type": "main", + "index": 0 + } + ] + ] + }, + "If1": { + "main": [ + [ + { + "node": "RPC set_name_wa", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "reply_promo01", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC set_name_wa": { + "main": [ + [ + { + "node": "reply_promo02", + "type": "main", + "index": 0 + } + ] + ] + }, + "reply_promo02": { + "main": [ + [ + { + "node": "Send message", + "type": "main", + "index": 0 + } + ] + ] + }, + "validate_id": { + "main": [ + [ + { + "node": "If2", + "type": "main", + "index": 0 + } + ] + ] + }, + "If2": { + "main": [ + [ + { + "node": "RPC set_id_wa", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "reply_ID", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC set_id_wa": { + "main": [ + [ + { + "node": "reply_promo03", + "type": "main", + "index": 0 + } + ] + ] + }, + "reply_promo03": { + "main": [ + [ + { + "node": "Send message", + "type": "main", + "index": 0 + } + ] + ] + }, + "validate_email": { + "main": [ + [ + { + "node": "If3", + "type": "main", + "index": 0 + } + ] + ] + }, + "If3": { + "main": [ + [ + { + "node": "RPC set_id_wa1", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "reply_promo03", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC set_id_wa1": { + "main": [ + [ + { + "node": "HTTP Request4", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC register_lot_code": { + "main": [ + [ + { + "node": "build_reply_from_register", + "type": "main", + "index": 0 + } + ] + ] + }, + "build_reply_from_register": { + "main": [ + [ + { + "node": "RPC register_lot_code1", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC register_lot_code1": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "reply_code": { + "main": [ + [ + { + "node": "Wait2", + "type": "main", + "index": 0 + } + ] + ] + }, + "parse_menu_choice": { + "main": [ + [ + { + "node": "Menu Router", + "type": "main", + "index": 0 + } + ] + ] + }, + "Menu Router": { + "main": [ + [ + { + "node": "RPC set_state_wa (ASK_CODE)", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "RPC set_state_wa (DONE)", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "HTTP Request6", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC set_state_wa (ASK_CODE)": { + "main": [ + [ + { + "node": "reply_code1", + "type": "main", + "index": 0 + } + ] + ] + }, + "reply_code1": { + "main": [ + [ + { + "node": "Send message", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC set_state_wa (DONE)": { + "main": [ + [ + { + "node": "reply_code2", + "type": "main", + "index": 0 + } + ] + ] + }, + "reply_code2": { + "main": [ + [ + { + "node": "Send message", + "type": "main", + "index": 0 + } + ] + ] + }, + "detect_hello": { + "main": [ + [ + { + "node": "isHello?", + "type": "main", + "index": 0 + } + ] + ] + }, + "isHello?": { + "main": [ + [ + { + "node": "RPC restart_wa", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "reply_done_hint", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC restart_wa": { + "main": [ + [ + { + "node": "Restart State Router", + "type": "main", + "index": 0 + } + ] + ] + }, + "Restart State Router": { + "main": [ + [ + { + "node": "reply_codeBienvenida", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "HTTP Request", + "type": "main", + "index": 0 + } + ] + ] + }, + "reply_done_hint": { + "main": [ + [ + { + "node": "Send message", + "type": "main", + "index": 0 + } + ] + ] + }, + "validate_location": { + "main": [ + [ + { + "node": "If4", + "type": "main", + "index": 0 + } + ] + ] + }, + "If4": { + "main": [ + [ + { + "node": "RPC set_location_wa", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "If6", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC set_location_wa": { + "main": [ + [ + { + "node": "HTTP Request7", + "type": "main", + "index": 0 + } + ] + ] + }, + "reply_reject": { + "main": [ + [ + { + "node": "Send message", + "type": "main", + "index": 0 + } + ] + ] + }, + "reply_ID": { + "main": [ + [ + { + "node": "Send message", + "type": "main", + "index": 0 + } + ] + ] + }, + "validate_lot_code_format": { + "main": [ + [ + { + "node": "If5", + "type": "main", + "index": 0 + } + ] + ] + }, + "If5": { + "main": [ + [ + { + "node": "RPC register_lot_code", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "parse_menu_choice", + "type": "main", + "index": 0 + } + ] + ] + }, + "If6": { + "main": [ + [ + { + "node": "HTTP Request3", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "HTTP Request4", + "type": "main", + "index": 0 + } + ] + ] + }, + "If7": { + "main": [ + [], + [] + ] + }, + "WhatsApp Trigger": { + "main": [ + [ + { + "node": "Code in JavaScript", + "type": "main", + "index": 0 + } + ] + ] + }, + "reply_codeBienvenida": { + "main": [ + [ + { + "node": "Send message", + "type": "main", + "index": 0 + } + ] + ] + }, + "validate_purchase_place": { + "main": [ + [ + { + "node": "If8", + "type": "main", + "index": 0 + } + ] + ] + }, + "If8": { + "main": [ + [ + { + "node": "RPC set_purchase_place_wa", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "HTTP Request7", + "type": "main", + "index": 0 + } + ] + ] + }, + "RPC set_purchase_place_wa": { + "main": [ + [ + { + "node": "HTTP Request1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait": { + "main": [ + [ + { + "node": "If", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request8": { + "main": [ + [ + { + "node": "Wait1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait1": { + "main": [ + [] + ] + }, + "Wait2": { + "main": [ + [ + { + "node": "Send message", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + [ + { + "node": "reply_code", + "type": "main", + "index": 0 + } + ], + [], + [], + [ + { + "node": "HTTP Request5", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": false, + "timeSavedMode": "fixed", + "errorWorkflow": "DQqN5Qiw2ynpLR6U", + "callerPolicy": "workflowsFromSameOwner" + }, + "versionId": "81236cab-0d30-4e0b-b417-977e0d37c0c8", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "14d8ef2f799eaca6e22b05585b4ba065f9b474746fdc1850fb666d88bf9e3c45" + }, + "id": "n0mJLVnZ3aF66ugD", + "tags": [] +} \ No newline at end of file