Skip to main content

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}