1use chrono::{DateTime, Local};
35use reqwest::Method;
36use serde_json::Value;
37
38use crate::{
39 ServiceDesk, TicketID,
40 client::{
41 Condition, CreateTicketData, Criteria, DetailedTicket, EditTicketData, ListInfo, LogicalOp,
42 NameWrapper, Note, NoteData, SearchRequest, TicketResponse, TicketSearchResponse,
43 },
44 error::Error,
45};
46
47pub struct TicketsClient<'a> {
49 pub(crate) client: &'a ServiceDesk,
50}
51
52impl<'a> TicketsClient<'a> {
53 pub fn search(self) -> TicketSearchBuilder<'a> {
55 TicketSearchBuilder {
56 client: self.client,
57 root_criteria: None,
58 children: vec![],
59 row_count: 100,
60 }
61 }
62
63 pub fn create(self) -> TicketCreateBuilder<'a> {
65 TicketCreateBuilder {
66 client: self.client,
67 subject: None,
68 description: None,
69 requester: None,
70 priority: "Low".to_string(),
71 account: None,
72 template: None,
73 udf_fields: None,
74 }
75 }
76}
77
78pub struct TicketClient<'a> {
80 pub(crate) client: &'a ServiceDesk,
81 pub(crate) id: TicketID,
82}
83
84impl<'a> TicketClient<'a> {
85 pub async fn get(self) -> Result<DetailedTicket, Error> {
87 self.client.ticket_details(self.id).await
88 }
89
90 pub async fn close(self, comment: &str) -> Result<(), Error> {
92 self.client.close_ticket(self.id, comment).await
93 }
94
95 pub async fn assign(self, technician: &str) -> Result<(), Error> {
97 self.client.assign_ticket(self.id, technician).await
98 }
99
100 pub async fn add_note(self, description: &str) -> Result<Note, Error> {
102 self.client
103 .add_note(
104 self.id,
105 &NoteData {
106 description: description.to_string(),
107 ..Default::default()
108 },
109 )
110 .await
111 }
112
113 pub fn note(self) -> NoteBuilder<'a> {
115 NoteBuilder {
116 client: self.client,
117 ticket_id: self.id,
118 description: String::new(),
119 mark_first_response: false,
120 add_to_linked_requests: false,
121 notify_technician: false,
122 show_to_requester: false,
123 }
124 }
125
126 pub async fn merge(self, ticket_ids: &[u64]) -> Result<(), Error> {
128 let ids: Vec<usize> = ticket_ids.iter().map(|id| *id as usize).collect();
129 self.client.merge(self.id.0 as usize, &ids).await
130 }
131
132 pub async fn edit(self, data: &EditTicketData) -> Result<(), Error> {
134 self.client.edit(self.id, data).await
135 }
136
137 pub async fn close_with_note(self, comment: &str) -> Result<(), Error> {
139 let id = self.id.clone();
140 self.client
141 .add_note(
142 id.clone(),
143 &NoteData {
144 description: comment.to_string(),
145 ..Default::default()
146 },
147 )
148 .await?;
149 self.client.close_ticket(id, comment).await
150 }
151}
152
153pub struct TicketSearchBuilder<'a> {
157 client: &'a ServiceDesk,
158 root_criteria: Option<Criteria>,
159 children: Vec<Criteria>,
160 row_count: u32,
161}
162
163#[derive(Debug)]
165pub enum TicketStatus {
166 Open,
167 Closed,
168 Cancelled,
169 OnHold,
170}
171
172impl std::fmt::Display for TicketStatus {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 let status_str = match self {
175 TicketStatus::Open => "Open",
176 TicketStatus::Closed => "Closed",
177 TicketStatus::Cancelled => "Cancelled",
178 TicketStatus::OnHold => "On Hold",
179 };
180 write!(f, "{}", status_str)
181 }
182}
183
184impl<'a> TicketSearchBuilder<'a> {
185 pub fn status(mut self, status: &str) -> Self {
187 self.root_criteria = Some(Criteria {
188 field: "status.name".to_string(),
189 condition: Condition::Is,
190 value: status.into(),
191 children: vec![],
192 logical_operator: None,
193 });
194 self
195 }
196
197 pub fn filter(self, filter: &TicketStatus) -> Self {
199 self.status(&filter.to_string())
200 }
201
202 pub fn open(self) -> Self {
204 self.status("Open")
205 }
206
207 pub fn closed(self) -> Self {
209 self.status("Closed")
210 }
211
212 pub fn created_after(mut self, time: DateTime<Local>) -> Self {
214 self.children.push(Criteria {
215 field: "created_time".to_string(),
216 condition: Condition::GreaterThan,
217 value: time.timestamp_millis().to_string().into(),
218 children: vec![],
219 logical_operator: Some(LogicalOp::And),
220 });
221 self
222 }
223
224 pub fn updated_after(mut self, time: DateTime<Local>) -> Self {
226 self.children.push(Criteria {
227 field: "last_updated_time".to_string(),
228 condition: Condition::GreaterThan,
229 value: time.timestamp_millis().to_string().into(),
230 children: vec![],
231 logical_operator: Some(LogicalOp::And),
232 });
233 self
234 }
235
236 pub fn subject_contains(mut self, value: &str) -> Self {
238 self.children.push(Criteria {
239 field: "subject".to_string(),
240 condition: Condition::Contains,
241 value: value.into(),
242 children: vec![],
243 logical_operator: Some(LogicalOp::And),
244 });
245 self
246 }
247
248 pub fn field_contains(mut self, field: &str, value: impl Into<Value>) -> Self {
250 self.children.push(Criteria {
251 field: field.to_string(),
252 condition: Condition::Contains,
253 value: value.into(),
254 children: vec![],
255 logical_operator: Some(LogicalOp::And),
256 });
257 self
258 }
259
260 pub fn field_equals(mut self, field: &str, value: impl Into<Value>) -> Self {
262 self.children.push(Criteria {
263 field: field.to_string(),
264 condition: Condition::Is,
265 value: value.into(),
266 children: vec![],
267 logical_operator: Some(LogicalOp::And),
268 });
269 self
270 }
271
272 pub fn limit(mut self, count: u32) -> Self {
274 self.row_count = count;
275 self
276 }
277
278 pub fn criteria(mut self, criteria: Criteria) -> Self {
280 if self.root_criteria.is_none() {
281 self.root_criteria = Some(criteria);
282 } else {
283 self.children.push(criteria);
284 }
285 self
286 }
287
288 pub async fn fetch(self) -> Result<Vec<DetailedTicket>, Error> {
290 let mut root = self.root_criteria.unwrap_or_else(|| Criteria {
291 field: "id".to_string(),
292 condition: Condition::GreaterThan,
293 value: "0".into(),
294 children: vec![],
295 logical_operator: None,
296 });
297
298 root.children = self.children;
299
300 let body = SearchRequest {
301 list_info: ListInfo {
302 row_count: self.row_count,
303 search_criteria: root,
304 },
305 };
306
307 let resp: Value = self
308 .client
309 .request_input_data(Method::GET, "/api/v3/requests", &body)
310 .await?;
311
312 let ticket_response: TicketSearchResponse = serde_json::from_value(resp)?;
313 Ok(ticket_response.requests)
314 }
315
316 pub async fn first(mut self) -> Result<Option<DetailedTicket>, Error> {
318 self.row_count = 1;
319 let results = self.fetch().await?;
320 Ok(results.into_iter().next())
321 }
322}
323
324pub struct TicketCreateBuilder<'a> {
329 client: &'a ServiceDesk,
330 subject: Option<String>,
331 description: Option<String>,
332 requester: Option<String>,
333 priority: String,
334 account: Option<String>,
335 template: Option<String>,
336 udf_fields: Option<Value>,
337}
338
339impl<'a> TicketCreateBuilder<'a> {
340 pub fn subject(mut self, subject: impl Into<String>) -> Self {
342 self.subject = Some(subject.into());
343 self
344 }
345
346 pub fn description(mut self, description: impl Into<String>) -> Self {
348 self.description = Some(description.into());
349 self
350 }
351
352 pub fn requester(mut self, requester: impl Into<String>) -> Self {
354 self.requester = Some(requester.into());
355 self
356 }
357
358 pub fn priority(mut self, priority: impl Into<String>) -> Self {
360 self.priority = priority.into();
361 self
362 }
363
364 pub fn account(mut self, account: impl Into<String>) -> Self {
366 self.account = Some(account.into());
367 self
368 }
369
370 pub fn template(mut self, template: impl Into<String>) -> Self {
372 self.template = Some(template.into());
373 self
374 }
375
376 pub fn udf_fields(mut self, fields: Value) -> Self {
378 self.udf_fields = Some(fields);
379 self
380 }
381
382 pub async fn send(self) -> Result<TicketResponse, Error> {
384 let subject = self
385 .subject
386 .ok_or_else(|| Error::Other("subject is required".to_string()))?;
387 let requester = self
388 .requester
389 .ok_or_else(|| Error::Other("requester is required".to_string()))?;
390
391 let data = CreateTicketData {
392 subject,
393 description: self.description.unwrap_or_default(),
394 requester: NameWrapper::new(requester),
395 priority: NameWrapper::new(self.priority),
396 account: NameWrapper::new(self.account.unwrap_or_default()),
397 template: NameWrapper::new(self.template.unwrap_or_default()),
398 udf_fields: self.udf_fields.unwrap_or(serde_json::json!({})),
399 };
400
401 self.client.create_ticket(&data).await
402 }
403}
404
405pub struct NoteBuilder<'a> {
409 client: &'a ServiceDesk,
410 ticket_id: TicketID,
411 description: String,
412 mark_first_response: bool,
413 add_to_linked_requests: bool,
414 notify_technician: bool,
415 show_to_requester: bool,
416}
417
418impl<'a> NoteBuilder<'a> {
419 pub fn description(mut self, description: impl Into<String>) -> Self {
421 self.description = description.into();
422 self
423 }
424
425 pub fn mark_first_response(mut self) -> Self {
427 self.mark_first_response = true;
428 self
429 }
430
431 pub fn add_to_linked_requests(mut self) -> Self {
433 self.add_to_linked_requests = true;
434 self
435 }
436
437 pub fn notify_technician(mut self) -> Self {
439 self.notify_technician = true;
440 self
441 }
442
443 pub fn show_to_requester(mut self) -> Self {
445 self.show_to_requester = true;
446 self
447 }
448
449 pub async fn send(self) -> Result<Note, Error> {
451 let note = NoteData {
452 description: self.description,
453 mark_first_response: self.mark_first_response,
454 add_to_linked_requests: self.add_to_linked_requests,
455 notify_technician: self.notify_technician,
456 show_to_requester: self.show_to_requester,
457 };
458
459 let note = self.client.add_note(self.ticket_id, ¬e).await?;
460 Ok(note)
461 }
462}
463
464impl ServiceDesk {
465 pub fn tickets(&self) -> TicketsClient<'_> {
467 TicketsClient { client: self }
468 }
469
470 pub fn ticket(&self, id: impl Into<TicketID>) -> TicketClient<'_> {
472 TicketClient {
473 client: self,
474 id: id.into(),
475 }
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn ticket_status_display() {
485 assert_eq!(TicketStatus::Open.to_string(), "Open");
486 assert_eq!(TicketStatus::Closed.to_string(), "Closed");
487 assert_eq!(TicketStatus::Cancelled.to_string(), "Cancelled");
488 assert_eq!(TicketStatus::OnHold.to_string(), "On Hold");
489 }
490}