Skip to main content

shopify_sdk/rest/resources/v2025_10/
transaction.rs

1//! Transaction resource implementation.
2//!
3//! This module provides the Transaction resource, which represents a payment
4//! transaction associated with an order in Shopify.
5//!
6//! # Nested Path Pattern
7//!
8//! Transactions are always accessed under an order:
9//! - List: `/orders/{order_id}/transactions`
10//! - Find: `/orders/{order_id}/transactions/{id}`
11//! - Create: `/orders/{order_id}/transactions`
12//! - Count: `/orders/{order_id}/transactions/count`
13//!
14//! Use `Transaction::all_with_parent()` to list transactions under a specific order.
15//!
16//! # Note
17//!
18//! Transactions cannot be updated or deleted. They represent immutable records
19//! of payment events.
20//!
21//! # Example
22//!
23//! ```rust,ignore
24//! use shopify_sdk::rest::{RestResource, ResourceResponse};
25//! use shopify_sdk::rest::resources::v2025_10::{Transaction, TransactionKind, TransactionListParams};
26//!
27//! // List transactions under a specific order
28//! let transactions = Transaction::all_with_parent(&client, "order_id", 450789469, None).await?;
29//! for txn in transactions.iter() {
30//!     println!("Transaction: {} - {:?}", txn.amount.as_deref().unwrap_or("0"), txn.kind);
31//! }
32//!
33//! // Create a capture transaction
34//! let mut transaction = Transaction {
35//!     order_id: Some(450789469),
36//!     kind: Some(TransactionKind::Capture),
37//!     amount: Some("199.99".to_string()),
38//!     ..Default::default()
39//! };
40//! let saved = transaction.save(&client).await?;
41//!
42//! // Count transactions for an order
43//! let count = Transaction::count_with_parent(&client, "order_id", 450789469, None).await?;
44//! println!("Total transactions: {}", count);
45//! ```
46
47use std::collections::HashMap;
48
49use chrono::{DateTime, Utc};
50use serde::{Deserialize, Serialize};
51
52use crate::clients::RestClient;
53use crate::rest::{
54    build_path, get_path, ResourceError, ResourceOperation, ResourcePath, RestResource,
55};
56use crate::HttpMethod;
57
58/// The kind of transaction.
59///
60/// Represents the type of payment operation performed.
61#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
62#[serde(rename_all = "snake_case")]
63pub enum TransactionKind {
64    /// Initial authorization of payment.
65    #[default]
66    Authorization,
67    /// Capture of previously authorized payment.
68    Capture,
69    /// Combined authorization and capture in one step.
70    Sale,
71    /// Cancellation of an authorization.
72    Void,
73    /// Return of funds to customer.
74    Refund,
75}
76
77/// The status of a transaction.
78///
79/// Indicates whether the transaction succeeded or failed.
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
81#[serde(rename_all = "snake_case")]
82pub enum TransactionStatus {
83    /// Transaction is pending completion.
84    #[default]
85    Pending,
86    /// Transaction failed.
87    Failure,
88    /// Transaction completed successfully.
89    Success,
90    /// Transaction encountered an error.
91    Error,
92}
93
94/// Payment details for a transaction.
95///
96/// Contains information about the payment method used.
97#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
98pub struct PaymentDetails {
99    /// The credit card bin number.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub credit_card_bin: Option<String>,
102
103    /// AVS result code.
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub avs_result_code: Option<String>,
106
107    /// CVV result code.
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub cvv_result_code: Option<String>,
110
111    /// The credit card number (masked).
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub credit_card_number: Option<String>,
114
115    /// The credit card company.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub credit_card_company: Option<String>,
118
119    /// The name on the credit card.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub credit_card_name: Option<String>,
122
123    /// The credit card wallet.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub credit_card_wallet: Option<String>,
126
127    /// The credit card expiration month.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub credit_card_expiration_month: Option<i32>,
130
131    /// The credit card expiration year.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub credit_card_expiration_year: Option<i32>,
134
135    /// The buyer action info (complex structure).
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub buyer_action_info: Option<serde_json::Value>,
138}
139
140/// Currency exchange adjustment for multi-currency transactions.
141#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
142pub struct CurrencyExchangeAdjustment {
143    /// The ID of the adjustment.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub id: Option<u64>,
146
147    /// The original amount.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub original_amount: Option<String>,
150
151    /// The final amount after adjustment.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub final_amount: Option<String>,
154
155    /// The currency.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub currency: Option<String>,
158
159    /// The adjustment amount.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub adjustment: Option<String>,
162}
163
164/// A payment transaction for an order.
165///
166/// Transactions represent payment events such as authorizations, captures,
167/// refunds, and voids. They are nested under orders and cannot be updated
168/// or deleted after creation.
169///
170/// # Nested Resource
171///
172/// Transactions follow the nested path pattern under orders:
173/// - All operations require `order_id` context
174/// - Use `all_with_parent()` to list transactions under an order
175/// - The `order_id` field is required for creating new transactions
176///
177/// # Fields
178///
179/// ## Read-Only Fields
180/// - `id` - The unique identifier of the transaction
181/// - `created_at` - When the transaction was created
182/// - `processed_at` - When the transaction was processed
183/// - `admin_graphql_api_id` - The GraphQL API ID
184///
185/// ## Writable Fields
186/// - `order_id` - The ID of the order this transaction belongs to
187/// - `kind` - The type of transaction (authorization, capture, sale, void, refund)
188/// - `amount` - The transaction amount
189/// - `currency` - The currency code
190/// - `gateway` - The payment gateway used
191/// - `parent_id` - The ID of the parent transaction (for captures/refunds)
192#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
193pub struct Transaction {
194    /// The unique identifier of the transaction.
195    /// Read-only field.
196    #[serde(skip_serializing)]
197    pub id: Option<u64>,
198
199    /// The ID of the order this transaction belongs to.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub order_id: Option<u64>,
202
203    /// The kind of transaction.
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub kind: Option<TransactionKind>,
206
207    /// The transaction amount.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub amount: Option<String>,
210
211    /// The status of the transaction.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub status: Option<TransactionStatus>,
214
215    /// The payment gateway used.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub gateway: Option<String>,
218
219    /// A message describing the transaction.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub message: Option<String>,
222
223    /// The error code if the transaction failed.
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub error_code: Option<String>,
226
227    /// The authorization code from the payment gateway.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub authorization: Option<String>,
230
231    /// When the authorization expires.
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub authorization_expires_at: Option<DateTime<Utc>>,
234
235    /// The currency code (e.g., "USD").
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub currency: Option<String>,
238
239    /// Whether this is a test transaction.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub test: Option<bool>,
242
243    /// The ID of the parent transaction.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub parent_id: Option<u64>,
246
247    /// The ID of the location.
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub location_id: Option<u64>,
250
251    /// The ID of the device.
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub device_id: Option<u64>,
254
255    /// The ID of the user who processed the transaction.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub user_id: Option<u64>,
258
259    /// The source name.
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub source_name: Option<String>,
262
263    /// When the transaction was processed.
264    /// Read-only field.
265    #[serde(skip_serializing)]
266    pub processed_at: Option<DateTime<Utc>>,
267
268    /// When the transaction was created.
269    /// Read-only field.
270    #[serde(skip_serializing)]
271    pub created_at: Option<DateTime<Utc>>,
272
273    /// The receipt from the payment gateway.
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub receipt: Option<serde_json::Value>,
276
277    /// Payment details.
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub payment_details: Option<PaymentDetails>,
280
281    /// Currency exchange adjustment.
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub currency_exchange_adjustment: Option<CurrencyExchangeAdjustment>,
284
285    /// Total unsettled set (complex structure).
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub total_unsettled_set: Option<serde_json::Value>,
288
289    /// Whether this is a manual payment gateway.
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub manual_payment_gateway: Option<bool>,
292
293    /// Amount rounding information.
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub amount_rounding: Option<serde_json::Value>,
296
297    /// Payments refund attributes.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub payments_refund_attributes: Option<serde_json::Value>,
300
301    /// Extended authorization attributes.
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub extended_authorization_attributes: Option<serde_json::Value>,
304
305    /// The admin GraphQL API ID for this transaction.
306    /// Read-only field.
307    #[serde(skip_serializing)]
308    pub admin_graphql_api_id: Option<String>,
309}
310
311impl Transaction {
312    /// Counts transactions under a specific order.
313    ///
314    /// # Arguments
315    ///
316    /// * `client` - The REST client to use for the request
317    /// * `parent_id_name` - The name of the parent ID parameter (should be `order_id`)
318    /// * `parent_id` - The order ID
319    /// * `params` - Optional parameters for filtering
320    ///
321    /// # Returns
322    ///
323    /// The count of matching transactions as a `u64`.
324    ///
325    /// # Errors
326    ///
327    /// Returns [`ResourceError::PathResolutionFailed`] if no count path exists.
328    ///
329    /// # Example
330    ///
331    /// ```rust,ignore
332    /// let count = Transaction::count_with_parent(&client, "order_id", 450789469, None).await?;
333    /// println!("Transactions in order: {}", count);
334    /// ```
335    pub async fn count_with_parent<ParentId: std::fmt::Display + Send>(
336        client: &RestClient,
337        parent_id_name: &str,
338        parent_id: ParentId,
339        params: Option<TransactionCountParams>,
340    ) -> Result<u64, ResourceError> {
341        let mut ids: HashMap<&str, String> = HashMap::new();
342        ids.insert(parent_id_name, parent_id.to_string());
343
344        let available_ids: Vec<&str> = ids.keys().copied().collect();
345        let path = get_path(Self::PATHS, ResourceOperation::Count, &available_ids).ok_or(
346            ResourceError::PathResolutionFailed {
347                resource: Self::NAME,
348                operation: "count",
349            },
350        )?;
351
352        let url = build_path(path.template, &ids);
353
354        // Build query params
355        let query = params
356            .map(|p| {
357                let value = serde_json::to_value(&p).map_err(|e| {
358                    ResourceError::Http(crate::clients::HttpError::Response(
359                        crate::clients::HttpResponseError {
360                            code: 400,
361                            message: format!("Failed to serialize params: {e}"),
362                            error_reference: None,
363                        },
364                    ))
365                })?;
366
367                let mut query = HashMap::new();
368                if let serde_json::Value::Object(map) = value {
369                    for (key, val) in map {
370                        match val {
371                            serde_json::Value::String(s) => {
372                                query.insert(key, s);
373                            }
374                            serde_json::Value::Number(n) => {
375                                query.insert(key, n.to_string());
376                            }
377                            serde_json::Value::Bool(b) => {
378                                query.insert(key, b.to_string());
379                            }
380                            _ => {}
381                        }
382                    }
383                }
384                Ok::<_, ResourceError>(query)
385            })
386            .transpose()?
387            .filter(|q| !q.is_empty());
388
389        let response = client.get(&url, query).await?;
390
391        if !response.is_ok() {
392            return Err(ResourceError::from_http_response(
393                response.code,
394                &response.body,
395                Self::NAME,
396                None,
397                response.request_id(),
398            ));
399        }
400
401        // Extract count from response
402        let count = response
403            .body
404            .get("count")
405            .and_then(serde_json::Value::as_u64)
406            .ok_or_else(|| {
407                ResourceError::Http(crate::clients::HttpError::Response(
408                    crate::clients::HttpResponseError {
409                        code: response.code,
410                        message: "Missing 'count' in response".to_string(),
411                        error_reference: response.request_id().map(ToString::to_string),
412                    },
413                ))
414            })?;
415
416        Ok(count)
417    }
418}
419
420impl RestResource for Transaction {
421    type Id = u64;
422    type FindParams = TransactionFindParams;
423    type AllParams = TransactionListParams;
424    type CountParams = TransactionCountParams;
425
426    const NAME: &'static str = "Transaction";
427    const PLURAL: &'static str = "transactions";
428
429    /// Paths for the Transaction resource.
430    ///
431    /// Transactions are NESTED under orders. All operations require `order_id`.
432    /// Note: Transactions cannot be updated or deleted.
433    const PATHS: &'static [ResourcePath] = &[
434        // All paths require order_id
435        ResourcePath::new(
436            HttpMethod::Get,
437            ResourceOperation::Find,
438            &["order_id", "id"],
439            "orders/{order_id}/transactions/{id}",
440        ),
441        ResourcePath::new(
442            HttpMethod::Get,
443            ResourceOperation::All,
444            &["order_id"],
445            "orders/{order_id}/transactions",
446        ),
447        ResourcePath::new(
448            HttpMethod::Get,
449            ResourceOperation::Count,
450            &["order_id"],
451            "orders/{order_id}/transactions/count",
452        ),
453        ResourcePath::new(
454            HttpMethod::Post,
455            ResourceOperation::Create,
456            &["order_id"],
457            "orders/{order_id}/transactions",
458        ),
459        // No Update or Delete paths - transactions are immutable
460    ];
461
462    fn get_id(&self) -> Option<Self::Id> {
463        self.id
464    }
465}
466
467/// Parameters for finding a single transaction.
468#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
469pub struct TransactionFindParams {
470    /// Comma-separated list of fields to include in the response.
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub fields: Option<String>,
473
474    /// Whether to return the amount in shop currency.
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub in_shop_currency: Option<bool>,
477}
478
479/// Parameters for listing transactions.
480#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
481pub struct TransactionListParams {
482    /// Maximum number of results to return (default: 50, max: 250).
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub limit: Option<u32>,
485
486    /// Return transactions after this ID.
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub since_id: Option<u64>,
489
490    /// Comma-separated list of fields to include in the response.
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub fields: Option<String>,
493
494    /// Whether to return the amount in shop currency.
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub in_shop_currency: Option<bool>,
497}
498
499/// Parameters for counting transactions.
500#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
501pub struct TransactionCountParams {
502    // No specific count params for transactions
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use crate::rest::{get_path, ResourceOperation};
509
510    #[test]
511    fn test_transaction_kind_enum_serialization() {
512        // Test serialization to snake_case
513        assert_eq!(
514            serde_json::to_string(&TransactionKind::Authorization).unwrap(),
515            "\"authorization\""
516        );
517        assert_eq!(
518            serde_json::to_string(&TransactionKind::Capture).unwrap(),
519            "\"capture\""
520        );
521        assert_eq!(
522            serde_json::to_string(&TransactionKind::Sale).unwrap(),
523            "\"sale\""
524        );
525        assert_eq!(
526            serde_json::to_string(&TransactionKind::Void).unwrap(),
527            "\"void\""
528        );
529        assert_eq!(
530            serde_json::to_string(&TransactionKind::Refund).unwrap(),
531            "\"refund\""
532        );
533
534        // Test deserialization from snake_case
535        let auth: TransactionKind = serde_json::from_str("\"authorization\"").unwrap();
536        let capture: TransactionKind = serde_json::from_str("\"capture\"").unwrap();
537        let sale: TransactionKind = serde_json::from_str("\"sale\"").unwrap();
538        let void_txn: TransactionKind = serde_json::from_str("\"void\"").unwrap();
539        let refund: TransactionKind = serde_json::from_str("\"refund\"").unwrap();
540
541        assert_eq!(auth, TransactionKind::Authorization);
542        assert_eq!(capture, TransactionKind::Capture);
543        assert_eq!(sale, TransactionKind::Sale);
544        assert_eq!(void_txn, TransactionKind::Void);
545        assert_eq!(refund, TransactionKind::Refund);
546
547        // Test default
548        assert_eq!(TransactionKind::default(), TransactionKind::Authorization);
549    }
550
551    #[test]
552    fn test_transaction_status_enum_serialization() {
553        // Test serialization
554        assert_eq!(
555            serde_json::to_string(&TransactionStatus::Pending).unwrap(),
556            "\"pending\""
557        );
558        assert_eq!(
559            serde_json::to_string(&TransactionStatus::Failure).unwrap(),
560            "\"failure\""
561        );
562        assert_eq!(
563            serde_json::to_string(&TransactionStatus::Success).unwrap(),
564            "\"success\""
565        );
566        assert_eq!(
567            serde_json::to_string(&TransactionStatus::Error).unwrap(),
568            "\"error\""
569        );
570
571        // Test deserialization
572        let success: TransactionStatus = serde_json::from_str("\"success\"").unwrap();
573        let failure: TransactionStatus = serde_json::from_str("\"failure\"").unwrap();
574
575        assert_eq!(success, TransactionStatus::Success);
576        assert_eq!(failure, TransactionStatus::Failure);
577
578        // Test default
579        assert_eq!(TransactionStatus::default(), TransactionStatus::Pending);
580    }
581
582    #[test]
583    fn test_transaction_nested_paths_require_order_id() {
584        // All paths should require order_id (nested under orders)
585
586        // Find requires both order_id and id
587        let find_path = get_path(Transaction::PATHS, ResourceOperation::Find, &["order_id", "id"]);
588        assert!(find_path.is_some());
589        assert_eq!(
590            find_path.unwrap().template,
591            "orders/{order_id}/transactions/{id}"
592        );
593
594        // Find with only id should fail (no standalone path)
595        let find_without_order = get_path(Transaction::PATHS, ResourceOperation::Find, &["id"]);
596        assert!(find_without_order.is_none());
597
598        // All requires order_id
599        let all_path = get_path(Transaction::PATHS, ResourceOperation::All, &["order_id"]);
600        assert!(all_path.is_some());
601        assert_eq!(
602            all_path.unwrap().template,
603            "orders/{order_id}/transactions"
604        );
605
606        // All without order_id should fail
607        let all_without_order = get_path(Transaction::PATHS, ResourceOperation::All, &[]);
608        assert!(all_without_order.is_none());
609
610        // Count requires order_id
611        let count_path = get_path(Transaction::PATHS, ResourceOperation::Count, &["order_id"]);
612        assert!(count_path.is_some());
613        assert_eq!(
614            count_path.unwrap().template,
615            "orders/{order_id}/transactions/count"
616        );
617
618        // Create requires order_id
619        let create_path = get_path(Transaction::PATHS, ResourceOperation::Create, &["order_id"]);
620        assert!(create_path.is_some());
621        assert_eq!(
622            create_path.unwrap().template,
623            "orders/{order_id}/transactions"
624        );
625
626        // No Update path
627        let update_path = get_path(
628            Transaction::PATHS,
629            ResourceOperation::Update,
630            &["order_id", "id"],
631        );
632        assert!(update_path.is_none());
633
634        // No Delete path
635        let delete_path = get_path(
636            Transaction::PATHS,
637            ResourceOperation::Delete,
638            &["order_id", "id"],
639        );
640        assert!(delete_path.is_none());
641    }
642
643    #[test]
644    fn test_transaction_struct_serialization() {
645        let transaction = Transaction {
646            id: Some(389404469),
647            order_id: Some(450789469),
648            kind: Some(TransactionKind::Capture),
649            amount: Some("199.99".to_string()),
650            status: Some(TransactionStatus::Success),
651            gateway: Some("bogus".to_string()),
652            message: Some("Transaction successful".to_string()),
653            currency: Some("USD".to_string()),
654            test: Some(true),
655            parent_id: Some(389404468),
656            created_at: Some(
657                DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
658                    .unwrap()
659                    .with_timezone(&Utc),
660            ),
661            admin_graphql_api_id: Some("gid://shopify/OrderTransaction/389404469".to_string()),
662            ..Default::default()
663        };
664
665        let json = serde_json::to_string(&transaction).unwrap();
666        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
667
668        // Writable fields should be present
669        assert_eq!(parsed["order_id"], 450789469);
670        assert_eq!(parsed["kind"], "capture");
671        assert_eq!(parsed["amount"], "199.99");
672        assert_eq!(parsed["status"], "success");
673        assert_eq!(parsed["gateway"], "bogus");
674        assert_eq!(parsed["message"], "Transaction successful");
675        assert_eq!(parsed["currency"], "USD");
676        assert_eq!(parsed["test"], true);
677        assert_eq!(parsed["parent_id"], 389404468);
678
679        // Read-only fields should be omitted
680        assert!(parsed.get("id").is_none());
681        assert!(parsed.get("created_at").is_none());
682        assert!(parsed.get("processed_at").is_none());
683        assert!(parsed.get("admin_graphql_api_id").is_none());
684    }
685
686    #[test]
687    fn test_transaction_deserialization_from_api_response() {
688        let json = r#"{
689            "id": 389404469,
690            "order_id": 450789469,
691            "kind": "capture",
692            "amount": "199.99",
693            "status": "success",
694            "gateway": "bogus",
695            "message": "Bogus Gateway: Forced success",
696            "error_code": null,
697            "authorization": "ch_1234567890",
698            "authorization_expires_at": "2024-01-22T10:30:00Z",
699            "currency": "USD",
700            "test": true,
701            "parent_id": 389404468,
702            "location_id": 655441491,
703            "user_id": 799407056,
704            "source_name": "web",
705            "processed_at": "2024-01-15T10:30:00Z",
706            "created_at": "2024-01-15T10:30:00Z",
707            "payment_details": {
708                "credit_card_bin": "424242",
709                "credit_card_number": "xxxx xxxx xxxx 4242",
710                "credit_card_company": "Visa",
711                "credit_card_name": "John Doe"
712            },
713            "receipt": {
714                "testcase": true,
715                "authorization": "ch_1234567890"
716            },
717            "admin_graphql_api_id": "gid://shopify/OrderTransaction/389404469"
718        }"#;
719
720        let transaction: Transaction = serde_json::from_str(json).unwrap();
721
722        assert_eq!(transaction.id, Some(389404469));
723        assert_eq!(transaction.order_id, Some(450789469));
724        assert_eq!(transaction.kind, Some(TransactionKind::Capture));
725        assert_eq!(transaction.amount, Some("199.99".to_string()));
726        assert_eq!(transaction.status, Some(TransactionStatus::Success));
727        assert_eq!(transaction.gateway, Some("bogus".to_string()));
728        assert_eq!(
729            transaction.authorization,
730            Some("ch_1234567890".to_string())
731        );
732        assert!(transaction.authorization_expires_at.is_some());
733        assert_eq!(transaction.currency, Some("USD".to_string()));
734        assert_eq!(transaction.test, Some(true));
735        assert_eq!(transaction.parent_id, Some(389404468));
736        assert_eq!(transaction.location_id, Some(655441491));
737        assert_eq!(transaction.user_id, Some(799407056));
738        assert!(transaction.processed_at.is_some());
739        assert!(transaction.created_at.is_some());
740        assert!(transaction.payment_details.is_some());
741        assert!(transaction.receipt.is_some());
742
743        let payment_details = transaction.payment_details.unwrap();
744        assert_eq!(payment_details.credit_card_bin, Some("424242".to_string()));
745        assert_eq!(payment_details.credit_card_company, Some("Visa".to_string()));
746    }
747
748    #[test]
749    fn test_transaction_list_params_serialization() {
750        let params = TransactionListParams {
751            limit: Some(50),
752            since_id: Some(100),
753            fields: Some("id,kind,amount".to_string()),
754            in_shop_currency: Some(true),
755        };
756
757        let json = serde_json::to_value(&params).unwrap();
758
759        assert_eq!(json["limit"], 50);
760        assert_eq!(json["since_id"], 100);
761        assert_eq!(json["fields"], "id,kind,amount");
762        assert_eq!(json["in_shop_currency"], true);
763
764        // Test empty params
765        let empty_params = TransactionListParams::default();
766        let empty_json = serde_json::to_value(&empty_params).unwrap();
767        assert_eq!(empty_json, serde_json::json!({}));
768    }
769
770    #[test]
771    fn test_transaction_find_params_serialization() {
772        let params = TransactionFindParams {
773            fields: Some("id,kind,amount".to_string()),
774            in_shop_currency: Some(true),
775        };
776
777        let json = serde_json::to_value(&params).unwrap();
778
779        assert_eq!(json["fields"], "id,kind,amount");
780        assert_eq!(json["in_shop_currency"], true);
781    }
782
783    #[test]
784    fn test_transaction_get_id_returns_correct_value() {
785        // Transaction with ID
786        let txn_with_id = Transaction {
787            id: Some(389404469),
788            order_id: Some(450789469),
789            kind: Some(TransactionKind::Capture),
790            ..Default::default()
791        };
792        assert_eq!(txn_with_id.get_id(), Some(389404469));
793
794        // Transaction without ID (new transaction)
795        let txn_without_id = Transaction {
796            id: None,
797            order_id: Some(450789469),
798            kind: Some(TransactionKind::Capture),
799            ..Default::default()
800        };
801        assert_eq!(txn_without_id.get_id(), None);
802    }
803
804    #[test]
805    fn test_transaction_constants() {
806        assert_eq!(Transaction::NAME, "Transaction");
807        assert_eq!(Transaction::PLURAL, "transactions");
808    }
809}