Skip to main content

datasynth_core/templates/
descriptions.rs

1//! Description templates for journal entry header and line text.
2//!
3//! Provides realistic, business-process-specific text templates
4//! for populating header_text and line_text fields.
5
6use crate::models::BusinessProcess;
7use rand::seq::IndexedRandom;
8use rand::Rng;
9use serde::{Deserialize, Serialize};
10
11/// Pattern for header text generation.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct HeaderTextPattern {
14    /// The pattern template with placeholders
15    pub template: String,
16    /// Business process this pattern applies to
17    pub business_process: BusinessProcess,
18}
19
20/// Pattern for line text generation.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct LineTextPattern {
23    /// The pattern template
24    pub template: String,
25    /// Account type prefix this applies to (e.g., "1" for assets, "5" for expenses)
26    pub account_prefix: Option<String>,
27}
28
29/// Context for text generation with available placeholders.
30#[derive(Debug, Clone, Default)]
31pub struct DescriptionContext {
32    /// Vendor name for P2P transactions
33    pub vendor_name: Option<String>,
34    /// Customer name for O2C transactions
35    pub customer_name: Option<String>,
36    /// Invoice number reference
37    pub invoice_number: Option<String>,
38    /// PO number reference
39    pub po_number: Option<String>,
40    /// Month name (e.g., "January")
41    pub month_name: Option<String>,
42    /// Year (e.g., "2024")
43    pub year: Option<String>,
44    /// Quarter (e.g., "Q1")
45    pub quarter: Option<String>,
46    /// Asset description
47    pub asset_description: Option<String>,
48    /// Project name
49    pub project_name: Option<String>,
50    /// Department name
51    pub department_name: Option<String>,
52    /// Employee name for H2R
53    pub employee_name: Option<String>,
54    /// Amount for reference
55    pub amount: Option<String>,
56}
57
58impl DescriptionContext {
59    /// Create a context with month and year.
60    pub fn with_period(month: u32, year: i32) -> Self {
61        let month_name = match month {
62            1 => "January",
63            2 => "February",
64            3 => "March",
65            4 => "April",
66            5 => "May",
67            6 => "June",
68            7 => "July",
69            8 => "August",
70            9 => "September",
71            10 => "October",
72            11 => "November",
73            12 => "December",
74            _ => "Unknown",
75        };
76
77        let quarter = match month {
78            1..=3 => "Q1",
79            4..=6 => "Q2",
80            7..=9 => "Q3",
81            10..=12 => "Q4",
82            _ => "Q1",
83        };
84
85        Self {
86            month_name: Some(month_name.to_string()),
87            year: Some(year.to_string()),
88            quarter: Some(quarter.to_string()),
89            ..Default::default()
90        }
91    }
92}
93
94/// Generator for journal entry descriptions.
95#[derive(Debug, Clone)]
96pub struct DescriptionGenerator {
97    /// Header text patterns by business process
98    header_patterns: Vec<HeaderTextPattern>,
99    /// Expense descriptions
100    expense_descriptions: Vec<&'static str>,
101    /// Revenue descriptions
102    revenue_descriptions: Vec<&'static str>,
103    /// Asset descriptions
104    asset_descriptions: Vec<&'static str>,
105    /// Liability descriptions
106    liability_descriptions: Vec<&'static str>,
107    /// Bank/cash descriptions
108    bank_descriptions: Vec<&'static str>,
109    /// Process-specific line descriptions for P2P
110    p2p_line_descriptions: Vec<&'static str>,
111    /// Process-specific line descriptions for O2C
112    o2c_line_descriptions: Vec<&'static str>,
113    /// Process-specific line descriptions for H2R (Hire to Retire)
114    h2r_line_descriptions: Vec<&'static str>,
115    /// Process-specific line descriptions for R2R (Record to Report)
116    r2r_line_descriptions: Vec<&'static str>,
117}
118
119impl Default for DescriptionGenerator {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl DescriptionGenerator {
126    /// Create a new description generator with default templates.
127    pub fn new() -> Self {
128        Self {
129            header_patterns: Self::default_header_patterns(),
130            expense_descriptions: Self::default_expense_descriptions(),
131            revenue_descriptions: Self::default_revenue_descriptions(),
132            asset_descriptions: Self::default_asset_descriptions(),
133            liability_descriptions: Self::default_liability_descriptions(),
134            bank_descriptions: Self::default_bank_descriptions(),
135            p2p_line_descriptions: Self::default_p2p_line_descriptions(),
136            o2c_line_descriptions: Self::default_o2c_line_descriptions(),
137            h2r_line_descriptions: Self::default_h2r_line_descriptions(),
138            r2r_line_descriptions: Self::default_r2r_line_descriptions(),
139        }
140    }
141
142    fn default_p2p_line_descriptions() -> Vec<&'static str> {
143        vec![
144            "Inventory purchase",
145            "Raw materials receipt",
146            "Goods received - standard",
147            "Vendor invoice posting",
148            "AP invoice match",
149            "Purchase goods receipt",
150            "Material receipt",
151            "Components inventory",
152            "Supplies procurement",
153            "Service receipt",
154            "Purchase payment",
155            "Vendor payment",
156            "AP settlement",
157            "GR/IR clearing",
158            "Price variance adjustment",
159            "Quantity variance",
160            "Procurement expense",
161            "Freight charges",
162            "Customs duties",
163            "Import taxes",
164        ]
165    }
166
167    fn default_o2c_line_descriptions() -> Vec<&'static str> {
168        vec![
169            "Product sales",
170            "Service delivery",
171            "Customer invoice",
172            "Revenue recognition",
173            "Sales order fulfillment",
174            "Goods shipped",
175            "Delivery completion",
176            "Customer receipt",
177            "AR receipt",
178            "Cash application",
179            "Sales discount given",
180            "Trade discount",
181            "Early payment discount",
182            "Finished goods sale",
183            "Merchandise sale",
184            "Contract revenue",
185            "Subscription revenue",
186            "License fee revenue",
187            "Commission earned",
188            "COGS recognition",
189        ]
190    }
191
192    fn default_h2r_line_descriptions() -> Vec<&'static str> {
193        vec![
194            "Salary expense",
195            "Wages allocation",
196            "Benefits expense",
197            "Payroll taxes",
198            "Commission payment",
199            "Bonus accrual",
200            "Vacation accrual",
201            "Health insurance",
202            "Retirement contribution",
203            "Training expense",
204            "Recruitment costs",
205            "Relocation expense",
206            "Employee reimbursement",
207            "Contractor payment",
208            "Temporary staff",
209            "Overtime payment",
210            "Shift differential",
211            "On-call allowance",
212            "Travel reimbursement",
213            "Expense report",
214        ]
215    }
216
217    fn default_r2r_line_descriptions() -> Vec<&'static str> {
218        vec![
219            "Period close adjustment",
220            "Depreciation expense",
221            "Amortization expense",
222            "Accrual entry",
223            "Accrual reversal",
224            "Reclassification entry",
225            "Intercompany elimination",
226            "Currency translation",
227            "FX revaluation",
228            "Reserve adjustment",
229            "Provision update",
230            "Impairment charge",
231            "Bad debt provision",
232            "Inventory adjustment",
233            "Valuation adjustment",
234            "Consolidation entry",
235            "Manual adjustment",
236            "Year-end closing",
237            "Opening balance",
238            "Trial balance adjustment",
239        ]
240    }
241
242    fn default_header_patterns() -> Vec<HeaderTextPattern> {
243        vec![
244            // O2C - Order to Cash
245            HeaderTextPattern {
246                template: "Customer Invoice - {CustomerName}".to_string(),
247                business_process: BusinessProcess::O2C,
248            },
249            HeaderTextPattern {
250                template: "Sales Order Fulfillment - {CustomerName}".to_string(),
251                business_process: BusinessProcess::O2C,
252            },
253            HeaderTextPattern {
254                template: "Revenue Recognition - {Month} {Year}".to_string(),
255                business_process: BusinessProcess::O2C,
256            },
257            HeaderTextPattern {
258                template: "Customer Payment Receipt - {CustomerName}".to_string(),
259                business_process: BusinessProcess::O2C,
260            },
261            HeaderTextPattern {
262                template: "AR Collection - {InvoiceNumber}".to_string(),
263                business_process: BusinessProcess::O2C,
264            },
265            HeaderTextPattern {
266                template: "Credit Memo - {CustomerName}".to_string(),
267                business_process: BusinessProcess::O2C,
268            },
269            HeaderTextPattern {
270                template: "Deferred Revenue Release - {Month}".to_string(),
271                business_process: BusinessProcess::O2C,
272            },
273            HeaderTextPattern {
274                template: "Sales Commission Accrual - {Quarter}".to_string(),
275                business_process: BusinessProcess::O2C,
276            },
277            // P2P - Procure to Pay
278            HeaderTextPattern {
279                template: "Vendor Invoice - {VendorName}".to_string(),
280                business_process: BusinessProcess::P2P,
281            },
282            HeaderTextPattern {
283                template: "Purchase Order - {PONumber}".to_string(),
284                business_process: BusinessProcess::P2P,
285            },
286            HeaderTextPattern {
287                template: "AP Payment Run - {Month} {Year}".to_string(),
288                business_process: BusinessProcess::P2P,
289            },
290            HeaderTextPattern {
291                template: "Vendor Payment - {VendorName}".to_string(),
292                business_process: BusinessProcess::P2P,
293            },
294            HeaderTextPattern {
295                template: "Expense Accrual - {Month} {Year}".to_string(),
296                business_process: BusinessProcess::P2P,
297            },
298            HeaderTextPattern {
299                template: "Travel Expense Report - {EmployeeName}".to_string(),
300                business_process: BusinessProcess::P2P,
301            },
302            HeaderTextPattern {
303                template: "Utility Bill - {VendorName}".to_string(),
304                business_process: BusinessProcess::P2P,
305            },
306            HeaderTextPattern {
307                template: "Goods Receipt - {PONumber}".to_string(),
308                business_process: BusinessProcess::P2P,
309            },
310            // R2R - Record to Report
311            HeaderTextPattern {
312                template: "Month End Close - {Month} {Year}".to_string(),
313                business_process: BusinessProcess::R2R,
314            },
315            HeaderTextPattern {
316                template: "Depreciation - {Month} {Year}".to_string(),
317                business_process: BusinessProcess::R2R,
318            },
319            HeaderTextPattern {
320                template: "Amortization - {Month} {Year}".to_string(),
321                business_process: BusinessProcess::R2R,
322            },
323            HeaderTextPattern {
324                template: "Accrual Reversal - {Month}".to_string(),
325                business_process: BusinessProcess::R2R,
326            },
327            HeaderTextPattern {
328                template: "Prepaid Expense Release - {Month}".to_string(),
329                business_process: BusinessProcess::R2R,
330            },
331            HeaderTextPattern {
332                template: "FX Revaluation - {Month} {Year}".to_string(),
333                business_process: BusinessProcess::R2R,
334            },
335            HeaderTextPattern {
336                template: "Bank Reconciliation Adjustment".to_string(),
337                business_process: BusinessProcess::R2R,
338            },
339            HeaderTextPattern {
340                template: "Manual Journal Entry - {Department}".to_string(),
341                business_process: BusinessProcess::R2R,
342            },
343            HeaderTextPattern {
344                template: "Intercompany Allocation - {Month}".to_string(),
345                business_process: BusinessProcess::R2R,
346            },
347            HeaderTextPattern {
348                template: "Cost Allocation - {Quarter} {Year}".to_string(),
349                business_process: BusinessProcess::R2R,
350            },
351            // H2R - Hire to Retire
352            HeaderTextPattern {
353                template: "Payroll - {Month} {Year}".to_string(),
354                business_process: BusinessProcess::H2R,
355            },
356            HeaderTextPattern {
357                template: "Benefits Accrual - {Month}".to_string(),
358                business_process: BusinessProcess::H2R,
359            },
360            HeaderTextPattern {
361                template: "Bonus Accrual - {Quarter} {Year}".to_string(),
362                business_process: BusinessProcess::H2R,
363            },
364            HeaderTextPattern {
365                template: "Pension Contribution - {Month}".to_string(),
366                business_process: BusinessProcess::H2R,
367            },
368            HeaderTextPattern {
369                template: "Stock Compensation - {Month} {Year}".to_string(),
370                business_process: BusinessProcess::H2R,
371            },
372            HeaderTextPattern {
373                template: "Payroll Tax Remittance - {Month}".to_string(),
374                business_process: BusinessProcess::H2R,
375            },
376            HeaderTextPattern {
377                template: "401k Contribution - {Month} {Year}".to_string(),
378                business_process: BusinessProcess::H2R,
379            },
380            // A2R - Acquire to Retire
381            HeaderTextPattern {
382                template: "Asset Acquisition - {AssetDescription}".to_string(),
383                business_process: BusinessProcess::A2R,
384            },
385            HeaderTextPattern {
386                template: "Capital Project - {ProjectName}".to_string(),
387                business_process: BusinessProcess::A2R,
388            },
389            HeaderTextPattern {
390                template: "Asset Disposal - {AssetDescription}".to_string(),
391                business_process: BusinessProcess::A2R,
392            },
393            HeaderTextPattern {
394                template: "Asset Transfer - {AssetDescription}".to_string(),
395                business_process: BusinessProcess::A2R,
396            },
397            HeaderTextPattern {
398                template: "CIP Settlement - {ProjectName}".to_string(),
399                business_process: BusinessProcess::A2R,
400            },
401            HeaderTextPattern {
402                template: "Impairment Write-down - {Quarter} {Year}".to_string(),
403                business_process: BusinessProcess::A2R,
404            },
405            // Treasury
406            HeaderTextPattern {
407                template: "Bank Transfer - {Month} {Year}".to_string(),
408                business_process: BusinessProcess::Treasury,
409            },
410            HeaderTextPattern {
411                template: "Cash Pooling - {Month}".to_string(),
412                business_process: BusinessProcess::Treasury,
413            },
414            HeaderTextPattern {
415                template: "Investment Transaction".to_string(),
416                business_process: BusinessProcess::Treasury,
417            },
418            HeaderTextPattern {
419                template: "Loan Interest Payment - {Month}".to_string(),
420                business_process: BusinessProcess::Treasury,
421            },
422            // Tax
423            HeaderTextPattern {
424                template: "Tax Provision - {Quarter} {Year}".to_string(),
425                business_process: BusinessProcess::Tax,
426            },
427            HeaderTextPattern {
428                template: "VAT/GST Remittance - {Month}".to_string(),
429                business_process: BusinessProcess::Tax,
430            },
431            HeaderTextPattern {
432                template: "Withholding Tax - {Month} {Year}".to_string(),
433                business_process: BusinessProcess::Tax,
434            },
435            // Intercompany
436            HeaderTextPattern {
437                template: "IC Service Charge - {Month} {Year}".to_string(),
438                business_process: BusinessProcess::Intercompany,
439            },
440            HeaderTextPattern {
441                template: "IC Management Fee - {Quarter}".to_string(),
442                business_process: BusinessProcess::Intercompany,
443            },
444            HeaderTextPattern {
445                template: "IC Goods Transfer".to_string(),
446                business_process: BusinessProcess::Intercompany,
447            },
448        ]
449    }
450
451    fn default_expense_descriptions() -> Vec<&'static str> {
452        vec![
453            "Office supplies and materials",
454            "Software subscription - monthly",
455            "Professional services fee",
456            "Travel expense - airfare",
457            "Travel expense - hotel",
458            "Travel expense - meals",
459            "Conference registration fee",
460            "Equipment maintenance",
461            "Telecommunication services",
462            "Internet and data services",
463            "Insurance premium",
464            "Legal services",
465            "Consulting services",
466            "Marketing materials",
467            "Advertising expense",
468            "Training and development",
469            "Membership and subscriptions",
470            "Postage and shipping",
471            "Utilities expense",
472            "Rent expense - monthly",
473            "Cleaning services",
474            "Security services",
475            "Repair and maintenance",
476            "Vehicle expense",
477            "Fuel expense",
478            "Bank charges",
479            "Credit card processing fees",
480            "Recruitment expense",
481            "Employee benefits",
482            "Medical insurance contribution",
483            "Office refreshments",
484            "Team building event",
485            "Client entertainment",
486            "Research materials",
487            "Cloud computing services",
488            "Data storage services",
489            "Audit fees",
490            "Tax preparation services",
491            "License and permits",
492            "Bad debt expense",
493        ]
494    }
495
496    fn default_revenue_descriptions() -> Vec<&'static str> {
497        vec![
498            "Product sales revenue",
499            "Service revenue",
500            "Consulting revenue",
501            "Subscription revenue - monthly",
502            "License fee revenue",
503            "Maintenance contract revenue",
504            "Support services revenue",
505            "Training revenue",
506            "Commission income",
507            "Referral fee income",
508            "Rental income",
509            "Interest income",
510            "Dividend income",
511            "Royalty income",
512            "Grant revenue",
513            "Milestone payment",
514            "Setup fee revenue",
515            "Implementation revenue",
516            "Project completion payment",
517            "Retainer fee",
518        ]
519    }
520
521    fn default_asset_descriptions() -> Vec<&'static str> {
522        vec![
523            "Cash receipt",
524            "Bank deposit",
525            "AR collection",
526            "Prepaid expense",
527            "Security deposit",
528            "Inventory receipt",
529            "Fixed asset addition",
530            "Computer equipment",
531            "Office furniture",
532            "Leasehold improvement",
533            "Software license",
534            "Patent acquisition",
535            "Investment purchase",
536            "Loan receivable",
537            "Intercompany receivable",
538            "Other current asset",
539            "Deferred tax asset",
540            "Work in progress",
541            "Raw materials",
542            "Finished goods",
543        ]
544    }
545
546    fn default_liability_descriptions() -> Vec<&'static str> {
547        vec![
548            "AP - vendor invoice",
549            "Accrued expense",
550            "Accrued payroll",
551            "Accrued bonus",
552            "Deferred revenue",
553            "Customer deposit",
554            "Sales tax payable",
555            "VAT payable",
556            "Income tax payable",
557            "Withholding tax",
558            "Pension liability",
559            "Lease liability",
560            "Loan payable - current",
561            "Loan payable - long term",
562            "Intercompany payable",
563            "Accrued interest",
564            "Warranty reserve",
565            "Legal reserve",
566            "Other accrued liability",
567            "Gift card liability",
568        ]
569    }
570
571    fn default_bank_descriptions() -> Vec<&'static str> {
572        vec![
573            "Wire transfer",
574            "ACH payment",
575            "Check deposit",
576            "Cash withdrawal",
577            "Bank fee",
578            "Interest earned",
579            "Transfer between accounts",
580            "Direct deposit",
581            "ATM withdrawal",
582            "Credit card payment",
583        ]
584    }
585
586    /// Generate header text for a business process.
587    pub fn generate_header_text(
588        &self,
589        process: BusinessProcess,
590        context: &DescriptionContext,
591        rng: &mut impl Rng,
592    ) -> String {
593        // Filter patterns for this business process
594        let matching: Vec<_> = self
595            .header_patterns
596            .iter()
597            .filter(|p| p.business_process == process)
598            .collect();
599
600        if matching.is_empty() {
601            return format!("{:?} Transaction", process);
602        }
603
604        let pattern = matching.choose(rng).expect("non-empty collection");
605        self.substitute_placeholders(&pattern.template, context, rng)
606    }
607
608    /// Generate line text based on account type.
609    pub fn generate_line_text(
610        &self,
611        gl_account: &str,
612        context: &DescriptionContext,
613        rng: &mut impl Rng,
614    ) -> String {
615        // Determine account type from first digit
616        let first_char = gl_account.chars().next().unwrap_or('0');
617
618        match first_char {
619            '1' => {
620                // Assets
621                self.asset_descriptions
622                    .choose(rng)
623                    .unwrap_or(&"Asset posting")
624                    .to_string()
625            }
626            '2' => {
627                // Liabilities
628                self.liability_descriptions
629                    .choose(rng)
630                    .unwrap_or(&"Liability posting")
631                    .to_string()
632            }
633            '3' => {
634                // Equity - use generic
635                "Equity adjustment".to_string()
636            }
637            '4' => {
638                // Revenue
639                self.revenue_descriptions
640                    .choose(rng)
641                    .unwrap_or(&"Revenue posting")
642                    .to_string()
643            }
644            '5' | '6' | '7' => {
645                // Expenses
646                self.expense_descriptions
647                    .choose(rng)
648                    .unwrap_or(&"Expense posting")
649                    .to_string()
650            }
651            '8' | '9' => {
652                // Statistical / Other
653                "Statistical posting".to_string()
654            }
655            '0' => {
656                // Cash/Bank
657                self.bank_descriptions
658                    .choose(rng)
659                    .unwrap_or(&"Bank transaction")
660                    .to_string()
661            }
662            _ => self.substitute_placeholders("Transaction posting", context, rng),
663        }
664    }
665
666    /// Generate line text based on business process and account type.
667    ///
668    /// This method provides semantically appropriate line descriptions that match
669    /// the business process context. If a business process is provided, it uses
670    /// process-specific descriptions; otherwise falls back to account-type-based
671    /// descriptions.
672    pub fn generate_line_text_for_process(
673        &self,
674        gl_account: &str,
675        business_process: Option<BusinessProcess>,
676        _context: &DescriptionContext,
677        rng: &mut impl Rng,
678    ) -> String {
679        // If business process is specified, use process-specific descriptions
680        if let Some(process) = business_process {
681            let pool = match process {
682                BusinessProcess::P2P => &self.p2p_line_descriptions,
683                BusinessProcess::O2C => &self.o2c_line_descriptions,
684                BusinessProcess::H2R => &self.h2r_line_descriptions,
685                BusinessProcess::R2R => &self.r2r_line_descriptions,
686                _ => {
687                    // For other processes, fall back to account-type-based
688                    return self.generate_line_text_by_account(gl_account, rng);
689                }
690            };
691
692            if let Some(desc) = pool.choose(rng) {
693                return (*desc).to_string();
694            }
695        }
696
697        // Fall back to account-type-based descriptions
698        self.generate_line_text_by_account(gl_account, rng)
699    }
700
701    /// Generate line text based solely on account type.
702    fn generate_line_text_by_account(&self, gl_account: &str, rng: &mut impl Rng) -> String {
703        let first_char = gl_account.chars().next().unwrap_or('0');
704
705        match first_char {
706            '1' => self
707                .asset_descriptions
708                .choose(rng)
709                .unwrap_or(&"Asset posting")
710                .to_string(),
711            '2' => self
712                .liability_descriptions
713                .choose(rng)
714                .unwrap_or(&"Liability posting")
715                .to_string(),
716            '3' => "Equity adjustment".to_string(),
717            '4' => self
718                .revenue_descriptions
719                .choose(rng)
720                .unwrap_or(&"Revenue posting")
721                .to_string(),
722            '5' | '6' | '7' => self
723                .expense_descriptions
724                .choose(rng)
725                .unwrap_or(&"Expense posting")
726                .to_string(),
727            '8' | '9' => "Statistical posting".to_string(),
728            '0' => self
729                .bank_descriptions
730                .choose(rng)
731                .unwrap_or(&"Bank transaction")
732                .to_string(),
733            _ => "Transaction posting".to_string(),
734        }
735    }
736
737    /// Substitute placeholders in a template string.
738    fn substitute_placeholders(
739        &self,
740        template: &str,
741        context: &DescriptionContext,
742        rng: &mut impl Rng,
743    ) -> String {
744        let mut result = template.to_string();
745
746        // Substitute all placeholders with context values or defaults
747        if let Some(ref val) = context.vendor_name {
748            result = result.replace("{VendorName}", val);
749        } else {
750            result = result.replace("{VendorName}", &self.generate_vendor_name(rng));
751        }
752
753        if let Some(ref val) = context.customer_name {
754            result = result.replace("{CustomerName}", val);
755        } else {
756            result = result.replace("{CustomerName}", &self.generate_customer_name(rng));
757        }
758
759        if let Some(ref val) = context.invoice_number {
760            result = result.replace("{InvoiceNumber}", val);
761        } else {
762            result = result.replace(
763                "{InvoiceNumber}",
764                &format!("INV-{:06}", rng.random_range(1..999999)),
765            );
766        }
767
768        if let Some(ref val) = context.po_number {
769            result = result.replace("{PONumber}", val);
770        } else {
771            result = result.replace(
772                "{PONumber}",
773                &format!("PO-{:06}", rng.random_range(1..999999)),
774            );
775        }
776
777        if let Some(ref val) = context.month_name {
778            result = result.replace("{Month}", val);
779        } else {
780            result = result.replace("{Month}", "January");
781        }
782
783        if let Some(ref val) = context.year {
784            result = result.replace("{Year}", val);
785        } else {
786            result = result.replace("{Year}", "2024");
787        }
788
789        if let Some(ref val) = context.quarter {
790            result = result.replace("{Quarter}", val);
791        } else {
792            result = result.replace("{Quarter}", "Q1");
793        }
794
795        if let Some(ref val) = context.asset_description {
796            result = result.replace("{AssetDescription}", val);
797        } else {
798            result = result.replace("{AssetDescription}", &self.generate_asset_description(rng));
799        }
800
801        if let Some(ref val) = context.project_name {
802            result = result.replace("{ProjectName}", val);
803        } else {
804            result = result.replace("{ProjectName}", &self.generate_project_name(rng));
805        }
806
807        if let Some(ref val) = context.department_name {
808            result = result.replace("{Department}", val);
809        } else {
810            result = result.replace("{Department}", "Finance");
811        }
812
813        if let Some(ref val) = context.employee_name {
814            result = result.replace("{EmployeeName}", val);
815        } else {
816            result = result.replace("{EmployeeName}", "Employee");
817        }
818
819        result
820    }
821
822    fn generate_vendor_name(&self, rng: &mut impl Rng) -> String {
823        let vendors = [
824            "Acme Supplies Inc",
825            "Global Tech Solutions",
826            "Office Depot",
827            "Amazon Business",
828            "Dell Technologies",
829            "Microsoft Corporation",
830            "Adobe Systems",
831            "Salesforce Inc",
832            "Oracle Corporation",
833            "ServiceNow Inc",
834            "Workday Inc",
835            "SAP America",
836            "IBM Corporation",
837            "Cisco Systems",
838            "HP Inc",
839            "Lenovo Group",
840            "Apple Inc",
841            "Google Cloud",
842            "AWS Inc",
843            "Zoom Communications",
844        ];
845        vendors.choose(rng).unwrap_or(&"Vendor").to_string()
846    }
847
848    fn generate_customer_name(&self, rng: &mut impl Rng) -> String {
849        let customers = [
850            "Northwind Traders",
851            "Contoso Ltd",
852            "Adventure Works",
853            "Fabrikam Inc",
854            "Tailspin Toys",
855            "Wide World Importers",
856            "Proseware Inc",
857            "Coho Vineyard",
858            "Alpine Ski House",
859            "Bellows College",
860            "Datum Corporation",
861            "Litware Inc",
862            "Lucerne Publishing",
863            "Margie Travel",
864            "Trey Research",
865            "Fourth Coffee",
866            "Graphic Design Institute",
867            "School of Fine Art",
868            "VanArsdel Ltd",
869            "Wingtip Toys",
870        ];
871        customers.choose(rng).unwrap_or(&"Customer").to_string()
872    }
873
874    fn generate_asset_description(&self, rng: &mut impl Rng) -> String {
875        let assets = [
876            "Server Equipment",
877            "Network Infrastructure",
878            "Office Renovation",
879            "Manufacturing Equipment",
880            "Delivery Vehicle",
881            "Computer Hardware",
882            "Software License",
883            "Building Improvement",
884            "Lab Equipment",
885            "Security System",
886        ];
887        assets.choose(rng).unwrap_or(&"Asset").to_string()
888    }
889
890    fn generate_project_name(&self, rng: &mut impl Rng) -> String {
891        let projects = [
892            "Digital Transformation",
893            "ERP Implementation",
894            "Data Center Upgrade",
895            "Office Expansion",
896            "Process Automation",
897            "Cloud Migration",
898            "Security Enhancement",
899            "Customer Portal",
900            "Mobile App Development",
901            "Infrastructure Modernization",
902        ];
903        projects.choose(rng).unwrap_or(&"Project").to_string()
904    }
905}
906
907#[cfg(test)]
908#[allow(clippy::unwrap_used)]
909mod tests {
910    use super::*;
911    use rand::SeedableRng;
912    use rand_chacha::ChaCha8Rng;
913
914    #[test]
915    fn test_header_text_generation() {
916        let mut rng = ChaCha8Rng::seed_from_u64(42);
917        let generator = DescriptionGenerator::new();
918        let context = DescriptionContext::with_period(3, 2024);
919
920        let text = generator.generate_header_text(BusinessProcess::P2P, &context, &mut rng);
921        assert!(!text.is_empty());
922    }
923
924    #[test]
925    fn test_line_text_generation() {
926        let mut rng = ChaCha8Rng::seed_from_u64(42);
927        let generator = DescriptionGenerator::new();
928        let context = DescriptionContext::default();
929
930        // Test expense account
931        let expense_text = generator.generate_line_text("500100", &context, &mut rng);
932        assert!(!expense_text.is_empty());
933
934        // Test revenue account
935        let revenue_text = generator.generate_line_text("400100", &context, &mut rng);
936        assert!(!revenue_text.is_empty());
937
938        // Test asset account
939        let asset_text = generator.generate_line_text("100000", &context, &mut rng);
940        assert!(!asset_text.is_empty());
941    }
942
943    #[test]
944    fn test_placeholder_substitution() {
945        let mut rng = ChaCha8Rng::seed_from_u64(42);
946        let generator = DescriptionGenerator::new();
947        let mut context = DescriptionContext::with_period(6, 2024);
948        context.vendor_name = Some("Test Vendor Inc".to_string());
949
950        let text = generator.generate_header_text(BusinessProcess::P2P, &context, &mut rng);
951
952        // Should not contain raw placeholders
953        assert!(!text.contains('{'));
954        assert!(!text.contains('}'));
955    }
956
957    #[test]
958    fn test_all_business_processes() {
959        let mut rng = ChaCha8Rng::seed_from_u64(42);
960        let generator = DescriptionGenerator::new();
961        let context = DescriptionContext::with_period(1, 2024);
962
963        let processes = [
964            BusinessProcess::O2C,
965            BusinessProcess::P2P,
966            BusinessProcess::R2R,
967            BusinessProcess::H2R,
968            BusinessProcess::A2R,
969            BusinessProcess::Treasury,
970            BusinessProcess::Tax,
971            BusinessProcess::Intercompany,
972        ];
973
974        for process in processes {
975            let text = generator.generate_header_text(process, &context, &mut rng);
976            assert!(!text.is_empty(), "Empty text for {:?}", process);
977        }
978    }
979}