Skip to main content

concur_api/api/
expense.rs

1//! SAP Concur Expense APIs v4.
2//!
3//! Covers Expense Reports, Expenses (entries), and Workflow.
4
5use crate::client::ConcurClient;
6use crate::error::Result;
7use crate::models::{
8    CustomData, ExchangeRate, ExpenseType, Link, Location, Money, PaymentType, Vendor,
9};
10use reqwest::Method;
11use serde::{Deserialize, Serialize};
12
13/// Expense Reports API.
14pub struct ReportsApi<'a> {
15    client: &'a ConcurClient,
16}
17
18impl<'a> ReportsApi<'a> {
19    pub fn new(client: &'a ConcurClient) -> Self {
20        Self { client }
21    }
22
23    fn base(&self, user_id: &str, context: &str) -> String {
24        format!(
25            "/expensereports/v4/users/{}/context/{}/reports",
26            user_id, context
27        )
28    }
29
30    /// Retrieve a report by ID.
31    pub async fn get(&self, user_id: &str, context: &str, report_id: &str) -> Result<Report> {
32        self.client
33            .request(
34                Method::GET,
35                &format!("{}/{}", self.base(user_id, context), report_id),
36            )
37            .await?
38            .send()
39            .await?
40            .json()
41            .await
42            .map_err(Into::into)
43    }
44
45    /// Create a new report.
46    pub async fn create(
47        &self,
48        user_id: &str,
49        context: &str,
50        report: &CreateReport,
51    ) -> Result<CreateReportResponse> {
52        let req = self
53            .client
54            .request(Method::POST, &self.base(user_id, context))
55            .await?;
56        let req = ConcurClient::with_json(req, report)?;
57        self.client.execute(req).await
58    }
59
60    /// Update an unsubmitted report (JSON Merge Patch).
61    pub async fn update(
62        &self,
63        user_id: &str,
64        context: &str,
65        report_id: &str,
66        patch: &CreateReport,
67    ) -> Result<()> {
68        let req = self
69            .client
70            .request(
71                Method::PATCH,
72                &format!("{}/{}", self.base(user_id, context), report_id),
73            )
74            .await?;
75        let req = ConcurClient::with_json(req, patch)?;
76        self.client.execute_unit(req).await
77    }
78
79    /// Delete an unsubmitted report.
80    pub async fn delete(&self, user_id: &str, context: &str, report_id: &str) -> Result<()> {
81        self.client
82            .execute_unit(
83                self.client
84                    .request(
85                        Method::DELETE,
86                        &format!("{}/{}", self.base(user_id, context), report_id),
87                    )
88                    .await?,
89            )
90            .await
91    }
92
93    /// Get configured form fields for a report.
94    pub async fn form_fields(
95        &self,
96        report_id: &str,
97        policy_id: Option<&str>,
98    ) -> Result<Vec<FormField>> {
99        let mut path = format!("/expensereports/v4/reports/{}/formFields", report_id);
100        if let Some(pid) = policy_id {
101            path.push_str(&format!("?policyId={}", urlencoding::encode(pid)));
102        }
103        self.client
104            .request(Method::GET, &path)
105            .await?
106            .send()
107            .await?
108            .json()
109            .await
110            .map_err(Into::into)
111    }
112}
113
114/// Expenses (entries) API.
115pub struct ExpensesApi<'a> {
116    client: &'a ConcurClient,
117}
118
119impl<'a> ExpensesApi<'a> {
120    pub fn new(client: &'a ConcurClient) -> Self {
121        Self { client }
122    }
123
124    fn base(&self, user_id: &str, context: &str, report_id: &str) -> String {
125        format!(
126            "/expensereports/v4/users/{}/context/{}/reports/{}/expenses",
127            user_id, context, report_id
128        )
129    }
130
131    /// List expenses on a report.
132    pub async fn list(
133        &self,
134        user_id: &str,
135        context: &str,
136        report_id: &str,
137    ) -> Result<Vec<Expense>> {
138        self.client
139            .request(Method::GET, &self.base(user_id, context, report_id))
140            .await?
141            .send()
142            .await?
143            .json()
144            .await
145            .map_err(Into::into)
146    }
147
148    /// Get a single expense.
149    pub async fn get(
150        &self,
151        user_id: &str,
152        context: &str,
153        report_id: &str,
154        expense_id: &str,
155    ) -> Result<Expense> {
156        self.client
157            .request(
158                Method::GET,
159                &format!("{}/{}", self.base(user_id, context, report_id), expense_id),
160            )
161            .await?
162            .send()
163            .await?
164            .json()
165            .await
166            .map_err(Into::into)
167    }
168
169    /// Create an expense on a report.
170    pub async fn create(
171        &self,
172        user_id: &str,
173        context: &str,
174        report_id: &str,
175        expense: &CreateExpense,
176    ) -> Result<CreateExpenseResponse> {
177        let req = self
178            .client
179            .request(Method::POST, &self.base(user_id, context, report_id))
180            .await?;
181        let req = ConcurClient::with_json(req, expense)?;
182        self.client.execute(req).await
183    }
184
185    /// Update an expense (JSON Merge Patch).
186    pub async fn update(
187        &self,
188        user_id: &str,
189        context: &str,
190        report_id: &str,
191        expense_id: &str,
192        patch: &CreateExpense,
193    ) -> Result<()> {
194        let req = self
195            .client
196            .request(
197                Method::PATCH,
198                &format!("{}/{}", self.base(user_id, context, report_id), expense_id),
199            )
200            .await?;
201        let req = ConcurClient::with_json(req, patch)?;
202        self.client.execute_unit(req).await
203    }
204
205    /// Delete an expense.
206    pub async fn delete(
207        &self,
208        user_id: &str,
209        context: &str,
210        report_id: &str,
211        expense_id: &str,
212    ) -> Result<()> {
213        self.client
214            .execute_unit(
215                self.client
216                    .request(
217                        Method::DELETE,
218                        &format!("{}/{}", self.base(user_id, context, report_id), expense_id),
219                    )
220                    .await?,
221            )
222            .await
223    }
224
225    /// List itemizations for an expense.
226    pub async fn itemizations(
227        &self,
228        user_id: &str,
229        context: &str,
230        report_id: &str,
231        expense_id: &str,
232    ) -> Result<Vec<serde_json::Value>> {
233        self.client
234            .request(
235                Method::GET,
236                &format!(
237                    "{}/{}/itemizations",
238                    self.base(user_id, context, report_id),
239                    expense_id
240                ),
241            )
242            .await?
243            .send()
244            .await?
245            .json()
246            .await
247            .map_err(Into::into)
248    }
249}
250
251/// Expense Workflow API.
252pub struct WorkflowApi<'a> {
253    client: &'a ConcurClient,
254}
255
256impl<'a> WorkflowApi<'a> {
257    pub fn new(client: &'a ConcurClient) -> Self {
258        Self { client }
259    }
260
261    /// Approve, send back, or recall a report.
262    pub async fn action(
263        &self,
264        report_id: &str,
265        action: ReportAction,
266        request: &WorkflowRequest,
267        user_id: Option<&str>,
268        context: Option<&str>,
269    ) -> Result<()> {
270        let path = match (user_id, context) {
271            (Some(uid), Some(ctx)) => format!(
272                "/expensereports/v4/users/{}/context/{}/reports/{}/{}",
273                uid,
274                ctx,
275                report_id,
276                action.path()
277            ),
278            _ => format!("/expensereports/v4/reports/{}/{}", report_id, action.path()),
279        };
280        let req = self.client.request(Method::PATCH, &path).await?;
281        let req = ConcurClient::with_json(req, request)?;
282        self.client.execute_unit(req).await
283    }
284
285    /// Submit a report.
286    pub async fn submit(&self, user_id: &str, report_id: &str) -> Result<()> {
287        self.client
288            .execute_unit(
289                self.client
290                    .request(
291                        Method::PATCH,
292                        &format!(
293                            "/expensereports/v4/users/{}/reports/{}/submit",
294                            user_id, report_id
295                        ),
296                    )
297                    .await?,
298            )
299            .await
300    }
301
302    /// Get cost objects for a report.
303    pub async fn cost_objects(&self, user_id: &str, report_id: &str) -> Result<Vec<CostObject>> {
304        self.client
305            .request(
306                Method::GET,
307                &format!(
308                    "/expensereports/v4/users/{}/reports/{}/costObjectsForApprover",
309                    user_id, report_id
310                ),
311            )
312            .await?
313            .send()
314            .await?
315            .json()
316            .await
317            .map_err(Into::into)
318    }
319}
320
321#[derive(Debug, Clone, Copy)]
322pub enum ReportAction {
323    Approve,
324    SendBack,
325    Recall,
326}
327
328impl ReportAction {
329    fn path(&self) -> &'static str {
330        match self {
331            ReportAction::Approve => "approve",
332            ReportAction::SendBack => "sendBack",
333            ReportAction::Recall => "recall",
334        }
335    }
336}
337
338#[derive(Debug, Clone, Default, Serialize, Deserialize)]
339pub struct WorkflowRequest {
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub comment: Option<String>,
342    #[serde(
343        rename = "expenseRejectedComment",
344        skip_serializing_if = "Option::is_none"
345    )]
346    pub expense_rejected_comment: Option<String>,
347    #[serde(rename = "expectedStepCode", skip_serializing_if = "Option::is_none")]
348    pub expected_step_code: Option<String>,
349    #[serde(
350        rename = "expectedStepSequence",
351        skip_serializing_if = "Option::is_none"
352    )]
353    pub expected_step_sequence: Option<String>,
354    #[serde(rename = "statusId", skip_serializing_if = "Option::is_none")]
355    pub status_id: Option<String>,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct Report {
360    #[serde(rename = "reportId")]
361    pub report_id: String,
362    pub name: Option<String>,
363    #[serde(rename = "businessPurpose")]
364    pub business_purpose: Option<String>,
365    #[serde(rename = "approvalStatus")]
366    pub approval_status: Option<String>,
367    #[serde(rename = "approvalStatusId")]
368    pub approval_status_id: Option<String>,
369    #[serde(rename = "paymentStatus")]
370    pub payment_status: Option<String>,
371    #[serde(rename = "paymentStatusId")]
372    pub payment_status_id: Option<String>,
373    #[serde(rename = "reportDate")]
374    pub report_date: Option<String>,
375    #[serde(rename = "startDate")]
376    pub start_date: Option<String>,
377    #[serde(rename = "endDate")]
378    pub end_date: Option<String>,
379    #[serde(rename = "policyId")]
380    pub policy_id: Option<String>,
381    #[serde(rename = "policy")]
382    pub policy_name: Option<String>,
383    #[serde(rename = "countryCode")]
384    pub country_code: Option<String>,
385    #[serde(rename = "countrySubDivisionCode")]
386    pub country_sub_division_code: Option<String>,
387    #[serde(rename = "reportTotal")]
388    pub report_total: Option<Money>,
389    #[serde(rename = "claimedAmount")]
390    pub claimed_amount: Option<Money>,
391    #[serde(rename = "approvedAmount")]
392    pub approved_amount: Option<Money>,
393    #[serde(rename = "customData")]
394    pub custom_data: Option<Vec<CustomData>>,
395    pub links: Option<Vec<Link>>,
396    #[serde(rename = "creationDate")]
397    pub creation_date: Option<String>,
398    #[serde(rename = "submitDate")]
399    pub submit_date: Option<String>,
400    #[serde(rename = "userId")]
401    pub user_id: Option<String>,
402}
403
404#[derive(Debug, Clone, Default, Serialize, Deserialize)]
405pub struct CreateReport {
406    pub name: Option<String>,
407    #[serde(rename = "businessPurpose")]
408    pub business_purpose: Option<String>,
409    pub comment: Option<String>,
410    #[serde(rename = "countryCode")]
411    pub country_code: Option<String>,
412    #[serde(rename = "countrySubDivisionCode")]
413    pub country_sub_division_code: Option<String>,
414    #[serde(rename = "policyId")]
415    pub policy_id: Option<String>,
416    #[serde(rename = "reportSource")]
417    pub report_source: Option<String>,
418    #[serde(rename = "customData")]
419    pub custom_data: Option<Vec<CustomData>>,
420}
421
422#[derive(Debug, Clone, Deserialize)]
423pub struct CreateReportResponse {
424    pub uri: String,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct Expense {
429    #[serde(rename = "expenseId")]
430    pub expense_id: String,
431    #[serde(rename = "businessPurpose")]
432    pub business_purpose: Option<String>,
433    #[serde(rename = "transactionDate")]
434    pub transaction_date: Option<String>,
435    #[serde(rename = "transactionAmount")]
436    pub transaction_amount: Option<Money>,
437    #[serde(rename = "postedAmount")]
438    pub posted_amount: Option<Money>,
439    #[serde(rename = "approvedAmount")]
440    pub approved_amount: Option<Money>,
441    #[serde(rename = "claimedAmount")]
442    pub claimed_amount: Option<Money>,
443    #[serde(rename = "expenseType")]
444    pub expense_type: Option<ExpenseType>,
445    #[serde(rename = "paymentType")]
446    pub payment_type: Option<PaymentType>,
447    pub location: Option<Location>,
448    pub vendor: Option<Vendor>,
449    #[serde(rename = "receiptImageId")]
450    pub receipt_image_id: Option<String>,
451    #[serde(rename = "ereceiptImageId")]
452    pub ereceipt_image_id: Option<String>,
453    #[serde(rename = "exchangeRate")]
454    pub exchange_rate: Option<ExchangeRate>,
455    #[serde(rename = "attendeeCount")]
456    pub attendee_count: Option<i32>,
457    #[serde(rename = "isPersonalExpense")]
458    pub is_personal_expense: Option<bool>,
459    #[serde(rename = "customData")]
460    pub custom_data: Option<Vec<CustomData>>,
461    pub links: Option<Vec<Link>>,
462}
463
464#[derive(Debug, Clone, Default, Serialize, Deserialize)]
465pub struct CreateExpense {
466    #[serde(rename = "expenseTypeId")]
467    pub expense_type_id: Option<String>,
468    #[serde(rename = "transactionDate")]
469    pub transaction_date: Option<String>,
470    #[serde(rename = "transactionAmount")]
471    pub transaction_amount: Option<Money>,
472    #[serde(rename = "businessPurpose")]
473    pub business_purpose: Option<String>,
474    #[serde(rename = "paymentTypeId")]
475    pub payment_type_id: Option<String>,
476    #[serde(rename = "locationId")]
477    pub location_id: Option<String>,
478    pub vendor: Option<Vendor>,
479    pub comment: Option<String>,
480    #[serde(rename = "customData")]
481    pub custom_data: Option<Vec<CustomData>>,
482}
483
484#[derive(Debug, Clone, Deserialize)]
485pub struct CreateExpenseResponse {
486    pub uri: String,
487}
488
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct FormField {
491    #[serde(rename = "fieldId")]
492    pub field_id: String,
493    #[serde(rename = "fieldName")]
494    pub field_name: Option<String>,
495    #[serde(rename = "controlType")]
496    pub control_type: Option<String>,
497    #[serde(rename = "dataType")]
498    pub data_type: Option<String>,
499    #[serde(rename = "fieldAccess")]
500    pub field_access: Option<String>,
501    #[serde(rename = "isRequired")]
502    pub is_required: Option<bool>,
503    #[serde(rename = "listId")]
504    pub list_id: Option<String>,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct CostObject {
509    pub name: String,
510    #[serde(rename = "approvedAmount")]
511    pub approved_amount: Money,
512    #[serde(rename = "claimedAmount")]
513    pub claimed_amount: Money,
514    #[serde(rename = "approverId")]
515    pub approver_id: String,
516    pub expenses: Vec<CostObjectExpense>,
517    #[serde(rename = "isOwnedByCaller")]
518    pub is_owned_by_caller: bool,
519    #[serde(rename = "isFullyApproved")]
520    pub is_fully_approved: bool,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
524pub struct CostObjectExpense {
525    #[serde(rename = "approvedAmount")]
526    pub approved_amount: Money,
527    #[serde(rename = "postedAmount")]
528    pub posted_amount: Money,
529    pub id: String,
530}