square_ox/api/
terminal.rs

1/*!
2Terminals functionality of the [Square API](https://developer.squareup.com).
3 */
4
5use crate::api::{SquareAPI, Verb};
6use crate::client::SquareClient;
7use crate::errors::{SquareError, ValidationError};
8use crate::objects::{DeviceCheckoutOptions, Money, PaymentOptions, TerminalCheckout,
9                     TerminalCheckoutQuery, TerminalRefund, TerminalRefundQuery};
10use crate::objects::enums::{CheckoutOptionsPaymentType, TerminalCheckoutStatus};
11use crate::response::SquareResponse;
12
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15use crate::objects::TimeRange;
16use crate::builder::{AddField, Builder, ParentBuilder, Validate, Buildable};
17
18impl SquareClient {
19    pub fn terminal(&self) -> Terminal {
20        Terminal {
21            client: &self
22        }
23    }
24}
25
26pub struct Terminal<'a> {
27    client: &'a SquareClient,
28}
29
30impl<'a> Terminal<'a> {
31    /// Creates a Terminal checkout request and sends it to the specified device to take a payment
32    /// for the requested amount.
33    /// [Open in API Reference](https://developer.squareup.com/reference/square/terminal/create-terminal-checkout)
34        pub async fn create_checkout(self, body: CreateTerminalCheckoutBody)
35                              -> Result<SquareResponse, SquareError>{
36        self.client.request(
37            Verb::POST,
38            SquareAPI::Terminals("/checkouts".to_string()),
39            Some(&body),
40            None,
41        ).await
42    }
43
44    /// Returns a filtered list of Terminal checkout requests created by the application making the
45    /// request. <br/>
46    /// Only Terminal checkout requests created for the merchant scoped to the OAuth token are
47    /// returned. Terminal checkout requests are available for 30 days.
48    pub async fn search_checkout(self, body: SearchTerminalCheckoutBody)
49                              -> Result<SquareResponse, SquareError>{
50        self.client.request(
51            Verb::GET,
52            SquareAPI::Terminals("/checkouts/search".to_string()),
53            Some(&body),
54            None,
55        ).await
56    }
57
58    /// Retrieves a Terminal checkout request by `checkout_id`.<br/>
59    /// Terminal checkout requests are available for 30 days.
60    pub async fn get_checkout(self, checkout_id: String)
61                              -> Result<SquareResponse, SquareError>{
62        self.client.request(
63            Verb::GET,
64            SquareAPI::Terminals(format!("/checkouts/{}", checkout_id)),
65            None::<&CreateTerminalCheckoutBody>,
66            None,
67        ).await
68    }
69
70    /// Cancels a Terminal checkout request if the status of the request permits it.
71    pub async fn cancel_checkout(self, checkout_id: String)
72                              -> Result<SquareResponse, SquareError>{
73        self.client.request(
74            Verb::POST,
75            SquareAPI::Terminals(format!("/checkouts/{}/cancel", checkout_id)),
76            None::<&CreateTerminalCheckoutBody>,
77            None,
78        ).await
79    }
80
81    /// Creates a request to refund an Interac payment completed on a Square Terminal. <br/>
82    /// Refunds for Interac payments on a Square Terminal are supported only for Interac debit cards
83    /// in Canada. Other refunds for Terminal payments should use the Refunds API. For more
84    /// information, see [Refunds API](https://developer.squareup.com/reference/square/refunds-api).
85    pub async fn create_refund(self, body: CreateTerminalRefundBody)
86                              -> Result<SquareResponse, SquareError>{
87        self.client.request(
88            Verb::POST,
89            SquareAPI::Terminals("/refunds".to_string()),
90            Some(&body),
91            None,
92        ).await
93    }
94
95    /// Retrieves a filtered list of Interac Terminal refund requests created by the seller making
96    /// the request.
97    /// [Open in API Reference](https://developer.squareup.com/reference/square/terminal/search-terminal-refunds)
98    pub async fn search_refunds(self, body: SearchTerminalRefundBody)
99                              -> Result<SquareResponse, SquareError>{
100        self.client.request(
101            Verb::POST,
102            SquareAPI::Terminals("/refunds/search".to_string()),
103            Some(&body),
104            None,
105        ).await
106    }
107
108    /// Retrieves an Interac Terminal refund object by ID.
109    /// [Open in API Reference](https://developer.squareup.com/reference/square/terminal/get-terminal-refund)
110    pub async fn get_refund(self, terminal_refund_id: String)
111                              -> Result<SquareResponse, SquareError>{
112        self.client.request(
113            Verb::GET,
114            SquareAPI::Terminals(format!("/refunds/{}", terminal_refund_id)),
115            None::<&CreateTerminalRefundBody>,
116            None,
117        ).await
118    }
119
120    /// Cancels an Interac Terminal refund request by refund request ID if the status of the request
121    /// permits it.
122    /// [Open in API Reference](https://developer.squareup.com/reference/square/terminal/cancel-terminal-refund)
123    pub async fn cancel_refund(self, terminal_refund_id: String)
124                              -> Result<SquareResponse, SquareError>{
125        self.client.request(
126            Verb::POST,
127            SquareAPI::Terminals(format!("/refunds/{}/cancel", terminal_refund_id)),
128            None::<&CreateTerminalRefundBody>,
129            None,
130        ).await
131    }
132}
133
134// -------------------------------------------------------------------------------------------------
135// CreateTerminalCheckoutBody builder implementation
136// -------------------------------------------------------------------------------------------------
137#[derive(Clone, Debug, Serialize, Deserialize)]
138pub struct CreateTerminalCheckoutBody {
139    idempotency_key: Option<String>,
140    checkout: TerminalCheckout,
141}
142
143impl Default for CreateTerminalCheckoutBody {
144    fn default() -> Self {
145        CreateTerminalCheckoutBody {
146            idempotency_key: None,
147            checkout: TerminalCheckout::default(),
148        }
149    }
150}
151
152impl Validate for CreateTerminalCheckoutBody {
153    fn validate(mut self) -> Result<Self, ValidationError> where Self: Sized {
154        if self.checkout.amount_money.is_some() &&
155            self.checkout.device_options.is_some() {
156            self.idempotency_key = Some(Uuid::new_v4().to_string());
157            Ok(self)
158        } else {
159            Err(ValidationError)
160        }
161    }
162}
163
164impl<T: ParentBuilder> Builder<CreateTerminalCheckoutBody, T> {
165    pub fn amount_money(mut self, amount: Money) -> Self {
166        self.body.checkout.amount_money = Some(amount);
167
168        self
169    }
170
171    pub fn device_options(mut self, device_options: DeviceCheckoutOptions) -> Self {
172        self.body.checkout.device_options = Some(device_options);
173
174        self
175    }
176
177    pub fn customer_id(mut self, customer_id: String) -> Self {
178        self.body.checkout.customer_id = Some(customer_id);
179
180        self
181    }
182
183    pub fn deadline_duration(mut self, deadline_duration: String) -> Self {
184        self.body.checkout.deadline_duration = Some(deadline_duration);
185
186        self
187    }
188
189    pub fn note(mut self, note: String) -> Self {
190        self.body.checkout.note = Some(note);
191
192        self
193    }
194
195    pub fn order_id(mut self, order_id: String) -> Self {
196        self.body.checkout.order_id = Some(order_id);
197
198        self
199    }
200
201    pub fn payment_type(mut self, payment_type: CheckoutOptionsPaymentType) -> Self {
202        self.body.checkout.payment_type = Some(payment_type);
203
204        self
205    }
206
207    pub fn payment_options(mut self, payment_options: PaymentOptions) -> Self {
208        self.body.checkout.payment_options = Some(payment_options);
209
210        self
211    }
212
213    pub fn reference_id(mut self, reference_id: String) -> Self {
214        self.body.checkout.reference_id = Some(reference_id);
215
216        self
217    }
218}
219
220impl AddField<DeviceCheckoutOptions> for CreateTerminalCheckoutBody {
221    fn add_field(&mut self, field: DeviceCheckoutOptions) {
222        self.checkout.device_options = Some(field);
223    }
224}
225
226// -------------------------------------------------------------------------------------------------
227// SearchTerminalCheckoutBody builder implementation
228// -------------------------------------------------------------------------------------------------
229#[derive(Clone, Debug, Serialize, Deserialize, Default)]
230pub struct SearchTerminalCheckoutBody {
231    query: Option<TerminalCheckoutQuery>,
232    cursor: Option<String>,
233    limit: Option<i32>,
234}
235
236impl Validate for SearchTerminalCheckoutBody {
237    fn validate(self) -> Result<Self, ValidationError> where Self: Sized {
238        Ok(self)
239    }
240}
241
242impl<T: ParentBuilder> Builder<SearchTerminalCheckoutBody, T> {
243    pub fn query(mut self, query: TerminalCheckoutQuery) -> Self {
244        self.body.query = Some(query);
245
246        self
247    }
248
249    pub fn cursor(mut self, cursor: String) -> Self {
250        self.body.cursor = Some(cursor);
251
252        self
253    }
254
255    pub fn limit(mut self, limit: i32) -> Self {
256        self.body.limit = Some(limit);
257
258        self
259    }
260}
261
262impl AddField<TerminalCheckoutQuery> for SearchTerminalCheckoutBody {
263    fn add_field(&mut self, field: TerminalCheckoutQuery) {
264        self.query = Some(field);
265    }
266}
267
268// -------------------------------------------------------------------------------------------------
269// CreateTerminalRefundBody builder implementation
270// -------------------------------------------------------------------------------------------------
271#[derive(Clone, Debug, Serialize, Deserialize, Default)]
272pub struct CreateTerminalRefundBody {
273    idempotency_key: Option<String>,
274    refund: TerminalRefund,
275}
276
277impl Validate for CreateTerminalRefundBody {
278    fn validate(mut self) -> Result<Self, ValidationError> where Self: Sized {
279        if self.refund.device_id.is_some() &&
280            self.refund.amount_money.is_some() &&
281            self.refund.reason.is_some() &&
282            self.refund.payment_id.is_some() {
283            self.idempotency_key = Some(Uuid::new_v4().to_string());
284
285            Ok(self)
286        } else {
287            Err(ValidationError)
288        }
289    }
290}
291
292impl<T: ParentBuilder> Builder<CreateTerminalRefundBody, T> {
293    pub fn amount_money(mut self, amount_money: Money) -> Self {
294        self.body.refund.amount_money = Some(amount_money);
295
296        self
297    }
298
299    pub fn device_id(mut self, device_id: String) -> Self {
300        self.body.refund.device_id = Some(device_id);
301
302        self
303    }
304
305    pub fn payment_id(mut self, payment_id: String) -> Self {
306        self.body.refund.payment_id = Some(payment_id);
307
308        self
309    }
310
311    pub fn reason(mut self, reason: String) -> Self {
312        self.body.refund.reason = Some(reason);
313
314        self
315    }
316
317    pub fn deadline_duration(mut self, reason: String) -> Self {
318        self.body.refund.deadline_duration = Some(reason);
319
320        self
321    }
322}
323
324// -------------------------------------------------------------------------------------------------
325// SearchTerminalRefundBody builder implementation
326// -------------------------------------------------------------------------------------------------
327#[derive(Clone, Debug, Serialize, Deserialize, Default)]
328pub struct SearchTerminalRefundBody {
329    cursor: Option<String>,
330    limit: Option<i32>,
331    query: Option<TerminalRefundQuery>,
332}
333
334impl Validate for SearchTerminalRefundBody {
335    fn validate(self) -> Result<Self, ValidationError> where Self: Sized {
336        Ok(self)
337    }
338}
339
340impl<T: ParentBuilder> Builder<SearchTerminalRefundBody, T> {
341    pub fn query(mut self, query: TerminalRefundQuery) -> Self {
342        self.body.query = Some(query);
343
344        self
345    }
346
347    pub fn limit(mut self, limit: i32) -> Self {
348        self.body.limit = Some(limit);
349
350        self
351    }
352
353    pub fn cursor(mut self, cursor: String) -> Self {
354        self.body.cursor = Some(cursor);
355
356        self
357    }
358}
359
360impl AddField<TerminalRefundQuery> for SearchTerminalRefundBody {
361    fn add_field(&mut self, field: TerminalRefundQuery) {
362        self.query = Some(field);
363    }
364}
365
366#[cfg(test)]
367mod test_terminals {
368    use crate::builder::BackIntoBuilder;
369    use super::*;
370    use crate::objects::enums::{Currency, SortOrder};
371    use crate::objects::{TerminalCheckoutQueryFilter, TerminalCheckoutQuerySort};
372
373    #[tokio::test]
374    async fn test_create_terminal_checkout_body_builder() {
375        let expected = CreateTerminalCheckoutBody {
376            idempotency_key: None,
377            checkout: TerminalCheckout {
378                id: None,
379                amount_money: Some(Money {
380                    amount: Some(10),
381                    currency: Currency::USD
382                }),
383                device_options: Some(DeviceCheckoutOptions {
384                    device_id: Some("some_id".to_string()),
385                    collect_signature: Some(true),
386                    show_itemized_cart: None,
387                    skip_receipt_screen: Some(true),
388                    tip_settings: None
389                }),
390                app_fee_money: None,
391                app_id: None,
392                cancel_reason: None,
393                created_at: None,
394                customer_id: None,
395                deadline_duration: None,
396                location_id: None,
397                note: None,
398                order_id: None,
399                payment_ids: None,
400                payment_options: None,
401                payment_type: None,
402                reference_id: None,
403                status: None,
404                updated_at: None
405            }
406        };
407
408        let mut actual = Builder::from(CreateTerminalCheckoutBody::default())
409            .amount_money(Money { amount: Some(10), currency: Currency::USD })
410            .sub_builder_from(DeviceCheckoutOptions::default())
411            .device_id("some_id".to_string())
412            .collect_signature()
413            .skip_receipt_screen()
414            .build()
415            .unwrap()
416            .build()
417            .unwrap();
418
419        assert!(actual.idempotency_key.is_some());
420
421        actual.idempotency_key = None;
422
423        assert_eq!(format!("{:?}", expected), format!("{:?}", actual))
424    }
425
426    #[tokio::test]
427    async fn test_search_terminal_checkout_body_builder() {
428        let expected = SearchTerminalCheckoutBody {
429            query: Some(TerminalCheckoutQuery {
430                filter: Some(TerminalCheckoutQueryFilter {
431                    created_at: None,
432                    device_id: Some("some_id".to_string()),
433                    status: None
434                }),
435                sort: Some(TerminalCheckoutQuerySort {
436                    sort_order: Some(SortOrder::Asc)
437                })
438            }),
439            cursor: None,
440            limit: Some(10)
441        };
442
443        let actual = Builder::from(SearchTerminalCheckoutBody::default())
444            .limit(10)
445            .sub_builder_from(TerminalCheckoutQuery::default())
446            .sort_ascending()
447            .device_id("some_id".to_string())
448            .build()
449            .unwrap()
450            .build()
451            .unwrap();
452
453        assert_eq!(format!("{:?}", expected), format!("{:?}", actual))
454    }
455
456    // #[tokio::test]
457    async fn test_search_checkouts() {
458        use dotenv::dotenv;
459        use std::env;
460
461        dotenv().ok();
462        let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
463        let sut = SquareClient::new(&access_token);
464
465        let input = SearchTerminalCheckoutBody {
466            query: Some(TerminalCheckoutQuery {
467                filter: Some(TerminalCheckoutQueryFilter {
468                    created_at: None,
469                    device_id: None,
470                    status: Some(TerminalCheckoutStatus::Completed)
471                }),
472                sort: Some(TerminalCheckoutQuerySort {
473                    sort_order: Some(SortOrder::Asc)
474                })
475            }),
476            cursor: None,
477            limit: Some(10)
478        };
479
480        let res = sut.terminal()
481            .search_checkout(input)
482            .await;
483
484        assert!(res.is_err())
485    }
486
487    #[tokio::test]
488    async fn test_create_terminal_refund_body_builder() {
489        let expected = CreateTerminalRefundBody {
490            idempotency_key: None,
491            refund: TerminalRefund {
492                id: None,
493                amount_money: Some(Money {
494                    amount: Some(10),
495                    currency: Currency::USD
496                }),
497                device_id: Some("some_id".to_string()),
498                payment_id: Some("some_id".to_string()),
499                reason: Some("some reason".to_string()),
500                app_id: None,
501                cancel_reason: None,
502                created_at: None,
503                deadline_duration: None,
504                location_id: None,
505                order_id: None,
506                refund_id: None,
507                status: None,
508                updated_at: None
509            }
510        };
511
512        let mut actual = Builder::from(CreateTerminalRefundBody::default())
513            .amount_money(Money { amount: Some(10), currency: Currency::USD })
514            .device_id("some_id".to_string())
515            .payment_id("some_id".to_string())
516            .reason("some reason".to_string())
517            .build()
518            .unwrap();
519
520        actual.idempotency_key = None;
521
522        assert_eq!(format!("{:?}", expected), format!("{:?}", actual))
523    }
524
525    #[tokio::test]
526    async fn test_create_terminal_refund_body_builder_fail() {
527
528        let res = Builder::from(CreateTerminalRefundBody::default())
529            .payment_id("some_id".to_string())
530            .device_id("some_id".to_string())
531            .amount_money(Money { amount: Some(10), currency: Currency::USD })
532            .build();
533
534        assert!(res.is_err())
535    }
536
537    #[tokio::test]
538    async fn test_search_terminal_refund_body_builder() {
539        use crate::objects::TerminalRefundQueryFilter;
540        let expected = SearchTerminalRefundBody {
541            cursor: Some("some cursor".to_string()),
542            limit: Some(10),
543            query: Some(TerminalRefundQuery{
544                filter: Some(TerminalRefundQueryFilter {
545                    created_at: None,
546                    device_id: Some("some_id".to_string()),
547                    status: Some(TerminalCheckoutStatus::CancelRequested)
548                }),
549                sort: Some(TerminalCheckoutQuerySort { sort_order: Some(SortOrder::Desc) })
550            })
551        };
552
553        let actual = Builder::from(SearchTerminalRefundBody::default())
554            .limit(10)
555            .cursor("some cursor".to_string())
556            .sub_builder_from(TerminalRefundQuery::default())
557            .device_id("some_id".to_string())
558            .sort_descending()
559            .cancel_requested()
560            .build()
561            .unwrap()
562            .build()
563            .unwrap();
564
565        assert_eq!(format!("{:?}", expected), format!("{:?}", actual))
566    }
567}
568