Skip to main content

garbage_code_hunter/debt_invoice/
display.rs

1//! Display debt invoice in terminal or JSON.
2
3use super::cost_model::Invoice;
4use crate::common::i18n_ext::t;
5use colored::Colorize;
6
7/// Format invoice for terminal display.
8pub fn format_terminal(invoice: &Invoice, lang: &str) -> String {
9    let mut out = String::new();
10
11    out.push_str(&format!(
12        "\n{}\n",
13        t(
14            lang,
15            "\u{1f4b8} 技术债账单",
16            "\u{1f4b8} Technical Debt Invoice"
17        )
18        .bold()
19    ));
20    out.push_str(&format!("{}\n\n", "\u{2501}".repeat(40)));
21
22    // Header
23    out.push_str(&format!(
24        "  {:<30} {:>6} {:>10} {:>12}\n",
25        t(lang, "类别", "Category").bold(),
26        t(lang, "数量", "Count").bold(),
27        t(lang, "工时", "Hours").bold(),
28        t(lang, "成本 (USD)", "Cost (USD)").bold()
29    ));
30    out.push_str(&format!("  {}\n", "\u{2500}".repeat(62)));
31
32    // Line items
33    for item in &invoice.items {
34        let cost_str = format!("${:.0}", item.estimated_cost);
35        out.push_str(&format!(
36            "  {:<30} {:>6} {:>10.1} {:>12}\n",
37            item.category, item.count, item.estimated_hours, cost_str
38        ));
39        out.push_str(&format!("    {}\n", item.pain_description.dimmed()));
40    }
41
42    out.push_str(&format!("  {}\n", "\u{2500}".repeat(62)));
43
44    // Totals
45    out.push_str(&format!(
46        "  {:<30} {:>6} {:>10.1} {:>12}\n",
47        "TOTAL".bold(),
48        invoice.items.iter().map(|i| i.count).sum::<usize>(),
49        invoice.total_hours,
50        format!("${:.0}", invoice.total_cost).bold()
51    ));
52
53    out.push('\n');
54
55    // Interest calculation
56    out.push_str(&format!(
57        "{}\n",
58        t(lang, "\u{1f4c8} 利息", "\u{1f4c8} Interest").bold()
59    ));
60    out.push_str(&format!(
61        "  {}: {:.0}%\n",
62        t(lang, "周利率", "Weekly interest rate"),
63        invoice.weekly_interest_rate
64    ));
65    let monthly_cost = invoice.total_cost * (1.0 + invoice.weekly_interest_rate / 100.0).powf(4.0)
66        - invoice.total_cost;
67    out.push_str(&format!(
68        "  {}: ${:.0}\n",
69        t(lang, "月利息", "Monthly interest"),
70        monthly_cost
71    ));
72    let yearly_cost = invoice.total_cost * (1.0 + invoice.weekly_interest_rate / 100.0).powf(52.0)
73        - invoice.total_cost;
74    out.push_str(&format!(
75        "  {}: ${:.0}\n",
76        t(lang, "年利息", "Yearly interest"),
77        yearly_cost
78    ));
79
80    out.push('\n');
81
82    // Project lifespan
83    out.push_str(&format!(
84        "{}\n",
85        t(lang, "\u{1f52e} 项目寿命", "\u{1f52e} Project Lifespan").bold()
86    ));
87    out.push_str(&format!(
88        "  {}\n",
89        t(
90            lang,
91            "代码库变得无法维护的预估时间:",
92            "Estimated time before codebase becomes unmaintainable:"
93        )
94    ));
95    let lifespan_str = if invoice.project_lifespan_months >= 24 {
96        format!(
97            "{} {}",
98            invoice.project_lifespan_months,
99            t(lang, "个月(暂时安全)", "months (you're fine, for now)")
100        )
101        .green()
102        .to_string()
103    } else if invoice.project_lifespan_months >= 12 {
104        format!(
105            "{} {}",
106            invoice.project_lifespan_months,
107            t(
108                lang,
109                "个月(该重构了)",
110                "months (better start refactoring)"
111            )
112        )
113        .yellow()
114        .to_string()
115    } else {
116        format!(
117            "{} {}",
118            invoice.project_lifespan_months,
119            t(
120                lang,
121                "个月(红色警报 \u{1f6a8})",
122                "months (CODE RED \u{1f6a8})"
123            )
124        )
125        .red()
126        .bold()
127        .to_string()
128    };
129    out.push_str(&format!("  {}\n", lifespan_str));
130
131    out
132}
133
134/// Format invoice as JSON.
135pub fn format_json(invoice: &Invoice) -> String {
136    serde_json::json!({
137        "items": invoice.items.iter().map(|item| {
138            serde_json::json!({
139                "category": item.category,
140                "count": item.count,
141                "estimated_hours": item.estimated_hours,
142                "estimated_cost": item.estimated_cost,
143                "pain_description": item.pain_description,
144            })
145        }).collect::<Vec<_>>(),
146        "total_hours": invoice.total_hours,
147        "total_cost": invoice.total_cost,
148        "project_lifespan_months": invoice.project_lifespan_months,
149        "weekly_interest_rate": invoice.weekly_interest_rate,
150    })
151    .to_string()
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::debt_invoice::cost_model::InvoiceItem;
158
159    fn make_invoice() -> Invoice {
160        Invoice {
161            items: vec![InvoiceItem {
162                category: "unwrap() abuse".to_string(),
163                count: 10,
164                estimated_hours: 20.0,
165                estimated_cost: 1500.0,
166                pain_description: "test".to_string(),
167            }],
168            total_hours: 20.0,
169            total_cost: 1500.0,
170            project_lifespan_months: 12,
171            weekly_interest_rate: 3.0,
172        }
173    }
174
175    #[test]
176    fn test_format_terminal() {
177        let invoice = make_invoice();
178        let out = format_terminal(&invoice, "en-US");
179        assert!(out.contains("Technical Debt Invoice"));
180        assert!(out.contains("unwrap() abuse"));
181        assert!(out.contains("$1500"));
182    }
183
184    #[test]
185    fn test_format_json() {
186        let invoice = make_invoice();
187        let json = format_json(&invoice);
188        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
189        assert_eq!(parsed["total_cost"], 1500.0);
190    }
191
192    #[test]
193    fn test_format_terminal_chinese() {
194        let invoice = make_invoice();
195        let out = format_terminal(&invoice, "zh-CN");
196        assert!(out.contains("技术债账单"));
197        assert!(out.contains("类别"));
198        assert!(out.contains("成本"));
199        assert!(out.contains("利息"));
200        assert!(out.contains("项目寿命"));
201    }
202
203    #[test]
204    fn test_format_terminal_zero_items() {
205        let invoice = Invoice {
206            items: vec![],
207            total_hours: 0.0,
208            total_cost: 0.0,
209            project_lifespan_months: 24,
210            weekly_interest_rate: 0.0,
211        };
212        let out = format_terminal(&invoice, "en-US");
213        assert!(out.contains("Technical Debt Invoice"));
214        assert!(out.contains("TOTAL"));
215    }
216
217    #[test]
218    fn test_format_terminal_critical_lifespan() {
219        let invoice = Invoice {
220            items: vec![],
221            total_hours: 0.0,
222            total_cost: 0.0,
223            project_lifespan_months: 6,
224            weekly_interest_rate: 0.0,
225        };
226        let out = format_terminal(&invoice, "en-US");
227        assert!(out.contains("CODE RED"));
228    }
229
230    #[test]
231    fn test_format_terminal_warning_lifespan() {
232        let invoice = Invoice {
233            items: vec![],
234            total_hours: 0.0,
235            total_cost: 0.0,
236            project_lifespan_months: 18,
237            weekly_interest_rate: 0.0,
238        };
239        let out = format_terminal(&invoice, "en-US");
240        assert!(out.contains("better start refactoring"));
241    }
242}