use serde::{Deserialize, Serialize};
use tracing::info;
use crate::error::{FinTSError, Result};
use crate::protocol::*;
use crate::types::*;
use crate::workflow::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChallengeInfo {
pub challenge: ChallengeText,
pub challenge_hhduc: Option<HhdUcData>,
pub decoupled: bool,
pub tan_methods: Vec<TanMethod>,
pub allowed_security_functions: Vec<SecurityFunction>,
pub no_tan_required: bool,
}
pub use crate::workflow::FetchOpts as FetchOptions;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncResult {
pub iban: Iban,
pub bic: Bic,
pub balance: Option<AccountBalance>,
pub transactions: Vec<Transaction>,
pub holdings: Vec<SecurityHolding>,
pub system_id: Option<SystemId>,
}
enum FlowState {
WaitingForTan {
dialog: Dialog<TanPending>,
task_reference: TaskReference,
},
Authenticated {
dialog: Dialog<Open>,
},
Done,
}
pub struct Flow {
bank: AnyBank,
state: FlowState,
system_id: SystemId,
}
impl Flow {
pub async fn initiate_with_bank(
bank: AnyBank,
username: &UserId,
pin: &Pin,
product_id: &ProductId,
system_id: Option<&SystemId>,
target_iban: Option<&Iban>,
target_bic: Option<&Bic>,
) -> Result<(Self, ChallengeInfo)> {
Self::initiate_inner(bank, username, pin, product_id, system_id, target_iban, target_bic).await
}
pub async fn initiate(
bank_id: &str,
username: &UserId,
pin: &Pin,
product_id: &ProductId,
system_id: Option<&SystemId>,
target_iban: Option<&Iban>,
target_bic: Option<&Bic>,
) -> Result<(Self, ChallengeInfo)> {
let bank = bank_ops(bank_id)?;
Self::initiate_inner(bank, username, pin, product_id, system_id, target_iban, target_bic).await
}
async fn initiate_inner(
bank: AnyBank,
username: &UserId,
pin: &Pin,
product_id: &ProductId,
system_id: Option<&SystemId>,
target_iban: Option<&Iban>,
target_bic: Option<&Bic>,
) -> Result<(Self, ChallengeInfo)> {
let outcome = bank.initiate(
username, pin, product_id, system_id, target_iban, target_bic,
).await?;
match outcome {
InitiateOutcome::NeedTan(result) => {
let info = ChallengeInfo {
challenge: result.challenge.challenge.clone(),
challenge_hhduc: result.challenge.challenge_hhduc.clone(),
decoupled: result.challenge.decoupled,
tan_methods: result.tan_methods,
allowed_security_functions: result.allowed_security_functions,
no_tan_required: false,
};
let flow = Flow {
bank,
state: FlowState::WaitingForTan {
dialog: result.dialog,
task_reference: result.challenge.task_reference,
},
system_id: result.system_id,
};
Ok((flow, info))
}
InitiateOutcome::Authenticated(result) => {
let info = ChallengeInfo {
challenge: ChallengeText::new(""),
challenge_hhduc: None,
decoupled: false,
tan_methods: result.tan_methods,
allowed_security_functions: result.allowed_security_functions,
no_tan_required: true,
};
let flow = Flow {
bank,
state: FlowState::Authenticated { dialog: result.dialog },
system_id: result.system_id,
};
Ok((flow, info))
}
}
}
pub async fn confirm_and_fetch(
&mut self,
iban: &str,
bic: &str,
days: u32,
) -> Result<SyncResult> {
self.confirm_and_fetch_opts(iban, bic, &FetchOpts::all(days)).await
}
pub async fn confirm_and_fetch_opts(
&mut self,
iban: &str,
bic: &str,
opts: &FetchOpts,
) -> Result<SyncResult> {
let state = std::mem::replace(&mut self.state, FlowState::Done);
match state {
FlowState::WaitingForTan { dialog, task_reference } => {
let poll_result = dialog.poll(&task_reference).await?;
match poll_result {
PollResult::Confirmed(mut open, _response) => {
info!("[Flow] TAN confirmed — fetching data");
let account = self.resolve_account(iban, bic)?;
let fetch = self.bank.fetch_with_opts(&mut open, &account, opts).await?;
let sys_id = open.system_id().clone();
open.end().await.ok();
Ok(SyncResult {
iban: Iban::new(iban), bic: Bic::new(bic),
balance: fetch.balance, transactions: fetch.transactions,
holdings: fetch.holdings,
system_id: Some(sys_id),
})
}
PollResult::Pending(dialog) => {
self.state = FlowState::WaitingForTan { dialog, task_reference };
Err(FinTSError::Dialog(
"TAN still pending: user has not yet confirmed in banking app".into()
))
}
}
}
FlowState::Authenticated { mut dialog } => {
info!("[Flow] Already authenticated — fetching directly");
let account = self.resolve_account(iban, bic)?;
let fetch = self.bank.fetch_with_opts(&mut dialog, &account, opts).await?;
let sys_id = dialog.system_id().clone();
dialog.end().await.ok();
Ok(SyncResult {
iban: Iban::new(iban), bic: Bic::new(bic),
balance: fetch.balance, transactions: fetch.transactions,
holdings: fetch.holdings,
system_id: Some(sys_id),
})
}
FlowState::Done => {
Err(FinTSError::Dialog("Flow already completed".into()))
}
}
}
pub async fn confirm_and_fetch_holdings(
&mut self,
iban: &str,
bic: &str,
) -> Result<Vec<SecurityHolding>> {
let state = std::mem::replace(&mut self.state, FlowState::Done);
match state {
FlowState::WaitingForTan { dialog, task_reference } => {
let poll_result = dialog.poll(&task_reference).await?;
match poll_result {
PollResult::Confirmed(mut open, _response) => {
info!("[Flow] TAN confirmed — fetching holdings");
let account = self.resolve_account(iban, bic)?;
let holdings = self.bank.fetch_holdings(&mut open, &account).await?;
open.end().await.ok();
Ok(holdings)
}
PollResult::Pending(dialog) => {
self.state = FlowState::WaitingForTan { dialog, task_reference };
Err(FinTSError::Dialog(
"TAN still pending: user has not yet confirmed in banking app".into()
))
}
}
}
FlowState::Authenticated { mut dialog } => {
info!("[Flow] Already authenticated — fetching holdings directly");
let account = self.resolve_account(iban, bic)?;
let holdings = self.bank.fetch_holdings(&mut dialog, &account).await?;
dialog.end().await.ok();
Ok(holdings)
}
FlowState::Done => {
Err(FinTSError::Dialog("Flow already completed".into()))
}
}
}
pub fn system_id(&self) -> &SystemId { &self.system_id }
fn resolve_account(&self, iban: &str, bic: &str) -> Result<Account> {
let bic = if bic.is_empty() { self.bank.config().bic.as_str() } else { bic };
Account::new(iban, bic)
}
}