Skip to main content

fints/
flow.rs

1//! High-level Flow API for the GraphQL layer.
2//!
3//! Orchestrates the two-step interactive TAN flow:
4//! 1. `Flow::initiate()` → challenge info, holds dialog in TanPending/Open
5//! 2. `Flow::confirm_and_fetch()` → polls TAN, fetches data
6
7use serde::{Deserialize, Serialize};
8use tracing::info;
9
10use crate::error::{FinTSError, Result};
11use crate::protocol::*;
12use crate::types::*;
13use crate::workflow::*;
14
15// ═══════════════════════════════════════════════════════════════════════════════
16// Public result types
17// ═══════════════════════════════════════════════════════════════════════════════
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ChallengeInfo {
21    pub challenge: ChallengeText,
22    pub challenge_hhduc: Option<HhdUcData>,
23    pub decoupled: bool,
24    pub tan_methods: Vec<TanMethod>,
25    pub allowed_security_functions: Vec<SecurityFunction>,
26    /// If true, skip TAN step — SCA exemption.
27    pub no_tan_required: bool,
28}
29
30/// Type alias for `FetchOpts` — preferred name in the flow/public API.
31/// Both names refer to the same type from the workflow module.
32pub use crate::workflow::FetchOpts as FetchOptions;
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SyncResult {
36    pub iban: Iban,
37    pub bic: Bic,
38    pub balance: Option<AccountBalance>,
39    pub transactions: Vec<Transaction>,
40    pub holdings: Vec<SecurityHolding>,
41    pub system_id: Option<SystemId>,
42}
43
44// ═══════════════════════════════════════════════════════════════════════════════
45// Flow
46// ═══════════════════════════════════════════════════════════════════════════════
47
48enum FlowState {
49    WaitingForTan {
50        dialog: Dialog<TanPending>,
51        task_reference: TaskReference,
52    },
53    Authenticated {
54        dialog: Dialog<Open>,
55    },
56    Done,
57}
58
59pub struct Flow {
60    bank: AnyBank,
61    state: FlowState,
62    system_id: SystemId,
63}
64
65impl Flow {
66    /// Step 1: initiate connection using an already-resolved AnyBank.
67    /// Use this when you have a custom BankConfig (e.g. non-registry banks).
68    pub async fn initiate_with_bank(
69        bank: AnyBank,
70        username: &UserId,
71        pin: &Pin,
72        product_id: &ProductId,
73        system_id: Option<&SystemId>,
74        target_iban: Option<&Iban>,
75        target_bic: Option<&Bic>,
76    ) -> Result<(Self, ChallengeInfo)> {
77        Self::initiate_inner(bank, username, pin, product_id, system_id, target_iban, target_bic).await
78    }
79
80    /// Step 1: initiate connection, get TAN challenge.
81    pub async fn initiate(
82        bank_id: &str,
83        username: &UserId,
84        pin: &Pin,
85        product_id: &ProductId,
86        system_id: Option<&SystemId>,
87        target_iban: Option<&Iban>,
88        target_bic: Option<&Bic>,
89    ) -> Result<(Self, ChallengeInfo)> {
90        let bank = bank_ops(bank_id)?;
91        Self::initiate_inner(bank, username, pin, product_id, system_id, target_iban, target_bic).await
92    }
93
94    async fn initiate_inner(
95        bank: AnyBank,
96        username: &UserId,
97        pin: &Pin,
98        product_id: &ProductId,
99        system_id: Option<&SystemId>,
100        target_iban: Option<&Iban>,
101        target_bic: Option<&Bic>,
102    ) -> Result<(Self, ChallengeInfo)> {
103        let outcome = bank.initiate(
104            username, pin, product_id, system_id, target_iban, target_bic,
105        ).await?;
106
107        match outcome {
108            InitiateOutcome::NeedTan(result) => {
109                let info = ChallengeInfo {
110                    challenge: result.challenge.challenge.clone(),
111                    challenge_hhduc: result.challenge.challenge_hhduc.clone(),      
112                    decoupled: result.challenge.decoupled,
113                    tan_methods: result.tan_methods,
114                    allowed_security_functions: result.allowed_security_functions,
115                    no_tan_required: false,
116                };
117                let flow = Flow {
118                    bank,
119                    state: FlowState::WaitingForTan {
120                        dialog: result.dialog,
121                        task_reference: result.challenge.task_reference,
122                    },
123                    system_id: result.system_id,
124                };
125                Ok((flow, info))
126            }
127            InitiateOutcome::Authenticated(result) => {
128                let info = ChallengeInfo {
129                    challenge: ChallengeText::new(""),
130                    challenge_hhduc: None,      
131                    decoupled: false,
132                    tan_methods: result.tan_methods,
133                    allowed_security_functions: result.allowed_security_functions,
134                    no_tan_required: true,
135                };
136                let flow = Flow {
137                    bank,
138                    state: FlowState::Authenticated { dialog: result.dialog },
139                    system_id: result.system_id,
140                };
141                Ok((flow, info))
142            }
143        }
144    }
145
146    /// Step 2: confirm TAN and fetch everything (balance + transactions + holdings).
147    /// Equivalent to `confirm_and_fetch_opts(iban, bic, FetchOpts::all(days))`.
148    pub async fn confirm_and_fetch(
149        &mut self,
150        iban: &str,
151        bic: &str,
152        days: u32,
153    ) -> Result<SyncResult> {
154        self.confirm_and_fetch_opts(iban, bic, &FetchOpts::all(days)).await
155    }
156
157    /// Step 2 with fine-grained fetch control.
158    /// Use `FetchOpts` to choose which data to retrieve in a single dialog.
159    pub async fn confirm_and_fetch_opts(
160        &mut self,
161        iban: &str,
162        bic: &str,
163        opts: &FetchOpts,
164    ) -> Result<SyncResult> {
165        let state = std::mem::replace(&mut self.state, FlowState::Done);
166
167        match state {
168            FlowState::WaitingForTan { dialog, task_reference } => {
169                let poll_result = dialog.poll(&task_reference).await?;
170
171                match poll_result {
172                    PollResult::Confirmed(mut open, _response) => {
173                        info!("[Flow] TAN confirmed — fetching data");
174                        let account = self.resolve_account(iban, bic)?;
175                        let fetch = self.bank.fetch_with_opts(&mut open, &account, opts).await?;
176                        let sys_id = open.system_id().clone();
177                        open.end().await.ok();
178                        Ok(SyncResult {
179                            iban: Iban::new(iban), bic: Bic::new(bic),
180                            balance: fetch.balance, transactions: fetch.transactions,
181                            holdings: fetch.holdings,
182                            system_id: Some(sys_id),
183                        })
184                    }
185                    PollResult::Pending(dialog) => {
186                        self.state = FlowState::WaitingForTan { dialog, task_reference };
187                        Err(FinTSError::Dialog(
188                            "TAN still pending: user has not yet confirmed in banking app".into()
189                        ))
190                    }
191                }
192            }
193            FlowState::Authenticated { mut dialog } => {
194                info!("[Flow] Already authenticated — fetching directly");
195                let account = self.resolve_account(iban, bic)?;
196                let fetch = self.bank.fetch_with_opts(&mut dialog, &account, opts).await?;
197                let sys_id = dialog.system_id().clone();
198                dialog.end().await.ok();
199                Ok(SyncResult {
200                    iban: Iban::new(iban), bic: Bic::new(bic),
201                    balance: fetch.balance, transactions: fetch.transactions,
202                    holdings: fetch.holdings,
203                    system_id: Some(sys_id),
204                })
205            }
206            FlowState::Done => {
207                Err(FinTSError::Dialog("Flow already completed".into()))
208            }
209        }
210    }
211
212    /// Step 2 (alternative): confirm TAN and fetch only securities holdings.
213    pub async fn confirm_and_fetch_holdings(
214        &mut self,
215        iban: &str,
216        bic: &str,
217    ) -> Result<Vec<SecurityHolding>> {
218        let state = std::mem::replace(&mut self.state, FlowState::Done);
219
220        match state {
221            FlowState::WaitingForTan { dialog, task_reference } => {
222                let poll_result = dialog.poll(&task_reference).await?;
223
224                match poll_result {
225                    PollResult::Confirmed(mut open, _response) => {
226                        info!("[Flow] TAN confirmed — fetching holdings");
227                        let account = self.resolve_account(iban, bic)?;
228                        let holdings = self.bank.fetch_holdings(&mut open, &account).await?;
229                        open.end().await.ok();
230                        Ok(holdings)
231                    }
232                    PollResult::Pending(dialog) => {
233                        self.state = FlowState::WaitingForTan { dialog, task_reference };
234                        Err(FinTSError::Dialog(
235                            "TAN still pending: user has not yet confirmed in banking app".into()
236                        ))
237                    }
238                }
239            }
240            FlowState::Authenticated { mut dialog } => {
241                info!("[Flow] Already authenticated — fetching holdings directly");
242                let account = self.resolve_account(iban, bic)?;
243                let holdings = self.bank.fetch_holdings(&mut dialog, &account).await?;
244                dialog.end().await.ok();
245                Ok(holdings)
246            }
247            FlowState::Done => {
248                Err(FinTSError::Dialog("Flow already completed".into()))
249            }
250        }
251    }
252
253    pub fn system_id(&self) -> &SystemId { &self.system_id }
254
255    /// Resolve account with bank's BIC as fallback.
256    fn resolve_account(&self, iban: &str, bic: &str) -> Result<Account> {
257        let bic = if bic.is_empty() { self.bank.config().bic.as_str() } else { bic };
258        Account::new(iban, bic)
259    }
260}