Skip to main content

shopify_sdk/rest/resources/v2025_10/
recurring_application_charge.rs

1//! RecurringApplicationCharge resource implementation.
2//!
3//! This module provides the [`RecurringApplicationCharge`] resource for managing
4//! subscription-based charges in Shopify apps. Recurring charges allow apps to
5//! bill merchants on a regular basis (typically monthly).
6//!
7//! # API Endpoints
8//!
9//! - `GET /recurring_application_charges.json` - List all recurring charges
10//! - `POST /recurring_application_charges.json` - Create a new recurring charge
11//! - `GET /recurring_application_charges/{id}.json` - Retrieve a single charge
12//! - `DELETE /recurring_application_charges/{id}.json` - Cancel a recurring charge
13//! - `PUT /recurring_application_charges/{id}/customize.json` - Update capped amount
14//!
15//! Note: Recurring charges cannot be updated after creation, except for
16//! the capped_amount via the customize endpoint.
17//!
18//! # Example
19//!
20//! ```rust,ignore
21//! use shopify_sdk::rest::{RestResource, ResourceResponse};
22//! use shopify_sdk::rest::resources::v2025_10::{
23//!     RecurringApplicationCharge, RecurringApplicationChargeListParams
24//! };
25//!
26//! // Create a new recurring charge with trial period
27//! let charge = RecurringApplicationCharge {
28//!     name: Some("Pro Plan".to_string()),
29//!     price: Some("29.99".to_string()),
30//!     return_url: Some("https://myapp.com/charge-callback".to_string()),
31//!     trial_days: Some(14),
32//!     capped_amount: Some("100.00".to_string()),
33//!     terms: Some("$29.99/month plus usage".to_string()),
34//!     test: Some(true), // Test charges don't actually bill
35//!     ..Default::default()
36//! };
37//! let saved = charge.save(&client).await?;
38//!
39//! // Redirect merchant to the confirmation_url for approval
40//! if let Some(url) = saved.confirmation_url.as_ref() {
41//!     println!("Redirect merchant to: {}", url);
42//! }
43//!
44//! // Check charge status
45//! if saved.is_active() {
46//!     println!("Subscription is active!");
47//! } else if saved.is_in_trial() {
48//!     println!("Subscription is in trial period");
49//! }
50//!
51//! // Update capped amount for usage-based billing
52//! let updated = saved.customize(&client, "200.00").await?;
53//!
54//! // Get the current active charge
55//! let current = RecurringApplicationCharge::current(&client).await?;
56//! ```
57
58use 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/// A recurring application charge (subscription).
68///
69/// Recurring application charges allow apps to bill merchants on a recurring
70/// basis. After creating a charge, redirect the merchant to the `confirmation_url`
71/// for them to approve the subscription.
72///
73/// # Charge Lifecycle
74///
75/// 1. App creates a charge via POST
76/// 2. App redirects merchant to `confirmation_url`
77/// 3. Merchant approves or declines the charge
78/// 4. Merchant is redirected back to the app's `return_url`
79/// 5. If approved, the subscription begins (possibly with a trial)
80///
81/// # Trial Periods
82///
83/// Set `trial_days` to give merchants a free trial before billing begins.
84/// Use `is_in_trial()` to check if the subscription is currently in trial.
85///
86/// # Usage-Based Billing
87///
88/// For usage-based pricing, set a `capped_amount` which limits the total
89/// charges per billing period. Use the `customize()` method to update the cap.
90/// Create `UsageCharge` records to add usage fees up to the cap.
91///
92/// # Fields
93///
94/// ## Read-Only Fields
95/// - `id` - The unique identifier of the charge
96/// - `confirmation_url` - The URL to redirect merchant for approval
97/// - `status` - The current status of the charge
98/// - `currency` - The currency object with the currency code
99/// - `activated_on` - When the charge was activated
100/// - `billing_on` - The next billing date
101/// - `cancelled_on` - When the charge was cancelled (if applicable)
102/// - `trial_ends_on` - When the trial period ends
103/// - `created_at` - When the charge was created
104/// - `updated_at` - When the charge was last updated
105///
106/// ## Writable Fields
107/// - `name` - The name of the charge (displayed to merchant)
108/// - `price` - The recurring price
109/// - `return_url` - The URL to redirect after merchant action
110/// - `test` - Whether this is a test charge
111/// - `capped_amount` - Maximum usage charge per billing period
112/// - `terms` - Terms displayed to merchant on confirmation page
113/// - `trial_days` - Number of trial days before billing begins
114#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
115pub struct RecurringApplicationCharge {
116    /// The unique identifier of the recurring charge.
117    /// Read-only field.
118    #[serde(skip_serializing)]
119    pub id: Option<u64>,
120
121    /// The name of the recurring charge.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub name: Option<String>,
124
125    /// The recurring price of the charge.
126    /// Must be a string representing the monetary amount (e.g., "29.99").
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub price: Option<String>,
129
130    /// The current status of the charge.
131    /// Read-only field.
132    #[serde(skip_serializing)]
133    pub status: Option<ChargeStatus>,
134
135    /// Whether this is a test charge.
136    /// Test charges don't actually bill the merchant.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub test: Option<bool>,
139
140    /// The URL to redirect the merchant after they approve/decline the charge.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub return_url: Option<String>,
143
144    /// The URL where the merchant can approve/decline the charge.
145    /// Read-only field, populated after creating the charge.
146    #[serde(skip_serializing)]
147    pub confirmation_url: Option<String>,
148
149    /// The currency information for the charge.
150    /// Read-only field containing the currency code.
151    #[serde(skip_serializing)]
152    pub currency: Option<ChargeCurrency>,
153
154    /// The maximum usage charge amount per billing period.
155    /// Used for usage-based billing with capped amounts.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub capped_amount: Option<String>,
158
159    /// The terms displayed to the merchant on the confirmation page.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub terms: Option<String>,
162
163    /// The number of trial days before billing begins.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub trial_days: Option<i32>,
166
167    /// When the trial period ends.
168    /// Read-only field.
169    #[serde(skip_serializing)]
170    pub trial_ends_on: Option<DateTime<Utc>>,
171
172    /// When the charge was activated.
173    /// Read-only field.
174    #[serde(skip_serializing)]
175    pub activated_on: Option<DateTime<Utc>>,
176
177    /// The next billing date.
178    /// Read-only field.
179    #[serde(skip_serializing)]
180    pub billing_on: Option<DateTime<Utc>>,
181
182    /// When the charge was cancelled.
183    /// Read-only field.
184    #[serde(skip_serializing)]
185    pub cancelled_on: Option<DateTime<Utc>>,
186
187    /// When the charge was created.
188    /// Read-only field.
189    #[serde(skip_serializing)]
190    pub created_at: Option<DateTime<Utc>>,
191
192    /// When the charge was last updated.
193    /// Read-only field.
194    #[serde(skip_serializing)]
195    pub updated_at: Option<DateTime<Utc>>,
196}
197
198impl RecurringApplicationCharge {
199    /// Returns `true` if the charge is active (approved and billing).
200    ///
201    /// # Example
202    ///
203    /// ```rust,ignore
204    /// if charge.is_active() {
205    ///     println!("Subscription is active!");
206    /// }
207    /// ```
208    #[must_use]
209    pub fn is_active(&self) -> bool {
210        self.status.as_ref().map_or(false, ChargeStatus::is_active)
211    }
212
213    /// Returns `true` if the charge is pending merchant approval.
214    ///
215    /// # Example
216    ///
217    /// ```rust,ignore
218    /// if charge.is_pending() {
219    ///     println!("Waiting for merchant approval");
220    /// }
221    /// ```
222    #[must_use]
223    pub fn is_pending(&self) -> bool {
224        self.status.as_ref().map_or(false, ChargeStatus::is_pending)
225    }
226
227    /// Returns `true` if the charge has been cancelled.
228    ///
229    /// # Example
230    ///
231    /// ```rust,ignore
232    /// if charge.is_cancelled() {
233    ///     println!("Subscription has been cancelled");
234    /// }
235    /// ```
236    #[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    /// Returns `true` if this is a test charge.
244    ///
245    /// Test charges don't actually bill the merchant and are
246    /// automatically approved on development stores.
247    ///
248    /// # Example
249    ///
250    /// ```rust,ignore
251    /// if charge.is_test() {
252    ///     println!("This is a test charge");
253    /// }
254    /// ```
255    #[must_use]
256    pub fn is_test(&self) -> bool {
257        self.test.unwrap_or(false)
258    }
259
260    /// Returns `true` if the subscription is currently in trial period.
261    ///
262    /// This checks if `trial_ends_on` is set and in the future.
263    ///
264    /// # Example
265    ///
266    /// ```rust,ignore
267    /// if charge.is_in_trial() {
268    ///     println!("Subscription is in trial until {:?}", charge.trial_ends_on);
269    /// }
270    /// ```
271    #[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    /// Updates the capped amount for usage-based billing.
278    ///
279    /// Sends a PUT request to `/recurring_application_charges/{id}/customize.json`
280    /// with the new capped amount.
281    ///
282    /// # Arguments
283    ///
284    /// * `client` - The REST client to use for the request
285    /// * `capped_amount` - The new maximum usage charge amount
286    ///
287    /// # Errors
288    ///
289    /// Returns [`ResourceError::NotFound`] if the charge doesn't exist.
290    /// Returns [`ResourceError::PathResolutionFailed`] if the charge has no ID.
291    ///
292    /// # Example
293    ///
294    /// ```rust,ignore
295    /// let charge = RecurringApplicationCharge::find(&client, 123, None).await?.into_inner();
296    /// let updated = charge.customize(&client, "200.00").await?;
297    /// println!("New capped amount: {:?}", updated.capped_amount);
298    /// ```
299    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        // Parse the response - Shopify returns the charge wrapped in "recurring_application_charge" key
329        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    /// Retrieves the currently active recurring charge for the app.
357    ///
358    /// This is a convenience method that lists all charges with `status=active`
359    /// and returns the first one. Most apps should only have one active charge.
360    ///
361    /// # Arguments
362    ///
363    /// * `client` - The REST client to use for the request
364    ///
365    /// # Returns
366    ///
367    /// The currently active recurring charge, or `None` if no active charge exists.
368    ///
369    /// # Errors
370    ///
371    /// Returns an error if the API request fails.
372    ///
373    /// # Example
374    ///
375    /// ```rust,ignore
376    /// if let Some(charge) = RecurringApplicationCharge::current(&client).await? {
377    ///     println!("Active subscription: {} at {}/month",
378    ///         charge.name.as_deref().unwrap_or(""),
379    ///         charge.price.as_deref().unwrap_or("0")
380    ///     );
381    /// } else {
382    ///     println!("No active subscription");
383    /// }
384    /// ```
385    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    /// Paths for the RecurringApplicationCharge resource.
406    ///
407    /// Note: No Update path - use `customize()` for updating capped_amount.
408    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        // Note: customize endpoint is handled separately via the customize() method
434    ];
435
436    fn get_id(&self) -> Option<Self::Id> {
437        self.id
438    }
439}
440
441/// Parameters for finding a single recurring application charge.
442#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
443pub struct RecurringApplicationChargeFindParams {
444    /// Comma-separated list of fields to include in the response.
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub fields: Option<String>,
447}
448
449/// Parameters for listing recurring application charges.
450#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
451pub struct RecurringApplicationChargeListParams {
452    /// Maximum number of results to return (default: 50, max: 250).
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub limit: Option<u32>,
455
456    /// Return charges after this ID.
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub since_id: Option<u64>,
459
460    /// Filter by status (e.g., "active", "pending", "cancelled").
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub status: Option<String>,
463
464    /// Comma-separated list of fields to include in the response.
465    #[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        // Writable fields should be present
520        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        // Read-only fields should be omitted
529        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        // Test is_active
588        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        // Test is_pending
597        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        // Test is_cancelled
605        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        // Test is_test
613        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        // Test default
626        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        // Test with future trial_ends_on (in trial)
636        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        // Test with past trial_ends_on (trial ended)
644        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        // Test with no trial_ends_on (no trial)
652        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        // Find path
662        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        // All path
674        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        // Create path
679        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        // Delete path
689        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        // No Update path (use customize method)
702        let update_path = get_path(
703            RecurringApplicationCharge::PATHS,
704            ResourceOperation::Update,
705            &["id"],
706        );
707        assert!(update_path.is_none());
708
709        // No Count path
710        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(&params).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        // Empty params should serialize to empty object
734        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        // Verify the customize method exists with correct signature
763        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        // Verify PathResolutionFailed error is returned when charge has no ID
772        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        // Verify the current method exists with correct signature
779        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}