1use std::path::Path;
35
36use chrono::{DateTime, Local};
37use reqwest::Method;
38use serde::{Deserialize, Serialize};
39use serde_json::Value;
40
41use crate::{
42 Priority, ServiceDesk, TicketID, UserInfo,
43 client::{
44 Condition, CreateTicketData, Criteria, DetailedTicket, EditTicketData, ListInfo, LogicalOp,
45 Note, NoteData, SearchRequest, TicketData, TicketSearchResponse,
46 },
47 error::Error,
48};
49
50pub struct TicketsClient<'a> {
52 pub(crate) client: &'a ServiceDesk,
53}
54
55impl<'a> TicketsClient<'a> {
56 #[must_use]
58 pub fn search(self) -> TicketSearchBuilder<'a> {
59 TicketSearchBuilder {
60 client: self.client,
61 root_criteria: None,
62 children: vec![],
63 row_count: 100,
64 }
65 }
66
67 #[must_use]
69 pub fn create(self) -> TicketCreateBuilder<'a> {
70 TicketCreateBuilder {
71 client: self.client,
72 subject: None,
73 description: None,
74 requester: None,
75 priority: Priority::low(),
76 account: None,
77 template: None,
78 udf_fields: None,
79 }
80 }
81}
82
83pub struct TicketClient<'a> {
85 pub(crate) client: &'a ServiceDesk,
86 pub(crate) id: TicketID,
87}
88
89impl<'a> TicketClient<'a> {
90 pub async fn get(&self) -> Result<DetailedTicket, Error> {
92 self.client.ticket_details(self.id).await
93 }
94
95 pub async fn close(&self, comment: &str) -> Result<(), Error> {
97 self.client.close_ticket(self.id, comment).await
98 }
99
100 pub async fn assign(&self, technician: &str) -> Result<(), Error> {
102 self.client.assign_ticket(self.id, technician).await
103 }
104
105 pub async fn conversations(&self) -> Result<Value, Error> {
106 self.client.get_conversations(self.id).await
107 }
108
109 pub async fn conversation_content(&self, content_url: &str) -> Result<Value, Error> {
110 self.client.get_conversation_content(content_url).await
111 }
112
113 pub async fn add_attachment(&self, file_path: impl AsRef<Path>) -> Result<(), Error> {
114 self.client.add_attachment(self.id, file_path).await
115 }
116
117 pub async fn all_attachment_links(&self) -> Result<Vec<String>, Error> {
120 let ticket = self.client.ticket(self.id).get().await?;
121 let mut links = Vec::new();
122 if let Some(attachments) = ticket.attachments {
123 for attachment in attachments {
124 links.push(format!(
125 "{}{}",
126 self.client.base_url, attachment.content_url
127 ));
128 }
129 }
130 if let Ok(attachments) = self.client.get_conversation_attachment_urls(self.id).await {
131 for url in attachments {
132 links.push(url);
133 }
134 }
135 Ok(links)
136 }
137
138 pub async fn add_note(&self, description: &str) -> Result<Note, Error> {
140 self.client
141 .add_note(
142 self.id,
143 &NoteData {
144 description: description.to_string(),
145 ..Default::default()
146 },
147 )
148 .await
149 }
150
151 pub async fn add_worklog(&self, worklog: &WorklogData) -> Result<Value, Error> {
152 self.client.add_worklog(self.id, worklog).await
153 }
154
155 #[must_use]
157 pub fn note(&self) -> NoteBuilder<'a> {
158 NoteBuilder {
159 client: self.client,
160 id: self.id,
161 description: String::new(),
162 mark_first_response: false,
163 add_to_linked_requests: false,
164 notify_technician: false,
165 show_to_requester: false,
166 }
167 }
168
169 #[must_use]
171 pub fn worklog(&self) -> WorklogBuilder<'a> {
172 WorklogBuilder {
173 client: self.client,
174 id: self.id,
175 owner: None,
176 description: None,
177 start_time: None,
178 end_time: None,
179 exchange_rate: None,
180 mark_first_response: None,
181 include_nonoperational_hours: None,
182 }
183 }
184
185 pub async fn merge(&self, ticket_ids: &[TicketID]) -> Result<(), Error> {
187 self.client.merge(self.id, ticket_ids).await
188 }
189
190 pub async fn merged_ticket_ids(&self) -> Result<Vec<TicketID>, Error> {
192 self.client.merged_ticket_ids(self.id).await
193 }
194
195 pub async fn edit(&self, data: &EditTicketData) -> Result<(), Error> {
197 self.client.edit(self.id, data).await
198 }
199
200 pub async fn close_with_note(&self, comment: &str) -> Result<(), Error> {
202 self.client
203 .add_note(
204 self.id,
205 &NoteData {
206 description: comment.to_string(),
207 ..Default::default()
208 },
209 )
210 .await?;
211 self.client.close_ticket(self.id, comment).await
212 }
213}
214
215pub struct TicketSearchBuilder<'a> {
219 client: &'a ServiceDesk,
220 root_criteria: Option<Criteria>,
221 children: Vec<Criteria>,
222 row_count: u32,
223}
224
225#[derive(Debug, PartialEq, Eq)]
227pub enum TicketStatus {
228 Open,
229 Closed,
230 Cancelled,
231 OnHold,
232}
233
234impl std::fmt::Display for TicketStatus {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 let status_str = match self {
237 TicketStatus::Open => "Open",
238 TicketStatus::Closed => "Closed",
239 TicketStatus::Cancelled => "Cancelled",
240 TicketStatus::OnHold => "On Hold",
241 };
242 write!(f, "{status_str}")
243 }
244}
245
246impl TicketSearchBuilder<'_> {
247 #[must_use]
249 pub fn status(mut self, status: &str) -> Self {
250 self.root_criteria = Some(Criteria {
251 field: "status.name".to_string(),
252 condition: Condition::Is,
253 value: status.into(),
254 children: vec![],
255 logical_operator: None,
256 });
257 self
258 }
259
260 #[must_use]
262 pub fn filter(self, filter: &TicketStatus) -> Self {
263 self.status(&filter.to_string())
264 }
265
266 #[must_use]
268 pub fn open(self) -> Self {
269 self.status("Open")
270 }
271
272 #[must_use]
274 pub fn closed(self) -> Self {
275 self.status("Closed")
276 }
277
278 #[must_use]
280 pub fn created_after(mut self, time: DateTime<Local>) -> Self {
281 self.children.push(Criteria {
282 field: "created_time".to_string(),
283 condition: Condition::GreaterThan,
284 value: time.timestamp_millis().to_string().into(),
285 children: vec![],
286 logical_operator: Some(LogicalOp::And),
287 });
288 self
289 }
290
291 #[must_use]
293 pub fn updated_after(mut self, time: DateTime<Local>) -> Self {
294 self.children.push(Criteria {
295 field: "last_updated_time".to_string(),
296 condition: Condition::GreaterThan,
297 value: time.timestamp_millis().to_string().into(),
298 children: vec![],
299 logical_operator: Some(LogicalOp::And),
300 });
301 self
302 }
303
304 #[must_use]
306 pub fn subject_contains(mut self, value: &str) -> Self {
307 self.children.push(Criteria {
308 field: "subject".to_string(),
309 condition: Condition::Contains,
310 value: value.into(),
311 children: vec![],
312 logical_operator: Some(LogicalOp::And),
313 });
314 self
315 }
316
317 pub fn field_contains(mut self, field: &str, value: impl Into<Value>) -> Self {
319 self.children.push(Criteria {
320 field: field.to_string(),
321 condition: Condition::Contains,
322 value: value.into(),
323 children: vec![],
324 logical_operator: Some(LogicalOp::And),
325 });
326 self
327 }
328
329 pub fn field_equals(mut self, field: &str, value: impl Into<Value>) -> Self {
331 self.children.push(Criteria {
332 field: field.to_string(),
333 condition: Condition::Is,
334 value: value.into(),
335 children: vec![],
336 logical_operator: Some(LogicalOp::And),
337 });
338 self
339 }
340
341 #[must_use]
343 pub fn limit(mut self, count: u32) -> Self {
344 self.row_count = count;
345 self
346 }
347
348 #[must_use]
350 pub fn criteria(mut self, criteria: Criteria) -> Self {
351 if self.root_criteria.is_none() {
352 self.root_criteria = Some(criteria);
353 } else {
354 self.children.push(criteria);
355 }
356 self
357 }
358
359 pub async fn fetch(self) -> Result<Vec<DetailedTicket>, Error> {
361 let mut root = self.root_criteria.unwrap_or_else(|| Criteria {
362 field: "id".to_string(),
363 condition: Condition::GreaterThan,
364 value: "0".into(),
365 children: vec![],
366 logical_operator: None,
367 });
368
369 root.children = self.children;
370
371 let body = SearchRequest {
372 list_info: ListInfo {
373 row_count: self.row_count,
374 search_criteria: root,
375 },
376 };
377
378 let resp: Value = self
379 .client
380 .request_input_data(Method::GET, "/api/v3/requests", &body)
381 .await?;
382
383 let ticket_response: TicketSearchResponse = serde_json::from_value(resp)?;
384 Ok(ticket_response.requests)
385 }
386
387 pub async fn first(mut self) -> Result<Option<DetailedTicket>, Error> {
389 self.row_count = 1;
390 let results = self.fetch().await?;
391 Ok(results.into_iter().next())
392 }
393}
394
395pub struct TicketCreateBuilder<'a> {
400 client: &'a ServiceDesk,
401 subject: Option<String>,
402 description: Option<String>,
403 requester: Option<String>,
404 priority: Priority,
405 account: Option<String>,
406 template: Option<String>,
407 udf_fields: Option<Value>,
408}
409
410impl TicketCreateBuilder<'_> {
411 pub fn subject(mut self, subject: impl Into<String>) -> Self {
413 self.subject = Some(subject.into());
414 self
415 }
416
417 pub fn description(mut self, description: impl Into<String>) -> Self {
419 self.description = Some(description.into());
420 self
421 }
422
423 pub fn requester(mut self, requester: impl Into<String>) -> Self {
425 self.requester = Some(requester.into());
426 self
427 }
428
429 #[must_use]
431 pub fn priority(mut self, priority: Priority) -> Self {
432 self.priority = priority;
433 self
434 }
435
436 pub fn account(mut self, account: impl Into<String>) -> Self {
438 self.account = Some(account.into());
439 self
440 }
441
442 pub fn template(mut self, template: impl Into<String>) -> Self {
444 self.template = Some(template.into());
445 self
446 }
447
448 #[must_use]
450 pub fn udf_fields(mut self, fields: Value) -> Self {
451 self.udf_fields = Some(fields);
452 self
453 }
454
455 pub async fn send(self) -> Result<TicketData, Error> {
457 let subject = self
458 .subject
459 .ok_or_else(|| Error::Other("subject is required".to_string()))?;
460 let requester = self
461 .requester
462 .ok_or_else(|| Error::Other("requester is required".to_string()))?;
463
464 let data = CreateTicketData {
465 subject,
466 description: self.description.unwrap_or_default(),
467 requester,
468 priority: self.priority,
469 account: self.account.unwrap_or_default(),
470 template: self.template.unwrap_or_default(),
471 udf_fields: self.udf_fields.unwrap_or(serde_json::json!({})),
472 };
473
474 self.client.create_ticket(&data).await
475 }
476}
477
478pub struct NoteBuilder<'a> {
482 client: &'a ServiceDesk,
483 id: TicketID,
484 description: String,
485 mark_first_response: bool,
486 add_to_linked_requests: bool,
487 notify_technician: bool,
488 show_to_requester: bool,
489}
490
491impl NoteBuilder<'_> {
492 pub fn description(mut self, description: impl Into<String>) -> Self {
494 self.description = description.into();
495 self
496 }
497
498 #[must_use]
500 pub fn mark_first_response(mut self) -> Self {
501 self.mark_first_response = true;
502 self
503 }
504
505 #[must_use]
507 pub fn add_to_linked_requests(mut self) -> Self {
508 self.add_to_linked_requests = true;
509 self
510 }
511
512 #[must_use]
514 pub fn notify_technician(mut self) -> Self {
515 self.notify_technician = true;
516 self
517 }
518
519 #[must_use]
521 pub fn show_to_requester(mut self) -> Self {
522 self.show_to_requester = true;
523 self
524 }
525
526 #[must_use]
528 pub fn build(self) -> NoteData {
529 NoteData {
530 description: self.description,
531 mark_first_response: self.mark_first_response,
532 add_to_linked_requests: self.add_to_linked_requests,
533 notify_technician: self.notify_technician,
534 show_to_requester: self.show_to_requester,
535 }
536 }
537
538 pub async fn send(self) -> Result<Note, Error> {
540 let client = self.client;
541 let id = self.id;
542 let note = self.build();
543 client.add_note(id, ¬e).await
544 }
545}
546
547#[derive(Debug, Serialize, Deserialize)]
548pub struct WorklogData {
549 owner: UserInfo,
550 description: String,
551 #[serde(serialize_with = "serialize_sdp_time")]
552 start_time: DateTime<Local>,
553 #[serde(serialize_with = "serialize_sdp_time")]
554 end_time: DateTime<Local>,
555 #[serde(skip_serializing_if = "Option::is_none")]
556 exchange_rate: Option<f64>,
557 mark_first_response: bool,
558 include_nonoperational_hours: bool,
559}
560
561fn serialize_sdp_time<S>(dt: &DateTime<Local>, serializer: S) -> Result<S::Ok, S::Error>
562where
563 S: serde::Serializer,
564{
565 use serde::ser::SerializeStruct;
566 let mut s = serializer.serialize_struct("SdpTime", 1)?;
567 s.serialize_field("value", &dt.timestamp_millis())?;
568 s.end()
569}
570
571pub struct WorklogBuilder<'a> {
572 client: &'a ServiceDesk,
573 id: TicketID,
574 owner: Option<UserInfo>,
575 description: Option<String>,
576 start_time: Option<DateTime<Local>>,
577 end_time: Option<DateTime<Local>>,
578 exchange_rate: Option<f64>,
579 mark_first_response: Option<bool>,
580 include_nonoperational_hours: Option<bool>,
581}
582
583impl WorklogBuilder<'_> {
584 #[must_use]
585 pub fn owner(mut self, owner: UserInfo) -> Self {
586 self.owner = Some(owner);
587 self
588 }
589
590 pub fn description(mut self, description: impl Into<String>) -> Self {
592 self.description = Some(description.into());
593 self
594 }
595
596 #[must_use]
598 pub fn start_time(mut self, start_time: DateTime<Local>) -> Self {
599 self.start_time = Some(start_time);
600 self
601 }
602
603 #[must_use]
605 pub fn end_time(mut self, end_time: DateTime<Local>) -> Self {
606 self.end_time = Some(end_time);
607 self
608 }
609
610 #[must_use]
612 pub fn exchange_rate(mut self, exchange_rate: f64) -> Self {
613 self.exchange_rate = Some(exchange_rate);
614 self
615 }
616
617 #[must_use]
619 pub fn mark_first_response(mut self) -> Self {
620 self.mark_first_response = Some(true);
621 self
622 }
623
624 #[must_use]
626 pub fn include_nonoperational_hours(mut self) -> Self {
627 self.include_nonoperational_hours = Some(true);
628 self
629 }
630
631 pub fn build(self) -> Result<WorklogData, Error> {
633 Ok(WorklogData {
634 owner: self
635 .owner
636 .ok_or_else(|| Error::FieldRequired("owner".to_string()))?,
637 description: self.description.unwrap_or_default(),
638 start_time: self.start_time.unwrap_or_else(Local::now),
639 end_time: self.end_time.unwrap_or_else(Local::now),
640 exchange_rate: self.exchange_rate,
641 mark_first_response: self.mark_first_response.unwrap_or(false),
642 include_nonoperational_hours: self.include_nonoperational_hours.unwrap_or(false),
643 })
644 }
645
646 pub async fn send(self) -> Result<Value, Error> {
648 let client = self.client;
649 let id = self.id;
650 let worklog = self.build()?;
651 client.add_worklog(id, &worklog).await
652 }
653}
654
655impl ServiceDesk {
656 #[must_use]
658 pub fn tickets(&self) -> TicketsClient<'_> {
659 TicketsClient { client: self }
660 }
661
662 pub fn ticket(&self, id: impl Into<TicketID>) -> TicketClient<'_> {
664 TicketClient {
665 client: self,
666 id: id.into(),
667 }
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674
675 #[test]
676 fn ticket_status_display() {
677 assert_eq!(TicketStatus::Open.to_string(), "Open");
678 assert_eq!(TicketStatus::Closed.to_string(), "Closed");
679 assert_eq!(TicketStatus::Cancelled.to_string(), "Cancelled");
680 assert_eq!(TicketStatus::OnHold.to_string(), "On Hold");
681 }
682}