use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::clients::RestClient;
use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
use crate::HttpMethod;
use super::common::{ChargeCurrency, ChargeStatus};
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct RecurringApplicationCharge {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<String>,
#[serde(skip_serializing)]
pub status: Option<ChargeStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_url: Option<String>,
#[serde(skip_serializing)]
pub confirmation_url: Option<String>,
#[serde(skip_serializing)]
pub currency: Option<ChargeCurrency>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capped_amount: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terms: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trial_days: Option<i32>,
#[serde(skip_serializing)]
pub trial_ends_on: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub activated_on: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub billing_on: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub cancelled_on: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub created_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub updated_at: Option<DateTime<Utc>>,
}
impl RecurringApplicationCharge {
#[must_use]
pub fn is_active(&self) -> bool {
self.status.as_ref().map_or(false, ChargeStatus::is_active)
}
#[must_use]
pub fn is_pending(&self) -> bool {
self.status.as_ref().map_or(false, ChargeStatus::is_pending)
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.status
.as_ref()
.map_or(false, ChargeStatus::is_cancelled)
}
#[must_use]
pub fn is_test(&self) -> bool {
self.test.unwrap_or(false)
}
#[must_use]
pub fn is_in_trial(&self) -> bool {
self.trial_ends_on
.map_or(false, |ends_on| ends_on > Utc::now())
}
pub async fn customize(
&self,
client: &RestClient,
capped_amount: &str,
) -> Result<Self, ResourceError> {
let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
resource: Self::NAME,
operation: "customize",
})?;
let path = format!("recurring_application_charges/{id}/customize");
let body = serde_json::json!({
"recurring_application_charge": {
"capped_amount": capped_amount
}
});
let response = client.put(&path, body, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
Some(&id.to_string()),
response.request_id(),
));
}
let charge: Self = response
.body
.get("recurring_application_charge")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'recurring_application_charge' in response".to_string(),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
.and_then(|v| {
serde_json::from_value(v.clone()).map_err(|e| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: format!("Failed to deserialize recurring_application_charge: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(charge)
}
pub async fn current(client: &RestClient) -> Result<Option<Self>, ResourceError> {
let params = RecurringApplicationChargeListParams {
status: Some("active".to_string()),
..Default::default()
};
let response = Self::all(client, Some(params)).await?;
Ok(response.into_inner().into_iter().next())
}
}
impl RestResource for RecurringApplicationCharge {
type Id = u64;
type FindParams = RecurringApplicationChargeFindParams;
type AllParams = RecurringApplicationChargeListParams;
type CountParams = ();
const NAME: &'static str = "RecurringApplicationCharge";
const PLURAL: &'static str = "recurring_application_charges";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"recurring_application_charges/{id}",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::All,
&[],
"recurring_application_charges",
),
ResourcePath::new(
HttpMethod::Post,
ResourceOperation::Create,
&[],
"recurring_application_charges",
),
ResourcePath::new(
HttpMethod::Delete,
ResourceOperation::Delete,
&["id"],
"recurring_application_charges/{id}",
),
];
fn get_id(&self) -> Option<Self::Id> {
self.id
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct RecurringApplicationChargeFindParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct RecurringApplicationChargeListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub since_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_recurring_application_charge_serialization() {
let charge = RecurringApplicationCharge {
id: Some(12345),
name: Some("Pro Plan".to_string()),
price: Some("29.99".to_string()),
status: Some(ChargeStatus::Active),
test: Some(true),
return_url: Some("https://myapp.com/callback".to_string()),
confirmation_url: Some("https://shop.myshopify.com/confirm".to_string()),
currency: Some(ChargeCurrency::new("USD")),
capped_amount: Some("100.00".to_string()),
terms: Some("$29.99/month plus usage".to_string()),
trial_days: Some(14),
trial_ends_on: Some(
DateTime::parse_from_rfc3339("2024-02-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
),
activated_on: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.unwrap()
.with_timezone(&Utc),
),
billing_on: Some(
DateTime::parse_from_rfc3339("2024-02-15T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
),
cancelled_on: None,
created_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.unwrap()
.with_timezone(&Utc),
),
updated_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:35:00Z")
.unwrap()
.with_timezone(&Utc),
),
};
let json = serde_json::to_string(&charge).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["name"], "Pro Plan");
assert_eq!(parsed["price"], "29.99");
assert_eq!(parsed["test"], true);
assert_eq!(parsed["return_url"], "https://myapp.com/callback");
assert_eq!(parsed["capped_amount"], "100.00");
assert_eq!(parsed["terms"], "$29.99/month plus usage");
assert_eq!(parsed["trial_days"], 14);
assert!(parsed.get("id").is_none());
assert!(parsed.get("status").is_none());
assert!(parsed.get("confirmation_url").is_none());
assert!(parsed.get("currency").is_none());
assert!(parsed.get("trial_ends_on").is_none());
assert!(parsed.get("activated_on").is_none());
assert!(parsed.get("billing_on").is_none());
assert!(parsed.get("cancelled_on").is_none());
assert!(parsed.get("created_at").is_none());
assert!(parsed.get("updated_at").is_none());
}
#[test]
fn test_recurring_application_charge_deserialization() {
let json = r#"{
"id": 455696195,
"name": "Super Mega Plan",
"price": "15.00",
"status": "active",
"test": true,
"return_url": "https://super-duper.shopifyapps.com/",
"confirmation_url": "https://jsmith.myshopify.com/admin/charges/455696195/confirm_recurring_application_charge",
"currency": {
"currency": "USD"
},
"capped_amount": "100.00",
"terms": "$1 for 1000 emails",
"trial_days": 7,
"trial_ends_on": "2024-02-01T00:00:00Z",
"activated_on": "2024-01-15T10:30:00Z",
"billing_on": "2024-02-15T00:00:00Z",
"cancelled_on": null,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:35:00Z"
}"#;
let charge: RecurringApplicationCharge = serde_json::from_str(json).unwrap();
assert_eq!(charge.id, Some(455696195));
assert_eq!(charge.name, Some("Super Mega Plan".to_string()));
assert_eq!(charge.price, Some("15.00".to_string()));
assert_eq!(charge.status, Some(ChargeStatus::Active));
assert_eq!(charge.test, Some(true));
assert!(charge.confirmation_url.is_some());
assert_eq!(charge.currency.as_ref().unwrap().code(), Some("USD"));
assert_eq!(charge.capped_amount, Some("100.00".to_string()));
assert_eq!(charge.terms, Some("$1 for 1000 emails".to_string()));
assert_eq!(charge.trial_days, Some(7));
assert!(charge.trial_ends_on.is_some());
assert!(charge.activated_on.is_some());
assert!(charge.billing_on.is_some());
assert!(charge.cancelled_on.is_none());
assert!(charge.created_at.is_some());
assert!(charge.updated_at.is_some());
}
#[test]
fn test_recurring_application_charge_convenience_methods() {
let active_charge = RecurringApplicationCharge {
status: Some(ChargeStatus::Active),
..Default::default()
};
assert!(active_charge.is_active());
assert!(!active_charge.is_pending());
assert!(!active_charge.is_cancelled());
let pending_charge = RecurringApplicationCharge {
status: Some(ChargeStatus::Pending),
..Default::default()
};
assert!(pending_charge.is_pending());
assert!(!pending_charge.is_active());
let cancelled_charge = RecurringApplicationCharge {
status: Some(ChargeStatus::Cancelled),
..Default::default()
};
assert!(cancelled_charge.is_cancelled());
assert!(!cancelled_charge.is_active());
let test_charge = RecurringApplicationCharge {
test: Some(true),
..Default::default()
};
assert!(test_charge.is_test());
let non_test_charge = RecurringApplicationCharge {
test: Some(false),
..Default::default()
};
assert!(!non_test_charge.is_test());
let default_charge = RecurringApplicationCharge::default();
assert!(!default_charge.is_test());
assert!(!default_charge.is_active());
assert!(!default_charge.is_pending());
assert!(!default_charge.is_cancelled());
}
#[test]
fn test_recurring_application_charge_is_in_trial() {
let future_date = Utc::now() + chrono::Duration::days(7);
let in_trial_charge = RecurringApplicationCharge {
trial_ends_on: Some(future_date),
..Default::default()
};
assert!(in_trial_charge.is_in_trial());
let past_date = Utc::now() - chrono::Duration::days(7);
let trial_ended_charge = RecurringApplicationCharge {
trial_ends_on: Some(past_date),
..Default::default()
};
assert!(!trial_ended_charge.is_in_trial());
let no_trial_charge = RecurringApplicationCharge {
trial_ends_on: None,
..Default::default()
};
assert!(!no_trial_charge.is_in_trial());
}
#[test]
fn test_recurring_application_charge_paths() {
let find_path = get_path(
RecurringApplicationCharge::PATHS,
ResourceOperation::Find,
&["id"],
);
assert!(find_path.is_some());
assert_eq!(
find_path.unwrap().template,
"recurring_application_charges/{id}"
);
let all_path = get_path(RecurringApplicationCharge::PATHS, ResourceOperation::All, &[]);
assert!(all_path.is_some());
assert_eq!(all_path.unwrap().template, "recurring_application_charges");
let create_path = get_path(
RecurringApplicationCharge::PATHS,
ResourceOperation::Create,
&[],
);
assert!(create_path.is_some());
assert_eq!(create_path.unwrap().template, "recurring_application_charges");
assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
let delete_path = get_path(
RecurringApplicationCharge::PATHS,
ResourceOperation::Delete,
&["id"],
);
assert!(delete_path.is_some());
assert_eq!(
delete_path.unwrap().template,
"recurring_application_charges/{id}"
);
assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
let update_path = get_path(
RecurringApplicationCharge::PATHS,
ResourceOperation::Update,
&["id"],
);
assert!(update_path.is_none());
let count_path = get_path(
RecurringApplicationCharge::PATHS,
ResourceOperation::Count,
&[],
);
assert!(count_path.is_none());
}
#[test]
fn test_recurring_application_charge_list_params() {
let params = RecurringApplicationChargeListParams {
limit: Some(50),
since_id: Some(100),
status: Some("active".to_string()),
fields: Some("id,name,price".to_string()),
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["limit"], 50);
assert_eq!(json["since_id"], 100);
assert_eq!(json["status"], "active");
assert_eq!(json["fields"], "id,name,price");
let empty_params = RecurringApplicationChargeListParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_recurring_application_charge_constants() {
assert_eq!(RecurringApplicationCharge::NAME, "RecurringApplicationCharge");
assert_eq!(
RecurringApplicationCharge::PLURAL,
"recurring_application_charges"
);
}
#[test]
fn test_recurring_application_charge_get_id() {
let charge_with_id = RecurringApplicationCharge {
id: Some(12345),
..Default::default()
};
assert_eq!(charge_with_id.get_id(), Some(12345));
let charge_without_id = RecurringApplicationCharge::default();
assert_eq!(charge_without_id.get_id(), None);
}
#[test]
fn test_customize_method_signature() {
fn _assert_customize_signature<F, Fut>(f: F)
where
F: Fn(&RecurringApplicationCharge, &RestClient, &str) -> Fut,
Fut: std::future::Future<Output = Result<RecurringApplicationCharge, ResourceError>>,
{
let _ = f;
}
let charge_without_id = RecurringApplicationCharge::default();
assert!(charge_without_id.get_id().is_none());
}
#[test]
fn test_current_method_signature() {
fn _assert_current_signature<F, Fut>(f: F)
where
F: Fn(&RestClient) -> Fut,
Fut: std::future::Future<Output = Result<Option<RecurringApplicationCharge>, ResourceError>>,
{
let _ = f;
}
}
}