1use reqwest::Method;
2use serde::{Serialize, de::DeserializeOwned};
3use serde_aux::field_attributes::deserialize_number_from_string;
4
5#[derive(Debug, Serialize, Deserialize)]
6pub struct InnerResponseMessage {
7 status_code: u32,
8 #[serde(rename = "type")]
9 type_field: String,
10 message: String,
11}
12
13#[derive(Debug, Serialize, Deserialize)]
18pub struct SdpResponseStatus {
19 pub status_code: u32,
20 pub messages: Option<Vec<InnerResponseMessage>>,
21 pub status: String,
22}
23
24impl SdpResponseStatus {
25 pub fn into_error(self) -> Error {
27 if let Some(messages) = &self.messages
29 && let Some(msg) = messages.first()
30 {
31 return Error::from_sdp(msg.status_code, msg.message.clone(), None);
32 }
33 Error::from_sdp(self.status_code, self.status, None)
35 }
36}
37
38#[derive(Debug, Serialize, Deserialize)]
39struct SdpGenericResponse {
40 response_status: SdpResponseStatus,
41}
42
43impl ServiceDesk {
44 pub(crate) async fn request_json<T, R>(
45 &self,
46 method: Method,
47 path: &str,
48 body: &T,
49 ) -> Result<R, Error>
50 where
51 T: Serialize + ?Sized + std::fmt::Debug,
52 R: DeserializeOwned,
53 {
54 let url = self.base_url.join(path)?;
55 let request_builder = self.inner.request(method, url).json(body);
56
57 let response = self.inner.execute(request_builder.build()?).await?;
58 if response.error_for_status_ref().is_err() {
59 let error = response.json::<SdpGenericResponse>().await?;
60 tracing::error!(error = ?error, "SDP Error Response");
61 return Err(error.response_status.into_error());
62 }
63
64 let parsed = response.json::<R>().await?;
65 tracing::debug!("completed sdp request");
66 Ok(parsed)
67 }
68
69 pub(crate) async fn request_form<T, R>(
70 &self,
71 method: Method,
72 path: &str,
73 body: &T,
74 ) -> Result<R, Error>
75 where
76 T: Serialize + ?Sized + std::fmt::Debug,
77 R: DeserializeOwned,
78 {
79 let url = self.base_url.join(path)?;
80
81 let request_builder = self
82 .inner
83 .request(method, url)
84 .form(&[("input_data", serde_json::to_string(body)?)]);
85
86 let response = self.inner.execute(request_builder.build()?).await?;
87 if response.error_for_status_ref().is_err() {
88 let error = response.json::<SdpGenericResponse>().await?;
89 tracing::error!(error = ?error, "SDP Error Response");
90 return Err(error.response_status.into_error());
91 }
92
93 let parsed = response.json::<R>().await?;
94 tracing::debug!("completed sdp request");
95 Ok(parsed)
96 }
97
98 pub(crate) async fn request_input_data<T, R>(
99 &self,
100 method: Method,
101 path: &str,
102 body: &T,
103 ) -> Result<R, Error>
104 where
105 T: Serialize + ?Sized + std::fmt::Debug,
106 R: DeserializeOwned,
107 {
108 let url = self.base_url.join(path)?;
109
110 let request_builder = self
111 .inner
112 .request(method, url)
113 .header("Content-Type", "application/x-www-form-urlencoded")
114 .query(&[("input_data", serde_json::to_string(body)?)]);
115
116 let response = self.inner.execute(request_builder.build()?).await?;
117 if response.error_for_status_ref().is_err() {
118 let error = response.json::<SdpGenericResponse>().await?;
119 tracing::error!(error = ?error, "SDP Error Response");
120 return Err(error.response_status.into_error());
121 }
122 let result = response.json::<R>().await?;
123 tracing::debug!("completed sdp request");
124 Ok(result)
125 }
126
127 async fn request<T, R>(
128 &self,
129 method: Method,
130 path: &str,
131 path_parameter: &T,
132 ) -> Result<R, Error>
133 where
134 T: std::fmt::Display,
135 R: DeserializeOwned,
136 {
137 let url = self
138 .base_url
139 .join(path)?
140 .join(&path_parameter.to_string())?;
141
142 let request_builder = self.inner.request(method, url);
143 let response = self.inner.execute(request_builder.build()?).await?;
144 if response.error_for_status_ref().is_err() {
145 let error = response.json::<SdpGenericResponse>().await?;
146 tracing::error!(error = ?error, "SDP Error Response");
147 return Err(error.response_status.into_error());
148 }
149
150 let value = serde_json::to_string(&response.json::<Value>().await?)?;
151 let response: R = serde_json::from_str(&value)?;
152 tracing::debug!("completed sdp request");
153 Ok(response)
154 }
155
156 pub async fn ticket_details(
157 &self,
158 ticket_id: impl Into<TicketID>,
159 ) -> Result<DetailedTicket, Error> {
160 let ticket_id = ticket_id.into();
161 tracing::info!(ticket_id = %ticket_id, "fetching ticket details");
162 let resp: DetailedTicketResponse = self
163 .request(Method::GET, "/api/v3/requests/", &ticket_id)
164 .await?;
165 Ok(resp.request)
166 }
167
168 pub async fn edit(
173 &self,
174 ticket_id: impl Into<TicketID>,
175 data: &EditTicketData,
176 ) -> Result<(), Error> {
177 let ticket_id = ticket_id.into();
178 tracing::info!(ticket_id = %ticket_id, "editing ticket");
179 let _: SdpGenericResponse = self
180 .request_input_data(
181 Method::PUT,
182 &format!("/api/v3/requests/{}", ticket_id),
183 &EditTicketRequest { request: data },
184 )
185 .await?;
186 Ok(())
187 }
188
189 pub async fn add_note(
191 &self,
192 ticket_id: impl Into<TicketID>,
193 note: &NoteData,
194 ) -> Result<Note, Error> {
195 let ticket_id = ticket_id.into();
196 tracing::info!(ticket_id = %ticket_id, "adding note");
197 let resp: NoteResponse = self
198 .request_input_data(
199 Method::POST,
200 &format!("/api/v3/requests/{}/notes", ticket_id),
201 &AddNoteRequest { note },
202 )
203 .await?;
204 Ok(resp.note)
205 }
206
207 pub async fn get_note(
209 &self,
210 ticket_id: impl Into<TicketID>,
211 note_id: impl Into<NoteID>,
212 ) -> Result<Note, Error> {
213 let ticket_id = ticket_id.into();
214 let note_id = note_id.into();
215 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "fetching note");
216 let url = format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id);
217 let resp: NoteResponse = self.request(Method::GET, &url, &"").await?;
218 Ok(resp.note)
219 }
220
221 pub async fn list_notes(
223 &self,
224 ticket_id: impl Into<TicketID>,
225 row_count: Option<u32>,
226 start_index: Option<u32>,
227 ) -> Result<Vec<Note>, Error> {
228 let ticket_id = ticket_id.into();
229 tracing::info!(ticket_id = %ticket_id, "listing notes");
230 let body = ListNotesRequest {
231 list_info: NotesListInfo {
232 row_count: row_count.unwrap_or(100),
233 start_index: start_index.unwrap_or(1),
234 },
235 };
236 let resp: Value = self
237 .request_input_data(
238 Method::GET,
239 &format!("/api/v3/requests/{}/notes", ticket_id),
240 &body,
241 )
242 .await?;
243 let resp: NotesListResponse = serde_json::from_value(resp)?;
244 Ok(resp.notes)
245 }
246
247 pub async fn edit_note(
249 &self,
250 ticket_id: impl Into<TicketID>,
251 note_id: impl Into<NoteID>,
252 note: &NoteData,
253 ) -> Result<Note, Error> {
254 let ticket_id = ticket_id.into();
255 let note_id = note_id.into();
256 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "editing note");
257 let resp: NoteResponse = self
258 .request_input_data(
259 Method::PUT,
260 &format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id),
261 &EditNoteRequest { request_note: note },
262 )
263 .await?;
264 Ok(resp.note)
265 }
266
267 pub async fn delete_note(
269 &self,
270 ticket_id: impl Into<TicketID>,
271 note_id: impl Into<NoteID>,
272 ) -> Result<(), Error> {
273 let ticket_id = ticket_id.into();
274 let note_id = note_id.into();
275 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "deleting note");
276 let _: SdpGenericResponse = self
277 .request(
278 Method::DELETE,
279 &format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id),
280 &"",
281 )
282 .await?;
283 Ok(())
284 }
285
286 pub async fn assign_ticket(
288 &self,
289 ticket_id: impl Into<TicketID>,
290 technician_name: &str,
291 ) -> Result<(), Error> {
292 let ticket_id = ticket_id.into();
293 tracing::info!(ticket_id = %ticket_id, technician = %technician_name, "assigning ticket");
294 let _: SdpGenericResponse = self
295 .request_input_data(
296 Method::PUT,
297 &format!("/api/v3/requests/{}/assign", ticket_id),
298 &AssignTicketRequest {
299 request: AssignTicketData {
300 technician: NameWrapper::new(technician_name),
301 },
302 },
303 )
304 .await?;
305 Ok(())
306 }
307
308 pub async fn create_ticket(&self, data: &CreateTicketData) -> Result<TicketResponse, Error> {
310 tracing::info!(subject = %data.subject, "creating ticket");
311 let resp = self
312 .request_input_data(
313 Method::POST,
314 "/api/v3/requests",
315 &CreateTicketRequest { request: data },
316 )
317 .await?;
318 Ok(resp)
319 }
320
321 pub async fn search_tickets(&self, criteria: Criteria) -> Result<Vec<DetailedTicket>, Error> {
327 tracing::info!("searching tickets");
328 let resp = self
329 .request_input_data(
330 Method::GET,
331 "/api/v3/requests",
332 &SearchRequest {
333 list_info: ListInfo {
334 row_count: 100,
335 search_criteria: criteria,
336 },
337 },
338 )
339 .await?;
340
341 let ticket_response: TicketSearchResponse = serde_json::from_value(resp)?;
342
343 Ok(ticket_response.requests)
344 }
345
346 pub async fn close_ticket(
348 &self,
349 ticket_id: impl Into<TicketID>,
350 closure_comments: &str,
351 ) -> Result<(), Error> {
352 let ticket_id = ticket_id.into();
353 tracing::info!(ticket_id = %ticket_id, "closing ticket");
354 let _: SdpGenericResponse = self
355 .request_json(
356 Method::PUT,
357 &format!("/api/v3/requests/{}/close", ticket_id),
358 &CloseTicketRequest {
359 request: CloseTicketData {
360 closure_info: ClosureInfo {
361 closure_comments: closure_comments.to_string(),
362 closure_code: "Closed".to_string(),
363 },
364 },
365 },
366 )
367 .await?;
368 Ok(())
369 }
370
371 pub async fn merge(&self, ticket_id: usize, merge_ids: &[usize]) -> Result<(), Error> {
375 tracing::info!(ticket_id = %ticket_id, count = merge_ids.len(), "merging tickets");
376 if merge_ids.len() > 49 {
377 tracing::warn!("attempted to merge more than 49 tickets");
378 return Err(Error::from_sdp(
379 400,
380 "Cannot merge more than 49 tickets at once".to_string(),
381 None,
382 ));
383 }
384 let merge_requests: Vec<MergeRequestId> = merge_ids
385 .iter()
386 .map(|id| MergeRequestId { id: id.to_string() })
387 .collect();
388
389 let _: SdpGenericResponse = self
390 .request_form(
391 Method::PUT,
392 &format!("/api/v3/requests/{}/merge_requests", ticket_id),
393 &MergeTicketsRequest { merge_requests },
394 )
395 .await?;
396 Ok(())
397 }
398}
399
400use serde::Deserialize;
401use serde_json::Value;
402
403use crate::{NoteID, ServiceDesk, TicketID, UserID, error::Error};
404
405#[derive(Deserialize, Serialize, Debug, Clone)]
406pub(crate) struct SearchRequest {
407 pub(crate) list_info: ListInfo,
408}
409
410#[derive(Deserialize, Serialize, Debug, Clone)]
411pub struct ListInfo {
412 pub row_count: u32,
413 pub search_criteria: Criteria,
414}
415
416#[derive(Deserialize, Serialize, Debug, Clone)]
421pub struct Criteria {
422 pub field: String,
423 pub condition: Condition,
424 pub value: Value,
425
426 #[serde(skip_serializing_if = "Vec::is_empty")]
427 pub children: Vec<Criteria>,
428
429 #[serde(skip_serializing_if = "Option::is_none")]
430 pub logical_operator: Option<LogicalOp>,
431}
432
433impl Default for Criteria {
434 fn default() -> Self {
435 Criteria {
436 field: String::new(),
437 condition: Condition::Is,
438 value: Value::Null,
439 children: vec![],
440 logical_operator: None,
441 }
442 }
443}
444
445#[derive(Deserialize, Serialize, Debug, Clone)]
448#[serde(rename_all = "snake_case")]
449pub enum Condition {
450 #[serde(rename = "is")]
451 Is,
452 #[serde(rename = "greater than")]
453 GreaterThan,
454 #[serde(rename = "lesser than")]
455 LesserThan,
456 #[serde(rename = "contains")]
457 Contains,
458}
459
460#[derive(Deserialize, Serialize, Debug, Clone)]
462pub enum LogicalOp {
463 #[serde(rename = "AND")]
464 And,
465 #[serde(rename = "OR")]
466 Or,
467}
468
469#[derive(Deserialize, Serialize, Debug)]
470pub struct TicketSearchResponse {
471 pub requests: Vec<DetailedTicket>,
472}
473
474#[derive(Deserialize, Serialize, Debug, Clone)]
475pub struct Account {
476 pub id: String,
477 pub name: String,
478}
479
480#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
481struct DetailedTicketResponse {
482 request: DetailedTicket,
483 #[serde(skip_serializing)]
484 response_status: ResponseStatus,
485}
486
487#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
488#[serde(rename = "request")]
489pub struct DetailedTicket {
490 pub id: String,
491 pub subject: String,
492 pub description: Option<String>,
493 pub status: Status,
494 pub priority: Priority,
495 pub requester: Option<UserInfo>,
496 pub technician: Option<UserInfo>,
497 #[serde(skip_serializing)]
498 pub created_by: UserInfo,
499 pub created_time: TimeEntry,
500 pub resolution: Option<Resolution>,
501 pub due_by_time: Option<TimeEntry>,
502 pub resolved_time: Option<TimeEntry>,
503 pub completed_time: Option<TimeEntry>,
504
505 pub udf_fields: Option<Value>,
506
507 pub closure_info: Option<Value>,
508 pub site: Option<Value>,
509 pub department: Option<Value>,
510 pub account: Option<Value>,
511}
512
513#[derive(Serialize, Debug)]
514struct EditTicketRequest<'a> {
515 request: &'a EditTicketData,
516}
517
518#[derive(Serialize, Debug)]
519pub struct EditTicketData {
520 pub subject: String,
521 pub description: Option<String>,
522 pub requester: Option<NameWrapper>,
523 pub priority: Option<NameWrapper>,
524 pub udf_fields: Option<Value>,
525}
526
527impl From<DetailedTicket> for EditTicketData {
528 fn from(value: DetailedTicket) -> Self {
529 Self {
530 subject: value.subject,
531 description: Some(value.description.unwrap_or_default()),
532 requester: Some(NameWrapper::new(value.requester.unwrap_or_default().name)),
533 priority: Some(value.priority.name.into()),
534 udf_fields: value.udf_fields,
535 }
536 }
537}
538
539#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
540pub struct ResponseStatus {
541 pub status: String,
542 pub status_code: i64,
543}
544
545#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
546pub struct Status {
547 pub id: String,
548 pub name: String,
549 pub color: Option<String>,
550}
551
552#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
553pub struct Priority {
554 pub id: String,
555 pub name: String,
556 pub color: Option<String>,
557}
558
559#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
560pub struct UserInfo {
561 pub id: UserID,
562 pub name: String,
563 pub email_id: Option<String>,
564 pub account: Option<Value>,
565 pub department: Option<Value>,
566 #[serde(default)]
567 pub is_vipuser: bool,
568 pub mobile: Option<String>,
569 pub org_user_status: Option<String>,
570 pub phone: Option<String>,
571 #[serde(skip_serializing)]
572 pub profile_pic: Option<Value>,
573}
574
575#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
576pub struct Resolution {
577 pub content: String,
578 pub submitted_by: Option<UserInfo>,
579 pub submitted_on: Option<TimeEntry>,
580 pub resolution_attachments: Option<Vec<Attachment>>,
581}
582
583#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
584pub struct Attachment {
585 pub id: String,
586 pub name: String,
587 pub content_url: String,
588 pub content_type: Option<String>,
589 pub description: Option<String>,
590 pub module: Option<String>,
591 pub size: Option<SizeInfo>,
592 pub attached_by: Option<UserInfo>,
593 pub attached_on: Option<TimeEntry>,
594}
595
596#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
597pub struct SizeInfo {
598 pub display_value: String,
599 pub value: u64,
600}
601
602#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
603pub struct TimeEntry {
604 pub display_value: String,
605 pub value: String,
606}
607
608#[derive(Serialize, Debug)]
609struct CreateTicketRequest<'a> {
610 request: &'a CreateTicketData,
611}
612
613#[derive(Serialize, Debug)]
614pub struct CreateTicketData {
615 pub subject: String,
616 pub description: String,
617 pub requester: NameWrapper,
618 pub priority: NameWrapper,
619 pub udf_fields: Value,
623 pub account: NameWrapper,
624 pub template: NameWrapper,
625}
626
627impl Default for CreateTicketData {
628 fn default() -> Self {
629 CreateTicketData {
630 subject: String::new(),
631 description: String::new(),
632 requester: NameWrapper::new(""),
633 priority: NameWrapper::new("Low"),
634 udf_fields: Value::Null,
635 account: NameWrapper::new(""),
636 template: NameWrapper::new(""),
637 }
638 }
639}
640
641#[derive(Serialize, Debug)]
642pub struct NameWrapper {
643 pub name: String,
644}
645
646impl From<&str> for NameWrapper {
647 fn from(name: &str) -> Self {
648 Self {
649 name: name.to_string(),
650 }
651 }
652}
653impl From<String> for NameWrapper {
654 fn from(name: String) -> Self {
655 Self { name }
656 }
657}
658impl NameWrapper {
659 pub fn new(name: impl Into<String>) -> Self {
660 Self { name: name.into() }
661 }
662}
663
664impl std::ops::Deref for NameWrapper {
665 type Target = String;
666
667 fn deref(&self) -> &Self::Target {
668 &self.name
669 }
670}
671
672impl std::ops::DerefMut for NameWrapper {
673 fn deref_mut(&mut self) -> &mut Self::Target {
674 &mut self.name
675 }
676}
677
678#[derive(Serialize, Debug)]
679struct CloseTicketRequest {
680 request: CloseTicketData,
681}
682
683#[derive(Serialize, Debug)]
684struct CloseTicketData {
685 closure_info: ClosureInfo,
686}
687
688#[derive(Serialize, Debug)]
689struct ClosureInfo {
690 closure_comments: String,
691 closure_code: String,
692}
693
694#[derive(Serialize, Debug)]
695struct AddNoteRequest<'a> {
696 note: &'a NoteData,
697}
698
699#[derive(Serialize, Debug, Default)]
700pub struct NoteData {
701 pub mark_first_response: bool,
702 pub add_to_linked_requests: bool,
703 pub notify_technician: bool,
704 pub show_to_requester: bool,
705 pub description: String,
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize)]
710pub struct NoteResponse {
711 pub note: Note,
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
715pub struct NotesListResponse {
716 pub list_info: Option<ListInfoResponse>,
717 pub notes: Vec<Note>,
718 pub response_status: Vec<ResponseStatus>,
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize)]
722pub struct ListInfoResponse {
723 pub has_more_rows: bool,
724 pub page: u32,
725 pub row_count: u32,
726 pub sort_field: String,
727 pub sort_order: String,
728 pub start_index: u32,
729}
730
731#[derive(Debug, Clone, Serialize, Deserialize)]
732pub struct Note {
733 pub id: String,
734 #[serde(default)]
735 pub description: String,
736 #[serde(default)]
737 pub show_to_requester: bool,
738 #[serde(default)]
739 pub mark_first_response: bool,
740 #[serde(default)]
741 pub notify_technician: bool,
742 #[serde(default)]
743 pub add_to_linked_requests: bool,
744 pub created_time: Option<TimeEntry>,
745 pub created_by: Option<UserInfo>,
746 pub last_updated_time: Option<TimeEntry>,
747}
748
749#[derive(Serialize, Debug)]
750struct EditNoteRequest<'a> {
751 request_note: &'a NoteData,
752}
753
754#[derive(Serialize, Debug)]
755struct ListNotesRequest {
756 list_info: NotesListInfo,
757}
758
759#[derive(Serialize, Debug)]
760struct NotesListInfo {
761 row_count: u32,
762 start_index: u32,
763}
764
765#[derive(Serialize, Debug)]
766struct AssignTicketRequest {
767 request: AssignTicketData,
768}
769
770#[derive(Serialize, Debug)]
771struct AssignTicketData {
772 technician: NameWrapper,
773}
774
775#[derive(Serialize, Debug)]
776struct MergeTicketsRequest {
777 merge_requests: Vec<MergeRequestId>,
778}
779
780#[derive(Serialize, Debug)]
781struct MergeRequestId {
782 id: String,
783}
784
785#[derive(Debug, Clone, Serialize, Deserialize)]
786pub struct TicketResponse {
787 pub request: TicketData,
788 pub response_status: ResponseStatus,
789}
790
791#[derive(Debug, Clone, Serialize, Deserialize)]
792pub struct TicketData {
793 #[serde(deserialize_with = "deserialize_number_from_string")]
794 pub id: u64,
795 pub subject: String,
796 pub description: String,
797 pub status: Status,
798 pub priority: Priority,
799 pub created_time: TimeEntry,
800 pub requester: UserInfo,
801 pub account: Account,
802 pub template: TemplateInfo,
803 pub udf_fields: Option<Value>,
804}
805
806#[derive(Debug, Clone, Serialize, Deserialize)]
807pub struct TemplateInfo {
808 pub id: String,
809 pub name: String,
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 #[test]
817 fn criteria_default() {
818 let criteria = Criteria::default();
819 assert!(criteria.field.is_empty());
820 assert!(matches!(criteria.condition, Condition::Is));
821 assert!(criteria.value.is_null());
822 assert!(criteria.children.is_empty());
823 assert!(criteria.logical_operator.is_none());
824 }
825
826 #[test]
827 fn create_ticket_data_default() {
828 let data = CreateTicketData::default();
829 assert!(data.subject.is_empty());
830 assert!(data.description.is_empty());
831 assert!(data.requester.name.is_empty());
832 assert_eq!(data.priority.name, "Low");
833 assert!(data.udf_fields.is_null());
834 assert!(data.account.name.is_empty());
835 assert!(data.template.name.is_empty());
836 }
837}