--
-- This file is part of TALER
-- Copyright (C) 2026 Taler Systems SA
--
-- TALER is free software; you can redistribute it and/or modify it under the
-- terms of the GNU General Public License as published by the Free Software
-- Foundation; either version 3, or (at your option) any later version.
--
-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License along with
-- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
--

-- The exchange's own record of why it aggregated a payout and then did not
-- make it.
--
-- aggregation_transient already holds that reason, but it cannot be the
-- answer for an auditor: it has no serial ID, it is UPDATEd in place and it
-- is DELETEd once the transfer goes out, so replication -- which walks each
-- table by its serial ID and only ever appends -- can neither carry it nor
-- notice that it changed.  An external auditor could therefore see that a
-- payout was decided and not executed, but never why, and had to guess from
-- the KYC tables whether the exchange was withholding the money lawfully or
-- simply sitting on it.
--
-- This table is that claim, append-only: one row per occasion on which the
-- aggregator decided not to pay out, stating what it was holding and why.
-- The exchange is not taken at its word -- the auditor still recomputes the
-- amount and checks a KYC claim against the legitimization tables -- but a
-- disagreement is now a finding against a statement the exchange signed up
-- to, rather than the auditor's own inference against nothing.
--
-- Rows are not garbage collected on their own.  batch_deposit_serial_id
-- names the last deposit of the aggregate to reach its wire deadline, so
-- exchange_do_main_gc() removing that deposit takes the deferral with it:
-- the claim lives exactly as long as the deposits it is a claim about.

CREATE FUNCTION create_table_aggregation_deferrals(
  IN partition_suffix TEXT DEFAULT NULL
)
RETURNS VOID
LANGUAGE plpgsql
AS $$
DECLARE
  table_name TEXT DEFAULT 'aggregation_deferrals';
BEGIN
  PERFORM create_partitioned_table(
    'CREATE TABLE %I'
      '(aggregation_deferral_serial_id BIGINT GENERATED BY DEFAULT AS IDENTITY'
      ',batch_deposit_serial_id INT8 NOT NULL'
      ',wtid_raw BYTEA NOT NULL CHECK (LENGTH(wtid_raw)=32)'
      ',wire_target_h_payto BYTEA NOT NULL CHECK (LENGTH(wire_target_h_payto)=32)'
      ',amount taler_amount NOT NULL'
      ',deferral_reason INT4 NOT NULL'
      ',legitimization_requirement_serial_id INT8 NOT NULL DEFAULT(0)'
      ',deferral_time INT8 NOT NULL'
    ') %s ;'
    ,table_name
    ,'PARTITION BY HASH (batch_deposit_serial_id)'
    ,partition_suffix
  );
  PERFORM comment_partitioned_table(
     'reasons the aggregator gave for not executing a wire transfer it had already decided on; append-only, and thus replicated to the auditor (unlike aggregation_transient)'
    ,table_name
    ,partition_suffix
  );
  PERFORM comment_partitioned_column(
     'the deposit of the aggregate that is the last to reach its wire deadline; only present to tie the lifetime of this row to that of the deposits it talks about, as garbage collecting the deposit cascades to here'
    ,'batch_deposit_serial_id'
    ,table_name
    ,partition_suffix
  );
  PERFORM comment_partitioned_column(
     'identifier of the wire transfer that was not executed'
    ,'wtid_raw'
    ,table_name
    ,partition_suffix
  );
  PERFORM comment_partitioned_column(
     'unsalted hash of the (full) payto URI of the account that was to receive the funds'
    ,'wire_target_h_payto'
    ,table_name
    ,partition_suffix
  );
  PERFORM comment_partitioned_column(
     'sum of the aggregated deposits (minus refunds and deposit fees) that was withheld, matching aggregation_transient.amount at the time'
    ,'amount'
    ,table_name
    ,partition_suffix
  );
  PERFORM comment_partitioned_column(
     'why the transfer was not made: 1 for an aggregate too small to cover the wire fee, 2 for an open KYC/AML requirement against the recipient'
    ,'deferral_reason'
    ,table_name
    ,partition_suffix
  );
  PERFORM comment_partitioned_column(
     'legitimization measure that has to be satisfied before the transfer can be made, or 0 if the transfer was not deferred for KYC reasons'
    ,'legitimization_requirement_serial_id'
    ,table_name
    ,partition_suffix
  );
  PERFORM comment_partitioned_column(
     'when the aggregator made this decision'
    ,'deferral_time'
    ,table_name
    ,partition_suffix
  );
END
$$;


CREATE FUNCTION constrain_table_aggregation_deferrals(
  IN partition_suffix TEXT
)
RETURNS VOID
LANGUAGE plpgsql
AS $$
DECLARE
  table_name TEXT DEFAULT 'aggregation_deferrals';
BEGIN
  table_name = concat_ws('_', table_name, partition_suffix);
  EXECUTE FORMAT (
    'ALTER TABLE ' || table_name ||
    ' ADD CONSTRAINT ' || table_name || '_aggregation_deferral_serial_id_key'
    ' UNIQUE (aggregation_deferral_serial_id)'
  );
  EXECUTE FORMAT (
    'CREATE INDEX ' || table_name || '_by_wtid_raw_index '
    'ON ' || table_name || ' '
    '(wtid_raw);'
  );
  EXECUTE FORMAT (
    'COMMENT ON INDEX ' || table_name || '_by_wtid_raw_index '
    'IS ' || quote_literal('for get_aggregation_deferral_by_wtid') || ';'
  );
END
$$;


CREATE FUNCTION foreign_table_aggregation_deferrals()
RETURNS VOID
LANGUAGE plpgsql
AS $$
DECLARE
  table_name TEXT DEFAULT 'aggregation_deferrals';
BEGIN
  EXECUTE FORMAT (
    'ALTER TABLE ' || table_name ||
    ' ADD CONSTRAINT ' || table_name || '_foreign_deposit'
    ' FOREIGN KEY (batch_deposit_serial_id)'
    ' REFERENCES batch_deposits (batch_deposit_serial_id)'
    ' ON DELETE CASCADE'
  );
END
$$;


INSERT INTO exchange_tables
    (name
    ,version
    ,action
    ,partitioned
    ,by_range)
  VALUES
    ('aggregation_deferrals'
    ,'exchange-0012'
    ,'create'
    ,TRUE
    ,FALSE),
    ('aggregation_deferrals'
    ,'exchange-0012'
    ,'constrain'
    ,TRUE
    ,FALSE),
    ('aggregation_deferrals'
    ,'exchange-0012'
    ,'foreign'
    ,TRUE
    ,FALSE);
