1use crate::client::SquareClient;
6use crate::api::{Verb, SquareAPI};
7use crate::errors::{SquareError, SearchQueryBuildError, BookingsPostBuildError, BookingsCancelBuildError, ValidationError};
8use crate::response::SquareResponse;
9use crate::objects::{AppointmentSegment, Booking, FilterValue, enums::BusinessAppointmentSettingsBookingLocationType, StartAtRange, SegmentFilter, AvailabilityQueryFilter};
10
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13use crate::builder::{AddField, Builder, ParentBuilder, Validate, Buildable, BackIntoBuilder};
14use square_ox_derive::Builder;
15
16impl SquareClient {
17 pub fn bookings(&self) -> Bookings {
18 Bookings {
19 client: &self
20 }
21 }
22}
23
24pub struct Bookings<'a> {
25 client: &'a SquareClient,
26}
27
28impl<'a> Bookings<'a> {
29 pub async fn list(self, search_query: Option<Vec<(String, String)>>)
36 -> Result<SquareResponse, SquareError> {
37 self.client.request(
38 Verb::GET,
39 SquareAPI::Bookings("".to_string()),
40 None::<&BookingsPost>,
41 search_query,
42 ).await
43 }
44
45 pub async fn search_availability(self, search_query: SearchAvailabilityQuery)
51 -> Result<SquareResponse, SquareError> {
52 self.client.request(
53 Verb::POST,
54 SquareAPI::Bookings("/availability/search".to_string()),
55 Some(&search_query),
56 None,
57 ).await
58 }
59
60 pub async fn create(self, booking_post: BookingsPost)
66 -> Result<SquareResponse, SquareError> {
67 self.client.request(
68 Verb::POST,
69 SquareAPI::Bookings("".to_string()),
70 Some(&booking_post),
71 None,
72 ).await
73 }
74
75 pub async fn update(self, updated_booking: BookingsPost, booking_id: String)
81 -> Result<SquareResponse, SquareError> {
82 self.client.request(
83 Verb::PUT,
84 SquareAPI::Bookings(format!("/{}", booking_id)),
85 Some(&updated_booking),
86 None,
87 ).await
88 }
89
90 pub async fn retrieve(self, booking_id: String)
95 -> Result<SquareResponse, SquareError> {
96 self.client.request(
97 Verb::GET,
98 SquareAPI::Bookings(format!("/{}", booking_id)),
99 None::<&BookingsPost>,
100 None,
101 ).await
102 }
103
104 pub async fn cancel(&self, booking_to_cancel: BookingsCancel)
111 -> Result<SquareResponse, SquareError> {
112 self.client.request(
113 Verb::POST,
114 SquareAPI::Bookings(format!("/{}/cancel",
115 booking_to_cancel.booking_id.unwrap().clone())),
116 Some(&booking_to_cancel.body),
117 None,
118 ).await
119 }
120
121 pub async fn retrieve_business_profile(self)
123 -> Result<SquareResponse, SquareError> {
124 self.client.request(
125 Verb::GET,
126 SquareAPI::Bookings("/business-booking-profile".to_string()),
127 None::<&BookingsPost>,
128 None,
129 ).await
130 }
131
132 pub async fn list_team_member_profiles(self, search_query: Option<Vec<(String, String)>>)
138 -> Result<SquareResponse, SquareError> {
139 self.client.request(
140 Verb::GET,
141 SquareAPI::Bookings("/team-member-booking-profiles".to_string()),
142 None::<&BookingsPost>,
143 search_query,
144 ).await
145 }
146
147 pub async fn retrieve_team_member_profiles(self, team_member_id: String)
153 -> Result<SquareResponse, SquareError> {
154 self.client.request(
155 Verb::GET,
156 SquareAPI::Bookings(format!("/team-member-booking-profiles/{}", team_member_id)),
157 None::<&BookingsPost>,
158 None,
159 ).await
160 }
161}
162
163#[derive(Default)]
167pub struct ListBookingsQueryBuilder {
168 limit: Option<i64>,
169 cursor: Option<String>,
170 team_member_id: Option<String>,
171 location_id: Option<String>,
172 start_at_min: Option<String>,
173 start_at_max: Option<String>,
174}
175
176impl ListBookingsQueryBuilder {
177 pub fn new() -> Self {
178 Default::default()
179 }
180
181 pub fn limit(mut self, limit: i64) -> Self {
183 self.limit = Some(limit);
184
185 self
186 }
187
188 pub fn cursor<S: Into<String>>(mut self, cursor: S) -> Self {
191 self.cursor = Some(cursor.into());
192
193 self
194 }
195
196 pub fn team_member_id<S: Into<String>>(mut self, team_member_id: S) -> Self {
199 self.team_member_id = Some(team_member_id.into());
200
201 self
202 }
203
204 pub fn location_id<S: Into<String>>(mut self, location_id: S) -> Self {
207 self.location_id = Some(location_id.into());
208
209 self
210 }
211
212 pub fn start_at_min<S: Into<String>>(mut self, start_at_min: S) -> Self {
221 self.start_at_min = Some(start_at_min.into());
222
223 self
224 }
225
226 pub fn start_at_max<S: Into<String>>(mut self, start_at_max: S) -> Self {
235 self.start_at_max = Some(start_at_max.into());
236
237 self
238 }
239
240 pub async fn build(self) -> Vec<(String, String)> {
241 let ListBookingsQueryBuilder {
242 limit,
243 cursor,
244 team_member_id,
245 location_id,
246 start_at_min,
247 start_at_max,
248
249 } = self;
250
251 let mut res = vec![];
252
253 if let Some(limit) = limit {
254 res.push(("limit".to_string(), limit.to_string()))
255 }
256
257 if let Some(cursor) = cursor {
258 res.push(("cursor".to_string(), cursor))
259 }
260
261 if let Some(team_member_id) = team_member_id {
262 res.push(("team_member_id".to_string(), team_member_id))
263 }
264
265 if let Some(location_id) = location_id {
266 res.push(("location_id".to_string(), location_id))
267 }
268
269 if let Some(start_at_min) = start_at_min {
270 res.push(("start_at_min".to_string(), start_at_min))
271 }
272
273 if let Some(start_at_max) = start_at_max {
274 res.push(("start_at_max".to_string(), start_at_max))
275 }
276
277 res
278 }
279}
280
281#[derive(Default)]
285pub struct ListTeamMemberBookingsProfileBuilder {
286 limit: Option<i32>,
287 cursor: Option<String>,
288 bookable_only: Option<bool>,
289 location_id: Option<String>,
290}
291
292impl ListTeamMemberBookingsProfileBuilder {
293 pub fn new() -> Self {
294 Default::default()
295 }
296
297 pub fn limit(mut self, limit: i32) -> Self {
299 self.limit = Some(limit);
300
301 self
302 }
303
304 pub fn cursor<S: Into<String>>(mut self, cursor: S) -> Self {
307 self.cursor = Some(cursor.into());
308
309 self
310 }
311
312 pub fn bookable_only(mut self) -> Self {
314 self.bookable_only = Some(true);
315
316 self
317 }
318
319 pub fn location_id<S: Into<String>>(mut self, location_id: S) -> Self {
322 self.location_id = Some(location_id.into());
323
324 self
325 }
326
327 pub async fn build(self) -> Vec<(String, String)> {
328 let ListTeamMemberBookingsProfileBuilder {
329 limit,
330 cursor,
331 bookable_only,
332 location_id,
333 } = self;
334
335 let mut res = vec![];
336
337 if let Some(limit) = limit {
338 res.push(("limit".to_string(), limit.to_string()))
339 }
340 if let Some(cursor) = cursor {
341 res.push(("cursor".to_string(), cursor))
342 }
343 if let Some(bookable_only) = bookable_only {
344 res.push(("bookable_only".to_string(), bookable_only.to_string()))
345 }
346 if let Some(location_id) = location_id {
347 res.push(("location_id".to_string(), location_id))
348 }
349
350 res
351 }
352}
353
354#[derive(Serialize, Debug, Deserialize, Default, Builder)]
386pub struct BookingsPost {
387 #[builder_rand("uuid")]
388 idempotency_key: Option<String>,
389 booking: Booking,
390}
391
392impl AddField<Booking> for BookingsPost {
393 fn add_field(&mut self, field: Booking) {
394 self.booking = field;
395 }
396}
397
398#[derive(Serialize, Debug, Deserialize, Default)]
402pub struct BookingsCancel {
403 #[serde(skip_serializing_if = "Option::is_none")]
404 booking_id: Option<String>,
405 #[serde(skip_serializing_if = "Option::is_none")]
406 body: Option<BookingsCancelBody>,
407}
408
409impl Validate for BookingsCancel {
410 fn validate(mut self) -> Result<Self, ValidationError> where Self: Sized {
411 if self.booking_id.is_some() {
412 if let Some(body) = self.body.as_mut() {
413 body.idempotency_key = Some(Uuid::new_v4().to_string())
414 };
415
416 Ok(self)
417 } else {
418 Err(ValidationError)
419 }
420 }
421}
422
423impl<T: ParentBuilder> Builder<BookingsCancel, T> {
424 pub fn booking_id<S: Into<String>>(mut self, booking_id: S) -> Self {
425 self.body.booking_id = Some(booking_id.into());
426
427 self
428 }
429
430 pub fn booking_version(mut self, booking_version: i32) -> Self {
431 if let Some(body) = self.body.body.as_mut() {
432 body.booking_version = Some(booking_version)
433 } else {
434 self.body.body = Some(BookingsCancelBody {
435 idempotency_key: None,
436 booking_version: Some(booking_version)
437 })
438 }
439
440 self
441 }
442}
443
444#[derive(Serialize, Debug, Deserialize)]
445pub struct BookingsCancelBody {
446 #[serde(skip_serializing_if = "Option::is_none")]
447 idempotency_key: Option<String>,
448 #[serde(skip_serializing_if = "Option::is_none")]
449 booking_version: Option<i32>,
450}
451
452#[derive(Serialize, Debug, Deserialize, Default)]
458pub struct SearchAvailabilityQuery {
459 query: QueryBody,
460}
461
462impl Validate for SearchAvailabilityQuery {
463 fn validate(self) -> Result<Self, ValidationError> where Self: Sized {
464 if self.query.filter.start_at_range.is_some() {
465 Ok(self)
466 } else {
467 Err(ValidationError)
468 }
469 }
470}
471
472impl<T: ParentBuilder> Builder<SearchAvailabilityQuery, T> {
473 pub fn start_at_range<S: Into<String>>(mut self, start: S, end: S) -> Self {
474 self.body.query.filter.start_at_range = Some(StartAtRange {
475 end_at: end.into(),
476 start_at: start.into(),
477 });
478
479 self
480 }
481
482 pub fn location_id<S: Into<String>>(mut self, location_id: S) -> Self {
483 self.body.query.filter.location_id = Some(location_id.into());
484
485 self
486 }
487
488 pub fn segment_filters<S: Into<String>>(mut self, service_variation_id: S) -> Self {
489 let new_filter = SegmentFilter {
490 service_variation_id: service_variation_id.into(),
491 team_member_id_filter: None
492 };
493
494 match self.body.query.filter.segment_filters.as_mut() {
495 Some(filters) => {
496 filters.push(new_filter);
497 },
498 None => {
499 let filters = vec![new_filter];
500 self.body.query.filter.segment_filters = Some(filters)
501 }
502 };
503
504 self
505 }
506}
507
508#[derive(Serialize, Debug, Deserialize, Default)]
509pub struct QueryBody {
510 filter: AvailabilityQueryFilter,
511}
512
513#[cfg(test)]
514mod test_bookings {
515 use super::*;
516
517 #[tokio::test]
518 async fn test_search_query_builder() {
519 let expected = SearchAvailabilityQuery {
520 query: QueryBody {
521 filter: AvailabilityQueryFilter {
522 start_at_range: Some(StartAtRange {
523 end_at: "2023-10-12T07:20:50.52Z".to_string(),
524 start_at: "2022-10-12T07:20:50.52Z".to_string(),
525 }),
526 booking_id: None,
527 location_id: Some("LPNXWH14W6S47".to_string()),
528 segment_filters: None
529 }
530 }
531 };
532
533 let actual = Builder::from(SearchAvailabilityQuery::default())
534 .start_at_range(
535 "2022-10-12T07:20:50.52Z",
536 "2023-10-12T07:20:50.52Z")
537 .location_id("LPNXWH14W6S47")
538 .build()
539 .unwrap();
540
541 assert_eq!(format!("{:?}", expected), format!("{:?}", actual))
542 }
543
544 #[tokio::test]
545 async fn test_search_availability() {
546 use dotenv::dotenv;
547 use std::env;
548
549 dotenv().ok();
550 let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
551 let sut = SquareClient::new(&access_token);
552
553 let input = Builder::from(SearchAvailabilityQuery::default())
554 .start_at_range(
555 "2022-09-12T07:20:50.52Z",
556 "2022-10-12T07:20:50.52Z")
557 .location_id("L1JC53TYHS40Z")
558 .segment_filters("BJHURKYAIAQIDMY267GZNYNW")
559 .build().unwrap();
560
561 let result = sut.bookings().search_availability(input).await;
562
563 assert!(result.is_ok())
564 }
565
566 #[tokio::test]
567 async fn test_booking_post_builder() {
568 let actual = Builder::from(BookingsPost::default())
569 .sub_builder_from(Booking::default())
570 .start_at("2022-10-11T16:30:00Z")
571 .location_id("L1JC53TYHS40Z")
572 .customer_id("7PB8P9553RYA3F672D15369VK4")
573 .sub_builder_from(AppointmentSegment::default())
574 .duration_minutes(60.00)
575 .team_member_id("TMKFnToW8ByXrcm6")
576 .service_variation_id("BSOL4BB6RCMX6SH4KQIFWZDP")
577 .service_variation_version(1655427266071)
578 .build()
579 .unwrap()
580 .build()
581 .unwrap()
582 .build();
583
584 let expected = Booking {
585 id: None,
586 all_day: None,
587 appointment_segments: Some(vec![AppointmentSegment {
588 duration_minutes: 60.00,
589 team_member_id: "TMKFnToW8ByXrcm6".to_string(),
590 any_team_member_id: None,
591 intermission_minutes: None,
592 resource_ids: None,
593 service_variation_id: "BSOL4BB6RCMX6SH4KQIFWZDP".to_string(),
594 service_variation_version: 1655427266071,
595 }]),
596 created_at: None,
597 booking_creator_details: None,
598 customer_id: Some("7PB8P9553RYA3F672D15369VK4".to_string()),
599 customer_note: None,
600 location_id: Some("L1JC53TYHS40Z".to_string()),
601 location_type: None,
602 seller_note: None,
603 source: None,
604 start_at: Some("2022-10-11T16:30:00Z".to_string()),
605 status: None,
606 transition_time_minutes: None,
607 updated_at: None,
608 version: None
609 };
610
611 assert!(actual.is_ok());
612 assert_eq!(format!("{:?}", expected), format!("{:?}", actual.unwrap().booking))
613 }
614
615 #[tokio::test]
616 async fn test_booking_post_builder_fail() {
617 let res = Builder::from(BookingsPost::default())
618 .sub_builder_from(Booking::default())
619 .start_at("2022-10-11T16:30:00Z")
620 .customer_id("7PB8P9553RYA3F672D15369VK4")
621 .sub_builder_from(AppointmentSegment::default())
622 .duration_minutes(60.00)
623 .team_member_id("TMKFnToW8ByXrcm6")
624 .service_variation_id("BSOL4BB6RCMX6SH4KQIFWZDP")
625 .service_variation_version(1655427266071)
626 .build()
627 .unwrap()
628 .build();
629
630 assert!(res.is_err());
631 }
632
633 async fn test_create_booking() {
635 use dotenv::dotenv;
636 use std::env;
637
638 dotenv().ok();
639 let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
640 let sut = SquareClient::new(&access_token);
641
642 let input = BookingsPost {
643 idempotency_key: Some(Uuid::new_v4().to_string()),
644 booking: Booking {
645 id: None,
646 all_day: None,
647 appointment_segments: Some(vec![AppointmentSegment {
648 duration_minutes: 60.00,
649 team_member_id: "TMKFnToW8ByXrcm6".to_string(),
650 any_team_member_id: None,
651 intermission_minutes: None,
652 resource_ids: None,
653 service_variation_id: "BJHURKYAIAQIDMY267GZNYNW".to_string(),
654 service_variation_version: 1655427266071,
655 }]),
656 created_at: None,
657 booking_creator_details: None,
658 customer_id: Some("7PB8P9553RYA3F672D15369VK4".to_string()),
659 customer_note: None,
660 location_id: Some("L1JC53TYHS40Z".to_string()),
661 location_type: None,
662 seller_note: None,
663 source: None,
664 start_at: Some("2022-10-11T16:30:00Z".to_string()),
665 status: None,
666 transition_time_minutes: None,
667 updated_at: None,
668 version: None
669 }
670 };
671
672 let res = sut.bookings().create(input).await;
673
674 assert!(res.is_ok())
675 }
676
677 #[tokio::test]
678 async fn test_retrieve_booking() {
679 use dotenv::dotenv;
680 use std::env;
681
682 dotenv().ok();
683 let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
684 let sut = SquareClient::new(&access_token);
685
686 let res = sut.bookings()
687 .retrieve("burxkwa4ot1ydg".to_string())
688 .await;
689
690 assert!(res.is_ok())
691 }
692
693 #[tokio::test]
694 async fn test_bookings_cancel_builder() {
695 let expected = BookingsCancel {
696 booking_id: Some("9uv6i3p5x5ao1p".to_string()),
697 body: Some(BookingsCancelBody {
698 idempotency_key: Some(Uuid::new_v4().to_string()),
699 booking_version: None
700 })
701 };
702 let actual = Builder::from(BookingsCancel::default())
703 .booking_id("9uv6i3p5x5ao1p").build();
704
705 assert!(actual.is_ok());
706 assert_eq!(format!("{:?}", expected.booking_id),
707 format!("{:?}", actual.unwrap().booking_id));
708 }
709
710 #[tokio::test]
711 async fn test_bookings_cancel_builder_fail() {
712
713 let res = Builder::from(BookingsCancel::default()).build();
714
715 assert!(res.is_err());
716 }
717
718 #[tokio::test]
719 async fn test_cancel_booking() {
720 use dotenv::dotenv;
721 use std::env;
722
723 dotenv().ok();
724 let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
725 let sut = SquareClient::new(&access_token);
726
727 let input = BookingsCancel {
728 booking_id: Some("pi7kr2va3y4h4f".to_string()),
729 body: Some(BookingsCancelBody {
730 idempotency_key: Some(Uuid::new_v4().to_string()),
731 booking_version: None
732 })
733 };
734
735 let res = sut.bookings().cancel(input).await;
736
737 assert!(res.is_ok())
738 }
739
740 #[tokio::test]
741 async fn test_update_booking() {
742 use dotenv::dotenv;
743 use std::env;
744
745 dotenv().ok();
746 let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
747 let sut = SquareClient::new(&access_token);
748
749 let input = BookingsPost {
750 idempotency_key: Some(Uuid::new_v4().to_string()),
751 booking: Booking {
752 id: None,
753 all_day: None,
754 appointment_segments: Some(vec![AppointmentSegment {
755 duration_minutes: 60.00,
756 team_member_id: "TMKFnToW8ByXrcm6".to_string(),
757 any_team_member_id: None,
758 intermission_minutes: None,
759 resource_ids: None,
760 service_variation_id: "BSOL4BB6RCMX6SH4KQIFWZDP".to_string(),
761 service_variation_version: 1655427266071,
762 }]),
763 created_at: None,
764 booking_creator_details: None,
765 customer_id: Some("7PB8P9553RYA3F672D15369VK4".to_string()),
766 customer_note: None,
767 location_id: Some("L1JC53TYHS40Z".to_string()),
768 location_type: None,
769 seller_note: Some("be nice!".to_string()),
770 source: None,
771 start_at: Some("2022-10-11T16:30:00Z".to_string()),
772 status: None,
773 transition_time_minutes: None,
774 updated_at: None,
775 version: None
776 }
777 };
778
779 let res = sut.bookings()
780 .update(input, "oruft3c9lh0duq".to_string())
781 .await;
782
783 assert!(res.is_ok())
784 }
785
786 #[tokio::test]
787 async fn test_list_bookings_query_builder() {
788 let expected = vec![
789 ("location_id".to_string(), "L1JC53TYHS40Z".to_string()),
790 ("start_at_min".to_string(), "2022-09-12T07:20:50.52Z".to_string()),
791 ];
792
793 let actual = ListBookingsQueryBuilder::new()
794 .location_id("L1JC53TYHS40Z")
795 .start_at_min("2022-09-12T07:20:50.52Z")
796 .build()
797 .await;
798
799 assert_eq!(expected, actual)
800
801
802 }
803
804 #[tokio::test]
805 async fn test_list_bookings() {
806 use dotenv::dotenv;
807 use std::env;
808
809 dotenv().ok();
810 let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
811 let sut = SquareClient::new(&access_token);
812
813 let input = vec![
814 ("start_at_min".to_string(), "2022-09-12T07:20:50.52Z".to_string())
815 ];
816
817 let res = sut.bookings().list(Some(input)).await;
818
819 assert!(res.is_ok())
820 }
821
822 #[tokio::test]
823 async fn test_retrieve_business_booking_profile() {
824 use dotenv::dotenv;
825 use std::env;
826
827 dotenv().ok();
828 let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
829 let sut = SquareClient::new(&access_token);
830
831 let res = sut.bookings().retrieve_business_profile().await;
832
833 assert!(res.is_ok())
834 }
835
836 #[tokio::test]
837 async fn test_list_team_member_booking_profile_query_builder() {
838 let expected = vec![
839 ("limit".to_string(), "10".to_string()),
840 ("bookable_only".to_string(), "true".to_string()),
841 ("location_id".to_string(), "L1JC53TYHS40Z".to_string()),
842 ];
843
844 let actual = ListTeamMemberBookingsProfileBuilder::new()
845 .bookable_only()
846 .limit(10)
847 .location_id("L1JC53TYHS40Z")
848 .build()
849 .await;
850
851 assert_eq!(expected, actual)
852
853
854 }
855
856 #[tokio::test]
857 async fn test_list_team_member_booking_profiles() {
858 use dotenv::dotenv;
859 use std::env;
860
861 dotenv().ok();
862 let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
863 let sut = SquareClient::new(&access_token);
864
865 let input = vec![
866 ("limit".to_string(), "10".to_string()),
867 ("bookable_only".to_string(), "true".to_string()),
868 ("location_id".to_string(), "L1JC53TYHS40Z".to_string()),
869 ];
870
871 let res = sut.bookings()
872 .list_team_member_profiles(Some(input))
873 .await;
874
875 assert!(res.is_ok())
876 }
877
878 #[tokio::test]
879 async fn test_retrieve_team_member_booking_profile() {
880 use dotenv::dotenv;
881 use std::env;
882
883 dotenv().ok();
884 let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN to be set");
885 let sut = SquareClient::new(&access_token);
886
887 let res = sut.bookings()
888 .retrieve_team_member_profiles("TMKFnToW8ByXrcm6".to_string())
889 .await;
890
891 assert!(res.is_ok())
892 }
893}
894