use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::TrustLevel;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PaymentIntentState {
Draft,
PendingApproval,
Approved,
Executing,
Settled,
Rejected,
Cancelled,
}
impl PaymentIntentState {
pub fn is_terminal(self) -> bool {
matches!(self, Self::Settled | Self::Rejected | Self::Cancelled)
}
}
impl std::fmt::Display for PaymentIntentState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Draft => write!(f, "draft"),
Self::PendingApproval => write!(f, "pending_approval"),
Self::Approved => write!(f, "approved"),
Self::Executing => write!(f, "executing"),
Self::Settled => write!(f, "settled"),
Self::Rejected => write!(f, "rejected"),
Self::Cancelled => write!(f, "cancelled"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentIntent {
pub id: Uuid,
pub sku: String,
pub amount: u64,
pub currency: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_token: Option<String>,
pub state: PaymentIntentState,
pub trust_level: TrustLevel,
pub signature: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentPolicy {
pub auto_approve_threshold: u64,
pub require_approval_threshold: u64,
pub min_trust_for_auto_approve: TrustLevel,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notification_channels: Vec<String>,
}
impl Default for PaymentPolicy {
fn default() -> Self {
Self {
auto_approve_threshold: 5000, require_approval_threshold: 50_000, min_trust_for_auto_approve: TrustLevel::Known,
notification_channels: vec![],
}
}
}
impl PaymentPolicy {
pub fn evaluate(&self, amount: u64, trust_level: TrustLevel) -> PaymentPolicyDecision {
if trust_level < self.min_trust_for_auto_approve {
return PaymentPolicyDecision::RequireApproval;
}
if amount <= self.auto_approve_threshold {
return PaymentPolicyDecision::AutoApprove;
}
if amount > self.require_approval_threshold {
return PaymentPolicyDecision::RequireApproval;
}
PaymentPolicyDecision::RequireApproval
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaymentPolicyDecision {
AutoApprove,
RequireApproval,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_payment_intent_state_terminal() {
assert!(!PaymentIntentState::Draft.is_terminal());
assert!(!PaymentIntentState::PendingApproval.is_terminal());
assert!(!PaymentIntentState::Approved.is_terminal());
assert!(!PaymentIntentState::Executing.is_terminal());
assert!(PaymentIntentState::Settled.is_terminal());
assert!(PaymentIntentState::Rejected.is_terminal());
assert!(PaymentIntentState::Cancelled.is_terminal());
}
#[test]
fn test_payment_intent_state_display() {
assert_eq!(PaymentIntentState::Draft.to_string(), "draft");
assert_eq!(PaymentIntentState::PendingApproval.to_string(), "pending_approval");
assert_eq!(PaymentIntentState::Settled.to_string(), "settled");
}
#[test]
fn test_payment_intent_state_serde() {
let states = [
PaymentIntentState::Draft,
PaymentIntentState::PendingApproval,
PaymentIntentState::Approved,
PaymentIntentState::Executing,
PaymentIntentState::Settled,
PaymentIntentState::Rejected,
PaymentIntentState::Cancelled,
];
for state in states {
let json = serde_json::to_string(&state).unwrap();
let parsed: PaymentIntentState = serde_json::from_str(&json).unwrap();
assert_eq!(state, parsed);
}
}
#[test]
fn test_payment_intent_serde_round_trip() {
let intent = PaymentIntent {
id: Uuid::now_v7(),
sku: "WIDGET-001".to_string(),
amount: 2500,
currency: "USD".to_string(),
payment_token: None,
state: PaymentIntentState::Draft,
trust_level: TrustLevel::Known,
signature: "sha256=abc123".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
let json = serde_json::to_string(&intent).unwrap();
let parsed: PaymentIntent = serde_json::from_str(&json).unwrap();
assert_eq!(intent, parsed);
}
#[test]
fn test_payment_intent_optional_token_skipped() {
let intent = PaymentIntent {
id: Uuid::now_v7(),
sku: "SKU".to_string(),
amount: 100,
currency: "KES".to_string(),
payment_token: None,
state: PaymentIntentState::Draft,
trust_level: TrustLevel::Anonymous,
signature: "sig".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
let json = serde_json::to_string(&intent).unwrap();
assert!(!json.contains("paymentToken"));
}
#[test]
fn test_payment_policy_default() {
let policy = PaymentPolicy::default();
assert_eq!(policy.auto_approve_threshold, 5000);
assert_eq!(policy.require_approval_threshold, 50_000);
assert_eq!(policy.min_trust_for_auto_approve, TrustLevel::Known);
}
#[test]
fn test_payment_policy_auto_approve() {
let policy = PaymentPolicy::default();
assert_eq!(policy.evaluate(2500, TrustLevel::Known), PaymentPolicyDecision::AutoApprove);
}
#[test]
fn test_payment_policy_require_approval_high_amount() {
let policy = PaymentPolicy::default();
assert_eq!(
policy.evaluate(60_000, TrustLevel::Partner),
PaymentPolicyDecision::RequireApproval
);
}
#[test]
fn test_payment_policy_require_approval_low_trust() {
let policy = PaymentPolicy::default();
assert_eq!(
policy.evaluate(1000, TrustLevel::Anonymous),
PaymentPolicyDecision::RequireApproval
);
}
#[test]
fn test_payment_policy_serde_round_trip() {
let policy = PaymentPolicy {
auto_approve_threshold: 1000,
require_approval_threshold: 10_000,
min_trust_for_auto_approve: TrustLevel::Partner,
notification_channels: vec!["email".to_string(), "sms".to_string()],
};
let json = serde_json::to_string(&policy).unwrap();
let parsed: PaymentPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(policy, parsed);
}
}