use chrono::{DateTime, Duration, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::x402::{X402Asset, X402Network};
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum A2APaymentStatus {
#[default]
Pending,
Submitted,
Completed,
Failed,
Cancelled,
Refunded,
}
impl A2APaymentStatus {
#[must_use]
pub const fn allowed_transitions(self) -> &'static [Self] {
match self {
Self::Pending => &[Self::Submitted, Self::Cancelled],
Self::Submitted => &[Self::Completed, Self::Failed],
Self::Completed => &[Self::Refunded],
Self::Failed => &[Self::Pending],
Self::Cancelled | Self::Refunded => &[],
}
}
#[must_use]
pub fn can_transition_to(self, target: Self) -> bool {
self.allowed_transitions().contains(&target)
}
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Cancelled | Self::Refunded)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2APayment {
pub id: Uuid,
pub status: A2APaymentStatus,
pub sender_agent_id: Option<Uuid>,
pub sender_address: String,
pub recipient_agent_id: Option<Uuid>,
pub recipient_address: String,
pub amount: u64,
pub amount_decimal: Decimal,
pub asset: X402Asset,
pub network: X402Network,
pub memo: Option<String>,
pub reference_type: Option<A2AReferenceType>,
pub reference_id: Option<Uuid>,
pub idempotency_key: Option<String>,
pub intent_id: Option<Uuid>,
pub tx_hash: Option<String>,
pub block_number: Option<u64>,
pub metadata: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
impl A2APayment {
pub fn new(
sender_address: impl Into<String>,
recipient_address: impl Into<String>,
amount: u64,
asset: X402Asset,
) -> Self {
let now = Utc::now();
let decimals = asset.decimals();
let divisor = 10u64.pow(decimals as u32);
let amount_decimal = Decimal::from(amount) / Decimal::from(divisor);
Self {
id: Uuid::new_v4(),
status: A2APaymentStatus::Pending,
sender_agent_id: None,
sender_address: sender_address.into(),
recipient_agent_id: None,
recipient_address: recipient_address.into(),
amount,
amount_decimal,
asset,
network: X402Network::default(),
memo: None,
reference_type: None,
reference_id: None,
idempotency_key: None,
intent_id: None,
tx_hash: None,
block_number: None,
metadata: None,
created_at: now,
updated_at: now,
completed_at: None,
}
}
pub fn with_memo(mut self, memo: impl Into<String>) -> Self {
self.memo = Some(memo.into());
self
}
pub const fn with_network(mut self, network: X402Network) -> Self {
self.network = network;
self
}
pub const fn with_reference(mut self, ref_type: A2AReferenceType, ref_id: Uuid) -> Self {
self.reference_type = Some(ref_type);
self.reference_id = Some(ref_id);
self
}
pub fn complete(&mut self, tx_hash: Option<String>, block_number: Option<u64>) {
self.status = A2APaymentStatus::Completed;
self.tx_hash = tx_hash;
self.block_number = block_number;
self.completed_at = Some(Utc::now());
self.updated_at = Utc::now();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum A2AReferenceType {
Quote,
PaymentRequest,
Order,
Invoice,
ServiceCall,
Tip,
Refund,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PaymentRequestStatus {
#[default]
Pending,
Viewed,
Processing,
Paid,
Declined,
Expired,
Cancelled,
}
impl PaymentRequestStatus {
#[must_use]
pub const fn allowed_transitions(self) -> &'static [Self] {
match self {
Self::Pending => {
&[Self::Viewed, Self::Processing, Self::Declined, Self::Expired, Self::Cancelled]
}
Self::Viewed => &[Self::Processing, Self::Declined, Self::Expired, Self::Cancelled],
Self::Processing => &[Self::Paid, Self::Declined],
Self::Paid | Self::Declined | Self::Expired | Self::Cancelled => &[],
}
}
#[must_use]
pub fn can_transition_to(self, target: Self) -> bool {
self.allowed_transitions().contains(&target)
}
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Paid | Self::Declined | Self::Expired | Self::Cancelled)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentRequest {
pub id: Uuid,
pub status: PaymentRequestStatus,
pub requester_agent_id: Option<Uuid>,
pub requester_address: String,
pub payer_agent_id: Option<Uuid>,
pub payer_address: Option<String>,
pub amount: u64,
pub amount_decimal: Decimal,
pub asset: X402Asset,
pub accepted_networks: Vec<X402Network>,
pub description: String,
pub line_items: Option<String>,
pub reference_type: Option<A2AReferenceType>,
pub reference_id: Option<String>,
pub expires_at: DateTime<Utc>,
pub allow_partial: bool,
pub minimum_amount: Option<u64>,
pub amount_paid: u64,
pub payment_ids: Vec<Uuid>,
pub callback_url: Option<String>,
pub metadata: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub paid_at: Option<DateTime<Utc>>,
}
impl PaymentRequest {
pub fn new(
requester_address: impl Into<String>,
amount: u64,
asset: X402Asset,
description: impl Into<String>,
) -> Self {
let now = Utc::now();
let decimals = asset.decimals();
let divisor = 10u64.pow(decimals as u32);
let amount_decimal = Decimal::from(amount) / Decimal::from(divisor);
Self {
id: Uuid::new_v4(),
status: PaymentRequestStatus::Pending,
requester_agent_id: None,
requester_address: requester_address.into(),
payer_agent_id: None,
payer_address: None,
amount,
amount_decimal,
asset,
accepted_networks: vec![X402Network::default()],
description: description.into(),
line_items: None,
reference_type: None,
reference_id: None,
expires_at: now + Duration::hours(24),
allow_partial: false,
minimum_amount: None,
amount_paid: 0,
payment_ids: Vec::new(),
callback_url: None,
metadata: None,
created_at: now,
updated_at: now,
paid_at: None,
}
}
pub fn with_payer(mut self, payer_address: impl Into<String>) -> Self {
self.payer_address = Some(payer_address.into());
self
}
pub const fn with_expiry(mut self, expires_at: DateTime<Utc>) -> Self {
self.expires_at = expires_at;
self
}
pub const fn with_partial(mut self, minimum: Option<u64>) -> Self {
self.allow_partial = true;
self.minimum_amount = minimum;
self
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
pub const fn is_fully_paid(&self) -> bool {
self.amount_paid >= self.amount
}
pub fn record_payment(&mut self, payment_id: Uuid, amount: u64) {
self.amount_paid += amount;
self.payment_ids.push(payment_id);
self.updated_at = Utc::now();
if self.is_fully_paid() {
self.status = PaymentRequestStatus::Paid;
self.paid_at = Some(Utc::now());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum A2AQuoteStatus {
#[default]
Requested,
Quoted,
CounterOffered,
Accepted,
Declined,
Expired,
Fulfilled,
Cancelled,
}
impl A2AQuoteStatus {
#[must_use]
pub const fn allowed_transitions(self) -> &'static [Self] {
match self {
Self::Requested => &[Self::Quoted, Self::Cancelled, Self::Expired],
Self::Quoted => &[
Self::Accepted,
Self::CounterOffered,
Self::Declined,
Self::Expired,
Self::Cancelled,
],
Self::CounterOffered => {
&[Self::Quoted, Self::Accepted, Self::Declined, Self::Expired, Self::Cancelled]
}
Self::Accepted => &[Self::Fulfilled, Self::Cancelled],
Self::Declined | Self::Expired | Self::Fulfilled | Self::Cancelled => &[],
}
}
#[must_use]
pub fn can_transition_to(self, target: Self) -> bool {
self.allowed_transitions().contains(&target)
}
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Declined | Self::Expired | Self::Fulfilled | Self::Cancelled)
}
#[must_use]
pub const fn allows_negotiation(self) -> bool {
matches!(self, Self::Quoted | Self::CounterOffered)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum NegotiationType {
InitialQuote,
CounterOffer,
Revision,
Acceptance,
Decline,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NegotiationEntry {
pub round: u32,
pub initiated_by: String,
pub negotiation_type: NegotiationType,
pub proposed_total: u64,
pub message: Option<String>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CounterA2AQuote {
pub quote_id: Uuid,
pub proposed_total: u64,
pub proposed_fees: Option<u64>,
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviseA2AQuote {
pub quote_id: Uuid,
pub revised_total: u64,
pub revised_fees: Option<u64>,
pub revised_tax: Option<u64>,
pub expires_in_hours: Option<i64>,
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeclineA2AQuote {
pub quote_id: Uuid,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2AQuote {
pub id: Uuid,
pub status: A2AQuoteStatus,
pub buyer_agent_id: Option<Uuid>,
pub buyer_address: String,
pub seller_agent_id: Option<Uuid>,
pub seller_address: String,
pub items: Vec<A2AQuoteItem>,
pub subtotal: u64,
pub fees: u64,
pub tax: u64,
pub total: u64,
pub total_decimal: Decimal,
pub asset: X402Asset,
pub accepted_networks: Vec<X402Network>,
pub expires_at: DateTime<Utc>,
pub terms: Option<String>,
pub estimated_delivery: Option<String>,
pub delivery_method: Option<String>,
pub fulfillment_instructions: Option<String>,
pub payment_id: Option<Uuid>,
pub payment_request_id: Option<Uuid>,
pub counter_count: u32,
pub max_rounds: u32,
pub negotiation_history: Vec<NegotiationEntry>,
pub escrow_id: Option<Uuid>,
pub request_message: Option<String>,
pub response_message: Option<String>,
pub metadata: Option<String>,
pub created_at: DateTime<Utc>,
pub quoted_at: Option<DateTime<Utc>>,
pub accepted_at: Option<DateTime<Utc>>,
pub fulfilled_at: Option<DateTime<Utc>>,
pub updated_at: DateTime<Utc>,
}
impl A2AQuote {
pub fn request(
buyer_address: impl Into<String>,
seller_address: impl Into<String>,
items: Vec<A2AQuoteItem>,
asset: X402Asset,
) -> Self {
let now = Utc::now();
let subtotal: u64 = items.iter().map(|i| i.total()).sum();
let decimals = asset.decimals();
let divisor = 10u64.pow(decimals as u32);
Self {
id: Uuid::new_v4(),
status: A2AQuoteStatus::Requested,
buyer_agent_id: None,
buyer_address: buyer_address.into(),
seller_agent_id: None,
seller_address: seller_address.into(),
items,
subtotal,
fees: 0,
tax: 0,
total: subtotal,
total_decimal: Decimal::from(subtotal) / Decimal::from(divisor),
asset,
accepted_networks: vec![X402Network::default()],
expires_at: now + Duration::hours(24),
terms: None,
estimated_delivery: None,
delivery_method: None,
fulfillment_instructions: None,
payment_id: None,
payment_request_id: None,
counter_count: 0,
max_rounds: 5,
negotiation_history: Vec::new(),
escrow_id: None,
request_message: None,
response_message: None,
metadata: None,
created_at: now,
quoted_at: None,
accepted_at: None,
fulfilled_at: None,
updated_at: now,
}
}
pub fn provide_quote(&mut self, total: u64, fees: u64, tax: u64, expires_in_hours: i64) {
let decimals = self.asset.decimals();
let divisor = 10u64.pow(decimals as u32);
self.fees = fees;
self.tax = tax;
self.total = total;
self.total_decimal = Decimal::from(total) / Decimal::from(divisor);
self.status = A2AQuoteStatus::Quoted;
self.quoted_at = Some(Utc::now());
self.expires_at = Utc::now() + Duration::hours(expires_in_hours);
self.updated_at = Utc::now();
}
pub fn accept(&mut self) {
self.status = A2AQuoteStatus::Accepted;
self.accepted_at = Some(Utc::now());
self.updated_at = Utc::now();
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
pub fn fulfill(&mut self) {
self.status = A2AQuoteStatus::Fulfilled;
self.fulfilled_at = Some(Utc::now());
self.updated_at = Utc::now();
}
pub const fn with_max_rounds(mut self, max_rounds: u32) -> Self {
self.max_rounds = max_rounds;
self
}
pub const fn with_escrow(mut self, escrow_id: Uuid) -> Self {
self.escrow_id = Some(escrow_id);
self
}
pub fn counter_offer(&mut self, proposed_total: u64, message: Option<String>) -> bool {
if !self.status.allows_negotiation() {
return false;
}
if self.counter_count >= self.max_rounds {
return false;
}
self.counter_count += 1;
self.negotiation_history.push(NegotiationEntry {
round: self.counter_count,
initiated_by: self.buyer_address.clone(),
negotiation_type: NegotiationType::CounterOffer,
proposed_total,
message,
timestamp: Utc::now(),
});
self.status = A2AQuoteStatus::CounterOffered;
self.updated_at = Utc::now();
true
}
pub fn revise(
&mut self,
revised_total: u64,
fees: u64,
tax: u64,
expires_in_hours: i64,
message: Option<String>,
) {
let decimals = self.asset.decimals();
let divisor = 10u64.pow(decimals as u32);
self.negotiation_history.push(NegotiationEntry {
round: self.counter_count,
initiated_by: self.seller_address.clone(),
negotiation_type: NegotiationType::Revision,
proposed_total: revised_total,
message,
timestamp: Utc::now(),
});
self.fees = fees;
self.tax = tax;
self.total = revised_total;
self.total_decimal = Decimal::from(revised_total) / Decimal::from(divisor);
self.status = A2AQuoteStatus::Quoted;
self.expires_at = Utc::now() + Duration::hours(expires_in_hours);
self.updated_at = Utc::now();
}
pub fn decline(&mut self, reason: Option<String>) {
self.negotiation_history.push(NegotiationEntry {
round: self.counter_count,
initiated_by: self.buyer_address.clone(),
negotiation_type: NegotiationType::Decline,
proposed_total: self.total,
message: reason,
timestamp: Utc::now(),
});
self.status = A2AQuoteStatus::Declined;
self.updated_at = Utc::now();
}
#[must_use]
pub const fn is_negotiation_limit_reached(&self) -> bool {
self.counter_count >= self.max_rounds
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2AQuoteItem {
pub description: String,
pub sku: Option<String>,
pub quantity: u32,
pub unit_price: u64,
pub metadata: Option<String>,
}
impl A2AQuoteItem {
pub fn new(description: impl Into<String>, quantity: u32, unit_price: u64) -> Self {
Self { description: description.into(), sku: None, quantity, unit_price, metadata: None }
}
pub const fn total(&self) -> u64 {
self.unit_price * self.quantity as u64
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2AService {
pub id: Uuid,
pub agent_id: Uuid,
pub name: String,
pub description: String,
pub category: A2AServiceCategory,
pub pricing: A2APricing,
pub active: bool,
pub input_schema: Option<String>,
pub output_schema: Option<String>,
pub endpoint_url: Option<String>,
pub avg_response_time: Option<u32>,
pub success_rate: Option<f32>,
pub transaction_count: u64,
pub metadata: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum A2AServiceCategory {
Data,
Compute,
Api,
Content,
Analysis,
Goods,
DigitalGoods,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "model", rename_all = "snake_case")]
#[non_exhaustive]
pub enum A2APricing {
Fixed { amount: u64, asset: X402Asset, unit: String },
PerUnit { amount_per_unit: u64, asset: X402Asset, unit: String },
Tiered { tiers: Vec<PricingTier>, asset: X402Asset },
Quote,
Freemium { free_quota: u64, unit: String, overage_price: u64, asset: X402Asset },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PricingTier {
pub up_to: Option<u64>,
pub price_per_unit: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreateA2APayment {
pub recipient_agent_id: Option<Uuid>,
pub recipient_address: Option<String>,
pub amount: Option<u64>,
pub amount_decimal: Option<Decimal>,
pub asset: Option<X402Asset>,
pub network: Option<X402Network>,
pub memo: Option<String>,
pub reference_type: Option<A2AReferenceType>,
pub reference_id: Option<Uuid>,
pub idempotency_key: Option<String>,
pub metadata: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreatePaymentRequest {
pub payer_agent_id: Option<Uuid>,
pub payer_address: Option<String>,
pub amount: Option<u64>,
pub amount_decimal: Option<Decimal>,
pub asset: Option<X402Asset>,
pub accepted_networks: Option<Vec<X402Network>>,
pub description: String,
pub line_items: Option<Vec<PaymentRequestLineItem>>,
pub expires_in_hours: Option<i64>,
pub allow_partial: Option<bool>,
pub minimum_amount: Option<u64>,
pub callback_url: Option<String>,
pub metadata: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentRequestLineItem {
pub description: String,
pub quantity: u32,
pub unit_price: u64,
pub sku: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestA2AQuote {
pub seller_agent_id: Option<Uuid>,
pub seller_address: Option<String>,
pub items: Vec<QuoteItemRequest>,
pub asset: Option<X402Asset>,
pub message: Option<String>,
pub metadata: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuoteItemRequest {
pub description: String,
pub quantity: u32,
pub sku: Option<String>,
pub metadata: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvideA2AQuote {
pub quote_id: Uuid,
pub items: Option<Vec<A2AQuoteItem>>,
pub total: u64,
pub fees: Option<u64>,
pub tax: Option<u64>,
pub expires_in_hours: Option<i64>,
pub terms: Option<String>,
pub estimated_delivery: Option<String>,
pub message: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct A2APaymentFilter {
pub sender_address: Option<String>,
pub recipient_address: Option<String>,
pub sender_agent_id: Option<Uuid>,
pub recipient_agent_id: Option<Uuid>,
pub status: Option<A2APaymentStatus>,
pub asset: Option<X402Asset>,
pub network: Option<X402Network>,
pub reference_type: Option<A2AReferenceType>,
pub from_date: Option<DateTime<Utc>>,
pub to_date: Option<DateTime<Utc>>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PaymentRequestFilter {
pub requester_address: Option<String>,
pub payer_address: Option<String>,
pub status: Option<PaymentRequestStatus>,
pub include_expired: Option<bool>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct A2AQuoteFilter {
pub buyer_address: Option<String>,
pub seller_address: Option<String>,
pub buyer_agent_id: Option<Uuid>,
pub seller_agent_id: Option<Uuid>,
pub status: Option<A2AQuoteStatus>,
pub include_expired: Option<bool>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct A2AServiceFilter {
pub agent_id: Option<Uuid>,
pub category: Option<A2AServiceCategory>,
pub max_price: Option<u64>,
pub asset: Option<X402Asset>,
pub active_only: Option<bool>,
pub search: Option<String>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_a2a_payment_creation() {
let payment = A2APayment::new("0xsender", "0xrecipient", 1_000_000, X402Asset::Usdc)
.with_memo("Test payment");
assert_eq!(payment.amount, 1_000_000);
assert_eq!(payment.amount_decimal, Decimal::from(1));
assert_eq!(payment.status, A2APaymentStatus::Pending);
assert_eq!(payment.memo, Some("Test payment".to_string()));
}
#[test]
fn test_payment_request() {
let request =
PaymentRequest::new("0xrequester", 5_000_000, X402Asset::Usdc, "API access fee");
assert_eq!(request.amount, 5_000_000);
assert_eq!(request.amount_decimal, Decimal::from(5));
assert!(!request.is_expired());
assert!(!request.is_fully_paid());
}
#[test]
fn test_quote_flow() {
let items = vec![
A2AQuoteItem::new("Widget", 2, 500_000),
A2AQuoteItem::new("Service", 1, 1_000_000),
];
let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
assert_eq!(quote.status, A2AQuoteStatus::Requested);
assert_eq!(quote.subtotal, 2_000_000);
quote.provide_quote(2_100_000, 50_000, 50_000, 48);
assert_eq!(quote.status, A2AQuoteStatus::Quoted);
assert_eq!(quote.total, 2_100_000);
quote.accept();
assert_eq!(quote.status, A2AQuoteStatus::Accepted);
}
#[test]
fn payment_pending_can_go_to_submitted() {
assert!(A2APaymentStatus::Pending.can_transition_to(A2APaymentStatus::Submitted));
}
#[test]
fn payment_pending_can_go_to_cancelled() {
assert!(A2APaymentStatus::Pending.can_transition_to(A2APaymentStatus::Cancelled));
}
#[test]
fn payment_submitted_can_go_to_completed() {
assert!(A2APaymentStatus::Submitted.can_transition_to(A2APaymentStatus::Completed));
}
#[test]
fn payment_submitted_can_go_to_failed() {
assert!(A2APaymentStatus::Submitted.can_transition_to(A2APaymentStatus::Failed));
}
#[test]
fn payment_completed_can_go_to_refunded() {
assert!(A2APaymentStatus::Completed.can_transition_to(A2APaymentStatus::Refunded));
}
#[test]
fn payment_failed_can_retry() {
assert!(A2APaymentStatus::Failed.can_transition_to(A2APaymentStatus::Pending));
}
#[test]
fn payment_cancelled_is_terminal() {
assert!(A2APaymentStatus::Cancelled.is_terminal());
assert!(A2APaymentStatus::Cancelled.allowed_transitions().is_empty());
}
#[test]
fn payment_refunded_is_terminal() {
assert!(A2APaymentStatus::Refunded.is_terminal());
}
#[test]
fn payment_pending_is_not_terminal() {
assert!(!A2APaymentStatus::Pending.is_terminal());
}
#[test]
fn request_pending_can_go_to_viewed() {
assert!(PaymentRequestStatus::Pending.can_transition_to(PaymentRequestStatus::Viewed));
}
#[test]
fn request_viewed_can_go_to_processing() {
assert!(PaymentRequestStatus::Viewed.can_transition_to(PaymentRequestStatus::Processing));
}
#[test]
fn request_processing_can_go_to_paid() {
assert!(PaymentRequestStatus::Processing.can_transition_to(PaymentRequestStatus::Paid));
}
#[test]
fn request_paid_is_terminal() {
assert!(PaymentRequestStatus::Paid.is_terminal());
}
#[test]
fn request_declined_is_terminal() {
assert!(PaymentRequestStatus::Declined.is_terminal());
}
#[test]
fn request_expired_is_terminal() {
assert!(PaymentRequestStatus::Expired.is_terminal());
}
#[test]
fn request_cancelled_is_terminal() {
assert!(PaymentRequestStatus::Cancelled.is_terminal());
}
#[test]
fn quote_requested_can_go_to_quoted() {
assert!(A2AQuoteStatus::Requested.can_transition_to(A2AQuoteStatus::Quoted));
}
#[test]
fn quote_quoted_can_go_to_counter_offered() {
assert!(A2AQuoteStatus::Quoted.can_transition_to(A2AQuoteStatus::CounterOffered));
}
#[test]
fn quote_counter_offered_can_go_to_quoted() {
assert!(A2AQuoteStatus::CounterOffered.can_transition_to(A2AQuoteStatus::Quoted));
}
#[test]
fn quote_counter_offered_can_go_to_accepted() {
assert!(A2AQuoteStatus::CounterOffered.can_transition_to(A2AQuoteStatus::Accepted));
}
#[test]
fn quote_accepted_can_go_to_fulfilled() {
assert!(A2AQuoteStatus::Accepted.can_transition_to(A2AQuoteStatus::Fulfilled));
}
#[test]
fn quote_declined_is_terminal() {
assert!(A2AQuoteStatus::Declined.is_terminal());
}
#[test]
fn quote_fulfilled_is_terminal() {
assert!(A2AQuoteStatus::Fulfilled.is_terminal());
}
#[test]
fn quote_allows_negotiation() {
assert!(A2AQuoteStatus::Quoted.allows_negotiation());
assert!(A2AQuoteStatus::CounterOffered.allows_negotiation());
assert!(!A2AQuoteStatus::Requested.allows_negotiation());
assert!(!A2AQuoteStatus::Accepted.allows_negotiation());
}
#[test]
fn quote_counter_offered_display() {
assert_eq!(A2AQuoteStatus::CounterOffered.to_string(), "counter_offered");
}
#[test]
fn negotiation_counter_offer() {
let items = vec![A2AQuoteItem::new("Widget", 1, 1_000_000)];
let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
quote.provide_quote(1_100_000, 50_000, 50_000, 24);
assert_eq!(quote.status, A2AQuoteStatus::Quoted);
assert!(quote.counter_offer(900_000, Some("Too expensive".into())));
assert_eq!(quote.status, A2AQuoteStatus::CounterOffered);
assert_eq!(quote.counter_count, 1);
assert_eq!(quote.negotiation_history.len(), 1);
assert_eq!(quote.negotiation_history[0].negotiation_type, NegotiationType::CounterOffer);
}
#[test]
fn negotiation_revise() {
let items = vec![A2AQuoteItem::new("Service", 1, 500_000)];
let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
quote.provide_quote(600_000, 50_000, 50_000, 24);
quote.counter_offer(500_000, None);
quote.revise(550_000, 25_000, 25_000, 24, Some("Meet in the middle".into()));
assert_eq!(quote.status, A2AQuoteStatus::Quoted);
assert_eq!(quote.total, 550_000);
assert_eq!(quote.negotiation_history.len(), 2);
}
#[test]
fn negotiation_decline() {
let items = vec![A2AQuoteItem::new("Data", 1, 100_000)];
let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
quote.provide_quote(200_000, 0, 0, 24);
quote.decline(Some("Price too high".into()));
assert_eq!(quote.status, A2AQuoteStatus::Declined);
assert!(quote.status.is_terminal());
}
#[test]
fn negotiation_round_limit() {
let items = vec![A2AQuoteItem::new("Item", 1, 100)];
let mut quote =
A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc).with_max_rounds(2);
quote.provide_quote(200, 0, 0, 24);
assert!(quote.counter_offer(150, None));
quote.revise(175, 0, 0, 24, None);
assert!(quote.counter_offer(160, None));
quote.revise(165, 0, 0, 24, None);
assert!(!quote.counter_offer(163, None));
assert!(quote.is_negotiation_limit_reached());
}
#[test]
fn negotiation_not_allowed_in_wrong_state() {
let items = vec![A2AQuoteItem::new("Item", 1, 100)];
let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
assert!(!quote.counter_offer(50, None));
}
#[test]
fn with_escrow() {
let items = vec![A2AQuoteItem::new("Item", 1, 100)];
let escrow_id = Uuid::new_v4();
let quote =
A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc).with_escrow(escrow_id);
assert_eq!(quote.escrow_id, Some(escrow_id));
}
#[test]
fn default_max_rounds() {
let items = vec![A2AQuoteItem::new("Item", 1, 100)];
let quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
assert_eq!(quote.max_rounds, 5);
}
#[test]
fn full_negotiation_flow_to_acceptance() {
let items = vec![A2AQuoteItem::new("Consulting", 1, 10_000_000)];
let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
quote.provide_quote(12_000_000, 500_000, 500_000, 48);
quote.counter_offer(10_500_000, Some("Budget limited".into()));
quote.revise(11_000_000, 300_000, 200_000, 24, Some("Final offer".into()));
quote.accept();
assert_eq!(quote.status, A2AQuoteStatus::Accepted);
assert_eq!(quote.negotiation_history.len(), 2);
quote.fulfill();
assert_eq!(quote.status, A2AQuoteStatus::Fulfilled);
assert!(quote.status.is_terminal());
}
}