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