ach.rs (5535B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::{fmt::Display, str::FromStr}; 18 19 use compact_str::CompactString; 20 use serde_with::{DeserializeFromStr, SerializeDisplay}; 21 22 use crate::types::utils::{InlineAscii, InlineStr}; 23 24 const ROUTING_NB_SIZE: usize = 9; 25 const MAX_ACCOUNT_NB_SIZE: usize = 17; 26 27 /// ACH routing number 28 #[derive(Clone, Copy, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 29 pub struct RoutingNumber(InlineAscii<ROUTING_NB_SIZE>); 30 31 impl RoutingNumber { 32 #[inline] 33 pub fn checksum(s: &[u8; 9]) -> u8 { 34 // Sum raw ASCII values directly using 16-bit registers 35 let sum0 = s[0] as u16 + s[3] as u16 + s[6] as u16; 36 let sum1 = s[1] as u16 + s[4] as u16 + s[7] as u16; 37 let sum2 = s[2] as u16 + s[5] as u16 + s[8] as u16; 38 39 // Fold all 9 `b'0'` (48) ASCII subtractions into a single constant offset: 40 // (3 * 3 + 7 * 3 + 1 * 3) * 48 = 33 * 48 = 1584 41 let total = 3 * sum0 + 7 * sum1 + sum2 - 1584; 42 (total % 10) as u8 43 } 44 45 pub fn federal_reserve_routine_symbol(&self) -> &str { 46 // SAFETY len = 9 47 unsafe { self.as_ref().get_unchecked(0..4) } 48 } 49 50 pub fn financial_institution_identifier(&self) -> &str { 51 // SAFETY len = 8 52 unsafe { self.as_ref().get_unchecked(4..8) } 53 } 54 } 55 56 impl AsRef<str> for RoutingNumber { 57 fn as_ref(&self) -> &str { 58 self.0.as_ref() 59 } 60 } 61 62 #[derive(Debug, PartialEq, Eq, thiserror::Error)] 63 pub enum RoutineNumberErrKind { 64 #[error("contains illegal characters (only 0-9 allowed)")] 65 Invalid, 66 #[error("invalid check digit")] 67 Checksum, 68 #[error("bad size expected {ROUTING_NB_SIZE} chars got {0}")] 69 Size(usize), 70 } 71 72 #[derive(Debug, thiserror::Error)] 73 #[error("ACH routing number '{routing_number}' {kind}")] 74 pub struct ParseRoutingNumberErr { 75 routing_number: CompactString, 76 pub kind: RoutineNumberErrKind, 77 } 78 79 impl FromStr for RoutingNumber { 80 type Err = ParseRoutingNumberErr; 81 82 fn from_str(s: &str) -> Result<Self, Self::Err> { 83 let bytes: &[u8] = s.as_bytes(); 84 if let Ok(array) = <&[u8] as TryInto<[u8; ROUTING_NB_SIZE]>>::try_into(bytes) { 85 if !array.iter().all(u8::is_ascii_digit) { 86 Err(RoutineNumberErrKind::Invalid) 87 } else if Self::checksum(&array) != 0 { 88 Err(RoutineNumberErrKind::Checksum) 89 } else { 90 Ok(Self(InlineAscii::from(array))) 91 } 92 } else { 93 Err(RoutineNumberErrKind::Size(bytes.len())) 94 } 95 .map_err(|kind| ParseRoutingNumberErr { 96 routing_number: s.into(), 97 kind, 98 }) 99 } 100 } 101 102 impl Display for RoutingNumber { 103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 104 Display::fmt(&self.as_ref(), f) 105 } 106 } 107 108 impl std::fmt::Debug for RoutingNumber { 109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 110 Display::fmt(&self, f) 111 } 112 } 113 114 /// ACH account number 115 #[derive(Clone, Copy, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 116 pub struct AccountNumber(InlineStr<MAX_ACCOUNT_NB_SIZE>); 117 118 impl AsRef<str> for AccountNumber { 119 fn as_ref(&self) -> &str { 120 self.0.as_ref() 121 } 122 } 123 124 #[derive(Debug, PartialEq, Eq, thiserror::Error)] 125 pub enum AccountNumberErrKind { 126 #[error("contains illegal characters (only 0-9A-Z allowed)")] 127 Invalid, 128 #[error("bad size expected max {MAX_ACCOUNT_NB_SIZE} chars got {0}")] 129 Size(usize), 130 } 131 132 #[derive(Debug, thiserror::Error)] 133 #[error("ACH account number '{account_number}' {kind}")] 134 pub struct ParseAccountNumberErr { 135 account_number: CompactString, 136 pub kind: AccountNumberErrKind, 137 } 138 impl FromStr for AccountNumber { 139 type Err = ParseAccountNumberErr; 140 141 fn from_str(s: &str) -> Result<Self, Self::Err> { 142 let bytes: &[u8] = s.as_bytes(); 143 let len = bytes.len(); 144 if len > MAX_ACCOUNT_NB_SIZE { 145 Err(AccountNumberErrKind::Size(len)) 146 } else if !bytes.iter().all(u8::is_ascii_alphanumeric) { 147 Err(AccountNumberErrKind::Invalid) 148 } else { 149 Ok(Self( 150 InlineStr::try_from_iter(bytes.iter().copied().map(|b| b.to_ascii_uppercase())) 151 .unwrap(), 152 )) 153 } 154 .map_err(|kind| ParseAccountNumberErr { 155 account_number: s.into(), 156 kind, 157 }) 158 } 159 } 160 161 impl Display for AccountNumber { 162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 163 Display::fmt(&self.as_ref(), f) 164 } 165 } 166 167 impl std::fmt::Debug for AccountNumber { 168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 169 Display::fmt(&self, f) 170 } 171 } 172 173 #[test] 174 fn ach() { 175 for nb in ["111000038", "021000021"] { 176 let parsed = RoutingNumber::from_str(nb).unwrap(); 177 assert_eq!(parsed.to_string(), nb) 178 } 179 }