Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SchedulePlanChangeRequest, make CustomerCostParamsFilter pub #39

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 224 additions & 8 deletions src/client/customers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -282,6 +282,25 @@ pub struct CreditLedgerInvoiceSettingsRequestParams<'a> {
pub require_successful_payment: Option<bool>,
}

/// 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<String>,
/// 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<bool>,
}

/// The parameters used to create a customer credit ledger entry.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct AddIncrementCreditLedgerEntryRequestParams<'a> {
Expand Down Expand Up @@ -341,6 +360,21 @@ pub struct CustomerCreditBlock {
pub expiry_date: Option<OffsetDateTime>,
/// The price per credit.
pub per_unit_cost_basis: Option<String>,
/// 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
Expand All @@ -356,7 +390,19 @@ pub enum LedgerEntry {
/// Voiding of an existing ledger entry has been initiated
#[serde(rename = "void_initiated")]
VoidInitiated(VoidInitiatedLedgerEntry),
// TODO: additional ledger entry types
/// Expiration change
#[serde(rename = "expiration_change")]
ExpirationChange(BaseLedgerEntry),
/// Credit block expiry
#[serde(rename = "credit_block_expiry")]
CreditBlockExpiry(BaseLedgerEntry),
/// Decrement
#[serde(rename = "decrement")]
DecrementLedgerEntry(BaseLedgerEntry),
/// Amendment
#[serde(rename = "amendment")]
Amendment(BaseLedgerEntry),
// TODO: Keep up to date with https://docs.withorb.com/reference/create-ledger-entry
}

/// The state of a ledger entry
Expand Down Expand Up @@ -464,12 +510,58 @@ pub enum CostViewMode {
Cumulative,
}

/// The filters applied to the customer costs query.
#[derive(Debug, Default, Clone)]
struct CustomerCostParamsFilter<'a> {
timeframe_start: Option<&'a OffsetDateTime>,
timeframe_end: Option<&'a OffsetDateTime>,
view_mode: Option<CostViewMode>,
group_by: Option<&'a str>,
pub struct CustomerCostParamsFilter<'a> {
/// The start of the returned range. If not specified this defaults to the billing period start
/// date.
pub timeframe_start: Option<&'a OffsetDateTime>,
/// The start of the returned range. If not specified this defaults to the billing period end
/// date.
pub timeframe_end: Option<&'a OffsetDateTime>,
/// How costs should be broken down in the resultant day-by-day view.
pub view_mode: Option<CostViewMode>,
/// The custom attribute to group costs by.
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<CreditLedgerInvoiceSettingsRequestParams<'a>>,
}

/// 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<TopUp>,
}

/// 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<CreditLedgerInvoiceSettings>,
}

trait Filterable<T> {
Expand Down Expand Up @@ -513,7 +605,8 @@ impl Filterable<CustomerCostParamsFilter<'_>> for RequestBuilder {
/// Parameters for a Customer Costs query.
#[derive(Debug, Default, Clone)]
pub struct CustomerCostParams<'a> {
filter: CustomerCostParamsFilter<'a>,
/// The filters applied to the customer costs query.
pub filter: CustomerCostParamsFilter<'a>,
}

impl<'a> CustomerCostParams<'a> {
Expand Down Expand Up @@ -820,6 +913,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<LedgerEntry, Error> {
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,
Expand Down Expand Up @@ -849,4 +960,109 @@ impl Client {
let res: ArrayResponse<CustomerCostBucket> = 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<ListTopUpsResponse, Error> {
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<ListTopUpsResponse, Error> {
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<TopUp, Error> {
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<TopUp, Error> {
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(())
}
}
66 changes: 62 additions & 4 deletions src/client/invoices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,8 +38,9 @@ pub struct Invoice {
/// The subscription associated with this invoice.
pub subscription: Option<InvoiceSubscription>,
/// 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<OffsetDateTime>,
/// An automatically generated number to help track and reconcile invoices.
pub invoice_number: String,
/// The link to download the PDF representation of the invoice.
Expand All @@ -66,9 +67,19 @@ pub struct Invoice {
/// values.
#[serde(default)]
pub metadata: BTreeMap<String, String>,
/// The breakdown of prices in this invoice
pub line_items: Vec<InvoiceLineItem>,
// 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 {
Expand Down Expand Up @@ -122,6 +133,21 @@ impl InvoiceStatusFilter {
};
}

/// https://docs.withorb.com/reference/mark-invoice-as-paid
/// This endpoint allows an invoice's status to be set the paid status.
/// This can only be done to invoices that are in the issued status.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct MarkInvoiceAsPaidBody<'a> {
/// A date string to specify the date of the payment.
pub payment_received_date: Date,

/// An optional external ID to associate with the payment.
pub external_id: Option<&'a str>,

/// An optional note to associate with the payment.
pub notes: Option<&'a str>,
}

/// Parameters for a subscription list operation.
#[derive(Debug, Clone)]
pub struct InvoiceListParams<'a> {
Expand Down Expand Up @@ -219,10 +245,42 @@ impl Client {
/// Gets an invoice by ID.
pub async fn get_invoice(&self, id: &str) -> Result<Invoice, Error> {
let req = self.build_request(Method::GET, INVOICES.chain_one(id));
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<Invoice, Error> {
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)
}

/// Mark an invoice as paid. For example, this can be done in response to
/// Stripe's invoice paid webhook.
pub async fn mark_invoice_as_paid<'a>(
&self,
id: &str,
body: MarkInvoiceAsPaidBody<'a>,
) -> Result<Invoice, Error> {
let req = self.build_request(Method::POST, INVOICES.chain_one(id).chain_one("mark_paid"));
let req = req.json(&body);
let res = self.send_request(req).await?;
Ok(res)
}

/// This endpoint allows an invoice's status to be set the void status.
/// This can only be done to invoices that are in the issued status.
/// If the associated invoice has used the customer balance to change the amount due, the
/// customer balance operation will be reverted. For example, if the invoice used 10 of
/// $customer balance, that amount will be added back to the customer balance upon voiding.
pub async fn void_invoice<'a>(&self, id: &str) -> Result<Invoice, Error> {
let req = self.build_request(Method::POST, INVOICES.chain_one(id).chain_one("void"));
let res = self.send_request(req).await?;
Ok(res)
}

// TODO: get upcoming invoice.
// TODO: void invoice.
}
Loading