Skip to main content

datasynth_core/templates/realism/
reference_formats.rs

1//! Enhanced reference number format generation.
2//!
3//! Provides ERP-style reference number generation with multiple format
4//! options and realistic patterns.
5
6use rand::Rng;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Mutex;
11
12/// Reference format types.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
14#[serde(rename_all = "snake_case")]
15pub enum EnhancedReferenceFormat {
16    /// Standard format: PREFIX-YYYY-NNNNNN
17    #[default]
18    Standard,
19    /// SAP-style: 10-digit number (e.g., 4500000001)
20    SapStyle,
21    /// Oracle-style: PREFIX-ORG-YYYY-NNNNN
22    OracleStyle,
23    /// NetSuite-style: PREFIX-NNNNN
24    NetSuiteStyle,
25    /// Random alphanumeric: AAANNNNNNA
26    Alphanumeric,
27    /// UUID-based short reference
28    ShortUuid,
29    /// Date-based: YYYYMMDD-NNNN
30    DateBased,
31    /// Vendor invoice style (external): Various formats
32    VendorInvoice,
33    /// Bank reference: BANK-DATE-NNNN
34    BankReference,
35    /// Check number: 6-digit sequential
36    CheckNumber,
37}
38
39/// Reference style configuration.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
41#[serde(rename_all = "snake_case")]
42pub enum ReferenceStyle {
43    #[default]
44    Modern,
45    Legacy,
46    Erp,
47    Simple,
48}
49
50/// Enhanced reference generator with multiple format support.
51#[derive(Debug)]
52pub struct EnhancedReferenceGenerator {
53    counters: Mutex<HashMap<(EnhancedReferenceFormat, i32), AtomicU64>>,
54    sap_counter: AtomicU64,
55    check_counter: AtomicU64,
56}
57
58impl Clone for EnhancedReferenceGenerator {
59    fn clone(&self) -> Self {
60        Self::new()
61    }
62}
63
64impl Default for EnhancedReferenceGenerator {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl EnhancedReferenceGenerator {
71    /// Create a new reference generator.
72    pub fn new() -> Self {
73        Self {
74            counters: Mutex::new(HashMap::new()),
75            sap_counter: AtomicU64::new(4500000001),
76            check_counter: AtomicU64::new(100001),
77        }
78    }
79
80    /// Generate a reference number.
81    pub fn generate(
82        &self,
83        format: EnhancedReferenceFormat,
84        year: i32,
85        rng: &mut impl Rng,
86    ) -> String {
87        match format {
88            EnhancedReferenceFormat::Standard => self.generate_standard(year),
89            EnhancedReferenceFormat::SapStyle => self.generate_sap_style(),
90            EnhancedReferenceFormat::OracleStyle => self.generate_oracle_style(year),
91            EnhancedReferenceFormat::NetSuiteStyle => self.generate_netsuite_style(),
92            EnhancedReferenceFormat::Alphanumeric => self.generate_alphanumeric(rng),
93            EnhancedReferenceFormat::ShortUuid => self.generate_short_uuid(rng),
94            EnhancedReferenceFormat::DateBased => self.generate_date_based(year, rng),
95            EnhancedReferenceFormat::VendorInvoice => self.generate_vendor_invoice(rng),
96            EnhancedReferenceFormat::BankReference => self.generate_bank_reference(year, rng),
97            EnhancedReferenceFormat::CheckNumber => self.generate_check_number(),
98        }
99    }
100
101    /// Generate a reference for a specific document type.
102    pub fn generate_for_document(
103        &self,
104        doc_type: DocumentType,
105        year: i32,
106        _rng: &mut impl Rng,
107    ) -> String {
108        let prefix = doc_type.prefix();
109        let seq = self.next_sequence(EnhancedReferenceFormat::Standard, year);
110        format!("{}-{}-{:06}", prefix, year, seq)
111    }
112
113    /// Generate an external reference (vendor/bank style).
114    pub fn generate_external(&self, rng: &mut impl Rng) -> String {
115        self.generate_vendor_invoice(rng)
116    }
117
118    fn generate_standard(&self, year: i32) -> String {
119        let seq = self.next_sequence(EnhancedReferenceFormat::Standard, year);
120        format!("DOC-{}-{:06}", year, seq)
121    }
122
123    fn generate_sap_style(&self) -> String {
124        let num = self.sap_counter.fetch_add(1, Ordering::Relaxed);
125        format!("{:010}", num)
126    }
127
128    fn generate_oracle_style(&self, year: i32) -> String {
129        let seq = self.next_sequence(EnhancedReferenceFormat::OracleStyle, year);
130        format!("ORG1-{}-{:05}", year, seq)
131    }
132
133    fn generate_netsuite_style(&self) -> String {
134        let seq = self.next_sequence(EnhancedReferenceFormat::NetSuiteStyle, 0);
135        format!("INV{:05}", seq)
136    }
137
138    fn generate_alphanumeric(&self, rng: &mut impl Rng) -> String {
139        let letters: String = (0..3)
140            .map(|_| (b'A' + rng.gen_range(0..26)) as char)
141            .collect();
142        let numbers = rng.gen_range(100000..999999);
143        let check = (b'A' + rng.gen_range(0..26)) as char;
144        format!("{}{:06}{}", letters, numbers, check)
145    }
146
147    fn generate_short_uuid(&self, rng: &mut impl Rng) -> String {
148        let chars: String = (0..8)
149            .map(|_| {
150                let idx = rng.gen_range(0..36);
151                if idx < 10 {
152                    (b'0' + idx) as char
153                } else {
154                    (b'A' + idx - 10) as char
155                }
156            })
157            .collect();
158        chars
159    }
160
161    fn generate_date_based(&self, year: i32, rng: &mut impl Rng) -> String {
162        let month = rng.gen_range(1..=12);
163        let day = rng.gen_range(1..=28);
164        let seq = rng.gen_range(1..=9999);
165        format!("{}{:02}{:02}-{:04}", year, month, day, seq)
166    }
167
168    fn generate_vendor_invoice(&self, rng: &mut impl Rng) -> String {
169        let style = rng.gen_range(0..8);
170        match style {
171            0 => {
172                // INV-NNNNNNNN
173                format!("INV-{:08}", rng.gen_range(10000000..99999999))
174            }
175            1 => {
176                // Pure numbers
177                format!("{:010}", rng.gen_range(1000000000u64..9999999999))
178            }
179            2 => {
180                // V-NNN-NNNNNN
181                format!(
182                    "V{:03}-{:06}",
183                    rng.gen_range(100..999),
184                    rng.gen_range(100000..999999)
185                )
186            }
187            3 => {
188                // Letter + numbers
189                let letter = (b'A' + rng.gen_range(0..26)) as char;
190                format!("{}{:07}", letter, rng.gen_range(1000000..9999999))
191            }
192            4 => {
193                // YYYY-NNNNNN
194                let year = rng.gen_range(2020..=2025);
195                format!("{}-{:06}", year, rng.gen_range(1..999999))
196            }
197            5 => {
198                // PO-based
199                format!("PO{:08}", rng.gen_range(10000000..99999999))
200            }
201            6 => {
202                // Short alphanumeric
203                let alpha: String = (0..2)
204                    .map(|_| (b'A' + rng.gen_range(0..26)) as char)
205                    .collect();
206                format!("{}{:06}", alpha, rng.gen_range(100000..999999))
207            }
208            _ => {
209                // UUID-like
210                format!(
211                    "{:04X}-{:04X}",
212                    rng.gen_range(0..0xFFFF),
213                    rng.gen_range(0..0xFFFF)
214                )
215            }
216        }
217    }
218
219    fn generate_bank_reference(&self, year: i32, rng: &mut impl Rng) -> String {
220        let month = rng.gen_range(1..=12);
221        let day = rng.gen_range(1..=28);
222        let seq = rng.gen_range(1..=999999);
223        format!("BNK{}{:02}{:02}{:06}", year, month, day, seq)
224    }
225
226    fn generate_check_number(&self) -> String {
227        let num = self.check_counter.fetch_add(1, Ordering::Relaxed);
228        format!("{:06}", num)
229    }
230
231    fn next_sequence(&self, format: EnhancedReferenceFormat, year: i32) -> u64 {
232        let mut counters = self.counters.lock().unwrap();
233        let counter = counters
234            .entry((format, year))
235            .or_insert_with(|| AtomicU64::new(1));
236        counter.fetch_add(1, Ordering::Relaxed)
237    }
238
239    /// Reset all counters (useful for testing).
240    pub fn reset(&self) {
241        self.counters.lock().unwrap().clear();
242        self.sap_counter.store(4500000001, Ordering::Relaxed);
243        self.check_counter.store(100001, Ordering::Relaxed);
244    }
245}
246
247/// Document types for reference generation.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub enum DocumentType {
250    Invoice,
251    PurchaseOrder,
252    SalesOrder,
253    GoodsReceipt,
254    Payment,
255    JournalEntry,
256    CreditMemo,
257    DebitMemo,
258    Delivery,
259    Return,
260    Adjustment,
261    Transfer,
262}
263
264impl DocumentType {
265    /// Get the standard prefix for this document type.
266    pub fn prefix(&self) -> &'static str {
267        match self {
268            DocumentType::Invoice => "INV",
269            DocumentType::PurchaseOrder => "PO",
270            DocumentType::SalesOrder => "SO",
271            DocumentType::GoodsReceipt => "GR",
272            DocumentType::Payment => "PMT",
273            DocumentType::JournalEntry => "JE",
274            DocumentType::CreditMemo => "CM",
275            DocumentType::DebitMemo => "DM",
276            DocumentType::Delivery => "DL",
277            DocumentType::Return => "RET",
278            DocumentType::Adjustment => "ADJ",
279            DocumentType::Transfer => "TRF",
280        }
281    }
282
283    /// Get an alternative SAP-style document type code.
284    pub fn sap_code(&self) -> &'static str {
285        match self {
286            DocumentType::Invoice => "RE",
287            DocumentType::PurchaseOrder => "NB",
288            DocumentType::SalesOrder => "TA",
289            DocumentType::GoodsReceipt => "WE",
290            DocumentType::Payment => "ZP",
291            DocumentType::JournalEntry => "SA",
292            DocumentType::CreditMemo => "KR",
293            DocumentType::DebitMemo => "DR",
294            DocumentType::Delivery => "LF",
295            DocumentType::Return => "AF",
296            DocumentType::Adjustment => "AB",
297            DocumentType::Transfer => "UE",
298        }
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use rand::SeedableRng;
306    use rand_chacha::ChaCha8Rng;
307
308    #[test]
309    fn test_standard_format() {
310        let gen = EnhancedReferenceGenerator::new();
311        let mut rng = ChaCha8Rng::seed_from_u64(42);
312
313        let ref1 = gen.generate(EnhancedReferenceFormat::Standard, 2024, &mut rng);
314        assert!(ref1.starts_with("DOC-2024-"));
315        assert!(ref1.len() == 15);
316
317        let ref2 = gen.generate(EnhancedReferenceFormat::Standard, 2024, &mut rng);
318        assert_ne!(ref1, ref2); // Sequential
319    }
320
321    #[test]
322    fn test_sap_style_format() {
323        let gen = EnhancedReferenceGenerator::new();
324        let mut rng = ChaCha8Rng::seed_from_u64(42);
325
326        let ref1 = gen.generate(EnhancedReferenceFormat::SapStyle, 2024, &mut rng);
327        assert!(ref1.len() == 10);
328        assert!(ref1.starts_with("4500"));
329
330        let ref2 = gen.generate(EnhancedReferenceFormat::SapStyle, 2024, &mut rng);
331        assert_ne!(ref1, ref2);
332    }
333
334    #[test]
335    fn test_oracle_style_format() {
336        let gen = EnhancedReferenceGenerator::new();
337        let mut rng = ChaCha8Rng::seed_from_u64(42);
338
339        let ref1 = gen.generate(EnhancedReferenceFormat::OracleStyle, 2024, &mut rng);
340        assert!(ref1.starts_with("ORG1-2024-"));
341    }
342
343    #[test]
344    fn test_alphanumeric_format() {
345        let gen = EnhancedReferenceGenerator::new();
346        let mut rng = ChaCha8Rng::seed_from_u64(42);
347
348        let ref1 = gen.generate(EnhancedReferenceFormat::Alphanumeric, 2024, &mut rng);
349        assert!(ref1.len() == 10);
350        assert!(ref1.chars().take(3).all(|c| c.is_ascii_uppercase()));
351        assert!(ref1.chars().last().unwrap().is_ascii_uppercase());
352    }
353
354    #[test]
355    fn test_vendor_invoice_variety() {
356        let gen = EnhancedReferenceGenerator::new();
357        let mut rng = ChaCha8Rng::seed_from_u64(42);
358
359        let mut formats = std::collections::HashSet::new();
360        for _ in 0..100 {
361            let ref1 = gen.generate(EnhancedReferenceFormat::VendorInvoice, 2024, &mut rng);
362            // Check first 3 chars pattern
363            let pattern: String = ref1.chars().take(3).collect();
364            formats.insert(pattern);
365        }
366
367        // Should have variety in formats
368        assert!(formats.len() > 3);
369    }
370
371    #[test]
372    fn test_document_type_generation() {
373        let gen = EnhancedReferenceGenerator::new();
374        let mut rng = ChaCha8Rng::seed_from_u64(42);
375
376        let inv = gen.generate_for_document(DocumentType::Invoice, 2024, &mut rng);
377        assert!(inv.starts_with("INV-2024-"));
378
379        let po = gen.generate_for_document(DocumentType::PurchaseOrder, 2024, &mut rng);
380        assert!(po.starts_with("PO-2024-"));
381
382        let je = gen.generate_for_document(DocumentType::JournalEntry, 2024, &mut rng);
383        assert!(je.starts_with("JE-2024-"));
384    }
385
386    #[test]
387    fn test_check_number_sequential() {
388        let gen = EnhancedReferenceGenerator::new();
389        let mut rng = ChaCha8Rng::seed_from_u64(42);
390
391        let check1 = gen.generate(EnhancedReferenceFormat::CheckNumber, 2024, &mut rng);
392        let check2 = gen.generate(EnhancedReferenceFormat::CheckNumber, 2024, &mut rng);
393        let check3 = gen.generate(EnhancedReferenceFormat::CheckNumber, 2024, &mut rng);
394
395        // Should be sequential
396        let num1: u64 = check1.parse().unwrap();
397        let num2: u64 = check2.parse().unwrap();
398        let num3: u64 = check3.parse().unwrap();
399
400        assert_eq!(num2, num1 + 1);
401        assert_eq!(num3, num2 + 1);
402    }
403
404    #[test]
405    fn test_document_type_prefixes() {
406        assert_eq!(DocumentType::Invoice.prefix(), "INV");
407        assert_eq!(DocumentType::PurchaseOrder.prefix(), "PO");
408        assert_eq!(DocumentType::JournalEntry.prefix(), "JE");
409        assert_eq!(DocumentType::Payment.prefix(), "PMT");
410    }
411
412    #[test]
413    fn test_sap_codes() {
414        assert_eq!(DocumentType::Invoice.sap_code(), "RE");
415        assert_eq!(DocumentType::PurchaseOrder.sap_code(), "NB");
416        assert_eq!(DocumentType::GoodsReceipt.sap_code(), "WE");
417    }
418
419    #[test]
420    fn test_reset_counters() {
421        let gen = EnhancedReferenceGenerator::new();
422        let mut rng = ChaCha8Rng::seed_from_u64(42);
423
424        let ref1 = gen.generate(EnhancedReferenceFormat::Standard, 2024, &mut rng);
425        gen.reset();
426        let mut rng2 = ChaCha8Rng::seed_from_u64(42);
427        let ref2 = gen.generate(EnhancedReferenceFormat::Standard, 2024, &mut rng2);
428
429        assert_eq!(ref1, ref2);
430    }
431}