square_ox/api/
orders.rs

1/*!
2Orders 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::{Customer, Order, OrderReward, OrderServiceCharge, SearchOrdersQuery};
9use crate::response::SquareResponse;
10use crate::builder::{Builder, ParentBuilder, Validate, BackIntoBuilder, AddField, Buildable};
11use square_ox_derive::Builder;
12
13use serde::{Serialize, Deserialize};
14use uuid::Uuid;
15
16impl SquareClient {
17    pub fn orders(&self) -> Orders {
18        Orders {
19            client: &self,
20        }
21    }
22}
23
24pub struct Orders<'a> {
25    client: &'a SquareClient
26}
27
28impl<'a> Orders<'a> {
29    /// Creates a new [Order](Order) that can include information about products for purchase and settings
30    /// to apply to the purchase.
31    /// To pay for a created order, see [Pay for Orders](https://developer.squareup.com/docs/orders-api/pay-for-orders).
32    pub async fn create(self, body: CreateOrderBody)
33                      -> Result<SquareResponse, SquareError> {
34        self.client.request(
35            Verb::POST,
36            SquareAPI::Orders("".to_string()),
37            Some(&body),
38            None,
39        ).await
40    }
41
42    /// Search all orders for one or more locations.
43    /// [Open in API Reference](https://developer.squareup.com/reference/square/orders/search-orders).
44    pub async fn search(self, body: SearchOrderBody)
45                      -> Result<SquareResponse, SquareError> {
46        self.client.request(
47            Verb::POST,
48            SquareAPI::Orders("/search".to_string()),
49            Some(&body),
50            None,
51        ).await
52    }
53
54    /// Retrieves an [Order](Order) by ID.
55    /// [Open in API Reference](https://developer.squareup.com/reference/square/orders/retrieve-order).
56    pub async fn retrieve(self, id: String)
57                      -> Result<SquareResponse, SquareError> {
58        self.client.request(
59            Verb::GET,
60            SquareAPI::Orders(format!("/{}", id)),
61            None::<&SearchOrderBody>,
62            None,
63        ).await
64    }
65
66    /// Retrieves an [Order](Order) by ID.
67    /// [Open in API Reference](https://developer.squareup.com/reference/square/orders/retrieve-order).
68    pub async fn update(self, id: String, body: OrderUpdateBody)
69                      -> Result<SquareResponse, SquareError> {
70        self.client.request(
71            Verb::PUT,
72            SquareAPI::Orders(format!("/{}", id)),
73            Some(&body),
74            None,
75        ).await
76    }
77
78    /// Pay for an [Order](Order) using one or more approved payments or settle an order with a
79    /// total of 0.
80    /// [Open in API Reference](https://developer.squareup.com/reference/square/orders/pay-order).
81    pub async fn pay(self, id: String, body: PayOrderBody)
82                      -> Result<SquareResponse, SquareError> {
83        self.client.request(
84            Verb::POST,
85            SquareAPI::Orders(format!("/{}/pay", id)),
86            Some(&body),
87            None,
88        ).await
89    }
90
91    /// Enables applications to preview [Order](Order) pricing without creating an order.
92    /// [Open in API Reference](https://developer.squareup.com/reference/square/orders/calculate-order).
93    pub async fn calculate(self, body: OrderCalculateBody)
94                      -> Result<SquareResponse, SquareError> {
95        self.client.request(
96            Verb::POST,
97            SquareAPI::Orders("/calculate".to_string()),
98            Some(&body),
99            None,
100        ).await
101    }
102}
103
104#[derive(Clone, Debug, Serialize, Deserialize, Default, Builder)]
105pub struct CreateOrderBody {
106    #[serde(skip_serializing_if = "Option::is_none")]
107    #[builder_rand("uuid")]
108    #[builder_vis("private")]
109    idempotency_key: Option<String>,
110    order: Order,
111}
112
113impl AddField<Order> for CreateOrderBody {
114    fn add_field(&mut self, field: Order) {
115        self.order = field;
116    }
117}
118
119#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
120pub struct SearchOrderBody {
121    #[serde(skip_serializing_if = "Option::is_none")]
122    cursor: Option<String>,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    limit: Option<i32>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    #[builder_into]
127    location_ids: Option<Vec<String>>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    query: Option<SearchOrdersQuery>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    return_entries: Option<bool>
132}
133
134impl Default for SearchOrderBody {
135    fn default() -> Self {
136        SearchOrderBody {
137            cursor: None,
138            limit: None,
139            location_ids: None,
140            query: None,
141            return_entries: Some(true)
142        }
143    }
144}
145
146// implements the necessary traits to release an SearchOrdersQuery builder from a SearchOrderBody
147// builder
148impl AddField<SearchOrdersQuery> for SearchOrderBody {
149    fn add_field(&mut self, field: SearchOrdersQuery) {
150        self.query = Some(field);
151    }
152}
153
154#[derive(Clone, Debug, Serialize, Default, Builder)]
155pub struct OrderUpdateBody {
156    fields_to_clear: Option<Vec<String>>,
157    #[builder_rand("uuid")]
158    idempotency_key: Option<String>,
159    #[builder_validate("is_some")]
160    order: Option<Order>,
161}
162
163// implements the necessary traits to release an Order builder from a OrderUpdateBody
164// builder
165impl AddField<Order> for OrderUpdateBody {
166    fn add_field(&mut self, field: Order) {
167        self.order = Some(field);
168    }
169}
170
171#[derive(Clone, Debug, Serialize, Default, Builder)]
172pub struct PayOrderBody {
173    #[builder_rand("uuid")]
174    #[serde(skip_serializing_if = "Option::is_none")]
175    idempotency_key: Option<String>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    #[builder_validate("is_some")]
178    order_version: Option<i64>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    #[builder_validate("is_some")]
181    payment_ids: Option<Vec<String>>,
182}
183
184#[derive(Clone, Debug, Serialize, Default, Builder)]
185pub struct OrderCalculateBody {
186    #[builder_validate("is_some")]
187    #[serde(skip_serializing_if = "Option::is_none")]
188    order: Option<Order>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    proposed_rewards: Option<Vec<OrderReward>>,
191}
192
193// implements the necessary traits to release an Order builder from a OrderCalculateBody
194// builder
195impl AddField<Order> for OrderCalculateBody {
196    fn add_field(&mut self, field: Order) {
197        self.order = Some(field)
198    }
199}
200
201// implements the necessary traits to release an OrderReward builder from a OrderCalculateBody
202// builder
203impl AddField<OrderReward> for OrderCalculateBody {
204    fn add_field(&mut self, field: OrderReward) {
205        match self.proposed_rewards.as_mut() {
206            Some(rewards) => rewards.push(field),
207            None => self.proposed_rewards = Some(vec![field])
208        }
209    }
210}
211
212#[cfg(test)]
213mod test_orders {
214    use crate::builder::Nil;
215    use crate::objects;
216    use crate::objects::enums::{Currency, OrderServiceChargeCalculationPhase, SortOrder, SearchOrdersSortField};
217    use crate::objects::{Money, SearchOrdersSort};
218    use super::*;
219
220    #[tokio::test]
221    async fn test_create_order_body_builder() {
222        let expected = CreateOrderBody {
223            idempotency_key: None,
224            order: Order {
225                id: None,
226                location_id: Some("location_id".to_string()),
227                close_at: None,
228                created_at: None,
229                customer_id: Some("customer_id".to_string()),
230                discounts: None,
231                fulfillments: None,
232                line_items: None,
233                metadata: None,
234                net_amounts: None,
235                pricing_options: None,
236                reference_id: None,
237                refunds: None,
238                return_amounts: None,
239                returns: None,
240                rewards: None,
241                rounding_adjustment: None,
242                service_charges: Some(vec![OrderServiceCharge {
243                    amount_money: Some(Money{ amount: Some(10), currency: Currency::USD }),
244                    applied_money: None,
245                    applied_taxes: None,
246                    calculation_phase: Some(OrderServiceChargeCalculationPhase::TotalPhase),
247                    catalog_object_id: None,
248                    catalog_version: None,
249                    metadata: None,
250                    name: Some("some name".to_string()),
251                    percentage: None,
252                    taxable: None,
253                    total_money: None,
254                    total_tax_money: None,
255                    service_charge_type: None,
256                    uid: None
257                }]),
258                source: None,
259                state: None,
260                taxes: None,
261                tenders: None,
262                ticket_name: None,
263                total_discount_money: None,
264                total_money: None,
265                total_service_charge_money: None,
266                total_tax_money: None,
267                total_tip_money: None,
268                updated_at: None,
269                version: None
270            }
271            };
272
273            let mut actual = Builder::from(CreateOrderBody::default())
274                .sub_builder_from(Order::default())
275                .location_id("location_id")
276                .customer_id("customer_id".to_string())
277                .sub_builder_from(OrderServiceCharge::default())
278                .amount_money(Money { amount: Some(10), currency: Currency::USD })
279                .name("some name")
280                .calculation_phase(OrderServiceChargeCalculationPhase::TotalPhase)
281                .build()
282                .unwrap()
283                .build()
284                .unwrap()
285                .build()
286                .unwrap();
287
288        assert!(actual.idempotency_key.is_some());
289
290        actual.idempotency_key = None;
291
292        assert_eq!(format!("{:?}", expected), format!("{:?}", actual))
293    }
294
295    #[tokio::test]
296    async fn test_create_order_body_builder_fail() {
297        let actual = Builder::from(CreateOrderBody::default())
298            .sub_builder_from(Order::default())
299            .location_id("location_id".to_string())
300            .customer_id("customer_id".to_string())
301            .sub_builder_from(OrderServiceCharge::default())
302            .amount_money(Money { amount: Some(10), currency: Currency::USD })
303            .calculation_phase(OrderServiceChargeCalculationPhase::TotalPhase)
304            .build();
305
306        assert!(actual.is_err());
307    }
308
309    #[tokio::test]
310    async fn test_create_order() {
311        use dotenv::dotenv;
312        use std::env;
313
314        dotenv().ok();
315        let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
316        let sut = SquareClient::new(&access_token);
317
318        let input = CreateOrderBody {
319            idempotency_key: None,
320            order: objects::Order {
321                id: None,
322                location_id: Some("L1JC53TYHS40Z".to_string()),
323                close_at: None,
324                created_at: None,
325                customer_id: None,
326                discounts: None,
327                fulfillments: None,
328                line_items: None,
329                metadata: None,
330                net_amounts: None,
331                pricing_options: None,
332                reference_id: None,
333                refunds: None,
334                return_amounts: None,
335                returns: None,
336                rewards: None,
337                rounding_adjustment: None,
338                service_charges: Some(vec![OrderServiceCharge {
339                    amount_money: Some(Money{ amount: Some(15), currency: Currency::USD }),
340                    applied_money: None,
341                    applied_taxes: None,
342                    calculation_phase: Some(OrderServiceChargeCalculationPhase::TotalPhase),
343                    catalog_object_id: None,
344                    catalog_version: None,
345                    metadata: None,
346                    name: Some("some name".to_string()),
347                    percentage: None,
348                    taxable: None,
349                    total_money: None,
350                    total_tax_money: None,
351                    service_charge_type: None,
352                    uid: None
353                }]),
354                source: None,
355                state: None,
356                taxes: None,
357                tenders: None,
358                ticket_name: None,
359                total_discount_money: None,
360                total_money: None,
361                total_service_charge_money: None,
362                total_tax_money: None,
363                total_tip_money: None,
364                updated_at: None,
365                version: None
366            }
367        };
368
369        let res = sut.orders()
370            .create(input)
371            .await;
372
373        assert!(res.is_ok())
374    }
375
376    #[tokio::test]
377    async fn test_search_order_body_builder() {
378        let expected = SearchOrderBody {
379            cursor: None,
380            limit: Some(10),
381            location_ids: Some(vec!["e23icos".to_string(), "daiooaa".to_string(), "pßasmxaskm".to_string()]),
382            query: Some(SearchOrdersQuery {
383                filter: None,
384                sort: Some(SearchOrdersSort {
385                    sort_field: Some(SearchOrdersSortField::CreatedAt),
386                    sort_order: Some(SortOrder::Asc)
387                })
388            }),
389            return_entries: Some(true)
390        };
391
392        let actual = Builder::from(SearchOrderBody::default())
393            .location_ids(
394                vec![
395                    "e23icos".to_string(),
396                    "daiooaa".to_string(),
397                    "pßasmxaskm".to_string(),
398                ]
399            )
400            .limit(10)
401            .sub_builder_from(SearchOrdersQuery::default())
402            .sort_ascending()
403            .build()
404            .unwrap()
405            .build()
406            .unwrap();
407
408        assert_eq!(format!("{:?}", expected), format!("{:?}", actual))
409    }
410
411    #[tokio::test]
412    async fn test_search_orders() {
413        use dotenv::dotenv;
414        use std::env;
415
416        dotenv().ok();
417        let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
418        let sut = SquareClient::new(&access_token);
419
420        let input = SearchOrderBody {
421            cursor: None,
422            limit: None,
423            location_ids: Some(vec!["L1JC53TYHS40Z".to_string()]),
424            query: Some(SearchOrdersQuery {
425                filter: None,
426                sort: Some(SearchOrdersSort {
427                    sort_field: Some(SearchOrdersSortField::CreatedAt),
428                    sort_order: Some(SortOrder::Asc)
429                })
430            }),
431            return_entries: Some(true)
432        };
433
434        let res = sut.orders()
435            .search(input)
436            .await;
437
438        assert!(res.is_ok())
439    }
440
441    #[tokio::test]
442    async fn test_retrieve_order() {
443        use dotenv::dotenv;
444        use std::env;
445
446        dotenv().ok();
447        let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
448        let sut = SquareClient::new(&access_token);
449
450
451        let res = sut.orders()
452            .retrieve("HnbOXf4007VldqxbMvuzf0IjgyAZY".to_string())
453            .await;
454
455        assert!(res.is_ok())
456    }
457
458    #[tokio::test]
459    async fn test_update_order_body_fail() {
460
461        let res_vec = vec![
462            Builder::from(OrderUpdateBody::default())
463                .fields_to_clear(vec!["a_field".to_string(), "another_field".to_string()])
464                .sub_builder_from(Order::default())
465                .version(2)
466                .build(),
467        ];
468
469        res_vec.into_iter().for_each(|res| assert!(res.is_err()))
470    }
471
472    // #[tokio::test]
473    async fn test_update_order() {
474        use dotenv::dotenv;
475        use std::env;
476
477        dotenv().ok();
478        let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
479        let sut = SquareClient::new(&access_token);
480
481        let input = OrderUpdateBody {
482            fields_to_clear: None,
483            idempotency_key: Some(Uuid::new_v4().to_string()),
484            order: Some(Order {
485                id: None,
486                location_id: Some("L1JC53TYHS40Z".to_string()),
487                close_at: None,
488                created_at: None,
489                customer_id: None,
490                discounts: None,
491                fulfillments: None,
492                line_items: None,
493                metadata: None,
494                net_amounts: None,
495                pricing_options: None,
496                reference_id: None,
497                refunds: None,
498                return_amounts: None,
499                returns: None,
500                rewards: None,
501                rounding_adjustment: None,
502                service_charges: None,
503                source: None,
504                state: None,
505                taxes: None,
506                tenders: None,
507                ticket_name: None,
508                total_discount_money: None,
509                total_money: None,
510                total_service_charge_money: None,
511                total_tax_money: None,
512                total_tip_money: None,
513                updated_at: None,
514                version: Some(2)
515            })
516        };
517
518        println!("{:?}", &input);
519
520        let res = sut.orders()
521            .update("TJn1daLZuaMmPGL8vbeFGSdxB9HZY".to_string(), input)
522            .await;
523
524        assert!(res.is_ok())
525    }
526
527    #[tokio::test]
528    async fn test_pay_order_body_builder() {
529
530        let expected = PayOrderBody {
531            idempotency_key: None,
532            order_version: Some(3),
533            payment_ids: Some(vec!["some_id".to_string()])
534        };
535
536        let mut actual = Builder::from(PayOrderBody::default())
537            .order_version(3)
538            .payment_ids(vec!["some_id".to_string()])
539            .build()
540            .unwrap();
541
542        assert!(actual.idempotency_key.is_some());
543
544        actual.idempotency_key = None;
545
546        assert_eq!(format!("{:?}", expected), format!("{:?}", actual));
547    }
548
549    #[tokio::test]
550    async fn test_order_calculate_body_builder() {
551
552        let expected = OrderCalculateBody {
553            order: Some(Order {
554                id: None,
555                location_id: Some("location_id".to_string()),
556                close_at: None,
557                created_at: None,
558                customer_id: None,
559                discounts: None,
560                fulfillments: None,
561                line_items: None,
562                metadata: None,
563                net_amounts: None,
564                pricing_options: None,
565                reference_id: None,
566                refunds: None,
567                return_amounts: None,
568                returns: None,
569                rewards: None,
570                rounding_adjustment: None,
571                service_charges: Some(vec![
572                    OrderServiceCharge {
573                        amount_money: Some(Money {
574                            amount: Some(20),
575                            currency: Currency::USD
576                        }),
577                        applied_money: None,
578                        applied_taxes: None,
579                        calculation_phase: Some(OrderServiceChargeCalculationPhase::TotalPhase),
580                        catalog_object_id: None,
581                        catalog_version: None,
582                        metadata: None,
583                        name: Some("some name".to_string()),
584                        percentage: None,
585                        taxable: None,
586                        total_money: None,
587                        total_tax_money: None,
588                        service_charge_type: None,
589                        uid: None
590                    }
591                ]),
592                source: None,
593                state: None,
594                taxes: None,
595                tenders: None,
596                ticket_name: None,
597                total_discount_money: None,
598                total_money: None,
599                total_service_charge_money: None,
600                total_tax_money: None,
601                total_tip_money: None,
602                updated_at: None,
603                version: Some(3)
604            }),
605            proposed_rewards: None
606        };
607
608        let mut actual = Builder::from(OrderCalculateBody::default())
609            .sub_builder_from(Order::default())
610            .location_id("location_id".to_string())
611            .sub_builder_from(OrderServiceCharge::default())
612            .amount_money(Money { amount: Some(20), currency: Currency::USD })
613            .name("some name".to_string())
614            .calculation_phase(OrderServiceChargeCalculationPhase::TotalPhase)
615            .build()
616            .unwrap()
617            .version(3)
618            .build()
619            .unwrap()
620            .build()
621            .unwrap();
622
623        assert_eq!(format!("{:?}", expected), format!("{:?}", actual));
624    }
625
626    #[tokio::test]
627    async fn test_calculate_order() {
628        use dotenv::dotenv;
629        use std::env;
630
631        dotenv().ok();
632        let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
633        let sut = SquareClient::new(&access_token);
634
635        let input = OrderCalculateBody {
636            order: Some(Order {
637                id: None,
638                location_id: Some("L1JC53TYHS40Z".to_string()),
639                close_at: None,
640                created_at: None,
641                customer_id: None,
642                discounts: None,
643                fulfillments: None,
644                line_items: None,
645                metadata: None,
646                net_amounts: None,
647                pricing_options: None,
648                reference_id: None,
649                refunds: None,
650                return_amounts: None,
651                returns: None,
652                rewards: None,
653                rounding_adjustment: None,
654                service_charges: Some(vec![
655                    OrderServiceCharge {
656                        amount_money: Some(Money {
657                            amount: Some(20),
658                            currency: Currency::USD
659                        }),
660                        applied_money: None,
661                        applied_taxes: None,
662                        calculation_phase: Some(OrderServiceChargeCalculationPhase::TotalPhase),
663                        catalog_object_id: None,
664                        catalog_version: None,
665                        metadata: None,
666                        name: Some("some name".to_string()),
667                        percentage: None,
668                        taxable: None,
669                        total_money: None,
670                        total_tax_money: None,
671                        service_charge_type: None,
672                        uid: None
673                    }
674                ]),
675                source: None,
676                state: None,
677                taxes: None,
678                tenders: None,
679                ticket_name: None,
680                total_discount_money: None,
681                total_money: None,
682                total_service_charge_money: None,
683                total_tax_money: None,
684                total_tip_money: None,
685                updated_at: None,
686                version: Some(3)
687            }),
688            proposed_rewards: None
689        };
690
691        let res = sut.orders()
692            .calculate(input)
693            .await;
694
695        assert!(res.is_ok())
696    }
697}
698