--
-- This file is part of TALER
-- Copyright (C) 2023, 2024, 2025, 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/>

BEGIN;
SET search_path TO public;
CREATE EXTENSION IF NOT EXISTS pgcrypto;

SET search_path TO libeufin_nexus;

-- Remove all existing functions
DO
$do$
DECLARE
  _sql text;
BEGIN
  SELECT INTO _sql
        string_agg(format('DROP %s %s CASCADE;'
                        , CASE prokind
                            WHEN 'f' THEN 'FUNCTION'
                            WHEN 'p' THEN 'PROCEDURE'
                          END
                        , oid::regprocedure)
                  , E'\n')
  FROM   pg_proc
  WHERE  pronamespace = 'libeufin_nexus'::regnamespace;

  IF _sql IS NOT NULL THEN
    EXECUTE _sql;
  END IF;
END
$do$;

CREATE FUNCTION ebics_id_gen()
RETURNS TEXT
LANGUAGE sql AS $$
-- use gen_random_uuid to get some randomness
-- remove all - characters as they are not random
-- capitalise the UUID as some bank may still be case sensitive
-- end with 34 random chars which is valid for EBICS (max 35 chars)
SELECT upper(replace(gen_random_uuid()::text, '-', ''));
$$;


CREATE FUNCTION amount_normalize(
    IN amount taler_amount
  ,OUT normalized taler_amount
)
LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
  normalized.val = amount.val + amount.frac / 100000000;
  IF (normalized.val > 1::INT8<<52) THEN
    RAISE EXCEPTION 'amount value overflowed';
  END IF;
  normalized.frac = amount.frac % 100000000;

END $$;
COMMENT ON FUNCTION amount_normalize
  IS 'Returns the normalized amount by adding to the .val the value of (.frac / 100000000) and removing the modulus 100000000 from .frac.'
      'It raises an exception when the resulting .val is larger than 2^52';

CREATE FUNCTION amount_add(
   IN l taler_amount
  ,IN r taler_amount
  ,OUT sum taler_amount
)
LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
  sum = (l.val + r.val, l.frac + r.frac);
  SELECT normalized.val, normalized.frac INTO sum.val, sum.frac FROM amount_normalize(sum) as normalized;
END $$;
COMMENT ON FUNCTION amount_add
  IS 'Returns the normalized sum of two amounts. It raises an exception when the resulting .val is larger than 2^52';

CREATE FUNCTION register_outgoing(
  IN in_amount taler_amount
  ,IN in_debit_fee taler_amount
  ,IN in_subject TEXT
  ,IN in_execution_time INT8
  ,IN in_credit_payto TEXT
  ,IN in_end_to_end_id TEXT
  ,IN in_msg_id TEXT
  ,IN in_acct_svcr_ref TEXT
  ,IN in_wtid BYTEA
  ,IN in_exchange_url TEXT
  ,IN in_metadata TEXT
  ,OUT out_tx_id INT8
  ,OUT out_found BOOLEAN
  ,OUT out_initiated BOOLEAN
)
LANGUAGE plpgsql AS $$
DECLARE
init_id INT8;
local_amount taler_amount;
local_subject TEXT;
local_credit_payto TEXT;
local_wtid BYTEA;
local_exchange_base_url TEXT;
local_metadata TEXT;
local_end_to_end_id TEXT;
BEGIN
-- Check if already registered
SELECT outgoing_transaction_id, subject, credit_payto, (amount).val, (amount).frac,
    wtid, exchange_base_url, metadata
  INTO out_tx_id, local_subject, local_credit_payto, local_amount.val, local_amount.frac, 
    local_wtid, local_exchange_base_url, local_metadata
  FROM outgoing_transactions LEFT JOIN talerable_outgoing_transactions USING (outgoing_transaction_id)
  WHERE end_to_end_id = in_end_to_end_id OR acct_svcr_ref = in_acct_svcr_ref;
out_found=FOUND;
IF out_found THEN
  -- Check metadata
  -- TODO take subject if missing and more detailed credit payto
  IF in_subject IS NOT NULL AND local_subject != in_subject THEN
    RAISE NOTICE 'outgoing tx %: stored subject is ''%'' got ''%''', in_end_to_end_id, local_subject, in_subject;
  END IF;
  IF in_credit_payto IS NOT NULL AND local_credit_payto != in_credit_payto THEN
    RAISE NOTICE 'outgoing tx %: stored subject credit payto is % got %', in_end_to_end_id, local_credit_payto, in_credit_payto;
  END IF;
  IF local_amount IS DISTINCT FROM in_amount THEN
    RAISE NOTICE 'outgoing tx %: stored amount is % got %', in_end_to_end_id, local_amount, in_amount;
  END IF;
  IF local_wtid IS DISTINCT FROM in_wtid THEN
    RAISE NOTICE 'outgoing tx %: stored wtid is % got %', in_end_to_end_id, local_wtid, in_wtid;
  END IF;
  IF local_exchange_base_url IS DISTINCT FROM in_exchange_url THEN
    RAISE NOTICE 'outgoing tx %: stored exchange base url is % got %', in_end_to_end_id, local_exchange_base_url, in_exchange_url;
  END IF;
  IF local_metadata IS DISTINCT FROM in_metadata THEN
    RAISE NOTICE 'outgoing tx %: stored metadata is % got %', in_end_to_end_id, local_metadata, in_metadata;
  END IF;
END IF;

-- Check if initiated
SELECT initiated_outgoing_transaction_id, subject, credit_payto, (amount).val, (amount).frac,
    wtid, exchange_base_url, metadata
  INTO init_id, local_subject, local_credit_payto, local_amount.val, local_amount.frac, 
    local_wtid, local_exchange_base_url, local_metadata
  FROM initiated_outgoing_transactions LEFT JOIN transfer_operations USING (initiated_outgoing_transaction_id)
  WHERE end_to_end_id = in_end_to_end_id;
out_initiated=FOUND;
IF out_initiated AND NOT out_found THEN
  -- Check metadata
  -- TODO take subject if missing and more detailed credit payto
  IF in_subject IS NOT NULL AND local_subject != in_subject THEN
    RAISE NOTICE 'outgoing tx %: initiated subject is ''%'' got ''%''', in_end_to_end_id, local_subject, in_subject;
  END IF;
  IF local_credit_payto IS DISTINCT FROM in_credit_payto THEN
    RAISE NOTICE 'outgoing tx %: initiated subject credit payto is % got %', in_end_to_end_id, local_credit_payto, in_credit_payto;
  END IF;
  IF local_amount IS DISTINCT FROM in_amount THEN
    RAISE NOTICE 'outgoing tx %: initiated amount is % got %', in_end_to_end_id, local_amount, in_amount;
  END IF;
  IF in_wtid IS NOT NULL AND local_wtid != in_wtid THEN
    RAISE NOTICE 'outgoing tx %: initiated wtid is % got %', in_end_to_end_id, local_wtid, in_wtid;
  END IF;
  IF in_exchange_url IS NOT NULL AND local_exchange_base_url != in_exchange_url THEN
    RAISE NOTICE 'outgoing tx %: initiated exchange base url is % got %', in_end_to_end_id, local_exchange_base_url, in_exchange_url;
  END IF;
  IF in_metadata IS NOT NULL AND local_metadata != in_metadata THEN
    RAISE NOTICE 'outgoing tx %: initiated metadata is % got %', in_end_to_end_id, local_metadata, in_metadata;
  END IF;
END IF;

IF NOT out_found THEN
  -- Store the transaction in the database
  INSERT INTO outgoing_transactions (
     amount
    ,debit_fee
    ,subject
    ,execution_time
    ,credit_payto
    ,end_to_end_id
    ,acct_svcr_ref
  ) VALUES (
     in_amount
    ,in_debit_fee
    ,in_subject
    ,in_execution_time
    ,in_credit_payto
    ,in_end_to_end_id
    ,in_acct_svcr_ref
  )
    RETURNING outgoing_transaction_id
      INTO out_tx_id;

  -- Register as talerable if contains wtid
  IF in_wtid IS NOT NULL THEN
    SELECT end_to_end_id INTO local_end_to_end_id
      FROM talerable_outgoing_transactions
      JOIN outgoing_transactions USING (outgoing_transaction_id)
      WHERE wtid=in_wtid;
    IF FOUND THEN
      IF local_end_to_end_id != in_end_to_end_id THEN
        RAISE NOTICE 'wtid reuse: tx % and tx % have the same wtid %', in_end_to_end_id, local_end_to_end_id, in_wtid;
      END IF;
    ELSE
      INSERT INTO talerable_outgoing_transactions(
        outgoing_transaction_id,
        wtid,
        exchange_base_url,
        metadata
      ) VALUES (
        out_tx_id,
        in_wtid,
        in_exchange_url,
        in_metadata
      );
      PERFORM pg_notify('nexus_outgoing_tx', out_tx_id::text);
    END IF;
  END IF;

  IF out_initiated THEN
    -- Reconciles the related initiated transaction
    UPDATE initiated_outgoing_transactions
      SET 
        outgoing_transaction_id = out_tx_id
        ,status = 'success'
        ,status_msg = null
      WHERE initiated_outgoing_transaction_id = init_id 
        AND status != 'late_failure';
    
    -- Reconciles the related initiated batch
    UPDATE initiated_outgoing_batches
      SET status = 'success', status_msg = null
      WHERE message_id = in_msg_id AND status NOT IN ('success', 'permanent_failure', 'late_failure');
  END IF;
END IF;
END $$;
COMMENT ON FUNCTION register_outgoing
  IS 'Register an outgoing transaction and optionally reconciles the related initiated transaction with it';

CREATE FUNCTION register_incoming(
  IN in_amount taler_amount
  ,IN in_credit_fee taler_amount
  ,IN in_subject TEXT
  ,IN in_execution_time INT8
  ,IN in_debit_payto TEXT
  ,IN in_uetr UUID
  ,IN in_tx_id TEXT
  ,IN in_acct_svcr_ref TEXT
  ,IN in_type taler_incoming_type
  ,IN in_metadata BYTEA
  ,IN in_qr_reference_number TEXT
  -- Error status
  ,OUT out_reserve_pub_reuse BOOLEAN
  ,OUT out_mapping_reuse BOOLEAN
  ,OUT out_unknown_mapping BOOLEAN
  -- Success return
  ,OUT out_found BOOLEAN
  ,OUT out_completed BOOLEAN
  ,OUT out_talerable BOOLEAN
  ,OUT out_pending BOOLEAN
  ,OUT out_tx_id INT8
  ,OUT out_bounce_id TEXT
)
LANGUAGE plpgsql AS $$
DECLARE
local_ref TEXT;
local_amount taler_amount;
local_subject TEXT;
local_debit_payto TEXT;
local_authorization_pub BYTEA;
local_authorization_sig BYTEA;
BEGIN
IF in_credit_fee = (0, 0)::taler_amount THEN
  in_credit_fee = NULL;
END IF;
out_pending=FALSE;

-- Check if already registered
SELECT incoming_transaction_id, tx.subject, debit_payto, (tx.amount).val, (tx.amount).frac, metadata IS NOT NULL, end_to_end_id
  INTO out_tx_id, local_subject, local_debit_payto, local_amount.val, local_amount.frac, out_talerable, out_bounce_id
  FROM incoming_transactions AS tx
    LEFT JOIN talerable_incoming_transactions USING (incoming_transaction_id) 
    LEFT JOIN bounced_transactions USING (incoming_transaction_id)
    LEFT JOIN initiated_outgoing_transactions USING (initiated_outgoing_transaction_id)
  WHERE uetr = in_uetr OR tx_id = in_tx_id OR acct_svcr_ref = in_acct_svcr_ref;
out_found=FOUND;

IF NOT out_found OR NOT out_talerable THEN
  -- Resolve mapping logic
  IF in_type = 'map' OR in_qr_reference_number IS NOT NULL THEN
    SELECT type, account_pub, authorization_pub, authorization_sig,
        incoming_transaction_id IS NOT NULL AND NOT recurrent,
        incoming_transaction_id IS NOT NULL AND recurrent
      INTO in_type, in_metadata, local_authorization_pub, local_authorization_sig, out_mapping_reuse, out_pending
      FROM prepared_transfers
      WHERE authorization_pub = in_metadata OR reference_number = in_qr_reference_number;
    out_unknown_mapping = NOT FOUND;
    IF out_unknown_mapping OR out_mapping_reuse THEN
      RETURN;
    END IF;
  END IF;

  -- Check reserve pub reuse
  out_reserve_pub_reuse=NOT out_pending AND in_type = 'reserve' AND EXISTS(SELECT FROM talerable_incoming_transactions WHERE metadata = in_metadata AND type = 'reserve');
  IF out_reserve_pub_reuse THEN
    RETURN;
  END IF; 
END IF;

IF out_found THEN
  local_ref=COALESCE(in_uetr::text, in_tx_id, in_acct_svcr_ref);
  -- Check metadata
  IF in_subject != local_subject THEN
    RAISE NOTICE 'incoming tx %: stored subject is ''%'' got ''%''', local_ref, local_subject, in_subject;
  END IF;
  IF in_debit_payto != local_debit_payto THEN
    RAISE NOTICE 'incoming tx %: stored subject debit payto is % got %', local_ref, local_debit_payto, in_debit_payto;
  END IF;
  IF local_amount != in_amount THEN
    RAISE NOTICE 'incoming tx %: stored amount is % got %', local_ref, local_amount, in_amount;
  END IF;
  UPDATE incoming_transactions 
    SET subject=COALESCE(subject, in_subject),
        debit_payto=COALESCE(debit_payto, in_debit_payto),
        uetr=COALESCE(uetr, in_uetr),
        tx_id=COALESCE(tx_id, in_tx_id),
        acct_svcr_ref=COALESCE(acct_svcr_ref, in_acct_svcr_ref)
    WHERE incoming_transaction_id = out_tx_id;
  out_completed=local_debit_payto IS NULL AND in_debit_payto IS NOT NULL;
  IF out_completed THEN
    PERFORM pg_notify('nexus_revenue_tx', out_tx_id::text);
  END IF;
ELSE
  -- Store the transaction in the database
  INSERT INTO incoming_transactions (
    amount
    ,credit_fee
    ,subject
    ,execution_time
    ,debit_payto
    ,uetr
    ,tx_id
    ,acct_svcr_ref
  ) VALUES (
    in_amount
    ,in_credit_fee
    ,in_subject
    ,in_execution_time
    ,in_debit_payto
    ,in_uetr
    ,in_tx_id
    ,in_acct_svcr_ref
  ) RETURNING incoming_transaction_id INTO out_tx_id;
  IF in_subject IS NOT NULL AND in_debit_payto IS NOT NULL THEN
    PERFORM pg_notify('nexus_revenue_tx', out_tx_id::text);
  END IF;
  out_talerable=FALSE;
END IF;

-- Register as talerable if not already registered as such and not already bounced
IF in_type IS NOT NULL AND NOT out_talerable AND out_bounce_id IS NULL THEN
  If out_pending THEN
    -- Delay talerable registration until mapping again
    INSERT INTO pending_recurrent_incoming_transactions (incoming_transaction_id, authorization_pub)
      VALUES (out_tx_id, local_authorization_pub);
  ELSE
    UPDATE prepared_transfers
    SET incoming_transaction_id = out_tx_id
    WHERE (
       incoming_transaction_id IS NULL AND account_pub = in_metadata AND type='reserve'
    ) OR authorization_pub = local_authorization_pub;
    -- We cannot use ON CONFLICT here because conversion use a trigger before insertion that isn't idempotent
    INSERT INTO talerable_incoming_transactions (
      incoming_transaction_id
      ,type
      ,metadata
      ,authorization_pub
      ,authorization_sig
    ) VALUES (
      out_tx_id
      ,in_type
      ,in_metadata
      ,local_authorization_pub
      ,local_authorization_sig
    );
    PERFORM pg_notify('nexus_incoming_tx', out_tx_id::text);
    out_talerable=TRUE;
  END IF;
END IF;
END $$;

CREATE FUNCTION register_and_bounce_incoming(
  IN in_amount taler_amount
  ,IN in_credit_fee taler_amount
  ,IN in_subject TEXT
  ,IN in_execution_time INT8
  ,IN in_debit_payto TEXT
  ,IN in_uetr UUID
  ,IN in_tx_id TEXT
  ,IN in_acct_svcr_ref TEXT
  ,IN in_bounce_amount taler_amount
  ,IN in_now_date INT8
  ,IN in_bounce_id TEXT
  ,IN in_cause TEXT
  -- Error status
  ,OUT out_talerable BOOLEAN
  -- Success return
  ,OUT out_found BOOLEAN
  ,OUT out_completed BOOLEAN
  ,OUT out_tx_id INT8
  ,OUT out_bounce_id TEXT
)
LANGUAGE plpgsql AS $$
DECLARE
init_id INT8;
bounce_amount taler_amount;
BEGIN
-- Register incoming transaction
SELECT reg.out_found, reg.out_completed, reg.out_tx_id, reg.out_talerable
  INTO out_found, out_completed, out_tx_id, out_talerable
  FROM register_incoming(in_amount, in_credit_fee, in_subject, in_execution_time, in_debit_payto, in_uetr, in_tx_id, in_acct_svcr_ref, NULL, NULL, NULL) as reg;
-- Cannot bounce a transaction registered as talerable
IF out_talerable THEN
  RETURN;
END IF;
-- Bounce incoming transaction
SELECT bounce.out_bounce_id INTO out_bounce_id FROM bounce_incoming(out_tx_id, in_bounce_amount, in_bounce_id, in_now_date, in_cause) AS bounce;
END $$;

CREATE FUNCTION bounce_incoming(
  IN in_tx_id INT8
  ,IN in_bounce_amount taler_amount
  ,IN in_bounce_id TEXT
  ,IN in_now_date INT8
  ,IN in_cause TEXT
  ,OUT out_bounce_id TEXT
)
LANGUAGE plpgsql AS $$
DECLARE
local_bank_id TEXT;
payto_uri TEXT;
init_id INT8;
BEGIN
-- Check if already bounced
SELECT end_to_end_id INTO out_bounce_id
  FROM libeufin_nexus.initiated_outgoing_transactions
  JOIN libeufin_nexus.bounced_transactions USING (initiated_outgoing_transaction_id)
  WHERE incoming_transaction_id = in_tx_id;

-- Else initiate the bounce transaction
IF NOT FOUND THEN
  out_bounce_id = in_bounce_id;
  -- Get incoming transaction bank ID and creditor
  SELECT COALESCE(uetr::text, tx_id, acct_svcr_ref), debit_payto 
    INTO local_bank_id, payto_uri
    FROM libeufin_nexus.incoming_transactions
    WHERE incoming_transaction_id = in_tx_id;
  -- Initiate the bounce transaction
  INSERT INTO libeufin_nexus.initiated_outgoing_transactions (
    amount
    ,subject
    ,credit_payto
    ,initiation_time
    ,end_to_end_id
  ) VALUES (
    in_bounce_amount
    ,'bounce ' || local_bank_id || ': ' || in_cause
    ,payto_uri
    ,in_now_date
    ,in_bounce_id
  )
  RETURNING initiated_outgoing_transaction_id INTO init_id;
  -- Register the bounce
  INSERT INTO libeufin_nexus.bounced_transactions (incoming_transaction_id, initiated_outgoing_transaction_id)
    VALUES (in_tx_id, init_id);
END IF;

-- Delete from pending if any
DELETE FROM libeufin_nexus.pending_recurrent_incoming_transactions WHERE incoming_transaction_id = in_tx_id;
END$$;

CREATE FUNCTION taler_transfer(
  IN in_request_uid BYTEA,
  IN in_wtid BYTEA,
  IN in_subject TEXT,
  IN in_amount taler_amount,
  IN in_exchange_base_url TEXT,
  IN in_metadata TEXT,
  IN in_credit_account_payto TEXT,
  IN in_end_to_end_id TEXT,
  IN in_timestamp INT8,
  -- Error status
  OUT out_request_uid_reuse BOOLEAN,
  OUT out_wtid_reuse BOOLEAN,
  -- Success return
  OUT out_tx_row_id INT8,
  OUT out_timestamp INT8
)
LANGUAGE plpgsql AS $$
BEGIN
-- Check for idempotence and conflict
SELECT (amount != in_amount 
          OR credit_payto != in_credit_account_payto
          OR exchange_base_url != in_exchange_base_url
          OR metadata != in_metadata
          OR wtid != in_wtid)
        ,transfer_operations.initiated_outgoing_transaction_id, initiation_time
  INTO out_request_uid_reuse, out_tx_row_id, out_timestamp
  FROM transfer_operations
      JOIN initiated_outgoing_transactions
        ON transfer_operations.initiated_outgoing_transaction_id=initiated_outgoing_transactions.initiated_outgoing_transaction_id 
  WHERE transfer_operations.request_uid = in_request_uid;
IF FOUND THEN
  RETURN;
END IF;
out_wtid_reuse = EXISTS(SELECT FROM transfer_operations WHERE wtid = in_wtid);
IF out_wtid_reuse THEN
  RETURN;
END IF;
out_timestamp=in_timestamp;
-- Initiate bank transfer
INSERT INTO initiated_outgoing_transactions (
  amount
  ,subject
  ,credit_payto
  ,initiation_time
  ,end_to_end_id
) VALUES (
  in_amount
  ,in_subject
  ,in_credit_account_payto
  ,in_timestamp
  ,in_end_to_end_id
) RETURNING initiated_outgoing_transaction_id INTO out_tx_row_id;
-- Register outgoing transaction
INSERT INTO transfer_operations(
  initiated_outgoing_transaction_id
  ,request_uid
  ,wtid
  ,exchange_base_url
  ,metadata
) VALUES (
  out_tx_row_id
  ,in_request_uid
  ,in_wtid
  ,in_exchange_base_url
  ,in_metadata
);
out_timestamp = in_timestamp;
PERFORM pg_notify('nexus_outgoing_tx', out_tx_row_id::text);
END $$;

CREATE FUNCTION batch_outgoing_transactions(
  IN in_timestamp INT8,
  IN batch_ebics_id TEXT,
  IN require_ack BOOLEAN
)
RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
batch_id INT8;
local_sum taler_amount DEFAULT (0, 0)::taler_amount;
tx record;
BEGIN
-- Create a new batch only if some transactions are not batched
IF EXISTS(SELECT FROM initiated_outgoing_transactions WHERE initiated_outgoing_batch_id IS NULL AND (NOT require_ack OR NOT awaiting_ack)) THEN
  -- Create batch
  INSERT INTO initiated_outgoing_batches (creation_date, message_id)
    VALUES (in_timestamp, batch_ebics_id)
    RETURNING initiated_outgoing_batch_id INTO batch_id;
  -- Link batched payment while computing the sum of amounts
  FOR tx IN UPDATE initiated_outgoing_transactions 
    SET initiated_outgoing_batch_id=batch_id
    WHERE initiated_outgoing_batch_id IS NULL
    AND (NOT require_ack OR NOT awaiting_ack)
    RETURNING amount
  LOOP
    SELECT sum.val, sum.frac 
    INTO local_sum.val, local_sum.frac
    FROM amount_add(local_sum, tx.amount) AS sum;
  END LOOP;
  -- Update the batch with the sum of amounts
  UPDATE initiated_outgoing_batches SET sum=local_sum WHERE initiated_outgoing_batch_id=batch_id;
END IF;
END $$;

CREATE FUNCTION batch_status_update(
  IN in_message_id text,
  IN in_status submission_state,
  IN in_status_msg text,
  OUT out_ok BOOLEAN
)
LANGUAGE plpgsql AS $$
DECLARE
local_batch_id INT8;
BEGIN
  -- Check if there is a batch for this message id
  SELECT initiated_outgoing_batch_id INTO local_batch_id
    FROM initiated_outgoing_batches
    WHERE message_id = in_message_id;
  out_ok=FOUND;
  IF FOUND THEN
    -- Update unsettled batch status 
    UPDATE initiated_outgoing_batches 
    SET status = in_status, status_msg = in_status_msg
    WHERE initiated_outgoing_batch_id = local_batch_id 
      AND status NOT IN ('success', 'permanent_failure', 'late_failure');

    -- When a batch succeed it doesn't mean that individual transaction also succeed
    IF in_status = 'success' THEN
      in_status = 'pending';
    END IF;

    -- Update unsettled batch's transaction status
    UPDATE initiated_outgoing_transactions 
    SET status = in_status, status_msg = in_status_msg
    WHERE initiated_outgoing_batch_id = local_batch_id
      AND status NOT IN ('success', 'permanent_failure', 'late_failure');
  END IF;
END $$;

CREATE FUNCTION tx_status_update(
  IN in_end_to_end_id text,
  IN in_message_id text,
  IN in_status submission_state,
  IN in_status_msg text,
  OUT out_ok BOOLEAN
)
LANGUAGE plpgsql AS $$
DECLARE
local_status submission_state;
local_tx_id INT8;
BEGIN
  -- Check current tx status
  SELECT initiated_outgoing_transaction_id, status INTO local_tx_id, local_status
    FROM initiated_outgoing_transactions
    WHERE end_to_end_id = in_end_to_end_id;
  out_ok=FOUND;
  IF FOUND THEN
    -- Update unsettled transaction status
    IF in_status = 'permanent_failure' OR local_status NOT IN ('success', 'permanent_failure', 'late_failure') THEN
      IF in_status = 'permanent_failure' AND local_status = 'success' THEN
        in_status = 'late_failure';
      END IF;
      UPDATE initiated_outgoing_transactions 
      SET status = in_status, status_msg = in_status_msg
      WHERE initiated_outgoing_transaction_id = local_tx_id;
    END IF;

    -- Update unsettled batch status
    UPDATE initiated_outgoing_batches
    SET status = 'success', status_msg = NULL
    WHERE message_id = in_message_id
      AND status NOT IN ('success', 'permanent_failure', 'late_failure');
  END IF;
END $$;

CREATE FUNCTION register_prepared_transfers (
  IN in_type taler_incoming_type,
  IN in_account_pub BYTEA,
  IN in_authorization_pub BYTEA,
  IN in_authorization_sig BYTEA,
  IN in_recurrent BOOLEAN,
  IN in_reference_number TEXT,
  IN in_timestamp INT8,
  -- Error status
  OUT out_subject_reuse BOOLEAN,
  OUT out_reserve_pub_reuse BOOLEAN
)
LANGUAGE plpgsql AS $$
DECLARE
  talerable_tx INT8;
  idempotent BOOLEAN;
BEGIN

-- Check idempotency 
SELECT type = in_type 
    AND account_pub = in_account_pub
    AND recurrent = in_recurrent
    AND reference_number = in_reference_number
INTO idempotent
FROM prepared_transfers
WHERE authorization_pub = in_authorization_pub;

-- Check idempotency and delay garbage collection
IF FOUND AND idempotent THEN
  UPDATE prepared_transfers
  SET registered_at=in_timestamp
  WHERE authorization_pub=in_authorization_pub;
  RETURN;
END IF;

-- Check reserve pub reuse and reference_number clash
out_reserve_pub_reuse=in_type = 'reserve' AND (
  EXISTS(SELECT FROM talerable_incoming_transactions WHERE metadata = in_account_pub AND type = 'reserve')
  OR EXISTS(SELECT FROM prepared_transfers WHERE account_pub = in_account_pub AND type = 'reserve' AND authorization_pub != in_authorization_pub)
);
out_subject_reuse=EXISTS(SELECT FROM prepared_transfers WHERE authorization_pub != in_authorization_pub AND reference_number = in_reference_number);
IF out_reserve_pub_reuse OR out_subject_reuse THEN
  RETURN;
END IF;

IF in_recurrent THEN
  -- Finalize one pending right now
  WITH moved_tx AS (
    DELETE FROM pending_recurrent_incoming_transactions
    WHERE incoming_transaction_id = (
      SELECT incoming_transaction_id
      FROM pending_recurrent_incoming_transactions
      JOIN incoming_transactions USING (incoming_transaction_id)
      WHERE authorization_pub = in_authorization_pub
      ORDER BY execution_time ASC
      LIMIT 1
    )
    RETURNING incoming_transaction_id
  )
  INSERT INTO talerable_incoming_transactions (incoming_transaction_id, type, metadata, authorization_pub, authorization_sig)
  SELECT moved_tx.incoming_transaction_id, in_type, in_account_pub, in_authorization_pub, in_authorization_sig
  FROM moved_tx
  RETURNING incoming_transaction_id INTO talerable_tx;
  IF talerable_tx IS NOT NULL THEN
    PERFORM pg_notify('nexus_incoming_tx', talerable_tx::text);
  END IF;
ELSE
  -- Bounce all pending
  PERFORM bounce_incoming(incoming_transaction_id, amount, ebics_id_gen(), in_timestamp, 'cancelled mapping')
  FROM incoming_transactions
  JOIN pending_recurrent_incoming_transactions USING (incoming_transaction_id)
  WHERE authorization_pub = in_authorization_pub;
END IF;

-- Upsert registration
INSERT INTO prepared_transfers (
  type,
  account_pub,
  authorization_pub,
  authorization_sig,
  recurrent,
  reference_number,
  registered_at,
  incoming_transaction_id
) VALUES (
  in_type,
  in_account_pub,
  in_authorization_pub,
  in_authorization_sig,
  in_recurrent,
  in_reference_number,
  in_timestamp,
  talerable_tx
) ON CONFLICT (authorization_pub)
DO UPDATE SET
  type = EXCLUDED.type,
  account_pub = EXCLUDED.account_pub,
  recurrent = EXCLUDED.recurrent,
  reference_number = EXCLUDED.reference_number,
  registered_at = EXCLUDED.registered_at,
  incoming_transaction_id = EXCLUDED.incoming_transaction_id,
  authorization_sig = EXCLUDED.authorization_sig;
END $$;

CREATE FUNCTION delete_prepared_transfers (
  IN in_authorization_pub BYTEA,
  IN in_timestamp INT8,
  OUT out_found BOOLEAN
)
LANGUAGE plpgsql AS $$
BEGIN

-- Bounce all pending
PERFORM bounce_incoming(incoming_transaction_id, amount, ebics_id_gen(), in_timestamp, 'cancelled mapping')
FROM incoming_transactions
JOIN pending_recurrent_incoming_transactions USING (incoming_transaction_id)
WHERE authorization_pub = in_authorization_pub;

-- Delete registration
DELETE FROM prepared_transfers
WHERE authorization_pub = in_authorization_pub;
out_found = FOUND;

END $$;