-- EL LIBRO FISCAL TAMBIEN TIENE QUE VIVIR FUERA DEL EQUIPO (2026-07-29)
--
-- Hasta hoy, si se quema (o se roban, o se formatea) la laptop del negocio, se
-- quemo el libro fiscal completo. Y el libro es la DEFENSA del contribuyente:
-- es lo que enseña cuando el SENIAT le pregunta que vendio. Un sistema de
-- facturacion que se puede perder con un disco no sirve para lo que existe.
--
-- La Estacion ya sabia hacer un respaldo cifrado asiento por asiento
-- (apps/fiscal/src/respaldo.ts), pero ese archivo se quedaba en la misma
-- computadora que se puede quemar. Aca se abre el lugar donde va a vivir la
-- copia: en la imprenta.
--
-- LO QUE LA IMPRENTA NO PUEDE HACER, Y ES A PROPOSITO. Cada linea llega
-- CIFRADA con una frase que solo tiene el negocio. Gilberto guarda las cajas
-- pero no tiene la llave: no puede leer que vendio su cliente ni quien se lo
-- compro. Eso ademas es lo que exige el articulo 25 de la 000102
-- (confidencialidad de los documentos procesados).
--
-- APPEND ONLY DE VERDAD. Una linea guardada NO SE PUEDE MODIFICAR: hay un
-- candado que revienta cualquier UPDATE. Si se pudiera editar, el respaldo no
-- probaria nada. El DELETE si se deja (es la unica forma de dar de baja a un
-- cliente que se fue y de limpiar lo que crean las pruebas), pero editar una
-- linea guardada, jamas.
begin;

create table if not exists fiscal.respaldo (
  emisor_id uuid not null references fiscal.emisores(id) on delete cascade,
  -- El numero de asiento en el libro del negocio. Va sin huecos.
  seq int not null,
  iv text not null,
  tag text not null,
  datos text not null,
  recibido_en timestamptz not null default now(),
  primary key (emisor_id, seq),
  -- Una linea es un asiento cifrado, no un archivo: si viene algo enorme, es
  -- que alguien esta usando esto de deposito y no de respaldo.
  constraint respaldo_tamano_ck check (length(datos) <= 65536)
);

comment on table fiscal.respaldo is 'Copia cifrada del libro fiscal de cada cliente. La imprenta guarda, no lee: la frase la tiene el negocio.';

create index if not exists respaldo_emisor_seq on fiscal.respaldo (emisor_id, seq desc);

create or replace function fiscal.guard_respaldo_solo_agrega()
returns trigger
language plpgsql
as $fn$
begin
  raise exception 'El respaldo solo agrega: una linea guardada no se modifica';
end;
$fn$;

drop trigger if exists trg_respaldo_solo_agrega on fiscal.respaldo;
create trigger trg_respaldo_solo_agrega
  before update on fiscal.respaldo
  for each row execute function fiscal.guard_respaldo_solo_agrega();

-- HASTA DONDE TIENE LA IMPRENTA. La Estacion pregunta esto y manda de ahi en
-- adelante: el que recibe es el que sabe que le falta, no el que manda.
create or replace function fiscal.respaldo_hasta(p_llave text, p_rif text)
returns jsonb
language plpgsql
security definer
set search_path to 'fiscal', 'public', 'extensions'
as $fn$
declare
  v_emisor uuid;
  v_ultimo int;
  v_cuantas int;
begin
  v_emisor := fiscal.autenticar(p_llave, p_rif, 'respaldo: consulta');
  select coalesce(max(seq), 0), count(*) into v_ultimo, v_cuantas
    from fiscal.respaldo where emisor_id = v_emisor;
  return jsonb_build_object('ultimo_seq', v_ultimo, 'lineas', v_cuantas);
end;
$fn$;

-- GUARDAR. Se manda un puñado de lineas cifradas; las repetidas se ignoran sin
-- ruido (la Estacion puede reintentar sin miedo a duplicar).
create or replace function fiscal.guardar_respaldo(p_llave text, p_rif text, p_lineas jsonb)
returns jsonb
language plpgsql
security definer
set search_path to 'fiscal', 'public', 'extensions'
as $fn$
declare
  v_emisor uuid;
  v_guardadas int;
  v_ultimo int;
begin
  v_emisor := fiscal.autenticar(p_llave, p_rif, 'respaldo: guardar');
  if jsonb_typeof(p_lineas) <> 'array' then
    raise exception 'Se esperaba una lista de lineas';
  end if;
  if jsonb_array_length(p_lineas) > 500 then
    raise exception 'Demasiadas lineas de una vez (maximo 500)';
  end if;

  with nuevas as (
    select (l->>'seq')::int as seq, l->>'iv' as iv, l->>'tag' as tag, l->>'datos' as datos
      from jsonb_array_elements(p_lineas) l
  ), metidas as (
    insert into fiscal.respaldo (emisor_id, seq, iv, tag, datos)
    select v_emisor, seq, iv, tag, datos from nuevas
     where seq is not null and iv is not null and tag is not null and datos is not null
    on conflict (emisor_id, seq) do nothing
    returning 1
  )
  select count(*)::int into v_guardadas from metidas;

  select coalesce(max(seq), 0) into v_ultimo from fiscal.respaldo where emisor_id = v_emisor;
  return jsonb_build_object('guardadas', v_guardadas, 'ultimo_seq', v_ultimo);
end;
$fn$;

-- BAJAR. Para el dia que haya que levantar el libro en una maquina nueva.
create or replace function fiscal.bajar_respaldo(p_llave text, p_rif text, p_desde int, p_limite int)
returns jsonb
language plpgsql
security definer
set search_path to 'fiscal', 'public', 'extensions'
as $fn$
declare
  v_emisor uuid;
  v_lineas jsonb;
begin
  v_emisor := fiscal.autenticar(p_llave, p_rif, 'respaldo: bajar');
  select coalesce(jsonb_agg(jsonb_build_object('seq', seq, 'iv', iv, 'tag', tag, 'datos', datos) order by seq), '[]'::jsonb)
    into v_lineas
    from (
      select seq, iv, tag, datos from fiscal.respaldo
       where emisor_id = v_emisor and seq > coalesce(p_desde, 0)
       order by seq
       limit least(coalesce(p_limite, 200), 500)
    ) t;
  return jsonb_build_object('lineas', v_lineas);
end;
$fn$;

-- La puerta de la Estacion aprende tres acciones nuevas.
create or replace function public.fn_estacion(p_accion text, p_llave text, p_rif text, p_datos jsonb default '{}'::jsonb)
returns jsonb
language plpgsql
security definer
set search_path to 'public', 'fiscal'
as $fn$
begin
  case p_accion
    when 'pendientes' then
      return fiscal.ventas_pendientes(p_llave, p_rif, coalesce((p_datos->>'limite')::int, 50));
    when 'confirmar' then
      return fiscal.confirmar_sellado(p_llave, p_rif, (p_datos->>'cola_id')::uuid,
        nullif(p_datos->>'numero_documento','')::bigint,
        nullif(p_datos->>'numero_control','')::bigint,
        p_datos->>'token');
    when 'error' then
      return fiscal.reportar_error(p_llave, p_rif, (p_datos->>'cola_id')::uuid, p_datos->>'error');
    when 'latido' then
      return fiscal.latido(p_llave, p_rif, p_datos);
    when 'rescatar' then
      perform fiscal.autenticar(p_llave, p_rif, 'estacion: rescate');
      return jsonb_build_object('rescatadas', fiscal.rescatar_sin_encolar(coalesce((p_datos->>'horas')::int, 72)));
    when 'respaldo_hasta' then
      return fiscal.respaldo_hasta(p_llave, p_rif);
    when 'respaldo_guardar' then
      return fiscal.guardar_respaldo(p_llave, p_rif, coalesce(p_datos->'lineas', '[]'::jsonb));
    when 'respaldo_bajar' then
      return fiscal.bajar_respaldo(p_llave, p_rif, (p_datos->>'desde')::int, (p_datos->>'limite')::int);
    else
      raise exception 'Accion desconocida';
  end case;
end;
$fn$;

-- Y el panel de la imprenta ve, por cliente, si su libro esta a salvo afuera.
-- Cambia lo que devuelve (dos columnas nuevas), asi que hay que tumbarla antes.
drop function if exists fiscal.clientes();

create function fiscal.clientes()
returns table(
  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 timestamptz, fallidos_hoy integer,
  respaldo_lineas integer, respaldo_ultimo timestamptz
)
language sql
stable
security definer
set search_path to 'fiscal', 'public'
as $fn$
  select 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;
$fn$;

commit;
