use std::fmt;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::money::Money;
use crate::plan::PlanId;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProviderId(String);
impl ProviderId {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ProviderId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&str> for ProviderId {
fn from(id: &str) -> Self {
Self::new(id)
}
}
impl From<String> for ProviderId {
fn from(id: String) -> Self {
Self(id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SubscriptionStatus {
Incomplete,
IncompleteExpired,
Trialing,
Active,
Paused,
PastDue,
Unpaid,
Canceled,
}
impl SubscriptionStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Incomplete => "incomplete",
Self::IncompleteExpired => "incomplete_expired",
Self::Trialing => "trialing",
Self::Active => "active",
Self::Paused => "paused",
Self::PastDue => "past_due",
Self::Unpaid => "unpaid",
Self::Canceled => "canceled",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
Some(match value {
"incomplete" => Self::Incomplete,
"incomplete_expired" => Self::IncompleteExpired,
"trialing" => Self::Trialing,
"active" => Self::Active,
"paused" => Self::Paused,
"past_due" => Self::PastDue,
"unpaid" => Self::Unpaid,
"canceled" => Self::Canceled,
_ => return None,
})
}
#[must_use]
pub const fn rank(self) -> u8 {
match self {
Self::Incomplete => 0,
Self::Paused => 1,
Self::Trialing => 2,
Self::Active => 3,
Self::PastDue => 4,
Self::Unpaid => 5,
Self::IncompleteExpired => 6,
Self::Canceled => 7,
}
}
#[must_use]
pub const fn is_live(self) -> bool {
matches!(
self,
Self::Trialing | Self::Active | Self::PastDue | Self::Paused | Self::Incomplete
)
}
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Canceled | Self::IncompleteExpired)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum InvoiceStatus {
Draft,
Open,
Paid,
Uncollectible,
Void,
}
impl InvoiceStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Draft => "draft",
Self::Open => "open",
Self::Paid => "paid",
Self::Uncollectible => "uncollectible",
Self::Void => "void",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
Some(match value {
"draft" => Self::Draft,
"open" => Self::Open,
"paid" => Self::Paid,
"uncollectible" => Self::Uncollectible,
"void" => Self::Void,
_ => return None,
})
}
#[must_use]
pub const fn rank(self) -> u8 {
match self {
Self::Draft => 0,
Self::Open => 1,
Self::Uncollectible => 2,
Self::Void => 3,
Self::Paid => 4,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Customer {
pub id: String,
pub user_id: Option<String>,
pub provider: String,
pub provider_customer_id: ProviderId,
pub email: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Customer {
#[must_use]
pub fn new(
id: impl Into<String>,
provider: impl Into<String>,
provider_customer_id: impl Into<ProviderId>,
now: DateTime<Utc>,
) -> Self {
Self {
id: id.into(),
user_id: None,
provider: provider.into(),
provider_customer_id: provider_customer_id.into(),
email: None,
created_at: now,
updated_at: now,
}
}
#[must_use]
pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
#[must_use]
pub fn with_email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Subscription {
pub id: String,
pub customer_id: String,
pub provider_subscription_id: ProviderId,
pub provider_price_id: Option<ProviderId>,
pub plan_id: Option<PlanId>,
pub status: SubscriptionStatus,
pub quantity: i64,
pub current_period_end: Option<DateTime<Utc>>,
pub cancel_at_period_end: bool,
pub last_event_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Subscription {
#[must_use]
pub fn new(
id: impl Into<String>,
customer_id: impl Into<String>,
provider_subscription_id: impl Into<ProviderId>,
status: SubscriptionStatus,
last_event_at: DateTime<Utc>,
now: DateTime<Utc>,
) -> Self {
Self {
id: id.into(),
customer_id: customer_id.into(),
provider_subscription_id: provider_subscription_id.into(),
provider_price_id: None,
plan_id: None,
status,
quantity: 1,
current_period_end: None,
cancel_at_period_end: false,
last_event_at,
created_at: now,
updated_at: now,
}
}
#[must_use]
pub fn with_price(mut self, price_id: impl Into<ProviderId>) -> Self {
self.provider_price_id = Some(price_id.into());
self
}
#[must_use]
pub fn with_plan(mut self, plan_id: impl Into<PlanId>) -> Self {
self.plan_id = Some(plan_id.into());
self
}
#[must_use]
pub const fn with_quantity(mut self, quantity: i64) -> Self {
self.quantity = quantity;
self
}
#[must_use]
pub const fn with_period_end(mut self, end: DateTime<Utc>) -> Self {
self.current_period_end = Some(end);
self
}
#[must_use]
pub const fn with_cancel_at_period_end(mut self, cancel: bool) -> Self {
self.cancel_at_period_end = cancel;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Invoice {
pub id: String,
pub customer_id: String,
pub subscription_id: Option<String>,
#[serde(default)]
pub provider_subscription_id: Option<ProviderId>,
pub provider_invoice_id: ProviderId,
pub status: InvoiceStatus,
pub amount_due: Money,
pub amount_paid: Money,
pub attempt_count: i64,
pub next_payment_attempt: Option<DateTime<Utc>>,
pub last_event_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Invoice {
#[must_use]
#[allow(
clippy::too_many_arguments,
reason = "every argument is a required column; the optional ones use `with_*`"
)]
pub fn new(
id: impl Into<String>,
customer_id: impl Into<String>,
provider_invoice_id: impl Into<ProviderId>,
status: InvoiceStatus,
amount_due: Money,
amount_paid: Money,
last_event_at: DateTime<Utc>,
now: DateTime<Utc>,
) -> Self {
Self {
id: id.into(),
customer_id: customer_id.into(),
subscription_id: None,
provider_subscription_id: None,
provider_invoice_id: provider_invoice_id.into(),
status,
amount_due,
amount_paid,
attempt_count: 0,
next_payment_attempt: None,
last_event_at,
created_at: now,
updated_at: now,
}
}
#[must_use]
pub fn with_subscription(mut self, subscription_id: impl Into<String>) -> Self {
self.subscription_id = Some(subscription_id.into());
self
}
#[must_use]
pub fn with_provider_subscription(mut self, id: impl Into<ProviderId>) -> Self {
self.provider_subscription_id = Some(id.into());
self
}
#[must_use]
pub const fn with_attempt_count(mut self, count: i64) -> Self {
self.attempt_count = count;
self
}
#[must_use]
pub const fn with_next_payment_attempt(mut self, at: DateTime<Utc>) -> Self {
self.next_payment_attempt = Some(at);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DunningState {
Pending,
Running,
Recovered,
Exhausted,
Canceled,
}
impl DunningState {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Running => "running",
Self::Recovered => "recovered",
Self::Exhausted => "exhausted",
Self::Canceled => "canceled",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
Some(match value {
"pending" => Self::Pending,
"running" => Self::Running,
"recovered" => Self::Recovered,
"exhausted" => Self::Exhausted,
"canceled" => Self::Canceled,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DunningAttempt {
pub invoice_id: String,
pub customer_id: String,
pub subscription_id: Option<String>,
pub attempt: i64,
pub next_attempt_at: DateTime<Utc>,
pub state: DunningState,
pub updated_at: DateTime<Utc>,
}
impl DunningAttempt {
#[must_use]
pub fn new(
invoice_id: impl Into<String>,
customer_id: impl Into<String>,
attempt: i64,
next_attempt_at: DateTime<Utc>,
state: DunningState,
updated_at: DateTime<Utc>,
) -> Self {
Self {
invoice_id: invoice_id.into(),
customer_id: customer_id.into(),
subscription_id: None,
attempt,
next_attempt_at,
state,
updated_at,
}
}
#[must_use]
pub fn with_subscription(mut self, subscription_id: impl Into<String>) -> Self {
self.subscription_id = Some(subscription_id.into());
self
}
}