-- EL SERIAL DE LA ESTACION FISCAL.
--
-- Cada cliente de la imprenta recibe un serial unico (EF-00001-7: correlativo
-- + digito verificador), asignado POR LA IMPRENTA al dar de alta, igual que el
-- registro de una maquina fiscal. El cliente lo ve en su Estacion, va escrito
-- en el contrato, y CUALQUIERA lo comprueba en la pagina publica del sello.
--
-- Decisiones de Gilberto (2026-07-30): formato EF-NNNNN-D; la verificacion
-- publica muestra razon social y RIF completos (es lo que valida el contrato,
-- y esos datos ya van en cada factura del negocio); un cliente suspendido se
-- muestra SUSPENDIDO, que es honesto y aprieta el cobro.

begin;

-- 1. La columna y el correlativo (sin saltos, jamas se repite).
alter table fiscal.emisores add column if not exists serial text;
create sequence if not exists fiscal.serial_correlativo;

-- 2. El digito verificador: modulo 11 con pesos 2 al 6 de derecha a izquierda,
--    el mismo espiritu del RIF. Un serial inventado o mal copiado rebota solo.
create or replace function fiscal.fn_serial_dv(p_num int)
returns int
language sql immutable
as $$
  select (11 - (
    (substr(lpad(p_num::text, 5, '0'), 5, 1)::int * 2) +
    (substr(lpad(p_num::text, 5, '0'), 4, 1)::int * 3) +
    (substr(lpad(p_num::text, 5, '0'), 3, 1)::int * 4) +
    (substr(lpad(p_num::text, 5, '0'), 2, 1)::int * 5) +
    (substr(lpad(p_num::text, 5, '0'), 1, 1)::int * 6)
  ) % 11) % 11 % 10;
$$;

create or replace function fiscal.fn_armar_serial(p_num int)
returns text
language sql immutable
as $$
  select 'EF-' || lpad(p_num::text, 5, '0') || '-' || fiscal.fn_serial_dv(p_num);
$$;

create or replace function fiscal.fn_serial_valido(p_serial text)
returns boolean
language sql immutable
as $$
  select case
    when p_serial is null or p_serial !~ '^EF-\d{5}-\d$' then false
    else fiscal.fn_serial_dv(substr(p_serial, 4, 5)::int) = substr(p_serial, 10, 1)::int
  end;
$$;

-- 3. Se asigna SOLO al nacer el emisor, por cualquier via de alta. El cliente
--    no lo elige ni lo puede cambiar: es de la imprenta.
create or replace function fiscal.tg_asignar_serial()
returns trigger
language plpgsql
as $$
begin
  if new.serial is null then
    new.serial := fiscal.fn_armar_serial(nextval('fiscal.serial_correlativo')::int);
  end if;
  return new;
end $$;

drop trigger if exists tg_asignar_serial on fiscal.emisores;
create trigger tg_asignar_serial
  before insert on fiscal.emisores
  for each row execute function fiscal.tg_asignar_serial();

-- 4. Los emisores que ya existen reciben el suyo por orden de llegada
--    (el primero de la casa queda con el EF-00001).
do $$
declare r record;
begin
  for r in select id from fiscal.emisores where serial is null order by creado_en loop
    update fiscal.emisores
       set serial = fiscal.fn_armar_serial(nextval('fiscal.serial_correlativo')::int)
     where id = r.id;
  end loop;
end $$;

alter table fiscal.emisores alter column serial set not null;
alter table fiscal.emisores add constraint emisores_serial_unico unique (serial);
alter table fiscal.emisores add constraint emisores_serial_valido check (fiscal.fn_serial_valido(serial));

-- 5. El latido devuelve el serial: la Estacion lo aprende sola, nadie lo teclea.
create or replace function fiscal.latido(p_llave text, p_rif text, p_datos jsonb default '{}'::jsonb)
returns jsonb
language plpgsql
security definer
set search_path to 'fiscal', 'public'
as $function$
declare
  v_emisor uuid;
  v_serial text;
  v_pend int;
  v_ultima timestamptz;
  v_espera int;
begin
  v_emisor := fiscal.autenticar(p_llave, p_rif, 'estacion: latido');
  select serial into v_serial from fiscal.emisores where id = v_emisor;

  insert into fiscal.estaciones (emisor_id, nombre, version, equipo, ultimo_latido,
                                 pendientes, ultimo_documento, libro_integro)
    values (v_emisor,
            coalesce(nullif(p_datos->>'nombre', ''), 'Estacion Fiscal'),
            p_datos->>'version',
            coalesce(nullif(p_datos->>'equipo', ''), 'unico'),
            now(),
            coalesce((p_datos->>'pendientes')::int, 0),
            nullif(p_datos->>'ultimo_documento', '')::bigint,
            (p_datos->>'libro_integro')::boolean)
    on conflict (emisor_id, equipo) do update set
      nombre = excluded.nombre,
      version = excluded.version,
      ultimo_latido = now(),
      pendientes = excluded.pendientes,
      ultimo_documento = excluded.ultimo_documento,
      libro_integro = excluded.libro_integro;

  select count(*), max(creado_en) into v_pend, v_ultima
    from fiscal.cola_gustito
   where emisor_id = v_emisor and estado = 'pendiente';

  -- El ritmo lo marca el movimiento del negocio, no el reloj.
  v_espera := case
    when v_pend > 0 then 10                                              -- hay trabajo: ya mismo
    when v_ultima > now() - interval '15 minutes' then 20                -- vendio hace nada: atento
    when v_ultima > now() - interval '2 hours' then 60                   -- venia vendiendo: tranquilo
    else 300                                                             -- cerrado o sin movimiento
  end;

  return jsonb_build_object(
    'ok', true,
    'pendientes_en_nube', v_pend,
    'habilitado', true,
    'serial', v_serial,
    'volver_en_segundos', v_espera
  );
end;
$function$;

-- 6. La lista de clientes del panel trae el serial (para copiarlo al contrato).
--    DROP previo: CREATE OR REPLACE no puede cambiar el tipo de retorno.
drop function fiscal.clientes();
create function fiscal.clientes()
returns table(serial text, rif text, razon_social text, correo text, telefono text,
              sistema text, serie_asignada text, activo boolean, rif_comprobado boolean,
              llaves_activas integer, documentos_mes integer, total_mes_bs numeric,
              ultimo_documento timestamp with time zone, fallidos_hoy integer,
              respaldo_lineas integer, respaldo_ultimo timestamp with time zone)
language sql
stable security definer
set search_path to 'fiscal', 'public'
as $function$
  select e.serial, e.rif, e.razon_social, e.correo, e.telefono, e.sistema, e.serie_asignada,
         e.activo, e.rif_comprobado,
         (select count(*)::int from fiscal.llaves l where l.emisor_id = e.id and l.activa),
         (select count(*)::int from fiscal.documentos d
           where d.emisor_id = e.id and d.fecha_emision >= date_trunc('month', now())),
         coalesce((select round(sum(d.total_bs), 2) from fiscal.documentos d
           where d.emisor_id = e.id and d.fecha_emision >= date_trunc('month', now())), 0),
         (select max(d.recibido_en) from fiscal.documentos d where d.emisor_id = e.id),
         (select count(*)::int from fiscal.intentos i
           where i.tipo = 'estacion' and i.quien = e.rif and not i.entro and i.en >= current_date),
         (select count(*)::int from fiscal.respaldo r where r.emisor_id = e.id),
         (select max(r.recibido_en) from fiscal.respaldo r where r.emisor_id = e.id)
    from fiscal.emisores e
   order by e.razon_social;
$function$;

-- 7. LA VERIFICACION PUBLICA, con su freno. El serial es correlativo: sin
--    freno, cualquiera enumera EF-00001, EF-00002... y se lleva la cartera de
--    clientes completa. Con 10 consultas por hora por direccion, enumerar no
--    es practico.
create table if not exists fiscal.consultas_serial (
  desde text not null,
  en timestamptz not null default now()
);
create index if not exists consultas_serial_idx on fiscal.consultas_serial (desde, en);

create or replace function fiscal.verificar_serial(p_serial text, p_desde text default null)
returns jsonb
language plpgsql
security definer
set search_path to 'fiscal', 'public'
as $function$
declare
  v record;
  v_cuantas int;
  v_limpio text;
begin
  if p_desde is not null then
    select count(*) into v_cuantas from fiscal.consultas_serial
     where desde = p_desde and en > now() - interval '1 hour';
    if v_cuantas >= 10 then
      raise exception 'Demasiadas consultas desde esta direccion. Intenta mas tarde.';
    end if;
    insert into fiscal.consultas_serial (desde) values (p_desde);
    delete from fiscal.consultas_serial where en < now() - interval '2 hours';
  end if;

  v_limpio := upper(trim(coalesce(p_serial, '')));
  -- La basura ni llega a la tabla: el verificador la rebota aqui mismo.
  if not fiscal.fn_serial_valido(v_limpio) then
    return jsonb_build_object('registrada', false, 'motivo', 'serial_invalido');
  end if;

  select e.serial, e.razon_social, e.rif, e.activo, e.creado_en into v
    from fiscal.emisores e
   where e.serial = v_limpio;
  if not found then
    return jsonb_build_object('registrada', false, 'motivo', 'no_registrada');
  end if;

  return jsonb_build_object(
    'registrada', true,
    'estado', case when v.activo then 'activa' else 'suspendida' end,
    'serial', v.serial,
    'razon_social', v.razon_social,
    'rif', v.rif,
    'desde', to_char(v.creado_en, 'MM-YYYY')
  );
end;
$function$;

-- 8. El envoltorio para la Pages Function publica. Nadie mas lo llama: se le
--    quita el permiso a anon y authenticated (solo service role, que es quien
--    firma la Function del sello).
create or replace function public.fn_verificar_estacion(p_serial text, p_desde text default null)
returns jsonb
language sql
security definer
set search_path to 'public', 'fiscal'
as $function$
  select fiscal.verificar_serial(p_serial, p_desde);
$function$;

revoke all on function public.fn_verificar_estacion(text, text) from public;
revoke all on function public.fn_verificar_estacion(text, text) from anon;
revoke all on function public.fn_verificar_estacion(text, text) from authenticated;

commit;
