1use chrono::{DateTime, Utc};
59use serde::{Deserialize, Serialize};
60
61use crate::clients::RestClient;
62use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
63use crate::HttpMethod;
64
65use super::common::{ChargeCurrency, ChargeStatus};
66
67#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
115pub struct RecurringApplicationCharge {
116 #[serde(skip_serializing)]
119 pub id: Option<u64>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub name: Option<String>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
128 pub price: Option<String>,
129
130 #[serde(skip_serializing)]
133 pub status: Option<ChargeStatus>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
138 pub test: Option<bool>,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub return_url: Option<String>,
143
144 #[serde(skip_serializing)]
147 pub confirmation_url: Option<String>,
148
149 #[serde(skip_serializing)]
152 pub currency: Option<ChargeCurrency>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
157 pub capped_amount: Option<String>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub terms: Option<String>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub trial_days: Option<i32>,
166
167 #[serde(skip_serializing)]
170 pub trial_ends_on: Option<DateTime<Utc>>,
171
172 #[serde(skip_serializing)]
175 pub activated_on: Option<DateTime<Utc>>,
176
177 #[serde(skip_serializing)]
180 pub billing_on: Option<DateTime<Utc>>,
181
182 #[serde(skip_serializing)]
185 pub cancelled_on: Option<DateTime<Utc>>,
186
187 #[serde(skip_serializing)]
190 pub created_at: Option<DateTime<Utc>>,
191
192 #[serde(skip_serializing)]
195 pub updated_at: Option<DateTime<Utc>>,
196}
197
198impl RecurringApplicationCharge {
199 #[must_use]
209 pub fn is_active(&self) -> bool {
210 self.status.as_ref().map_or(false, ChargeStatus::is_active)
211 }
212
213 #[must_use]
223 pub fn is_pending(&self) -> bool {
224 self.status.as_ref().map_or(false, ChargeStatus::is_pending)
225 }
226
227 #[must_use]
237 pub fn is_cancelled(&self) -> bool {
238 self.status
239 .as_ref()
240 .map_or(false, ChargeStatus::is_cancelled)
241 }
242
243 #[must_use]
256 pub fn is_test(&self) -> bool {
257 self.test.unwrap_or(false)
258 }
259
260 #[must_use]
272 pub fn is_in_trial(&self) -> bool {
273 self.trial_ends_on
274 .map_or(false, |ends_on| ends_on > Utc::now())
275 }
276
277 pub async fn customize(
300 &self,
301 client: &RestClient,
302 capped_amount: &str,
303 ) -> Result<Self, ResourceError> {
304 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
305 resource: Self::NAME,
306 operation: "customize",
307 })?;
308
309 let path = format!("recurring_application_charges/{id}/customize");
310 let body = serde_json::json!({
311 "recurring_application_charge": {
312 "capped_amount": capped_amount
313 }
314 });
315
316 let response = client.put(&path, body, None).await?;
317
318 if !response.is_ok() {
319 return Err(ResourceError::from_http_response(
320 response.code,
321 &response.body,
322 Self::NAME,
323 Some(&id.to_string()),
324 response.request_id(),
325 ));
326 }
327
328 let charge: Self = response
330 .body
331 .get("recurring_application_charge")
332 .ok_or_else(|| {
333 ResourceError::Http(crate::clients::HttpError::Response(
334 crate::clients::HttpResponseError {
335 code: response.code,
336 message: "Missing 'recurring_application_charge' in response".to_string(),
337 error_reference: response.request_id().map(ToString::to_string),
338 },
339 ))
340 })
341 .and_then(|v| {
342 serde_json::from_value(v.clone()).map_err(|e| {
343 ResourceError::Http(crate::clients::HttpError::Response(
344 crate::clients::HttpResponseError {
345 code: response.code,
346 message: format!(
347 "Failed to deserialize recurring_application_charge: {e}"
348 ),
349 error_reference: response.request_id().map(ToString::to_string),
350 },
351 ))
352 })
353 })?;
354
355 Ok(charge)
356 }
357
358 pub async fn current(client: &RestClient) -> Result<Option<Self>, ResourceError> {
388 let params = RecurringApplicationChargeListParams {
389 status: Some("active".to_string()),
390 ..Default::default()
391 };
392
393 let response = Self::all(client, Some(params)).await?;
394 Ok(response.into_inner().into_iter().next())
395 }
396}
397
398impl RestResource for RecurringApplicationCharge {
399 type Id = u64;
400 type FindParams = RecurringApplicationChargeFindParams;
401 type AllParams = RecurringApplicationChargeListParams;
402 type CountParams = ();
403
404 const NAME: &'static str = "RecurringApplicationCharge";
405 const PLURAL: &'static str = "recurring_application_charges";
406
407 const PATHS: &'static [ResourcePath] = &[
411 ResourcePath::new(
412 HttpMethod::Get,
413 ResourceOperation::Find,
414 &["id"],
415 "recurring_application_charges/{id}",
416 ),
417 ResourcePath::new(
418 HttpMethod::Get,
419 ResourceOperation::All,
420 &[],
421 "recurring_application_charges",
422 ),
423 ResourcePath::new(
424 HttpMethod::Post,
425 ResourceOperation::Create,
426 &[],
427 "recurring_application_charges",
428 ),
429 ResourcePath::new(
430 HttpMethod::Delete,
431 ResourceOperation::Delete,
432 &["id"],
433 "recurring_application_charges/{id}",
434 ),
435 ];
437
438 fn get_id(&self) -> Option<Self::Id> {
439 self.id
440 }
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
445pub struct RecurringApplicationChargeFindParams {
446 #[serde(skip_serializing_if = "Option::is_none")]
448 pub fields: Option<String>,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
453pub struct RecurringApplicationChargeListParams {
454 #[serde(skip_serializing_if = "Option::is_none")]
456 pub limit: Option<u32>,
457
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub since_id: Option<u64>,
461
462 #[serde(skip_serializing_if = "Option::is_none")]
464 pub status: Option<String>,
465
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub fields: Option<String>,
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use crate::rest::{get_path, ResourceOperation};
475
476 #[test]
477 fn test_recurring_application_charge_serialization() {
478 let charge = RecurringApplicationCharge {
479 id: Some(12345),
480 name: Some("Pro Plan".to_string()),
481 price: Some("29.99".to_string()),
482 status: Some(ChargeStatus::Active),
483 test: Some(true),
484 return_url: Some("https://myapp.com/callback".to_string()),
485 confirmation_url: Some("https://shop.myshopify.com/confirm".to_string()),
486 currency: Some(ChargeCurrency::new("USD")),
487 capped_amount: Some("100.00".to_string()),
488 terms: Some("$29.99/month plus usage".to_string()),
489 trial_days: Some(14),
490 trial_ends_on: Some(
491 DateTime::parse_from_rfc3339("2024-02-01T00:00:00Z")
492 .unwrap()
493 .with_timezone(&Utc),
494 ),
495 activated_on: Some(
496 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
497 .unwrap()
498 .with_timezone(&Utc),
499 ),
500 billing_on: Some(
501 DateTime::parse_from_rfc3339("2024-02-15T00:00:00Z")
502 .unwrap()
503 .with_timezone(&Utc),
504 ),
505 cancelled_on: None,
506 created_at: Some(
507 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
508 .unwrap()
509 .with_timezone(&Utc),
510 ),
511 updated_at: Some(
512 DateTime::parse_from_rfc3339("2024-01-15T10:35:00Z")
513 .unwrap()
514 .with_timezone(&Utc),
515 ),
516 };
517
518 let json = serde_json::to_string(&charge).unwrap();
519 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
520
521 assert_eq!(parsed["name"], "Pro Plan");
523 assert_eq!(parsed["price"], "29.99");
524 assert_eq!(parsed["test"], true);
525 assert_eq!(parsed["return_url"], "https://myapp.com/callback");
526 assert_eq!(parsed["capped_amount"], "100.00");
527 assert_eq!(parsed["terms"], "$29.99/month plus usage");
528 assert_eq!(parsed["trial_days"], 14);
529
530 assert!(parsed.get("id").is_none());
532 assert!(parsed.get("status").is_none());
533 assert!(parsed.get("confirmation_url").is_none());
534 assert!(parsed.get("currency").is_none());
535 assert!(parsed.get("trial_ends_on").is_none());
536 assert!(parsed.get("activated_on").is_none());
537 assert!(parsed.get("billing_on").is_none());
538 assert!(parsed.get("cancelled_on").is_none());
539 assert!(parsed.get("created_at").is_none());
540 assert!(parsed.get("updated_at").is_none());
541 }
542
543 #[test]
544 fn test_recurring_application_charge_deserialization() {
545 let json = r#"{
546 "id": 455696195,
547 "name": "Super Mega Plan",
548 "price": "15.00",
549 "status": "active",
550 "test": true,
551 "return_url": "https://super-duper.shopifyapps.com/",
552 "confirmation_url": "https://jsmith.myshopify.com/admin/charges/455696195/confirm_recurring_application_charge",
553 "currency": {
554 "currency": "USD"
555 },
556 "capped_amount": "100.00",
557 "terms": "$1 for 1000 emails",
558 "trial_days": 7,
559 "trial_ends_on": "2024-02-01T00:00:00Z",
560 "activated_on": "2024-01-15T10:30:00Z",
561 "billing_on": "2024-02-15T00:00:00Z",
562 "cancelled_on": null,
563 "created_at": "2024-01-15T10:30:00Z",
564 "updated_at": "2024-01-15T10:35:00Z"
565 }"#;
566
567 let charge: RecurringApplicationCharge = serde_json::from_str(json).unwrap();
568
569 assert_eq!(charge.id, Some(455696195));
570 assert_eq!(charge.name, Some("Super Mega Plan".to_string()));
571 assert_eq!(charge.price, Some("15.00".to_string()));
572 assert_eq!(charge.status, Some(ChargeStatus::Active));
573 assert_eq!(charge.test, Some(true));
574 assert!(charge.confirmation_url.is_some());
575 assert_eq!(charge.currency.as_ref().unwrap().code(), Some("USD"));
576 assert_eq!(charge.capped_amount, Some("100.00".to_string()));
577 assert_eq!(charge.terms, Some("$1 for 1000 emails".to_string()));
578 assert_eq!(charge.trial_days, Some(7));
579 assert!(charge.trial_ends_on.is_some());
580 assert!(charge.activated_on.is_some());
581 assert!(charge.billing_on.is_some());
582 assert!(charge.cancelled_on.is_none());
583 assert!(charge.created_at.is_some());
584 assert!(charge.updated_at.is_some());
585 }
586
587 #[test]
588 fn test_recurring_application_charge_convenience_methods() {
589 let active_charge = RecurringApplicationCharge {
591 status: Some(ChargeStatus::Active),
592 ..Default::default()
593 };
594 assert!(active_charge.is_active());
595 assert!(!active_charge.is_pending());
596 assert!(!active_charge.is_cancelled());
597
598 let pending_charge = RecurringApplicationCharge {
600 status: Some(ChargeStatus::Pending),
601 ..Default::default()
602 };
603 assert!(pending_charge.is_pending());
604 assert!(!pending_charge.is_active());
605
606 let cancelled_charge = RecurringApplicationCharge {
608 status: Some(ChargeStatus::Cancelled),
609 ..Default::default()
610 };
611 assert!(cancelled_charge.is_cancelled());
612 assert!(!cancelled_charge.is_active());
613
614 let test_charge = RecurringApplicationCharge {
616 test: Some(true),
617 ..Default::default()
618 };
619 assert!(test_charge.is_test());
620
621 let non_test_charge = RecurringApplicationCharge {
622 test: Some(false),
623 ..Default::default()
624 };
625 assert!(!non_test_charge.is_test());
626
627 let default_charge = RecurringApplicationCharge::default();
629 assert!(!default_charge.is_test());
630 assert!(!default_charge.is_active());
631 assert!(!default_charge.is_pending());
632 assert!(!default_charge.is_cancelled());
633 }
634
635 #[test]
636 fn test_recurring_application_charge_is_in_trial() {
637 let future_date = Utc::now() + chrono::Duration::days(7);
639 let in_trial_charge = RecurringApplicationCharge {
640 trial_ends_on: Some(future_date),
641 ..Default::default()
642 };
643 assert!(in_trial_charge.is_in_trial());
644
645 let past_date = Utc::now() - chrono::Duration::days(7);
647 let trial_ended_charge = RecurringApplicationCharge {
648 trial_ends_on: Some(past_date),
649 ..Default::default()
650 };
651 assert!(!trial_ended_charge.is_in_trial());
652
653 let no_trial_charge = RecurringApplicationCharge {
655 trial_ends_on: None,
656 ..Default::default()
657 };
658 assert!(!no_trial_charge.is_in_trial());
659 }
660
661 #[test]
662 fn test_recurring_application_charge_paths() {
663 let find_path = get_path(
665 RecurringApplicationCharge::PATHS,
666 ResourceOperation::Find,
667 &["id"],
668 );
669 assert!(find_path.is_some());
670 assert_eq!(
671 find_path.unwrap().template,
672 "recurring_application_charges/{id}"
673 );
674
675 let all_path = get_path(
677 RecurringApplicationCharge::PATHS,
678 ResourceOperation::All,
679 &[],
680 );
681 assert!(all_path.is_some());
682 assert_eq!(all_path.unwrap().template, "recurring_application_charges");
683
684 let create_path = get_path(
686 RecurringApplicationCharge::PATHS,
687 ResourceOperation::Create,
688 &[],
689 );
690 assert!(create_path.is_some());
691 assert_eq!(
692 create_path.unwrap().template,
693 "recurring_application_charges"
694 );
695 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
696
697 let delete_path = get_path(
699 RecurringApplicationCharge::PATHS,
700 ResourceOperation::Delete,
701 &["id"],
702 );
703 assert!(delete_path.is_some());
704 assert_eq!(
705 delete_path.unwrap().template,
706 "recurring_application_charges/{id}"
707 );
708 assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
709
710 let update_path = get_path(
712 RecurringApplicationCharge::PATHS,
713 ResourceOperation::Update,
714 &["id"],
715 );
716 assert!(update_path.is_none());
717
718 let count_path = get_path(
720 RecurringApplicationCharge::PATHS,
721 ResourceOperation::Count,
722 &[],
723 );
724 assert!(count_path.is_none());
725 }
726
727 #[test]
728 fn test_recurring_application_charge_list_params() {
729 let params = RecurringApplicationChargeListParams {
730 limit: Some(50),
731 since_id: Some(100),
732 status: Some("active".to_string()),
733 fields: Some("id,name,price".to_string()),
734 };
735
736 let json = serde_json::to_value(¶ms).unwrap();
737 assert_eq!(json["limit"], 50);
738 assert_eq!(json["since_id"], 100);
739 assert_eq!(json["status"], "active");
740 assert_eq!(json["fields"], "id,name,price");
741
742 let empty_params = RecurringApplicationChargeListParams::default();
744 let empty_json = serde_json::to_value(&empty_params).unwrap();
745 assert_eq!(empty_json, serde_json::json!({}));
746 }
747
748 #[test]
749 fn test_recurring_application_charge_constants() {
750 assert_eq!(
751 RecurringApplicationCharge::NAME,
752 "RecurringApplicationCharge"
753 );
754 assert_eq!(
755 RecurringApplicationCharge::PLURAL,
756 "recurring_application_charges"
757 );
758 }
759
760 #[test]
761 fn test_recurring_application_charge_get_id() {
762 let charge_with_id = RecurringApplicationCharge {
763 id: Some(12345),
764 ..Default::default()
765 };
766 assert_eq!(charge_with_id.get_id(), Some(12345));
767
768 let charge_without_id = RecurringApplicationCharge::default();
769 assert_eq!(charge_without_id.get_id(), None);
770 }
771
772 #[test]
773 fn test_customize_method_signature() {
774 fn _assert_customize_signature<F, Fut>(f: F)
776 where
777 F: Fn(&RecurringApplicationCharge, &RestClient, &str) -> Fut,
778 Fut: std::future::Future<Output = Result<RecurringApplicationCharge, ResourceError>>,
779 {
780 let _ = f;
781 }
782
783 let charge_without_id = RecurringApplicationCharge::default();
785 assert!(charge_without_id.get_id().is_none());
786 }
787
788 #[test]
789 fn test_current_method_signature() {
790 fn _assert_current_signature<F, Fut>(f: F)
792 where
793 F: Fn(&RestClient) -> Fut,
794 Fut: std::future::Future<
795 Output = Result<Option<RecurringApplicationCharge>, ResourceError>,
796 >,
797 {
798 let _ = f;
799 }
800 }
801}