1use chrono::Utc;
2use std::path::PathBuf;
3
4use crate::cli::TemplateCmd;
5use crate::clauses;
6use crate::db::{Client, Contract, ContractClauseRow, Issuer};
7use crate::error::Result;
8use crate::output::{print_success, Ctx};
9use crate::render;
10use crate::tax::Jurisdiction;
11use crate::typst_assets;
12
13pub fn run(cmd: TemplateCmd, ctx: Ctx) -> Result<()> {
14 match cmd {
15 TemplateCmd::List => {
16 let names = typst_assets::list_templates()?;
17 print_success(ctx, &names, |ns| {
18 if ns.is_empty() {
19 println!("(no templates)");
20 } else {
21 for n in ns {
22 println!(" {n}");
23 }
24 }
25 });
26 Ok(())
27 }
28 TemplateCmd::Preview { name, kind, out } => {
29 let issuer = sample_issuer();
30 let client = sample_client();
31 let contract = sample_contract(&kind);
32 let pack = clauses::load_pack(&kind, &contract.clause_pack)?;
33 let clause_rows: Vec<ContractClauseRow> = pack
34 .pack
35 .default_clauses
36 .iter()
37 .enumerate()
38 .map(|(i, slug)| ContractClauseRow {
39 id: 0,
40 contract_id: 0,
41 position: i as i64,
42 slug: slug.clone(),
43 heading: None,
44 body: None,
45 })
46 .collect();
47 let mut data =
48 render::build_render_data(&contract, &issuer, &client, &clause_rows, &pack, false, true)?;
49 let out_path =
50 PathBuf::from(out.unwrap_or_else(|| format!("preview-{name}-{kind}.pdf")));
51 render::render_to_pdf(&name, &mut data, &issuer, &out_path)?;
52 print_success(ctx, &out_path.display().to_string(), |p| {
53 println!("preview → {p}");
54 });
55 Ok(())
56 }
57 }
58}
59
60fn sample_issuer() -> Issuer {
61 Issuer {
62 id: 0,
63 slug: "acme".into(),
64 name: "Acme Studio".into(),
65 legal_name: Some("Acme Studio Pte. Ltd.".into()),
66 jurisdiction: Jurisdiction::Sg,
67 tax_registered: false,
68 tax_id: None,
69 company_no: Some("202312345A".into()),
70 tagline: None,
71 address: vec![
72 "1 Marina Bay".into(),
73 "Singapore 018989".into(),
74 ],
75 email: Some("hello@acme.example".into()),
76 phone: None,
77 bank_details: None,
78 default_template: "helvetica-nera".into(),
79 currency: None,
80 symbol: None,
81 number_format: "{issuer}-{year}-{seq:04}".into(),
82 logo_path: None,
83 default_output_dir: None,
84 default_notes: None,
85 }
86}
87
88fn sample_client() -> Client {
89 Client {
90 id: 0,
91 slug: "meridian".into(),
92 name: "Meridian & Co.".into(),
93 legal_name: Some("Meridian & Co. Pty Ltd".into()),
94 company_no: Some("ACN 600 123 456".into()),
95 legal_jurisdiction: Some("Victoria, Australia".into()),
96 attn: Some("Sophie Lin, Head of Marketing".into()),
97 country: Some("AU".into()),
98 tax_id: None,
99 address: vec![
100 "401 Collins Street".into(),
101 "Melbourne VIC 3000".into(),
102 "Australia".into(),
103 ],
104 email: Some("sophie@meridian.example".into()),
105 notes: None,
106 default_issuer_slug: None,
107 default_template: None,
108 }
109}
110
111fn sample_contract(kind: &str) -> Contract {
112 let today = Utc::now().date_naive().format("%Y-%m-%d").to_string();
113 let terms = match kind {
114 "nda" => serde_json::json!({
115 "mutuality": "mutual",
116 "disclosing_side": "both",
117 "purpose": "a potential collaboration on a longevity research project",
118 "confidentiality_years": 3,
119 }),
120 "consulting" => serde_json::json!({
121 "purpose": "the design and delivery of a customer-facing dashboard for the Client's flagship product",
122 "deliverables": [
123 "Discovery interviews and a one-page strategy memo",
124 "Three rounds of high-fidelity Figma designs",
125 "Production-ready front-end implementation in React",
126 "One handover session with the Client's engineering team",
127 ],
128 "ip_assignment": "client",
129 "confidentiality_years": 3,
130 "termination_notice_days": 30,
131 }),
132 "msa" => serde_json::json!({
133 "ip_assignment": "client",
134 "confidentiality_years": 3,
135 "termination_notice_days": 30,
136 }),
137 "sow" => serde_json::json!({
138 "purpose": "Implementation of the customer dashboard described in the kick-off memo dated 2026-04-01.",
139 "deliverables": [
140 "Functional prototype by week 4",
141 "Production deployment by week 10",
142 ],
143 }),
144 "service" => serde_json::json!({
145 "purpose": "managed hosting and monthly performance reviews for the Customer's web application",
146 "deliverables": [
147 "24/7 monitoring of production endpoints",
148 "Monthly performance review and recommendations",
149 ],
150 "termination_notice_days": 60,
151 }),
152 _ => serde_json::json!({}),
153 };
154 let (fee_type, fee_minor, fee_cur, fee_sched) = match kind {
155 "consulting" => (Some("fixed".into()), Some(84_000_00i64), Some("SGD".into()), Some("on-completion".into())),
156 "msa" => (None, None, None, None),
157 "sow" => (Some("fixed".into()), Some(34_000_00i64), Some("SGD".into()), Some("on-milestone".into())),
158 "service" => (Some("retainer".into()), Some(5_000_00i64), Some("SGD".into()), Some("monthly".into())),
159 _ => (None, None, None, None),
160 };
161 let title = match kind {
162 "nda" => "Mutual NDA — Acme × Meridian".into(),
163 "consulting" => "Customer Dashboard — Consulting Engagement".into(),
164 "msa" => "Master Services Agreement — Acme × Meridian".into(),
165 "sow" => "SOW #1 — Customer Dashboard Implementation".into(),
166 "service" => "Managed Hosting & Performance Reviews".into(),
167 _ => format!("Sample {kind} agreement"),
168 };
169 Contract {
170 id: 0,
171 number: format!("{}-acme-2026-0001", prefix_for(kind)),
172 kind: kind.to_string(),
173 issuer_id: 0,
174 client_id: 0,
175 title,
176 effective_date: today,
177 end_date: None,
178 term_months: Some(12),
179 governing_law: "Singapore".into(),
180 venue: None,
181 status: "draft".into(),
182 sent_at: None,
183 signed_at: None,
184 terminated_at: None,
185 notes: None,
186 fee_type,
187 fee_amount_minor: fee_minor,
188 fee_currency: fee_cur,
189 fee_schedule: fee_sched,
190 terms_json: terms.to_string(),
191 clause_pack: "standard".into(),
192 clause_pack_version: "1.0".into(),
193 default_template: None,
194 signed_by_us_name: None,
195 signed_by_us_title: None,
196 signed_by_us_at: None,
197 signed_by_them_name: None,
198 signed_by_them_title: None,
199 signed_by_them_at: None,
200 created_at: String::new(),
201 updated_at: String::new(),
202 }
203}
204
205fn prefix_for(kind: &str) -> &'static str {
206 match kind {
207 "consulting" => "CTR",
208 "nda" => "NDA",
209 "msa" => "MSA",
210 "sow" => "SOW",
211 "service" => "SVC",
212 _ => "DOC",
213 }
214}