Skip to main content

sdp_request_client/
client.rs

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