1use codes_iso_3166::part_1::CountryCode;
17use codes_iso_4217::CurrencyCode;
18use futures_core::Stream;
19use futures_util::stream::TryStreamExt;
20use reqwest::{Method, RequestBuilder};
21use serde::{Deserialize, Serialize};
22use serde_enum_str::{Deserialize_enum_str, Serialize_enum_str};
23use time::format_description::well_known::Rfc3339;
24use time::{OffsetDateTime, UtcOffset};
25
26use crate::client::taxes::{TaxId, TaxIdRequest};
27use crate::client::Client;
28use crate::config::ListParams;
29use crate::error::Error;
30use crate::serde::Empty;
31use crate::util::StrIteratorExt;
32
33const CUSTOMERS_PATH: [&str; 1] = ["customers"];
34
35#[derive(Deserialize)]
36struct ArrayResponse<T> {
37 data: Vec<T>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
42pub enum CustomerId<'a> {
43 #[serde(rename = "customer_id")]
45 Orb(&'a str),
46 #[serde(rename = "external_customer_id")]
48 External(&'a str),
49}
50
51impl<'a> Default for CustomerId<'a> {
52 fn default() -> CustomerId<'a> {
53 CustomerId::Orb("")
54 }
55}
56
57#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize)]
59pub struct CreateCustomerRequest<'a> {
60 #[serde(skip_serializing_if = "Option::is_none")]
63 #[serde(rename = "external_customer_id")]
64 pub external_id: Option<&'a str>,
65 pub name: &'a str,
67 pub email: &'a str,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub additional_emails: Option<Vec<&'a str>>,
72 #[serde(skip_serializing_if = "Option::is_none")]
75 pub timezone: Option<&'a str>,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 #[serde(flatten)]
79 pub payment_provider: Option<CustomerPaymentProviderRequest<'a>>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub shipping_address: Option<AddressRequest<'a>>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub billing_address: Option<AddressRequest<'a>>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub currency: Option<CurrencyCode>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub tax_id: Option<TaxIdRequest<'a>>,
92 #[serde(skip_serializing)]
96 pub idempotency_key: Option<&'a str>,
97}
98
99#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize)]
101pub struct UpdateCustomerRequest<'a> {
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub name: Option<&'a str>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub email: Option<&'a str>,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub additional_emails: Option<Vec<&'a str>>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 #[serde(flatten)]
114 pub payment_provider: Option<CustomerPaymentProviderRequest<'a>>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub shipping_address: Option<AddressRequest<'a>>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub billing_address: Option<AddressRequest<'a>>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub tax_id: Option<TaxIdRequest<'a>>,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
128pub struct CustomerPaymentProviderRequest<'a> {
129 #[serde(rename = "payment_provider")]
131 pub kind: PaymentProvider,
132 #[serde(rename = "payment_provider_id")]
135 pub id: &'a str,
136}
137
138#[allow(clippy::large_enum_variant)]
141#[derive(Deserialize)]
142#[serde(untagged)]
143pub(crate) enum CustomerResponse {
144 Normal(Customer),
145 Deleted { id: String, deleted: bool },
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
150pub struct Customer {
151 pub id: String,
153 #[serde(rename = "external_customer_id")]
156 pub external_id: Option<String>,
157 pub name: String,
159 pub email: String,
161 pub additional_emails: Vec<String>,
163 pub timezone: String,
166 pub payment_provider_id: Option<String>,
169 pub payment_provider: Option<PaymentProvider>,
171 pub shipping_address: Option<Address>,
173 pub billing_address: Option<Address>,
175 pub currency: Option<CurrencyCode>,
177 pub tax_id: Option<TaxId>,
179 pub auto_collection: bool,
181 pub balance: String,
183 #[serde(with = "time::serde::rfc3339")]
185 pub created_at: OffsetDateTime,
186 pub portal_url: Option<String>,
188}
189
190#[non_exhaustive]
192#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize_enum_str, Serialize_enum_str)]
193pub enum PaymentProvider {
194 #[serde(rename = "quickbooks")]
196 Quickbooks,
197 #[serde(rename = "bill.com")]
199 BillDotCom,
200 #[serde(rename = "stripe")]
202 Stripe,
203 #[serde(rename = "stripe_charge")]
205 StripeCharge,
206 #[serde(rename = "stripe_invoice")]
208 StripeInvoice,
209 #[serde(other)]
211 Other(String),
212}
213
214#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Deserialize, Serialize)]
216pub struct AddressRequest<'a> {
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub city: Option<&'a str>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub country: Option<CountryCode>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub line1: Option<&'a str>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub line2: Option<&'a str>,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub postal_code: Option<&'a str>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub state: Option<&'a str>,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
239pub struct Address {
240 pub city: Option<String>,
242 pub country: Option<CountryCode>,
244 pub line1: Option<String>,
246 pub line2: Option<String>,
248 pub postal_code: Option<String>,
250 pub state: Option<String>,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
256#[serde(tag = "entry_type")]
257pub enum LedgerEntryRequest<'a> {
258 #[serde(rename = "increment")]
260 Increment(AddIncrementCreditLedgerEntryRequestParams<'a>),
261 #[serde(rename = "void")]
263 Void(AddVoidCreditLedgerEntryRequestParams<'a>),
264 }
266
267#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize)]
269pub struct CreditLedgerInvoiceSettingsRequestParams<'a> {
270 pub auto_collection: bool,
273 pub net_terms: u64,
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub memo: Option<&'a str>,
279 #[serde(skip_serializing_if = "Option::is_none")]
282 pub require_successful_payment: Option<bool>,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
287pub struct AddIncrementCreditLedgerEntryRequestParams<'a> {
288 pub amount: serde_json::Number,
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub description: Option<&'a str>,
293 #[serde(with = "time::serde::rfc3339::option")]
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub expiry_date: Option<OffsetDateTime>,
297 #[serde(with = "time::serde::rfc3339::option")]
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub effective_date: Option<OffsetDateTime>,
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub per_unit_cost_basis: Option<&'a str>,
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub invoice_settings: Option<CreditLedgerInvoiceSettingsRequestParams<'a>>,
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize_enum_str, Serialize_enum_str)]
311pub enum VoidReason {
312 #[serde(rename = "refund")]
314 Refund,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
319pub struct AddVoidCreditLedgerEntryRequestParams<'a> {
320 pub amount: serde_json::Number,
322 pub block_id: &'a str,
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub void_reason: Option<VoidReason>,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub description: Option<&'a str>,
330}
331
332#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
334pub struct CustomerCreditBlock {
335 pub id: String,
337 pub balance: serde_json::Number,
339 #[serde(with = "time::serde::rfc3339::option")]
341 pub expiry_date: Option<OffsetDateTime>,
342 pub per_unit_cost_basis: Option<String>,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
348#[serde(tag = "entry_type")]
349pub enum LedgerEntry {
350 #[serde(rename = "increment")]
352 Increment(IncrementLedgerEntry),
353 #[serde(rename = "void")]
355 Void(VoidLedgerEntry),
356 #[serde(rename = "void_initiated")]
358 VoidInitiated(VoidInitiatedLedgerEntry),
359 }
361
362#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize_enum_str)]
364pub enum EntryStatus {
365 #[serde(rename = "committed")]
367 Committed,
368 #[serde(rename = "pending")]
370 Pending,
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
375pub struct CustomerIdentifier {
376 pub id: String,
378 pub external_customer_id: Option<String>,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
385pub struct LedgerEntryCreditBlock {
386 pub id: String,
388 #[serde(with = "time::serde::rfc3339::option")]
390 pub expiry_date: Option<OffsetDateTime>,
391 pub per_unit_cost_basis: Option<String>,
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
397pub struct BaseLedgerEntry {
398 pub id: String,
400 pub ledger_sequence_number: u64,
402 pub entry_status: EntryStatus,
404 pub customer: CustomerIdentifier,
406 pub starting_balance: serde_json::Number,
408 pub ending_balance: serde_json::Number,
410 pub amount: serde_json::Number,
412 #[serde(with = "time::serde::rfc3339")]
414 pub created_at: OffsetDateTime,
415 pub description: Option<String>,
417 pub credit_block: LedgerEntryCreditBlock,
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
423pub struct IncrementLedgerEntry {
424 #[serde(flatten)]
426 pub ledger: BaseLedgerEntry,
427}
428
429#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
431pub struct VoidLedgerEntry {
432 #[serde(flatten)]
434 pub ledger: BaseLedgerEntry,
435 pub void_reason: Option<String>,
437 pub void_amount: serde_json::Number,
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
443pub struct VoidInitiatedLedgerEntry {
444 #[serde(flatten)]
446 pub ledger: BaseLedgerEntry,
447 #[serde(with = "time::serde::rfc3339")]
449 pub new_block_expiry_date: OffsetDateTime,
450 pub void_reason: Option<String>,
452 pub void_amount: serde_json::Number,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize_enum_str, Serialize_enum_str)]
458pub enum CostViewMode {
459 #[serde(rename = "periodic")]
461 Periodic,
462 #[serde(rename = "cumulative")]
464 Cumulative,
465}
466
467#[derive(Debug, Default, Clone)]
468struct CustomerCostParamsFilter<'a> {
469 timeframe_start: Option<&'a OffsetDateTime>,
470 timeframe_end: Option<&'a OffsetDateTime>,
471 view_mode: Option<CostViewMode>,
472 group_by: Option<&'a str>,
473}
474
475trait Filterable<T> {
476 fn apply(self, filter: &T) -> Self;
478}
479
480impl Filterable<CustomerCostParamsFilter<'_>> for RequestBuilder {
481 fn apply(mut self, filter: &CustomerCostParamsFilter) -> Self {
483 if let Some(view_mode) = &filter.view_mode {
484 self = self.query(&[("view_mode", view_mode.to_string())]);
485 }
486 if let Some(group_by) = &filter.group_by {
487 self = self.query(&[("group_by", group_by)]);
488 }
489 if let Some(timeframe_start) = &filter.timeframe_start {
490 self = self.query(&[(
491 "timeframe_start",
492 timeframe_start
493 .to_offset(UtcOffset::UTC)
495 .format(&Rfc3339)
496 .unwrap(),
497 )]);
498 }
499 if let Some(timeframe_end) = &filter.timeframe_end {
500 self = self.query(&[(
501 "timeframe_end",
502 timeframe_end
503 .to_offset(UtcOffset::UTC)
505 .format(&Rfc3339)
506 .unwrap(),
507 )]);
508 }
509 self
510 }
511}
512
513#[derive(Debug, Default, Clone)]
515pub struct CustomerCostParams<'a> {
516 filter: CustomerCostParamsFilter<'a>,
517}
518
519impl<'a> CustomerCostParams<'a> {
520 pub const fn timeframe_start(mut self, timeframe_start: &'a OffsetDateTime) -> Self {
523 self.filter.timeframe_start = Some(timeframe_start);
524 self
525 }
526
527 pub const fn timeframe_end(mut self, timeframe_end: &'a OffsetDateTime) -> Self {
529 self.filter.timeframe_end = Some(timeframe_end);
530 self
531 }
532
533 pub const fn view_mode(mut self, view_mode: CostViewMode) -> Self {
535 self.filter.view_mode = Some(view_mode);
536 self
537 }
538
539 pub const fn group_by(mut self, group_by: &'a str) -> Self {
541 self.filter.group_by = Some(group_by);
542 self
543 }
544}
545
546#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
548pub struct CustomerCostBucket {
549 pub subtotal: String,
551 pub total: String,
553 #[serde(with = "time::serde::rfc3339")]
555 pub timeframe_start: OffsetDateTime,
556 #[serde(with = "time::serde::rfc3339")]
558 pub timeframe_end: OffsetDateTime,
559 pub per_price_costs: Vec<CustomerCostPriceBlock>,
561}
562
563#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
565pub struct CustomerCostPriceBlock {
566 pub quantity: Option<serde_json::Number>,
568 pub subtotal: String,
570 pub total: String,
572 pub price: CustomerCostPriceBlockPrice,
574 pub price_groups: Option<Vec<CustomerCostPriceBlockPriceGroup>>,
576}
577
578#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
580pub struct CustomerCostPriceBlockPriceGroup {
581 pub grouping_key: String,
583 pub grouping_value: Option<String>,
585 pub secondary_grouping_key: Option<String>,
587 pub secondary_grouping_value: Option<String>,
589 pub total: String,
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
597#[serde(tag = "model_type")]
598pub enum CustomerCostPriceBlockPrice {
599 #[serde(rename = "matrix")]
601 Matrix(CustomerCostPriceBlockMatrixPrice),
602 #[serde(rename = "unit")]
604 Unit(CustomerCostPriceBlockUnitPrice),
605 }
607
608#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
610pub struct CustomerCostPriceBlockMatrixPrice {
611 pub id: String,
613 #[serde(rename = "external_price_id")]
615 pub external_id: Option<String>,
616 pub item: CustomerCostItem,
618 pub matrix_config: CustomerCostPriceBlockMatrixPriceConfig,
620}
621
622#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
624pub struct CustomerCostItem {
625 pub id: String,
627 pub name: String,
629}
630
631#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
633pub struct CustomerCostPriceBlockMatrixPriceConfig {
634 pub default_unit_amount: String,
636 pub dimensions: Vec<Option<String>>,
638 pub matrix_values: Vec<CustomerCostPriceBlockMatrixPriceValue>,
640}
641
642#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
644pub struct CustomerCostPriceBlockMatrixPriceValue {
645 pub dimension_values: Vec<Option<String>>,
647 pub unit_amount: String,
649}
650
651#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
653pub struct CustomerCostPriceBlockUnitPrice {
654 pub id: String,
656 #[serde(rename = "external_price_id")]
658 pub external_id: Option<String>,
659 pub item: CustomerCostItem,
661 pub unit_config: CustomerCostPriceBlockUnitPriceConfig,
663}
664
665#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
667pub struct CustomerCostPriceBlockUnitPriceConfig {
668 pub unit_amount: String,
670}
671
672impl Client {
673 pub fn list_customers(
678 &self,
679 params: &ListParams,
680 ) -> impl Stream<Item = Result<Customer, Error>> + '_ {
681 let req = self.build_request(Method::GET, CUSTOMERS_PATH);
682 self.stream_paginated_request(params, req)
683 .try_filter_map(|res| async {
684 match res {
685 CustomerResponse::Normal(c) => Ok(Some(c)),
686 CustomerResponse::Deleted {
687 id: _,
688 deleted: true,
689 } => Ok(None),
690 CustomerResponse::Deleted { id, deleted: false } => {
691 Err(Error::UnexpectedResponse {
692 detail: format!(
693 "customer {id} used deleted response shape \
694 but deleted field was `false`"
695 ),
696 })
697 }
698 }
699 })
700 }
701
702 pub async fn create_customer(
704 &self,
705 customer: &CreateCustomerRequest<'_>,
706 ) -> Result<Customer, Error> {
707 let mut req = self.build_request(Method::POST, CUSTOMERS_PATH);
708 if let Some(key) = customer.idempotency_key {
709 req = req.header("Idempotency-Key", key);
710 }
711 let req = req.json(customer);
712 let res = self.send_request(req).await?;
713 Ok(res)
714 }
715
716 pub async fn get_customer(&self, id: &str) -> Result<Customer, Error> {
718 let req = self.build_request(Method::GET, CUSTOMERS_PATH.chain_one(id));
719 let res = self.send_request(req).await?;
720 Ok(res)
721 }
722
723 pub async fn get_customer_by_external_id(&self, external_id: &str) -> Result<Customer, Error> {
725 let req = self.build_request(
726 Method::GET,
727 CUSTOMERS_PATH
728 .chain_one("external_customer_id")
729 .chain_one(external_id),
730 );
731 let res = self.send_request(req).await?;
732 Ok(res)
733 }
734
735 pub async fn update_customer(
737 &self,
738 id: &str,
739 customer: &UpdateCustomerRequest<'_>,
740 ) -> Result<Customer, Error> {
741 let req = self.build_request(Method::PUT, CUSTOMERS_PATH.chain_one(id));
742 let req = req.json(customer);
743 let res = self.send_request(req).await?;
744 Ok(res)
745 }
746
747 pub async fn update_customer_by_external_id(
749 &self,
750 external_id: &str,
751 customer: &UpdateCustomerRequest<'_>,
752 ) -> Result<Customer, Error> {
753 let req = self.build_request(
754 Method::PUT,
755 CUSTOMERS_PATH
756 .chain_one("external_customer_id")
757 .chain_one(external_id),
758 );
759 let req = req.json(customer);
760 let res = self.send_request(req).await?;
761 Ok(res)
762 }
763
764 pub async fn delete_customer(&self, id: &str) -> Result<(), Error> {
766 let req = self.build_request(Method::DELETE, CUSTOMERS_PATH.chain_one(id));
767 let _: Empty = self.send_request(req).await?;
768 Ok(())
769 }
770
771 pub fn get_customer_credit_balance(
776 &self,
777 id: &str,
778 params: &ListParams,
779 ) -> impl Stream<Item = Result<CustomerCreditBlock, Error>> + '_ {
780 let req = self.build_request(
781 Method::GET,
782 CUSTOMERS_PATH.chain_one(id).chain_one("credits"),
783 );
784 self.stream_paginated_request(params, req)
785 }
786
787 pub fn get_customer_credit_balance_by_external_id(
792 &self,
793 external_id: &str,
794 params: &ListParams,
795 ) -> impl Stream<Item = Result<CustomerCreditBlock, Error>> + '_ {
796 let req = self.build_request(
797 Method::GET,
798 CUSTOMERS_PATH
799 .chain_one("external_customer_id")
800 .chain_one(external_id)
801 .chain_one("credits"),
802 );
803 self.stream_paginated_request(params, req)
804 }
805
806 pub async fn create_ledger_entry(
808 &self,
809 id: &str,
810 entry: &LedgerEntryRequest<'_>,
811 ) -> Result<LedgerEntry, Error> {
812 let req = self.build_request(
813 Method::POST,
814 CUSTOMERS_PATH
815 .chain_one(id)
816 .chain_one("credits")
817 .chain_one("ledger_entry"),
818 );
819 let req = req.json(entry);
820 self.send_request(req).await
821 }
822
823 pub async fn get_customer_costs(
825 &self,
826 id: &str,
827 params: &CustomerCostParams<'_>,
828 ) -> Result<Vec<CustomerCostBucket>, Error> {
829 let req = self.build_request(Method::GET, CUSTOMERS_PATH.chain_one(id).chain_one("costs"));
830 let req = req.apply(¶ms.filter);
831 let res: ArrayResponse<CustomerCostBucket> = self.send_request(req).await?;
832 Ok(res.data)
833 }
834
835 pub async fn get_customer_costs_by_external_id(
837 &self,
838 external_id: &str,
839 params: &CustomerCostParams<'_>,
840 ) -> Result<Vec<CustomerCostBucket>, Error> {
841 let req = self.build_request(
842 Method::GET,
843 CUSTOMERS_PATH
844 .chain_one("external_customer_id")
845 .chain_one(external_id)
846 .chain_one("costs"),
847 );
848 let req = req.apply(¶ms.filter);
849 let res: ArrayResponse<CustomerCostBucket> = self.send_request(req).await?;
850 Ok(res.data)
851 }
852}