fints/dkb.rs
1//! DKB (Deutsche Kreditbank) high-level API.
2//!
3//! Provides a clean, bank-specific entry point for DKB FinTS operations.
4//! All methods use the typestate Dialog under the hood — you cannot misuse them.
5//!
6//! ## Interactive two-step flow
7//!
8//! ```rust,no_run
9//! use fints::{dkb, Account, UserId, Pin, ProductId};
10//!
11//! # async fn example() -> fints::Result<()> {
12//! // Step 1: Connect and get TAN challenge
13//! let (session, challenge) = dkb::connect(
14//! &UserId::new("username"), &Pin::new("pin"), &ProductId::new("PRODUCT_ID"), None,
15//! ).await?;
16//! println!("Please confirm in your banking app: {}", challenge.challenge);
17//!
18//! // Step 2: Create a validated account (BIC required — compile-time safety)
19//! let account = Account::new("DE12345678901234", "BYLADEM1001")?;
20//!
21//! // Step 3: After user confirms, fetch data
22//! let result = session.fetch(&account, 365).await?;
23//! println!("Balance: {:?}", result.balance);
24//! println!("{} transactions", result.transactions.len());
25//! # Ok(())
26//! # }
27//! ```
28
29use crate::error::{FinTSError, Result};
30use crate::protocol::*;
31use crate::types::{SystemId, TaskReference, ChallengeText, HhdUcData, UserId, Pin, ProductId};
32use crate::workflow::{BankOps, Dkb, FetchResult, InitiateOutcome};
33
34/// A DKB session in progress. Wraps the dialog state machine.
35pub enum Session {
36 /// Waiting for TAN confirmation (pushTAN).
37 WaitingForTan {
38 dialog: Dialog<TanPending>,
39 task_reference: TaskReference,
40 bank: Dkb,
41 system_id: SystemId,
42 },
43 /// Already authenticated (SCA exemption).
44 Ready {
45 dialog: Dialog<Open>,
46 bank: Dkb,
47 system_id: SystemId,
48 },
49}
50
51/// Challenge information returned from `connect()`.
52pub struct Challenge {
53 /// Text to display to the user.
54 pub challenge: ChallengeText,
55 /// HHD-UC data for optical TAN methods (None for pushTAN).
56 pub challenge_hhduc: Option<HhdUcData>,
57 /// Whether this is a decoupled method (pushTAN = true).
58 pub decoupled: bool,
59 /// If true, no TAN needed — call `fetch()` immediately.
60 pub no_tan_required: bool,
61}
62
63/// Result of a successful fetch operation.
64pub use crate::workflow::FetchResult as SyncData;
65
66/// Connect to DKB and get a TAN challenge.
67///
68/// This performs:
69/// 1. Sync dialog (get system_id + BPD)
70/// 2. Normal dialog init with HKTAN:4 (triggers pushTAN)
71///
72/// Returns a `Session` (holding the dialog in the correct typestate)
73/// and a `Challenge` with info for the user.
74pub async fn connect(
75 username: &UserId,
76 pin: &Pin,
77 product_id: &ProductId,
78 system_id: Option<&SystemId>,
79) -> Result<(Session, Challenge)> {
80 let bank = Dkb::new();
81
82 let outcome = bank.initiate(username, pin, product_id, system_id, None, None).await?;
83
84 match outcome {
85 InitiateOutcome::NeedTan(result) => {
86 let challenge = Challenge {
87 challenge: result.challenge.challenge.clone(),
88 challenge_hhduc: result.challenge.challenge_hhduc.clone(),
89 decoupled: result.challenge.decoupled,
90 no_tan_required: false,
91 };
92 let session = Session::WaitingForTan {
93 dialog: result.dialog,
94 task_reference: result.challenge.task_reference,
95 bank,
96 system_id: result.system_id,
97 };
98 Ok((session, challenge))
99 }
100 InitiateOutcome::Authenticated(result) => {
101 let challenge = Challenge {
102 challenge: ChallengeText::new(""),
103 challenge_hhduc: None,
104 decoupled: false,
105 no_tan_required: true,
106 };
107 let session = Session::Ready {
108 dialog: result.dialog,
109 bank,
110 system_id: result.system_id,
111 };
112 Ok((session, challenge))
113 }
114 }
115}
116
117impl Session {
118 /// Fetch balance and transactions for an account.
119 ///
120 /// Takes `&Account` — IBAN and BIC are guaranteed present at compile time.
121 /// If TAN is pending (pushTAN), this polls the bank first.
122 /// Returns `Err` with "TAN still pending" if not yet confirmed — retry after waiting.
123 ///
124 /// On success, the dialog is closed and the session is consumed.
125 pub async fn fetch(
126 self,
127 account: &Account,
128 days: u32,
129 ) -> Result<FetchResult> {
130 match self {
131 Session::WaitingForTan { dialog, task_reference, bank, .. } => {
132 let poll_result = dialog.poll(&task_reference).await?;
133 match poll_result {
134 PollResult::Confirmed(mut open, _response) => {
135 let result = bank.fetch(&mut open, account, days).await?;
136 open.end().await.ok();
137 Ok(result)
138 }
139 PollResult::Pending(_dialog) => {
140 Err(FinTSError::Dialog(
141 "TAN still pending: user has not yet confirmed in banking app".into()
142 ))
143 }
144 }
145 }
146 Session::Ready { mut dialog, bank, .. } => {
147 let result = bank.fetch(&mut dialog, account, days).await?;
148 dialog.end().await.ok();
149 Ok(result)
150 }
151 }
152 }
153
154 /// Get the system ID (persist this for future sessions to avoid re-sync).
155 pub fn system_id(&self) -> &SystemId {
156 match self {
157 Session::WaitingForTan { system_id, .. } => system_id,
158 Session::Ready { system_id, .. } => system_id,
159 }
160 }
161}