payrix 0.3.0

Rust client for the Payrix payment processing API
//! Transaction reference types for the Payrix API.
//!
//! Transaction references (`txnRefs`) track reference values associated with
//! transactions at different processing stages.
//!
//! **OpenAPI schema:** `txnRefsResponse`

use payrix_macros::PayrixEntity;
use serde::{Deserialize, Serialize};

use super::{bool_from_int_default_false, PayrixId};

// =============================================================================
// Enums
// =============================================================================

/// Transaction reference stage values per OpenAPI spec.
///
/// **OpenAPI schema:** `txnRefStage`
///
/// Indicates which processing stage the reference was captured at.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TxnRefStage {
    /// Authorization stage.
    #[default]
    Auth,
    /// Capture stage.
    Capture,
    /// Draft stage.
    Draft,
    /// File stage.
    File,
    /// Refund stage.
    Refund,
    /// Retrieval stage.
    Retrieval,
    /// ThreatMetrix fraud check stage.
    #[serde(rename = "threatMetrix")]
    ThreatMetrix,
}

// =============================================================================
// TxnRef (Response)
// =============================================================================

/// A Payrix transaction reference.
///
/// Transaction references associate reference values (such as processor
/// reference numbers) with transactions at specific processing stages.
///
/// **OpenAPI schema:** `txnRefsResponse`
#[derive(Debug, Clone, Serialize, Deserialize, PayrixEntity)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
#[serde(rename_all = "camelCase")]
pub struct TxnRef {
    // -------------------------------------------------------------------------
    // Core Identifiers
    // -------------------------------------------------------------------------

    /// The ID of this resource.
    ///
    /// **OpenAPI type:** string
    #[payrix(readonly)]
    pub id: PayrixId,

    /// The date and time at which this resource was created.
    ///
    /// Format: `YYYY-MM-DD HH:MM:SS.SSSS`
    ///
    /// **OpenAPI type:** string
    #[payrix(readonly)]
    #[serde(default)]
    pub created: Option<String>,

    /// The date and time at which this resource was modified.
    ///
    /// Format: `YYYY-MM-DD HH:MM:SS.SSSS`
    ///
    /// **OpenAPI type:** string
    #[payrix(readonly)]
    #[serde(default)]
    pub modified: Option<String>,

    /// The identifier of the Login that created this resource.
    ///
    /// **OpenAPI type:** string
    #[payrix(readonly)]
    #[serde(default)]
    pub creator: Option<PayrixId>,

    /// The identifier of the Login that last modified this resource.
    ///
    /// **OpenAPI type:** string
    #[payrix(readonly)]
    #[serde(default)]
    pub modifier: Option<PayrixId>,

    // -------------------------------------------------------------------------
    // Relationships
    // -------------------------------------------------------------------------

    /// The ID of the transaction this reference belongs to.
    ///
    /// **OpenAPI type:** string (ref: txnRefsModelTxn)
    #[serde(default)]
    pub txn: Option<PayrixId>,

    // -------------------------------------------------------------------------
    // Reference Data
    // -------------------------------------------------------------------------

    /// The reference value.
    ///
    /// Note: The JSON field name is `ref` but this is a Rust reserved keyword,
    /// so it is mapped to `ref_value` in the struct.
    ///
    /// **OpenAPI type:** string
    #[serde(default, rename = "ref")]
    pub ref_value: Option<String>,

    /// The processing stage at which this reference was captured.
    ///
    /// **OpenAPI type:** string (ref: txnRefStage)
    #[serde(default)]
    pub stage: Option<TxnRefStage>,

    /// The platform identifier.
    ///
    /// **OpenAPI type:** string
    #[serde(default)]
    pub platform: Option<String>,

    // -------------------------------------------------------------------------
    // Status Flags
    // -------------------------------------------------------------------------

    /// Whether this resource is marked as inactive.
    ///
    /// **OpenAPI type:** integer (ref: Inactive)
    #[serde(default, with = "bool_from_int_default_false")]
    pub inactive: bool,

    /// Whether this resource is marked as frozen.
    ///
    /// **OpenAPI type:** integer (ref: Frozen)
    #[serde(default, with = "bool_from_int_default_false")]
    pub frozen: bool,
}

// =============================================================================
// Tests
// =============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    // ==================== TxnRefStage Tests ====================

    #[test]
    fn txn_ref_stage_serialize_all_variants() {
        assert_eq!(serde_json::to_string(&TxnRefStage::Auth).unwrap(), "\"auth\"");
        assert_eq!(serde_json::to_string(&TxnRefStage::Capture).unwrap(), "\"capture\"");
        assert_eq!(serde_json::to_string(&TxnRefStage::Draft).unwrap(), "\"draft\"");
        assert_eq!(serde_json::to_string(&TxnRefStage::File).unwrap(), "\"file\"");
        assert_eq!(serde_json::to_string(&TxnRefStage::Refund).unwrap(), "\"refund\"");
        assert_eq!(serde_json::to_string(&TxnRefStage::Retrieval).unwrap(), "\"retrieval\"");
        assert_eq!(serde_json::to_string(&TxnRefStage::ThreatMetrix).unwrap(), "\"threatMetrix\"");
    }

    #[test]
    fn txn_ref_stage_deserialize_all_variants() {
        assert_eq!(serde_json::from_str::<TxnRefStage>("\"auth\"").unwrap(), TxnRefStage::Auth);
        assert_eq!(serde_json::from_str::<TxnRefStage>("\"capture\"").unwrap(), TxnRefStage::Capture);
        assert_eq!(serde_json::from_str::<TxnRefStage>("\"draft\"").unwrap(), TxnRefStage::Draft);
        assert_eq!(serde_json::from_str::<TxnRefStage>("\"file\"").unwrap(), TxnRefStage::File);
        assert_eq!(serde_json::from_str::<TxnRefStage>("\"refund\"").unwrap(), TxnRefStage::Refund);
        assert_eq!(serde_json::from_str::<TxnRefStage>("\"retrieval\"").unwrap(), TxnRefStage::Retrieval);
        assert_eq!(serde_json::from_str::<TxnRefStage>("\"threatMetrix\"").unwrap(), TxnRefStage::ThreatMetrix);
    }

    #[test]
    fn txn_ref_stage_default() {
        assert_eq!(TxnRefStage::default(), TxnRefStage::Auth);
    }

    #[test]
    fn txn_ref_stage_invalid_value() {
        assert!(serde_json::from_str::<TxnRefStage>("\"invalid\"").is_err());
    }

    // ==================== TxnRef Struct Tests ====================

    #[test]
    fn txn_ref_deserialize_full() {
        let json = r#"{
            "id": "t1_trf_12345678901234567890123",
            "created": "2025-01-15 10:30:00.0000",
            "modified": "2025-01-15 10:30:00.0000",
            "creator": "t1_lgn_12345678901234567890123",
            "modifier": "t1_lgn_12345678901234567890124",
            "txn": "t1_txn_12345678901234567890123",
            "ref": "REF-12345-AUTH",
            "stage": "auth",
            "platform": "VANTIV",
            "inactive": 0,
            "frozen": 0
        }"#;

        let txn_ref: TxnRef = serde_json::from_str(json).unwrap();
        assert_eq!(txn_ref.id.as_str(), "t1_trf_12345678901234567890123");
        assert_eq!(txn_ref.txn.as_ref().unwrap().as_str(), "t1_txn_12345678901234567890123");
        assert_eq!(txn_ref.ref_value.as_deref(), Some("REF-12345-AUTH"));
        assert_eq!(txn_ref.stage, Some(TxnRefStage::Auth));
        assert_eq!(txn_ref.platform.as_deref(), Some("VANTIV"));
        assert!(!txn_ref.inactive);
        assert!(!txn_ref.frozen);
    }

    #[test]
    fn txn_ref_deserialize_minimal() {
        let json = r#"{"id": "t1_trf_12345678901234567890123"}"#;

        let txn_ref: TxnRef = serde_json::from_str(json).unwrap();
        assert_eq!(txn_ref.id.as_str(), "t1_trf_12345678901234567890123");
        assert!(txn_ref.txn.is_none());
        assert!(txn_ref.ref_value.is_none());
        assert!(txn_ref.stage.is_none());
        assert!(txn_ref.platform.is_none());
        assert!(!txn_ref.inactive);
        assert!(!txn_ref.frozen);
    }

    #[test]
    fn txn_ref_deserialize_capture_stage() {
        let json = r#"{
            "id": "t1_trf_12345678901234567890123",
            "ref": "CAPTURE-REF-789",
            "stage": "capture"
        }"#;

        let txn_ref: TxnRef = serde_json::from_str(json).unwrap();
        assert_eq!(txn_ref.ref_value.as_deref(), Some("CAPTURE-REF-789"));
        assert_eq!(txn_ref.stage, Some(TxnRefStage::Capture));
    }

    #[test]
    fn txn_ref_deserialize_threat_metrix_stage() {
        let json = r#"{
            "id": "t1_trf_12345678901234567890123",
            "ref": "TM-SESSION-ID-123",
            "stage": "threatMetrix"
        }"#;

        let txn_ref: TxnRef = serde_json::from_str(json).unwrap();
        assert_eq!(txn_ref.stage, Some(TxnRefStage::ThreatMetrix));
    }

    #[test]
    fn txn_ref_serialize_roundtrip() {
        let json = r#"{
            "id": "t1_trf_12345678901234567890123",
            "txn": "t1_txn_12345678901234567890123",
            "ref": "REF-ROUNDTRIP",
            "stage": "refund"
        }"#;

        let txn_ref: TxnRef = serde_json::from_str(json).unwrap();
        let serialized = serde_json::to_string(&txn_ref).unwrap();
        let deserialized: TxnRef = serde_json::from_str(&serialized).unwrap();
        assert_eq!(txn_ref.id, deserialized.id);
        assert_eq!(txn_ref.ref_value, deserialized.ref_value);
        assert_eq!(txn_ref.stage, deserialized.stage);
    }

    #[test]
    fn txn_ref_bool_from_int() {
        let json = r#"{"id": "t1_trf_12345678901234567890123", "inactive": 1, "frozen": 0}"#;
        let txn_ref: TxnRef = serde_json::from_str(json).unwrap();
        assert!(txn_ref.inactive);
        assert!(!txn_ref.frozen);
    }
}