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(&Value::Null)
242 .get("attachments")
243 .cloned()
244 .unwrap_or_default(),
245 )?;
246 Ok(attachment)
247 }
248
249 pub async fn merged_ticket_ids(
255 &self,
256 ticket_id: impl Into<TicketID>,
257 ) -> Result<Vec<TicketID>, Error> {
258 let ticket_id = ticket_id.into();
259 let conversations = self.get_conversations_typed(ticket_id).await?;
260 let mut merged = Vec::new();
261 for conversation in conversations.conversations {
262 let Some(content_url) = conversation.content_url.as_deref() else {
263 continue;
264 };
265 let body: Value = self.request_with_path(Method::GET, content_url).await?;
266 let notification = &body["notification"];
267 if notification["notification_history"]["operation"] != "MERGE" {
268 continue;
269 }
270 if let Some(id) = notification["merged_request_id"].as_str()
271 && let Ok(id) = id.parse::<u64>()
272 {
273 merged.push(TicketID(id));
274 }
275 }
276 Ok(merged)
277 }
278
279 pub async fn get_conversation_attachment_urls(
280 &self,
281 ticket_id: impl Into<TicketID>,
282 ) -> Result<Vec<String>, Error> {
283 let conversations = self.get_conversations_typed(ticket_id).await?;
284 let mut links = HashSet::new();
285
286 for conversation in conversations.conversations {
287 if !conversation.has_attachments {
288 continue;
289 }
290
291 let Some(content_url) = conversation.content_url.as_deref() else {
292 continue;
293 };
294
295 let attachments = self.get_conversation_attachments(content_url).await?;
296 for attachment in attachments {
297 links.insert(normalize_attachment_url(
298 &self.base_url,
299 &attachment.content_url,
300 )?);
301 }
302 }
303
304 let mut links: Vec<String> = links.into_iter().collect();
305 links.sort();
306 Ok(links)
307 }
308
309 pub async fn download_attachment(&self, attachment_url: &str) -> Result<Vec<u8>, Error> {
310 tracing::info!(attachment_url = %attachment_url, "downloading attachment");
311 let url = self.base_url.join(attachment_url)?;
312 let response = self.inner.get(url).send().await?;
313 if response.error_for_status_ref().is_err() {
314 let error = response.json::<SdpGenericResponse>().await.map_err(|e| {
315 tracing::error!(error = ?e, "Failed to parse SDP error response");
316 Error::from_sdp(
317 500,
318 "Failed to parse SDP error response".to_string(),
319 Some(e.to_string()),
320 )
321 })?;
322 tracing::error!(error = ?error, "SDP Error Response");
323 return Err(error.response_status.into_error());
324 }
325 let bytes = response.bytes().await?;
326 Ok(bytes.to_vec())
327 }
328
329 pub async fn edit(
334 &self,
335 ticket_id: impl Into<TicketID>,
336 data: &EditTicketData,
337 ) -> Result<(), Error> {
338 let ticket_id = ticket_id.into();
339 tracing::info!(ticket_id = %ticket_id, "editing ticket");
340 let _: SdpGenericResponse = self
341 .request_input_data(
342 Method::PUT,
343 &format!("/api/v3/requests/{}", ticket_id),
344 &EditTicketRequest { request: data },
345 )
346 .await?;
347 Ok(())
348 }
349
350 pub async fn add_note(
352 &self,
353 ticket_id: impl Into<TicketID>,
354 note: &NoteData,
355 ) -> Result<Note, Error> {
356 let ticket_id = ticket_id.into();
357 tracing::info!(ticket_id = %ticket_id, "adding note");
358 let resp: NoteResponse = self
359 .request_input_data(
360 Method::POST,
361 &format!("/api/v3/requests/{}/notes", ticket_id),
362 &AddNoteRequest { note },
363 )
364 .await?;
365 Ok(resp.note)
366 }
367
368 pub async fn get_note(
370 &self,
371 ticket_id: impl Into<TicketID>,
372 note_id: impl Into<NoteID>,
373 ) -> Result<Note, Error> {
374 let ticket_id = ticket_id.into();
375 let note_id = note_id.into();
376 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "fetching note");
377 let url = format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id);
378 let resp: NoteResponse = self.request(Method::GET, &url, &"").await?;
379 Ok(resp.note)
380 }
381
382 pub async fn list_notes(
384 &self,
385 ticket_id: impl Into<TicketID>,
386 row_count: Option<u32>,
387 start_index: Option<u32>,
388 ) -> Result<Vec<Note>, Error> {
389 let ticket_id = ticket_id.into();
390 tracing::info!(ticket_id = %ticket_id, "listing notes");
391 let body = ListNotesRequest {
392 list_info: NotesListInfo {
393 row_count: row_count.unwrap_or(100),
394 start_index: start_index.unwrap_or(1),
395 },
396 };
397 let resp: Value = self
398 .request_input_data(
399 Method::GET,
400 &format!("/api/v3/requests/{}/notes", ticket_id),
401 &body,
402 )
403 .await?;
404 let resp: NotesListResponse = serde_json::from_value(resp)?;
405 Ok(resp.notes)
406 }
407
408 pub async fn edit_note(
410 &self,
411 ticket_id: impl Into<TicketID>,
412 note_id: impl Into<NoteID>,
413 note: &NoteData,
414 ) -> Result<Note, Error> {
415 let ticket_id = ticket_id.into();
416 let note_id = note_id.into();
417 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "editing note");
418 let resp: NoteResponse = self
419 .request_input_data(
420 Method::PUT,
421 &format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id),
422 &EditNoteRequest { request_note: note },
423 )
424 .await?;
425 Ok(resp.note)
426 }
427
428 pub async fn delete_note(
430 &self,
431 ticket_id: impl Into<TicketID>,
432 note_id: impl Into<NoteID>,
433 ) -> Result<(), Error> {
434 let ticket_id = ticket_id.into();
435 let note_id = note_id.into();
436 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "deleting note");
437 let _: SdpGenericResponse = self
438 .request(
439 Method::DELETE,
440 &format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id),
441 &"",
442 )
443 .await?;
444 Ok(())
445 }
446
447 pub async fn assign_ticket(
449 &self,
450 ticket_id: impl Into<TicketID>,
451 technician_name: &str,
452 ) -> Result<(), Error> {
453 let ticket_id = ticket_id.into();
454 tracing::info!(ticket_id = %ticket_id, technician = %technician_name, "assigning ticket");
455 let _: SdpGenericResponse = self
456 .request_input_data(
457 Method::PUT,
458 &format!("/api/v3/requests/{}/assign", ticket_id),
459 &AssignTicketRequest {
460 request: AssignTicketData {
461 technician: technician_name.to_string(),
462 },
463 },
464 )
465 .await?;
466 Ok(())
467 }
468
469 pub async fn create_ticket(&self, data: &CreateTicketData) -> Result<TicketData, Error> {
471 tracing::info!(subject = %data.subject, "creating ticket");
472 let resp: TicketResponse = self
473 .request_input_data(
474 Method::POST,
475 "/api/v3/requests",
476 &CreateTicketRequest { request: data },
477 )
478 .await?;
479 Ok(resp.request)
480 }
481
482 pub async fn search_tickets(&self, criteria: Criteria) -> Result<Vec<DetailedTicket>, Error> {
489 tracing::info!("searching tickets");
490 let resp = self
491 .request_input_data(
492 Method::GET,
493 "/api/v3/requests",
494 &SearchRequest {
495 list_info: ListInfo {
496 row_count: 100,
497 search_criteria: criteria,
498 },
499 },
500 )
501 .await?;
502
503 let ticket_response: TicketSearchResponse = serde_json::from_value(resp)?;
504
505 Ok(ticket_response.requests)
506 }
507
508 pub async fn close_ticket(
510 &self,
511 ticket_id: impl Into<TicketID>,
512 closure_comments: &str,
513 ) -> Result<(), Error> {
514 let ticket_id = ticket_id.into();
515 tracing::info!(ticket_id = %ticket_id, "closing ticket");
516 let _: SdpGenericResponse = self
517 .request_json(
518 Method::PUT,
519 &format!("/api/v3/requests/{}/close", ticket_id),
520 &CloseTicketRequest {
521 request: CloseTicketData {
522 closure_info: ClosureInfo {
523 closure_comments: closure_comments.to_string(),
524 closure_code: "Closed".to_string(),
525 },
526 },
527 },
528 )
529 .await?;
530 Ok(())
531 }
532
533 pub async fn merge(
537 &self,
538 ticket_id: impl Into<TicketID>,
539 merge_ids: &[TicketID],
540 ) -> Result<(), Error> {
541 let ticket_id = ticket_id.into();
542 tracing::info!(ticket_id = %ticket_id, count = merge_ids.len(), "merging tickets");
543 if merge_ids.len() > 49 {
544 tracing::warn!("attempted to merge more than 49 tickets");
545 return Err(Error::from_sdp(
546 400,
547 "Cannot merge more than 49 tickets at once".to_string(),
548 None,
549 ));
550 }
551 let merge_requests: Vec<MergeRequestId> = merge_ids
552 .iter()
553 .map(|id| MergeRequestId {
554 id: id.0.to_string(),
555 })
556 .collect();
557
558 let _: SdpGenericResponse = self
559 .request_form(
560 Method::PUT,
561 &format!("/api/v3/requests/{}/merge_requests", ticket_id),
562 &MergeTicketsRequest { merge_requests },
563 )
564 .await?;
565 Ok(())
566 }
567}
568
569use serde::Deserialize;
570use serde_json::Value;
571
572use crate::{NoteID, ServiceDesk, TicketID, UserID, error::Error};
573
574#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
575pub(crate) struct SearchRequest {
576 pub(crate) list_info: ListInfo,
577}
578
579#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
580pub struct ListInfo {
581 pub row_count: u32,
582 pub search_criteria: Criteria,
583}
584
585#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
590pub struct Criteria {
591 pub field: String,
592 pub condition: Condition,
593 pub value: Value,
594
595 #[serde(skip_serializing_if = "Vec::is_empty")]
596 pub children: Vec<Criteria>,
597
598 #[serde(skip_serializing_if = "Option::is_none")]
599 pub logical_operator: Option<LogicalOp>,
600}
601
602impl Default for Criteria {
603 fn default() -> Self {
604 Criteria {
605 field: String::new(),
606 condition: Condition::Is,
607 value: Value::Null,
608 children: vec![],
609 logical_operator: None,
610 }
611 }
612}
613
614#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
617#[serde(rename_all = "snake_case")]
618pub enum Condition {
619 #[serde(rename = "is")]
620 Is,
621 #[serde(rename = "greater than")]
622 GreaterThan,
623 #[serde(rename = "lesser than")]
624 LesserThan,
625 #[serde(rename = "contains")]
626 Contains,
627}
628
629#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
631pub enum LogicalOp {
632 #[serde(rename = "AND")]
633 And,
634 #[serde(rename = "OR")]
635 Or,
636}
637
638#[derive(Deserialize, Serialize, Debug, PartialEq)]
639pub struct TicketSearchResponse {
640 pub requests: Vec<DetailedTicket>,
641}
642
643#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
644pub struct Account {
645 pub id: String,
646 pub name: String,
647}
648
649#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
650struct DetailedTicketResponse {
651 request: DetailedTicket,
652 #[serde(skip_serializing)]
653 response_status: ResponseStatus,
654}
655
656#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
657#[serde(rename = "request")]
658pub struct DetailedTicket {
659 pub id: TicketID,
660 pub subject: String,
661 pub description: Option<String>,
662 pub status: Status,
663 pub priority: Option<Priority>,
664 pub requester: Option<UserInfo>,
665 pub technician: Option<UserInfo>,
666 #[serde(skip_serializing)]
667 pub created_by: UserInfo,
668 pub created_time: TimeEntry,
669 pub resolution: Option<Resolution>,
670 pub due_by_time: Option<TimeEntry>,
671 pub resolved_time: Option<TimeEntry>,
672 pub completed_time: Option<TimeEntry>,
673 pub udf_fields: Option<Value>,
674 pub attachments: Option<Vec<Attachment>>,
675 pub closure_info: Option<Value>,
676 pub site: Option<Value>,
677 pub department: Option<Value>,
678 pub account: Option<Value>,
679}
680
681#[derive(Serialize, Debug)]
682struct EditTicketRequest<'a> {
683 request: &'a EditTicketData,
684}
685
686#[derive(Serialize, Deserialize, Debug, PartialEq)]
694pub struct EditTicketData {
695 pub subject: String,
696 pub status: Status,
697 pub description: Option<String>,
698 pub requester: Option<UserInfo>,
699 pub priority: Option<Priority>,
700 pub udf_fields: Option<Value>,
702}
703
704impl From<DetailedTicket> for EditTicketData {
705 fn from(value: DetailedTicket) -> Self {
706 Self {
707 subject: value.subject,
708 status: value.status,
709 description: value.description,
710 requester: value.requester,
711 priority: value.priority,
712 udf_fields: value.udf_fields,
713 }
714 }
715}
716
717#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
718pub(crate) struct ResponseStatus {
719 pub(crate) status: String,
720 pub(crate) status_code: i64,
721}
722
723pub const STATUS_ID_OPEN: u64 = 2;
724pub const STATUS_ID_ASSIGNED: u64 = 5;
725pub const STATUS_ID_CANCELLED: u64 = 7;
726pub const STATUS_ID_CLOSED: u64 = 1;
727pub const STATUS_ID_IN_PROGRESS: u64 = 6;
728pub const STATUS_ID_ONHOLD: u64 = 3;
729pub const STATUS_ID_RESOLVED: u64 = 4;
730
731#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
732pub struct Status {
733 pub id: String,
734 pub name: String,
735 pub color: Option<String>,
736}
737
738impl Status {
739 pub fn open() -> Self {
740 Status {
741 id: STATUS_ID_OPEN.to_string(),
742 name: "Open".to_string(),
743 color: Some("#0066ff".to_string()),
744 }
745 }
746
747 pub fn assigned() -> Self {
748 Status {
749 id: STATUS_ID_ASSIGNED.to_string(),
750 name: "Assigned".to_string(),
751 color: Some("#0000ff".to_string()),
753 }
754 }
755
756 pub fn cancelled() -> Self {
757 Status {
758 id: STATUS_ID_CANCELLED.to_string(),
759 name: "Cancelled".to_string(),
760 color: Some("#999999".to_string()),
762 }
763 }
764
765 pub fn closed() -> Self {
766 Status {
767 id: STATUS_ID_CLOSED.to_string(),
768 name: "Closed".to_string(),
769 color: Some("#006600".to_string()),
770 }
771 }
772
773 pub fn in_progress() -> Self {
774 Status {
775 id: STATUS_ID_IN_PROGRESS.to_string(),
776 name: "In Progress".to_string(),
777 color: Some("#00ffcc".to_string()),
778 }
779 }
780
781 pub fn onhold() -> Self {
782 Status {
783 id: STATUS_ID_ONHOLD.to_string(),
784 name: "On Hold".to_string(),
785 color: Some("#ff0000".to_string()),
786 }
787 }
788
789 pub fn resolved() -> Self {
790 Status {
791 id: STATUS_ID_RESOLVED.to_string(),
792 name: "Resolved".to_string(),
793 color: Some("#00ff66".to_string()),
794 }
795 }
796}
797
798#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
803pub struct Priority {
804 pub id: String,
805 pub name: String,
806 pub color: Option<String>,
807}
808
809pub const PRIORITY_ID_LOW: u64 = 1;
810pub const PRIORITY_ID_MEDIUM: u64 = 3;
811pub const PRIORITY_ID_HIGH: u64 = 4;
812pub const PRIORITY_ID_CRITICAL: u64 = 301;
813
814impl Priority {
853 pub fn low() -> Self {
854 Priority {
855 id: PRIORITY_ID_LOW.to_string(),
856 name: "Low".to_string(),
857 color: Some("#288251".to_string()),
858 }
859 }
860
861 pub fn medium() -> Self {
862 Priority {
863 id: PRIORITY_ID_MEDIUM.to_string(),
864 name: "Medium".to_string(),
865 color: Some("#efb116".to_string()),
866 }
867 }
868
869 pub fn high() -> Self {
870 Priority {
871 id: PRIORITY_ID_HIGH.to_string(),
872 name: "High".to_string(),
873 color: Some("#ff5e00".to_string()),
874 }
875 }
876
877 pub fn critical() -> Self {
880 Priority {
881 id: PRIORITY_ID_CRITICAL.to_string(),
882 name: "Critical".to_string(),
883 color: Some("#8b0808".to_string()),
884 }
885 }
886}
887
888#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
889pub struct UserInfo {
890 pub id: UserID,
891 pub name: String,
892 pub email_id: Option<String>,
893 pub account: Option<Value>,
894 pub department: Option<Value>,
895 #[serde(default)]
896 pub is_vipuser: bool,
897 pub mobile: Option<String>,
898 pub org_user_status: Option<String>,
899 pub phone: Option<String>,
900 #[serde(skip_serializing)]
901 pub profile_pic: Option<Value>,
902}
903
904#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
905pub struct Resolution {
906 pub content: Option<String>,
907 pub submitted_by: Option<UserInfo>,
908 pub submitted_on: Option<TimeEntry>,
909 pub resolution_attachments: Option<Vec<Attachment>>,
910}
911
912#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
913pub struct Attachment {
914 pub id: String,
915 pub name: String,
916 pub content_url: String,
917 pub content_type: Option<String>,
918 pub description: Option<String>,
919 pub module: Option<String>,
920 pub size: Option<SizeInfo>,
921 pub attached_by: Option<UserInfo>,
922 pub attached_on: Option<TimeEntry>,
923}
924
925#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
926pub struct SizeInfo {
927 pub display_value: String,
928 pub value: u64,
929}
930
931#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
932pub struct TimeEntry {
933 pub display_value: String,
934 pub value: String,
935}
936
937#[derive(Serialize, Debug)]
938struct CreateTicketRequest<'a> {
939 request: &'a CreateTicketData,
940}
941
942#[derive(Serialize, Deserialize, Debug, PartialEq)]
943pub struct CreateTicketData {
944 pub subject: String,
945 pub description: String,
946 #[serde(
947 serialize_with = "serialize_name_object",
948 deserialize_with = "deserialize_name_object"
949 )]
950 pub requester: String,
951 #[serde(
952 serialize_with = "serialize_name_object",
953 deserialize_with = "deserialize_name_object"
954 )]
955 pub priority: String,
956 pub udf_fields: Value,
960 #[serde(
961 serialize_with = "serialize_name_object",
962 deserialize_with = "deserialize_name_object"
963 )]
964 pub account: String,
965 #[serde(
966 serialize_with = "serialize_name_object",
967 deserialize_with = "deserialize_name_object"
968 )]
969 pub template: String,
970}
971
972impl Default for CreateTicketData {
973 fn default() -> Self {
974 CreateTicketData {
975 subject: String::new(),
976 description: String::new(),
977 requester: String::new(),
978 priority: "Low".to_string(),
979 udf_fields: Value::Null,
980 account: String::new(),
981 template: String::new(),
982 }
983 }
984}
985
986pub(crate) fn deserialize_name_object<'de, D>(deserializer: D) -> Result<String, D::Error>
987where
988 D: Deserializer<'de>,
989{
990 #[derive(Deserialize)]
991 struct NameObject {
992 name: String,
993 }
994
995 Ok(NameObject::deserialize(deserializer)?.name)
996}
997
998pub(crate) fn serialize_name_object<S>(name: &String, serializer: S) -> Result<S::Ok, S::Error>
999where
1000 S: Serializer,
1001{
1002 let mut s = serializer.serialize_struct("NameWrapper", 1)?;
1003 s.serialize_field("name", name)?;
1004 s.end()
1005}
1006
1007#[allow(dead_code)]
1008#[derive(Serialize, Debug, PartialEq, Eq)]
1009pub(crate) struct NameWrapper {
1010 pub(crate) name: String,
1011}
1012
1013impl From<&str> for NameWrapper {
1014 fn from(name: &str) -> Self {
1015 Self {
1016 name: name.to_string(),
1017 }
1018 }
1019}
1020
1021impl From<String> for NameWrapper {
1022 fn from(name: String) -> Self {
1023 Self { name }
1024 }
1025}
1026
1027impl std::ops::Deref for NameWrapper {
1028 type Target = String;
1029
1030 fn deref(&self) -> &Self::Target {
1031 &self.name
1032 }
1033}
1034
1035impl std::ops::DerefMut for NameWrapper {
1036 fn deref_mut(&mut self) -> &mut Self::Target {
1037 &mut self.name
1038 }
1039}
1040
1041#[derive(Serialize, Debug, PartialEq, Eq)]
1042struct CloseTicketRequest {
1043 request: CloseTicketData,
1044}
1045
1046#[derive(Serialize, Debug, PartialEq, Eq)]
1047struct CloseTicketData {
1048 closure_info: ClosureInfo,
1049}
1050
1051#[derive(Serialize, Debug, PartialEq, Eq)]
1052struct ClosureInfo {
1053 closure_comments: String,
1054 closure_code: String,
1055}
1056
1057#[derive(Serialize, Debug)]
1058struct AddNoteRequest<'a> {
1059 note: &'a NoteData,
1060}
1061
1062#[derive(Serialize, Debug, Default, PartialEq, Eq)]
1063pub struct NoteData {
1064 pub mark_first_response: bool,
1065 pub add_to_linked_requests: bool,
1066 pub notify_technician: bool,
1067 pub show_to_requester: bool,
1068 pub description: String,
1069}
1070
1071#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1073pub(crate) struct NoteResponse {
1074 pub(crate) note: Note,
1075}
1076
1077#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1078pub struct NotesListResponse {
1079 pub list_info: Option<ListInfoResponse>,
1080 pub notes: Vec<Note>,
1081 pub response_status: Vec<ResponseStatus>,
1082}
1083
1084#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1085pub struct ListInfoResponse {
1086 pub has_more_rows: bool,
1087 pub page: u32,
1088 pub row_count: u32,
1089 pub sort_field: String,
1090 pub sort_order: String,
1091 pub start_index: u32,
1092}
1093
1094#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1095pub struct Note {
1096 pub id: NoteID,
1097 #[serde(default)]
1098 pub description: String,
1099 #[serde(default)]
1100 pub show_to_requester: bool,
1101 #[serde(default)]
1102 pub mark_first_response: bool,
1103 #[serde(default)]
1104 pub notify_technician: bool,
1105 #[serde(default)]
1106 pub add_to_linked_requests: bool,
1107 pub created_time: Option<TimeEntry>,
1108 pub created_by: Option<UserInfo>,
1109 pub last_updated_time: Option<TimeEntry>,
1110}
1111
1112#[derive(Serialize, Debug)]
1113struct EditNoteRequest<'a> {
1114 request_note: &'a NoteData,
1115}
1116
1117#[derive(Serialize, Debug)]
1118struct ListNotesRequest {
1119 list_info: NotesListInfo,
1120}
1121
1122#[derive(Debug, PartialEq, Eq, Deserialize)]
1123struct ConversationsResponse {
1124 #[serde(default)]
1125 conversations: Vec<ConversationSummary>,
1126}
1127
1128#[derive(Debug, PartialEq, Eq, Deserialize)]
1129struct ConversationSummary {
1130 #[serde(default)]
1131 has_attachments: bool,
1132 #[serde(default)]
1133 content_url: Option<String>,
1134}
1135
1136fn normalize_attachment_url(base_url: &reqwest::Url, value: &str) -> Result<String, Error> {
1137 Ok(base_url.join(value)?.to_string())
1138}
1139
1140#[derive(Serialize, Debug, PartialEq, Eq)]
1141struct NotesListInfo {
1142 row_count: u32,
1143 start_index: u32,
1144}
1145
1146#[derive(Serialize, Debug, PartialEq, Eq)]
1147struct AssignTicketRequest {
1148 request: AssignTicketData,
1149}
1150
1151#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
1152struct AssignTicketData {
1153 #[serde(
1154 serialize_with = "serialize_name_object",
1155 deserialize_with = "deserialize_name_object"
1156 )]
1157 technician: String,
1158}
1159
1160#[derive(Serialize, Debug, PartialEq, Eq)]
1161struct MergeTicketsRequest {
1162 merge_requests: Vec<MergeRequestId>,
1163}
1164
1165#[derive(Serialize, Debug, PartialEq, Eq)]
1166struct MergeRequestId {
1167 id: String,
1168}
1169
1170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1171pub(crate) struct TicketResponse {
1172 pub(crate) request: TicketData,
1173 pub(crate) response_status: ResponseStatus,
1174}
1175
1176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1177pub struct TicketData {
1178 pub id: TicketID,
1179 pub subject: String,
1180 pub description: Option<String>,
1181 pub status: Status,
1182 pub priority: Option<Priority>,
1183 pub created_time: TimeEntry,
1184 pub requester: Option<UserInfo>,
1185 pub account: Account,
1186 pub template: TemplateInfo,
1187 pub udf_fields: Option<Value>,
1188}
1189
1190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1191pub struct TemplateInfo {
1192 pub id: String,
1193 pub name: String,
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199 use serde_json::json;
1200
1201 #[test]
1202 fn criteria_default() {
1203 let criteria = Criteria::default();
1204 assert!(criteria.field.is_empty());
1205 assert!(matches!(criteria.condition, Condition::Is));
1206 assert!(criteria.value.is_null());
1207 assert!(criteria.children.is_empty());
1208 assert!(criteria.logical_operator.is_none());
1209 }
1210
1211 #[test]
1212 fn create_ticket_data_default() {
1213 let data = CreateTicketData::default();
1214 assert!(data.subject.is_empty());
1215 assert!(data.description.is_empty());
1216 assert!(data.requester.is_empty());
1217 assert_eq!(data.priority, "Low");
1218 assert!(data.udf_fields.is_null());
1219 assert!(data.account.is_empty());
1220 assert!(data.template.is_empty());
1221 }
1222
1223 #[test]
1224 fn create_ticket_data_serializes_name_fields_as_objects() {
1225 let data = CreateTicketData {
1226 subject: "test".to_string(),
1227 description: "body".to_string(),
1228 requester: "NETXP".to_string(),
1229 priority: "High".to_string(),
1230 udf_fields: json!({}),
1231 account: "SOC".to_string(),
1232 template: "SOC-with-alert-id".to_string(),
1233 };
1234
1235 let serialized = serde_json::to_value(&data).unwrap();
1236 println!("Serialized CreateTicketData: {}", serialized);
1237
1238 assert_eq!(serialized["requester"], json!({ "name": "NETXP" }));
1239 assert_eq!(serialized["priority"], json!({ "name": "High" }));
1240 assert_eq!(serialized["account"], json!({ "name": "SOC" }));
1241 assert_eq!(
1242 serialized["template"],
1243 json!({ "name": "SOC-with-alert-id" })
1244 );
1245 }
1246
1247 #[test]
1248 fn edit_ticket_data_serializes_optional_name_fields_as_objects() {
1249 let data = EditTicketData {
1250 subject: "test".to_string(),
1251 status: Status {
1252 id: "1".to_string(),
1253 name: "Open".to_string(),
1254 color: None,
1255 },
1256 description: None,
1257 requester: Some(UserInfo {
1258 id: UserID("123".to_string()),
1259 name: "NETXP".to_string(),
1260 ..Default::default()
1261 }),
1262 priority: Some(Priority::high()),
1263 udf_fields: None,
1264 };
1265
1266 let serialized = serde_json::to_value(&data).unwrap();
1267
1268 assert_eq!(serialized["requester"]["name"], "NETXP");
1269 assert_eq!(serialized["priority"]["name"], "High");
1270 assert!(serialized["description"].is_null());
1271 assert_eq!(serialized["status"]["name"], "Open");
1272 }
1273}