1use serde::{Deserialize, Serialize};
8use tracing::info;
9
10use crate::error::{FinTSError, Result};
11use crate::protocol::*;
12use crate::types::*;
13use crate::workflow::*;
14
15#[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 pub no_tan_required: bool,
28}
29
30pub 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
44enum 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 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 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 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 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 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 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}