datasynth_core/templates/
references.rs

1//! Reference number generation for journal entries.
2//!
3//! Generates realistic document reference numbers like invoice numbers,
4//! purchase orders, sales orders, etc.
5
6use crate::models::BusinessProcess;
7use rand::Rng;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12/// Types of reference numbers.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ReferenceType {
16    /// Invoice number (vendor or customer)
17    Invoice,
18    /// Purchase order number
19    PurchaseOrder,
20    /// Sales order number
21    SalesOrder,
22    /// Goods receipt number
23    GoodsReceipt,
24    /// Payment reference
25    PaymentReference,
26    /// Asset tag number
27    AssetTag,
28    /// Project number
29    ProjectNumber,
30    /// Expense report number
31    ExpenseReport,
32    /// Contract number
33    ContractNumber,
34    /// Batch number
35    BatchNumber,
36    /// Internal document number
37    InternalDocument,
38}
39
40impl ReferenceType {
41    /// Get the default prefix for this reference type.
42    pub fn default_prefix(&self) -> &'static str {
43        match self {
44            Self::Invoice => "INV",
45            Self::PurchaseOrder => "PO",
46            Self::SalesOrder => "SO",
47            Self::GoodsReceipt => "GR",
48            Self::PaymentReference => "PAY",
49            Self::AssetTag => "FA",
50            Self::ProjectNumber => "PRJ",
51            Self::ExpenseReport => "EXP",
52            Self::ContractNumber => "CTR",
53            Self::BatchNumber => "BATCH",
54            Self::InternalDocument => "DOC",
55        }
56    }
57
58    /// Get the typical reference type for a business process.
59    pub fn for_business_process(process: BusinessProcess) -> Self {
60        match process {
61            BusinessProcess::O2C => Self::SalesOrder,
62            BusinessProcess::P2P => Self::PurchaseOrder,
63            BusinessProcess::R2R => Self::InternalDocument,
64            BusinessProcess::H2R => Self::ExpenseReport,
65            BusinessProcess::A2R => Self::AssetTag,
66            BusinessProcess::Treasury => Self::PaymentReference,
67            BusinessProcess::Tax => Self::InternalDocument,
68            BusinessProcess::Intercompany => Self::InternalDocument,
69        }
70    }
71}
72
73/// Format for reference numbers.
74#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub enum ReferenceFormat {
76    /// Simple sequential: PREFIX-000001
77    Sequential,
78    /// Year-prefixed: PREFIX-YYYY-000001
79    #[default]
80    YearPrefixed,
81    /// Year-month: PREFIX-YYYYMM-00001
82    YearMonthPrefixed,
83    /// Random alphanumeric: PREFIX-XXXXXX
84    Random,
85    /// Company-year: PREFIX-COMP-YYYY-00001
86    CompanyYearPrefixed,
87}
88
89/// Configuration for a reference type.
90#[derive(Debug, Clone)]
91pub struct ReferenceConfig {
92    /// Prefix for the reference
93    pub prefix: String,
94    /// Format to use
95    pub format: ReferenceFormat,
96    /// Number of digits in the sequence
97    pub sequence_digits: usize,
98    /// Starting sequence number
99    pub start_sequence: u64,
100}
101
102impl Default for ReferenceConfig {
103    fn default() -> Self {
104        Self {
105            prefix: "REF".to_string(),
106            format: ReferenceFormat::YearPrefixed,
107            sequence_digits: 6,
108            start_sequence: 1,
109        }
110    }
111}
112
113/// Generator for reference numbers.
114#[derive(Debug)]
115pub struct ReferenceGenerator {
116    /// Configuration by reference type
117    configs: HashMap<ReferenceType, ReferenceConfig>,
118    /// Counters by reference type and year
119    counters: HashMap<(ReferenceType, Option<i32>), AtomicU64>,
120    /// Default year for generation
121    default_year: i32,
122    /// Company code for company-prefixed formats
123    company_code: String,
124}
125
126impl Default for ReferenceGenerator {
127    fn default() -> Self {
128        Self::new(2024, "1000")
129    }
130}
131
132impl ReferenceGenerator {
133    /// Create a new reference generator.
134    pub fn new(year: i32, company_code: &str) -> Self {
135        let mut configs = HashMap::new();
136
137        // Set up default configurations for each type
138        for ref_type in [
139            ReferenceType::Invoice,
140            ReferenceType::PurchaseOrder,
141            ReferenceType::SalesOrder,
142            ReferenceType::GoodsReceipt,
143            ReferenceType::PaymentReference,
144            ReferenceType::AssetTag,
145            ReferenceType::ProjectNumber,
146            ReferenceType::ExpenseReport,
147            ReferenceType::ContractNumber,
148            ReferenceType::BatchNumber,
149            ReferenceType::InternalDocument,
150        ] {
151            configs.insert(
152                ref_type,
153                ReferenceConfig {
154                    prefix: ref_type.default_prefix().to_string(),
155                    format: ReferenceFormat::YearPrefixed,
156                    sequence_digits: 6,
157                    start_sequence: 1,
158                },
159            );
160        }
161
162        Self {
163            configs,
164            counters: HashMap::new(),
165            default_year: year,
166            company_code: company_code.to_string(),
167        }
168    }
169
170    /// Set the company code.
171    pub fn with_company_code(mut self, code: &str) -> Self {
172        self.company_code = code.to_string();
173        self
174    }
175
176    /// Set the default year.
177    pub fn with_year(mut self, year: i32) -> Self {
178        self.default_year = year;
179        self
180    }
181
182    /// Set configuration for a reference type.
183    pub fn set_config(&mut self, ref_type: ReferenceType, config: ReferenceConfig) {
184        self.configs.insert(ref_type, config);
185    }
186
187    /// Set a custom prefix for a reference type.
188    pub fn set_prefix(&mut self, ref_type: ReferenceType, prefix: &str) {
189        if let Some(config) = self.configs.get_mut(&ref_type) {
190            config.prefix = prefix.to_string();
191        }
192    }
193
194    /// Get the next sequence number for a reference type and optional year.
195    fn next_sequence(&mut self, ref_type: ReferenceType, year: Option<i32>) -> u64 {
196        let key = (ref_type, year);
197        let config = self.configs.get(&ref_type).cloned().unwrap_or_default();
198
199        let counter = self
200            .counters
201            .entry(key)
202            .or_insert_with(|| AtomicU64::new(config.start_sequence));
203
204        counter.fetch_add(1, Ordering::SeqCst)
205    }
206
207    /// Generate a reference number.
208    pub fn generate(&mut self, ref_type: ReferenceType) -> String {
209        self.generate_for_year(ref_type, self.default_year)
210    }
211
212    /// Generate a reference number for a specific year.
213    pub fn generate_for_year(&mut self, ref_type: ReferenceType, year: i32) -> String {
214        let config = self.configs.get(&ref_type).cloned().unwrap_or_default();
215        let seq = self.next_sequence(ref_type, Some(year));
216
217        match config.format {
218            ReferenceFormat::Sequential => {
219                format!(
220                    "{}-{:0width$}",
221                    config.prefix,
222                    seq,
223                    width = config.sequence_digits
224                )
225            }
226            ReferenceFormat::YearPrefixed => {
227                format!(
228                    "{}-{}-{:0width$}",
229                    config.prefix,
230                    year,
231                    seq,
232                    width = config.sequence_digits
233                )
234            }
235            ReferenceFormat::YearMonthPrefixed => {
236                // Use a default month; in practice, pass month as parameter
237                format!(
238                    "{}-{}01-{:0width$}",
239                    config.prefix,
240                    year,
241                    seq,
242                    width = config.sequence_digits - 1
243                )
244            }
245            ReferenceFormat::Random => {
246                // Generate random alphanumeric suffix
247                let suffix: String = (0..config.sequence_digits)
248                    .map(|_| {
249                        let idx = rand::thread_rng().gen_range(0..36);
250                        if idx < 10 {
251                            (b'0' + idx) as char
252                        } else {
253                            (b'A' + idx - 10) as char
254                        }
255                    })
256                    .collect();
257                format!("{}-{}", config.prefix, suffix)
258            }
259            ReferenceFormat::CompanyYearPrefixed => {
260                format!(
261                    "{}-{}-{}-{:0width$}",
262                    config.prefix,
263                    self.company_code,
264                    year,
265                    seq,
266                    width = config.sequence_digits
267                )
268            }
269        }
270    }
271
272    /// Generate a reference for a business process.
273    pub fn generate_for_process(&mut self, process: BusinessProcess) -> String {
274        let ref_type = ReferenceType::for_business_process(process);
275        self.generate(ref_type)
276    }
277
278    /// Generate a reference for a business process and year.
279    pub fn generate_for_process_year(&mut self, process: BusinessProcess, year: i32) -> String {
280        let ref_type = ReferenceType::for_business_process(process);
281        self.generate_for_year(ref_type, year)
282    }
283
284    /// Generate an external reference (vendor invoice, etc.) with random elements.
285    pub fn generate_external_reference(&self, rng: &mut impl Rng) -> String {
286        // External references often have different formats
287        let formats = [
288            // Vendor invoice formats
289            |rng: &mut dyn rand::RngCore| format!("INV{:08}", rng.gen_range(10000000u64..99999999)),
290            |rng: &mut dyn rand::RngCore| {
291                format!("{:010}", rng.gen_range(1000000000u64..9999999999))
292            },
293            |rng: &mut dyn rand::RngCore| {
294                format!(
295                    "V{}-{:06}",
296                    rng.gen_range(100..999),
297                    rng.gen_range(1..999999)
298                )
299            },
300            |rng: &mut dyn rand::RngCore| {
301                format!(
302                    "{}{:07}",
303                    (b'A' + rng.gen_range(0..26)) as char,
304                    rng.gen_range(1000000..9999999)
305                )
306            },
307        ];
308
309        let idx = rng.gen_range(0..formats.len());
310        formats[idx](rng)
311    }
312}
313
314/// Builder for configuring reference generation.
315#[derive(Debug, Clone, Default)]
316pub struct ReferenceGeneratorBuilder {
317    year: Option<i32>,
318    company_code: Option<String>,
319    invoice_prefix: Option<String>,
320    po_prefix: Option<String>,
321    so_prefix: Option<String>,
322}
323
324impl ReferenceGeneratorBuilder {
325    /// Create a new builder.
326    pub fn new() -> Self {
327        Self::default()
328    }
329
330    /// Set the year.
331    pub fn year(mut self, year: i32) -> Self {
332        self.year = Some(year);
333        self
334    }
335
336    /// Set the company code.
337    pub fn company_code(mut self, code: &str) -> Self {
338        self.company_code = Some(code.to_string());
339        self
340    }
341
342    /// Set invoice prefix.
343    pub fn invoice_prefix(mut self, prefix: &str) -> Self {
344        self.invoice_prefix = Some(prefix.to_string());
345        self
346    }
347
348    /// Set PO prefix.
349    pub fn po_prefix(mut self, prefix: &str) -> Self {
350        self.po_prefix = Some(prefix.to_string());
351        self
352    }
353
354    /// Set SO prefix.
355    pub fn so_prefix(mut self, prefix: &str) -> Self {
356        self.so_prefix = Some(prefix.to_string());
357        self
358    }
359
360    /// Build the generator.
361    pub fn build(self) -> ReferenceGenerator {
362        let year = self.year.unwrap_or(2024);
363        let company = self.company_code.as_deref().unwrap_or("1000");
364
365        let mut gen = ReferenceGenerator::new(year, company);
366
367        if let Some(prefix) = self.invoice_prefix {
368            gen.set_prefix(ReferenceType::Invoice, &prefix);
369        }
370        if let Some(prefix) = self.po_prefix {
371            gen.set_prefix(ReferenceType::PurchaseOrder, &prefix);
372        }
373        if let Some(prefix) = self.so_prefix {
374            gen.set_prefix(ReferenceType::SalesOrder, &prefix);
375        }
376
377        gen
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_sequential_generation() {
387        let mut gen = ReferenceGenerator::new(2024, "1000");
388
389        let ref1 = gen.generate(ReferenceType::Invoice);
390        let ref2 = gen.generate(ReferenceType::Invoice);
391        let ref3 = gen.generate(ReferenceType::Invoice);
392
393        assert!(ref1.starts_with("INV-2024-"));
394        assert!(ref2.starts_with("INV-2024-"));
395        assert!(ref3.starts_with("INV-2024-"));
396
397        // Should be sequential
398        assert_ne!(ref1, ref2);
399        assert_ne!(ref2, ref3);
400    }
401
402    #[test]
403    fn test_different_types() {
404        let mut gen = ReferenceGenerator::new(2024, "1000");
405
406        let inv = gen.generate(ReferenceType::Invoice);
407        let po = gen.generate(ReferenceType::PurchaseOrder);
408        let so = gen.generate(ReferenceType::SalesOrder);
409
410        assert!(inv.starts_with("INV-"));
411        assert!(po.starts_with("PO-"));
412        assert!(so.starts_with("SO-"));
413    }
414
415    #[test]
416    fn test_year_based_counters() {
417        let mut gen = ReferenceGenerator::new(2024, "1000");
418
419        let ref_2024 = gen.generate_for_year(ReferenceType::Invoice, 2024);
420        let ref_2025 = gen.generate_for_year(ReferenceType::Invoice, 2025);
421
422        assert!(ref_2024.contains("2024"));
423        assert!(ref_2025.contains("2025"));
424
425        // Different years should have independent counters
426        assert!(ref_2024.ends_with("000001"));
427        assert!(ref_2025.ends_with("000001"));
428    }
429
430    #[test]
431    fn test_business_process_mapping() {
432        let mut gen = ReferenceGenerator::new(2024, "1000");
433
434        let o2c_ref = gen.generate_for_process(BusinessProcess::O2C);
435        let p2p_ref = gen.generate_for_process(BusinessProcess::P2P);
436
437        assert!(o2c_ref.starts_with("SO-")); // Sales Order
438        assert!(p2p_ref.starts_with("PO-")); // Purchase Order
439    }
440
441    #[test]
442    fn test_custom_prefix() {
443        let mut gen = ReferenceGenerator::new(2024, "ACME");
444        gen.set_prefix(ReferenceType::Invoice, "ACME-INV");
445
446        let inv = gen.generate(ReferenceType::Invoice);
447        assert!(inv.starts_with("ACME-INV-"));
448    }
449
450    #[test]
451    fn test_builder() {
452        let mut gen = ReferenceGeneratorBuilder::new()
453            .year(2025)
454            .company_code("CORP")
455            .invoice_prefix("CORP-INV")
456            .build();
457
458        let inv = gen.generate(ReferenceType::Invoice);
459        assert!(inv.starts_with("CORP-INV-2025-"));
460    }
461
462    #[test]
463    fn test_external_reference() {
464        use rand::SeedableRng;
465        use rand_chacha::ChaCha8Rng;
466
467        let gen = ReferenceGenerator::new(2024, "1000");
468        let mut rng = ChaCha8Rng::seed_from_u64(42);
469
470        let ext_ref = gen.generate_external_reference(&mut rng);
471        assert!(!ext_ref.is_empty());
472    }
473}