diff --git a/src/client/customers.rs b/src/client/customers.rs index e452c6f..ad164f8 100644 --- a/src/client/customers.rs +++ b/src/client/customers.rs @@ -265,7 +265,7 @@ pub enum LedgerEntryRequest<'a> { } /// Optional invoicing settings for a credit purchase. -#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct CreditLedgerInvoiceSettingsRequestParams<'a> { /// Whether the credits purchase invoice should auto collect with the customer's saved payment /// method. @@ -282,6 +282,25 @@ pub struct CreditLedgerInvoiceSettingsRequestParams<'a> { pub require_successful_payment: Option, } +/// Optional invoicing settings for a credit purchase. +/// Unlike CreditLedgerInvoiceSettingsRequestParams, this struct is owned. +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct CreditLedgerInvoiceSettings { + /// Whether the credits purchase invoice should auto collect with the customer's saved payment + /// method. + pub auto_collection: bool, + /// The difference between the invoice date and the issue date for the invoice. If due on issue, + /// set this to `0`. + pub net_terms: u64, + /// An optional memo to display on the invoice + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, + /// Whether the credits should be withheld from the customer account until the invoice is paid. + /// This applies primarily to stripe invoicing. + #[serde(skip_serializing_if = "Option::is_none")] + pub require_successful_payment: Option, +} + /// The parameters used to create a customer credit ledger entry. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] pub struct AddIncrementCreditLedgerEntryRequestParams<'a> { @@ -341,6 +360,21 @@ pub struct CustomerCreditBlock { pub expiry_date: Option, /// The price per credit. pub per_unit_cost_basis: Option, + /// The status of the credit block. Credit blocks are initialized into `pending_payment` + /// when require_successful_payment is set in [`CreditLedgerInvoiceSettings`]. + pub status: CreditBlockStatus, +} + +/// The status of the credit block. Credit blocks are initialized into `pending_payment` +/// when require_successful_payment is set in [`CreditLedgerInvoiceSettings`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub enum CreditBlockStatus { + /// A credit block that should count to the total balance. + #[serde(rename = "active")] + Active, + /// A credit block which hasn't been marked as paid. + #[serde(rename = "pending_payment")] + PendingPayment, } /// The type of ledger entry @@ -479,6 +513,45 @@ pub struct CustomerCostParamsFilter<'a> { pub group_by: Option<&'a str>, } +/// Configures automatic payments for the customer +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct CreateTopUpRequest<'a> { + /// The currency used for this topup. + pub currency: CurrencyCode, + /// The threshold at which to initiate the topup payment. + pub threshold: &'a str, + /// The amount that should be purchase. + pub amount: &'a str, + /// The cost basis of the credits purchase. + pub per_unit_cost_basis: &'a str, + /// Additional settings for configuring the invoice + pub invoice_settings: Option>, +} + +/// The response for a user's listing topups. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ListTopUpsResponse { + /// The list of topups. + pub data: Vec, +} + +/// Configures an external payment or invoicing solution for a customer. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct TopUp { + /// The Orb-assigned ID of this topup. + pub id: String, + /// The currency used for this topup. + pub currency: CurrencyCode, + /// The threshold at which to initiate the topup payment. + pub threshold: String, + /// The amount that should be purchase. + pub amount: String, + /// The cost basis of the credits purchase. + pub per_unit_cost_basis: String, + /// Additional settings for configuring the invoice + pub invoice_settings: Option, +} + trait Filterable { /// Apply the filter to a request. fn apply(self, filter: &T) -> Self; @@ -828,6 +901,24 @@ impl Client { self.send_request(req).await } + /// Create a new ledger entry for the specified customer's balance by external ID + pub async fn create_ledger_entry_by_external_id( + &self, + external_id: &str, + entry: &LedgerEntryRequest<'_>, + ) -> Result { + let req = self.build_request( + Method::POST, + CUSTOMERS_PATH + .chain_one("external_customer_id") + .chain_one(external_id) + .chain_one("credits") + .chain_one("ledger_entry"), + ); + let req = req.json(entry); + self.send_request(req).await + } + /// Fetch a day-by-day snapshot of a customer's costs. pub async fn get_customer_costs( &self, @@ -857,4 +948,109 @@ impl Client { let res: ArrayResponse = self.send_request(req).await?; Ok(res.data) } + + /// List top-ups for a customer. There is a maximum of one topup per currency, so in most + /// cases, pagination is not important. + pub async fn get_customer_topup(&self, id: &str) -> Result { + let req = self.build_request( + Method::GET, + CUSTOMERS_PATH + .chain_one(id) + .chain_one("credits") + .chain_one("top_ups"), + ); + let res: ListTopUpsResponse = self.send_request(req).await?; + Ok(res) + } + + /// List top-ups for a customer by external id. There is a maximum of one topup per currency. + pub async fn get_customer_topup_by_external_id( + &self, + external_id: &str, + ) -> Result { + let req = self.build_request( + Method::GET, + CUSTOMERS_PATH + .chain_one("external_customer_id") + .chain_one(external_id) + .chain_one("credits") + .chain_one("top_ups"), + ); + let res: ListTopUpsResponse = self.send_request(req).await?; + Ok(res) + } + + /// Create top-up for a customer. There is a maximum of one topup per currency. + pub async fn create_customer_topup<'a>( + &self, + id: &str, + body: CreateTopUpRequest<'a>, + ) -> Result { + let req = self.build_request( + Method::POST, + CUSTOMERS_PATH + .chain_one(id) + .chain_one("credits") + .chain_one("top_ups"), + ); + let req: RequestBuilder = req.json(&body); + let res: TopUp = self.send_request(req).await?; + Ok(res) + } + + /// Create top-up for a customer by external id. There is a maximum of one topup per currency. + pub async fn create_customer_topup_by_external_id<'a>( + &self, + external_id: &str, + body: CreateTopUpRequest<'a>, + ) -> Result { + let req = self.build_request( + Method::POST, + CUSTOMERS_PATH + .chain_one("external_customer_id") + .chain_one(external_id) + .chain_one("credits") + .chain_one("top_ups"), + ); + let req: RequestBuilder = req.json(&body); + let res: TopUp = self.send_request(req).await?; + Ok(res) + } + + /// Create top-up for a customer. There is a maximum of one topup per currency. + pub async fn delete_customer_topup<'a>( + &self, + user_id: &str, + top_up_id: &str, + ) -> Result<(), Error> { + let req = self.build_request( + Method::DELETE, + CUSTOMERS_PATH + .chain_one(user_id) + .chain_one("credits") + .chain_one("top_ups") + .chain_one(top_up_id), + ); + let _: Empty = self.send_request(req).await?; + Ok(()) + } + + /// Create top-up for a customer by external id. There is a maximum of one topup per currency. + pub async fn delete_customer_topup_by_external_id<'a>( + &self, + user_external_id: &str, + top_up_id: &str, + ) -> Result<(), Error> { + let req = self.build_request( + Method::DELETE, + CUSTOMERS_PATH + .chain_one("external_customer_id") + .chain_one(user_external_id) + .chain_one("credits") + .chain_one("top_ups") + .chain_one(top_up_id), + ); + let _: Empty = self.send_request(req).await?; + Ok(()) + } } diff --git a/src/client/invoices.rs b/src/client/invoices.rs index e9ee9a5..ac9ad07 100644 --- a/src/client/invoices.rs +++ b/src/client/invoices.rs @@ -18,7 +18,7 @@ use std::collections::BTreeMap; use futures_core::Stream; use reqwest::Method; use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; use crate::client::customers::CustomerId; use crate::client::Client; @@ -38,8 +38,9 @@ pub struct Invoice { /// The subscription associated with this invoice. pub subscription: Option, /// The issue date of the invoice. - #[serde(with = "time::serde::rfc3339")] - pub invoice_date: OffsetDateTime, + #[serde(with = "time::serde::rfc3339::option")] + #[serde(default)] + pub invoice_date: Option, /// An automatically generated number to help track and reconcile invoices. pub invoice_number: String, /// The link to download the PDF representation of the invoice. @@ -66,9 +67,19 @@ pub struct Invoice { /// values. #[serde(default)] pub metadata: BTreeMap, + /// The breakdown of prices in this invoice + pub line_items: Vec, // TODO: many missing fields. } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct InvoiceLineItem { + /// The name of the price associated with this line item. + pub name: String, + /// The line amount before any line item-specific discounts or minimums. + pub subtotal: String, +} + /// Identifies the customer associated with an [`Invoice`]. #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct InvoiceCustomer { @@ -128,8 +139,7 @@ impl InvoiceStatusFilter { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] pub struct MarkInvoiceAsPaidBody<'a> { /// A date string to specify the date of the payment. - #[serde(with = "time::serde::rfc3339")] - pub payment_received_date: OffsetDateTime, + pub payment_received_date: Date, /// An optional external ID to associate with the payment. pub external_id: Option<&'a str>, @@ -235,7 +245,16 @@ impl Client { /// Gets an invoice by ID. pub async fn get_invoice(&self, id: &str) -> Result { let req = self.build_request(Method::GET, INVOICES.chain_one(id)); - let res = self.send_request(req).await?; + let res: Invoice = self.send_request(req).await?; + Ok(res) + } + + /// Fetches the upcoming invoice for the current billing period given a subscription. + pub async fn get_upcoming_invoice(&self, subscription_id: &str) -> Result { + let req = self + .build_request(Method::GET, INVOICES.chain_one("upcoming")) + .query(&[("subscription_id", subscription_id)]); + let res: Invoice = self.send_request(req).await?; Ok(res) } diff --git a/src/lib.rs b/src/lib.rs index 8451ce1..70b66e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,14 +44,15 @@ mod util; pub use client::customers::{ AddIncrementCreditLedgerEntryRequestParams, AddVoidCreditLedgerEntryRequestParams, Address, - AddressRequest, CostViewMode, CreateCustomerRequest, CreditLedgerInvoiceSettingsRequestParams, - Customer, CustomerCostBucket, CustomerCostItem, CustomerCostParams, CustomerCostParamsFilter, - CustomerCostPriceBlock, CustomerCostPriceBlockMatrixPrice, - CustomerCostPriceBlockMatrixPriceConfig, CustomerCostPriceBlockMatrixPriceValue, - CustomerCostPriceBlockPrice, CustomerCostPriceBlockPriceGroup, CustomerCostPriceBlockUnitPrice, + AddressRequest, CostViewMode, CreateCustomerRequest, CreateTopUpRequest, CreditBlockStatus, + CreditLedgerInvoiceSettingsRequestParams, Customer, CustomerCostBucket, CustomerCostItem, + CustomerCostParams, CustomerCostParamsFilter, CustomerCostPriceBlock, + CustomerCostPriceBlockMatrixPrice, CustomerCostPriceBlockMatrixPriceConfig, + CustomerCostPriceBlockMatrixPriceValue, CustomerCostPriceBlockPrice, + CustomerCostPriceBlockPriceGroup, CustomerCostPriceBlockUnitPrice, CustomerCostPriceBlockUnitPriceConfig, CustomerCreditBlock, CustomerId, - CustomerPaymentProviderRequest, LedgerEntry, LedgerEntryRequest, PaymentProvider, - UpdateCustomerRequest, VoidReason, + CustomerPaymentProviderRequest, LedgerEntry, LedgerEntryRequest, ListTopUpsResponse, + PaymentProvider, TopUp, UpdateCustomerRequest, VoidReason, }; pub use client::events::{ AmendEventRequest, Event, EventPropertyValue, EventSearchParams, IngestEventDebugResponse,