paystack/models/
plans_models.rs

1//! Plans Models
2//! =============
3//! This file contains the models and options for the Plans endpoint of the Paystack API
4
5use std::fmt;
6
7use derive_builder::Builder;
8use serde::{Deserialize, Serialize};
9
10use crate::utils::string_or_number_to_u32;
11use crate::{Currency, Domain, Subscription};
12
13/// Request body to create a plan on your integration.
14/// Should be created via `PlanRequestBuilder`
15#[derive(Clone, Default, Debug, Serialize, Deserialize, Builder)]
16pub struct PlanRequest {
17    /// Name of plan
18    pub name: String,
19    /// Amount for the plan. Should be in the subunit of the supported currency
20    pub amount: String,
21    /// Interval in words, Use the `Interval` Enum for valid options.
22    pub interval: Interval,
23    /// A description of this plan
24    #[builder(setter(strip_option))]
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub description: Option<String>,
27    /// Set to false if you don't want invoices to be sent to your customers
28    #[builder(setter(strip_option), default)]
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub send_invoices: Option<bool>,
31    /// Set to false if you don't want text messages to be sent to your customers
32    // NB: docs says string, but should be bool.
33    #[builder(setter(strip_option), default)]
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub send_sms: Option<bool>,
36    /// Currency in which the amount is set.
37    /// Defaults to the Default Currency of the integration
38    #[builder(setter(strip_option), default)]
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub currency: Option<Currency>,
41    /// Number of invoices to raise during subscription to this plan.
42    /// Can be overridden by specifying an `invoice_limit` while subscribing.
43    #[builder(setter(strip_option), default)]
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub invoice_limit: Option<u8>,
46}
47
48/// Options for the different payment intervals for plans supported by the paystack API.
49#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
50#[serde(rename_all = "lowercase")]
51pub enum Interval {
52    Daily,
53    Weekly,
54    #[default]
55    Monthly,
56    Quarterly,
57    /// Every 6 months
58    Biannually,
59    Annually,
60}
61
62impl fmt::Display for Interval {
63    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64        let interval = match self {
65            Interval::Daily => "daily",
66            Interval::Weekly => "weekly",
67            Interval::Monthly => "monthly",
68            Interval::Quarterly => "quarterly",
69            Interval::Biannually => "biannually",
70            Interval::Annually => "annually",
71        };
72        write!(f, "{interval}")
73    }
74}
75
76// TODO: figure out the the other plan status
77#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
78#[serde(rename_all = "lowercase")]
79#[non_exhaustive]
80pub enum PlanStatus {
81    #[default]
82    Active,
83    Archived,
84    Deleted,
85}
86
87impl fmt::Display for PlanStatus {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        let plan_status = match self {
90            PlanStatus::Active => "Active",
91            PlanStatus::Archived => "Archived",
92            PlanStatus::Deleted => "Deleted",
93        };
94        write!(f, "{plan_status}")
95    }
96}
97
98/// Request body to update a plan on your integration.
99/// Should be created via `PlanUpdateRequestBuilder`
100#[derive(Debug, Clone, Default, Serialize, Deserialize, Builder)]
101#[builder(setter(strip_option), default)]
102pub struct PlanUpdateRequest {
103    /// Name of plan
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub name: Option<String>,
106    /// Amount for the plan. Should be in the subunit of the supported currency
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub amount: Option<String>,
109    /// Interval in words, Use the `Interval` Enum for valid options.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub interval: Option<Interval>,
112    /// A description of this plan
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub description: Option<String>,
115    /// Set to false if you don't want invoices to be sent to your customers
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub send_invoices: Option<bool>,
118    /// Set to false if you don't want text messages to be sent to your customers
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub send_sms: Option<bool>,
121    /// Currency in which the amount is set.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub currency: Option<Currency>,
124    /// Number of invoices to raise during subscription to this plan.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub invoice_limit: Option<u8>,
127    /// Set to `true` if you want the existing subscriptions to use the new changes
128    /// Set to `false` and only new subscriptions will be changed.
129    /// Defaults to true when not set.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub update_existing_subscriptions: Option<bool>,
132}
133
134/// This struct represents the data of the create plan response.
135#[derive(Clone, Debug, Serialize, Deserialize, Default)]
136pub struct PlanResponseData {
137    pub subscriptions: Option<Vec<Subscription>>,
138    pub name: String,
139    #[serde(deserialize_with = "string_or_number_to_u32")]
140    pub amount: u32,
141    pub interval: Interval,
142    pub integration: u32,
143    pub domain: Domain,
144    pub plan_code: String,
145    pub description: Option<String>,
146    pub send_invoices: Option<bool>,
147    pub send_sms: bool,
148    pub hosted_page: bool,
149    pub hosted_page_url: Option<String>,
150    pub hosted_page_summary: Option<String>,
151    pub currency: Currency,
152    pub id: u32,
153    #[serde(rename = "createdAt")]
154    pub created_at: String,
155    #[serde(rename = "updatedAt")]
156    pub updated_at: String,
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use std::error::Error;
163
164    #[test]
165    fn can_create_plan_request_with_builder() -> Result<(), Box<dyn Error>> {
166        let plan = PlanRequestBuilder::default()
167            .name("test plan".to_string())
168            .amount("100000".to_string())
169            .interval(Interval::Monthly)
170            .description("some description".to_string())
171            .build()?;
172
173        assert_eq!(plan.name, "test plan");
174        assert_eq!(plan.amount, "100000");
175        assert_eq!(plan.interval, Interval::Monthly);
176        assert_eq!(plan.description, Some("some description".to_string()));
177
178        Ok(())
179    }
180
181    #[test]
182    fn cannot_create_plan_request_without_compulsory_field() -> Result<(), Box<dyn Error>> {
183        let plan = PlanRequestBuilder::default()
184            .currency(Currency::XOF)
185            .build();
186
187        assert!(plan.is_err());
188
189        Ok(())
190    }
191}