1use serde::{Deserialize, Serialize};
4use std::convert::TryFrom;
5use std::str::FromStr;
6
7#[derive(Debug, Clone, thiserror::Error)]
9#[error("Invalid NZ bank account number: '{0}' (expected format: XX-XXXX-XXXXXXX-XXX)")]
10pub struct InvalidBankAccountError(pub String);
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[repr(u8)]
18pub enum BankPrefix {
19 Anz = 1,
21 Bnz = 2,
23 Westpac = 3,
25 AnzWise = 4,
27 ChinaConstruction = 5,
29 AnzNational = 6,
31 Nab = 8,
33 Icbc = 10,
35 AnzPostBank = 11,
37 Asb = 12,
39 WestpacTrust = 13,
41 WestpacOtago = 14,
43 Tsb = 15,
45 WestpacSouthland = 16,
47 WestpacBop = 17,
49 WestpacCanterbury = 18,
51 WestpacWaikato = 19,
53 WestpacWellington = 20,
55 WestpacWestland = 21,
57 WestpacSouthCant = 22,
59 WestpacAuckland = 23,
61 AsbPartner = 24,
63 AnzPartner = 25,
65 Hsbc = 30,
67 Citibank = 31,
69 Kiwibank = 38,
71 BankOfChina = 88,
73}
74
75impl BankPrefix {
76 pub const fn as_str(&self) -> &'static str {
78 match self {
79 Self::Anz => "01",
80 Self::Bnz => "02",
81 Self::Westpac => "03",
82 Self::AnzWise => "04",
83 Self::ChinaConstruction => "05",
84 Self::AnzNational => "06",
85 Self::Nab => "08",
86 Self::Icbc => "10",
87 Self::AnzPostBank => "11",
88 Self::Asb => "12",
89 Self::WestpacTrust => "13",
90 Self::WestpacOtago => "14",
91 Self::Tsb => "15",
92 Self::WestpacSouthland => "16",
93 Self::WestpacBop => "17",
94 Self::WestpacCanterbury => "18",
95 Self::WestpacWaikato => "19",
96 Self::WestpacWellington => "20",
97 Self::WestpacWestland => "21",
98 Self::WestpacSouthCant => "22",
99 Self::WestpacAuckland => "23",
100 Self::AsbPartner => "24",
101 Self::AnzPartner => "25",
102 Self::Hsbc => "30",
103 Self::Citibank => "31",
104 Self::Kiwibank => "38",
105 Self::BankOfChina => "88",
106 }
107 }
108
109 pub const fn bank_name(&self) -> &'static str {
111 match self {
112 Self::Anz
113 | Self::AnzNational
114 | Self::AnzPostBank
115 | Self::AnzWise
116 | Self::AnzPartner => "ANZ",
117 Self::Bnz | Self::Nab => "Bank of New Zealand",
118 Self::Westpac
119 | Self::WestpacTrust
120 | Self::WestpacOtago
121 | Self::WestpacSouthland
122 | Self::WestpacBop
123 | Self::WestpacCanterbury
124 | Self::WestpacWaikato
125 | Self::WestpacWellington
126 | Self::WestpacWestland
127 | Self::WestpacSouthCant
128 | Self::WestpacAuckland => "Westpac",
129 Self::Asb | Self::AsbPartner => "ASB",
130 Self::Kiwibank => "Kiwibank",
131 Self::Tsb => "TSB",
132 Self::ChinaConstruction => "China Construction Bank",
133 Self::Icbc => "ICBC",
134 Self::Hsbc => "HSBC",
135 Self::Citibank => "Citibank",
136 Self::BankOfChina => "Bank of China",
137 }
138 }
139}
140
141impl FromStr for BankPrefix {
142 type Err = ();
143 fn from_str(s: &str) -> Result<Self, Self::Err> {
144 let numeric_part = s.trim_start_matches('0');
145 if numeric_part.is_empty() {
146 return Err(());
147 }
148 let val = numeric_part.parse::<u8>().map_err(|_| ())?;
149 Self::try_from(val)
150 }
151}
152
153impl TryFrom<u8> for BankPrefix {
154 type Error = ();
155 fn try_from(value: u8) -> Result<Self, Self::Error> {
156 match value {
157 1 => Ok(Self::Anz),
158 2 => Ok(Self::Bnz),
159 3 => Ok(Self::Westpac),
160 4 => Ok(Self::AnzWise),
161 5 => Ok(Self::ChinaConstruction),
162 6 => Ok(Self::AnzNational),
163 8 => Ok(Self::Nab),
164 10 => Ok(Self::Icbc),
165 11 => Ok(Self::AnzPostBank),
166 12 => Ok(Self::Asb),
167 13 => Ok(Self::WestpacTrust),
168 14 => Ok(Self::WestpacOtago),
169 15 => Ok(Self::Tsb),
170 16 => Ok(Self::WestpacSouthland),
171 17 => Ok(Self::WestpacBop),
172 18 => Ok(Self::WestpacCanterbury),
173 19 => Ok(Self::WestpacWaikato),
174 20 => Ok(Self::WestpacWellington),
175 21 => Ok(Self::WestpacWestland),
176 22 => Ok(Self::WestpacSouthCant),
177 23 => Ok(Self::WestpacAuckland),
178 24 => Ok(Self::AsbPartner),
179 25 => Ok(Self::AnzPartner),
180 30 => Ok(Self::Hsbc),
181 31 => Ok(Self::Citibank),
182 38 => Ok(Self::Kiwibank),
183 88 => Ok(Self::BankOfChina),
184 _ => Err(()),
185 }
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
201#[serde(transparent)]
202pub struct BankAccountNumber(String);
203
204impl BankAccountNumber {
205 pub fn new<T: Into<String>>(value: T) -> Result<Self, InvalidBankAccountError> {
207 let s = value.into();
208 let validate_parts = |parts: &[&str]| -> Result<(), ()> {
209 if parts.len() != 4 {
210 return Err(());
211 }
212 let Some(bank_code) = parts.get(0) else {
213 return Err(());
214 };
215 let Some(branch) = parts.get(1) else {
216 return Err(());
217 };
218 let Some(account) = parts.get(2) else {
219 return Err(());
220 };
221 let Some(suffix) = parts.get(3) else {
222 return Err(());
223 };
224
225 if BankPrefix::from_str(bank_code).is_err() {
226 return Err(());
227 }
228 if branch.len() != 4 || account.len() != 7 || suffix.len() != 3 {
229 return Err(());
230 }
231 if !parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit())) {
232 return Err(());
233 }
234 Ok(())
235 };
236
237 if s.contains('-') {
238 let parts: Vec<&str> = s.split('-').collect();
239 if validate_parts(&parts).is_err() {
240 return Err(InvalidBankAccountError(s));
241 }
242 Ok(Self(s))
243 } else {
244 if s.len() != 16 || !s.chars().all(|c| c.is_ascii_digit()) {
245 return Err(InvalidBankAccountError(s));
246 }
247 let bank_code = s
249 .get(0..2)
250 .ok_or_else(|| InvalidBankAccountError(s.clone()))?;
251 let branch = s
252 .get(2..6)
253 .ok_or_else(|| InvalidBankAccountError(s.clone()))?;
254 let account = s
255 .get(6..13)
256 .ok_or_else(|| InvalidBankAccountError(s.clone()))?;
257 let suffix = s
258 .get(13..16)
259 .ok_or_else(|| InvalidBankAccountError(s.clone()))?;
260
261 let parts = vec![bank_code, branch, account, suffix];
262 if validate_parts(&parts).is_err() {
263 return Err(InvalidBankAccountError(s));
264 }
265 let formatted = format!("{}-{}-{}-{}", bank_code, branch, account, suffix);
266 Ok(Self(formatted))
267 }
268 }
269
270 pub fn prefix(&self) -> BankPrefix {
272 BankPrefix::from_str(self.bank_code()).expect("Invalid prefix in stored account number")
273 }
274
275 pub fn bank_code(&self) -> &str {
277 self.0
278 .get(0..2)
279 .expect("bank code is always present in validated account number")
280 }
281
282 pub fn branch_code(&self) -> &str {
284 self.0
285 .get(3..7)
286 .expect("branch code is always present in validated account number")
287 }
288
289 pub fn account_number(&self) -> &str {
291 self.0
292 .get(8..15)
293 .expect("account number is always present in validated account number")
294 }
295
296 pub fn suffix(&self) -> &str {
298 self.0
299 .get(16..19)
300 .expect("suffix is always present in validated account number")
301 }
302
303 pub fn as_str(&self) -> &str {
305 &self.0
306 }
307}
308
309impl FromStr for BankAccountNumber {
310 type Err = InvalidBankAccountError;
311
312 fn from_str(s: &str) -> Result<Self, Self::Err> {
313 Self::new(s)
314 }
315}
316
317impl TryFrom<String> for BankAccountNumber {
318 type Error = InvalidBankAccountError;
319
320 fn try_from(value: String) -> Result<Self, Self::Error> {
321 Self::new(value)
322 }
323}
324
325impl TryFrom<&str> for BankAccountNumber {
326 type Error = InvalidBankAccountError;
327
328 fn try_from(value: &str) -> Result<Self, Self::Error> {
329 Self::new(value)
330 }
331}
332
333impl std::fmt::Display for BankAccountNumber {
334 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 write!(f, "{}", self.0)
336 }
337}
338
339impl AsRef<str> for BankAccountNumber {
340 fn as_ref(&self) -> &str {
341 &self.0
342 }
343}
344
345impl std::ops::Deref for BankAccountNumber {
346 type Target = str;
347 fn deref(&self) -> &Self::Target {
348 &self.0
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_component_extraction() {
358 let raw = "01-2345-6789012-000";
361 let account = BankAccountNumber::new(raw).expect("Should be valid");
362
363 assert_eq!(account.bank_code(), "01");
364 assert_eq!(account.branch_code(), "2345");
365 assert_eq!(account.account_number(), "6789012");
366 assert_eq!(account.suffix(), "000");
367 }
368
369 #[test]
370 fn test_component_extraction_from_unformatted() {
371 let raw = "3890000000000123"; let account = BankAccountNumber::new(raw).expect("Should be valid");
374
375 assert_eq!(account.as_str(), "38-9000-0000000-123");
377
378 assert_eq!(account.prefix(), BankPrefix::Kiwibank);
379 assert_eq!(account.bank_code(), "38");
380 assert_eq!(account.branch_code(), "9000");
381 assert_eq!(account.account_number(), "0000000");
382 assert_eq!(account.suffix(), "123");
383 }
384
385 #[test]
386 #[allow(clippy::unwrap_used, reason = "Tests are allowed to unwrap")]
387 fn test_extraction_integrity() {
388 let account = BankAccountNumber::new("12-3456-7890123-001").unwrap();
390
391 let reconstructed = format!(
392 "{}-{}-{}-{}",
393 account.bank_code(),
394 account.branch_code(),
395 account.account_number(),
396 account.suffix()
397 );
398
399 assert_eq!(account.as_str(), reconstructed);
400 }
401}