Skip to main content

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