1use crate::client::archive::ArchiveClient;
2use crate::config::Config;
3use crate::error::YukiError;
4use crate::output::{OutputFormat, format_json, format_table, is_tty};
5
6pub async fn list(
7 config: &Config,
8 _admin: Option<&str>,
9 folder: Option<&str>,
10 doc_type: Option<&str>,
11 format: Option<&str>,
12) -> Result<(), YukiError> {
13 let mut client = ArchiveClient::new();
14 client.authenticate(&config.api_key).await?;
15
16 let docs = match (folder, doc_type) {
17 (Some(f), _) => {
18 let folder_id: i32 = f.parse().unwrap_or(0);
19 client
20 .documents_in_folder(folder_id, "2000-01-01", "2099-12-31")
21 .await?
22 }
23 (None, Some(t)) => {
24 let doc_type_id: i32 = t.parse().unwrap_or(0);
25 let xml = client.documents_by_type(doc_type_id).await?;
26 let headers = vec!["Raw XML".into()];
28 let rows = vec![vec![xml]];
29 let fmt = OutputFormat::from_flag(format, is_tty());
30 match fmt {
31 OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
32 OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
33 }
34 return Ok(());
35 }
36 (None, None) => {
37 client
38 .documents_in_folder(0, "2000-01-01", "2099-12-31")
39 .await?
40 }
41 };
42
43 let headers = vec![
44 "ID".into(),
45 "Date".into(),
46 "Amount".into(),
47 "Contact".into(),
48 "Subject".into(),
49 "File".into(),
50 ];
51 let rows: Vec<Vec<String>> = docs
52 .into_iter()
53 .map(|d| {
54 vec![
55 d.id,
56 d.document_date,
57 d.amount,
58 d.contact_name,
59 d.subject,
60 d.file_name,
61 ]
62 })
63 .collect();
64
65 let fmt = OutputFormat::from_flag(format, is_tty());
66 match fmt {
67 OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
68 OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
69 }
70 Ok(())
71}
72
73pub async fn search(
74 config: &Config,
75 _admin: Option<&str>,
76 query: &str,
77 format: Option<&str>,
78) -> Result<(), YukiError> {
79 let mut client = ArchiveClient::new();
80 client.authenticate(&config.api_key).await?;
81
82 let year = current_year();
84 let start = format!("{year}-01-01");
85 let end = format!("{year}-12-31");
86
87 let docs = client.search_documents(query, &start, &end).await?;
88
89 let headers = vec![
90 "ID".into(),
91 "Date".into(),
92 "Amount".into(),
93 "Contact".into(),
94 "Subject".into(),
95 "File".into(),
96 ];
97 let rows: Vec<Vec<String>> = docs
98 .into_iter()
99 .map(|d| {
100 vec![
101 d.id,
102 d.document_date,
103 d.amount,
104 d.contact_name,
105 d.subject,
106 d.file_name,
107 ]
108 })
109 .collect();
110
111 let fmt = OutputFormat::from_flag(format, is_tty());
112 match fmt {
113 OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
114 OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
115 }
116 Ok(())
117}
118
119pub async fn exists(
124 config: &Config,
125 _admin: Option<&str>,
126 amount: f64,
127 date: &str,
128 contact: Option<&str>,
129 format: Option<&str>,
130) -> Result<(), YukiError> {
131 let (search_start, search_end, filter_start, filter_end) = date_range(date)?;
132
133 let mut client = ArchiveClient::new();
134 client.authenticate(&config.api_key).await?;
135
136 let search_text = contact.unwrap_or("");
137 let docs = client
138 .search_documents(search_text, &search_start, &search_end)
139 .await?;
140
141 let matched: Vec<_> = docs
142 .into_iter()
143 .filter(|d| {
144 let amount_matches = d
145 .amount
146 .trim()
147 .parse::<f64>()
148 .map(|a| (a - amount).abs() <= 0.01)
149 .unwrap_or(false);
150 let date_matches = date_in_range(&d.document_date, &filter_start, &filter_end);
151 amount_matches && date_matches
152 })
153 .collect();
154
155 let headers = vec![
156 "ID".into(),
157 "Date".into(),
158 "Amount".into(),
159 "Contact".into(),
160 "Subject".into(),
161 "File".into(),
162 ];
163 let rows: Vec<Vec<String>> = matched
164 .iter()
165 .map(|d| {
166 vec![
167 d.id.clone(),
168 d.document_date.clone(),
169 d.amount.clone(),
170 d.contact_name.clone(),
171 d.subject.clone(),
172 d.file_name.clone(),
173 ]
174 })
175 .collect();
176
177 let fmt = OutputFormat::from_flag(format, is_tty());
178 match fmt {
179 OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
180 OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
181 }
182
183 if rows.is_empty() {
184 return Err(YukiError::NotFound(
185 "no matching document found".to_string(),
186 ));
187 }
188
189 Ok(())
190}
191
192fn current_year() -> u32 {
193 use std::time::{SystemTime, UNIX_EPOCH};
194 let secs = SystemTime::now()
195 .duration_since(UNIX_EPOCH)
196 .unwrap_or_default()
197 .as_secs();
198 1970 + (secs / 31_557_600) as u32
199}
200
201fn date_range(date: &str) -> Result<(String, String, String, String), YukiError> {
206 let parts: Vec<&str> = date.split('-').collect();
207 match parts.len() {
208 3 => {
209 let y: i32 = parts[0].parse().unwrap_or(0);
211 let m: i32 = parts[1].parse().unwrap_or(0);
212 let search_start = format!("{y:04}-{:02}-01", (m - 1).max(1));
213 let search_end = format!("{y:04}-{:02}-28", (m + 1).min(12));
214 let filter_start = shift_days(date, -7);
215 let filter_end = shift_days(date, 7);
216 Ok((search_start, search_end, filter_start, filter_end))
217 }
218 _ => {
219 let (start, end) = crate::period::parse_period(date)?;
221 Ok((start.clone(), end.clone(), start, end))
222 }
223 }
224}
225
226fn shift_days(date: &str, days: i32) -> String {
228 let to_days = |s: &str| -> i32 {
229 let p: Vec<&str> = s.split('-').collect();
230 if p.len() < 3 {
231 return 0;
232 }
233 let y: i32 = p[0].parse().unwrap_or(0);
234 let m: i32 = p[1].parse().unwrap_or(0);
235 let d: i32 = p[2].parse().unwrap_or(0);
236 y * 365 + m * 30 + d
237 };
238 let from_days = |total: i32| -> String {
239 let y = total / 365;
240 let rem = total % 365;
241 let m = (rem / 30).clamp(1, 12);
242 let d = (rem % 30).clamp(1, 28);
243 format!("{y:04}-{m:02}-{d:02}")
244 };
245 from_days(to_days(date) + days)
246}
247
248fn date_in_range(doc_date: &str, filter_start: &str, filter_end: &str) -> bool {
250 let normalized = doc_date.split('T').next().unwrap_or(doc_date);
251 normalized >= filter_start && normalized <= filter_end
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn date_range_exact_date() {
260 let (ss, se, fs, fe) = date_range("2025-03-15").unwrap();
261 assert_eq!(ss, "2025-02-01");
263 assert_eq!(se, "2025-04-28");
264 assert!(fs.as_str() <= "2025-03-08");
266 assert!(fe.as_str() >= "2025-03-22");
267 }
268
269 #[test]
270 fn date_range_month_period() {
271 let (ss, se, fs, fe) = date_range("2025-03").unwrap();
272 assert_eq!(ss, "2025-03-01");
273 assert_eq!(se, "2025-03-31");
274 assert_eq!(fs, ss);
275 assert_eq!(fe, se);
276 }
277
278 #[test]
279 fn date_range_quarter_period() {
280 let (ss, se, fs, fe) = date_range("2025-Q1").unwrap();
281 assert_eq!(ss, "2025-01-01");
282 assert_eq!(se, "2025-03-31");
283 assert_eq!(fs, ss);
284 assert_eq!(fe, se);
285 }
286
287 #[test]
288 fn date_range_year_period() {
289 let (ss, se, fs, fe) = date_range("2025").unwrap();
290 assert_eq!(ss, "2025-01-01");
291 assert_eq!(se, "2025-12-31");
292 assert_eq!(fs, ss);
293 assert_eq!(fe, se);
294 }
295
296 #[test]
297 fn date_in_range_within() {
298 assert!(date_in_range("2025-03-15", "2025-03-01", "2025-03-31"));
299 }
300
301 #[test]
302 fn date_in_range_strips_time() {
303 assert!(date_in_range(
304 "2025-03-15T10:30:00",
305 "2025-03-01",
306 "2025-03-31"
307 ));
308 }
309
310 #[test]
311 fn date_in_range_outside() {
312 assert!(!date_in_range("2025-04-01", "2025-03-01", "2025-03-31"));
313 }
314
315 #[test]
316 fn date_in_range_boundaries() {
317 assert!(date_in_range("2025-03-01", "2025-03-01", "2025-03-31"));
318 assert!(date_in_range("2025-03-31", "2025-03-01", "2025-03-31"));
319 }
320
321 #[test]
322 fn shift_days_forward() {
323 let result = shift_days("2025-03-15", 7);
324 assert!(result.as_str() > "2025-03-15");
325 }
326
327 #[test]
328 fn shift_days_backward() {
329 let result = shift_days("2025-03-15", -7);
330 assert!(result.as_str() < "2025-03-15");
331 }
332}