api.rs (8640B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2024, 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 jiff::Timestamp; 18 use sqlx::PgPool; 19 use taler_common::{ 20 api_params::{History, Page}, 21 api_revenue::RevenueIncomingHistory, 22 api_transfer::{ 23 RegistrationRequest, RegistrationResponse, SubjectFormat, TransferSubject, Unregistration, 24 }, 25 api_wire::{ 26 AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest, 27 IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse, 28 TransferState, TransferStatus, 29 }, 30 db::IncomingType, 31 error_code::ErrorCode, 32 types::{ 33 amount::Currency, 34 payto::{FullQuery, payto}, 35 timestamp::TalerTimestamp, 36 }, 37 }; 38 use tokio::sync::watch::Sender; 39 40 use crate::{ 41 api::{TalerApi, revenue::Revenue, transfer::PreparedTransfer, wire::WireGateway}, 42 error::{ApiResult, failure, failure_code}, 43 subject::fmt_in_subject, 44 test::db::{self, AddIncomingResult}, 45 }; 46 47 /// Taler API implementation for tests 48 pub struct TestApi { 49 pub currency: Currency, 50 pub pool: PgPool, 51 pub outgoing_channel: Sender<i64>, 52 pub incoming_channel: Sender<i64>, 53 } 54 55 impl TalerApi for TestApi { 56 fn currency(&self) -> &str { 57 self.currency.as_ref() 58 } 59 60 fn implementation(&self) -> &'static str { 61 "urn:net:taler:specs:taler-test-api:taler-rust" 62 } 63 } 64 65 impl WireGateway for TestApi { 66 async fn transfer(&self, req: TransferRequest) -> ApiResult<TransferResponse> { 67 req.credit_account.query::<FullQuery>()?; 68 let result = db::transfer(&self.pool, &req).await?; 69 match result { 70 db::TransferResult::Success(transfer_response) => Ok(transfer_response), 71 db::TransferResult::RequestUidReuse => { 72 Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED)) 73 } 74 db::TransferResult::WtidReuse => { 75 Err(failure_code(ErrorCode::BANK_TRANSFER_WTID_REUSED)) 76 } 77 } 78 } 79 80 async fn transfer_page( 81 &self, 82 page: Page, 83 status: Option<TransferState>, 84 ) -> ApiResult<TransferList> { 85 Ok(TransferList { 86 transfers: db::transfer_page(&self.pool, &status, &page, &self.currency).await?, 87 debit_account: payto("payto://test"), 88 }) 89 } 90 91 async fn transfer_by_id(&self, id: u64) -> ApiResult<Option<TransferStatus>> { 92 Ok(db::transfer_by_id(&self.pool, id, &self.currency).await?) 93 } 94 95 async fn outgoing_history(&self, params: History) -> ApiResult<OutgoingHistory> { 96 let txs = db::outgoing_revenue(&self.pool, ¶ms, &self.currency, || { 97 self.outgoing_channel.subscribe() 98 }) 99 .await?; 100 Ok(OutgoingHistory { 101 outgoing_transactions: txs, 102 debit_account: payto("payto://test"), 103 }) 104 } 105 106 async fn incoming_history(&self, params: History) -> ApiResult<IncomingHistory> { 107 let txs = db::incoming_history(&self.pool, ¶ms, &self.currency, || { 108 self.incoming_channel.subscribe() 109 }) 110 .await?; 111 Ok(IncomingHistory { 112 incoming_transactions: txs, 113 credit_account: payto("payto://test"), 114 }) 115 } 116 117 async fn add_incoming_reserve( 118 &self, 119 req: AddIncomingRequest, 120 ) -> ApiResult<AddIncomingResponse> { 121 let res = db::add_incoming( 122 &self.pool, 123 &req.amount, 124 &req.debit_account, 125 "", 126 &Timestamp::now(), 127 IncomingType::reserve, 128 &req.reserve_pub, 129 ) 130 .await?; 131 match res { 132 AddIncomingResult::Success { id, created_at } => Ok(AddIncomingResponse { 133 timestamp: created_at.into(), 134 row_id: id, 135 }), 136 AddIncomingResult::ReservePubReuse => { 137 Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) 138 } 139 AddIncomingResult::UnknownMapping | AddIncomingResult::MappingReuse => { 140 unreachable!("mapping not used") 141 } 142 } 143 } 144 145 async fn add_incoming_kyc(&self, req: AddKycauthRequest) -> ApiResult<AddIncomingResponse> { 146 let res = db::add_incoming( 147 &self.pool, 148 &req.amount, 149 &req.debit_account, 150 "", 151 &Timestamp::now(), 152 IncomingType::kyc, 153 &req.account_pub, 154 ) 155 .await?; 156 match res { 157 AddIncomingResult::Success { id, created_at } => Ok(AddIncomingResponse { 158 timestamp: created_at.into(), 159 row_id: id, 160 }), 161 AddIncomingResult::ReservePubReuse => { 162 Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) 163 } 164 AddIncomingResult::UnknownMapping | AddIncomingResult::MappingReuse => { 165 unreachable!("mapping not used") 166 } 167 } 168 } 169 170 async fn add_incoming_mapped(&self, req: AddMappedRequest) -> ApiResult<AddIncomingResponse> { 171 let res = db::add_incoming( 172 &self.pool, 173 &req.amount, 174 &req.debit_account, 175 "", 176 &Timestamp::now(), 177 IncomingType::map, 178 &req.authorization_pub, 179 ) 180 .await?; 181 match res { 182 AddIncomingResult::Success { id, created_at } => Ok(AddIncomingResponse { 183 timestamp: created_at.into(), 184 row_id: id, 185 }), 186 AddIncomingResult::ReservePubReuse => { 187 Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) 188 } 189 AddIncomingResult::UnknownMapping => { 190 Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_UNKNOWN)) 191 } 192 AddIncomingResult::MappingReuse => { 193 Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_REUSED)) 194 } 195 } 196 } 197 198 fn support_account_check(&self) -> bool { 199 false 200 } 201 } 202 203 impl Revenue for TestApi { 204 async fn history(&self, params: History) -> ApiResult<RevenueIncomingHistory> { 205 let txs = db::revenue_history(&self.pool, ¶ms, &self.currency, || { 206 self.incoming_channel.subscribe() 207 }) 208 .await?; 209 Ok(RevenueIncomingHistory { 210 incoming_transactions: txs, 211 credit_account: payto("payto://test"), 212 }) 213 } 214 } 215 216 impl PreparedTransfer for TestApi { 217 fn supported_formats(&self) -> &[SubjectFormat] { 218 &[SubjectFormat::SIMPLE] 219 } 220 221 async fn registration(&self, req: RegistrationRequest) -> ApiResult<RegistrationResponse> { 222 match db::transfer_register(&self.pool, &req).await? { 223 db::RegistrationResult::Success => { 224 let simple = TransferSubject::Simple { 225 credit_amount: req.credit_amount, 226 subject: if req.authorization_pub == req.account_pub && !req.recurrent { 227 fmt_in_subject(req.r#type.into(), &req.account_pub) 228 } else { 229 fmt_in_subject(IncomingType::map, &req.authorization_pub) 230 }, 231 }; 232 ApiResult::Ok(RegistrationResponse { 233 subjects: vec![simple], 234 expiration: TalerTimestamp::Never, 235 }) 236 } 237 db::RegistrationResult::ReservePubReuse => { 238 ApiResult::Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) 239 } 240 } 241 } 242 243 async fn unregistration(&self, req: Unregistration) -> ApiResult<()> { 244 if !db::transfer_unregister(&self.pool, &req).await? { 245 Err(failure( 246 ErrorCode::BANK_TRANSACTION_NOT_FOUND, 247 format!("Prepared transfer '{}' not found", req.authorization_pub), 248 )) 249 } else { 250 Ok(()) 251 } 252 } 253 }