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!("Failed to deserialize recurring_application_charge: {e}"),
347 error_reference: response.request_id().map(ToString::to_string),
348 },
349 ))
350 })
351 })?;
352
353 Ok(charge)
354 }
355
356 pub async fn current(client: &RestClient) -> Result<Option<Self>, ResourceError> {
386 let params = RecurringApplicationChargeListParams {
387 status: Some("active".to_string()),
388 ..Default::default()
389 };
390
391 let response = Self::all(client, Some(params)).await?;
392 Ok(response.into_inner().into_iter().next())
393 }
394}
395
396impl RestResource for RecurringApplicationCharge {
397 type Id = u64;
398 type FindParams = RecurringApplicationChargeFindParams;
399 type AllParams = RecurringApplicationChargeListParams;
400 type CountParams = ();
401
402 const NAME: &'static str = "RecurringApplicationCharge";
403 const PLURAL: &'static str = "recurring_application_charges";
404
405 const PATHS: &'static [ResourcePath] = &[
409 ResourcePath::new(
410 HttpMethod::Get,
411 ResourceOperation::Find,
412 &["id"],
413 "recurring_application_charges/{id}",
414 ),
415 ResourcePath::new(
416 HttpMethod::Get,
417 ResourceOperation::All,
418 &[],
419 "recurring_application_charges",
420 ),
421 ResourcePath::new(
422 HttpMethod::Post,
423 ResourceOperation::Create,
424 &[],
425 "recurring_application_charges",
426 ),
427 ResourcePath::new(
428 HttpMethod::Delete,
429 ResourceOperation::Delete,
430 &["id"],
431 "recurring_application_charges/{id}",
432 ),
433 ];
435
436 fn get_id(&self) -> Option<Self::Id> {
437 self.id
438 }
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
443pub struct RecurringApplicationChargeFindParams {
444 #[serde(skip_serializing_if = "Option::is_none")]
446 pub fields: Option<String>,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
451pub struct RecurringApplicationChargeListParams {
452 #[serde(skip_serializing_if = "Option::is_none")]
454 pub limit: Option<u32>,
455
456 #[serde(skip_serializing_if = "Option::is_none")]
458 pub since_id: Option<u64>,
459
460 #[serde(skip_serializing_if = "Option::is_none")]
462 pub status: Option<String>,
463
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub fields: Option<String>,
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use crate::rest::{get_path, ResourceOperation};
473
474 #[test]
475 fn test_recurring_application_charge_serialization() {
476 let charge = RecurringApplicationCharge {
477 id: Some(12345),
478 name: Some("Pro Plan".to_string()),
479 price: Some("29.99".to_string()),
480 status: Some(ChargeStatus::Active),
481 test: Some(true),
482 return_url: Some("https://myapp.com/callback".to_string()),
483 confirmation_url: Some("https://shop.myshopify.com/confirm".to_string()),
484 currency: Some(ChargeCurrency::new("USD")),
485 capped_amount: Some("100.00".to_string()),
486 terms: Some("$29.99/month plus usage".to_string()),
487 trial_days: Some(14),
488 trial_ends_on: Some(
489 DateTime::parse_from_rfc3339("2024-02-01T00:00:00Z")
490 .unwrap()
491 .with_timezone(&Utc),
492 ),
493 activated_on: Some(
494 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
495 .unwrap()
496 .with_timezone(&Utc),
497 ),
498 billing_on: Some(
499 DateTime::parse_from_rfc3339("2024-02-15T00:00:00Z")
500 .unwrap()
501 .with_timezone(&Utc),
502 ),
503 cancelled_on: None,
504 created_at: Some(
505 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
506 .unwrap()
507 .with_timezone(&Utc),
508 ),
509 updated_at: Some(
510 DateTime::parse_from_rfc3339("2024-01-15T10:35:00Z")
511 .unwrap()
512 .with_timezone(&Utc),
513 ),
514 };
515
516 let json = serde_json::to_string(&charge).unwrap();
517 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
518
519 assert_eq!(parsed["name"], "Pro Plan");
521 assert_eq!(parsed["price"], "29.99");
522 assert_eq!(parsed["test"], true);
523 assert_eq!(parsed["return_url"], "https://myapp.com/callback");
524 assert_eq!(parsed["capped_amount"], "100.00");
525 assert_eq!(parsed["terms"], "$29.99/month plus usage");
526 assert_eq!(parsed["trial_days"], 14);
527
528 assert!(parsed.get("id").is_none());
530 assert!(parsed.get("status").is_none());
531 assert!(parsed.get("confirmation_url").is_none());
532 assert!(parsed.get("currency").is_none());
533 assert!(parsed.get("trial_ends_on").is_none());
534 assert!(parsed.get("activated_on").is_none());
535 assert!(parsed.get("billing_on").is_none());
536 assert!(parsed.get("cancelled_on").is_none());
537 assert!(parsed.get("created_at").is_none());
538 assert!(parsed.get("updated_at").is_none());
539 }
540
541 #[test]
542 fn test_recurring_application_charge_deserialization() {
543 let json = r#"{
544 "id": 455696195,
545 "name": "Super Mega Plan",
546 "price": "15.00",
547 "status": "active",
548 "test": true,
549 "return_url": "https://super-duper.shopifyapps.com/",
550 "confirmation_url": "https://jsmith.myshopify.com/admin/charges/455696195/confirm_recurring_application_charge",
551 "currency": {
552 "currency": "USD"
553 },
554 "capped_amount": "100.00",
555 "terms": "$1 for 1000 emails",
556 "trial_days": 7,
557 "trial_ends_on": "2024-02-01T00:00:00Z",
558 "activated_on": "2024-01-15T10:30:00Z",
559 "billing_on": "2024-02-15T00:00:00Z",
560 "cancelled_on": null,
561 "created_at": "2024-01-15T10:30:00Z",
562 "updated_at": "2024-01-15T10:35:00Z"
563 }"#;
564
565 let charge: RecurringApplicationCharge = serde_json::from_str(json).unwrap();
566
567 assert_eq!(charge.id, Some(455696195));
568 assert_eq!(charge.name, Some("Super Mega Plan".to_string()));
569 assert_eq!(charge.price, Some("15.00".to_string()));
570 assert_eq!(charge.status, Some(ChargeStatus::Active));
571 assert_eq!(charge.test, Some(true));
572 assert!(charge.confirmation_url.is_some());
573 assert_eq!(charge.currency.as_ref().unwrap().code(), Some("USD"));
574 assert_eq!(charge.capped_amount, Some("100.00".to_string()));
575 assert_eq!(charge.terms, Some("$1 for 1000 emails".to_string()));
576 assert_eq!(charge.trial_days, Some(7));
577 assert!(charge.trial_ends_on.is_some());
578 assert!(charge.activated_on.is_some());
579 assert!(charge.billing_on.is_some());
580 assert!(charge.cancelled_on.is_none());
581 assert!(charge.created_at.is_some());
582 assert!(charge.updated_at.is_some());
583 }
584
585 #[test]
586 fn test_recurring_application_charge_convenience_methods() {
587 let active_charge = RecurringApplicationCharge {
589 status: Some(ChargeStatus::Active),
590 ..Default::default()
591 };
592 assert!(active_charge.is_active());
593 assert!(!active_charge.is_pending());
594 assert!(!active_charge.is_cancelled());
595
596 let pending_charge = RecurringApplicationCharge {
598 status: Some(ChargeStatus::Pending),
599 ..Default::default()
600 };
601 assert!(pending_charge.is_pending());
602 assert!(!pending_charge.is_active());
603
604 let cancelled_charge = RecurringApplicationCharge {
606 status: Some(ChargeStatus::Cancelled),
607 ..Default::default()
608 };
609 assert!(cancelled_charge.is_cancelled());
610 assert!(!cancelled_charge.is_active());
611
612 let test_charge = RecurringApplicationCharge {
614 test: Some(true),
615 ..Default::default()
616 };
617 assert!(test_charge.is_test());
618
619 let non_test_charge = RecurringApplicationCharge {
620 test: Some(false),
621 ..Default::default()
622 };
623 assert!(!non_test_charge.is_test());
624
625 let default_charge = RecurringApplicationCharge::default();
627 assert!(!default_charge.is_test());
628 assert!(!default_charge.is_active());
629 assert!(!default_charge.is_pending());
630 assert!(!default_charge.is_cancelled());
631 }
632
633 #[test]
634 fn test_recurring_application_charge_is_in_trial() {
635 let future_date = Utc::now() + chrono::Duration::days(7);
637 let in_trial_charge = RecurringApplicationCharge {
638 trial_ends_on: Some(future_date),
639 ..Default::default()
640 };
641 assert!(in_trial_charge.is_in_trial());
642
643 let past_date = Utc::now() - chrono::Duration::days(7);
645 let trial_ended_charge = RecurringApplicationCharge {
646 trial_ends_on: Some(past_date),
647 ..Default::default()
648 };
649 assert!(!trial_ended_charge.is_in_trial());
650
651 let no_trial_charge = RecurringApplicationCharge {
653 trial_ends_on: None,
654 ..Default::default()
655 };
656 assert!(!no_trial_charge.is_in_trial());
657 }
658
659 #[test]
660 fn test_recurring_application_charge_paths() {
661 let find_path = get_path(
663 RecurringApplicationCharge::PATHS,
664 ResourceOperation::Find,
665 &["id"],
666 );
667 assert!(find_path.is_some());
668 assert_eq!(
669 find_path.unwrap().template,
670 "recurring_application_charges/{id}"
671 );
672
673 let all_path = get_path(RecurringApplicationCharge::PATHS, ResourceOperation::All, &[]);
675 assert!(all_path.is_some());
676 assert_eq!(all_path.unwrap().template, "recurring_application_charges");
677
678 let create_path = get_path(
680 RecurringApplicationCharge::PATHS,
681 ResourceOperation::Create,
682 &[],
683 );
684 assert!(create_path.is_some());
685 assert_eq!(create_path.unwrap().template, "recurring_application_charges");
686 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
687
688 let delete_path = get_path(
690 RecurringApplicationCharge::PATHS,
691 ResourceOperation::Delete,
692 &["id"],
693 );
694 assert!(delete_path.is_some());
695 assert_eq!(
696 delete_path.unwrap().template,
697 "recurring_application_charges/{id}"
698 );
699 assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
700
701 let update_path = get_path(
703 RecurringApplicationCharge::PATHS,
704 ResourceOperation::Update,
705 &["id"],
706 );
707 assert!(update_path.is_none());
708
709 let count_path = get_path(
711 RecurringApplicationCharge::PATHS,
712 ResourceOperation::Count,
713 &[],
714 );
715 assert!(count_path.is_none());
716 }
717
718 #[test]
719 fn test_recurring_application_charge_list_params() {
720 let params = RecurringApplicationChargeListParams {
721 limit: Some(50),
722 since_id: Some(100),
723 status: Some("active".to_string()),
724 fields: Some("id,name,price".to_string()),
725 };
726
727 let json = serde_json::to_value(¶ms).unwrap();
728 assert_eq!(json["limit"], 50);
729 assert_eq!(json["since_id"], 100);
730 assert_eq!(json["status"], "active");
731 assert_eq!(json["fields"], "id,name,price");
732
733 let empty_params = RecurringApplicationChargeListParams::default();
735 let empty_json = serde_json::to_value(&empty_params).unwrap();
736 assert_eq!(empty_json, serde_json::json!({}));
737 }
738
739 #[test]
740 fn test_recurring_application_charge_constants() {
741 assert_eq!(RecurringApplicationCharge::NAME, "RecurringApplicationCharge");
742 assert_eq!(
743 RecurringApplicationCharge::PLURAL,
744 "recurring_application_charges"
745 );
746 }
747
748 #[test]
749 fn test_recurring_application_charge_get_id() {
750 let charge_with_id = RecurringApplicationCharge {
751 id: Some(12345),
752 ..Default::default()
753 };
754 assert_eq!(charge_with_id.get_id(), Some(12345));
755
756 let charge_without_id = RecurringApplicationCharge::default();
757 assert_eq!(charge_without_id.get_id(), None);
758 }
759
760 #[test]
761 fn test_customize_method_signature() {
762 fn _assert_customize_signature<F, Fut>(f: F)
764 where
765 F: Fn(&RecurringApplicationCharge, &RestClient, &str) -> Fut,
766 Fut: std::future::Future<Output = Result<RecurringApplicationCharge, ResourceError>>,
767 {
768 let _ = f;
769 }
770
771 let charge_without_id = RecurringApplicationCharge::default();
773 assert!(charge_without_id.get_id().is_none());
774 }
775
776 #[test]
777 fn test_current_method_signature() {
778 fn _assert_current_signature<F, Fut>(f: F)
780 where
781 F: Fn(&RestClient) -> Fut,
782 Fut: std::future::Future<Output = Result<Option<RecurringApplicationCharge>, ResourceError>>,
783 {
784 let _ = f;
785 }
786 }
787}