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

-- @file merchant-0021.sql
-- @brief Tables for statistics
-- @author Christian Grothoff


BEGIN;

-- Check patch versioning is in place.
SELECT _v.register_patch('merchant-0021', NULL, NULL);

SET search_path TO merchant;

COMMENT ON TABLE merchant_transfers
  IS 'table represents confirmed incoming wire transfers';
COMMENT ON COLUMN merchant_transfers.credit_amount
  IS 'actual value of the confirmed wire transfer';

CREATE TABLE merchant_expected_transfers
  (expected_credit_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
  ,exchange_url TEXT NOT NULL
  ,wtid BYTEA NOT NULL CHECK (LENGTH(wtid)=32)
  ,expected_credit_amount taler_amount_currency
  ,wire_fee taler_amount_currency
  ,account_serial INT8 NOT NULL
   REFERENCES merchant_accounts (account_serial) ON DELETE CASCADE
  ,expected_time INT8 NOT NULL
  ,retry_time INT8 NOT NULL DEFAULT (0)
  ,last_http_status INT4 DEFAULT NULL
  ,last_ec INT4 DEFAULT NULL
  ,last_detail TEXT DEFAULT NULL
  ,retry_needed BOOLEAN NOT NULL DEFAULT TRUE
  ,signkey_serial BIGINT
   REFERENCES merchant_exchange_signing_keys (signkey_serial)
   ON DELETE CASCADE
  ,exchange_sig BYTEA CHECK (LENGTH(exchange_sig)=64) DEFAULT NULL
  ,h_details BYTEA CHECK (LENGTH(h_details)=64) DEFAULT NULL
  ,confirmed BOOLEAN NOT NULL DEFAULT FALSE
  ,UNIQUE (wtid, exchange_url, account_serial)
  );
COMMENT ON TABLE merchant_expected_transfers
  IS 'expected incoming wire transfers';
COMMENT ON COLUMN merchant_expected_transfers.expected_credit_serial
  IS 'Unique identifier for this expected wire transfer in this backend';
COMMENT ON COLUMN merchant_expected_transfers.exchange_url
  IS 'Base URL of the exchange that originated the wire transfer as extracted from the wire transfer subject';
COMMENT ON COLUMN merchant_expected_transfers.wtid
  IS 'Unique wire transfer identifier (or at least, should be unique by protocol) as selected by the exchange and extracted from the wire transfer subject';
COMMENT ON COLUMN merchant_expected_transfers.expected_credit_amount
  IS 'expected actual value of the (aggregated) wire transfer, excluding the wire fee; NULL if unknown';
COMMENT ON COLUMN merchant_expected_transfers.wire_fee
  IS 'wire fee the exchange claims to have charged us; NULL if unknown';
COMMENT ON COLUMN merchant_expected_transfers.account_serial
  IS 'Merchant bank account that should receive this wire transfer; also implies the merchant instance implicated by the wire transfer';
COMMENT ON COLUMN merchant_expected_transfers.expected_time
  IS 'Time when we should expect the exchange do do the wire transfer';
COMMENT ON COLUMN merchant_expected_transfers.retry_time
  IS 'Time when we should next inquire at the exchange about this wire transfer; used by taler-merchant-reconciliation to limit retries with the exchange in case of failures';
COMMENT ON COLUMN merchant_expected_transfers.last_http_status
  IS 'HTTP status of the last request to the exchange, 0 on timeout or if there was no request (200 on success)';
COMMENT ON COLUMN merchant_expected_transfers.last_ec
  IS 'Taler error code from the last request to the exchange, 0 on success or if there was no request';
COMMENT ON COLUMN merchant_expected_transfers.last_detail
  IS 'Taler error detail from the last request to the exchange, NULL on success or if there was no request';
COMMENT ON COLUMN merchant_expected_transfers.signkey_serial
  IS 'Identifies the online signing key of the exchange used to make the exchange_sig';
COMMENT ON COLUMN merchant_expected_transfers.exchange_sig
  IS 'Signature over the aggregation response from the exchange, or NULL on error or if we did not yet make that request';
COMMENT ON COLUMN merchant_expected_transfers.confirmed
  IS 'true once the merchant confirmed that this transfer was received and a matching transfer exists in the merchant_transfers table; set automatically via INSERT TRIGGER merchant_expected_transfers_insert_trigger';
COMMENT ON COLUMN merchant_expected_transfers.retry_needed
  IS 'true if we need to retry the HTTP request to the exchange (never did it, or transient failure)';
COMMENT ON COLUMN merchant_expected_transfers.h_details
  IS 'Hash over the aggregation details returned by the exchange, provided here for fast exchange_sig validation';

CREATE INDEX merchant_expected_transfers_by_open
  ON merchant_expected_transfers
  (retry_time ASC)
  WHERE NOT confirmed OR retry_needed;
COMMENT ON INDEX merchant_expected_transfers_by_open
  IS 'For select_open_transfers';

-- Migrate data. The backend will just re-do all of the
-- reconciliation work, so we only preserve confirmed transfers.
-- However, we must put those also into the new "merchant_expected_transfers"
-- table already.
DELETE FROM merchant_transfers
  WHERE NOT confirmed;

-- This index was replaced by merchant_expected_transfers_by_open.
DROP INDEX merchant_transfers_by_open;

-- These columns will be in the new merchant_expected_transfers table.
ALTER TABLE merchant_transfers
  ADD COLUMN bank_serial_id INT8,
  ADD COLUMN expected BOOL DEFAULT FALSE,
  ADD COLUMN execution_time INT8 DEFAULT (0),
  DROP COLUMN ready_time,
  DROP COLUMN confirmed,
  DROP COLUMN failed,
  DROP COLUMN verified,
  DROP COLUMN validation_status;

COMMENT ON COLUMN merchant_transfers.expected
  IS 'True if this wire transfer was expected (has matching entry in merchant_expected_transfers); set automatically via INSERT TRIGGER merchant_transfers_insert_trigger';
COMMENT ON COLUMN merchant_transfers.bank_serial_id
  IS 'Row ID of the wire transfer from the automated import; NULL if not available (like when a human manually imported the transfer)';
COMMENT ON COLUMN merchant_transfers.execution_time
  IS 'Time when the merchant transfer was added and thus roughly received in our bank account';

-- Note: if the bank_serial_id is NULL (manual import), we always
-- consider confirmed transfers to be 'UNIQUE'; thus we do
-- NOT use "NULLS NOT DISTINCT" here.

ALTER TABLE merchant_transfers
  DROP CONSTRAINT merchant_transfers_wtid_exchange_url_account_serial_key,
  ADD CONSTRAINT merchant_transfers_unique
    UNIQUE (wtid, exchange_url, account_serial, bank_serial_id);


-- Create triggers to set confirmed/expected status on INSERT.
CREATE FUNCTION merchant_expected_transfers_insert_trigger()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  UPDATE merchant_transfers
     SET expected = TRUE
   WHERE wtid = NEW.wtid
     AND exchange_url = NEW.exchange_url
     AND credit_amount = NEW.expected_credit_amount;
  NEW.confirmed = FOUND;
  RETURN NEW;
END $$;
COMMENT ON FUNCTION merchant_expected_transfers_insert_trigger
  IS 'Sets "confirmed" to TRUE for the new record if the expected transfer was already confirmed, and updates the already confirmed transfer to "expected"';

-- Whenever an expected transfer is added, check if it was already confirmed
CREATE TRIGGER merchant_expected_transfers_on_insert
  BEFORE INSERT
    ON merchant.merchant_expected_transfers
  FOR EACH ROW EXECUTE FUNCTION merchant_expected_transfers_insert_trigger();


CREATE FUNCTION merchant_transfers_insert_trigger()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  UPDATE merchant_expected_transfers
     SET confirmed = TRUE
   WHERE wtid = NEW.wtid
     AND exchange_url = NEW.exchange_url
     AND expected_credit_amount = NEW.credit_amount;
  NEW.expected = FOUND;
  RETURN NEW;
END $$;
COMMENT ON FUNCTION merchant_transfers_insert_trigger
  IS 'Sets "expected" to TRUE for the new record if the transfer was already expected, and updates the already confirmed transfer to "confirmed"';

-- Whenever a transfer is addeded, check if it was already expected
CREATE TRIGGER merchant_transfers_on_insert
  BEFORE INSERT
    ON merchant.merchant_transfers
  FOR EACH ROW EXECUTE FUNCTION merchant_transfers_insert_trigger();


-- Adjust contract terms table.
ALTER TABLE merchant_deposits
  ADD COLUMN settlement_retry_needed BOOL DEFAULT TRUE,
  ADD COLUMN settlement_retry_time INT8 DEFAULT (0),
  ADD COLUMN settlement_last_http_status INT4 DEFAULT NULL,
  ADD COLUMN settlement_last_ec INT4 DEFAULT NULL,
  ADD COLUMN settlement_last_detail TEXT DEFAULT NULL,
  ADD COLUMN settlement_wtid BYTEA CHECK (LENGTH(settlement_wtid)=32) DEFAULT NULL,
  ADD COLUMN settlement_coin_contribution taler_amount_currency DEFAULT NULL,
  ADD COLUMN settlement_expected_credit_serial INT8 DEFAULT NULL
    REFERENCES merchant_expected_transfers (expected_credit_serial),
  ADD COLUMN signkey_serial INT8 DEFAULT NULL
    REFERENCES merchant_exchange_signing_keys (signkey_serial)
    ON DELETE CASCADE,
  ADD COLUMN settlement_exchange_sig BYTEA
    DEFAULT NULL CHECK (LENGTH(settlement_exchange_sig)=64);

COMMENT ON COLUMN merchant_deposits.settlement_retry_needed
  IS 'True if we should ask the exchange in the future about the settlement';
COMMENT ON COLUMN merchant_deposits.settlement_retry_time
  IS 'When should we next ask the exchange about the settlement wire transfer for this coin, initially set to the wire transfer deadline plus a bit of slack';
COMMENT ON COLUMN merchant_deposits.settlement_last_http_status
  IS 'HTTP status of our last inquiry with the exchange for this deposit, NULL if we never inquired, 0 on timeout';
COMMENT ON COLUMN merchant_deposits.settlement_last_ec
  IS 'Taler error code for our last inquiry with the exchange for this deposit, NULL if we never inquired, 0 on success';
COMMENT ON COLUMN merchant_deposits.settlement_last_detail
  IS 'Taler error detail for our last inquiry with the exchange for this deposit, NULL if we never inquired or on success';
COMMENT ON COLUMN merchant_deposits.settlement_coin_contribution
  IS 'Contribution of this coin to the overall wire transfer made by the exchange as claimed by exchange_sig; should match amount_with_fee minus deposit_fee, NULL if we did not get a reply from the exchange';
COMMENT ON COLUMN merchant_deposits.settlement_expected_credit_serial
  IS 'Identifies the expected wire transfer from the exchange to the merchant that settled the deposit of coin, NULL if unknown';
COMMENT ON COLUMN merchant_deposits.signkey_serial
  IS 'Identifies the online signing key of the exchange used to make the exchange_sig, NULL for none';
COMMENT ON COLUMN merchant_deposits.settlement_exchange_sig
  IS 'Exchange signature of purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE, NULL if we did not get such an exchange signature';

CREATE INDEX merchant_deposits_by_settlement_open
  ON merchant_deposits
  (settlement_retry_time ASC)
  WHERE settlement_retry_needed;
COMMENT ON INDEX merchant_deposits_by_settlement_open
  IS 'For select_open_deposit_settlements';

CREATE INDEX merchant_deposits_by_deposit_confirmation
  ON merchant_deposits
  (deposit_confirmation_serial);


-- No 1:n mapping necessary, integrated into merchant_deposits table above.
DROP TABLE merchant_deposit_to_transfer;

-- We need to fully re-do the merchant_transfer_to_coin table,
-- and data should be re-constructed, so drop and re-build.
DROP TABLE merchant_transfer_to_coin;
CREATE TABLE merchant_expected_transfer_to_coin
  (deposit_serial BIGINT UNIQUE NOT NULL
     REFERENCES merchant_deposits (deposit_serial) ON DELETE CASCADE
  ,expected_credit_serial BIGINT NOT NULL
     REFERENCES merchant_expected_transfers (expected_credit_serial) ON DELETE CASCADE
  ,offset_in_exchange_list INT8 NOT NULL
  ,exchange_deposit_value taler_amount_currency NOT NULL
  ,exchange_deposit_fee taler_amount_currency NOT NULL
  );
CREATE INDEX IF NOT EXISTS merchant_transfers_by_credit
  ON merchant_expected_transfer_to_coin
  (expected_credit_serial);
COMMENT ON TABLE merchant_expected_transfer_to_coin
  IS 'Mapping of (credit) transfers to (deposited) coins';
COMMENT ON COLUMN merchant_expected_transfer_to_coin.deposit_serial
  IS 'Identifies the deposited coin that the wire transfer presumably settles';
COMMENT ON COLUMN merchant_expected_transfer_to_coin.expected_credit_serial
  IS 'Identifies the expected wire transfer that settles the given deposited coin';
COMMENT ON COLUMN merchant_expected_transfer_to_coin.offset_in_exchange_list
  IS 'The exchange settlement data includes an array of the settled coins; this is the index of the coin in that list, useful to reconstruct the correct sequence of coins as needed to check the exchange signature';
COMMENT ON COLUMN merchant_expected_transfer_to_coin.exchange_deposit_value
  IS 'Deposit value as claimed by the exchange, should match our values in merchant_deposits minus refunds';
COMMENT ON COLUMN merchant_expected_transfer_to_coin.exchange_deposit_fee
  IS 'Deposit value as claimed by the exchange, should match our values in merchant_deposits';


-- We need to fully re-do the merchant_transfer_signatures table,
-- and data should be re-constructed, so drop and re-build.

DROP TABLE merchant_transfer_signatures;
CREATE TABLE merchant_transfer_signatures
  (expected_credit_serial BIGINT PRIMARY KEY
     REFERENCES merchant_expected_transfers (expected_credit_serial)
     ON DELETE CASCADE
  ,signkey_serial BIGINT NOT NULL
     REFERENCES merchant_exchange_signing_keys (signkey_serial)
     ON DELETE CASCADE
  ,wire_fee taler_amount_currency NOT NULL
  ,credit_amount taler_amount_currency NOT NULL
  ,execution_time INT8 NOT NULL
  ,exchange_sig BYTEA NOT NULL CHECK (LENGTH(exchange_sig)=64)
  );
COMMENT ON TABLE merchant_transfer_signatures
  IS 'table represents the main information returned from the /transfer request to the exchange.';
COMMENT ON COLUMN merchant_transfer_signatures.expected_credit_serial
  IS 'expected wire transfer this signature is about';
COMMENT ON COLUMN merchant_transfer_signatures.signkey_serial
  IS 'Online signing key by the exchange that was used for the exchange_sig signature';
COMMENT ON COLUMN merchant_transfer_signatures.wire_fee
  IS 'wire fee charged by the exchange for this transfer';
COMMENT ON COLUMN merchant_transfer_signatures.exchange_sig
  IS 'signature by the exchange of purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE_DEPOSIT';
COMMENT ON COLUMN merchant_transfer_signatures.execution_time
  IS 'Execution time as claimed by the exchange, roughly matches time seen by merchant';
COMMENT ON COLUMN merchant_transfer_signatures.credit_amount
  IS 'actual value of the (aggregated) wire transfer, excluding the wire fee, according to the exchange';


COMMIT;
