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(|i| {
255 let idx = ((seq as usize)
256 .wrapping_mul(7)
257 .wrapping_add(i)
258 .wrapping_mul(13)
259 .wrapping_add(17))
260 % 36;
261 if idx < 10 {
262 (b'0' + idx as u8) as char
263 } else {
264 (b'A' + idx as u8 - 10) as char
265 }
266 })
267 .collect();
268 format!("{}-{}", config.prefix, suffix)
269 }
270 ReferenceFormat::CompanyYearPrefixed => {
271 format!(
272 "{}-{}-{}-{:0width$}",
273 config.prefix,
274 self.company_code,
275 year,
276 seq,
277 width = config.sequence_digits
278 )
279 }
280 }
281 }
282
283 pub fn generate_for_process(&mut self, process: BusinessProcess) -> String {
285 let ref_type = ReferenceType::for_business_process(process);
286 self.generate(ref_type)
287 }
288
289 pub fn generate_for_process_year(&mut self, process: BusinessProcess, year: i32) -> String {
291 let ref_type = ReferenceType::for_business_process(process);
292 self.generate_for_year(ref_type, year)
293 }
294
295 pub fn generate_external_reference(&self, rng: &mut impl Rng) -> String {
297 let formats = [
299 |rng: &mut dyn rand::RngCore| {
301 format!("INV{:08}", rng.random_range(10000000u64..99999999))
302 },
303 |rng: &mut dyn rand::RngCore| {
304 format!("{:010}", rng.random_range(1000000000u64..9999999999))
305 },
306 |rng: &mut dyn rand::RngCore| {
307 format!(
308 "V{}-{:06}",
309 rng.random_range(100..999),
310 rng.random_range(1..999999)
311 )
312 },
313 |rng: &mut dyn rand::RngCore| {
314 format!(
315 "{}{:07}",
316 (b'A' + rng.random_range(0..26)) as char,
317 rng.random_range(1000000..9999999)
318 )
319 },
320 ];
321
322 let idx = rng.random_range(0..formats.len());
323 formats[idx](rng)
324 }
325}
326
327#[derive(Debug, Clone, Default)]
329pub struct ReferenceGeneratorBuilder {
330 year: Option<i32>,
331 company_code: Option<String>,
332 invoice_prefix: Option<String>,
333 po_prefix: Option<String>,
334 so_prefix: Option<String>,
335}
336
337impl ReferenceGeneratorBuilder {
338 pub fn new() -> Self {
340 Self::default()
341 }
342
343 pub fn year(mut self, year: i32) -> Self {
345 self.year = Some(year);
346 self
347 }
348
349 pub fn company_code(mut self, code: &str) -> Self {
351 self.company_code = Some(code.to_string());
352 self
353 }
354
355 pub fn invoice_prefix(mut self, prefix: &str) -> Self {
357 self.invoice_prefix = Some(prefix.to_string());
358 self
359 }
360
361 pub fn po_prefix(mut self, prefix: &str) -> Self {
363 self.po_prefix = Some(prefix.to_string());
364 self
365 }
366
367 pub fn so_prefix(mut self, prefix: &str) -> Self {
369 self.so_prefix = Some(prefix.to_string());
370 self
371 }
372
373 pub fn build(self) -> ReferenceGenerator {
375 let year = self.year.unwrap_or(2024);
376 let company = self.company_code.as_deref().unwrap_or("1000");
377
378 let mut gen = ReferenceGenerator::new(year, company);
379
380 if let Some(prefix) = self.invoice_prefix {
381 gen.set_prefix(ReferenceType::Invoice, &prefix);
382 }
383 if let Some(prefix) = self.po_prefix {
384 gen.set_prefix(ReferenceType::PurchaseOrder, &prefix);
385 }
386 if let Some(prefix) = self.so_prefix {
387 gen.set_prefix(ReferenceType::SalesOrder, &prefix);
388 }
389
390 gen
391 }
392}
393
394#[cfg(test)]
395#[allow(clippy::unwrap_used)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_sequential_generation() {
401 let mut gen = ReferenceGenerator::new(2024, "1000");
402
403 let ref1 = gen.generate(ReferenceType::Invoice);
404 let ref2 = gen.generate(ReferenceType::Invoice);
405 let ref3 = gen.generate(ReferenceType::Invoice);
406
407 assert!(ref1.starts_with("INV-2024-"));
408 assert!(ref2.starts_with("INV-2024-"));
409 assert!(ref3.starts_with("INV-2024-"));
410
411 assert_ne!(ref1, ref2);
413 assert_ne!(ref2, ref3);
414 }
415
416 #[test]
417 fn test_different_types() {
418 let mut gen = ReferenceGenerator::new(2024, "1000");
419
420 let inv = gen.generate(ReferenceType::Invoice);
421 let po = gen.generate(ReferenceType::PurchaseOrder);
422 let so = gen.generate(ReferenceType::SalesOrder);
423
424 assert!(inv.starts_with("INV-"));
425 assert!(po.starts_with("PO-"));
426 assert!(so.starts_with("SO-"));
427 }
428
429 #[test]
430 fn test_year_based_counters() {
431 let mut gen = ReferenceGenerator::new(2024, "1000");
432
433 let ref_2024 = gen.generate_for_year(ReferenceType::Invoice, 2024);
434 let ref_2025 = gen.generate_for_year(ReferenceType::Invoice, 2025);
435
436 assert!(ref_2024.contains("2024"));
437 assert!(ref_2025.contains("2025"));
438
439 assert!(ref_2024.ends_with("000001"));
441 assert!(ref_2025.ends_with("000001"));
442 }
443
444 #[test]
445 fn test_business_process_mapping() {
446 let mut gen = ReferenceGenerator::new(2024, "1000");
447
448 let o2c_ref = gen.generate_for_process(BusinessProcess::O2C);
449 let p2p_ref = gen.generate_for_process(BusinessProcess::P2P);
450
451 assert!(o2c_ref.starts_with("SO-")); assert!(p2p_ref.starts_with("PO-")); }
454
455 #[test]
456 fn test_custom_prefix() {
457 let mut gen = ReferenceGenerator::new(2024, "ACME");
458 gen.set_prefix(ReferenceType::Invoice, "ACME-INV");
459
460 let inv = gen.generate(ReferenceType::Invoice);
461 assert!(inv.starts_with("ACME-INV-"));
462 }
463
464 #[test]
465 fn test_builder() {
466 let mut gen = ReferenceGeneratorBuilder::new()
467 .year(2025)
468 .company_code("CORP")
469 .invoice_prefix("CORP-INV")
470 .build();
471
472 let inv = gen.generate(ReferenceType::Invoice);
473 assert!(inv.starts_with("CORP-INV-2025-"));
474 }
475
476 #[test]
477 fn test_external_reference() {
478 use rand::SeedableRng;
479 use rand_chacha::ChaCha8Rng;
480
481 let gen = ReferenceGenerator::new(2024, "1000");
482 let mut rng = ChaCha8Rng::seed_from_u64(42);
483
484 let ext_ref = gen.generate_external_reference(&mut rng);
485 assert!(!ext_ref.is_empty());
486 }
487}