Skip to main content

use_ach/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Common ACH primitives.
8pub mod prelude {
9    pub use crate::{
10        AchAccountType, AchAddendaIndicator, AchCompanyId, AchEntry, AchEntryDirection, AchError,
11        AchIndividualId, AchStandardEntryClass, AchTraceNumber, AchTransactionCode,
12    };
13}
14
15/// Conservative ACH Standard Entry Class vocabulary.
16#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum AchStandardEntryClass {
18    /// Corporate credit or debit entry.
19    Ccd,
20    /// Corporate trade exchange entry.
21    Ctx,
22    /// Prearranged payment and deposit entry.
23    Ppd,
24    /// Telephone-initiated entry.
25    Tel,
26    /// Internet-initiated or mobile entry.
27    Web,
28    /// Accounts receivable check conversion entry.
29    Arc,
30    /// Back office conversion entry.
31    Boc,
32    /// Point-of-purchase entry.
33    Pop,
34    /// Re-presented check entry.
35    Rck,
36    /// International ACH transaction entry.
37    Iat,
38}
39
40impl AchStandardEntryClass {
41    /// Returns the three-character SEC code.
42    #[must_use]
43    pub const fn as_str(self) -> &'static str {
44        match self {
45            Self::Ccd => "CCD",
46            Self::Ctx => "CTX",
47            Self::Ppd => "PPD",
48            Self::Tel => "TEL",
49            Self::Web => "WEB",
50            Self::Arc => "ARC",
51            Self::Boc => "BOC",
52            Self::Pop => "POP",
53            Self::Rck => "RCK",
54            Self::Iat => "IAT",
55        }
56    }
57
58    /// Creates a SEC value from a three-character code.
59    ///
60    /// # Errors
61    ///
62    /// Returns [`AchError::InvalidStandardEntryClass`] when the code is not in this crate's
63    /// conservative SEC vocabulary.
64    pub fn from_code(value: impl AsRef<str>) -> Result<Self, AchError> {
65        let value = value.as_ref().trim();
66        if value.eq_ignore_ascii_case("CCD") {
67            Ok(Self::Ccd)
68        } else if value.eq_ignore_ascii_case("CTX") {
69            Ok(Self::Ctx)
70        } else if value.eq_ignore_ascii_case("PPD") {
71            Ok(Self::Ppd)
72        } else if value.eq_ignore_ascii_case("TEL") {
73            Ok(Self::Tel)
74        } else if value.eq_ignore_ascii_case("WEB") {
75            Ok(Self::Web)
76        } else if value.eq_ignore_ascii_case("ARC") {
77            Ok(Self::Arc)
78        } else if value.eq_ignore_ascii_case("BOC") {
79            Ok(Self::Boc)
80        } else if value.eq_ignore_ascii_case("POP") {
81            Ok(Self::Pop)
82        } else if value.eq_ignore_ascii_case("RCK") {
83            Ok(Self::Rck)
84        } else if value.eq_ignore_ascii_case("IAT") {
85            Ok(Self::Iat)
86        } else {
87            Err(AchError::InvalidStandardEntryClass)
88        }
89    }
90}
91
92impl fmt::Display for AchStandardEntryClass {
93    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94        formatter.write_str(self.as_str())
95    }
96}
97
98impl FromStr for AchStandardEntryClass {
99    type Err = AchError;
100
101    fn from_str(value: &str) -> Result<Self, Self::Err> {
102        Self::from_code(value)
103    }
104}
105
106/// Broad ACH account type vocabulary.
107#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub enum AchAccountType {
109    /// Checking account.
110    Checking,
111    /// Savings account.
112    Savings,
113    /// Loan account.
114    Loan,
115}
116
117impl AchAccountType {
118    /// Returns a stable lowercase account type label.
119    #[must_use]
120    pub const fn as_str(self) -> &'static str {
121        match self {
122            Self::Checking => "checking",
123            Self::Savings => "savings",
124            Self::Loan => "loan",
125        }
126    }
127}
128
129impl fmt::Display for AchAccountType {
130    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131        formatter.write_str(self.as_str())
132    }
133}
134
135/// ACH entry direction vocabulary.
136#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
137pub enum AchEntryDirection {
138    /// Credit entry.
139    Credit,
140    /// Debit entry.
141    Debit,
142}
143
144impl AchEntryDirection {
145    /// Returns a stable lowercase direction label.
146    #[must_use]
147    pub const fn as_str(self) -> &'static str {
148        match self {
149            Self::Credit => "credit",
150            Self::Debit => "debit",
151        }
152    }
153}
154
155impl fmt::Display for AchEntryDirection {
156    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157        formatter.write_str(self.as_str())
158    }
159}
160
161/// Conservative ACH transaction-code vocabulary.
162#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
163pub enum AchTransactionCode {
164    /// Credit destined for a checking account.
165    CheckingCredit,
166    /// Prenotification credit destined for a checking account.
167    CheckingPrenoteCredit,
168    /// Debit destined for a checking account.
169    CheckingDebit,
170    /// Prenotification debit destined for a checking account.
171    CheckingPrenoteDebit,
172    /// Credit destined for a savings account.
173    SavingsCredit,
174    /// Prenotification credit destined for a savings account.
175    SavingsPrenoteCredit,
176    /// Debit destined for a savings account.
177    SavingsDebit,
178    /// Prenotification debit destined for a savings account.
179    SavingsPrenoteDebit,
180    /// Credit destined for a loan account.
181    LoanCredit,
182    /// Prenotification credit destined for a loan account.
183    LoanPrenoteCredit,
184}
185
186impl AchTransactionCode {
187    /// Returns the two-digit NACHA transaction code as a number.
188    #[must_use]
189    pub const fn code(self) -> u8 {
190        match self {
191            Self::CheckingCredit => 22,
192            Self::CheckingPrenoteCredit => 23,
193            Self::CheckingDebit => 27,
194            Self::CheckingPrenoteDebit => 28,
195            Self::SavingsCredit => 32,
196            Self::SavingsPrenoteCredit => 33,
197            Self::SavingsDebit => 37,
198            Self::SavingsPrenoteDebit => 38,
199            Self::LoanCredit => 52,
200            Self::LoanPrenoteCredit => 53,
201        }
202    }
203
204    /// Creates a transaction code from a numeric NACHA transaction code.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`AchError::InvalidTransactionCode`] when the code is not in this crate's
209    /// conservative transaction-code vocabulary.
210    pub const fn from_code(code: u8) -> Result<Self, AchError> {
211        match code {
212            22 => Ok(Self::CheckingCredit),
213            23 => Ok(Self::CheckingPrenoteCredit),
214            27 => Ok(Self::CheckingDebit),
215            28 => Ok(Self::CheckingPrenoteDebit),
216            32 => Ok(Self::SavingsCredit),
217            33 => Ok(Self::SavingsPrenoteCredit),
218            37 => Ok(Self::SavingsDebit),
219            38 => Ok(Self::SavingsPrenoteDebit),
220            52 => Ok(Self::LoanCredit),
221            53 => Ok(Self::LoanPrenoteCredit),
222            _ => Err(AchError::InvalidTransactionCode),
223        }
224    }
225
226    /// Returns the account type implied by the transaction code.
227    #[must_use]
228    pub const fn account_type(self) -> AchAccountType {
229        match self {
230            Self::CheckingCredit
231            | Self::CheckingPrenoteCredit
232            | Self::CheckingDebit
233            | Self::CheckingPrenoteDebit => AchAccountType::Checking,
234            Self::SavingsCredit
235            | Self::SavingsPrenoteCredit
236            | Self::SavingsDebit
237            | Self::SavingsPrenoteDebit => AchAccountType::Savings,
238            Self::LoanCredit | Self::LoanPrenoteCredit => AchAccountType::Loan,
239        }
240    }
241
242    /// Returns the direction implied by the transaction code.
243    #[must_use]
244    pub const fn direction(self) -> AchEntryDirection {
245        match self {
246            Self::CheckingCredit
247            | Self::CheckingPrenoteCredit
248            | Self::SavingsCredit
249            | Self::SavingsPrenoteCredit
250            | Self::LoanCredit
251            | Self::LoanPrenoteCredit => AchEntryDirection::Credit,
252            Self::CheckingDebit
253            | Self::CheckingPrenoteDebit
254            | Self::SavingsDebit
255            | Self::SavingsPrenoteDebit => AchEntryDirection::Debit,
256        }
257    }
258
259    /// Returns whether the transaction code is a prenotification code.
260    #[must_use]
261    pub const fn is_prenote(self) -> bool {
262        matches!(
263            self,
264            Self::CheckingPrenoteCredit
265                | Self::CheckingPrenoteDebit
266                | Self::SavingsPrenoteCredit
267                | Self::SavingsPrenoteDebit
268                | Self::LoanPrenoteCredit
269        )
270    }
271}
272
273impl fmt::Display for AchTransactionCode {
274    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
275        write!(formatter, "{:02}", self.code())
276    }
277}
278
279impl FromStr for AchTransactionCode {
280    type Err = AchError;
281
282    fn from_str(value: &str) -> Result<Self, Self::Err> {
283        let value = value.trim();
284        let bytes = value.as_bytes();
285        if bytes.len() != 2 || !bytes.iter().all(u8::is_ascii_digit) {
286            return Err(AchError::InvalidTransactionCode);
287        }
288
289        let code = ((bytes[0] - b'0') * 10) + (bytes[1] - b'0');
290        Self::from_code(code)
291    }
292}
293
294/// ACH addenda indicator vocabulary.
295#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
296pub enum AchAddendaIndicator {
297    /// No addenda record is attached.
298    NoAddenda,
299    /// One or more addenda records are attached.
300    Addenda,
301}
302
303impl AchAddendaIndicator {
304    /// Returns the single-character NACHA addenda indicator.
305    #[must_use]
306    pub const fn as_str(self) -> &'static str {
307        match self {
308            Self::NoAddenda => "0",
309            Self::Addenda => "1",
310        }
311    }
312
313    /// Returns whether addenda are present.
314    #[must_use]
315    pub const fn has_addenda(self) -> bool {
316        matches!(self, Self::Addenda)
317    }
318
319    /// Creates an addenda indicator from `0` or `1`.
320    ///
321    /// # Errors
322    ///
323    /// Returns [`AchError::InvalidAddendaIndicator`] when the value is not `0` or `1`.
324    pub fn from_code(value: impl AsRef<str>) -> Result<Self, AchError> {
325        match value.as_ref().trim() {
326            "0" => Ok(Self::NoAddenda),
327            "1" => Ok(Self::Addenda),
328            _ => Err(AchError::InvalidAddendaIndicator),
329        }
330    }
331}
332
333impl fmt::Display for AchAddendaIndicator {
334    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
335        formatter.write_str(self.as_str())
336    }
337}
338
339impl FromStr for AchAddendaIndicator {
340    type Err = AchError;
341
342    fn from_str(value: &str) -> Result<Self, Self::Err> {
343        Self::from_code(value)
344    }
345}
346
347/// A validated 15-digit ACH trace number.
348#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
349pub struct AchTraceNumber(String);
350
351impl AchTraceNumber {
352    /// Creates an ACH trace number from 15 digits.
353    ///
354    /// # Errors
355    ///
356    /// Returns [`AchError::InvalidTraceNumberLength`] when the trimmed input is not 15 bytes and
357    /// [`AchError::InvalidTraceNumberCharacter`] when any byte is not a digit.
358    pub fn new(value: impl AsRef<str>) -> Result<Self, AchError> {
359        let value = value.as_ref().trim();
360        if value.len() != 15 {
361            return Err(AchError::InvalidTraceNumberLength);
362        }
363
364        if !value.bytes().all(|byte| byte.is_ascii_digit()) {
365            return Err(AchError::InvalidTraceNumberCharacter);
366        }
367
368        Ok(Self(value.to_owned()))
369    }
370
371    /// Returns the full 15-digit trace number.
372    #[must_use]
373    pub fn as_str(&self) -> &str {
374        &self.0
375    }
376
377    /// Returns the first eight digits of the trace number.
378    #[must_use]
379    pub fn odfi_identification(&self) -> &str {
380        &self.0[..8]
381    }
382
383    /// Returns the final seven digits of the trace number.
384    #[must_use]
385    pub fn sequence_number(&self) -> &str {
386        &self.0[8..]
387    }
388}
389
390impl AsRef<str> for AchTraceNumber {
391    fn as_ref(&self) -> &str {
392        self.as_str()
393    }
394}
395
396impl fmt::Display for AchTraceNumber {
397    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
398        formatter.write_str(self.as_str())
399    }
400}
401
402impl FromStr for AchTraceNumber {
403    type Err = AchError;
404
405    fn from_str(value: &str) -> Result<Self, Self::Err> {
406        Self::new(value)
407    }
408}
409
410/// A conservatively validated ACH company identifier.
411#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
412pub struct AchCompanyId(String);
413
414impl AchCompanyId {
415    /// Creates an ACH company identifier from 1 to 10 conservative ASCII characters.
416    ///
417    /// # Errors
418    ///
419    /// Returns [`AchError::EmptyCompanyId`] when the trimmed input is empty,
420    /// [`AchError::CompanyIdTooLong`] when the input is longer than 10 bytes, and
421    /// [`AchError::InvalidCompanyIdCharacter`] when the input contains unsupported characters.
422    pub fn new(value: impl AsRef<str>) -> Result<Self, AchError> {
423        validate_identifier(
424            value.as_ref(),
425            10,
426            AchError::EmptyCompanyId,
427            AchError::CompanyIdTooLong,
428            AchError::InvalidCompanyIdCharacter,
429        )
430        .map(Self)
431    }
432
433    /// Returns the company identifier.
434    #[must_use]
435    pub fn as_str(&self) -> &str {
436        &self.0
437    }
438}
439
440impl AsRef<str> for AchCompanyId {
441    fn as_ref(&self) -> &str {
442        self.as_str()
443    }
444}
445
446impl fmt::Display for AchCompanyId {
447    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
448        formatter.write_str(self.as_str())
449    }
450}
451
452impl FromStr for AchCompanyId {
453    type Err = AchError;
454
455    fn from_str(value: &str) -> Result<Self, Self::Err> {
456        Self::new(value)
457    }
458}
459
460/// A conservatively validated ACH individual identifier.
461#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
462pub struct AchIndividualId(String);
463
464impl AchIndividualId {
465    /// Creates an ACH individual identifier from 1 to 15 conservative ASCII characters.
466    ///
467    /// # Errors
468    ///
469    /// Returns [`AchError::EmptyIndividualId`] when the trimmed input is empty,
470    /// [`AchError::IndividualIdTooLong`] when the input is longer than 15 bytes, and
471    /// [`AchError::InvalidIndividualIdCharacter`] when the input contains unsupported characters.
472    pub fn new(value: impl AsRef<str>) -> Result<Self, AchError> {
473        validate_identifier(
474            value.as_ref(),
475            15,
476            AchError::EmptyIndividualId,
477            AchError::IndividualIdTooLong,
478            AchError::InvalidIndividualIdCharacter,
479        )
480        .map(Self)
481    }
482
483    /// Returns the individual identifier.
484    #[must_use]
485    pub fn as_str(&self) -> &str {
486        &self.0
487    }
488}
489
490impl AsRef<str> for AchIndividualId {
491    fn as_ref(&self) -> &str {
492        self.as_str()
493    }
494}
495
496impl fmt::Display for AchIndividualId {
497    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
498        formatter.write_str(self.as_str())
499    }
500}
501
502impl FromStr for AchIndividualId {
503    type Err = AchError;
504
505    fn from_str(value: &str) -> Result<Self, Self::Err> {
506        Self::new(value)
507    }
508}
509
510/// Lightweight ACH entry metadata composed from validated primitives.
511#[derive(Clone, Debug, Eq, PartialEq)]
512pub struct AchEntry {
513    standard_entry_class: AchStandardEntryClass,
514    transaction_code: AchTransactionCode,
515    trace_number: AchTraceNumber,
516    company_id: AchCompanyId,
517    individual_id: AchIndividualId,
518    addenda_indicator: AchAddendaIndicator,
519}
520
521impl AchEntry {
522    /// Creates ACH entry metadata with no addenda indicator attached.
523    #[must_use]
524    pub const fn new(
525        standard_entry_class: AchStandardEntryClass,
526        transaction_code: AchTransactionCode,
527        trace_number: AchTraceNumber,
528        company_id: AchCompanyId,
529        individual_id: AchIndividualId,
530    ) -> Self {
531        Self {
532            standard_entry_class,
533            transaction_code,
534            trace_number,
535            company_id,
536            individual_id,
537            addenda_indicator: AchAddendaIndicator::NoAddenda,
538        }
539    }
540
541    /// Returns the standard entry class.
542    #[must_use]
543    pub const fn standard_entry_class(&self) -> AchStandardEntryClass {
544        self.standard_entry_class
545    }
546
547    /// Returns the transaction code.
548    #[must_use]
549    pub const fn transaction_code(&self) -> AchTransactionCode {
550        self.transaction_code
551    }
552
553    /// Returns the trace number.
554    #[must_use]
555    pub const fn trace_number(&self) -> &AchTraceNumber {
556        &self.trace_number
557    }
558
559    /// Returns the company identifier.
560    #[must_use]
561    pub const fn company_id(&self) -> &AchCompanyId {
562        &self.company_id
563    }
564
565    /// Returns the individual identifier.
566    #[must_use]
567    pub const fn individual_id(&self) -> &AchIndividualId {
568        &self.individual_id
569    }
570
571    /// Returns the addenda indicator.
572    #[must_use]
573    pub const fn addenda_indicator(&self) -> AchAddendaIndicator {
574        self.addenda_indicator
575    }
576
577    /// Sets the addenda indicator.
578    #[must_use]
579    pub const fn with_addenda_indicator(mut self, addenda_indicator: AchAddendaIndicator) -> Self {
580        self.addenda_indicator = addenda_indicator;
581        self
582    }
583}
584
585/// Errors returned by ACH primitives.
586#[derive(Clone, Copy, Debug, Eq, PartialEq)]
587pub enum AchError {
588    /// The SEC code is outside this crate's conservative vocabulary.
589    InvalidStandardEntryClass,
590    /// The transaction code is outside this crate's conservative vocabulary.
591    InvalidTransactionCode,
592    /// The addenda indicator was not `0` or `1`.
593    InvalidAddendaIndicator,
594    /// ACH trace numbers must be exactly 15 digits.
595    InvalidTraceNumberLength,
596    /// ACH trace numbers must contain only digits.
597    InvalidTraceNumberCharacter,
598    /// The company identifier was empty after trimming whitespace.
599    EmptyCompanyId,
600    /// The company identifier was longer than 10 bytes.
601    CompanyIdTooLong,
602    /// The company identifier contained an unsupported character.
603    InvalidCompanyIdCharacter,
604    /// The individual identifier was empty after trimming whitespace.
605    EmptyIndividualId,
606    /// The individual identifier was longer than 15 bytes.
607    IndividualIdTooLong,
608    /// The individual identifier contained an unsupported character.
609    InvalidIndividualIdCharacter,
610}
611
612impl fmt::Display for AchError {
613    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
614        match self {
615            Self::InvalidStandardEntryClass => {
616                formatter.write_str("ACH standard entry class is unsupported")
617            },
618            Self::InvalidTransactionCode => {
619                formatter.write_str("ACH transaction code is unsupported")
620            },
621            Self::InvalidAddendaIndicator => {
622                formatter.write_str("ACH addenda indicator must be 0 or 1")
623            },
624            Self::InvalidTraceNumberLength => {
625                formatter.write_str("ACH trace number must be exactly 15 digits")
626            },
627            Self::InvalidTraceNumberCharacter => {
628                formatter.write_str("ACH trace number must contain only digits")
629            },
630            Self::EmptyCompanyId => formatter.write_str("ACH company identifier cannot be empty"),
631            Self::CompanyIdTooLong => {
632                formatter.write_str("ACH company identifier cannot exceed 10 bytes")
633            },
634            Self::InvalidCompanyIdCharacter => {
635                formatter.write_str("ACH company identifier contains an unsupported character")
636            },
637            Self::EmptyIndividualId => {
638                formatter.write_str("ACH individual identifier cannot be empty")
639            },
640            Self::IndividualIdTooLong => {
641                formatter.write_str("ACH individual identifier cannot exceed 15 bytes")
642            },
643            Self::InvalidIndividualIdCharacter => {
644                formatter.write_str("ACH individual identifier contains an unsupported character")
645            },
646        }
647    }
648}
649
650impl Error for AchError {}
651
652fn validate_identifier(
653    value: &str,
654    max_len: usize,
655    empty_error: AchError,
656    too_long_error: AchError,
657    invalid_character_error: AchError,
658) -> Result<String, AchError> {
659    let value = value.trim();
660    if value.is_empty() {
661        return Err(empty_error);
662    }
663
664    if value.len() > max_len {
665        return Err(too_long_error);
666    }
667
668    if !value.bytes().all(is_identifier_byte) {
669        return Err(invalid_character_error);
670    }
671
672    Ok(value.to_owned())
673}
674
675const fn is_identifier_byte(byte: u8) -> bool {
676    byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')
677}
678
679#[cfg(test)]
680mod tests {
681    use core::str::FromStr;
682
683    use super::{
684        AchAccountType, AchAddendaIndicator, AchCompanyId, AchEntry, AchEntryDirection, AchError,
685        AchIndividualId, AchStandardEntryClass, AchTraceNumber, AchTransactionCode,
686    };
687
688    #[test]
689    fn parses_and_displays_standard_entry_classes() -> Result<(), AchError> {
690        assert_eq!(
691            AchStandardEntryClass::from_code("ppd")?,
692            AchStandardEntryClass::Ppd
693        );
694        assert_eq!(AchStandardEntryClass::Web.as_str(), "WEB");
695        assert_eq!(AchStandardEntryClass::Ccd.to_string(), "CCD");
696        assert_eq!(
697            AchStandardEntryClass::from_code("XYZ"),
698            Err(AchError::InvalidStandardEntryClass)
699        );
700        Ok(())
701    }
702
703    #[test]
704    fn exposes_transaction_code_behavior() -> Result<(), AchError> {
705        let credit = AchTransactionCode::from_code(22)?;
706        let debit = AchTransactionCode::from_str("38")?;
707
708        assert_eq!(credit.code(), 22);
709        assert_eq!(credit.account_type(), AchAccountType::Checking);
710        assert_eq!(credit.direction(), AchEntryDirection::Credit);
711        assert!(!credit.is_prenote());
712        assert_eq!(debit.account_type(), AchAccountType::Savings);
713        assert_eq!(debit.direction(), AchEntryDirection::Debit);
714        assert!(debit.is_prenote());
715        assert_eq!(
716            AchTransactionCode::from_code(99),
717            Err(AchError::InvalidTransactionCode)
718        );
719        Ok(())
720    }
721
722    #[test]
723    fn validates_trace_numbers() -> Result<(), AchError> {
724        let trace = AchTraceNumber::new("123456780000001")?;
725
726        assert_eq!(trace.as_str(), "123456780000001");
727        assert_eq!(trace.odfi_identification(), "12345678");
728        assert_eq!(trace.sequence_number(), "0000001");
729        assert_eq!(
730            AchTraceNumber::new("12345678000001"),
731            Err(AchError::InvalidTraceNumberLength)
732        );
733        assert_eq!(
734            AchTraceNumber::new("12345678000000A"),
735            Err(AchError::InvalidTraceNumberCharacter)
736        );
737        Ok(())
738    }
739
740    #[test]
741    fn validates_identifiers() -> Result<(), AchError> {
742        let company_id = AchCompanyId::new(" 1234567890 ")?;
743        let individual_id = AchIndividualId::new("EMPLOYEE-001")?;
744
745        assert_eq!(company_id.as_str(), "1234567890");
746        assert_eq!(individual_id.as_str(), "EMPLOYEE-001");
747        assert_eq!(AchCompanyId::new(""), Err(AchError::EmptyCompanyId));
748        assert_eq!(
749            AchCompanyId::new("12345678901"),
750            Err(AchError::CompanyIdTooLong)
751        );
752        assert_eq!(
753            AchIndividualId::new("employee 001"),
754            Err(AchError::InvalidIndividualIdCharacter)
755        );
756        Ok(())
757    }
758
759    #[test]
760    fn supports_addenda_indicator() -> Result<(), AchError> {
761        assert_eq!(
762            AchAddendaIndicator::from_code("0")?,
763            AchAddendaIndicator::NoAddenda
764        );
765        assert_eq!(
766            AchAddendaIndicator::from_code("1")?,
767            AchAddendaIndicator::Addenda
768        );
769        assert_eq!(AchAddendaIndicator::Addenda.as_str(), "1");
770        assert!(AchAddendaIndicator::Addenda.has_addenda());
771        assert_eq!(
772            AchAddendaIndicator::from_code("2"),
773            Err(AchError::InvalidAddendaIndicator)
774        );
775        Ok(())
776    }
777
778    #[test]
779    fn creates_entry_metadata() -> Result<(), AchError> {
780        let entry = AchEntry::new(
781            AchStandardEntryClass::Ppd,
782            AchTransactionCode::CheckingCredit,
783            AchTraceNumber::new("123456780000001")?,
784            AchCompanyId::new("1234567890")?,
785            AchIndividualId::new("EMPLOYEE001")?,
786        )
787        .with_addenda_indicator(AchAddendaIndicator::Addenda);
788
789        assert_eq!(entry.standard_entry_class(), AchStandardEntryClass::Ppd);
790        assert_eq!(entry.transaction_code(), AchTransactionCode::CheckingCredit);
791        assert_eq!(entry.trace_number().as_str(), "123456780000001");
792        assert_eq!(entry.company_id().as_str(), "1234567890");
793        assert_eq!(entry.individual_id().as_str(), "EMPLOYEE001");
794        assert_eq!(entry.addenda_indicator(), AchAddendaIndicator::Addenda);
795        Ok(())
796    }
797}