paykit_lib/
payment_reference.rs1use crate::{PaykitError, Result};
2
3#[derive(Clone, PartialEq, Eq, Hash)]
5pub struct PaymentReference(String);
6
7pub const PAYMENT_REFERENCE_MAX_LEN: usize = 256;
9
10impl PaymentReference {
11 pub fn new(reference: impl Into<String>) -> Result<Self> {
14 let reference = reference.into();
15 if reference.is_empty() {
16 return Err(PaykitError::Validation(
17 "Payment Reference must not be empty".into(),
18 ));
19 }
20 let char_count = reference.chars().count();
21 if char_count > PAYMENT_REFERENCE_MAX_LEN {
22 return Err(PaykitError::Validation(format!(
23 "Payment Reference must not exceed {PAYMENT_REFERENCE_MAX_LEN} characters, got {char_count}"
24 )));
25 }
26 if let Some((pos, ch)) = reference.char_indices().find(|&(_, ch)| ch.is_control()) {
27 return Err(PaykitError::Validation(format!(
28 "Payment Reference must not contain control character U+{:04X} at byte {pos}",
29 ch as u32
30 )));
31 }
32 Ok(Self(reference))
33 }
34
35 pub fn as_str(&self) -> &str {
37 &self.0
38 }
39}
40
41impl std::fmt::Display for PaymentReference {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.write_str(&self.0)
44 }
45}
46
47impl std::fmt::Debug for PaymentReference {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.write_str("PaymentReference(<redacted>)")
50 }
51}
52
53impl AsRef<str> for PaymentReference {
54 fn as_ref(&self) -> &str {
55 &self.0
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::PaykitError;
63
64 #[test]
65 fn test_payment_reference_accepts_text() {
66 let reference = PaymentReference::new("invoice 2026/0001").unwrap();
67 assert_eq!(reference.as_str(), "invoice 2026/0001");
68 assert_eq!(format!("{reference}"), "invoice 2026/0001");
69 assert_eq!(format!("{reference:?}"), "PaymentReference(<redacted>)");
70 }
71
72 #[test]
73 fn test_payment_reference_rejects_empty() {
74 let err = PaymentReference::new("").unwrap_err();
75 assert!(
76 matches!(err, PaykitError::Validation(ref msg) if msg.contains("must not be empty"))
77 );
78 }
79
80 #[test]
81 fn test_payment_reference_rejects_control_characters() {
82 let err = PaymentReference::new("invoice\n2026").unwrap_err();
83 assert!(
84 matches!(err, PaykitError::Validation(ref msg) if msg.contains("control character"))
85 );
86 }
87
88 #[test]
89 fn test_payment_reference_rejects_over_max_length() {
90 let reference = "a".repeat(PAYMENT_REFERENCE_MAX_LEN + 1);
91 let err = PaymentReference::new(reference).unwrap_err();
92 assert!(matches!(err, PaykitError::Validation(ref msg) if msg.contains("must not exceed")));
93 }
94}