fints_institute_db/
lib.rs

1//! This is a simple crate providing a convenient and safe interface to FinTS information of German
2//! banks. During the build it will download a file with all the banks which it will then put
3//! into the library itself so that no extra files have to be taken care of.
4//!
5//! Usage is easy:
6//!
7//! # Examples
8//!
9//! ```
10//! use fints_institute_db::get_bank_by_bank_code;
11//!
12//! let bank = get_bank_by_bank_code("12070000").unwrap();
13//! println!("{:?}", bank.pin_tan_address);
14//! ```
15
16use serde::{Deserialize, Serialize};
17use std::str::FromStr;
18
19static BANKS: &str = include_str!(concat!(env!("OUT_DIR"), "/blz.properties"));
20
21#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
22pub struct Bank {
23    pub bank_code: String,
24    pub institute: String,
25    pub location: String,
26    pub bic: String,
27    pub checksum_method: String,
28    pub rdh_address: Option<String>,
29    pub pin_tan_address: Option<String>,
30    pub rdh_version: Option<String>,
31    pub pin_tan_version: Option<String>,
32}
33
34impl FromStr for Bank {
35    type Err = String;
36
37    fn from_str(s: &str) -> Result<Self, Self::Err> {
38        // It's a bit of weird format that looks like this:
39        // bank_code=institute|location|bic|checksum_method|rdh_address|pin_tan_address|rdh_version|pin_tan_version
40        let first_part: Vec<&str> = s.split('=').collect();
41        let second_part: Vec<&str> = first_part[1].split('|').collect();
42
43        let bank = Bank {
44            bank_code: first_part[0].to_string(),
45            institute: second_part[0].to_string(),
46            location: second_part[1].to_string(),
47            bic: second_part[2].to_string(),
48            checksum_method: second_part[3].to_string(),
49            rdh_address: if second_part[4].is_empty() {
50                None
51            } else {
52                Some(second_part[4].to_string())
53            },
54            pin_tan_address: if second_part[5].is_empty() {
55                None
56            } else {
57                Some(second_part[5].to_string())
58            },
59            rdh_version: if second_part[6].is_empty() {
60                None
61            } else {
62                Some(second_part[6].to_string())
63            },
64            pin_tan_version: if second_part[7].is_empty() {
65                None
66            } else {
67                Some(second_part[7].to_string())
68            },
69        };
70        Ok(bank)
71    }
72}
73
74/// Retrieves the first bank with `bank_code`
75///
76/// Usually this is what you want unless you care about specific bank branches.
77///
78/// # Examples
79///
80/// ```
81/// use fints_institute_db::get_bank_by_bank_code;
82///
83/// let bank = get_bank_by_bank_code("12070000");
84/// println!("{:?}", bank);
85/// ```
86pub fn get_bank_by_bank_code(bank_code: &str) -> Option<Bank> {
87    for bank in BANKS.lines() {
88        let first_part: Vec<&str> = bank.split('=').collect();
89        let current_bank_code = first_part[0];
90        if current_bank_code == bank_code {
91            return Some(Bank::from_str(bank).expect(
92                    "Invalid bank format found in source file.
93                    This definitely shouldn't happen and is a serious issue with the bank info source file."
94            ));
95        }
96    }
97    None
98}
99
100/// Retrieves the bank by its `bic`
101///
102/// # Examples
103///
104/// ```
105/// use fints_institute_db::get_bank_by_bic;
106///
107/// let bank = get_bank_by_bic("GENODEM1MEN");
108/// println!("{:?}", bank);
109/// ```
110pub fn get_bank_by_bic(bic: &str) -> Option<Bank> {
111    BANKS
112        .lines()
113        .map(Bank::from_str)
114        .filter_map(Result::ok)
115        .find(|b| b.bic == bic)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use pretty_assertions::assert_eq;
122
123    /// assertion helper to keep the bank find tests DRY
124    fn assert_bank_matches(bank: &Bank) {
125        assert_eq!(bank.bank_code, "44761312");
126        assert_eq!(bank.institute, "Mendener Bank");
127        assert_eq!(bank.location, "Menden (Sauerland)");
128        assert_eq!(bank.bic, "GENODEM1MEN");
129        assert_eq!(bank.checksum_method, "34");
130        assert_eq!(bank.rdh_address, Some("hbci.gad.de".to_string()));
131        assert_eq!(
132            bank.pin_tan_address,
133            Some("https://fints1.atruvia.de/cgi-bin/hbciservlet".to_string())
134        );
135        assert_eq!(bank.rdh_version, Some("300".to_string()));
136        assert_eq!(bank.pin_tan_version, Some("300".to_string()));
137    }
138
139    // These tests use the real CSV input data.
140    // As such, the tests might break randomly if the input document changes.
141    // This should happen rarely enough that it's still worthwhile to test with real data, though.
142
143    #[test]
144    fn get_bank_by_bank_code_test() {
145        let bank_code = "44761312";
146        let bank = get_bank_by_bank_code(bank_code).unwrap();
147
148        assert_bank_matches(&bank);
149    }
150
151    #[test]
152    fn get_bank_by_bic_test() {
153        let bic = "GENODEM1MEN";
154        let bank = get_bank_by_bic(bic).unwrap();
155
156        assert_bank_matches(&bank);
157    }
158}
159
160#[cfg(doctest)]
161doc_comment::doctest!("../../README.md", readme);