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 add_worklog(
369 &self,
370 ticket_id: impl Into<TicketID>,
371 worklog: &WorklogData,
372 ) -> Result<Value, Error> {
373 let ticket_id = ticket_id.into();
374 tracing::info!(ticket_id = %ticket_id, "adding worklog");
375 let resp: Value = self
376 .request_input_data(
377 Method::POST,
378 &format!("/api/v3/requests/{}/worklogs", ticket_id),
379 &AddWorklogRequest { worklog },
380 )
381 .await?;
382 Ok(resp)
383 }
384
385 pub async fn get_note(
387 &self,
388 ticket_id: impl Into<TicketID>,
389 note_id: impl Into<NoteID>,
390 ) -> Result<Note, Error> {
391 let ticket_id = ticket_id.into();
392 let note_id = note_id.into();
393 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "fetching note");
394 let url = format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id);
395 let resp: NoteResponse = self.request(Method::GET, &url, &"").await?;
396 Ok(resp.note)
397 }
398
399 pub async fn list_notes(
401 &self,
402 ticket_id: impl Into<TicketID>,
403 row_count: Option<u32>,
404 start_index: Option<u32>,
405 ) -> Result<Vec<Note>, Error> {
406 let ticket_id = ticket_id.into();
407 tracing::info!(ticket_id = %ticket_id, "listing notes");
408 let body = ListNotesRequest {
409 list_info: NotesListInfo {
410 row_count: row_count.unwrap_or(100),
411 start_index: start_index.unwrap_or(1),
412 },
413 };
414 let resp: Value = self
415 .request_input_data(
416 Method::GET,
417 &format!("/api/v3/requests/{}/notes", ticket_id),
418 &body,
419 )
420 .await?;
421 let resp: NotesListResponse = serde_json::from_value(resp)?;
422 Ok(resp.notes)
423 }
424
425 pub async fn edit_note(
427 &self,
428 ticket_id: impl Into<TicketID>,
429 note_id: impl Into<NoteID>,
430 note: &NoteData,
431 ) -> Result<Note, Error> {
432 let ticket_id = ticket_id.into();
433 let note_id = note_id.into();
434 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "editing note");
435 let resp: NoteResponse = self
436 .request_input_data(
437 Method::PUT,
438 &format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id),
439 &EditNoteRequest { request_note: note },
440 )
441 .await?;
442 Ok(resp.note)
443 }
444
445 pub async fn delete_note(
447 &self,
448 ticket_id: impl Into<TicketID>,
449 note_id: impl Into<NoteID>,
450 ) -> Result<(), Error> {
451 let ticket_id = ticket_id.into();
452 let note_id = note_id.into();
453 tracing::info!(ticket_id = %ticket_id, note_id = %note_id, "deleting note");
454 let _: SdpGenericResponse = self
455 .request(
456 Method::DELETE,
457 &format!("/api/v3/requests/{}/notes/{}", ticket_id, note_id),
458 &"",
459 )
460 .await?;
461 Ok(())
462 }
463
464 pub async fn assign_ticket(
466 &self,
467 ticket_id: impl Into<TicketID>,
468 technician_name: &str,
469 ) -> Result<(), Error> {
470 let ticket_id = ticket_id.into();
471 tracing::info!(ticket_id = %ticket_id, technician = %technician_name, "assigning ticket");
472 let _: SdpGenericResponse = self
473 .request_input_data(
474 Method::PUT,
475 &format!("/api/v3/requests/{}/assign", ticket_id),
476 &AssignTicketRequest {
477 request: AssignTicketData {
478 technician: technician_name.to_string(),
479 },
480 },
481 )
482 .await?;
483 Ok(())
484 }
485
486 pub async fn create_ticket(&self, data: &CreateTicketData) -> Result<TicketData, Error> {
488 tracing::info!(subject = %data.subject, "creating ticket");
489 let resp: TicketResponse = self
490 .request_input_data(
491 Method::POST,
492 "/api/v3/requests",
493 &CreateTicketRequest { request: data },
494 )
495 .await?;
496 Ok(resp.request)
497 }
498
499 pub async fn search_tickets(&self, criteria: Criteria) -> Result<Vec<DetailedTicket>, Error> {
506 tracing::info!("searching tickets");
507 let resp = self
508 .request_input_data(
509 Method::GET,
510 "/api/v3/requests",
511 &SearchRequest {
512 list_info: ListInfo {
513 row_count: 100,
514 search_criteria: criteria,
515 },
516 },
517 )
518 .await?;
519
520 let ticket_response: TicketSearchResponse = serde_json::from_value(resp)?;
521
522 Ok(ticket_response.requests)
523 }
524
525 pub async fn close_ticket(
527 &self,
528 ticket_id: impl Into<TicketID>,
529 closure_comments: &str,
530 ) -> Result<(), Error> {
531 let ticket_id = ticket_id.into();
532 tracing::info!(ticket_id = %ticket_id, "closing ticket");
533 let _: SdpGenericResponse = self
534 .request_json(
535 Method::PUT,
536 &format!("/api/v3/requests/{}/close", ticket_id),
537 &CloseTicketRequest {
538 request: CloseTicketData {
539 closure_info: ClosureInfo {
540 closure_comments: closure_comments.to_string(),
541 closure_code: "Closed".to_string(),
542 },
543 },
544 },
545 )
546 .await?;
547 Ok(())
548 }
549
550 pub async fn merge(
554 &self,
555 ticket_id: impl Into<TicketID>,
556 merge_ids: &[TicketID],
557 ) -> Result<(), Error> {
558 let ticket_id = ticket_id.into();
559 tracing::info!(ticket_id = %ticket_id, count = merge_ids.len(), "merging tickets");
560 if merge_ids.len() > 49 {
561 tracing::warn!("attempted to merge more than 49 tickets");
562 return Err(Error::from_sdp(
563 400,
564 "Cannot merge more than 49 tickets at once".to_string(),
565 None,
566 ));
567 }
568 let merge_requests: Vec<MergeRequestId> = merge_ids
569 .iter()
570 .map(|id| MergeRequestId {
571 id: id.0.to_string(),
572 })
573 .collect();
574
575 let _: SdpGenericResponse = self
576 .request_form(
577 Method::PUT,
578 &format!("/api/v3/requests/{}/merge_requests", ticket_id),
579 &MergeTicketsRequest { merge_requests },
580 )
581 .await?;
582 Ok(())
583 }
584}
585
586use serde::Deserialize;
587use serde_json::Value;
588
589use crate::builders::WorklogData;
590use crate::{NoteID, ServiceDesk, TicketID, UserID, error::Error};
591
592#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
593pub(crate) struct SearchRequest {
594 pub(crate) list_info: ListInfo,
595}
596
597#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
598pub struct ListInfo {
599 pub row_count: u32,
600 pub search_criteria: Criteria,
601}
602
603#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
608pub struct Criteria {
609 pub field: String,
610 pub condition: Condition,
611 pub value: Value,
612
613 #[serde(skip_serializing_if = "Vec::is_empty")]
614 pub children: Vec<Criteria>,
615
616 #[serde(skip_serializing_if = "Option::is_none")]
617 pub logical_operator: Option<LogicalOp>,
618}
619
620impl Default for Criteria {
621 fn default() -> Self {
622 Criteria {
623 field: String::new(),
624 condition: Condition::Is,
625 value: Value::Null,
626 children: vec![],
627 logical_operator: None,
628 }
629 }
630}
631
632#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
635#[serde(rename_all = "snake_case")]
636pub enum Condition {
637 #[serde(rename = "is")]
638 Is,
639 #[serde(rename = "greater than")]
640 GreaterThan,
641 #[serde(rename = "lesser than")]
642 LesserThan,
643 #[serde(rename = "contains")]
644 Contains,
645}
646
647#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
649pub enum LogicalOp {
650 #[serde(rename = "AND")]
651 And,
652 #[serde(rename = "OR")]
653 Or,
654}
655
656#[derive(Deserialize, Serialize, Debug, PartialEq)]
657pub struct TicketSearchResponse {
658 pub requests: Vec<DetailedTicket>,
659}
660
661#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
662pub struct Account {
663 pub id: String,
664 pub name: String,
665}
666
667#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
668struct DetailedTicketResponse {
669 request: DetailedTicket,
670 #[serde(skip_serializing)]
671 response_status: ResponseStatus,
672}
673
674#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
675#[serde(rename = "request")]
676pub struct DetailedTicket {
677 pub id: TicketID,
678 pub subject: String,
679 pub description: Option<String>,
680 pub status: Status,
681 pub priority: Option<Priority>,
682 pub requester: Option<UserInfo>,
683 pub technician: Option<UserInfo>,
684 #[serde(skip_serializing)]
685 pub created_by: UserInfo,
686 pub created_time: TimeEntry,
687 pub resolution: Option<Resolution>,
688 pub due_by_time: Option<TimeEntry>,
689 pub resolved_time: Option<TimeEntry>,
690 pub completed_time: Option<TimeEntry>,
691 pub udf_fields: Option<Value>,
692 pub attachments: Option<Vec<Attachment>>,
693 pub closure_info: Option<Value>,
694 pub site: Option<Value>,
695 pub department: Option<Value>,
696 pub account: Option<Value>,
697}
698
699#[derive(Serialize, Debug)]
700struct EditTicketRequest<'a> {
701 request: &'a EditTicketData,
702}
703
704#[derive(Serialize, Deserialize, Debug, PartialEq)]
712pub struct EditTicketData {
713 pub subject: String,
714 pub status: Status,
715 pub description: Option<String>,
716 pub requester: Option<UserInfo>,
717 pub priority: Option<Priority>,
718 pub udf_fields: Option<Value>,
720}
721
722impl From<DetailedTicket> for EditTicketData {
723 fn from(value: DetailedTicket) -> Self {
724 Self {
725 subject: value.subject,
726 status: value.status,
727 description: value.description,
728 requester: value.requester,
729 priority: value.priority,
730 udf_fields: value.udf_fields,
731 }
732 }
733}
734
735#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
736pub(crate) struct ResponseStatus {
737 pub(crate) status: String,
738 pub(crate) status_code: i64,
739}
740
741pub const STATUS_ID_OPEN: u64 = 2;
742pub const STATUS_ID_ASSIGNED: u64 = 5;
743pub const STATUS_ID_CANCELLED: u64 = 7;
744pub const STATUS_ID_CLOSED: u64 = 1;
745pub const STATUS_ID_IN_PROGRESS: u64 = 6;
746pub const STATUS_ID_ONHOLD: u64 = 3;
747pub const STATUS_ID_RESOLVED: u64 = 4;
748
749#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750pub struct Status {
751 pub id: String,
752 pub name: String,
753 pub color: Option<String>,
754}
755
756impl Status {
757 pub fn open() -> Self {
758 Status {
759 id: STATUS_ID_OPEN.to_string(),
760 name: "Open".to_string(),
761 color: Some("#0066ff".to_string()),
762 }
763 }
764
765 pub fn assigned() -> Self {
766 Status {
767 id: STATUS_ID_ASSIGNED.to_string(),
768 name: "Assigned".to_string(),
769 color: Some("#0000ff".to_string()),
771 }
772 }
773
774 pub fn cancelled() -> Self {
775 Status {
776 id: STATUS_ID_CANCELLED.to_string(),
777 name: "Cancelled".to_string(),
778 color: Some("#999999".to_string()),
780 }
781 }
782
783 pub fn closed() -> Self {
784 Status {
785 id: STATUS_ID_CLOSED.to_string(),
786 name: "Closed".to_string(),
787 color: Some("#006600".to_string()),
788 }
789 }
790
791 pub fn in_progress() -> Self {
792 Status {
793 id: STATUS_ID_IN_PROGRESS.to_string(),
794 name: "In Progress".to_string(),
795 color: Some("#00ffcc".to_string()),
796 }
797 }
798
799 pub fn onhold() -> Self {
800 Status {
801 id: STATUS_ID_ONHOLD.to_string(),
802 name: "On Hold".to_string(),
803 color: Some("#ff0000".to_string()),
804 }
805 }
806
807 pub fn resolved() -> Self {
808 Status {
809 id: STATUS_ID_RESOLVED.to_string(),
810 name: "Resolved".to_string(),
811 color: Some("#00ff66".to_string()),
812 }
813 }
814}
815
816#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
821pub struct Priority {
822 pub id: String,
823 pub name: String,
824 pub color: Option<String>,
825}
826
827pub const PRIORITY_ID_LOW: u64 = 1;
828pub const PRIORITY_ID_MEDIUM: u64 = 3;
829pub const PRIORITY_ID_HIGH: u64 = 4;
830pub const PRIORITY_ID_CRITICAL: u64 = 301;
831
832impl Priority {
871 pub fn low() -> Self {
872 Priority {
873 id: PRIORITY_ID_LOW.to_string(),
874 name: "Low".to_string(),
875 color: Some("#288251".to_string()),
876 }
877 }
878
879 pub fn medium() -> Self {
880 Priority {
881 id: PRIORITY_ID_MEDIUM.to_string(),
882 name: "Medium".to_string(),
883 color: Some("#efb116".to_string()),
884 }
885 }
886
887 pub fn high() -> Self {
888 Priority {
889 id: PRIORITY_ID_HIGH.to_string(),
890 name: "High".to_string(),
891 color: Some("#ff5e00".to_string()),
892 }
893 }
894
895 pub fn critical() -> Self {
898 Priority {
899 id: PRIORITY_ID_CRITICAL.to_string(),
900 name: "Critical".to_string(),
901 color: Some("#8b0808".to_string()),
902 }
903 }
904}
905
906#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
907pub struct UserInfo {
908 pub id: UserID,
909 pub name: String,
910 pub email_id: Option<String>,
911 pub account: Option<Value>,
912 pub department: Option<Value>,
913 #[serde(default)]
914 pub is_vipuser: bool,
915 pub mobile: Option<String>,
916 pub org_user_status: Option<String>,
917 pub phone: Option<String>,
918 #[serde(skip_serializing)]
919 pub profile_pic: Option<Value>,
920}
921
922#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
923pub struct Resolution {
924 pub content: Option<String>,
925 pub submitted_by: Option<UserInfo>,
926 pub submitted_on: Option<TimeEntry>,
927 pub resolution_attachments: Option<Vec<Attachment>>,
928}
929
930#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
931pub struct Attachment {
932 pub id: String,
933 pub name: String,
934 pub content_url: String,
935 pub content_type: Option<String>,
936 pub description: Option<String>,
937 pub module: Option<String>,
938 pub size: Option<SizeInfo>,
939 pub attached_by: Option<UserInfo>,
940 pub attached_on: Option<TimeEntry>,
941}
942
943#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
944pub struct SizeInfo {
945 pub display_value: String,
946 pub value: u64,
947}
948
949#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
950pub struct TimeEntry {
951 pub display_value: String,
952 pub value: String,
953}
954
955#[derive(Serialize, Debug)]
956struct CreateTicketRequest<'a> {
957 request: &'a CreateTicketData,
958}
959
960#[derive(Serialize, Deserialize, Debug, PartialEq)]
961pub struct CreateTicketData {
962 pub subject: String,
963 pub description: String,
964 #[serde(
965 serialize_with = "serialize_name_object",
966 deserialize_with = "deserialize_name_object"
967 )]
968 pub requester: String,
969 #[serde(
970 serialize_with = "serialize_name_object",
971 deserialize_with = "deserialize_name_object"
972 )]
973 pub priority: String,
974 pub udf_fields: Value,
978 #[serde(
979 serialize_with = "serialize_name_object",
980 deserialize_with = "deserialize_name_object"
981 )]
982 pub account: String,
983 #[serde(
984 serialize_with = "serialize_name_object",
985 deserialize_with = "deserialize_name_object"
986 )]
987 pub template: String,
988}
989
990impl Default for CreateTicketData {
991 fn default() -> Self {
992 CreateTicketData {
993 subject: String::new(),
994 description: String::new(),
995 requester: String::new(),
996 priority: "Low".to_string(),
997 udf_fields: Value::Null,
998 account: String::new(),
999 template: String::new(),
1000 }
1001 }
1002}
1003
1004pub(crate) fn deserialize_name_object<'de, D>(deserializer: D) -> Result<String, D::Error>
1005where
1006 D: Deserializer<'de>,
1007{
1008 #[derive(Deserialize)]
1009 struct NameObject {
1010 name: String,
1011 }
1012
1013 Ok(NameObject::deserialize(deserializer)?.name)
1014}
1015
1016pub(crate) fn serialize_name_object<S>(name: &String, serializer: S) -> Result<S::Ok, S::Error>
1017where
1018 S: Serializer,
1019{
1020 let mut s = serializer.serialize_struct("NameWrapper", 1)?;
1021 s.serialize_field("name", name)?;
1022 s.end()
1023}
1024
1025#[allow(dead_code)]
1026#[derive(Serialize, Debug, PartialEq, Eq)]
1027pub(crate) struct NameWrapper {
1028 pub(crate) name: String,
1029}
1030
1031impl From<&str> for NameWrapper {
1032 fn from(name: &str) -> Self {
1033 Self {
1034 name: name.to_string(),
1035 }
1036 }
1037}
1038
1039impl From<String> for NameWrapper {
1040 fn from(name: String) -> Self {
1041 Self { name }
1042 }
1043}
1044
1045impl std::ops::Deref for NameWrapper {
1046 type Target = String;
1047
1048 fn deref(&self) -> &Self::Target {
1049 &self.name
1050 }
1051}
1052
1053impl std::ops::DerefMut for NameWrapper {
1054 fn deref_mut(&mut self) -> &mut Self::Target {
1055 &mut self.name
1056 }
1057}
1058
1059#[derive(Serialize, Debug, PartialEq, Eq)]
1060struct CloseTicketRequest {
1061 request: CloseTicketData,
1062}
1063
1064#[derive(Serialize, Debug, PartialEq, Eq)]
1065struct CloseTicketData {
1066 closure_info: ClosureInfo,
1067}
1068
1069#[derive(Serialize, Debug, PartialEq, Eq)]
1070struct ClosureInfo {
1071 closure_comments: String,
1072 closure_code: String,
1073}
1074
1075#[derive(Serialize, Debug)]
1076struct AddNoteRequest<'a> {
1077 note: &'a NoteData,
1078}
1079
1080#[derive(Serialize, Debug)]
1081struct AddWorklogRequest<'a> {
1082 worklog: &'a WorklogData,
1083}
1084
1085#[derive(Serialize, Debug, Default, PartialEq, Eq)]
1086pub struct NoteData {
1087 pub mark_first_response: bool,
1088 pub add_to_linked_requests: bool,
1089 pub notify_technician: bool,
1090 pub show_to_requester: bool,
1091 pub description: String,
1092}
1093
1094#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1096pub(crate) struct NoteResponse {
1097 pub(crate) note: Note,
1098}
1099
1100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1101pub struct NotesListResponse {
1102 pub list_info: Option<ListInfoResponse>,
1103 pub notes: Vec<Note>,
1104 pub response_status: Vec<ResponseStatus>,
1105}
1106
1107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1108pub struct ListInfoResponse {
1109 pub has_more_rows: bool,
1110 pub page: u32,
1111 pub row_count: u32,
1112 pub sort_field: String,
1113 pub sort_order: String,
1114 pub start_index: u32,
1115}
1116
1117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1118pub struct Note {
1119 pub id: NoteID,
1120 #[serde(default)]
1121 pub description: String,
1122 #[serde(default)]
1123 pub show_to_requester: bool,
1124 #[serde(default)]
1125 pub mark_first_response: bool,
1126 #[serde(default)]
1127 pub notify_technician: bool,
1128 #[serde(default)]
1129 pub add_to_linked_requests: bool,
1130 pub created_time: Option<TimeEntry>,
1131 pub created_by: Option<UserInfo>,
1132 pub last_updated_time: Option<TimeEntry>,
1133}
1134
1135#[derive(Serialize, Debug)]
1136struct EditNoteRequest<'a> {
1137 request_note: &'a NoteData,
1138}
1139
1140#[derive(Serialize, Debug)]
1141struct ListNotesRequest {
1142 list_info: NotesListInfo,
1143}
1144
1145#[derive(Debug, PartialEq, Eq, Deserialize)]
1146struct ConversationsResponse {
1147 #[serde(default)]
1148 conversations: Vec<ConversationSummary>,
1149}
1150
1151#[derive(Debug, PartialEq, Eq, Deserialize)]
1152struct ConversationSummary {
1153 #[serde(default)]
1154 has_attachments: bool,
1155 #[serde(default)]
1156 content_url: Option<String>,
1157}
1158
1159fn normalize_attachment_url(base_url: &reqwest::Url, value: &str) -> Result<String, Error> {
1160 Ok(base_url.join(value)?.to_string())
1161}
1162
1163#[derive(Serialize, Debug, PartialEq, Eq)]
1164struct NotesListInfo {
1165 row_count: u32,
1166 start_index: u32,
1167}
1168
1169#[derive(Serialize, Debug, PartialEq, Eq)]
1170struct AssignTicketRequest {
1171 request: AssignTicketData,
1172}
1173
1174#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
1175struct AssignTicketData {
1176 #[serde(
1177 serialize_with = "serialize_name_object",
1178 deserialize_with = "deserialize_name_object"
1179 )]
1180 technician: String,
1181}
1182
1183#[derive(Serialize, Debug, PartialEq, Eq)]
1184struct MergeTicketsRequest {
1185 merge_requests: Vec<MergeRequestId>,
1186}
1187
1188#[derive(Serialize, Debug, PartialEq, Eq)]
1189struct MergeRequestId {
1190 id: String,
1191}
1192
1193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1194pub(crate) struct TicketResponse {
1195 pub(crate) request: TicketData,
1196 pub(crate) response_status: ResponseStatus,
1197}
1198
1199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1200pub struct TicketData {
1201 pub id: TicketID,
1202 pub subject: String,
1203 pub description: Option<String>,
1204 pub status: Status,
1205 pub priority: Option<Priority>,
1206 pub created_time: TimeEntry,
1207 pub requester: Option<UserInfo>,
1208 pub account: Account,
1209 pub template: TemplateInfo,
1210 pub udf_fields: Option<Value>,
1211}
1212
1213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1214pub struct TemplateInfo {
1215 pub id: String,
1216 pub name: String,
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221 use super::*;
1222 use serde_json::json;
1223
1224 #[test]
1225 fn criteria_default() {
1226 let criteria = Criteria::default();
1227 assert!(criteria.field.is_empty());
1228 assert!(matches!(criteria.condition, Condition::Is));
1229 assert!(criteria.value.is_null());
1230 assert!(criteria.children.is_empty());
1231 assert!(criteria.logical_operator.is_none());
1232 }
1233
1234 #[test]
1235 fn create_ticket_data_default() {
1236 let data = CreateTicketData::default();
1237 assert!(data.subject.is_empty());
1238 assert!(data.description.is_empty());
1239 assert!(data.requester.is_empty());
1240 assert_eq!(data.priority, "Low");
1241 assert!(data.udf_fields.is_null());
1242 assert!(data.account.is_empty());
1243 assert!(data.template.is_empty());
1244 }
1245
1246 #[test]
1247 fn create_ticket_data_serializes_name_fields_as_objects() {
1248 let data = CreateTicketData {
1249 subject: "test".to_string(),
1250 description: "body".to_string(),
1251 requester: "NETXP".to_string(),
1252 priority: "High".to_string(),
1253 udf_fields: json!({}),
1254 account: "SOC".to_string(),
1255 template: "SOC-with-alert-id".to_string(),
1256 };
1257
1258 let serialized = serde_json::to_value(&data).unwrap();
1259 println!("Serialized CreateTicketData: {}", serialized);
1260
1261 assert_eq!(serialized["requester"], json!({ "name": "NETXP" }));
1262 assert_eq!(serialized["priority"], json!({ "name": "High" }));
1263 assert_eq!(serialized["account"], json!({ "name": "SOC" }));
1264 assert_eq!(
1265 serialized["template"],
1266 json!({ "name": "SOC-with-alert-id" })
1267 );
1268 }
1269
1270 #[test]
1271 fn edit_ticket_data_serializes_optional_name_fields_as_objects() {
1272 let data = EditTicketData {
1273 subject: "test".to_string(),
1274 status: Status {
1275 id: "1".to_string(),
1276 name: "Open".to_string(),
1277 color: None,
1278 },
1279 description: None,
1280 requester: Some(UserInfo {
1281 id: UserID("123".to_string()),
1282 name: "NETXP".to_string(),
1283 ..Default::default()
1284 }),
1285 priority: Some(Priority::high()),
1286 udf_fields: None,
1287 };
1288
1289 let serialized = serde_json::to_value(&data).unwrap();
1290
1291 assert_eq!(serialized["requester"]["name"], "NETXP");
1292 assert_eq!(serialized["priority"]["name"], "High");
1293 assert!(serialized["description"].is_null());
1294 assert_eq!(serialized["status"]["name"], "Open");
1295 }
1296}