1use crate::models::BusinessProcess;
7use rand::Rng;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ReferenceType {
16 Invoice,
18 PurchaseOrder,
20 SalesOrder,
22 GoodsReceipt,
24 PaymentReference,
26 AssetTag,
28 ProjectNumber,
30 ExpenseReport,
32 ContractNumber,
34 BatchNumber,
36 InternalDocument,
38}
39
40impl ReferenceType {
41 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub enum ReferenceFormat {
82 Sequential,
84 #[default]
86 YearPrefixed,
87 YearMonthPrefixed,
89 Random,
91 CompanyYearPrefixed,
93}
94
95#[derive(Debug, Clone)]
97pub struct ReferenceConfig {
98 pub prefix: String,
100 pub format: ReferenceFormat,
102 pub sequence_digits: usize,
104 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#[derive(Debug)]
121pub struct ReferenceGenerator {
122 configs: HashMap<ReferenceType, ReferenceConfig>,
124 counters: HashMap<(ReferenceType, Option<i32>), AtomicU64>,
126 default_year: i32,
128 company_code: String,
130}
131
132impl Default for ReferenceGenerator {
133 fn default() -> Self {
134 Self::new(2024, "1000")
135 }
136}
137
138impl ReferenceGenerator {
139 pub fn new(year: i32, company_code: &str) -> Self {
141 let mut configs = HashMap::new();
142
143 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 pub fn with_company_code(mut self, code: &str) -> Self {
178 self.company_code = code.to_string();
179 self
180 }
181
182 pub fn with_year(mut self, year: i32) -> Self {
184 self.default_year = year;
185 self
186 }
187
188 pub fn set_config(&mut self, ref_type: ReferenceType, config: ReferenceConfig) {
190 self.configs.insert(ref_type, config);
191 }
192
193 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 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 pub fn generate(&mut self, ref_type: ReferenceType) -> String {
215 self.generate_for_year(ref_type, self.default_year)
216 }
217
218 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 format!(
244 "{}-{}01-{:0width$}",
245 config.prefix,
246 year,
247 seq,
248 width = config.sequence_digits - 1
249 )
250 }
251 ReferenceFormat::Random => {
252 let suffix: String = (0..config.sequence_digits)
254 .map(|_| {
255 let idx = rand::rng().random_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 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 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 pub fn generate_external_reference(&self, rng: &mut impl Rng) -> String {
292 let formats = [
294 |rng: &mut dyn rand::RngCore| {
296 format!("INV{:08}", rng.random_range(10000000u64..99999999))
297 },
298 |rng: &mut dyn rand::RngCore| {
299 format!("{:010}", rng.random_range(1000000000u64..9999999999))
300 },
301 |rng: &mut dyn rand::RngCore| {
302 format!(
303 "V{}-{:06}",
304 rng.random_range(100..999),
305 rng.random_range(1..999999)
306 )
307 },
308 |rng: &mut dyn rand::RngCore| {
309 format!(
310 "{}{:07}",
311 (b'A' + rng.random_range(0..26)) as char,
312 rng.random_range(1000000..9999999)
313 )
314 },
315 ];
316
317 let idx = rng.random_range(0..formats.len());
318 formats[idx](rng)
319 }
320}
321
322#[derive(Debug, Clone, Default)]
324pub struct ReferenceGeneratorBuilder {
325 year: Option<i32>,
326 company_code: Option<String>,
327 invoice_prefix: Option<String>,
328 po_prefix: Option<String>,
329 so_prefix: Option<String>,
330}
331
332impl ReferenceGeneratorBuilder {
333 pub fn new() -> Self {
335 Self::default()
336 }
337
338 pub fn year(mut self, year: i32) -> Self {
340 self.year = Some(year);
341 self
342 }
343
344 pub fn company_code(mut self, code: &str) -> Self {
346 self.company_code = Some(code.to_string());
347 self
348 }
349
350 pub fn invoice_prefix(mut self, prefix: &str) -> Self {
352 self.invoice_prefix = Some(prefix.to_string());
353 self
354 }
355
356 pub fn po_prefix(mut self, prefix: &str) -> Self {
358 self.po_prefix = Some(prefix.to_string());
359 self
360 }
361
362 pub fn so_prefix(mut self, prefix: &str) -> Self {
364 self.so_prefix = Some(prefix.to_string());
365 self
366 }
367
368 pub fn build(self) -> ReferenceGenerator {
370 let year = self.year.unwrap_or(2024);
371 let company = self.company_code.as_deref().unwrap_or("1000");
372
373 let mut gen = ReferenceGenerator::new(year, company);
374
375 if let Some(prefix) = self.invoice_prefix {
376 gen.set_prefix(ReferenceType::Invoice, &prefix);
377 }
378 if let Some(prefix) = self.po_prefix {
379 gen.set_prefix(ReferenceType::PurchaseOrder, &prefix);
380 }
381 if let Some(prefix) = self.so_prefix {
382 gen.set_prefix(ReferenceType::SalesOrder, &prefix);
383 }
384
385 gen
386 }
387}
388
389#[cfg(test)]
390#[allow(clippy::unwrap_used)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_sequential_generation() {
396 let mut gen = ReferenceGenerator::new(2024, "1000");
397
398 let ref1 = gen.generate(ReferenceType::Invoice);
399 let ref2 = gen.generate(ReferenceType::Invoice);
400 let ref3 = gen.generate(ReferenceType::Invoice);
401
402 assert!(ref1.starts_with("INV-2024-"));
403 assert!(ref2.starts_with("INV-2024-"));
404 assert!(ref3.starts_with("INV-2024-"));
405
406 assert_ne!(ref1, ref2);
408 assert_ne!(ref2, ref3);
409 }
410
411 #[test]
412 fn test_different_types() {
413 let mut gen = ReferenceGenerator::new(2024, "1000");
414
415 let inv = gen.generate(ReferenceType::Invoice);
416 let po = gen.generate(ReferenceType::PurchaseOrder);
417 let so = gen.generate(ReferenceType::SalesOrder);
418
419 assert!(inv.starts_with("INV-"));
420 assert!(po.starts_with("PO-"));
421 assert!(so.starts_with("SO-"));
422 }
423
424 #[test]
425 fn test_year_based_counters() {
426 let mut gen = ReferenceGenerator::new(2024, "1000");
427
428 let ref_2024 = gen.generate_for_year(ReferenceType::Invoice, 2024);
429 let ref_2025 = gen.generate_for_year(ReferenceType::Invoice, 2025);
430
431 assert!(ref_2024.contains("2024"));
432 assert!(ref_2025.contains("2025"));
433
434 assert!(ref_2024.ends_with("000001"));
436 assert!(ref_2025.ends_with("000001"));
437 }
438
439 #[test]
440 fn test_business_process_mapping() {
441 let mut gen = ReferenceGenerator::new(2024, "1000");
442
443 let o2c_ref = gen.generate_for_process(BusinessProcess::O2C);
444 let p2p_ref = gen.generate_for_process(BusinessProcess::P2P);
445
446 assert!(o2c_ref.starts_with("SO-")); assert!(p2p_ref.starts_with("PO-")); }
449
450 #[test]
451 fn test_custom_prefix() {
452 let mut gen = ReferenceGenerator::new(2024, "ACME");
453 gen.set_prefix(ReferenceType::Invoice, "ACME-INV");
454
455 let inv = gen.generate(ReferenceType::Invoice);
456 assert!(inv.starts_with("ACME-INV-"));
457 }
458
459 #[test]
460 fn test_builder() {
461 let mut gen = ReferenceGeneratorBuilder::new()
462 .year(2025)
463 .company_code("CORP")
464 .invoice_prefix("CORP-INV")
465 .build();
466
467 let inv = gen.generate(ReferenceType::Invoice);
468 assert!(inv.starts_with("CORP-INV-2025-"));
469 }
470
471 #[test]
472 fn test_external_reference() {
473 use rand::SeedableRng;
474 use rand_chacha::ChaCha8Rng;
475
476 let gen = ReferenceGenerator::new(2024, "1000");
477 let mut rng = ChaCha8Rng::seed_from_u64(42);
478
479 let ext_ref = gen.generate_external_reference(&mut rng);
480 assert!(!ext_ref.is_empty());
481 }
482}