1use chrono::{DateTime, Local};
35use reqwest::Method;
36use serde_json::Value;
37
38use crate::{
39 ServiceDesk, TicketID,
40 client::{
41 Condition, CreateTicketData, Criteria, DetailedTicket, EditTicketData, ListInfo, LogicalOp,
42 Note, NoteData, SearchRequest, TicketData, TicketSearchResponse,
43 },
44 error::Error,
45};
46
47pub struct TicketsClient<'a> {
49 pub(crate) client: &'a ServiceDesk,
50}
51
52impl<'a> TicketsClient<'a> {
53 pub fn search(self) -> TicketSearchBuilder<'a> {
55 TicketSearchBuilder {
56 client: self.client,
57 root_criteria: None,
58 children: vec![],
59 row_count: 100,
60 }
61 }
62
63 pub fn create(self) -> TicketCreateBuilder<'a> {
65 TicketCreateBuilder {
66 client: self.client,
67 subject: None,
68 description: None,
69 requester: None,
70 priority: "Low".to_string(),
71 account: None,
72 template: None,
73 udf_fields: None,
74 }
75 }
76}
77
78pub struct TicketClient<'a> {
80 pub(crate) client: &'a ServiceDesk,
81 pub(crate) id: TicketID,
82}
83
84impl<'a> TicketClient<'a> {
85 pub async fn get(&self) -> Result<DetailedTicket, Error> {
87 self.client.ticket_details(self.id).await
88 }
89
90 pub async fn close(&self, comment: &str) -> Result<(), Error> {
92 self.client.close_ticket(self.id, comment).await
93 }
94
95 pub async fn assign(&self, technician: &str) -> Result<(), Error> {
97 self.client.assign_ticket(self.id, technician).await
98 }
99
100 pub async fn conversations(&self) -> Result<Value, Error> {
101 self.client.get_conversations(self.id).await
102 }
103
104 pub async fn conversation_content(&self, content_url: &str) -> Result<Value, Error> {
105 self.client.get_conversation_content(content_url).await
106 }
107
108 pub async fn all_attachment_links(&self) -> Result<Vec<String>, Error> {
111 let ticket = self.client.ticket(self.id).get().await?;
112 let mut links = Vec::new();
113 if let Some(attachments) = ticket.attachments {
114 for attachment in attachments {
115 links.push(format!(
116 "{}{}",
117 self.client.base_url, attachment.content_url
118 ));
119 }
120 }
121 if let Ok(attachments) = self.client.get_conversation_attachment_urls(self.id).await {
122 for url in attachments {
123 links.push(url);
124 }
125 }
126 Ok(links)
127 }
128
129 pub async fn add_note(&self, description: &str) -> Result<Note, Error> {
131 self.client
132 .add_note(
133 self.id,
134 &NoteData {
135 description: description.to_string(),
136 ..Default::default()
137 },
138 )
139 .await
140 }
141
142 pub fn note(&self) -> NoteBuilder<'a> {
144 NoteBuilder {
145 client: self.client,
146 ticket_id: self.id,
147 description: String::new(),
148 mark_first_response: false,
149 add_to_linked_requests: false,
150 notify_technician: false,
151 show_to_requester: false,
152 }
153 }
154
155 pub async fn merge(&self, ticket_ids: &[TicketID]) -> Result<(), Error> {
157 self.client.merge(self.id, ticket_ids).await
158 }
159
160 pub async fn edit(&self, data: &EditTicketData) -> Result<(), Error> {
162 self.client.edit(self.id, data).await
163 }
164
165 pub async fn close_with_note(&self, comment: &str) -> Result<(), Error> {
167 self.client
168 .add_note(
169 self.id,
170 &NoteData {
171 description: comment.to_string(),
172 ..Default::default()
173 },
174 )
175 .await?;
176 self.client.close_ticket(self.id, comment).await
177 }
178}
179
180pub struct TicketSearchBuilder<'a> {
184 client: &'a ServiceDesk,
185 root_criteria: Option<Criteria>,
186 children: Vec<Criteria>,
187 row_count: u32,
188}
189
190#[derive(Debug, PartialEq, Eq)]
192pub enum TicketStatus {
193 Open,
194 Closed,
195 Cancelled,
196 OnHold,
197}
198
199impl std::fmt::Display for TicketStatus {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 let status_str = match self {
202 TicketStatus::Open => "Open",
203 TicketStatus::Closed => "Closed",
204 TicketStatus::Cancelled => "Cancelled",
205 TicketStatus::OnHold => "On Hold",
206 };
207 write!(f, "{}", status_str)
208 }
209}
210
211impl<'a> TicketSearchBuilder<'a> {
212 pub fn status(mut self, status: &str) -> Self {
214 self.root_criteria = Some(Criteria {
215 field: "status.name".to_string(),
216 condition: Condition::Is,
217 value: status.into(),
218 children: vec![],
219 logical_operator: None,
220 });
221 self
222 }
223
224 pub fn filter(self, filter: &TicketStatus) -> Self {
226 self.status(&filter.to_string())
227 }
228
229 pub fn open(self) -> Self {
231 self.status("Open")
232 }
233
234 pub fn closed(self) -> Self {
236 self.status("Closed")
237 }
238
239 pub fn created_after(mut self, time: DateTime<Local>) -> Self {
241 self.children.push(Criteria {
242 field: "created_time".to_string(),
243 condition: Condition::GreaterThan,
244 value: time.timestamp_millis().to_string().into(),
245 children: vec![],
246 logical_operator: Some(LogicalOp::And),
247 });
248 self
249 }
250
251 pub fn updated_after(mut self, time: DateTime<Local>) -> Self {
253 self.children.push(Criteria {
254 field: "last_updated_time".to_string(),
255 condition: Condition::GreaterThan,
256 value: time.timestamp_millis().to_string().into(),
257 children: vec![],
258 logical_operator: Some(LogicalOp::And),
259 });
260 self
261 }
262
263 pub fn subject_contains(mut self, value: &str) -> Self {
265 self.children.push(Criteria {
266 field: "subject".to_string(),
267 condition: Condition::Contains,
268 value: value.into(),
269 children: vec![],
270 logical_operator: Some(LogicalOp::And),
271 });
272 self
273 }
274
275 pub fn field_contains(mut self, field: &str, value: impl Into<Value>) -> Self {
277 self.children.push(Criteria {
278 field: field.to_string(),
279 condition: Condition::Contains,
280 value: value.into(),
281 children: vec![],
282 logical_operator: Some(LogicalOp::And),
283 });
284 self
285 }
286
287 pub fn field_equals(mut self, field: &str, value: impl Into<Value>) -> Self {
289 self.children.push(Criteria {
290 field: field.to_string(),
291 condition: Condition::Is,
292 value: value.into(),
293 children: vec![],
294 logical_operator: Some(LogicalOp::And),
295 });
296 self
297 }
298
299 pub fn limit(mut self, count: u32) -> Self {
301 self.row_count = count;
302 self
303 }
304
305 pub fn criteria(mut self, criteria: Criteria) -> Self {
307 if self.root_criteria.is_none() {
308 self.root_criteria = Some(criteria);
309 } else {
310 self.children.push(criteria);
311 }
312 self
313 }
314
315 pub async fn fetch(self) -> Result<Vec<DetailedTicket>, Error> {
317 let mut root = self.root_criteria.unwrap_or_else(|| Criteria {
318 field: "id".to_string(),
319 condition: Condition::GreaterThan,
320 value: "0".into(),
321 children: vec![],
322 logical_operator: None,
323 });
324
325 root.children = self.children;
326
327 let body = SearchRequest {
328 list_info: ListInfo {
329 row_count: self.row_count,
330 search_criteria: root,
331 },
332 };
333
334 let resp: Value = self
335 .client
336 .request_input_data(Method::GET, "/api/v3/requests", &body)
337 .await?;
338
339 let ticket_response: TicketSearchResponse = serde_json::from_value(resp)?;
340 Ok(ticket_response.requests)
341 }
342
343 pub async fn first(mut self) -> Result<Option<DetailedTicket>, Error> {
345 self.row_count = 1;
346 let results = self.fetch().await?;
347 Ok(results.into_iter().next())
348 }
349}
350
351pub struct TicketCreateBuilder<'a> {
356 client: &'a ServiceDesk,
357 subject: Option<String>,
358 description: Option<String>,
359 requester: Option<String>,
360 priority: String,
361 account: Option<String>,
362 template: Option<String>,
363 udf_fields: Option<Value>,
364}
365
366impl<'a> TicketCreateBuilder<'a> {
367 pub fn subject(mut self, subject: impl Into<String>) -> Self {
369 self.subject = Some(subject.into());
370 self
371 }
372
373 pub fn description(mut self, description: impl Into<String>) -> Self {
375 self.description = Some(description.into());
376 self
377 }
378
379 pub fn requester(mut self, requester: impl Into<String>) -> Self {
381 self.requester = Some(requester.into());
382 self
383 }
384
385 pub fn priority(mut self, priority: impl Into<String>) -> Self {
387 self.priority = priority.into();
388 self
389 }
390
391 pub fn account(mut self, account: impl Into<String>) -> Self {
393 self.account = Some(account.into());
394 self
395 }
396
397 pub fn template(mut self, template: impl Into<String>) -> Self {
399 self.template = Some(template.into());
400 self
401 }
402
403 pub fn udf_fields(mut self, fields: Value) -> Self {
405 self.udf_fields = Some(fields);
406 self
407 }
408
409 pub async fn send(self) -> Result<TicketData, Error> {
411 let subject = self
412 .subject
413 .ok_or_else(|| Error::Other("subject is required".to_string()))?;
414 let requester = self
415 .requester
416 .ok_or_else(|| Error::Other("requester is required".to_string()))?;
417
418 let data = CreateTicketData {
419 subject,
420 description: self.description.unwrap_or_default(),
421 requester,
422 priority: self.priority,
423 account: self.account.unwrap_or_default(),
424 template: self.template.unwrap_or_default(),
425 udf_fields: self.udf_fields.unwrap_or(serde_json::json!({})),
426 };
427
428 self.client.create_ticket(&data).await
429 }
430}
431
432pub struct NoteBuilder<'a> {
436 client: &'a ServiceDesk,
437 ticket_id: TicketID,
438 description: String,
439 mark_first_response: bool,
440 add_to_linked_requests: bool,
441 notify_technician: bool,
442 show_to_requester: bool,
443}
444
445impl<'a> NoteBuilder<'a> {
446 pub fn description(mut self, description: impl Into<String>) -> Self {
448 self.description = description.into();
449 self
450 }
451
452 pub fn mark_first_response(mut self) -> Self {
454 self.mark_first_response = true;
455 self
456 }
457
458 pub fn add_to_linked_requests(mut self) -> Self {
460 self.add_to_linked_requests = true;
461 self
462 }
463
464 pub fn notify_technician(mut self) -> Self {
466 self.notify_technician = true;
467 self
468 }
469
470 pub fn show_to_requester(mut self) -> Self {
472 self.show_to_requester = true;
473 self
474 }
475
476 pub async fn send(self) -> Result<Note, Error> {
478 let note = NoteData {
479 description: self.description,
480 mark_first_response: self.mark_first_response,
481 add_to_linked_requests: self.add_to_linked_requests,
482 notify_technician: self.notify_technician,
483 show_to_requester: self.show_to_requester,
484 };
485
486 let note = self.client.add_note(self.ticket_id, ¬e).await?;
487 Ok(note)
488 }
489}
490
491impl ServiceDesk {
492 pub fn tickets(&self) -> TicketsClient<'_> {
494 TicketsClient { client: self }
495 }
496
497 pub fn ticket(&self, id: impl Into<TicketID>) -> TicketClient<'_> {
499 TicketClient {
500 client: self,
501 id: id.into(),
502 }
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn ticket_status_display() {
512 assert_eq!(TicketStatus::Open.to_string(), "Open");
513 assert_eq!(TicketStatus::Closed.to_string(), "Closed");
514 assert_eq!(TicketStatus::Cancelled.to_string(), "Cancelled");
515 assert_eq!(TicketStatus::OnHold.to_string(), "On Hold");
516 }
517}