Skip to main content

stateset_core/models/
a2a.rs

1//! Agent-to-Agent (A2A) Commerce Models
2//!
3//! Core domain types for agent-to-agent payments, quotes, and commerce negotiations.
4//! Enables seamless value transfer between AI agents in the iCommerce ecosystem.
5//!
6//! ## A2A Payment Flow
7//!
8//! 1. **Direct Payment**: Agent A pays Agent B directly
9//! 2. **Payment Request**: Agent B requests payment from Agent A
10//! 3. **Quote Flow**: Agent A requests quote → Agent B provides quote → Agent A accepts & pays
11//!
12//! ## Example
13//!
14//! ```rust
15//! use stateset_core::models::a2a::A2APayment;
16//! use stateset_core::models::x402::X402Asset;
17//!
18//! // Direct payment between two agent wallets
19//! let payment = A2APayment::new(
20//!     "0x1234abcd1234abcd1234abcd1234abcd1234abcd",
21//!     "0x5678efab5678efab5678efab5678efab5678efab",
22//!     1_000_000, // 1 USDC (6 decimals)
23//!     X402Asset::Usdc,
24//! );
25//! assert_eq!(payment.amount, 1_000_000);
26//! ```
27
28use chrono::{DateTime, Duration, Utc};
29use rust_decimal::Decimal;
30use serde::{Deserialize, Serialize};
31use uuid::Uuid;
32
33use super::x402::{X402Asset, X402Network};
34
35// =============================================================================
36// A2A Payment (Direct Agent-to-Agent Transfer)
37// =============================================================================
38
39/// Status of an A2A payment
40#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
41#[strum(serialize_all = "snake_case")]
42#[serde(rename_all = "snake_case")]
43#[non_exhaustive]
44pub enum A2APaymentStatus {
45    /// Payment created, pending signature
46    #[default]
47    Pending,
48    /// Payment signed and submitted
49    Submitted,
50    /// Payment confirmed/settled
51    Completed,
52    /// Payment failed
53    Failed,
54    /// Payment cancelled by sender
55    Cancelled,
56    /// Payment refunded
57    Refunded,
58}
59
60impl A2APaymentStatus {
61    /// Return the set of states this status can transition to.
62    #[must_use]
63    pub const fn allowed_transitions(self) -> &'static [Self] {
64        match self {
65            Self::Pending => &[Self::Submitted, Self::Cancelled],
66            Self::Submitted => &[Self::Completed, Self::Failed],
67            Self::Completed => &[Self::Refunded],
68            Self::Failed => &[Self::Pending],
69            Self::Cancelled | Self::Refunded => &[],
70        }
71    }
72
73    /// Check whether a transition to `target` is valid.
74    #[must_use]
75    pub fn can_transition_to(self, target: Self) -> bool {
76        self.allowed_transitions().contains(&target)
77    }
78
79    /// Whether this status is terminal (no further transitions possible).
80    #[must_use]
81    pub const fn is_terminal(self) -> bool {
82        matches!(self, Self::Cancelled | Self::Refunded)
83    }
84}
85
86/// A2A Payment - Direct transfer between agents
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct A2APayment {
89    /// Unique payment ID
90    pub id: Uuid,
91
92    /// Current status
93    pub status: A2APaymentStatus,
94
95    // =========================================================================
96    // Participants
97    // =========================================================================
98    /// Sender agent ID (from `agent_cards`)
99    pub sender_agent_id: Option<Uuid>,
100
101    /// Sender wallet address
102    pub sender_address: String,
103
104    /// Recipient agent ID (from `agent_cards`)
105    pub recipient_agent_id: Option<Uuid>,
106
107    /// Recipient wallet address
108    pub recipient_address: String,
109
110    // =========================================================================
111    // Amount
112    // =========================================================================
113    /// Amount in smallest unit (e.g., 1000000 = 1 USDC)
114    pub amount: u64,
115
116    /// Human-readable amount
117    pub amount_decimal: Decimal,
118
119    /// Payment asset
120    pub asset: X402Asset,
121
122    /// Network for settlement
123    pub network: X402Network,
124
125    // =========================================================================
126    // Context
127    // =========================================================================
128    /// Human-readable memo/description
129    pub memo: Option<String>,
130
131    /// Reference to what this payment is for
132    pub reference_type: Option<A2AReferenceType>,
133
134    /// Reference ID (`quote_id`, `request_id`, `order_id`, etc.)
135    pub reference_id: Option<Uuid>,
136
137    /// Idempotency key for deduplication
138    pub idempotency_key: Option<String>,
139
140    // =========================================================================
141    // Settlement
142    // =========================================================================
143    /// Associated x402 payment intent ID
144    pub intent_id: Option<Uuid>,
145
146    /// On-chain transaction hash
147    pub tx_hash: Option<String>,
148
149    /// Block number where settled
150    pub block_number: Option<u64>,
151
152    // =========================================================================
153    // Metadata
154    // =========================================================================
155    /// Additional metadata (JSON)
156    pub metadata: Option<String>,
157
158    /// Created timestamp
159    pub created_at: DateTime<Utc>,
160
161    /// Updated timestamp
162    pub updated_at: DateTime<Utc>,
163
164    /// Completed timestamp
165    pub completed_at: Option<DateTime<Utc>>,
166}
167
168impl A2APayment {
169    /// Create a new A2A payment
170    pub fn new(
171        sender_address: impl Into<String>,
172        recipient_address: impl Into<String>,
173        amount: u64,
174        asset: X402Asset,
175    ) -> Self {
176        let now = Utc::now();
177        let decimals = asset.decimals();
178        let divisor = 10u64.pow(u32::from(decimals));
179        let amount_decimal = Decimal::from(amount) / Decimal::from(divisor);
180
181        Self {
182            id: Uuid::new_v4(),
183            status: A2APaymentStatus::Pending,
184            sender_agent_id: None,
185            sender_address: sender_address.into(),
186            recipient_agent_id: None,
187            recipient_address: recipient_address.into(),
188            amount,
189            amount_decimal,
190            asset,
191            network: X402Network::default(),
192            memo: None,
193            reference_type: None,
194            reference_id: None,
195            idempotency_key: None,
196            intent_id: None,
197            tx_hash: None,
198            block_number: None,
199            metadata: None,
200            created_at: now,
201            updated_at: now,
202            completed_at: None,
203        }
204    }
205
206    /// Set memo
207    pub fn with_memo(mut self, memo: impl Into<String>) -> Self {
208        self.memo = Some(memo.into());
209        self
210    }
211
212    /// Set network
213    #[must_use]
214    pub const fn with_network(mut self, network: X402Network) -> Self {
215        self.network = network;
216        self
217    }
218
219    /// Set reference
220    #[must_use]
221    pub const fn with_reference(mut self, ref_type: A2AReferenceType, ref_id: Uuid) -> Self {
222        self.reference_type = Some(ref_type);
223        self.reference_id = Some(ref_id);
224        self
225    }
226
227    /// Mark as completed
228    pub fn complete(&mut self, tx_hash: Option<String>, block_number: Option<u64>) {
229        self.status = A2APaymentStatus::Completed;
230        self.tx_hash = tx_hash;
231        self.block_number = block_number;
232        self.completed_at = Some(Utc::now());
233        self.updated_at = Utc::now();
234    }
235}
236
237/// Reference type for A2A payments
238#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize)]
239#[strum(serialize_all = "snake_case")]
240#[serde(rename_all = "snake_case")]
241#[non_exhaustive]
242pub enum A2AReferenceType {
243    /// Payment for a quote
244    Quote,
245    /// Payment for a payment request
246    PaymentRequest,
247    /// Payment for an order
248    Order,
249    /// Payment for an invoice
250    Invoice,
251    /// Payment for a service call
252    ServiceCall,
253    /// Tip/gratuity
254    Tip,
255    /// Refund
256    Refund,
257    /// Other
258    Other,
259}
260
261// =============================================================================
262// Payment Request (Agent Requests Payment from Another)
263// =============================================================================
264
265/// Status of a payment request
266#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
267#[strum(serialize_all = "snake_case")]
268#[serde(rename_all = "snake_case")]
269#[non_exhaustive]
270pub enum PaymentRequestStatus {
271    /// Request created, awaiting payment
272    #[default]
273    Pending,
274    /// Request viewed by payer
275    Viewed,
276    /// Payment in progress
277    Processing,
278    /// Payment completed
279    Paid,
280    /// Request declined by payer
281    Declined,
282    /// Request expired
283    Expired,
284    /// Request cancelled by requester
285    Cancelled,
286}
287
288impl PaymentRequestStatus {
289    /// Return the set of states this status can transition to.
290    #[must_use]
291    pub const fn allowed_transitions(self) -> &'static [Self] {
292        match self {
293            Self::Pending => {
294                &[Self::Viewed, Self::Processing, Self::Declined, Self::Expired, Self::Cancelled]
295            }
296            Self::Viewed => &[Self::Processing, Self::Declined, Self::Expired, Self::Cancelled],
297            Self::Processing => &[Self::Paid, Self::Declined],
298            Self::Paid | Self::Declined | Self::Expired | Self::Cancelled => &[],
299        }
300    }
301
302    /// Check whether a transition to `target` is valid.
303    #[must_use]
304    pub fn can_transition_to(self, target: Self) -> bool {
305        self.allowed_transitions().contains(&target)
306    }
307
308    /// Whether this status is terminal.
309    #[must_use]
310    pub const fn is_terminal(self) -> bool {
311        matches!(self, Self::Paid | Self::Declined | Self::Expired | Self::Cancelled)
312    }
313}
314
315/// Payment Request - Agent requests payment from another agent
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct PaymentRequest {
318    /// Unique request ID
319    pub id: Uuid,
320
321    /// Current status
322    pub status: PaymentRequestStatus,
323
324    // =========================================================================
325    // Participants
326    // =========================================================================
327    /// Requester (payee) agent ID
328    pub requester_agent_id: Option<Uuid>,
329
330    /// Requester wallet address
331    pub requester_address: String,
332
333    /// Payer agent ID (who should pay)
334    pub payer_agent_id: Option<Uuid>,
335
336    /// Payer wallet address (if known)
337    pub payer_address: Option<String>,
338
339    // =========================================================================
340    // Amount
341    // =========================================================================
342    /// Requested amount in smallest unit
343    pub amount: u64,
344
345    /// Human-readable amount
346    pub amount_decimal: Decimal,
347
348    /// Requested asset
349    pub asset: X402Asset,
350
351    /// Accepted networks
352    pub accepted_networks: Vec<X402Network>,
353
354    // =========================================================================
355    // Details
356    // =========================================================================
357    /// Description of what the payment is for
358    pub description: String,
359
360    /// Detailed line items (JSON array)
361    pub line_items: Option<String>,
362
363    /// Reference type
364    pub reference_type: Option<A2AReferenceType>,
365
366    /// External reference ID
367    pub reference_id: Option<String>,
368
369    // =========================================================================
370    // Validity
371    // =========================================================================
372    /// When the request expires
373    pub expires_at: DateTime<Utc>,
374
375    /// Whether partial payments are accepted
376    pub allow_partial: bool,
377
378    /// Minimum partial amount (if `allow_partial`)
379    pub minimum_amount: Option<u64>,
380
381    // =========================================================================
382    // Payment Tracking
383    // =========================================================================
384    /// Amount paid so far (for partial payments)
385    pub amount_paid: u64,
386
387    /// Associated A2A payment IDs
388    pub payment_ids: Vec<Uuid>,
389
390    // =========================================================================
391    // Callback
392    // =========================================================================
393    /// Webhook URL to notify on payment
394    pub callback_url: Option<String>,
395
396    // =========================================================================
397    // Metadata
398    // =========================================================================
399    /// Additional metadata
400    pub metadata: Option<String>,
401
402    /// Created timestamp
403    pub created_at: DateTime<Utc>,
404
405    /// Updated timestamp
406    pub updated_at: DateTime<Utc>,
407
408    /// Paid timestamp
409    pub paid_at: Option<DateTime<Utc>>,
410}
411
412impl PaymentRequest {
413    /// Create a new payment request
414    pub fn new(
415        requester_address: impl Into<String>,
416        amount: u64,
417        asset: X402Asset,
418        description: impl Into<String>,
419    ) -> Self {
420        let now = Utc::now();
421        let decimals = asset.decimals();
422        let divisor = 10u64.pow(u32::from(decimals));
423        let amount_decimal = Decimal::from(amount) / Decimal::from(divisor);
424
425        Self {
426            id: Uuid::new_v4(),
427            status: PaymentRequestStatus::Pending,
428            requester_agent_id: None,
429            requester_address: requester_address.into(),
430            payer_agent_id: None,
431            payer_address: None,
432            amount,
433            amount_decimal,
434            asset,
435            accepted_networks: vec![X402Network::default()],
436            description: description.into(),
437            line_items: None,
438            reference_type: None,
439            reference_id: None,
440            expires_at: now + Duration::hours(24),
441            allow_partial: false,
442            minimum_amount: None,
443            amount_paid: 0,
444            payment_ids: Vec::new(),
445            callback_url: None,
446            metadata: None,
447            created_at: now,
448            updated_at: now,
449            paid_at: None,
450        }
451    }
452
453    /// Set payer
454    pub fn with_payer(mut self, payer_address: impl Into<String>) -> Self {
455        self.payer_address = Some(payer_address.into());
456        self
457    }
458
459    /// Set expiry
460    #[must_use]
461    pub const fn with_expiry(mut self, expires_at: DateTime<Utc>) -> Self {
462        self.expires_at = expires_at;
463        self
464    }
465
466    /// Allow partial payments
467    #[must_use]
468    pub const fn with_partial(mut self, minimum: Option<u64>) -> Self {
469        self.allow_partial = true;
470        self.minimum_amount = minimum;
471        self
472    }
473
474    /// Check if expired
475    #[must_use]
476    pub fn is_expired(&self) -> bool {
477        Utc::now() > self.expires_at
478    }
479
480    /// Check if fully paid
481    #[must_use]
482    pub const fn is_fully_paid(&self) -> bool {
483        self.amount_paid >= self.amount
484    }
485
486    /// Record a payment
487    pub fn record_payment(&mut self, payment_id: Uuid, amount: u64) {
488        self.amount_paid += amount;
489        self.payment_ids.push(payment_id);
490        self.updated_at = Utc::now();
491
492        if self.is_fully_paid() {
493            self.status = PaymentRequestStatus::Paid;
494            self.paid_at = Some(Utc::now());
495        }
496    }
497}
498
499// =============================================================================
500// A2A Quote (Price Quote for Goods/Services)
501// =============================================================================
502
503/// Status of a quote
504#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
505#[strum(serialize_all = "snake_case")]
506#[serde(rename_all = "snake_case")]
507#[non_exhaustive]
508pub enum A2AQuoteStatus {
509    /// Quote requested, awaiting response
510    #[default]
511    Requested,
512    /// Quote provided by seller
513    Quoted,
514    /// Counter-offer made by buyer
515    CounterOffered,
516    /// Quote accepted by buyer
517    Accepted,
518    /// Quote declined by buyer
519    Declined,
520    /// Quote expired
521    Expired,
522    /// Quote fulfilled (paid and delivered)
523    Fulfilled,
524    /// Quote cancelled
525    Cancelled,
526}
527
528impl A2AQuoteStatus {
529    /// Return the set of states this status can transition to.
530    #[must_use]
531    pub const fn allowed_transitions(self) -> &'static [Self] {
532        match self {
533            Self::Requested => &[Self::Quoted, Self::Cancelled, Self::Expired],
534            Self::Quoted => &[
535                Self::Accepted,
536                Self::CounterOffered,
537                Self::Declined,
538                Self::Expired,
539                Self::Cancelled,
540            ],
541            Self::CounterOffered => {
542                &[Self::Quoted, Self::Accepted, Self::Declined, Self::Expired, Self::Cancelled]
543            }
544            Self::Accepted => &[Self::Fulfilled, Self::Cancelled],
545            Self::Declined | Self::Expired | Self::Fulfilled | Self::Cancelled => &[],
546        }
547    }
548
549    /// Check whether a transition to `target` is valid.
550    #[must_use]
551    pub fn can_transition_to(self, target: Self) -> bool {
552        self.allowed_transitions().contains(&target)
553    }
554
555    /// Whether this status is terminal.
556    #[must_use]
557    pub const fn is_terminal(self) -> bool {
558        matches!(self, Self::Declined | Self::Expired | Self::Fulfilled | Self::Cancelled)
559    }
560
561    /// Whether this status allows negotiation (counter-offers).
562    #[must_use]
563    pub const fn allows_negotiation(self) -> bool {
564        matches!(self, Self::Quoted | Self::CounterOffered)
565    }
566}
567
568// =============================================================================
569// Negotiation Types
570// =============================================================================
571
572/// Type of negotiation action.
573#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize)]
574#[strum(serialize_all = "snake_case")]
575#[serde(rename_all = "snake_case")]
576#[non_exhaustive]
577pub enum NegotiationType {
578    /// Initial quote from seller.
579    InitialQuote,
580    /// Counter-offer from buyer.
581    CounterOffer,
582    /// Revised quote from seller (in response to counter).
583    Revision,
584    /// Acceptance.
585    Acceptance,
586    /// Decline.
587    Decline,
588}
589
590/// A single entry in the negotiation history.
591#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct NegotiationEntry {
593    /// Round number (1-based).
594    pub round: u32,
595    /// Who initiated this action (address).
596    pub initiated_by: String,
597    /// Type of negotiation action.
598    pub negotiation_type: NegotiationType,
599    /// Proposed total amount.
600    pub proposed_total: u64,
601    /// Optional message.
602    pub message: Option<String>,
603    /// Timestamp.
604    pub timestamp: DateTime<Utc>,
605}
606
607/// Input for counter-offering a quote.
608#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct CounterA2AQuote {
610    /// Quote ID to counter.
611    pub quote_id: Uuid,
612    /// Counter-proposed total.
613    pub proposed_total: u64,
614    /// Counter-proposed fees.
615    pub proposed_fees: Option<u64>,
616    /// Message to seller.
617    pub message: Option<String>,
618}
619
620/// Input for revising a quote (seller response to counter-offer).
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct ReviseA2AQuote {
623    /// Quote ID to revise.
624    pub quote_id: Uuid,
625    /// Revised total.
626    pub revised_total: u64,
627    /// Revised fees.
628    pub revised_fees: Option<u64>,
629    /// Revised tax.
630    pub revised_tax: Option<u64>,
631    /// Expiry (hours from now).
632    pub expires_in_hours: Option<i64>,
633    /// Message to buyer.
634    pub message: Option<String>,
635}
636
637/// Input for declining a quote.
638#[derive(Debug, Clone, Serialize, Deserialize)]
639pub struct DeclineA2AQuote {
640    /// Quote ID to decline.
641    pub quote_id: Uuid,
642    /// Reason for declining.
643    pub reason: Option<String>,
644}
645
646/// A2A Quote - Price quote between agents
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct A2AQuote {
649    /// Unique quote ID
650    pub id: Uuid,
651
652    /// Current status
653    pub status: A2AQuoteStatus,
654
655    // =========================================================================
656    // Participants
657    // =========================================================================
658    /// Buyer agent ID
659    pub buyer_agent_id: Option<Uuid>,
660
661    /// Buyer wallet address
662    pub buyer_address: String,
663
664    /// Seller agent ID
665    pub seller_agent_id: Option<Uuid>,
666
667    /// Seller wallet address
668    pub seller_address: String,
669
670    // =========================================================================
671    // Quote Details
672    // =========================================================================
673    /// Line items
674    pub items: Vec<A2AQuoteItem>,
675
676    /// Subtotal (sum of line items)
677    pub subtotal: u64,
678
679    /// Fees (platform, processing, etc.)
680    pub fees: u64,
681
682    /// Tax amount
683    pub tax: u64,
684
685    /// Total amount
686    pub total: u64,
687
688    /// Human-readable total
689    pub total_decimal: Decimal,
690
691    /// Quote currency/asset
692    pub asset: X402Asset,
693
694    /// Accepted networks for payment
695    pub accepted_networks: Vec<X402Network>,
696
697    // =========================================================================
698    // Validity
699    // =========================================================================
700    /// When the quote expires
701    pub expires_at: DateTime<Utc>,
702
703    /// Terms and conditions
704    pub terms: Option<String>,
705
706    // =========================================================================
707    // Fulfillment
708    // =========================================================================
709    /// Estimated delivery time (ISO 8601 duration)
710    pub estimated_delivery: Option<String>,
711
712    /// Delivery method
713    pub delivery_method: Option<String>,
714
715    /// Fulfillment instructions (for seller)
716    pub fulfillment_instructions: Option<String>,
717
718    // =========================================================================
719    // Payment
720    // =========================================================================
721    /// Associated payment ID (when accepted and paid)
722    pub payment_id: Option<Uuid>,
723
724    /// Associated payment request ID
725    pub payment_request_id: Option<Uuid>,
726
727    // =========================================================================
728    // Negotiation
729    // =========================================================================
730    /// Number of counter-offers made.
731    pub counter_count: u32,
732
733    /// Maximum negotiation rounds allowed (default: 5).
734    pub max_rounds: u32,
735
736    /// Negotiation history entries.
737    pub negotiation_history: Vec<NegotiationEntry>,
738
739    /// Associated escrow ID (if escrow-backed).
740    pub escrow_id: Option<Uuid>,
741
742    // =========================================================================
743    // Metadata
744    // =========================================================================
745    /// Request message from buyer
746    pub request_message: Option<String>,
747
748    /// Response message from seller
749    pub response_message: Option<String>,
750
751    /// Additional metadata
752    pub metadata: Option<String>,
753
754    /// Created timestamp
755    pub created_at: DateTime<Utc>,
756
757    /// Quoted timestamp (when seller responds)
758    pub quoted_at: Option<DateTime<Utc>>,
759
760    /// Accepted timestamp
761    pub accepted_at: Option<DateTime<Utc>>,
762
763    /// Fulfilled timestamp
764    pub fulfilled_at: Option<DateTime<Utc>>,
765
766    /// Updated timestamp
767    pub updated_at: DateTime<Utc>,
768}
769
770impl A2AQuote {
771    /// Create a new quote request
772    pub fn request(
773        buyer_address: impl Into<String>,
774        seller_address: impl Into<String>,
775        items: Vec<A2AQuoteItem>,
776        asset: X402Asset,
777    ) -> Self {
778        let now = Utc::now();
779        let subtotal: u64 = items.iter().map(A2AQuoteItem::total).sum();
780        let decimals = asset.decimals();
781        let divisor = 10u64.pow(u32::from(decimals));
782
783        Self {
784            id: Uuid::new_v4(),
785            status: A2AQuoteStatus::Requested,
786            buyer_agent_id: None,
787            buyer_address: buyer_address.into(),
788            seller_agent_id: None,
789            seller_address: seller_address.into(),
790            items,
791            subtotal,
792            fees: 0,
793            tax: 0,
794            total: subtotal,
795            total_decimal: Decimal::from(subtotal) / Decimal::from(divisor),
796            asset,
797            accepted_networks: vec![X402Network::default()],
798            expires_at: now + Duration::hours(24),
799            terms: None,
800            estimated_delivery: None,
801            delivery_method: None,
802            fulfillment_instructions: None,
803            payment_id: None,
804            payment_request_id: None,
805            counter_count: 0,
806            max_rounds: 5,
807            negotiation_history: Vec::new(),
808            escrow_id: None,
809            request_message: None,
810            response_message: None,
811            metadata: None,
812            created_at: now,
813            quoted_at: None,
814            accepted_at: None,
815            fulfilled_at: None,
816            updated_at: now,
817        }
818    }
819
820    /// Seller provides quote (updates pricing)
821    pub fn provide_quote(&mut self, total: u64, fees: u64, tax: u64, expires_in_hours: i64) {
822        let decimals = self.asset.decimals();
823        let divisor = 10u64.pow(u32::from(decimals));
824
825        self.fees = fees;
826        self.tax = tax;
827        self.total = total;
828        self.total_decimal = Decimal::from(total) / Decimal::from(divisor);
829        self.status = A2AQuoteStatus::Quoted;
830        self.quoted_at = Some(Utc::now());
831        self.expires_at = Utc::now() + Duration::hours(expires_in_hours);
832        self.updated_at = Utc::now();
833    }
834
835    /// Buyer accepts quote
836    pub fn accept(&mut self) {
837        self.status = A2AQuoteStatus::Accepted;
838        self.accepted_at = Some(Utc::now());
839        self.updated_at = Utc::now();
840    }
841
842    /// Check if expired
843    #[must_use]
844    pub fn is_expired(&self) -> bool {
845        Utc::now() > self.expires_at
846    }
847
848    /// Mark as fulfilled
849    pub fn fulfill(&mut self) {
850        self.status = A2AQuoteStatus::Fulfilled;
851        self.fulfilled_at = Some(Utc::now());
852        self.updated_at = Utc::now();
853    }
854
855    // =========================================================================
856    // Negotiation Methods
857    // =========================================================================
858
859    /// Set maximum negotiation rounds.
860    #[must_use]
861    pub const fn with_max_rounds(mut self, max_rounds: u32) -> Self {
862        self.max_rounds = max_rounds;
863        self
864    }
865
866    /// Link this quote to an escrow.
867    #[must_use]
868    pub const fn with_escrow(mut self, escrow_id: Uuid) -> Self {
869        self.escrow_id = Some(escrow_id);
870        self
871    }
872
873    /// Buyer counter-offers on the quote.
874    ///
875    /// Returns `false` if the round limit would be exceeded or the status
876    /// doesn't allow negotiation.
877    pub fn counter_offer(&mut self, proposed_total: u64, message: Option<String>) -> bool {
878        if !self.status.allows_negotiation() {
879            return false;
880        }
881        if self.counter_count >= self.max_rounds {
882            return false;
883        }
884
885        self.counter_count += 1;
886        self.negotiation_history.push(NegotiationEntry {
887            round: self.counter_count,
888            initiated_by: self.buyer_address.clone(),
889            negotiation_type: NegotiationType::CounterOffer,
890            proposed_total,
891            message,
892            timestamp: Utc::now(),
893        });
894        self.status = A2AQuoteStatus::CounterOffered;
895        self.updated_at = Utc::now();
896        true
897    }
898
899    /// Seller revises the quote in response to a counter-offer.
900    pub fn revise(
901        &mut self,
902        revised_total: u64,
903        fees: u64,
904        tax: u64,
905        expires_in_hours: i64,
906        message: Option<String>,
907    ) {
908        let decimals = self.asset.decimals();
909        let divisor = 10u64.pow(u32::from(decimals));
910
911        self.negotiation_history.push(NegotiationEntry {
912            round: self.counter_count,
913            initiated_by: self.seller_address.clone(),
914            negotiation_type: NegotiationType::Revision,
915            proposed_total: revised_total,
916            message,
917            timestamp: Utc::now(),
918        });
919
920        self.fees = fees;
921        self.tax = tax;
922        self.total = revised_total;
923        self.total_decimal = Decimal::from(revised_total) / Decimal::from(divisor);
924        self.status = A2AQuoteStatus::Quoted;
925        self.expires_at = Utc::now() + Duration::hours(expires_in_hours);
926        self.updated_at = Utc::now();
927    }
928
929    /// Decline the quote with an optional reason.
930    pub fn decline(&mut self, reason: Option<String>) {
931        self.negotiation_history.push(NegotiationEntry {
932            round: self.counter_count,
933            initiated_by: self.buyer_address.clone(),
934            negotiation_type: NegotiationType::Decline,
935            proposed_total: self.total,
936            message: reason,
937            timestamp: Utc::now(),
938        });
939        self.status = A2AQuoteStatus::Declined;
940        self.updated_at = Utc::now();
941    }
942
943    /// Check if the negotiation round limit has been reached.
944    #[must_use]
945    pub const fn is_negotiation_limit_reached(&self) -> bool {
946        self.counter_count >= self.max_rounds
947    }
948}
949
950/// Line item in a quote
951#[derive(Debug, Clone, Serialize, Deserialize)]
952pub struct A2AQuoteItem {
953    /// Item description
954    pub description: String,
955
956    /// SKU or service code
957    pub sku: Option<String>,
958
959    /// Quantity
960    pub quantity: u32,
961
962    /// Unit price in smallest unit
963    pub unit_price: u64,
964
965    /// Item metadata
966    pub metadata: Option<String>,
967}
968
969impl A2AQuoteItem {
970    /// Create a new quote item
971    pub fn new(description: impl Into<String>, quantity: u32, unit_price: u64) -> Self {
972        Self { description: description.into(), sku: None, quantity, unit_price, metadata: None }
973    }
974
975    /// Calculate total for this line item
976    #[must_use]
977    pub const fn total(&self) -> u64 {
978        self.unit_price * self.quantity as u64
979    }
980}
981
982// =============================================================================
983// A2A Service Listing (Agent Advertises Services)
984// =============================================================================
985
986/// A2A Service - A service offered by an agent
987#[derive(Debug, Clone, Serialize, Deserialize)]
988pub struct A2AService {
989    /// Unique service ID
990    pub id: Uuid,
991
992    /// Agent ID offering this service
993    pub agent_id: Uuid,
994
995    /// Service name
996    pub name: String,
997
998    /// Service description
999    pub description: String,
1000
1001    /// Service category
1002    pub category: A2AServiceCategory,
1003
1004    /// Pricing model
1005    pub pricing: A2APricing,
1006
1007    /// Whether the service is active
1008    pub active: bool,
1009
1010    /// Supported input formats
1011    pub input_schema: Option<String>,
1012
1013    /// Output format description
1014    pub output_schema: Option<String>,
1015
1016    /// Service endpoint (if applicable)
1017    pub endpoint_url: Option<String>,
1018
1019    /// Average response time (seconds)
1020    pub avg_response_time: Option<u32>,
1021
1022    /// Success rate (0.0 - 1.0)
1023    pub success_rate: Option<f32>,
1024
1025    /// Number of completed transactions
1026    pub transaction_count: u64,
1027
1028    /// Additional metadata
1029    pub metadata: Option<String>,
1030
1031    /// Created timestamp
1032    pub created_at: DateTime<Utc>,
1033
1034    /// Updated timestamp
1035    pub updated_at: DateTime<Utc>,
1036}
1037
1038/// Service categories
1039#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1040#[serde(rename_all = "snake_case")]
1041#[non_exhaustive]
1042pub enum A2AServiceCategory {
1043    /// Data/information services
1044    Data,
1045    /// Computation/processing services
1046    Compute,
1047    /// API access
1048    Api,
1049    /// Content generation
1050    Content,
1051    /// Analysis/insights
1052    Analysis,
1053    /// Physical goods
1054    Goods,
1055    /// Digital goods
1056    DigitalGoods,
1057    /// Other services
1058    Other,
1059}
1060
1061/// Pricing models for services
1062#[derive(Debug, Clone, Serialize, Deserialize)]
1063#[serde(tag = "model", rename_all = "snake_case")]
1064#[non_exhaustive]
1065pub enum A2APricing {
1066    /// Fixed price per unit/call
1067    Fixed { amount: u64, asset: X402Asset, unit: String },
1068    /// Price per token/byte/etc.
1069    PerUnit { amount_per_unit: u64, asset: X402Asset, unit: String },
1070    /// Tiered pricing
1071    Tiered { tiers: Vec<PricingTier>, asset: X402Asset },
1072    /// Custom/quote required
1073    Quote,
1074    /// Free tier available
1075    Freemium { free_quota: u64, unit: String, overage_price: u64, asset: X402Asset },
1076}
1077
1078/// Pricing tier for tiered pricing
1079#[derive(Debug, Clone, Serialize, Deserialize)]
1080pub struct PricingTier {
1081    pub up_to: Option<u64>,
1082    pub price_per_unit: u64,
1083}
1084
1085// =============================================================================
1086// Input Types for A2A Operations
1087// =============================================================================
1088
1089/// Input for creating an A2A payment
1090#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1091pub struct CreateA2APayment {
1092    /// Recipient agent ID or wallet address (one required)
1093    pub recipient_agent_id: Option<Uuid>,
1094    pub recipient_address: Option<String>,
1095
1096    /// Amount in smallest unit (or use `amount_decimal`)
1097    pub amount: Option<u64>,
1098    pub amount_decimal: Option<Decimal>,
1099
1100    /// Asset (default: USDC)
1101    pub asset: Option<X402Asset>,
1102
1103    /// Network (default: Set Chain)
1104    pub network: Option<X402Network>,
1105
1106    /// Memo/description
1107    pub memo: Option<String>,
1108
1109    /// Reference
1110    pub reference_type: Option<A2AReferenceType>,
1111    pub reference_id: Option<Uuid>,
1112
1113    /// Idempotency key
1114    pub idempotency_key: Option<String>,
1115
1116    /// Additional metadata
1117    pub metadata: Option<String>,
1118}
1119
1120/// Input for creating a payment request
1121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1122pub struct CreatePaymentRequest {
1123    /// Payer agent ID or wallet (optional - can be open request)
1124    pub payer_agent_id: Option<Uuid>,
1125    pub payer_address: Option<String>,
1126
1127    /// Amount
1128    pub amount: Option<u64>,
1129    pub amount_decimal: Option<Decimal>,
1130
1131    /// Asset
1132    pub asset: Option<X402Asset>,
1133
1134    /// Networks accepted
1135    pub accepted_networks: Option<Vec<X402Network>>,
1136
1137    /// Description (required)
1138    pub description: String,
1139
1140    /// Line items
1141    pub line_items: Option<Vec<PaymentRequestLineItem>>,
1142
1143    /// Expiry (hours from now, default 24)
1144    pub expires_in_hours: Option<i64>,
1145
1146    /// Allow partial payments
1147    pub allow_partial: Option<bool>,
1148    pub minimum_amount: Option<u64>,
1149
1150    /// Callback URL
1151    pub callback_url: Option<String>,
1152
1153    /// Metadata
1154    pub metadata: Option<String>,
1155}
1156
1157/// Line item for payment request
1158#[derive(Debug, Clone, Serialize, Deserialize)]
1159pub struct PaymentRequestLineItem {
1160    pub description: String,
1161    pub quantity: u32,
1162    pub unit_price: u64,
1163    pub sku: Option<String>,
1164}
1165
1166/// Input for requesting a quote
1167#[derive(Debug, Clone, Serialize, Deserialize)]
1168pub struct RequestA2AQuote {
1169    /// Seller agent ID or wallet
1170    pub seller_agent_id: Option<Uuid>,
1171    pub seller_address: Option<String>,
1172
1173    /// Items to quote
1174    pub items: Vec<QuoteItemRequest>,
1175
1176    /// Preferred asset
1177    pub asset: Option<X402Asset>,
1178
1179    /// Message to seller
1180    pub message: Option<String>,
1181
1182    /// Metadata
1183    pub metadata: Option<String>,
1184}
1185
1186/// Item in a quote request
1187#[derive(Debug, Clone, Serialize, Deserialize)]
1188pub struct QuoteItemRequest {
1189    pub description: String,
1190    pub quantity: u32,
1191    pub sku: Option<String>,
1192    pub metadata: Option<String>,
1193}
1194
1195/// Input for responding to a quote
1196#[derive(Debug, Clone, Serialize, Deserialize)]
1197pub struct ProvideA2AQuote {
1198    /// Quote ID to respond to
1199    pub quote_id: Uuid,
1200
1201    /// Updated items with pricing
1202    pub items: Option<Vec<A2AQuoteItem>>,
1203
1204    /// Total amount
1205    pub total: u64,
1206
1207    /// Fees
1208    pub fees: Option<u64>,
1209
1210    /// Tax
1211    pub tax: Option<u64>,
1212
1213    /// Expiry (hours from now)
1214    pub expires_in_hours: Option<i64>,
1215
1216    /// Terms
1217    pub terms: Option<String>,
1218
1219    /// Estimated delivery
1220    pub estimated_delivery: Option<String>,
1221
1222    /// Message to buyer
1223    pub message: Option<String>,
1224}
1225
1226// =============================================================================
1227// Filter Types
1228// =============================================================================
1229
1230/// Filter for listing A2A payments
1231#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1232pub struct A2APaymentFilter {
1233    pub sender_address: Option<String>,
1234    pub recipient_address: Option<String>,
1235    pub sender_agent_id: Option<Uuid>,
1236    pub recipient_agent_id: Option<Uuid>,
1237    pub status: Option<A2APaymentStatus>,
1238    pub asset: Option<X402Asset>,
1239    pub network: Option<X402Network>,
1240    pub reference_type: Option<A2AReferenceType>,
1241    pub from_date: Option<DateTime<Utc>>,
1242    pub to_date: Option<DateTime<Utc>>,
1243    pub limit: Option<u32>,
1244    pub offset: Option<u32>,
1245}
1246
1247/// Filter for listing payment requests
1248#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1249pub struct PaymentRequestFilter {
1250    pub requester_address: Option<String>,
1251    pub payer_address: Option<String>,
1252    pub status: Option<PaymentRequestStatus>,
1253    pub include_expired: Option<bool>,
1254    pub limit: Option<u32>,
1255    pub offset: Option<u32>,
1256}
1257
1258/// Filter for listing quotes
1259#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1260pub struct A2AQuoteFilter {
1261    pub buyer_address: Option<String>,
1262    pub seller_address: Option<String>,
1263    pub buyer_agent_id: Option<Uuid>,
1264    pub seller_agent_id: Option<Uuid>,
1265    pub status: Option<A2AQuoteStatus>,
1266    pub include_expired: Option<bool>,
1267    pub limit: Option<u32>,
1268    pub offset: Option<u32>,
1269}
1270
1271/// Filter for discovering services
1272#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1273pub struct A2AServiceFilter {
1274    pub agent_id: Option<Uuid>,
1275    pub category: Option<A2AServiceCategory>,
1276    pub max_price: Option<u64>,
1277    pub asset: Option<X402Asset>,
1278    pub active_only: Option<bool>,
1279    pub search: Option<String>,
1280    pub limit: Option<u32>,
1281    pub offset: Option<u32>,
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286    use super::*;
1287
1288    #[test]
1289    fn test_a2a_payment_creation() {
1290        let payment = A2APayment::new("0xsender", "0xrecipient", 1_000_000, X402Asset::Usdc)
1291            .with_memo("Test payment");
1292
1293        assert_eq!(payment.amount, 1_000_000);
1294        assert_eq!(payment.amount_decimal, Decimal::from(1));
1295        assert_eq!(payment.status, A2APaymentStatus::Pending);
1296        assert_eq!(payment.memo, Some("Test payment".to_string()));
1297    }
1298
1299    #[test]
1300    fn test_payment_request() {
1301        let request =
1302            PaymentRequest::new("0xrequester", 5_000_000, X402Asset::Usdc, "API access fee");
1303
1304        assert_eq!(request.amount, 5_000_000);
1305        assert_eq!(request.amount_decimal, Decimal::from(5));
1306        assert!(!request.is_expired());
1307        assert!(!request.is_fully_paid());
1308    }
1309
1310    #[test]
1311    fn test_quote_flow() {
1312        let items = vec![
1313            A2AQuoteItem::new("Widget", 2, 500_000),
1314            A2AQuoteItem::new("Service", 1, 1_000_000),
1315        ];
1316
1317        let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
1318        assert_eq!(quote.status, A2AQuoteStatus::Requested);
1319        assert_eq!(quote.subtotal, 2_000_000); // 2*500000 + 1*1000000
1320
1321        // Seller provides quote
1322        quote.provide_quote(2_100_000, 50_000, 50_000, 48);
1323        assert_eq!(quote.status, A2AQuoteStatus::Quoted);
1324        assert_eq!(quote.total, 2_100_000);
1325
1326        // Buyer accepts
1327        quote.accept();
1328        assert_eq!(quote.status, A2AQuoteStatus::Accepted);
1329    }
1330
1331    // =========================================================================
1332    // A2APaymentStatus state machine
1333    // =========================================================================
1334
1335    #[test]
1336    fn payment_pending_can_go_to_submitted() {
1337        assert!(A2APaymentStatus::Pending.can_transition_to(A2APaymentStatus::Submitted));
1338    }
1339
1340    #[test]
1341    fn payment_pending_can_go_to_cancelled() {
1342        assert!(A2APaymentStatus::Pending.can_transition_to(A2APaymentStatus::Cancelled));
1343    }
1344
1345    #[test]
1346    fn payment_submitted_can_go_to_completed() {
1347        assert!(A2APaymentStatus::Submitted.can_transition_to(A2APaymentStatus::Completed));
1348    }
1349
1350    #[test]
1351    fn payment_submitted_can_go_to_failed() {
1352        assert!(A2APaymentStatus::Submitted.can_transition_to(A2APaymentStatus::Failed));
1353    }
1354
1355    #[test]
1356    fn payment_completed_can_go_to_refunded() {
1357        assert!(A2APaymentStatus::Completed.can_transition_to(A2APaymentStatus::Refunded));
1358    }
1359
1360    #[test]
1361    fn payment_failed_can_retry() {
1362        assert!(A2APaymentStatus::Failed.can_transition_to(A2APaymentStatus::Pending));
1363    }
1364
1365    #[test]
1366    fn payment_cancelled_is_terminal() {
1367        assert!(A2APaymentStatus::Cancelled.is_terminal());
1368        assert!(A2APaymentStatus::Cancelled.allowed_transitions().is_empty());
1369    }
1370
1371    #[test]
1372    fn payment_refunded_is_terminal() {
1373        assert!(A2APaymentStatus::Refunded.is_terminal());
1374    }
1375
1376    #[test]
1377    fn payment_pending_is_not_terminal() {
1378        assert!(!A2APaymentStatus::Pending.is_terminal());
1379    }
1380
1381    // =========================================================================
1382    // PaymentRequestStatus state machine
1383    // =========================================================================
1384
1385    #[test]
1386    fn request_pending_can_go_to_viewed() {
1387        assert!(PaymentRequestStatus::Pending.can_transition_to(PaymentRequestStatus::Viewed));
1388    }
1389
1390    #[test]
1391    fn request_viewed_can_go_to_processing() {
1392        assert!(PaymentRequestStatus::Viewed.can_transition_to(PaymentRequestStatus::Processing));
1393    }
1394
1395    #[test]
1396    fn request_processing_can_go_to_paid() {
1397        assert!(PaymentRequestStatus::Processing.can_transition_to(PaymentRequestStatus::Paid));
1398    }
1399
1400    #[test]
1401    fn request_paid_is_terminal() {
1402        assert!(PaymentRequestStatus::Paid.is_terminal());
1403    }
1404
1405    #[test]
1406    fn request_declined_is_terminal() {
1407        assert!(PaymentRequestStatus::Declined.is_terminal());
1408    }
1409
1410    #[test]
1411    fn request_expired_is_terminal() {
1412        assert!(PaymentRequestStatus::Expired.is_terminal());
1413    }
1414
1415    #[test]
1416    fn request_cancelled_is_terminal() {
1417        assert!(PaymentRequestStatus::Cancelled.is_terminal());
1418    }
1419
1420    // =========================================================================
1421    // A2AQuoteStatus state machine
1422    // =========================================================================
1423
1424    #[test]
1425    fn quote_requested_can_go_to_quoted() {
1426        assert!(A2AQuoteStatus::Requested.can_transition_to(A2AQuoteStatus::Quoted));
1427    }
1428
1429    #[test]
1430    fn quote_quoted_can_go_to_counter_offered() {
1431        assert!(A2AQuoteStatus::Quoted.can_transition_to(A2AQuoteStatus::CounterOffered));
1432    }
1433
1434    #[test]
1435    fn quote_counter_offered_can_go_to_quoted() {
1436        assert!(A2AQuoteStatus::CounterOffered.can_transition_to(A2AQuoteStatus::Quoted));
1437    }
1438
1439    #[test]
1440    fn quote_counter_offered_can_go_to_accepted() {
1441        assert!(A2AQuoteStatus::CounterOffered.can_transition_to(A2AQuoteStatus::Accepted));
1442    }
1443
1444    #[test]
1445    fn quote_accepted_can_go_to_fulfilled() {
1446        assert!(A2AQuoteStatus::Accepted.can_transition_to(A2AQuoteStatus::Fulfilled));
1447    }
1448
1449    #[test]
1450    fn quote_declined_is_terminal() {
1451        assert!(A2AQuoteStatus::Declined.is_terminal());
1452    }
1453
1454    #[test]
1455    fn quote_fulfilled_is_terminal() {
1456        assert!(A2AQuoteStatus::Fulfilled.is_terminal());
1457    }
1458
1459    #[test]
1460    fn quote_allows_negotiation() {
1461        assert!(A2AQuoteStatus::Quoted.allows_negotiation());
1462        assert!(A2AQuoteStatus::CounterOffered.allows_negotiation());
1463        assert!(!A2AQuoteStatus::Requested.allows_negotiation());
1464        assert!(!A2AQuoteStatus::Accepted.allows_negotiation());
1465    }
1466
1467    #[test]
1468    fn quote_counter_offered_display() {
1469        assert_eq!(A2AQuoteStatus::CounterOffered.to_string(), "counter_offered");
1470    }
1471
1472    // =========================================================================
1473    // Negotiation flow
1474    // =========================================================================
1475
1476    #[test]
1477    fn negotiation_counter_offer() {
1478        let items = vec![A2AQuoteItem::new("Widget", 1, 1_000_000)];
1479        let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
1480
1481        // Seller quotes
1482        quote.provide_quote(1_100_000, 50_000, 50_000, 24);
1483        assert_eq!(quote.status, A2AQuoteStatus::Quoted);
1484
1485        // Buyer counter-offers
1486        assert!(quote.counter_offer(900_000, Some("Too expensive".into())));
1487        assert_eq!(quote.status, A2AQuoteStatus::CounterOffered);
1488        assert_eq!(quote.counter_count, 1);
1489        assert_eq!(quote.negotiation_history.len(), 1);
1490        assert_eq!(quote.negotiation_history[0].negotiation_type, NegotiationType::CounterOffer);
1491    }
1492
1493    #[test]
1494    fn negotiation_revise() {
1495        let items = vec![A2AQuoteItem::new("Service", 1, 500_000)];
1496        let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
1497
1498        quote.provide_quote(600_000, 50_000, 50_000, 24);
1499        quote.counter_offer(500_000, None);
1500
1501        // Seller revises
1502        quote.revise(550_000, 25_000, 25_000, 24, Some("Meet in the middle".into()));
1503        assert_eq!(quote.status, A2AQuoteStatus::Quoted);
1504        assert_eq!(quote.total, 550_000);
1505        assert_eq!(quote.negotiation_history.len(), 2);
1506    }
1507
1508    #[test]
1509    fn negotiation_decline() {
1510        let items = vec![A2AQuoteItem::new("Data", 1, 100_000)];
1511        let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
1512
1513        quote.provide_quote(200_000, 0, 0, 24);
1514        quote.decline(Some("Price too high".into()));
1515        assert_eq!(quote.status, A2AQuoteStatus::Declined);
1516        assert!(quote.status.is_terminal());
1517    }
1518
1519    #[test]
1520    fn negotiation_round_limit() {
1521        let items = vec![A2AQuoteItem::new("Item", 1, 100)];
1522        let mut quote =
1523            A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc).with_max_rounds(2);
1524
1525        quote.provide_quote(200, 0, 0, 24);
1526
1527        // Round 1
1528        assert!(quote.counter_offer(150, None));
1529        quote.revise(175, 0, 0, 24, None);
1530
1531        // Round 2
1532        assert!(quote.counter_offer(160, None));
1533
1534        // Round 3 — should fail
1535        quote.revise(165, 0, 0, 24, None);
1536        assert!(!quote.counter_offer(163, None));
1537        assert!(quote.is_negotiation_limit_reached());
1538    }
1539
1540    #[test]
1541    fn negotiation_not_allowed_in_wrong_state() {
1542        let items = vec![A2AQuoteItem::new("Item", 1, 100)];
1543        let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
1544
1545        // Can't counter-offer on a Requested quote (not yet Quoted)
1546        assert!(!quote.counter_offer(50, None));
1547    }
1548
1549    #[test]
1550    fn with_escrow() {
1551        let items = vec![A2AQuoteItem::new("Item", 1, 100)];
1552        let escrow_id = Uuid::new_v4();
1553        let quote =
1554            A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc).with_escrow(escrow_id);
1555        assert_eq!(quote.escrow_id, Some(escrow_id));
1556    }
1557
1558    #[test]
1559    fn default_max_rounds() {
1560        let items = vec![A2AQuoteItem::new("Item", 1, 100)];
1561        let quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
1562        assert_eq!(quote.max_rounds, 5);
1563    }
1564
1565    #[test]
1566    fn full_negotiation_flow_to_acceptance() {
1567        let items = vec![A2AQuoteItem::new("Consulting", 1, 10_000_000)];
1568        let mut quote = A2AQuote::request("0xbuyer", "0xseller", items, X402Asset::Usdc);
1569
1570        // Seller quotes
1571        quote.provide_quote(12_000_000, 500_000, 500_000, 48);
1572
1573        // Buyer counters
1574        quote.counter_offer(10_500_000, Some("Budget limited".into()));
1575
1576        // Seller revises
1577        quote.revise(11_000_000, 300_000, 200_000, 24, Some("Final offer".into()));
1578
1579        // Buyer accepts
1580        quote.accept();
1581        assert_eq!(quote.status, A2AQuoteStatus::Accepted);
1582        assert_eq!(quote.negotiation_history.len(), 2); // counter + revision
1583
1584        // Seller fulfills
1585        quote.fulfill();
1586        assert_eq!(quote.status, A2AQuoteStatus::Fulfilled);
1587        assert!(quote.status.is_terminal());
1588    }
1589}