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 merged_ticket_ids(&self) -> Result<Vec<TicketID>, Error> {
162 self.client.merged_ticket_ids(self.id).await
163 }
164
165 pub async fn edit(&self, data: &EditTicketData) -> Result<(), Error> {
167 self.client.edit(self.id, data).await
168 }
169
170 pub async fn close_with_note(&self, comment: &str) -> Result<(), Error> {
172 self.client
173 .add_note(
174 self.id,
175 &NoteData {
176 description: comment.to_string(),
177 ..Default::default()
178 },
179 )
180 .await?;
181 self.client.close_ticket(self.id, comment).await
182 }
183}
184
185pub struct TicketSearchBuilder<'a> {
189 client: &'a ServiceDesk,
190 root_criteria: Option<Criteria>,
191 children: Vec<Criteria>,
192 row_count: u32,
193}
194
195#[derive(Debug, PartialEq, Eq)]
197pub enum TicketStatus {
198 Open,
199 Closed,
200 Cancelled,
201 OnHold,
202}
203
204impl std::fmt::Display for TicketStatus {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 let status_str = match self {
207 TicketStatus::Open => "Open",
208 TicketStatus::Closed => "Closed",
209 TicketStatus::Cancelled => "Cancelled",
210 TicketStatus::OnHold => "On Hold",
211 };
212 write!(f, "{}", status_str)
213 }
214}
215
216impl<'a> TicketSearchBuilder<'a> {
217 pub fn status(mut self, status: &str) -> Self {
219 self.root_criteria = Some(Criteria {
220 field: "status.name".to_string(),
221 condition: Condition::Is,
222 value: status.into(),
223 children: vec![],
224 logical_operator: None,
225 });
226 self
227 }
228
229 pub fn filter(self, filter: &TicketStatus) -> Self {
231 self.status(&filter.to_string())
232 }
233
234 pub fn open(self) -> Self {
236 self.status("Open")
237 }
238
239 pub fn closed(self) -> Self {
241 self.status("Closed")
242 }
243
244 pub fn created_after(mut self, time: DateTime<Local>) -> Self {
246 self.children.push(Criteria {
247 field: "created_time".to_string(),
248 condition: Condition::GreaterThan,
249 value: time.timestamp_millis().to_string().into(),
250 children: vec![],
251 logical_operator: Some(LogicalOp::And),
252 });
253 self
254 }
255
256 pub fn updated_after(mut self, time: DateTime<Local>) -> Self {
258 self.children.push(Criteria {
259 field: "last_updated_time".to_string(),
260 condition: Condition::GreaterThan,
261 value: time.timestamp_millis().to_string().into(),
262 children: vec![],
263 logical_operator: Some(LogicalOp::And),
264 });
265 self
266 }
267
268 pub fn subject_contains(mut self, value: &str) -> Self {
270 self.children.push(Criteria {
271 field: "subject".to_string(),
272 condition: Condition::Contains,
273 value: value.into(),
274 children: vec![],
275 logical_operator: Some(LogicalOp::And),
276 });
277 self
278 }
279
280 pub fn field_contains(mut self, field: &str, value: impl Into<Value>) -> Self {
282 self.children.push(Criteria {
283 field: field.to_string(),
284 condition: Condition::Contains,
285 value: value.into(),
286 children: vec![],
287 logical_operator: Some(LogicalOp::And),
288 });
289 self
290 }
291
292 pub fn field_equals(mut self, field: &str, value: impl Into<Value>) -> Self {
294 self.children.push(Criteria {
295 field: field.to_string(),
296 condition: Condition::Is,
297 value: value.into(),
298 children: vec![],
299 logical_operator: Some(LogicalOp::And),
300 });
301 self
302 }
303
304 pub fn limit(mut self, count: u32) -> Self {
306 self.row_count = count;
307 self
308 }
309
310 pub fn criteria(mut self, criteria: Criteria) -> Self {
312 if self.root_criteria.is_none() {
313 self.root_criteria = Some(criteria);
314 } else {
315 self.children.push(criteria);
316 }
317 self
318 }
319
320 pub async fn fetch(self) -> Result<Vec<DetailedTicket>, Error> {
322 let mut root = self.root_criteria.unwrap_or_else(|| Criteria {
323 field: "id".to_string(),
324 condition: Condition::GreaterThan,
325 value: "0".into(),
326 children: vec![],
327 logical_operator: None,
328 });
329
330 root.children = self.children;
331
332 let body = SearchRequest {
333 list_info: ListInfo {
334 row_count: self.row_count,
335 search_criteria: root,
336 },
337 };
338
339 let resp: Value = self
340 .client
341 .request_input_data(Method::GET, "/api/v3/requests", &body)
342 .await?;
343
344 let ticket_response: TicketSearchResponse = serde_json::from_value(resp)?;
345 Ok(ticket_response.requests)
346 }
347
348 pub async fn first(mut self) -> Result<Option<DetailedTicket>, Error> {
350 self.row_count = 1;
351 let results = self.fetch().await?;
352 Ok(results.into_iter().next())
353 }
354}
355
356pub struct TicketCreateBuilder<'a> {
361 client: &'a ServiceDesk,
362 subject: Option<String>,
363 description: Option<String>,
364 requester: Option<String>,
365 priority: String,
366 account: Option<String>,
367 template: Option<String>,
368 udf_fields: Option<Value>,
369}
370
371impl<'a> TicketCreateBuilder<'a> {
372 pub fn subject(mut self, subject: impl Into<String>) -> Self {
374 self.subject = Some(subject.into());
375 self
376 }
377
378 pub fn description(mut self, description: impl Into<String>) -> Self {
380 self.description = Some(description.into());
381 self
382 }
383
384 pub fn requester(mut self, requester: impl Into<String>) -> Self {
386 self.requester = Some(requester.into());
387 self
388 }
389
390 pub fn priority(mut self, priority: impl Into<String>) -> Self {
392 self.priority = priority.into();
393 self
394 }
395
396 pub fn account(mut self, account: impl Into<String>) -> Self {
398 self.account = Some(account.into());
399 self
400 }
401
402 pub fn template(mut self, template: impl Into<String>) -> Self {
404 self.template = Some(template.into());
405 self
406 }
407
408 pub fn udf_fields(mut self, fields: Value) -> Self {
410 self.udf_fields = Some(fields);
411 self
412 }
413
414 pub async fn send(self) -> Result<TicketData, Error> {
416 let subject = self
417 .subject
418 .ok_or_else(|| Error::Other("subject is required".to_string()))?;
419 let requester = self
420 .requester
421 .ok_or_else(|| Error::Other("requester is required".to_string()))?;
422
423 let data = CreateTicketData {
424 subject,
425 description: self.description.unwrap_or_default(),
426 requester,
427 priority: self.priority,
428 account: self.account.unwrap_or_default(),
429 template: self.template.unwrap_or_default(),
430 udf_fields: self.udf_fields.unwrap_or(serde_json::json!({})),
431 };
432
433 self.client.create_ticket(&data).await
434 }
435}
436
437pub struct NoteBuilder<'a> {
441 client: &'a ServiceDesk,
442 ticket_id: TicketID,
443 description: String,
444 mark_first_response: bool,
445 add_to_linked_requests: bool,
446 notify_technician: bool,
447 show_to_requester: bool,
448}
449
450impl<'a> NoteBuilder<'a> {
451 pub fn description(mut self, description: impl Into<String>) -> Self {
453 self.description = description.into();
454 self
455 }
456
457 pub fn mark_first_response(mut self) -> Self {
459 self.mark_first_response = true;
460 self
461 }
462
463 pub fn add_to_linked_requests(mut self) -> Self {
465 self.add_to_linked_requests = true;
466 self
467 }
468
469 pub fn notify_technician(mut self) -> Self {
471 self.notify_technician = true;
472 self
473 }
474
475 pub fn show_to_requester(mut self) -> Self {
477 self.show_to_requester = true;
478 self
479 }
480
481 pub async fn send(self) -> Result<Note, Error> {
483 let note = NoteData {
484 description: self.description,
485 mark_first_response: self.mark_first_response,
486 add_to_linked_requests: self.add_to_linked_requests,
487 notify_technician: self.notify_technician,
488 show_to_requester: self.show_to_requester,
489 };
490
491 let note = self.client.add_note(self.ticket_id, ¬e).await?;
492 Ok(note)
493 }
494}
495
496impl ServiceDesk {
497 pub fn tickets(&self) -> TicketsClient<'_> {
499 TicketsClient { client: self }
500 }
501
502 pub fn ticket(&self, id: impl Into<TicketID>) -> TicketClient<'_> {
504 TicketClient {
505 client: self,
506 id: id.into(),
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 #[test]
516 fn ticket_status_display() {
517 assert_eq!(TicketStatus::Open.to_string(), "Open");
518 assert_eq!(TicketStatus::Closed.to_string(), "Closed");
519 assert_eq!(TicketStatus::Cancelled.to_string(), "Cancelled");
520 assert_eq!(TicketStatus::OnHold.to_string(), "On Hold");
521 }
522}