1use std::collections::HashSet;
2
3use reqwest::Method;
4use serde::{Deserializer, Serialize, Serializer, de::DeserializeOwned, ser::SerializeStruct};
5
6#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
7pub struct InnerResponseMessage {
8 status_code: u32,
9 #[serde(rename = "type")]
10 type_field: String,
11 message: String,
12}
13
14#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
19pub struct SdpResponseStatus {
20 pub status_code: u32,
21 pub messages: Option<Vec<InnerResponseMessage>>,
22 pub status: String,
23}
24
25impl SdpResponseStatus {
26 pub fn into_error(self) -> Error {
28 if let Some(messages) = &self.messages
30 && let Some(msg) = messages.first()
31 {
32 return Error::from_sdp(msg.status_code, msg.message.clone(), None);
33 }
34 Error::from_sdp(self.status_code, self.status, None)
36 }
37}
38
39#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
40struct SdpGenericResponse {
41 response_status: SdpResponseStatus,
42}
43
44impl ServiceDesk {
45 pub(crate) async fn request_json<T, R>(
46 &self,
47 method: Method,
48 path: &str,
49 body: &T,
50 ) -> Result<R, Error>
51 where
52 T: Serialize + ?Sized + std::fmt::Debug,
53 R: DeserializeOwned,
54 {
55 let url = self.base_url.join(path)?;
56 let request_builder = self.inner.request(method, url).json(body);
57
58 let response = self.inner.execute(request_builder.build()?).await?;
59 if response.error_for_status_ref().is_err() {
60 let error = response.json::<SdpGenericResponse>().await?;
61 tracing::error!(error = ?error, "SDP Error Response");
62 return Err(error.response_status.into_error());
63 }
64
65 let parsed = response.json::<R>().await?;
66 tracing::debug!("completed sdp request");
67 Ok(parsed)
68 }
69
70 pub(crate) async fn request_form<T, R>(
71 &self,
72 method: Method,
73 path: &str,
74 body: &T,
75 ) -> Result<R, Error>
76 where
77 T: Serialize + ?Sized + std::fmt::Debug,
78 R: DeserializeOwned,
79 {
80 let url = self.base_url.join(path)?;
81
82 let request_builder = self
83 .inner
84 .request(method, url)
85 .form(&[("input_data", serde_json::to_string(body)?)]);
86
87 let response = self.inner.execute(request_builder.build()?).await?;
88 if response.error_for_status_ref().is_err() {
89 let error = response.json::<SdpGenericResponse>().await?;
90 tracing::error!(error = ?error, "SDP Error Response");
91 return Err(error.response_status.into_error());
92 }
93
94 let parsed = response.json::<R>().await?;
95 tracing::debug!("completed sdp request");
96 Ok(parsed)
97 }
98
99 pub(crate) async fn request_input_data<T, R>(
100 &self,
101 method: Method,
102 path: &str,
103 body: &T,
104 ) -> Result<R, Error>
105 where
106 T: Serialize + ?Sized + std::fmt::Debug,
107 R: DeserializeOwned,
108 {
109 let url = self.base_url.join(path)?;
110
111 let request_builder = self
112 .inner
113 .request(method, url)
114 .header("Content-Type", "application/x-www-form-urlencoded")
115 .query(&[("input_data", serde_json::to_string(body)?)]);
116
117 let response = self.inner.execute(request_builder.build()?).await?;
118 if response.error_for_status_ref().is_err() {
119 let error = response.json::<SdpGenericResponse>().await?;
120 tracing::error!(error = ?error, "SDP Error Response");
121 return Err(error.response_status.into_error());
122 }
123 let result = response.json::<R>().await?;
124 tracing::debug!("completed sdp request");
125 Ok(result)
126 }
127
128 async fn request<T, R>(
129 &self,
130 method: Method,
131 path: &str,
132 path_parameter: &T,
133 ) -> Result<R, Error>
134 where
135 T: std::fmt::Display,
136 R: DeserializeOwned,
137 {
138 let url = self
139 .base_url
140 .join(path)?
141 .join(&path_parameter.to_string())?;
142
143 let request_builder = self.inner.request(method, url);
144 let response = self.inner.execute(request_builder.build()?).await?;
145 if response.error_for_status_ref().is_err() {
146 let error = response.json::<SdpGenericResponse>().await.map_err(|e| {
147 tracing::error!(error = ?e, "Failed to parse SDP error response");
148 Error::from_sdp(
149 500,
150 "Failed to parse SDP error response".to_string(),
151 Some(e.to_string()),
152 )
153 })?;
154 tracing::error!(error = ?error, "SDP Error Response");
155 return Err(error.response_status.into_error());
156 }
157
158 let response = response.json::<R>().await.map_err(|e| {
159 tracing::error!(error = ?e, "Failed to parse SDP response");
160 Error::from_sdp(
161 500,
162 "Failed to parse SDP response".to_string(),
163 Some(e.to_string()),
164 )
165 })?;
166
167 tracing::debug!("completed sdp request");
168 Ok(response)
169 }
170
171 async fn request_with_path<R>(&self, method: Method, path: &str) -> Result<R, Error>
172 where
173 R: DeserializeOwned,
174 {
175 let url = self.base_url.join(path)?;
176
177 let request_builder = self.inner.request(method, url);
178 let response = self.inner.execute(request_builder.build()?).await?;
179 if response.error_for_status_ref().is_err() {
180 let error = response.json::<SdpGenericResponse>().await.map_err(|e| {
181 tracing::error!(error = ?e, "Failed to parse SDP error response");
182 Error::from_sdp(
183 500,
184 "Failed to parse SDP error response".to_string(),
185 Some(e.to_string()),
186 )
187 })?;
188 tracing::error!(error = ?error, "SDP Error Response");
189 return Err(error.response_status.into_error());
190 }
191
192 let parsed = response.json::<R>().await?;
193 tracing::debug!("completed sdp request");
194 Ok(parsed)
195 }
196
197 pub async fn ticket_details(
198 &self,
199 ticket_id: impl Into<TicketID>,
200 ) -> Result<DetailedTicket, Error> {
201 let ticket_id = ticket_id.into();
202 tracing::info!(ticket_id = %ticket_id, "fetching ticket details");
203 let resp: DetailedTicketResponse = self
204 .request(Method::GET, "/api/v3/requests/", &ticket_id)
205 .await?;
206 Ok(resp.request)
207 }
208
209 pub async fn get_conversations(&self, ticket_id: impl Into<TicketID>) -> Result<Value, Error> {
210 let ticket_id = ticket_id.into();
211 tracing::info!(ticket_id = %ticket_id, "fetching ticket details");
212 let path = format!("/api/v3/requests/{}/conversations", &ticket_id);
213 let resp: Value = self.request_with_path(Method::GET, &path).await?;
214 Ok(resp)
215 }
216
217 async fn get_conversations_typed(
218 &self,
219 ticket_id: impl Into<TicketID>,
220 ) -> Result<ConversationsResponse, Error> {
221 let ticket_id = ticket_id.into();
222 tracing::info!(ticket_id = %ticket_id, "fetching ticket conversations");
223 let path = format!("/api/v3/requests/{}/conversations", &ticket_id);
224 self.request_with_path(Method::GET, &path).await
225 }
226
227 pub async fn get_conversation_content(&self, content_url: &str) -> Result<Value, Error> {
228 tracing::info!(content_url = %content_url, "fetching conversation content");
229 let resp: Value = self.request_with_path(Method::GET, content_url).await?;
230 Ok(resp)
231 }
232
233 async fn get_conversation_attachments(
234 &self,
235 content_url: &str,
236 ) -> Result<Vec<Attachment>, Error> {
237 tracing::info!(content_url = %content_url, "fetching conversation attachments");
238 let resp: Value = self.request_with_path(Method::GET, content_url).await?;
239 let attachment: Vec<Attachment> = serde_json::from_value(
240 resp.get("notification")
241 .unwrap_or_default()
242 .get("attachments")
243 .cloned()
244 .unwrap_or_default(),
245 )?;
246 Ok(attachment)
247 }
248
249 pub async fn get_conversation_attachment_urls(
250 &self,
251 ticket_id: impl Into<TicketID>,
252 ) -> Result<Vec<String>, Error> {
253 let conversations = self.get_conversations_typed(ticket_id).await?;
254 let mut links = HashSet::new();
255
256 for conversation in conversations.conversations {
257 if !conversation.has_attachments {
258 continue;
259 }
260
261 let Some(content_url) = conversation.content_url.as_deref() else {
262 continue;
263 };
264
265 let attachments = self.get_conversation_attachments(content_url).await?;
266 for attachment in attachments {
267 links.insert(normalize_attachment_url(
268 &self.base_url,
269 &attachment.content_url,
270 )?);
271 }
272 }
273
274 let mut links: Vec<String> = links.into_iter().collect();
275 links.sort();
276 Ok(links)
277 }
278
279 pub async fn download_attachment(&self, attachment_url: &str) -> Result<Vec<u8>, Error> {
280 tracing::info!(attachment_url = %attachment_url, "downloading attachment");
281 let url = self.base_url.join(attachment_url)?;
282 let response = self.inner.get(url).send().await?;
283 if response.error_for_status_ref().is_err() {
284 let error = response.json::<SdpGenericResponse>().await.map_err(|e| {
285 tracing::error!(error = ?e, "Failed to parse SDP error response");
286 Error::from_sdp(
287 500,
288 "Failed to parse SDP error response".to_string(),
289 Some(e.to_string()),
290 )
291 })?;
292 tracing::error!(error = ?error, "SDP Error Response");
293 return Err(error.response_status.into_error());
294 }
295 let bytes = response.bytes().await?;
296 Ok(bytes.to_vec())
297 }
298
299 pub async fn edit(
304 &self,
305 ticket_id: impl Into<TicketID>,
306 data: &EditTicketData,
307 ) -> Result<(), Error> {
308 let ticket_id = ticket_id.into();
309 tracing::info!(ticket_id = %ticket_id, "editing ticket");
310 let _: SdpGenericResponse = self
311 .request_input_data(
312 Method::PUT,
313 &format!("/api/v3/requests/{}", ticket_id),
314 &EditTicketRequest { request: data },
315 )
316 .await?;
317 Ok(())
318 }
319
320 pub async fn add_note(
322 &self,
323 ticket_id: impl Into<TicketID>,
324 note: &NoteData,
325 ) -> Result<Note, Error> {
326 let ticket_id = ticket_id.into();
327 tracing::info!(ticket_id = %ticket_id, "adding note");
328 let resp: NoteResponse = self
329 .request_input_data(
330 Method::POST,
331 &format!("/api/v3/requests/{}/notes", ticket_id),
332 &AddNoteRequest { note },
333 )
334 .await?;
335 Ok(resp.note)
336 }
337
338 pub async fn get_note(
340 &self,
341 ticket_id: impl Into<TicketID>,
342 note_id: impl Into<NoteID>,
343 ) -> Result<Note, Error> {
344 let ticket_id = ticket_id.into();
345 let note_id = note_id.into();
346 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "fetching note");
347 let url = format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id);
348 let resp: NoteResponse = self.request(Method::GET, &url, &"").await?;
349 Ok(resp.note)
350 }
351
352 pub async fn list_notes(
354 &self,
355 ticket_id: impl Into<TicketID>,
356 row_count: Option<u32>,
357 start_index: Option<u32>,
358 ) -> Result<Vec<Note>, Error> {
359 let ticket_id = ticket_id.into();
360 tracing::info!(ticket_id = %ticket_id, "listing notes");
361 let body = ListNotesRequest {
362 list_info: NotesListInfo {
363 row_count: row_count.unwrap_or(100),
364 start_index: start_index.unwrap_or(1),
365 },
366 };
367 let resp: Value = self
368 .request_input_data(
369 Method::GET,
370 &format!("/api/v3/requests/{}/notes", ticket_id),
371 &body,
372 )
373 .await?;
374 let resp: NotesListResponse = serde_json::from_value(resp)?;
375 Ok(resp.notes)
376 }
377
378 pub async fn edit_note(
380 &self,
381 ticket_id: impl Into<TicketID>,
382 note_id: impl Into<NoteID>,
383 note: &NoteData,
384 ) -> Result<Note, Error> {
385 let ticket_id = ticket_id.into();
386 let note_id = note_id.into();
387 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "editing note");
388 let resp: NoteResponse = self
389 .request_input_data(
390 Method::PUT,
391 &format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id),
392 &EditNoteRequest { request_note: note },
393 )
394 .await?;
395 Ok(resp.note)
396 }
397
398 pub async fn delete_note(
400 &self,
401 ticket_id: impl Into<TicketID>,
402 note_id: impl Into<NoteID>,
403 ) -> Result<(), Error> {
404 let ticket_id = ticket_id.into();
405 let note_id = note_id.into();
406 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "deleting note");
407 let _: SdpGenericResponse = self
408 .request(
409 Method::DELETE,
410 &format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id),
411 &"",
412 )
413 .await?;
414 Ok(())
415 }
416
417 pub async fn assign_ticket(
419 &self,
420 ticket_id: impl Into<TicketID>,
421 technician_name: &str,
422 ) -> Result<(), Error> {
423 let ticket_id = ticket_id.into();
424 tracing::info!(ticket_id = %ticket_id, technician = %technician_name, "assigning ticket");
425 let _: SdpGenericResponse = self
426 .request_input_data(
427 Method::PUT,
428 &format!("/api/v3/requests/{}/assign", ticket_id),
429 &AssignTicketRequest {
430 request: AssignTicketData {
431 technician: technician_name.to_string(),
432 },
433 },
434 )
435 .await?;
436 Ok(())
437 }
438
439 pub async fn create_ticket(&self, data: &CreateTicketData) -> Result<TicketData, Error> {
441 tracing::info!(subject = %data.subject, "creating ticket");
442 let resp: TicketResponse = self
443 .request_input_data(
444 Method::POST,
445 "/api/v3/requests",
446 &CreateTicketRequest { request: data },
447 )
448 .await?;
449 Ok(resp.request)
450 }
451
452 pub async fn search_tickets(&self, criteria: Criteria) -> Result<Vec<DetailedTicket>, Error> {
459 tracing::info!("searching tickets");
460 let resp = self
461 .request_input_data(
462 Method::GET,
463 "/api/v3/requests",
464 &SearchRequest {
465 list_info: ListInfo {
466 row_count: 100,
467 search_criteria: criteria,
468 },
469 },
470 )
471 .await?;
472
473 let ticket_response: TicketSearchResponse = serde_json::from_value(resp)?;
474
475 Ok(ticket_response.requests)
476 }
477
478 pub async fn close_ticket(
480 &self,
481 ticket_id: impl Into<TicketID>,
482 closure_comments: &str,
483 ) -> Result<(), Error> {
484 let ticket_id = ticket_id.into();
485 tracing::info!(ticket_id = %ticket_id, "closing ticket");
486 let _: SdpGenericResponse = self
487 .request_json(
488 Method::PUT,
489 &format!("/api/v3/requests/{}/close", ticket_id),
490 &CloseTicketRequest {
491 request: CloseTicketData {
492 closure_info: ClosureInfo {
493 closure_comments: closure_comments.to_string(),
494 closure_code: "Closed".to_string(),
495 },
496 },
497 },
498 )
499 .await?;
500 Ok(())
501 }
502
503 pub async fn merge(
507 &self,
508 ticket_id: impl Into<TicketID>,
509 merge_ids: &[TicketID],
510 ) -> Result<(), Error> {
511 let ticket_id = ticket_id.into();
512 tracing::info!(ticket_id = %ticket_id, count = merge_ids.len(), "merging tickets");
513 if merge_ids.len() > 49 {
514 tracing::warn!("attempted to merge more than 49 tickets");
515 return Err(Error::from_sdp(
516 400,
517 "Cannot merge more than 49 tickets at once".to_string(),
518 None,
519 ));
520 }
521 let merge_requests: Vec<MergeRequestId> = merge_ids
522 .iter()
523 .map(|id| MergeRequestId {
524 id: id.0.to_string(),
525 })
526 .collect();
527
528 let _: SdpGenericResponse = self
529 .request_form(
530 Method::PUT,
531 &format!("/api/v3/requests/{}/merge_requests", ticket_id),
532 &MergeTicketsRequest { merge_requests },
533 )
534 .await?;
535 Ok(())
536 }
537}
538
539use serde::Deserialize;
540use serde_json::Value;
541
542use crate::{NoteID, ServiceDesk, TicketID, UserID, error::Error};
543
544#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
545pub(crate) struct SearchRequest {
546 pub(crate) list_info: ListInfo,
547}
548
549#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
550pub struct ListInfo {
551 pub row_count: u32,
552 pub search_criteria: Criteria,
553}
554
555#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
560pub struct Criteria {
561 pub field: String,
562 pub condition: Condition,
563 pub value: Value,
564
565 #[serde(skip_serializing_if = "Vec::is_empty")]
566 pub children: Vec<Criteria>,
567
568 #[serde(skip_serializing_if = "Option::is_none")]
569 pub logical_operator: Option<LogicalOp>,
570}
571
572impl Default for Criteria {
573 fn default() -> Self {
574 Criteria {
575 field: String::new(),
576 condition: Condition::Is,
577 value: Value::Null,
578 children: vec![],
579 logical_operator: None,
580 }
581 }
582}
583
584#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
587#[serde(rename_all = "snake_case")]
588pub enum Condition {
589 #[serde(rename = "is")]
590 Is,
591 #[serde(rename = "greater than")]
592 GreaterThan,
593 #[serde(rename = "lesser than")]
594 LesserThan,
595 #[serde(rename = "contains")]
596 Contains,
597}
598
599#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
601pub enum LogicalOp {
602 #[serde(rename = "AND")]
603 And,
604 #[serde(rename = "OR")]
605 Or,
606}
607
608#[derive(Deserialize, Serialize, Debug, PartialEq)]
609pub struct TicketSearchResponse {
610 pub requests: Vec<DetailedTicket>,
611}
612
613#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
614pub struct Account {
615 pub id: String,
616 pub name: String,
617}
618
619#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
620struct DetailedTicketResponse {
621 request: DetailedTicket,
622 #[serde(skip_serializing)]
623 response_status: ResponseStatus,
624}
625
626#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
627#[serde(rename = "request")]
628pub struct DetailedTicket {
629 pub id: TicketID,
630 pub subject: String,
631 pub description: Option<String>,
632 pub status: Status,
633 pub priority: Option<Priority>,
634 pub requester: Option<UserInfo>,
635 pub technician: Option<UserInfo>,
636 #[serde(skip_serializing)]
637 pub created_by: UserInfo,
638 pub created_time: TimeEntry,
639 pub resolution: Option<Resolution>,
640 pub due_by_time: Option<TimeEntry>,
641 pub resolved_time: Option<TimeEntry>,
642 pub completed_time: Option<TimeEntry>,
643 pub udf_fields: Option<Value>,
644 pub attachments: Option<Vec<Attachment>>,
645 pub closure_info: Option<Value>,
646 pub site: Option<Value>,
647 pub department: Option<Value>,
648 pub account: Option<Value>,
649}
650
651#[derive(Serialize, Debug)]
652struct EditTicketRequest<'a> {
653 request: &'a EditTicketData,
654}
655
656#[derive(Serialize, Deserialize, Debug, PartialEq)]
664pub struct EditTicketData {
665 pub subject: String,
666 pub status: Status,
667 pub description: Option<String>,
668 pub requester: Option<UserInfo>,
669 pub priority: Option<Priority>,
670 pub udf_fields: Option<Value>,
672}
673
674impl From<DetailedTicket> for EditTicketData {
675 fn from(value: DetailedTicket) -> Self {
676 Self {
677 subject: value.subject,
678 status: value.status,
679 description: value.description,
680 requester: value.requester,
681 priority: value.priority,
682 udf_fields: value.udf_fields,
683 }
684 }
685}
686
687#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
688pub(crate) struct ResponseStatus {
689 pub(crate) status: String,
690 pub(crate) status_code: i64,
691}
692
693pub const STATUS_ID_OPEN: u64 = 2;
694pub const STATUS_ID_ASSIGNED: u64 = 5;
695pub const STATUS_ID_CANCELLED: u64 = 7;
696pub const STATUS_ID_CLOSED: u64 = 1;
697pub const STATUS_ID_IN_PROGRESS: u64 = 6;
698pub const STATUS_ID_ONHOLD: u64 = 3;
699pub const STATUS_ID_RESOLVED: u64 = 4;
700
701#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
702pub struct Status {
703 pub id: String,
704 pub name: String,
705 pub color: Option<String>,
706}
707
708impl Status {
709 pub fn open() -> Self {
710 Status {
711 id: STATUS_ID_OPEN.to_string(),
712 name: "Open".to_string(),
713 color: Some("#0066ff".to_string()),
714 }
715 }
716
717 pub fn assigned() -> Self {
718 Status {
719 id: STATUS_ID_ASSIGNED.to_string(),
720 name: "Assigned".to_string(),
721 color: Some("#0000ff".to_string()),
723 }
724 }
725
726 pub fn cancelled() -> Self {
727 Status {
728 id: STATUS_ID_CANCELLED.to_string(),
729 name: "Cancelled".to_string(),
730 color: Some("#999999".to_string()),
732 }
733 }
734
735 pub fn closed() -> Self {
736 Status {
737 id: STATUS_ID_CLOSED.to_string(),
738 name: "Closed".to_string(),
739 color: Some("#006600".to_string()),
740 }
741 }
742
743 pub fn in_progress() -> Self {
744 Status {
745 id: STATUS_ID_IN_PROGRESS.to_string(),
746 name: "In Progress".to_string(),
747 color: Some("#00ffcc".to_string()),
748 }
749 }
750
751 pub fn onhold() -> Self {
752 Status {
753 id: STATUS_ID_ONHOLD.to_string(),
754 name: "On Hold".to_string(),
755 color: Some("#ff0000".to_string()),
756 }
757 }
758
759 pub fn resolved() -> Self {
760 Status {
761 id: STATUS_ID_RESOLVED.to_string(),
762 name: "Resolved".to_string(),
763 color: Some("#00ff66".to_string()),
764 }
765 }
766}
767
768#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
773pub struct Priority {
774 pub id: String,
775 pub name: String,
776 pub color: Option<String>,
777}
778
779pub const PRIORITY_ID_LOW: u64 = 1;
780pub const PRIORITY_ID_MEDIUM: u64 = 3;
781pub const PRIORITY_ID_HIGH: u64 = 4;
782pub const PRIORITY_ID_CRITICAL: u64 = 301;
783
784impl Priority {
823 pub fn low() -> Self {
824 Priority {
825 id: PRIORITY_ID_LOW.to_string(),
826 name: "Low".to_string(),
827 color: Some("#288251".to_string()),
828 }
829 }
830
831 pub fn medium() -> Self {
832 Priority {
833 id: PRIORITY_ID_MEDIUM.to_string(),
834 name: "Medium".to_string(),
835 color: Some("#efb116".to_string()),
836 }
837 }
838
839 pub fn high() -> Self {
840 Priority {
841 id: PRIORITY_ID_HIGH.to_string(),
842 name: "High".to_string(),
843 color: Some("#ff5e00".to_string()),
844 }
845 }
846
847 pub fn critical() -> Self {
850 Priority {
851 id: PRIORITY_ID_CRITICAL.to_string(),
852 name: "Critical".to_string(),
853 color: Some("#8b0808".to_string()),
854 }
855 }
856}
857
858#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
859pub struct UserInfo {
860 pub id: UserID,
861 pub name: String,
862 pub email_id: Option<String>,
863 pub account: Option<Value>,
864 pub department: Option<Value>,
865 #[serde(default)]
866 pub is_vipuser: bool,
867 pub mobile: Option<String>,
868 pub org_user_status: Option<String>,
869 pub phone: Option<String>,
870 #[serde(skip_serializing)]
871 pub profile_pic: Option<Value>,
872}
873
874#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
875pub struct Resolution {
876 pub content: Option<String>,
877 pub submitted_by: Option<UserInfo>,
878 pub submitted_on: Option<TimeEntry>,
879 pub resolution_attachments: Option<Vec<Attachment>>,
880}
881
882#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
883pub struct Attachment {
884 pub id: String,
885 pub name: String,
886 pub content_url: String,
887 pub content_type: Option<String>,
888 pub description: Option<String>,
889 pub module: Option<String>,
890 pub size: Option<SizeInfo>,
891 pub attached_by: Option<UserInfo>,
892 pub attached_on: Option<TimeEntry>,
893}
894
895#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
896pub struct SizeInfo {
897 pub display_value: String,
898 pub value: u64,
899}
900
901#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
902pub struct TimeEntry {
903 pub display_value: String,
904 pub value: String,
905}
906
907#[derive(Serialize, Debug)]
908struct CreateTicketRequest<'a> {
909 request: &'a CreateTicketData,
910}
911
912#[derive(Serialize, Deserialize, Debug, PartialEq)]
913pub struct CreateTicketData {
914 pub subject: String,
915 pub description: String,
916 #[serde(
917 serialize_with = "serialize_name_object",
918 deserialize_with = "deserialize_name_object"
919 )]
920 pub requester: String,
921 #[serde(
922 serialize_with = "serialize_name_object",
923 deserialize_with = "deserialize_name_object"
924 )]
925 pub priority: String,
926 pub udf_fields: Value,
930 #[serde(
931 serialize_with = "serialize_name_object",
932 deserialize_with = "deserialize_name_object"
933 )]
934 pub account: String,
935 #[serde(
936 serialize_with = "serialize_name_object",
937 deserialize_with = "deserialize_name_object"
938 )]
939 pub template: String,
940}
941
942impl Default for CreateTicketData {
943 fn default() -> Self {
944 CreateTicketData {
945 subject: String::new(),
946 description: String::new(),
947 requester: String::new(),
948 priority: "Low".to_string(),
949 udf_fields: Value::Null,
950 account: String::new(),
951 template: String::new(),
952 }
953 }
954}
955
956pub(crate) fn deserialize_name_object<'de, D>(deserializer: D) -> Result<String, D::Error>
957where
958 D: Deserializer<'de>,
959{
960 #[derive(Deserialize)]
961 struct NameObject {
962 name: String,
963 }
964
965 Ok(NameObject::deserialize(deserializer)?.name)
966}
967
968pub(crate) fn serialize_name_object<S>(name: &String, serializer: S) -> Result<S::Ok, S::Error>
969where
970 S: Serializer,
971{
972 let mut s = serializer.serialize_struct("NameWrapper", 1)?;
973 s.serialize_field("name", name)?;
974 s.end()
975}
976
977#[allow(dead_code)]
978#[derive(Serialize, Debug, PartialEq, Eq)]
979pub(crate) struct NameWrapper {
980 pub(crate) name: String,
981}
982
983impl From<&str> for NameWrapper {
984 fn from(name: &str) -> Self {
985 Self {
986 name: name.to_string(),
987 }
988 }
989}
990
991impl From<String> for NameWrapper {
992 fn from(name: String) -> Self {
993 Self { name }
994 }
995}
996
997impl std::ops::Deref for NameWrapper {
998 type Target = String;
999
1000 fn deref(&self) -> &Self::Target {
1001 &self.name
1002 }
1003}
1004
1005impl std::ops::DerefMut for NameWrapper {
1006 fn deref_mut(&mut self) -> &mut Self::Target {
1007 &mut self.name
1008 }
1009}
1010
1011#[derive(Serialize, Debug, PartialEq, Eq)]
1012struct CloseTicketRequest {
1013 request: CloseTicketData,
1014}
1015
1016#[derive(Serialize, Debug, PartialEq, Eq)]
1017struct CloseTicketData {
1018 closure_info: ClosureInfo,
1019}
1020
1021#[derive(Serialize, Debug, PartialEq, Eq)]
1022struct ClosureInfo {
1023 closure_comments: String,
1024 closure_code: String,
1025}
1026
1027#[derive(Serialize, Debug)]
1028struct AddNoteRequest<'a> {
1029 note: &'a NoteData,
1030}
1031
1032#[derive(Serialize, Debug, Default, PartialEq, Eq)]
1033pub struct NoteData {
1034 pub mark_first_response: bool,
1035 pub add_to_linked_requests: bool,
1036 pub notify_technician: bool,
1037 pub show_to_requester: bool,
1038 pub description: String,
1039}
1040
1041#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1043pub(crate) struct NoteResponse {
1044 pub(crate) note: Note,
1045}
1046
1047#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1048pub struct NotesListResponse {
1049 pub list_info: Option<ListInfoResponse>,
1050 pub notes: Vec<Note>,
1051 pub response_status: Vec<ResponseStatus>,
1052}
1053
1054#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1055pub struct ListInfoResponse {
1056 pub has_more_rows: bool,
1057 pub page: u32,
1058 pub row_count: u32,
1059 pub sort_field: String,
1060 pub sort_order: String,
1061 pub start_index: u32,
1062}
1063
1064#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1065pub struct Note {
1066 pub id: NoteID,
1067 #[serde(default)]
1068 pub description: String,
1069 #[serde(default)]
1070 pub show_to_requester: bool,
1071 #[serde(default)]
1072 pub mark_first_response: bool,
1073 #[serde(default)]
1074 pub notify_technician: bool,
1075 #[serde(default)]
1076 pub add_to_linked_requests: bool,
1077 pub created_time: Option<TimeEntry>,
1078 pub created_by: Option<UserInfo>,
1079 pub last_updated_time: Option<TimeEntry>,
1080}
1081
1082#[derive(Serialize, Debug)]
1083struct EditNoteRequest<'a> {
1084 request_note: &'a NoteData,
1085}
1086
1087#[derive(Serialize, Debug)]
1088struct ListNotesRequest {
1089 list_info: NotesListInfo,
1090}
1091
1092#[derive(Debug, PartialEq, Eq, Deserialize)]
1093struct ConversationsResponse {
1094 #[serde(default)]
1095 conversations: Vec<ConversationSummary>,
1096}
1097
1098#[derive(Debug, PartialEq, Eq, Deserialize)]
1099struct ConversationSummary {
1100 #[serde(default)]
1101 has_attachments: bool,
1102 #[serde(default)]
1103 content_url: Option<String>,
1104}
1105
1106fn normalize_attachment_url(base_url: &reqwest::Url, value: &str) -> Result<String, Error> {
1107 Ok(base_url.join(value)?.to_string())
1108}
1109
1110#[derive(Serialize, Debug, PartialEq, Eq)]
1111struct NotesListInfo {
1112 row_count: u32,
1113 start_index: u32,
1114}
1115
1116#[derive(Serialize, Debug, PartialEq, Eq)]
1117struct AssignTicketRequest {
1118 request: AssignTicketData,
1119}
1120
1121#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
1122struct AssignTicketData {
1123 #[serde(
1124 serialize_with = "serialize_name_object",
1125 deserialize_with = "deserialize_name_object"
1126 )]
1127 technician: String,
1128}
1129
1130#[derive(Serialize, Debug, PartialEq, Eq)]
1131struct MergeTicketsRequest {
1132 merge_requests: Vec<MergeRequestId>,
1133}
1134
1135#[derive(Serialize, Debug, PartialEq, Eq)]
1136struct MergeRequestId {
1137 id: String,
1138}
1139
1140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1141pub(crate) struct TicketResponse {
1142 pub(crate) request: TicketData,
1143 pub(crate) response_status: ResponseStatus,
1144}
1145
1146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1147pub struct TicketData {
1148 pub id: TicketID,
1149 pub subject: String,
1150 pub description: Option<String>,
1151 pub status: Status,
1152 pub priority: Option<Priority>,
1153 pub created_time: TimeEntry,
1154 pub requester: Option<UserInfo>,
1155 pub account: Account,
1156 pub template: TemplateInfo,
1157 pub udf_fields: Option<Value>,
1158}
1159
1160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1161pub struct TemplateInfo {
1162 pub id: String,
1163 pub name: String,
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168 use super::*;
1169 use serde_json::json;
1170
1171 #[test]
1172 fn criteria_default() {
1173 let criteria = Criteria::default();
1174 assert!(criteria.field.is_empty());
1175 assert!(matches!(criteria.condition, Condition::Is));
1176 assert!(criteria.value.is_null());
1177 assert!(criteria.children.is_empty());
1178 assert!(criteria.logical_operator.is_none());
1179 }
1180
1181 #[test]
1182 fn create_ticket_data_default() {
1183 let data = CreateTicketData::default();
1184 assert!(data.subject.is_empty());
1185 assert!(data.description.is_empty());
1186 assert!(data.requester.is_empty());
1187 assert_eq!(data.priority, "Low");
1188 assert!(data.udf_fields.is_null());
1189 assert!(data.account.is_empty());
1190 assert!(data.template.is_empty());
1191 }
1192
1193 #[test]
1194 fn create_ticket_data_serializes_name_fields_as_objects() {
1195 let data = CreateTicketData {
1196 subject: "test".to_string(),
1197 description: "body".to_string(),
1198 requester: "NETXP".to_string(),
1199 priority: "High".to_string(),
1200 udf_fields: json!({}),
1201 account: "SOC".to_string(),
1202 template: "SOC-with-alert-id".to_string(),
1203 };
1204
1205 let serialized = serde_json::to_value(&data).unwrap();
1206
1207 assert_eq!(serialized["requester"], json!({ "name": "NETXP" }));
1208 assert_eq!(serialized["priority"], json!({ "name": "High" }));
1209 assert_eq!(serialized["account"], json!({ "name": "SOC" }));
1210 assert_eq!(
1211 serialized["template"],
1212 json!({ "name": "SOC-with-alert-id" })
1213 );
1214 }
1215
1216 #[test]
1217 fn edit_ticket_data_serializes_optional_name_fields_as_objects() {
1218 let data = EditTicketData {
1219 subject: "test".to_string(),
1220 status: Status {
1221 id: "1".to_string(),
1222 name: "Open".to_string(),
1223 color: None,
1224 },
1225 description: None,
1226 requester: None,
1227 priority: Some(Priority::high()),
1228 udf_fields: None,
1229 };
1230
1231 let serialized = serde_json::to_value(&data).unwrap();
1232
1233 assert_eq!(serialized["requester"], json!({ "name": "NETXP" }));
1234 assert_eq!(serialized["priority"], json!({ "name": "High" }));
1235 }
1236}