Skip to main content

concur_api/api/
request.rs

1//! SAP Concur Travel Request API v4.
2//!
3//! Covers Request, Workflow, Expected Expenses, and Allocations.
4
5use crate::client::ConcurClient;
6use crate::error::Result;
7use crate::models::{Link, Money, OwnerRef, StatusRef};
8use reqwest::Method;
9use serde::{Deserialize, Serialize};
10
11/// Request API.
12pub struct RequestApi<'a> {
13    client: &'a ConcurClient,
14}
15
16impl<'a> RequestApi<'a> {
17    pub fn new(client: &'a ConcurClient) -> Self {
18        Self { client }
19    }
20
21    /// List requests.
22    pub async fn list(&self, query: &RequestListQuery) -> Result<RequestList> {
23        let mut path = "/travelrequest/v4/requests".to_string();
24        let mut params: Vec<(String, String)> = Vec::new();
25        if let Some(v) = &query.view {
26            params.push(("view".to_string(), v.clone()));
27        }
28        if let Some(v) = &query.user_id {
29            params.push(("userId".to_string(), v.clone()));
30        }
31        if let Some(v) = query.start {
32            params.push(("start".to_string(), v.to_string()));
33        }
34        if let Some(v) = query.limit {
35            params.push(("limit".to_string(), v.to_string()));
36        }
37        if let Some(v) = &query.approved_before {
38            params.push(("approvedBefore".to_string(), v.clone()));
39        }
40        if let Some(v) = &query.approved_after {
41            params.push(("approvedAfter".to_string(), v.clone()));
42        }
43        if let Some(v) = &query.modified_before {
44            params.push(("modifiedBefore".to_string(), v.clone()));
45        }
46        if let Some(v) = &query.modified_after {
47            params.push(("modifiedAfter".to_string(), v.clone()));
48        }
49        if let Some(v) = &query.sort_field {
50            params.push(("sortField".to_string(), v.clone()));
51        }
52        if let Some(v) = &query.sort_order {
53            params.push(("sortOrder".to_string(), v.clone()));
54        }
55        if !params.is_empty() {
56            path.push('?');
57            path.push_str(
58                &params
59                    .iter()
60                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
61                    .collect::<Vec<_>>()
62                    .join("&"),
63            );
64        }
65        self.client
66            .request(Method::GET, &path)
67            .await?
68            .send()
69            .await?
70            .json()
71            .await
72            .map_err(Into::into)
73    }
74
75    /// Get a request by UUID.
76    pub async fn get(&self, request_uuid: &str, user_id: Option<&str>) -> Result<Request> {
77        let mut path = format!("/travelrequest/v4/requests/{}", request_uuid);
78        if let Some(uid) = user_id {
79            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
80        }
81        self.client
82            .request(Method::GET, &path)
83            .await?
84            .send()
85            .await?
86            .json()
87            .await
88            .map_err(Into::into)
89    }
90
91    /// Create a new request.
92    pub async fn create(&self, request: &Request, user_id: Option<&str>) -> Result<Request> {
93        let mut path = "/travelrequest/v4/requests".to_string();
94        if let Some(uid) = user_id {
95            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
96        }
97        let req = self.client.request(Method::POST, &path).await?;
98        let req = ConcurClient::with_json(req, request)?;
99        self.client.execute(req).await
100    }
101
102    /// Update a request.
103    pub async fn update(
104        &self,
105        request_uuid: &str,
106        request: &Request,
107        user_id: Option<&str>,
108    ) -> Result<Request> {
109        let mut path = format!("/travelrequest/v4/requests/{}", request_uuid);
110        if let Some(uid) = user_id {
111            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
112        }
113        let req = self.client.request(Method::PUT, &path).await?;
114        let req = ConcurClient::with_json(req, request)?;
115        self.client.execute(req).await
116    }
117
118    /// Delete a request.
119    pub async fn delete(&self, request_uuid: &str, user_id: Option<&str>) -> Result<()> {
120        let mut path = format!("/travelrequest/v4/requests/{}", request_uuid);
121        if let Some(uid) = user_id {
122            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
123        }
124        self.client
125            .execute_unit(self.client.request(Method::DELETE, &path).await?)
126            .await
127    }
128}
129
130/// Request Workflow API.
131pub struct RequestWorkflowApi<'a> {
132    client: &'a ConcurClient,
133}
134
135impl<'a> RequestWorkflowApi<'a> {
136    pub fn new(client: &'a ConcurClient) -> Self {
137        Self { client }
138    }
139
140    /// Perform a workflow action: submit, approve, recall, sendback, cancel, close, reopen.
141    pub async fn action(
142        &self,
143        request_uuid: &str,
144        action: &str,
145        comment: Option<&str>,
146        user_id: Option<&str>,
147        company_id: Option<&str>,
148    ) -> Result<Request> {
149        let mut path = format!("/travelrequest/v4/requests/{}/{}", request_uuid, action);
150        let mut params: Vec<(String, String)> = Vec::new();
151        if let Some(c) = comment {
152            params.push(("comment".to_string(), c.to_string()));
153        }
154        if let Some(uid) = user_id {
155            params.push(("userId".to_string(), uid.to_string()));
156        }
157        if let Some(cid) = company_id {
158            params.push(("companyID".to_string(), cid.to_string()));
159        }
160        if !params.is_empty() {
161            path.push('?');
162            path.push_str(
163                &params
164                    .iter()
165                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
166                    .collect::<Vec<_>>()
167                    .join("&"),
168            );
169        }
170        let body = comment.map(|c| serde_json::json!({ "comment": c }));
171        let req = self.client.request(Method::POST, &path).await?;
172        let req = match body {
173            Some(b) => ConcurClient::with_json(req, &b)?,
174            None => req,
175        };
176        self.client.execute(req).await
177    }
178}
179
180/// Request Expected Expenses API.
181pub struct ExpectedExpensesApi<'a> {
182    client: &'a ConcurClient,
183}
184
185impl<'a> ExpectedExpensesApi<'a> {
186    pub fn new(client: &'a ConcurClient) -> Self {
187        Self { client }
188    }
189
190    /// List expected expenses for a request.
191    pub async fn list(
192        &self,
193        request_uuid: &str,
194        user_id: Option<&str>,
195    ) -> Result<Vec<ExpectedExpense>> {
196        let mut path = format!("/travelrequest/v4/requests/{}/expenses", request_uuid);
197        if let Some(uid) = user_id {
198            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
199        }
200        self.client
201            .request(Method::GET, &path)
202            .await?
203            .send()
204            .await?
205            .json()
206            .await
207            .map_err(Into::into)
208    }
209
210    /// Create an expected expense.
211    pub async fn create(
212        &self,
213        request_uuid: &str,
214        expense: &ExpectedExpense,
215        user_id: Option<&str>,
216    ) -> Result<ExpectedExpense> {
217        let mut path = format!("/travelrequest/v4/requests/{}/expenses", request_uuid);
218        if let Some(uid) = user_id {
219            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
220        }
221        let req = self.client.request(Method::POST, &path).await?;
222        let req = ConcurClient::with_json(req, expense)?;
223        self.client.execute(req).await
224    }
225
226    /// Get an expected expense.
227    pub async fn get(&self, expense_uuid: &str, user_id: Option<&str>) -> Result<ExpectedExpense> {
228        let mut path = format!("/travelrequest/v4/expenses/{}", expense_uuid);
229        if let Some(uid) = user_id {
230            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
231        }
232        self.client
233            .request(Method::GET, &path)
234            .await?
235            .send()
236            .await?
237            .json()
238            .await
239            .map_err(Into::into)
240    }
241
242    /// Update an expected expense.
243    pub async fn update(
244        &self,
245        expense_uuid: &str,
246        expense: &ExpectedExpense,
247        user_id: Option<&str>,
248    ) -> Result<ExpectedExpense> {
249        let mut path = format!("/travelrequest/v4/expenses/{}", expense_uuid);
250        if let Some(uid) = user_id {
251            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
252        }
253        let req = self.client.request(Method::PUT, &path).await?;
254        let req = ConcurClient::with_json(req, expense)?;
255        self.client.execute(req).await
256    }
257
258    /// Delete an expected expense.
259    pub async fn delete(&self, expense_uuid: &str, user_id: Option<&str>) -> Result<()> {
260        let mut path = format!("/travelrequest/v4/expenses/{}", expense_uuid);
261        if let Some(uid) = user_id {
262            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
263        }
264        self.client
265            .execute_unit(self.client.request(Method::DELETE, &path).await?)
266            .await
267    }
268}
269
270/// Request Allocations API.
271pub struct RequestAllocationsApi<'a> {
272    client: &'a ConcurClient,
273}
274
275impl<'a> RequestAllocationsApi<'a> {
276    pub fn new(client: &'a ConcurClient) -> Self {
277        Self { client }
278    }
279
280    /// Get an allocation.
281    pub async fn get(
282        &self,
283        request_uuid: &str,
284        allocation_uuid: &str,
285        user_id: Option<&str>,
286    ) -> Result<Vec<RequestAllocation>> {
287        let mut path = format!(
288            "/travelrequest/v4/requests/{}/allocations/{}",
289            request_uuid, allocation_uuid
290        );
291        if let Some(uid) = user_id {
292            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
293        }
294        self.client
295            .request(Method::GET, &path)
296            .await?
297            .send()
298            .await?
299            .json()
300            .await
301            .map_err(Into::into)
302    }
303
304    /// Update an allocation.
305    pub async fn update(
306        &self,
307        request_uuid: &str,
308        allocation_uuid: &str,
309        allocation: &RequestAllocation,
310        user_id: Option<&str>,
311    ) -> Result<Vec<RequestAllocation>> {
312        let mut path = format!(
313            "/travelrequest/v4/requests/{}/allocations/{}",
314            request_uuid, allocation_uuid
315        );
316        if let Some(uid) = user_id {
317            path.push_str(&format!("?userId={}", urlencoding::encode(uid)));
318        }
319        let req = self.client.request(Method::PUT, &path).await?;
320        let req = ConcurClient::with_json(req, allocation)?;
321        self.client.execute(req).await
322    }
323}
324
325#[derive(Debug, Clone, Default)]
326pub struct RequestListQuery {
327    pub view: Option<String>,
328    pub user_id: Option<String>,
329    pub start: Option<u32>,
330    pub limit: Option<u32>,
331    pub approved_before: Option<String>,
332    pub approved_after: Option<String>,
333    pub modified_before: Option<String>,
334    pub modified_after: Option<String>,
335    pub sort_field: Option<String>,
336    pub sort_order: Option<String>,
337}
338
339#[derive(Debug, Clone, Deserialize)]
340pub struct RequestList {
341    pub data: Vec<Request>,
342    pub operations: Option<Vec<Link>>,
343}
344
345#[derive(Debug, Clone, Default, Serialize, Deserialize)]
346pub struct Request {
347    pub id: Option<String>,
348    pub href: Option<String>,
349    #[serde(rename = "requestId")]
350    pub request_id: Option<String>,
351    pub name: Option<String>,
352    #[serde(rename = "businessPurpose")]
353    pub business_purpose: Option<String>,
354    pub comment: Option<String>,
355    #[serde(rename = "approvalStatus")]
356    pub approval_status: Option<StatusRef>,
357    pub approved: Option<bool>,
358    #[serde(rename = "pendingApproval")]
359    pub pending_approval: Option<bool>,
360    #[serde(rename = "canceledPostApproval")]
361    pub canceled_post_approval: Option<bool>,
362    pub closed: Option<bool>,
363    pub owner: Option<OwnerRef>,
364    #[serde(rename = "startDate")]
365    pub start_date: Option<String>,
366    #[serde(rename = "endDate")]
367    pub end_date: Option<String>,
368    #[serde(rename = "startTime")]
369    pub start_time: Option<String>,
370    #[serde(rename = "endTime")]
371    pub end_time: Option<String>,
372    #[serde(rename = "creationDate")]
373    pub creation_date: Option<String>,
374    #[serde(rename = "lastModified")]
375    pub last_modified: Option<String>,
376    #[serde(rename = "submitDate")]
377    pub submit_date: Option<String>,
378    #[serde(rename = "totalApprovedAmount")]
379    pub total_approved_amount: Option<Money>,
380    #[serde(rename = "totalPostedAmount")]
381    pub total_posted_amount: Option<Money>,
382    #[serde(rename = "totalRemainingAmount")]
383    pub total_remaining_amount: Option<Money>,
384    #[serde(rename = "mainDestination")]
385    pub main_destination: Option<serde_json::Value>,
386    pub policy: Option<serde_json::Value>,
387    #[serde(rename = "travelAgency")]
388    pub travel_agency: Option<serde_json::Value>,
389    pub expenses: Option<Vec<serde_json::Value>>,
390    pub operations: Option<Vec<Link>>,
391}
392
393#[derive(Debug, Clone, Default, Serialize, Deserialize)]
394pub struct ExpectedExpense {
395    pub id: Option<String>,
396    pub href: Option<String>,
397    #[serde(rename = "expenseType")]
398    pub expense_type: Option<serde_json::Value>,
399    #[serde(rename = "transactionDate")]
400    pub transaction_date: Option<String>,
401    #[serde(rename = "transactionAmount")]
402    pub transaction_amount: Option<Money>,
403    #[serde(rename = "postedAmount")]
404    pub posted_amount: Option<Money>,
405    #[serde(rename = "approvedAmount")]
406    pub approved_amount: Option<Money>,
407    #[serde(rename = "remainingAmount")]
408    pub remaining_amount: Option<Money>,
409    #[serde(rename = "businessPurpose")]
410    pub business_purpose: Option<String>,
411    pub location: Option<serde_json::Value>,
412    #[serde(rename = "tripData")]
413    pub trip_data: Option<serde_json::Value>,
414    pub allocations: Option<Vec<RequestAllocation>>,
415}
416
417#[derive(Debug, Clone, Default, Serialize, Deserialize)]
418pub struct RequestAllocation {
419    #[serde(rename = "allocationId")]
420    pub allocation_id: Option<String>,
421    #[serde(rename = "expenseId")]
422    pub expense_id: Option<String>,
423    #[serde(rename = "allocationAmount")]
424    pub allocation_amount: Option<Money>,
425    #[serde(rename = "approvedAmount")]
426    pub approved_amount: Option<Money>,
427    #[serde(rename = "postedAmount")]
428    pub posted_amount: Option<Money>,
429    pub percentage: Option<i32>,
430    #[serde(rename = "percentEdited")]
431    pub percent_edited: Option<bool>,
432    #[serde(rename = "systemAllocation")]
433    pub system_allocation: Option<bool>,
434}