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::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 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| 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#[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 pub fn new() -> Self {
333 Self::default()
334 }
335
336 pub fn year(mut self, year: i32) -> Self {
338 self.year = Some(year);
339 self
340 }
341
342 pub fn company_code(mut self, code: &str) -> Self {
344 self.company_code = Some(code.to_string());
345 self
346 }
347
348 pub fn invoice_prefix(mut self, prefix: &str) -> Self {
350 self.invoice_prefix = Some(prefix.to_string());
351 self
352 }
353
354 pub fn po_prefix(mut self, prefix: &str) -> Self {
356 self.po_prefix = Some(prefix.to_string());
357 self
358 }
359
360 pub fn so_prefix(mut self, prefix: &str) -> Self {
362 self.so_prefix = Some(prefix.to_string());
363 self
364 }
365
366 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 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 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-")); assert!(p2p_ref.starts_with("PO-")); }
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}