libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

export-nexus-withdrawals.sh (4774B)


      1 #!/bin/bash
      2 # This file is part of TALER
      3 # Copyright (C) 2026 Taler Systems SA
      4 #
      5 # TALER is free software; you can redistribute it and/or modify it under the
      6 # terms of the GNU General Public License as published by the Free Software
      7 # Foundation; either version 3, or (at your option) any later version.
      8 #
      9 # TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 #
     13 # You should have received a copy of the GNU General Public License along with
     14 # TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 
     16 set -euo pipefail
     17 
     18 usage() {
     19     cat <<'EOF'
     20 Usage: export-nexus-withdrawals.sh YYYY-MM-dd OUTPUT.csv [PSQL-CONNECTION]
     21 
     22 Export Nexus incoming transfers of type 'reserve', from midnight UTC on the
     23 given date (inclusive). Requires psql, OpenSSL, and PostgreSQL 12 or later.
     24 PSQL-CONNECTION is a database name or libpq connection string; if omitted,
     25 psql uses its usual PGDATABASE, PGHOST, PGUSER, etc. environment variables.
     26 
     27 Columns: incoming_transaction_id, execution_time_utc, amount, sender_iban_hash.
     28 Amounts use the currency configured for this Nexus instance (not stored in SQL).
     29 The hash is SHA-256(random 32-byte salt || uppercase IBAN), truncated to
     30 128 bits (32 hexadecimal characters). One fresh salt is used for the entire
     31 export, so senders can be grouped within a file but hashes change between runs.
     32 The salt is not included in the CSV or saved by the script.
     33 Names, payment subjects, and raw payto URIs are not exported.
     34 Unsupported/missing IBANs cause an error instead of silently dropping transfers.
     35 The output file is replaced only after a successful export, with mode 0600.
     36 
     37 Example:
     38   export-nexus-withdrawals.sh 2026-01-01 withdrawals.csv 'dbname=libeufin-nexus'
     39 EOF
     40 }
     41 
     42 if [[ ${1:-} == --help || ${1:-} == -h ]]; then
     43     usage
     44     exit 0
     45 fi
     46 if (( $# < 2 || $# > 3 )); then
     47     usage >&2
     48     exit 1
     49 fi
     50 if [[ ! $1 =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
     51     echo 'Start date must have the format YYYY-MM-dd.' >&2
     52     exit 1
     53 fi
     54 
     55 since=$1
     56 output=$2
     57 connection=()
     58 if (( $# == 3 )); then
     59     connection=(--dbname="$3")
     60 fi
     61 
     62 umask 077
     63 salt=$(openssl rand -hex 32)
     64 temporary=$(mktemp -- "${output}.tmp.XXXXXXXX")
     65 trap 'rm -f -- "$temporary"' EXIT
     66 
     67 # Feed the salt through stdin, keeping it out of the psql process arguments.
     68 # A repeatable-read snapshot keeps validation and export consistent.
     69 {
     70     printf '\\set export_salt %s\n' "$salt"
     71     cat <<'SQL'
     72 BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;
     73 SET LOCAL TIME ZONE 'UTC';
     74 SET LOCAL DateStyle = 'ISO, YMD';
     75 
     76 -- Timestamps in Nexus are microseconds since the Unix epoch.
     77 SELECT (extract(epoch FROM :'since'::date::timestamptz) * 1000000)::bigint AS cutoff
     78 \gset
     79 
     80 -- Match both payto://iban/IBAN and payto://iban/BIC/IBAN, ignoring
     81 -- query parameters (including the sender's name). Fail closed on other forms.
     82 \set iban_pattern '^payto://iban/(?:[A-Z0-9]+/)?([A-Z]{2}[0-9]{2}[A-Z0-9]{11,30})(?:[?].*)?$'
     83 SELECT NOT EXISTS (
     84     SELECT 1
     85     FROM libeufin_nexus.incoming_transactions AS i
     86     JOIN libeufin_nexus.talerable_incoming_transactions AS t
     87       USING (incoming_transaction_id)
     88     WHERE t.type = 'reserve' AND i.execution_time >= :cutoff
     89       AND (i.debit_payto IS NULL OR i.debit_payto !~* :'iban_pattern')
     90 ) AS valid_ibans
     91 \gset
     92 \if :valid_ibans
     93 \else
     94     \echo 'Export failed: a withdrawal has a missing or unsupported IBAN payto URI.'
     95     -- Make ON_ERROR_STOP terminate with a nonzero exit status.
     96     DO $$ BEGIN RAISE EXCEPTION 'Cannot export all sender IBANs'; END $$;
     97 \endif
     98 
     99 COPY (
    100     SELECT i.incoming_transaction_id,
    101            to_char(
    102                timestamptz 'epoch'
    103                    + (i.execution_time / 1000000) * interval '1 second'
    104                    + (i.execution_time % 1000000) * interval '1 microsecond',
    105                'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
    106            ) AS execution_time_utc,
    107            (i.amount).val::text || '.' || lpad((i.amount).frac::text, 8, '0') AS amount,
    108            left(encode(sha256(
    109                decode(:'export_salt', 'hex')
    110                || convert_to(upper(substring(i.debit_payto FROM ('(?i)' || :'iban_pattern'))), 'UTF8')
    111            ), 'hex'), 32) AS sender_iban_hash
    112     FROM libeufin_nexus.incoming_transactions AS i
    113     JOIN libeufin_nexus.talerable_incoming_transactions AS t
    114       USING (incoming_transaction_id)
    115     WHERE t.type = 'reserve' AND i.execution_time >= :cutoff
    116     ORDER BY i.execution_time, i.incoming_transaction_id
    117 ) TO STDOUT WITH (FORMAT CSV, HEADER);
    118 COMMIT;
    119 SQL
    120 } | psql -X --quiet --set=ON_ERROR_STOP=1 --set=since="$since" "${connection[@]}" > "$temporary"
    121 
    122 mv -f -- "$temporary" "$output"