taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

db.rs (49015B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2025, 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 Affero 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 Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::fmt::Display;
     18 
     19 use compact_str::CompactString;
     20 use jiff::Timestamp;
     21 use serde::{Serialize, de::DeserializeOwned};
     22 use sqlx::{PgConnection, PgPool, QueryBuilder, Row, postgres::PgRow};
     23 use taler_api::{
     24     db::{BindHelper, TypeHelper, history, page},
     25     serialized,
     26     subject::{IncomingKey, OutgoingSubject, fmt_out_subject},
     27 };
     28 use taler_common::{
     29     api::{
     30         HashCode, ShortHashCode,
     31         params::{History, Page},
     32         prepared::{RegistrationRequest, Unregistration},
     33         revenue::RevenueIncomingBankTransaction,
     34         wire::{
     35             IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferState,
     36             TransferStatus,
     37         },
     38     },
     39     config::Config,
     40     db::IncomingType,
     41     types::{
     42         amount::{Currency, Decimal},
     43         payto::{PaytoImpl as _, PaytoURI},
     44     },
     45 };
     46 use tokio::sync::watch::{Receiver, Sender};
     47 use url::Url;
     48 
     49 use crate::{
     50     config::parse_db_cfg,
     51     payto::{CyclosAccount, CyclosId, FullCyclosPayto},
     52 };
     53 
     54 const SCHEMA: &str = "cyclos";
     55 
     56 pub async fn pool(cfg: &Config) -> anyhow::Result<PgPool> {
     57     let db = parse_db_cfg(cfg)?;
     58     let pool = taler_common::db::pool(db.cfg, SCHEMA).await?;
     59     Ok(pool)
     60 }
     61 
     62 pub async fn dbinit(cfg: &Config, reset: bool) -> anyhow::Result<PgPool> {
     63     let db_cfg = parse_db_cfg(cfg)?;
     64     let pool = taler_common::db::pool(db_cfg.cfg, SCHEMA).await?;
     65     let mut db = pool.acquire().await?;
     66     taler_common::db::dbinit(&mut db, db_cfg.sql_dir.as_ref(), SCHEMA, reset).await?;
     67     Ok(pool)
     68 }
     69 
     70 pub async fn notification_listener(
     71     pool: PgPool,
     72     in_channel: Sender<i64>,
     73     taler_in_channel: Sender<i64>,
     74     out_channel: Sender<i64>,
     75     taler_out_channel: Sender<i64>,
     76 ) {
     77     taler_api::notification::notification_listener!(&pool,
     78         "tx_in" => (row_id: i64) {
     79             in_channel.send_replace(row_id);
     80         },
     81         "taler_in" => (row_id: i64) {
     82             taler_in_channel.send_replace(row_id);
     83         },
     84         "tx_out" => (row_id: i64) {
     85             out_channel.send_replace(row_id);
     86         },
     87         "taler_out" => (row_id: i64) {
     88             taler_out_channel.send_replace(row_id);
     89         }
     90     )
     91 }
     92 
     93 #[derive(Debug, Clone)]
     94 pub struct TxIn {
     95     pub transfer_id: i64,
     96     pub tx_id: Option<i64>,
     97     pub amount: Decimal,
     98     pub subject: String,
     99     pub debtor_id: i64,
    100     pub debtor_name: CompactString,
    101     pub valued_at: Timestamp,
    102 }
    103 
    104 impl Display for TxIn {
    105     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    106         let Self {
    107             transfer_id,
    108             tx_id,
    109             amount,
    110             subject,
    111             valued_at,
    112             debtor_id,
    113             debtor_name,
    114         } = self;
    115         let tx_id = match tx_id {
    116             Some(id) => format_args!(":{}", *id),
    117             None => format_args!(""),
    118         };
    119         write!(
    120             f,
    121             "{valued_at} {transfer_id}{tx_id} {amount} ({debtor_id} {debtor_name}) '{subject}'"
    122         )
    123     }
    124 }
    125 
    126 #[derive(Debug, Clone)]
    127 pub struct TxOut {
    128     pub transfer_id: i64,
    129     pub tx_id: Option<i64>,
    130     pub amount: Decimal,
    131     pub subject: String,
    132     pub creditor_id: i64,
    133     pub creditor_name: CompactString,
    134     pub valued_at: Timestamp,
    135 }
    136 
    137 impl Display for TxOut {
    138     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    139         let Self {
    140             transfer_id,
    141             tx_id,
    142             amount,
    143             subject,
    144             creditor_id,
    145             creditor_name,
    146             valued_at,
    147         } = self;
    148         let tx_id = match tx_id {
    149             Some(id) => format_args!(":{}", *id),
    150             None => format_args!(""),
    151         };
    152         write!(
    153             f,
    154             "{valued_at} {transfer_id}{tx_id} {amount} ({creditor_id} {creditor_name}) '{subject}'"
    155         )
    156     }
    157 }
    158 
    159 #[derive(Debug, PartialEq, Eq)]
    160 pub struct Initiated {
    161     pub id: i64,
    162     pub amount: Decimal,
    163     pub subject: String,
    164     pub creditor_id: i64,
    165     pub creditor_name: CompactString,
    166 }
    167 
    168 impl Display for Initiated {
    169     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    170         let Self {
    171             id,
    172             amount,
    173             subject,
    174             creditor_id,
    175             creditor_name,
    176         } = self;
    177         write!(
    178             f,
    179             "{id} {amount} ({creditor_id} {creditor_name}) '{subject}'"
    180         )
    181     }
    182 }
    183 
    184 #[derive(Debug, Clone)]
    185 pub struct TxInAdmin {
    186     pub amount: Decimal,
    187     pub subject: String,
    188     pub debtor_id: i64,
    189     pub debtor_name: CompactString,
    190     pub metadata: IncomingKey,
    191 }
    192 
    193 #[derive(Debug, PartialEq, Eq)]
    194 pub enum AddIncomingResult {
    195     Success {
    196         new: bool,
    197         pending: bool,
    198         row_id: u64,
    199         valued_at: Timestamp,
    200     },
    201     ReservePubReuse,
    202     UnknownMapping,
    203     MappingReuse,
    204 }
    205 
    206 /// Lock the database for worker execution
    207 pub async fn worker_lock(e: &mut PgConnection) -> sqlx::Result<bool> {
    208     sqlx::query("SELECT pg_try_advisory_lock(42)")
    209         .try_map(|r: PgRow| r.try_get(0))
    210         .fetch_one(e)
    211         .await
    212 }
    213 
    214 pub async fn register_tx_in_admin(
    215     db: &PgPool,
    216     tx: &TxInAdmin,
    217     now: &Timestamp,
    218 ) -> sqlx::Result<AddIncomingResult> {
    219     serialized!(
    220         sqlx::query(
    221             "
    222             SELECT out_reserve_pub_reuse, out_mapping_reuse, out_unknown_mapping, out_tx_row_id, out_valued_at, out_new, out_pending
    223             FROM register_tx_in(NULL, NULL, $1, $2, $3, $4, $5, $6, $7, $5)
    224         ",
    225         )
    226         .bind(tx.amount)
    227         .bind(&tx.subject)
    228         .bind(tx.debtor_id)
    229         .bind(&tx.debtor_name)
    230         .bind_timestamp(now)
    231         .bind(tx.metadata.ty)
    232         .bind(tx.metadata.key)
    233        .try_map(|r: PgRow| {
    234             Ok(if r.try_get_flag(0)? {
    235                 AddIncomingResult::ReservePubReuse
    236             } else if r.try_get_flag(1)? {
    237                 AddIncomingResult::MappingReuse
    238             } else if r.try_get_flag(2)? {
    239                 AddIncomingResult::UnknownMapping
    240             } else {
    241                 AddIncomingResult::Success {
    242                     row_id: r.try_get_u64(3)?,
    243                     valued_at: r.try_get_timestamp(4)?,
    244                     new: r.try_get(5)?,
    245                     pending: r.try_get(6)?
    246                 }
    247             })
    248         })
    249         .fetch_one(db)
    250     )
    251 }
    252 
    253 pub async fn register_tx_in(
    254     db: &mut PgConnection,
    255     tx: &TxIn,
    256     subject: &Option<IncomingKey>,
    257     now: &Timestamp,
    258 ) -> sqlx::Result<AddIncomingResult> {
    259     serialized!(
    260         sqlx::query(
    261             "
    262             SELECT out_reserve_pub_reuse, out_mapping_reuse, out_unknown_mapping, out_tx_row_id, out_valued_at, out_new, out_pending
    263             FROM register_tx_in($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
    264         ",
    265         )
    266         .bind(tx.transfer_id)
    267         .bind(tx.tx_id)
    268         .bind(tx.amount)
    269         .bind(&tx.subject)
    270         .bind(tx.debtor_id)
    271         .bind(&tx.debtor_name)
    272         .bind(tx.valued_at.as_microsecond())
    273         .bind(subject.as_ref().map(|it| it.ty))
    274         .bind(subject.as_ref().map(|it| it.key))
    275         .bind(now.as_microsecond())
    276         .try_map(|r: PgRow| {
    277             Ok(if r.try_get_flag(0)? {
    278                 AddIncomingResult::ReservePubReuse
    279             } else if r.try_get_flag(1)? {
    280                 AddIncomingResult::MappingReuse
    281             } else if r.try_get_flag(2)? {
    282                 AddIncomingResult::UnknownMapping
    283             } else {
    284                 AddIncomingResult::Success {
    285                     row_id: r.try_get_u64(3)?,
    286                     valued_at: r.try_get_timestamp(4)?,
    287                     new: r.try_get(5)?,
    288                     pending: r.try_get(6)?
    289                 }
    290             })
    291         })
    292         .fetch_one(&mut *db)
    293     )
    294 }
    295 
    296 #[derive(Debug)]
    297 pub enum TxOutKind {
    298     Simple,
    299     Bounce(i64),
    300     Talerable(OutgoingSubject),
    301 }
    302 
    303 #[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)]
    304 #[allow(non_camel_case_types)]
    305 #[sqlx(type_name = "register_result")]
    306 pub enum RegisterResult {
    307     /// Already registered
    308     idempotent,
    309     /// Initiated transaction
    310     known,
    311     /// Recovered unknown outgoing transaction
    312     recovered,
    313 }
    314 
    315 #[derive(Debug, PartialEq, Eq)]
    316 pub struct AddOutgoingResult {
    317     pub result: RegisterResult,
    318     pub row_id: i64,
    319 }
    320 
    321 pub async fn register_tx_out(
    322     db: &mut PgConnection,
    323     tx: &TxOut,
    324     kind: &TxOutKind,
    325     now: &Timestamp,
    326 ) -> sqlx::Result<AddOutgoingResult> {
    327     serialized!({
    328         let query = sqlx::query(
    329             "
    330                 SELECT out_result, out_tx_row_id
    331                 FROM register_tx_out($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
    332             ",
    333         )
    334         .bind(tx.transfer_id)
    335         .bind(tx.tx_id)
    336         .bind(tx.amount)
    337         .bind(&tx.subject)
    338         .bind(tx.creditor_id)
    339         .bind(&tx.creditor_name)
    340         .bind_timestamp(&tx.valued_at);
    341         let query = match kind {
    342             TxOutKind::Simple => query
    343                 .bind(None::<&[u8]>)
    344                 .bind(None::<&str>)
    345                 .bind(None::<&str>)
    346                 .bind(None::<i64>),
    347             TxOutKind::Bounce(bounced) => query
    348                 .bind(None::<&[u8]>)
    349                 .bind(None::<&str>)
    350                 .bind(None::<&str>)
    351                 .bind(*bounced),
    352             TxOutKind::Talerable(subject) => query
    353                 .bind(subject.wtid)
    354                 .bind(subject.exchange_base_url.as_str())
    355                 .bind(&subject.metadata)
    356                 .bind(None::<i64>),
    357         };
    358         query
    359             .bind_timestamp(now)
    360             .try_map(|r: PgRow| {
    361                 Ok(AddOutgoingResult {
    362                     result: r.try_get(0)?,
    363                     row_id: r.try_get(1)?,
    364                 })
    365             })
    366             .fetch_one(&mut *db)
    367     })
    368 }
    369 
    370 #[derive(Debug, PartialEq, Eq)]
    371 pub enum TransferResult {
    372     Success { id: u64, initiated_at: Timestamp },
    373     RequestUidReuse,
    374     WtidReuse,
    375 }
    376 
    377 #[derive(Debug, Clone)]
    378 pub struct Transfer {
    379     pub request_uid: HashCode,
    380     pub amount: Decimal,
    381     pub exchange_base_url: Url,
    382     pub metadata: Option<CompactString>,
    383     pub wtid: ShortHashCode,
    384     pub creditor_id: i64,
    385     pub creditor_name: CompactString,
    386 }
    387 
    388 pub async fn make_transfer(
    389     db: &PgPool,
    390     tx: &Transfer,
    391     now: &Timestamp,
    392 ) -> sqlx::Result<TransferResult> {
    393     let subject = fmt_out_subject(&tx.wtid, &tx.exchange_base_url, tx.metadata.as_deref());
    394     serialized!(
    395         sqlx::query(
    396             "
    397                 SELECT out_request_uid_reuse, out_wtid_reuse, out_initiated_row_id, out_initiated_at
    398                 FROM taler_transfer($1, $2, $3, $4, $5, $6, $7, $8, $9)
    399             ",
    400         )
    401         .bind(tx.request_uid)
    402         .bind(tx.wtid)
    403         .bind(&subject)
    404         .bind(tx.amount)
    405         .bind(tx.exchange_base_url.as_str())
    406         .bind(&tx.metadata)
    407         .bind(tx.creditor_id)
    408         .bind(&tx.creditor_name)
    409         .bind_timestamp(now)
    410         .try_map(|r: PgRow| {
    411             Ok(if r.try_get_flag(0)? {
    412                 TransferResult::RequestUidReuse
    413             } else if r.try_get_flag(1)? {
    414                 TransferResult::WtidReuse
    415             } else {
    416                 TransferResult::Success {
    417                     id: r.try_get_u64(2)?,
    418                     initiated_at: r.try_get_timestamp(3)?,
    419                 }
    420             })
    421         })
    422         .fetch_one(db)
    423     )
    424 }
    425 
    426 #[derive(Debug, PartialEq, Eq)]
    427 pub struct BounceResult {
    428     pub tx_id: u64,
    429     pub tx_new: bool,
    430 }
    431 
    432 pub async fn register_bounced_tx_in(
    433     db: &mut PgConnection,
    434     tx: &TxIn,
    435     chargeback_id: i64,
    436     reason: &str,
    437     now: &Timestamp,
    438 ) -> sqlx::Result<BounceResult> {
    439     serialized!(
    440         sqlx::query(
    441             "
    442                 SELECT out_tx_row_id, out_tx_new
    443                 FROM register_bounced_tx_in($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
    444             ",
    445         )
    446         .bind(tx.transfer_id)
    447         .bind(tx.tx_id)
    448         .bind(tx.amount)
    449         .bind(&tx.subject)
    450         .bind(tx.debtor_id)
    451         .bind(&tx.debtor_name)
    452         .bind_timestamp(&tx.valued_at)
    453         .bind(chargeback_id)
    454         .bind(reason)
    455         .bind_timestamp(now)
    456         .try_map(|r: PgRow| {
    457             Ok(BounceResult {
    458                 tx_id: r.try_get_u64(0)?,
    459                 tx_new: r.try_get(1)?,
    460             })
    461         })
    462         .fetch_one(&mut *db)
    463     )
    464 }
    465 
    466 pub async fn transfer_page(
    467     db: &PgPool,
    468     status: &Option<TransferState>,
    469     currency: &Currency,
    470     root: &CompactString,
    471     params: &Page,
    472 ) -> sqlx::Result<Vec<TransferListStatus>> {
    473     page(
    474         db,
    475         params,
    476         "initiated_id",
    477         || {
    478             let mut builder = QueryBuilder::new(
    479                 "
    480                     SELECT
    481                         initiated_id,
    482                         status,
    483                         amount,
    484                         credit_account,
    485                         credit_name,
    486                         initiated_at
    487                     FROM transfer
    488                     JOIN initiated USING (initiated_id)
    489                     WHERE
    490                 ",
    491             );
    492             if let Some(status) = status {
    493                 builder.push(" status = ").push_bind(status).push(" AND ");
    494             }
    495             builder
    496         },
    497         |r: PgRow| {
    498             Ok(TransferListStatus {
    499                 row_id: r.try_get_u64(0)?,
    500                 status: r.try_get(1)?,
    501                 amount: r.try_get_amount(2, currency)?,
    502                 credit_account: r.try_get_cyclos_fullpaytouri(3, 4, root)?,
    503                 timestamp: r.try_get_timestamp(5)?.into(),
    504             })
    505         },
    506     )
    507     .await
    508 }
    509 
    510 pub async fn outgoing_history(
    511     db: &PgPool,
    512     params: &History,
    513     currency: &Currency,
    514     root: &CompactString,
    515     listen: impl FnOnce() -> Receiver<i64>,
    516 ) -> sqlx::Result<Vec<OutgoingBankTransaction>> {
    517     history(
    518         db,
    519         "tx_out_id",
    520         params,
    521         listen,
    522         || {
    523             QueryBuilder::new(
    524                 "
    525                 SELECT
    526                     tx_out_id,
    527                     amount,
    528                     credit_account,
    529                     credit_name,
    530                     valued_at,
    531                     exchange_base_url,
    532                     metadata,
    533                     wtid
    534                 FROM taler_out
    535                 JOIN tx_out USING (tx_out_id)
    536                 WHERE
    537             ",
    538             )
    539         },
    540         |r: PgRow| {
    541             Ok(OutgoingBankTransaction {
    542                 row_id: r.try_get_u64(0)?,
    543                 amount: r.try_get_amount(1, currency)?,
    544                 debit_fee: None,
    545                 credit_account: r.try_get_cyclos_fullpaytouri(2, 3, root)?,
    546                 date: r.try_get_timestamp(4)?.into(),
    547                 exchange_base_url: r.try_get_url(5)?,
    548                 metadata: r.try_get(6)?,
    549                 wtid: r.try_get(7)?,
    550             })
    551         },
    552     )
    553     .await
    554 }
    555 
    556 pub async fn incoming_history(
    557     db: &PgPool,
    558     params: &History,
    559     currency: &Currency,
    560     root: &CompactString,
    561     listen: impl FnOnce() -> Receiver<i64>,
    562 ) -> sqlx::Result<Vec<IncomingBankTransaction>> {
    563     history(
    564         db,
    565         "tx_in_id",
    566         params,
    567         listen,
    568         || {
    569             QueryBuilder::new(
    570                 "
    571                 SELECT
    572                     type,
    573                     tx_in_id,
    574                     amount,
    575                     debit_account,
    576                     debit_name,
    577                     valued_at,
    578                     metadata,
    579                     authorization_pub,
    580                     authorization_sig
    581                 FROM taler_in
    582                 JOIN tx_in USING (tx_in_id)
    583                 WHERE
    584             ",
    585             )
    586         },
    587         |r: PgRow| {
    588             Ok(match r.try_get(0)? {
    589                 IncomingType::reserve => IncomingBankTransaction::Reserve {
    590                     row_id: r.try_get_u64(1)?,
    591                     amount: r.try_get_amount(2, currency)?,
    592                     credit_fee: None,
    593                     debit_account: r.try_get_cyclos_fullpaytouri(3, 4, root)?,
    594                     date: r.try_get_timestamp(5)?.into(),
    595                     reserve_pub: r.try_get(6)?,
    596                     authorization_pub: r.try_get(7)?,
    597                     authorization_sig: r.try_get(8)?,
    598                 },
    599                 IncomingType::kyc => IncomingBankTransaction::Kyc {
    600                     row_id: r.try_get_u64(1)?,
    601                     amount: r.try_get_amount(2, currency)?,
    602                     credit_fee: None,
    603                     debit_account: r.try_get_cyclos_fullpaytouri(3, 4, root)?,
    604                     date: r.try_get_timestamp(5)?.into(),
    605                     account_pub: r.try_get(6)?,
    606                     authorization_pub: r.try_get(7)?,
    607                     authorization_sig: r.try_get(8)?,
    608                 },
    609                 IncomingType::map => unimplemented!("MAP are never listed in the history"),
    610             })
    611         },
    612     )
    613     .await
    614 }
    615 
    616 pub async fn revenue_history(
    617     db: &PgPool,
    618     params: &History,
    619     currency: &Currency,
    620     root: &CompactString,
    621     listen: impl FnOnce() -> Receiver<i64>,
    622 ) -> sqlx::Result<Vec<RevenueIncomingBankTransaction>> {
    623     history(
    624         db,
    625         "tx_in_id",
    626         params,
    627         listen,
    628         || {
    629             QueryBuilder::new(
    630                 "
    631                 SELECT
    632                     tx_in_id,
    633                     valued_at,
    634                     amount,
    635                     debit_account,
    636                     debit_name,
    637                     subject
    638                 FROM tx_in
    639                 WHERE
    640             ",
    641             )
    642         },
    643         |r: PgRow| {
    644             Ok(RevenueIncomingBankTransaction {
    645                 row_id: r.try_get_u64(0)?,
    646                 date: r.try_get_timestamp(1)?.into(),
    647                 amount: r.try_get_amount(2, currency)?,
    648                 credit_fee: None,
    649                 debit_account: r.try_get_cyclos_fullpaytouri(3, 4, root)?,
    650                 subject: r.try_get(5)?,
    651             })
    652         },
    653     )
    654     .await
    655 }
    656 
    657 pub async fn transfer_by_id(
    658     db: &PgPool,
    659     id: u64,
    660     currency: &Currency,
    661     root: &CompactString,
    662 ) -> sqlx::Result<Option<TransferStatus>> {
    663     serialized!(
    664         sqlx::query(
    665             "
    666                 SELECT
    667                     status,
    668                     status_msg,
    669                     amount,
    670                     exchange_base_url,
    671                     metadata,
    672                     wtid,
    673                     credit_account,
    674                     credit_name,
    675                     initiated_at
    676                 FROM transfer
    677                 JOIN initiated USING (initiated_id)
    678                 WHERE initiated_id = $1
    679             ",
    680         )
    681         .bind(id as i64)
    682         .try_map(|r: PgRow| {
    683             Ok(TransferStatus {
    684                 status: r.try_get(0)?,
    685                 status_msg: r.try_get(1)?,
    686                 amount: r.try_get_amount(2, currency)?,
    687                 exchange_base_url: r.try_get(3)?,
    688                 metadata: r.try_get(4)?,
    689                 wtid: r.try_get(5)?,
    690                 credit_account: r.try_get_cyclos_fullpaytouri(6, 7, root)?,
    691                 timestamp: r.try_get_timestamp(8)?.into(),
    692             })
    693         })
    694         .fetch_optional(db)
    695     )
    696 }
    697 
    698 /** Get a batch of pending initiated transactions not attempted since [start] */
    699 pub async fn pending_batch(
    700     db: &mut PgConnection,
    701     start: &Timestamp,
    702 ) -> sqlx::Result<Vec<Initiated>> {
    703     serialized!(
    704         sqlx::query(
    705             "
    706             SELECT initiated_id, amount, subject, credit_account, credit_name
    707             FROM initiated
    708             WHERE tx_id IS NULL
    709                 AND status='pending'
    710                 AND (last_submitted IS NULL OR last_submitted < $1)
    711             LIMIT 100
    712         ",
    713         )
    714         .bind_timestamp(start)
    715         .try_map(|r: PgRow| {
    716             Ok(Initiated {
    717                 id: r.try_get(0)?,
    718                 amount: r.try_get(1)?,
    719                 subject: r.try_get(2)?,
    720                 creditor_id: r.try_get(3)?,
    721                 creditor_name: r.try_get(4)?,
    722             })
    723         })
    724         .fetch_all(&mut *db)
    725     )
    726 }
    727 
    728 /** Update status of a successful submitted initiated transaction */
    729 pub async fn initiated_submit_success(
    730     db: &mut PgConnection,
    731     initiated_id: i64,
    732     timestamp: &Timestamp,
    733     tx_id: i64,
    734 ) -> sqlx::Result<()> {
    735     serialized!(
    736         sqlx::query(
    737             "
    738                 UPDATE initiated
    739                 SET status='pending', submission_counter=submission_counter+1, last_submitted=$1, tx_id=$2
    740                 WHERE initiated_id=$3
    741             "
    742         )
    743         .bind_timestamp(timestamp)
    744         .bind(tx_id)
    745         .bind(initiated_id)
    746         .execute(&mut *db)
    747     )?;
    748     Ok(())
    749 }
    750 
    751 /** Update status of a permanently failed initiated transaction */
    752 pub async fn initiated_submit_permanent_failure(
    753     db: &mut PgConnection,
    754     initiated_id: i64,
    755     msg: &str,
    756 ) -> sqlx::Result<()> {
    757     serialized!(
    758         sqlx::query(
    759             "
    760                 UPDATE initiated
    761                 SET status='permanent_failure', status_msg=$1
    762                 WHERE initiated_id=$2
    763             ",
    764         )
    765         .bind(msg)
    766         .bind(initiated_id)
    767         .execute(&mut *db)
    768     )?;
    769     Ok(())
    770 }
    771 
    772 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    773 pub enum ChargebackFailureResult {
    774     Unknown,
    775     Known(u64),
    776     Idempotent(u64),
    777 }
    778 
    779 /** Update status of a charged back initiated transaction */
    780 pub async fn initiated_chargeback_failure(
    781     db: &mut PgConnection,
    782     transfer_id: i64,
    783 ) -> sqlx::Result<ChargebackFailureResult> {
    784     Ok(serialized!(
    785         sqlx::query("SELECT out_initiated_id, out_new FROM register_charge_back_failure($1)")
    786             .bind(transfer_id)
    787             .try_map(|r: PgRow| {
    788                 let id = r.try_get_u64(0)?;
    789                 Ok(if id == 0 {
    790                     ChargebackFailureResult::Unknown
    791                 } else if r.try_get(1)? {
    792                     ChargebackFailureResult::Known(id)
    793                 } else {
    794                     ChargebackFailureResult::Idempotent(id)
    795                 })
    796             })
    797             .fetch_optional(&mut *db)
    798     )?
    799     .unwrap_or(ChargebackFailureResult::Unknown))
    800 }
    801 
    802 /** Get JSON value from KV table */
    803 pub async fn kv_get<T: DeserializeOwned + Unpin + Send>(
    804     db: &mut PgConnection,
    805     key: &str,
    806 ) -> sqlx::Result<Option<T>> {
    807     serialized!(
    808         sqlx::query("SELECT value FROM kv WHERE key=$1")
    809             .bind(key)
    810             .try_map(|r| Ok(r.try_get::<sqlx::types::Json<T>, _>(0)?.0))
    811             .fetch_optional(&mut *db)
    812     )
    813 }
    814 
    815 /** Set JSON value in KV table */
    816 pub async fn kv_set<T: Serialize>(db: &mut PgConnection, key: &str, value: &T) -> sqlx::Result<()> {
    817     serialized!(
    818         sqlx::query("INSERT INTO kv (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value")
    819             .bind(key)
    820             .bind(sqlx::types::Json(value))
    821             .execute(&mut *db)
    822     )?;
    823     Ok(())
    824 }
    825 
    826 pub enum RegistrationResult {
    827     Success,
    828     ReservePubReuse,
    829 }
    830 
    831 pub async fn transfer_register(
    832     db: &PgPool,
    833     req: &RegistrationRequest,
    834 ) -> sqlx::Result<RegistrationResult> {
    835     let ty: IncomingType = req.r#type.into();
    836     serialized!(
    837         sqlx::query(
    838             "SELECT out_reserve_pub_reuse FROM register_prepared_transfers($1,$2,$3,$4,$5,$6)"
    839         )
    840         .bind(ty)
    841         .bind(req.account_pub)
    842         .bind(req.authorization_pub)
    843         .bind(req.authorization_sig)
    844         .bind(req.recurrent)
    845         .bind_timestamp(&Timestamp::now())
    846         .try_map(|r: PgRow| {
    847             Ok(if r.try_get_flag("out_reserve_pub_reuse")? {
    848                 RegistrationResult::ReservePubReuse
    849             } else {
    850                 RegistrationResult::Success
    851             })
    852         })
    853         .fetch_one(db)
    854     )
    855 }
    856 
    857 pub async fn transfer_unregister(db: &PgPool, req: &Unregistration) -> sqlx::Result<bool> {
    858     serialized!(
    859         sqlx::query("SELECT out_found FROM delete_prepared_transfers($1)")
    860             .bind(req.authorization_pub)
    861             .try_map(|r: PgRow| r.try_get_flag("out_found"))
    862             .fetch_one(db)
    863     )
    864 }
    865 
    866 pub trait CyclosTypeHelper {
    867     fn try_get_cyclos_fullpayto<I: sqlx::ColumnIndex<Self>>(
    868         &self,
    869         idx: I,
    870         name: I,
    871         root: &CompactString,
    872     ) -> sqlx::Result<FullCyclosPayto>;
    873     fn try_get_cyclos_fullpaytouri<I: sqlx::ColumnIndex<Self>>(
    874         &self,
    875         idx: I,
    876         name: I,
    877         root: &CompactString,
    878     ) -> sqlx::Result<PaytoURI>;
    879 }
    880 
    881 impl CyclosTypeHelper for PgRow {
    882     fn try_get_cyclos_fullpayto<I: sqlx::ColumnIndex<Self>>(
    883         &self,
    884         idx: I,
    885         name: I,
    886         root: &CompactString,
    887     ) -> sqlx::Result<FullCyclosPayto> {
    888         let idx = self.try_get(idx)?;
    889         let name = self.try_get(name)?;
    890         Ok(FullCyclosPayto::new(
    891             CyclosAccount {
    892                 id: CyclosId(idx),
    893                 root: root.clone(),
    894             },
    895             name,
    896         ))
    897     }
    898     fn try_get_cyclos_fullpaytouri<I: sqlx::ColumnIndex<Self>>(
    899         &self,
    900         idx: I,
    901         name: I,
    902         root: &CompactString,
    903     ) -> sqlx::Result<PaytoURI> {
    904         let idx = self.try_get(idx)?;
    905         let name = self.try_get(name)?;
    906         Ok(CyclosAccount {
    907             id: CyclosId(idx),
    908             root: root.clone(),
    909         }
    910         .as_full_uri(name))
    911     }
    912 }
    913 
    914 #[cfg(test)]
    915 mod test {
    916     use std::assert_matches;
    917 
    918     use compact_str::CompactString;
    919     use jiff::{Span, Timestamp};
    920     use serde_json::json;
    921     use sqlx::{PgPool, Postgres, Row as _, pool::PoolConnection, postgres::PgRow};
    922     use taler_api::{
    923         db::TypeHelper,
    924         notification::dummy_listen,
    925         subject::{IncomingKey, OutgoingSubject},
    926     };
    927     use taler_common::{
    928         api::{
    929             EddsaPublicKey, HashCode, ShortHashCode,
    930             params::{History, Page},
    931             wire::TransferState,
    932         },
    933         types::{
    934             amount::{Currency, decimal},
    935             url,
    936             utils::now_sql_stable_ts,
    937         },
    938     };
    939 
    940     use crate::{
    941         constants::CONFIG_SOURCE,
    942         db::{
    943             self, AddIncomingResult, AddOutgoingResult, BounceResult, ChargebackFailureResult,
    944             Transfer, TransferResult, TxIn, TxInAdmin, TxOut, TxOutKind,
    945         },
    946     };
    947 
    948     pub const CURR: Currency = Currency::TEST;
    949     pub const ROOT: CompactString = CompactString::const_new("localhost");
    950 
    951     async fn setup() -> (PoolConnection<Postgres>, PgPool) {
    952         taler_test_utils::db::db_test_setup(CONFIG_SOURCE).await
    953     }
    954 
    955     #[tokio::test]
    956     async fn kv() {
    957         let (mut db, _) = setup().await;
    958 
    959         let value = json!({
    960             "name": "Mr Smith",
    961             "no way": 32
    962         });
    963 
    964         assert_eq!(
    965             db::kv_get::<serde_json::Value>(&mut db, "value")
    966                 .await
    967                 .unwrap(),
    968             None
    969         );
    970         db::kv_set(&mut db, "value", &value).await.unwrap();
    971         db::kv_set(&mut db, "value", &value).await.unwrap();
    972         assert_eq!(
    973             db::kv_get::<serde_json::Value>(&mut db, "value")
    974                 .await
    975                 .unwrap(),
    976             Some(value)
    977         );
    978     }
    979 
    980     #[tokio::test]
    981     async fn tx_in() {
    982         let (mut db, pool) = setup().await;
    983 
    984         let mut routine = async |first: &Option<IncomingKey>, second: &Option<IncomingKey>| {
    985             let id = sqlx::query("SELECT count(*) + 1 FROM tx_in")
    986                 .try_map(|r: PgRow| r.try_get_u64(0))
    987                 .fetch_one(&mut *db)
    988                 .await
    989                 .unwrap();
    990             let now = now_sql_stable_ts();
    991             let later = now + Span::new().hours(2);
    992             let tx = TxIn {
    993                 transfer_id: now.as_microsecond() as i64,
    994                 tx_id: None,
    995                 amount: decimal("10"),
    996                 subject: "subject".to_owned(),
    997                 debtor_id: 31000163100000000,
    998                 debtor_name: "Name".into(),
    999                 valued_at: now,
   1000             };
   1001             // Insert
   1002             assert_eq!(
   1003                 db::register_tx_in(&mut db, &tx, first, &now)
   1004                     .await
   1005                     .expect("register tx in"),
   1006                 AddIncomingResult::Success {
   1007                     new: true,
   1008                     pending: false,
   1009                     row_id: id,
   1010                     valued_at: now,
   1011                 }
   1012             );
   1013             // Idempotent
   1014             assert_eq!(
   1015                 db::register_tx_in(
   1016                     &mut db,
   1017                     &TxIn {
   1018                         valued_at: later,
   1019                         ..tx.clone()
   1020                     },
   1021                     first,
   1022                     &now
   1023                 )
   1024                 .await
   1025                 .expect("register tx in"),
   1026                 AddIncomingResult::Success {
   1027                     new: false,
   1028                     pending: false,
   1029                     row_id: id,
   1030                     valued_at: now
   1031                 }
   1032             );
   1033             // Many
   1034             assert_eq!(
   1035                 db::register_tx_in(
   1036                     &mut db,
   1037                     &TxIn {
   1038                         transfer_id: later.as_microsecond() as i64,
   1039                         valued_at: later,
   1040                         ..tx
   1041                     },
   1042                     second,
   1043                     &now
   1044                 )
   1045                 .await
   1046                 .expect("register tx in"),
   1047                 AddIncomingResult::Success {
   1048                     new: true,
   1049                     pending: false,
   1050                     row_id: id + 1,
   1051                     valued_at: later
   1052                 }
   1053             );
   1054         };
   1055 
   1056         // Empty db
   1057         assert_eq!(
   1058             db::revenue_history(&pool, &History::default(), &CURR, &ROOT, dummy_listen)
   1059                 .await
   1060                 .unwrap(),
   1061             Vec::new()
   1062         );
   1063         assert_eq!(
   1064             db::incoming_history(&pool, &History::default(), &CURR, &ROOT, dummy_listen)
   1065                 .await
   1066                 .unwrap(),
   1067             Vec::new()
   1068         );
   1069 
   1070         // Regular transaction
   1071         routine(&None, &None).await;
   1072 
   1073         let first = EddsaPublicKey::rand();
   1074         let second = EddsaPublicKey::rand();
   1075 
   1076         // Reserve transaction
   1077         routine(
   1078             &Some(IncomingKey::reserve(first)),
   1079             &Some(IncomingKey::reserve(second)),
   1080         )
   1081         .await;
   1082 
   1083         // Kyc transaction
   1084         routine(
   1085             &Some(IncomingKey::kyc(first)),
   1086             &Some(IncomingKey::kyc(first)),
   1087         )
   1088         .await;
   1089 
   1090         // History
   1091         assert_eq!(
   1092             db::revenue_history(&pool, &History::default(), &CURR, &ROOT, dummy_listen)
   1093                 .await
   1094                 .unwrap()
   1095                 .len(),
   1096             6
   1097         );
   1098         assert_eq!(
   1099             db::incoming_history(&pool, &History::default(), &CURR, &ROOT, dummy_listen)
   1100                 .await
   1101                 .unwrap()
   1102                 .len(),
   1103             4
   1104         );
   1105     }
   1106 
   1107     #[tokio::test]
   1108     async fn tx_in_admin() {
   1109         let (_, pool) = setup().await;
   1110 
   1111         // Empty db
   1112         assert_eq!(
   1113             db::incoming_history(&pool, &History::default(), &CURR, &ROOT, dummy_listen)
   1114                 .await
   1115                 .unwrap(),
   1116             Vec::new()
   1117         );
   1118 
   1119         let now = now_sql_stable_ts();
   1120         let later = now + Span::new().hours(2);
   1121         let tx = TxInAdmin {
   1122             amount: decimal("10"),
   1123             subject: "subject".to_owned(),
   1124             debtor_id: 31000163100000000,
   1125             debtor_name: "Name".into(),
   1126             metadata: IncomingKey::reserve(EddsaPublicKey::rand()),
   1127         };
   1128         // Insert
   1129         assert_eq!(
   1130             db::register_tx_in_admin(&pool, &tx, &now)
   1131                 .await
   1132                 .expect("register tx in"),
   1133             AddIncomingResult::Success {
   1134                 new: true,
   1135                 pending: false,
   1136                 row_id: 1,
   1137                 valued_at: now
   1138             }
   1139         );
   1140         // Many
   1141         assert_eq!(
   1142             db::register_tx_in_admin(
   1143                 &pool,
   1144                 &TxInAdmin {
   1145                     subject: "Other".to_owned(),
   1146                     metadata: IncomingKey::reserve(EddsaPublicKey::rand()),
   1147                     ..tx.clone()
   1148                 },
   1149                 &later
   1150             )
   1151             .await
   1152             .expect("register tx in"),
   1153             AddIncomingResult::Success {
   1154                 new: true,
   1155                 pending: false,
   1156                 row_id: 2,
   1157                 valued_at: later
   1158             }
   1159         );
   1160 
   1161         // History
   1162         assert_eq!(
   1163             db::incoming_history(&pool, &History::default(), &CURR, &ROOT, dummy_listen)
   1164                 .await
   1165                 .unwrap()
   1166                 .len(),
   1167             2
   1168         );
   1169     }
   1170 
   1171     #[tokio::test]
   1172     async fn tx_out() {
   1173         let (mut db, pool) = setup().await;
   1174 
   1175         let mut routine = async |first: &TxOutKind, second: &TxOutKind| {
   1176             let transfer_id = sqlx::query("SELECT count(*) + 1 FROM tx_out")
   1177                 .try_map(|r: PgRow| r.try_get(0))
   1178                 .fetch_one(&mut *db)
   1179                 .await
   1180                 .unwrap();
   1181             let now = now_sql_stable_ts();
   1182             let later = now + Span::new().hours(2);
   1183             let tx = TxOut {
   1184                 transfer_id,
   1185                 tx_id: Some(transfer_id),
   1186                 amount: decimal("10"),
   1187                 subject: "subject".to_owned(),
   1188                 creditor_id: 31000163100000000,
   1189                 creditor_name: "Name".into(),
   1190                 valued_at: now,
   1191             };
   1192             assert_matches!(
   1193                 db::make_transfer(
   1194                     &pool,
   1195                     &Transfer {
   1196                         request_uid: HashCode::rand(),
   1197                         amount: decimal("10"),
   1198                         exchange_base_url: url("https://exchange.test.com/"),
   1199                         metadata: None,
   1200                         wtid: ShortHashCode::rand(),
   1201                         creditor_id: 31000163100000000,
   1202                         creditor_name: "Name".into()
   1203                     },
   1204                     &now
   1205                 )
   1206                 .await
   1207                 .unwrap(),
   1208                 TransferResult::Success { .. }
   1209             );
   1210             db::initiated_submit_success(&mut db, 1, &Timestamp::now(), transfer_id)
   1211                 .await
   1212                 .expect("status success");
   1213 
   1214             // Insert
   1215             assert_eq!(
   1216                 db::register_tx_out(&mut db, &tx, first, &now)
   1217                     .await
   1218                     .expect("register tx out"),
   1219                 AddOutgoingResult {
   1220                     result: db::RegisterResult::known,
   1221                     row_id: transfer_id,
   1222                 }
   1223             );
   1224             // Idempotent
   1225             assert_eq!(
   1226                 db::register_tx_out(
   1227                     &mut db,
   1228                     &TxOut {
   1229                         valued_at: later,
   1230                         ..tx.clone()
   1231                     },
   1232                     first,
   1233                     &now
   1234                 )
   1235                 .await
   1236                 .expect("register tx out"),
   1237                 AddOutgoingResult {
   1238                     result: db::RegisterResult::idempotent,
   1239                     row_id: transfer_id,
   1240                 }
   1241             );
   1242             // Recovered
   1243             assert_eq!(
   1244                 db::register_tx_out(
   1245                     &mut db,
   1246                     &TxOut {
   1247                         transfer_id: transfer_id + 1,
   1248                         tx_id: Some(transfer_id + 1),
   1249                         valued_at: later,
   1250                         ..tx.clone()
   1251                     },
   1252                     second,
   1253                     &now
   1254                 )
   1255                 .await
   1256                 .expect("register tx out"),
   1257                 AddOutgoingResult {
   1258                     result: db::RegisterResult::recovered,
   1259                     row_id: transfer_id + 1,
   1260                 }
   1261             );
   1262         };
   1263 
   1264         // Empty db
   1265         assert_eq!(
   1266             db::outgoing_history(&pool, &History::default(), &CURR, &ROOT, dummy_listen)
   1267                 .await
   1268                 .unwrap(),
   1269             Vec::new()
   1270         );
   1271 
   1272         // Regular transaction
   1273         routine(&TxOutKind::Simple, &TxOutKind::Simple).await;
   1274 
   1275         // Talerable transaction
   1276         routine(
   1277             &TxOutKind::Talerable(OutgoingSubject::rand()),
   1278             &TxOutKind::Talerable(OutgoingSubject::rand()),
   1279         )
   1280         .await;
   1281 
   1282         // Bounced transaction
   1283         routine(&TxOutKind::Bounce(21), &TxOutKind::Bounce(42)).await;
   1284 
   1285         // History
   1286         assert_eq!(
   1287             db::outgoing_history(&pool, &History::default(), &CURR, &ROOT, dummy_listen)
   1288                 .await
   1289                 .unwrap()
   1290                 .len(),
   1291             2
   1292         );
   1293     }
   1294 
   1295     // TODO tx out failure
   1296 
   1297     #[tokio::test]
   1298     async fn transfer() {
   1299         let (_, pool) = setup().await;
   1300 
   1301         // Empty db
   1302         assert_eq!(
   1303             db::transfer_by_id(&pool, 0, &CURR, &ROOT).await.unwrap(),
   1304             None
   1305         );
   1306         assert_eq!(
   1307             db::transfer_page(&pool, &None, &CURR, &ROOT, &Page::default())
   1308                 .await
   1309                 .unwrap(),
   1310             Vec::new()
   1311         );
   1312 
   1313         let req = Transfer {
   1314             request_uid: HashCode::rand(),
   1315             amount: decimal("10"),
   1316             exchange_base_url: url("https://exchange.test.com/"),
   1317             metadata: None,
   1318             wtid: ShortHashCode::rand(),
   1319             creditor_id: 31000163100000000,
   1320             creditor_name: "Name".into(),
   1321         };
   1322         let now = now_sql_stable_ts();
   1323         let later = now + Span::new().hours(2);
   1324         // Insert
   1325         assert_eq!(
   1326             db::make_transfer(&pool, &req, &now)
   1327                 .await
   1328                 .expect("transfer"),
   1329             TransferResult::Success {
   1330                 id: 1,
   1331                 initiated_at: now
   1332             }
   1333         );
   1334         // Idempotent
   1335         assert_eq!(
   1336             db::make_transfer(&pool, &req, &later)
   1337                 .await
   1338                 .expect("transfer"),
   1339             TransferResult::Success {
   1340                 id: 1,
   1341                 initiated_at: now
   1342             }
   1343         );
   1344         // Request UID reuse
   1345         assert_eq!(
   1346             db::make_transfer(
   1347                 &pool,
   1348                 &Transfer {
   1349                     wtid: ShortHashCode::rand(),
   1350                     ..req.clone()
   1351                 },
   1352                 &now
   1353             )
   1354             .await
   1355             .expect("transfer"),
   1356             TransferResult::RequestUidReuse
   1357         );
   1358         // wtid reuse
   1359         assert_eq!(
   1360             db::make_transfer(
   1361                 &pool,
   1362                 &Transfer {
   1363                     request_uid: HashCode::rand(),
   1364                     ..req.clone()
   1365                 },
   1366                 &now
   1367             )
   1368             .await
   1369             .expect("transfer"),
   1370             TransferResult::WtidReuse
   1371         );
   1372         // Many
   1373         assert_eq!(
   1374             db::make_transfer(
   1375                 &pool,
   1376                 &Transfer {
   1377                     request_uid: HashCode::rand(),
   1378                     wtid: ShortHashCode::rand(),
   1379                     ..req
   1380                 },
   1381                 &later
   1382             )
   1383             .await
   1384             .expect("transfer"),
   1385             TransferResult::Success {
   1386                 id: 2,
   1387                 initiated_at: later
   1388             }
   1389         );
   1390 
   1391         // Get
   1392         assert!(
   1393             db::transfer_by_id(&pool, 1, &CURR, &ROOT)
   1394                 .await
   1395                 .unwrap()
   1396                 .is_some()
   1397         );
   1398         assert!(
   1399             db::transfer_by_id(&pool, 2, &CURR, &ROOT)
   1400                 .await
   1401                 .unwrap()
   1402                 .is_some()
   1403         );
   1404         assert!(
   1405             db::transfer_by_id(&pool, 3, &CURR, &ROOT)
   1406                 .await
   1407                 .unwrap()
   1408                 .is_none()
   1409         );
   1410         assert_eq!(
   1411             db::transfer_page(&pool, &None, &CURR, &ROOT, &Page::default())
   1412                 .await
   1413                 .unwrap()
   1414                 .len(),
   1415             2
   1416         );
   1417     }
   1418 
   1419     #[tokio::test]
   1420     async fn bounce() {
   1421         let (mut db, _) = setup().await;
   1422 
   1423         let amount = decimal("10");
   1424         let now = now_sql_stable_ts();
   1425 
   1426         // Bounce
   1427         assert_eq!(
   1428             db::register_bounced_tx_in(
   1429                 &mut db,
   1430                 &TxIn {
   1431                     transfer_id: 12,
   1432                     tx_id: None,
   1433                     amount,
   1434                     subject: "subject".to_owned(),
   1435                     debtor_id: 31000163100000000,
   1436                     debtor_name: "Name".into(),
   1437                     valued_at: now
   1438                 },
   1439                 22,
   1440                 "good reason",
   1441                 &now
   1442             )
   1443             .await
   1444             .expect("bounce"),
   1445             BounceResult {
   1446                 tx_id: 1,
   1447                 tx_new: true
   1448             }
   1449         );
   1450         // Idempotent
   1451         assert_eq!(
   1452             db::register_bounced_tx_in(
   1453                 &mut db,
   1454                 &TxIn {
   1455                     transfer_id: 12,
   1456                     tx_id: None,
   1457                     amount,
   1458                     subject: "subject".to_owned(),
   1459                     debtor_id: 31000163100000000,
   1460                     debtor_name: "Name".into(),
   1461                     valued_at: now
   1462                 },
   1463                 22,
   1464                 "good reason",
   1465                 &now
   1466             )
   1467             .await
   1468             .expect("bounce"),
   1469             BounceResult {
   1470                 tx_id: 1,
   1471                 tx_new: false
   1472             }
   1473         );
   1474 
   1475         // Many
   1476         assert_eq!(
   1477             db::register_bounced_tx_in(
   1478                 &mut db,
   1479                 &TxIn {
   1480                     transfer_id: 13,
   1481                     tx_id: None,
   1482                     amount,
   1483                     subject: "subject".to_owned(),
   1484                     debtor_id: 31000163100000000,
   1485                     debtor_name: "Name".into(),
   1486                     valued_at: now
   1487                 },
   1488                 23,
   1489                 "good reason",
   1490                 &now
   1491             )
   1492             .await
   1493             .expect("bounce"),
   1494             BounceResult {
   1495                 tx_id: 2,
   1496                 tx_new: true
   1497             }
   1498         );
   1499     }
   1500 
   1501     #[tokio::test]
   1502     async fn status() {
   1503         let (mut db, pool) = setup().await;
   1504 
   1505         let check_status = async |id: u64, status: TransferState, msg: Option<&str>| {
   1506             let transfer = db::transfer_by_id(&pool, id, &CURR, &ROOT)
   1507                 .await
   1508                 .unwrap()
   1509                 .unwrap();
   1510             assert_eq!(
   1511                 (status, msg),
   1512                 (transfer.status, transfer.status_msg.as_deref())
   1513             );
   1514         };
   1515 
   1516         // Unknown transfer
   1517         db::initiated_submit_permanent_failure(&mut db, 1, "msg")
   1518             .await
   1519             .unwrap();
   1520         db::initiated_submit_success(&mut db, 1, &Timestamp::now(), 12)
   1521             .await
   1522             .unwrap();
   1523         assert_eq!(
   1524             db::initiated_chargeback_failure(&mut db, 1).await.unwrap(),
   1525             ChargebackFailureResult::Unknown
   1526         );
   1527 
   1528         // Failure
   1529         db::make_transfer(
   1530             &pool,
   1531             &Transfer {
   1532                 request_uid: HashCode::rand(),
   1533                 amount: decimal("1"),
   1534                 exchange_base_url: url("https://exchange.test.com/"),
   1535                 metadata: None,
   1536                 wtid: ShortHashCode::rand(),
   1537                 creditor_id: 31000163100000000,
   1538                 creditor_name: "Name".into(),
   1539             },
   1540             &Timestamp::now(),
   1541         )
   1542         .await
   1543         .expect("transfer");
   1544         check_status(1, TransferState::pending, None).await;
   1545         db::initiated_submit_permanent_failure(&mut db, 1, "error status")
   1546             .await
   1547             .unwrap();
   1548         check_status(1, TransferState::permanent_failure, Some("error status")).await;
   1549 
   1550         // Success
   1551         db::make_transfer(
   1552             &pool,
   1553             &Transfer {
   1554                 request_uid: HashCode::rand(),
   1555                 amount: decimal("1"),
   1556                 exchange_base_url: url("https://exchange.test.com/"),
   1557                 metadata: None,
   1558                 wtid: ShortHashCode::rand(),
   1559                 creditor_id: 31000163100000000,
   1560                 creditor_name: "Name".into(),
   1561             },
   1562             &Timestamp::now(),
   1563         )
   1564         .await
   1565         .expect("transfer");
   1566         check_status(2, TransferState::pending, None).await;
   1567         db::initiated_submit_success(&mut db, 2, &Timestamp::now(), 3)
   1568             .await
   1569             .unwrap();
   1570         check_status(2, TransferState::pending, None).await;
   1571         db::register_tx_out(
   1572             &mut db,
   1573             &TxOut {
   1574                 transfer_id: 5,
   1575                 tx_id: Some(3),
   1576                 amount: decimal("2"),
   1577                 subject: "".to_string(),
   1578                 creditor_id: 31000163100000000,
   1579                 creditor_name: "Name".into(),
   1580                 valued_at: Timestamp::now(),
   1581             },
   1582             &TxOutKind::Simple,
   1583             &Timestamp::now(),
   1584         )
   1585         .await
   1586         .unwrap();
   1587         check_status(2, TransferState::success, None).await;
   1588 
   1589         // Chargeback
   1590         assert_eq!(
   1591             db::initiated_chargeback_failure(&mut db, 5).await.unwrap(),
   1592             ChargebackFailureResult::Known(2)
   1593         );
   1594         check_status(2, TransferState::late_failure, Some("charged back")).await;
   1595         assert_eq!(
   1596             db::initiated_chargeback_failure(&mut db, 5).await.unwrap(),
   1597             ChargebackFailureResult::Idempotent(2)
   1598         );
   1599     }
   1600 
   1601     #[tokio::test]
   1602     async fn batch() {
   1603         let (mut db, pool) = setup().await;
   1604         let start = Timestamp::now();
   1605 
   1606         // Empty db
   1607         let pendings = db::pending_batch(&mut db, &start)
   1608             .await
   1609             .expect("pending_batch");
   1610         assert_eq!(pendings.len(), 0);
   1611 
   1612         // Some transfers
   1613         for i in 0..3 {
   1614             db::make_transfer(
   1615                 &pool,
   1616                 &Transfer {
   1617                     request_uid: HashCode::rand(),
   1618                     amount: decimal(format!("{}", i + 1)),
   1619                     exchange_base_url: url("https://exchange.test.com/"),
   1620                     metadata: None,
   1621                     wtid: ShortHashCode::rand(),
   1622                     creditor_id: 31000163100000000,
   1623                     creditor_name: "Name".into(),
   1624                 },
   1625                 &Timestamp::now(),
   1626             )
   1627             .await
   1628             .expect("transfer");
   1629         }
   1630         let pendings = db::pending_batch(&mut db, &start)
   1631             .await
   1632             .expect("pending_batch");
   1633         assert_eq!(pendings.len(), 3);
   1634 
   1635         // Max 100 txs in batch
   1636         for i in 0..100 {
   1637             db::make_transfer(
   1638                 &pool,
   1639                 &Transfer {
   1640                     request_uid: HashCode::rand(),
   1641                     amount: decimal(format!("{}", i + 1)),
   1642                     exchange_base_url: url("https://exchange.test.com/"),
   1643                     metadata: None,
   1644                     wtid: ShortHashCode::rand(),
   1645                     creditor_id: 31000163100000000,
   1646                     creditor_name: "Name".into(),
   1647                 },
   1648                 &Timestamp::now(),
   1649             )
   1650             .await
   1651             .expect("transfer");
   1652         }
   1653         let pendings = db::pending_batch(&mut db, &start)
   1654             .await
   1655             .expect("pending_batch");
   1656         assert_eq!(pendings.len(), 100);
   1657 
   1658         // Skip uploaded
   1659         for i in 0..=10 {
   1660             db::initiated_submit_success(&mut db, i, &Timestamp::now(), i)
   1661                 .await
   1662                 .expect("status success");
   1663         }
   1664         let pendings = db::pending_batch(&mut db, &start)
   1665             .await
   1666             .expect("pending_batch");
   1667         assert_eq!(pendings.len(), 93);
   1668 
   1669         // Skip failed
   1670         for i in 0..=10 {
   1671             db::initiated_submit_permanent_failure(&mut db, 10 + i, "failure")
   1672                 .await
   1673                 .expect("status failure");
   1674         }
   1675         let pendings = db::pending_batch(&mut db, &start)
   1676             .await
   1677             .expect("pending_batch");
   1678         assert_eq!(pendings.len(), 83);
   1679     }
   1680 }