Skip to main content

awp_types/
payment.rs

1//! AWP payment intent types.
2//!
3//! The AWP `PaymentIntent` is a simpler, owner-policy-driven lifecycle
4//! distinct from the ACP/AP2 transaction model in `adk-payments`.
5//! It represents the business owner's view of a payment flow.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use crate::TrustLevel;
12
13/// State of an AWP payment intent.
14///
15/// Lifecycle: `Draft → PendingApproval → Approved → Executing → Settled`
16///
17/// Terminal states: `Settled`, `Rejected`, `Cancelled`.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum PaymentIntentState {
21    /// Initial state — intent created but not yet submitted.
22    Draft,
23    /// Awaiting owner approval (amount exceeds auto-approve threshold).
24    PendingApproval,
25    /// Owner approved — ready for execution.
26    Approved,
27    /// Payment is being processed by the payment provider.
28    Executing,
29    /// Payment completed successfully.
30    Settled,
31    /// Owner or policy rejected the payment.
32    Rejected,
33    /// Buyer or system cancelled the payment.
34    Cancelled,
35}
36
37impl PaymentIntentState {
38    /// Whether this is a terminal state (no further transitions allowed).
39    pub fn is_terminal(self) -> bool {
40        matches!(self, Self::Settled | Self::Rejected | Self::Cancelled)
41    }
42}
43
44impl std::fmt::Display for PaymentIntentState {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            Self::Draft => write!(f, "draft"),
48            Self::PendingApproval => write!(f, "pending_approval"),
49            Self::Approved => write!(f, "approved"),
50            Self::Executing => write!(f, "executing"),
51            Self::Settled => write!(f, "settled"),
52            Self::Rejected => write!(f, "rejected"),
53            Self::Cancelled => write!(f, "cancelled"),
54        }
55    }
56}
57
58/// An AWP payment intent representing a business owner's view of a payment.
59///
60/// This is the protocol-level type shared between `adk-awp` and `adk-gateway`.
61/// The policy engine in `adk-awp` bridges this to `adk-payments` for execution.
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct PaymentIntent {
65    /// Unique payment intent identifier.
66    pub id: Uuid,
67    /// Product or service SKU.
68    pub sku: String,
69    /// Amount in smallest currency unit (e.g. cents).
70    pub amount: u64,
71    /// ISO 4217 currency code (e.g. "USD", "KES").
72    pub currency: String,
73    /// Payment token from the payment provider (set after authorization).
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub payment_token: Option<String>,
76    /// Current state in the lifecycle.
77    pub state: PaymentIntentState,
78    /// Trust level of the requester who initiated this intent.
79    pub trust_level: TrustLevel,
80    /// HMAC-SHA256 signature for integrity verification.
81    pub signature: String,
82    /// When this intent was created.
83    pub created_at: DateTime<Utc>,
84    /// When this intent was last updated.
85    pub updated_at: DateTime<Utc>,
86}
87
88/// Owner-configurable policy for auto-approving or requiring approval.
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct PaymentPolicy {
92    /// Auto-approve if amount is at or below this threshold (in smallest currency unit).
93    pub auto_approve_threshold: u64,
94    /// Require explicit owner approval above this threshold.
95    pub require_approval_threshold: u64,
96    /// Minimum trust level for auto-approval.
97    pub min_trust_for_auto_approve: TrustLevel,
98    /// Notification channel for approval requests (e.g. "email", "sms", "webhook").
99    #[serde(default, skip_serializing_if = "Vec::is_empty")]
100    pub notification_channels: Vec<String>,
101}
102
103impl Default for PaymentPolicy {
104    fn default() -> Self {
105        Self {
106            auto_approve_threshold: 5000,       // $50.00
107            require_approval_threshold: 50_000, // $500.00
108            min_trust_for_auto_approve: TrustLevel::Known,
109            notification_channels: vec![],
110        }
111    }
112}
113
114impl PaymentPolicy {
115    /// Evaluate whether a payment intent should be auto-approved, require
116    /// owner approval, or be auto-rejected.
117    pub fn evaluate(&self, amount: u64, trust_level: TrustLevel) -> PaymentPolicyDecision {
118        if trust_level < self.min_trust_for_auto_approve {
119            return PaymentPolicyDecision::RequireApproval;
120        }
121        if amount <= self.auto_approve_threshold {
122            return PaymentPolicyDecision::AutoApprove;
123        }
124        if amount > self.require_approval_threshold {
125            return PaymentPolicyDecision::RequireApproval;
126        }
127        PaymentPolicyDecision::RequireApproval
128    }
129}
130
131/// Decision from the payment policy engine.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum PaymentPolicyDecision {
134    /// Automatically approve — amount and trust level meet thresholds.
135    AutoApprove,
136    /// Require explicit owner approval.
137    RequireApproval,
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_payment_intent_state_terminal() {
146        assert!(!PaymentIntentState::Draft.is_terminal());
147        assert!(!PaymentIntentState::PendingApproval.is_terminal());
148        assert!(!PaymentIntentState::Approved.is_terminal());
149        assert!(!PaymentIntentState::Executing.is_terminal());
150        assert!(PaymentIntentState::Settled.is_terminal());
151        assert!(PaymentIntentState::Rejected.is_terminal());
152        assert!(PaymentIntentState::Cancelled.is_terminal());
153    }
154
155    #[test]
156    fn test_payment_intent_state_display() {
157        assert_eq!(PaymentIntentState::Draft.to_string(), "draft");
158        assert_eq!(PaymentIntentState::PendingApproval.to_string(), "pending_approval");
159        assert_eq!(PaymentIntentState::Settled.to_string(), "settled");
160    }
161
162    #[test]
163    fn test_payment_intent_state_serde() {
164        let states = [
165            PaymentIntentState::Draft,
166            PaymentIntentState::PendingApproval,
167            PaymentIntentState::Approved,
168            PaymentIntentState::Executing,
169            PaymentIntentState::Settled,
170            PaymentIntentState::Rejected,
171            PaymentIntentState::Cancelled,
172        ];
173        for state in states {
174            let json = serde_json::to_string(&state).unwrap();
175            let parsed: PaymentIntentState = serde_json::from_str(&json).unwrap();
176            assert_eq!(state, parsed);
177        }
178    }
179
180    #[test]
181    fn test_payment_intent_serde_round_trip() {
182        let intent = PaymentIntent {
183            id: Uuid::now_v7(),
184            sku: "WIDGET-001".to_string(),
185            amount: 2500,
186            currency: "USD".to_string(),
187            payment_token: None,
188            state: PaymentIntentState::Draft,
189            trust_level: TrustLevel::Known,
190            signature: "sha256=abc123".to_string(),
191            created_at: Utc::now(),
192            updated_at: Utc::now(),
193        };
194        let json = serde_json::to_string(&intent).unwrap();
195        let parsed: PaymentIntent = serde_json::from_str(&json).unwrap();
196        assert_eq!(intent, parsed);
197    }
198
199    #[test]
200    fn test_payment_intent_optional_token_skipped() {
201        let intent = PaymentIntent {
202            id: Uuid::now_v7(),
203            sku: "SKU".to_string(),
204            amount: 100,
205            currency: "KES".to_string(),
206            payment_token: None,
207            state: PaymentIntentState::Draft,
208            trust_level: TrustLevel::Anonymous,
209            signature: "sig".to_string(),
210            created_at: Utc::now(),
211            updated_at: Utc::now(),
212        };
213        let json = serde_json::to_string(&intent).unwrap();
214        assert!(!json.contains("paymentToken"));
215    }
216
217    #[test]
218    fn test_payment_policy_default() {
219        let policy = PaymentPolicy::default();
220        assert_eq!(policy.auto_approve_threshold, 5000);
221        assert_eq!(policy.require_approval_threshold, 50_000);
222        assert_eq!(policy.min_trust_for_auto_approve, TrustLevel::Known);
223    }
224
225    #[test]
226    fn test_payment_policy_auto_approve() {
227        let policy = PaymentPolicy::default();
228        assert_eq!(policy.evaluate(2500, TrustLevel::Known), PaymentPolicyDecision::AutoApprove);
229    }
230
231    #[test]
232    fn test_payment_policy_require_approval_high_amount() {
233        let policy = PaymentPolicy::default();
234        assert_eq!(
235            policy.evaluate(60_000, TrustLevel::Partner),
236            PaymentPolicyDecision::RequireApproval
237        );
238    }
239
240    #[test]
241    fn test_payment_policy_require_approval_low_trust() {
242        let policy = PaymentPolicy::default();
243        assert_eq!(
244            policy.evaluate(1000, TrustLevel::Anonymous),
245            PaymentPolicyDecision::RequireApproval
246        );
247    }
248
249    #[test]
250    fn test_payment_policy_serde_round_trip() {
251        let policy = PaymentPolicy {
252            auto_approve_threshold: 1000,
253            require_approval_threshold: 10_000,
254            min_trust_for_auto_approve: TrustLevel::Partner,
255            notification_channels: vec!["email".to_string(), "sms".to_string()],
256        };
257        let json = serde_json::to_string(&policy).unwrap();
258        let parsed: PaymentPolicy = serde_json::from_str(&json).unwrap();
259        assert_eq!(policy, parsed);
260    }
261}