Skip to main content

stateset_embedded/
x402.rs

1//! x402 Payment Protocol operations for AI agent commerce
2//!
3//! This module provides high-level operations for x402 stablecoin payments,
4//! enabling AI agents to transact with USDC, ssUSD, and other stablecoins
5//! on Set Chain L2 and other supported networks.
6//!
7//! # Overview
8//!
9//! The x402 protocol enables instant, low-cost stablecoin payments for AI agents:
10//! - Hybrid Ed25519 + ML-DSA-65 payment intents by default
11//! - Merkle proof verification for settlement confirmation
12//! - Multi-network support (Set Chain, Base, Ethereum, Arbitrum)
13//! - Multi-asset support (USDC, ssUSD, USDT, DAI)
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use stateset_embedded::{Commerce, CreateX402PaymentIntent, X402Network, X402Asset};
19//! use rust_decimal_macros::dec;
20//!
21//! let commerce = Commerce::new("./store.db")?;
22//!
23//! // Create a payment intent
24//! let intent = commerce.x402().create_intent(CreateX402PaymentIntent {
25//!     payer_address: "0xBuyer...".into(),
26//!     payee_address: "0xSeller...".into(),
27//!     amount: dec!(100.00),
28//!     asset: X402Asset::Usdc,
29//!     network: X402Network::SetChain,
30//!     ..Default::default()
31//! })?;
32//!
33//! // Sign the intent with its configured scheme. New intents default to hybrid
34//! // Ed25519 + ML-DSA-65 signatures.
35//! let signed = commerce.x402().sign_intent(intent.id, SignX402PaymentIntent {
36//!     intent_id: intent.id,
37//!     signature_scheme: None,
38//!     signature: "0x<ed25519_signature_component>".into(),
39//!     public_key: "0x<ed25519_public_key_component>".into(),
40//!     signature_bundle: Some(x402_signature_bundle),
41//!     public_key_bundle: Some(x402_public_key_bundle),
42//! })?;
43//!
44//! // After on-chain settlement, mark as settled
45//! let settled = commerce.x402().mark_settled(
46//!     intent.id,
47//!     "0xTxHash...",
48//!     12345678,
49//! )?;
50//! # Ok::<(), stateset_embedded::CommerceError>(())
51//! ```
52
53use stateset_core::{
54    A2APurchase, A2APurchaseFilter, AgentCard, AgentCardFilter, CreateA2APurchase, CreateA2AQuote,
55    CreateAgentCard, CreateX402PaymentIntent, PurchaseStatus, QuoteStatus, Result,
56    SignX402PaymentIntent, SkillQuote, SkillQuoteFilter, TrustLevel, UpdateAgentCard, X402Asset,
57    X402CreditAccount, X402CreditAdjustment, X402CreditDirection, X402CreditTransaction,
58    X402CreditTransactionFilter, X402IntentStatus, X402Network, X402PaymentIntent,
59    X402PaymentIntentFilter,
60};
61use stateset_db::Database;
62use std::sync::Arc;
63use uuid::Uuid;
64
65/// x402 payment protocol operations
66///
67/// Provides methods for creating, signing, and managing x402 payment intents,
68/// as well as agent card management for AI agent commerce.
69pub struct X402 {
70    db: Arc<dyn Database>,
71}
72
73impl std::fmt::Debug for X402 {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("X402").finish_non_exhaustive()
76    }
77}
78
79impl X402 {
80    /// Create a new X402 operations instance
81    pub fn new(db: Arc<dyn Database>) -> Self {
82        Self { db }
83    }
84
85    // ========================================================================
86    // Payment Intent Operations
87    // ========================================================================
88
89    /// Create a new x402 payment intent
90    ///
91    /// Creates an unsigned payment intent that must be signed by the payer
92    /// before it can be submitted for settlement.
93    ///
94    /// # Example
95    ///
96    /// ```rust,ignore
97    /// let intent = commerce.x402().create_intent(CreateX402PaymentIntent {
98    ///     payer_address: "0xBuyer...".into(),
99    ///     payee_address: "0xSeller...".into(),
100    ///     amount: dec!(50.00),
101    ///     asset: X402Asset::Usdc,
102    ///     network: X402Network::SetChain,
103    ///     cart_id: Some(cart.id),
104    ///     ..Default::default()
105    /// })?;
106    /// ```
107    pub fn create_intent(&self, input: CreateX402PaymentIntent) -> Result<X402PaymentIntent> {
108        self.db.x402_payment_intents().create(input)
109    }
110
111    /// Get a payment intent by ID
112    pub fn get_intent(&self, id: Uuid) -> Result<Option<X402PaymentIntent>> {
113        self.db.x402_payment_intents().get(id)
114    }
115
116    /// Sign a payment intent with its configured signature scheme
117    ///
118    /// The payer agent signs the intent's signing hash with their private key.
119    /// New intents default to hybrid Ed25519 + ML-DSA-65, and the signing request
120    /// must match the intent's configured scheme. This transitions the intent
121    /// from `Created` to `Signed` status.
122    ///
123    /// # Example
124    ///
125    /// ```rust,ignore
126    /// let signed = commerce.x402().sign_intent(intent.id, SignX402PaymentIntent {
127    ///     intent_id: intent.id,
128    ///     signature_scheme: None,
129    ///     signature: "0x<ed25519_signature_component>".into(),
130    ///     public_key: "0x<ed25519_public_key_component>".into(),
131    ///     signature_bundle: Some(x402_signature_bundle),
132    ///     public_key_bundle: Some(x402_public_key_bundle),
133    /// })?;
134    /// ```
135    pub fn sign_intent(&self, id: Uuid, input: SignX402PaymentIntent) -> Result<X402PaymentIntent> {
136        self.db.x402_payment_intents().sign(id, input)
137    }
138
139    /// Mark an intent as sequenced in a batch
140    ///
141    /// Called when the intent has been included in a settlement batch
142    /// but not yet confirmed on-chain.
143    pub fn mark_sequenced(
144        &self,
145        id: Uuid,
146        sequence_number: u64,
147        batch_id: Uuid,
148    ) -> Result<X402PaymentIntent> {
149        self.db.x402_payment_intents().mark_sequenced(id, sequence_number, batch_id)
150    }
151
152    /// Mark an intent as settled on-chain
153    ///
154    /// Called after the payment has been confirmed on the blockchain.
155    /// This is the final successful state for an intent.
156    ///
157    /// # Example
158    ///
159    /// ```rust,ignore
160    /// let settled = commerce.x402().mark_settled(
161    ///     intent.id,
162    ///     "0x1234...abcd",  // Transaction hash
163    ///     12345678,         // Block number
164    /// )?;
165    /// ```
166    pub fn mark_settled(
167        &self,
168        id: Uuid,
169        tx_hash: &str,
170        block_number: u64,
171    ) -> Result<X402PaymentIntent> {
172        self.db.x402_payment_intents().mark_settled(id, tx_hash, block_number)
173    }
174
175    /// Mark an intent as failed
176    ///
177    /// Called when the payment could not be processed (e.g., insufficient funds,
178    /// invalid signature, network error).
179    pub fn mark_failed(&self, id: Uuid, reason: &str) -> Result<X402PaymentIntent> {
180        self.db.x402_payment_intents().mark_failed(id, reason)
181    }
182
183    /// Mark an intent as expired
184    ///
185    /// Called when the intent's validity period has passed without settlement.
186    pub fn mark_expired(&self, id: Uuid) -> Result<X402PaymentIntent> {
187        self.db.x402_payment_intents().mark_expired(id)
188    }
189
190    /// Cancel a payment intent
191    ///
192    /// Can only cancel intents that are in `Created` or `Signed` status.
193    /// Once sequenced or settled, intents cannot be cancelled.
194    pub fn cancel_intent(&self, id: Uuid) -> Result<X402PaymentIntent> {
195        self.db.x402_payment_intents().cancel(id)
196    }
197
198    /// Get all payment intents for a cart
199    pub fn intents_for_cart(&self, cart_id: Uuid) -> Result<Vec<X402PaymentIntent>> {
200        self.db.x402_payment_intents().for_cart(cart_id)
201    }
202
203    /// Get all payment intents for an order
204    pub fn intents_for_order(&self, order_id: Uuid) -> Result<Vec<X402PaymentIntent>> {
205        self.db.x402_payment_intents().for_order(order_id)
206    }
207
208    /// Get the next nonce for a payer address
209    ///
210    /// Used to ensure payment intents are processed in order and to prevent
211    /// replay attacks.
212    pub fn get_next_nonce(&self, payer_address: &str) -> Result<u64> {
213        self.db.x402_payment_intents().get_next_nonce(payer_address)
214    }
215
216    /// List payment intents with optional filtering
217    pub fn list_intents(&self, filter: X402PaymentIntentFilter) -> Result<Vec<X402PaymentIntent>> {
218        self.db.x402_payment_intents().list(filter)
219    }
220
221    /// Count payment intents matching a filter
222    pub fn count_intents(&self, filter: X402PaymentIntentFilter) -> Result<u64> {
223        self.db.x402_payment_intents().count(filter)
224    }
225
226    /// Expire all stale intents that have passed their validity period
227    ///
228    /// Returns the number of intents that were expired.
229    pub fn expire_stale_intents(&self) -> Result<u64> {
230        self.db.x402_payment_intents().expire_stale_intents()
231    }
232
233    /// Get intents by status
234    pub fn intents_by_status(&self, status: X402IntentStatus) -> Result<Vec<X402PaymentIntent>> {
235        self.list_intents(X402PaymentIntentFilter { status: Some(status), ..Default::default() })
236    }
237
238    /// Get pending intents (created but not yet signed)
239    pub fn pending_intents(&self) -> Result<Vec<X402PaymentIntent>> {
240        self.intents_by_status(X402IntentStatus::Created)
241    }
242
243    /// Get signed intents awaiting settlement
244    pub fn signed_intents(&self) -> Result<Vec<X402PaymentIntent>> {
245        self.intents_by_status(X402IntentStatus::Signed)
246    }
247
248    /// Get settled intents
249    pub fn settled_intents(&self) -> Result<Vec<X402PaymentIntent>> {
250        self.intents_by_status(X402IntentStatus::Settled)
251    }
252
253    // ========================================================================
254    // A2A Commerce Operations
255    // ========================================================================
256
257    /// Create a new A2A quote
258    pub fn create_quote(&self, input: CreateA2AQuote) -> Result<SkillQuote> {
259        self.db.a2a_quotes().create_quote(input)
260    }
261
262    /// Get an A2A quote by ID
263    pub fn get_quote(&self, id: Uuid) -> Result<Option<SkillQuote>> {
264        self.db.a2a_quotes().get_quote(id)
265    }
266
267    /// Get an A2A quote by quote number
268    pub fn get_quote_by_number(&self, quote_number: &str) -> Result<Option<SkillQuote>> {
269        self.db.a2a_quotes().get_quote_by_number(quote_number)
270    }
271
272    /// Update A2A quote status
273    pub fn update_quote_status(&self, id: Uuid, status: QuoteStatus) -> Result<SkillQuote> {
274        self.db.a2a_quotes().update_quote_status(id, status)
275    }
276
277    /// List A2A quotes with filter
278    pub fn list_quotes(&self, filter: SkillQuoteFilter) -> Result<Vec<SkillQuote>> {
279        self.db.a2a_quotes().list_quotes(filter)
280    }
281
282    /// Count A2A quotes matching filter
283    pub fn count_quotes(&self, filter: SkillQuoteFilter) -> Result<u64> {
284        self.db.a2a_quotes().count_quotes(filter)
285    }
286
287    /// Create a new A2A purchase
288    pub fn create_purchase(&self, input: CreateA2APurchase) -> Result<A2APurchase> {
289        self.db.a2a_purchases().create_purchase(input)
290    }
291
292    /// Get an A2A purchase by ID
293    pub fn get_purchase(&self, id: Uuid) -> Result<Option<A2APurchase>> {
294        self.db.a2a_purchases().get_purchase(id)
295    }
296
297    /// Get an A2A purchase by purchase number
298    pub fn get_purchase_by_number(&self, purchase_number: &str) -> Result<Option<A2APurchase>> {
299        self.db.a2a_purchases().get_purchase_by_number(purchase_number)
300    }
301
302    /// Update A2A purchase status
303    pub fn update_purchase_status(&self, id: Uuid, status: PurchaseStatus) -> Result<A2APurchase> {
304        self.db.a2a_purchases().update_purchase_status(id, status)
305    }
306
307    /// Link A2A purchase to an order
308    pub fn link_purchase_to_order(&self, purchase_id: Uuid, order_id: Uuid) -> Result<A2APurchase> {
309        self.db.a2a_purchases().link_purchase_to_order(purchase_id, order_id)
310    }
311
312    /// Confirm delivery for an A2A purchase
313    pub fn confirm_delivery(
314        &self,
315        purchase_id: Uuid,
316        signature: &str,
317        rating: Option<u8>,
318        feedback: Option<&str>,
319    ) -> Result<A2APurchase> {
320        self.db.a2a_purchases().confirm_delivery(purchase_id, signature, rating, feedback)
321    }
322
323    /// List A2A purchases with filter
324    pub fn list_purchases(&self, filter: A2APurchaseFilter) -> Result<Vec<A2APurchase>> {
325        self.db.a2a_purchases().list_purchases(filter)
326    }
327
328    /// Count A2A purchases matching filter
329    pub fn count_purchases(&self, filter: A2APurchaseFilter) -> Result<u64> {
330        self.db.a2a_purchases().count_purchases(filter)
331    }
332
333    // ========================================================================
334    // Credit Ledger Operations (Metered Billing)
335    // ========================================================================
336
337    /// Get a credit account for a payer/asset/network
338    pub fn get_credit_account(
339        &self,
340        payer_address: &str,
341        asset: X402Asset,
342        network: X402Network,
343    ) -> Result<Option<X402CreditAccount>> {
344        self.db.x402_credits().get_account(payer_address, asset, network)
345    }
346
347    /// Get or create a credit account (balance default = 0)
348    pub fn get_or_create_credit_account(
349        &self,
350        payer_address: &str,
351        asset: X402Asset,
352        network: X402Network,
353    ) -> Result<X402CreditAccount> {
354        self.db.x402_credits().get_or_create_account(payer_address, asset, network)
355    }
356
357    /// Get current credit balance for a payer/asset/network
358    pub fn get_credit_balance(
359        &self,
360        payer_address: &str,
361        asset: X402Asset,
362        network: X402Network,
363    ) -> Result<u64> {
364        self.db.x402_credits().get_balance(payer_address, asset, network)
365    }
366
367    /// Apply a credit or debit adjustment
368    pub fn adjust_credit_balance(
369        &self,
370        input: X402CreditAdjustment,
371    ) -> Result<X402CreditTransaction> {
372        self.db.x402_credits().adjust_balance(input)
373    }
374
375    /// Credit an account (increase balance)
376    #[allow(clippy::too_many_arguments)]
377    pub fn credit_account(
378        &self,
379        payer_address: &str,
380        asset: X402Asset,
381        network: X402Network,
382        amount: u64,
383        reason: Option<String>,
384        reference_id: Option<String>,
385        metadata: Option<String>,
386    ) -> Result<X402CreditTransaction> {
387        self.adjust_credit_balance(X402CreditAdjustment {
388            payer_address: payer_address.to_string(),
389            asset,
390            network,
391            direction: X402CreditDirection::Credit,
392            amount,
393            reason,
394            reference_id,
395            metadata,
396        })
397    }
398
399    /// Debit an account (decrease balance)
400    #[allow(clippy::too_many_arguments)]
401    pub fn debit_account(
402        &self,
403        payer_address: &str,
404        asset: X402Asset,
405        network: X402Network,
406        amount: u64,
407        reason: Option<String>,
408        reference_id: Option<String>,
409        metadata: Option<String>,
410    ) -> Result<X402CreditTransaction> {
411        self.adjust_credit_balance(X402CreditAdjustment {
412            payer_address: payer_address.to_string(),
413            asset,
414            network,
415            direction: X402CreditDirection::Debit,
416            amount,
417            reason,
418            reference_id,
419            metadata,
420        })
421    }
422
423    /// List credit ledger transactions
424    pub fn list_credit_transactions(
425        &self,
426        filter: X402CreditTransactionFilter,
427    ) -> Result<Vec<X402CreditTransaction>> {
428        self.db.x402_credits().list_transactions(filter)
429    }
430
431    // ========================================================================
432    // Agent Card Operations
433    // ========================================================================
434
435    /// Register a new agent card
436    ///
437    /// Agent cards advertise an AI agent's commerce capabilities, including
438    /// supported payment networks, assets, and A2A skills.
439    ///
440    /// # Example
441    ///
442    /// ```rust,ignore
443    /// use stateset_core::{CreateAgentCard, X402Network, X402Asset, A2ASkill, TrustLevel};
444    ///
445    /// let card = commerce.x402().register_agent(CreateAgentCard {
446    ///     name: "Widget Seller Bot".into(),
447    ///     wallet_address: "0xSeller...".into(),
448    ///     public_key: "base64_ed25519_pubkey".into(),
449    ///     supported_networks: vec![X402Network::SetChain, X402Network::Base],
450    ///     supported_assets: vec![X402Asset::Usdc, X402Asset::SsUsd],
451    ///     a2a_skills: Some(vec![A2ASkill::Sell, A2ASkill::Quote, A2ASkill::Fulfill]),
452    ///     endpoint_url: Some("https://api.example.com/a2a".into()),
453    ///     ..Default::default()
454    /// })?;
455    /// ```
456    pub fn register_agent(&self, input: CreateAgentCard) -> Result<AgentCard> {
457        self.db.agent_cards().create(input)
458    }
459
460    /// Get an agent card by ID
461    pub fn get_agent(&self, id: Uuid) -> Result<Option<AgentCard>> {
462        self.db.agent_cards().get(id)
463    }
464
465    /// Get an agent card by wallet address
466    pub fn get_agent_by_wallet(&self, wallet_address: &str) -> Result<Option<AgentCard>> {
467        self.db.agent_cards().get_by_wallet(wallet_address)
468    }
469
470    /// Update an agent card
471    pub fn update_agent(&self, id: Uuid, input: UpdateAgentCard) -> Result<AgentCard> {
472        self.db.agent_cards().update(id, input)
473    }
474
475    /// Delete an agent card
476    pub fn delete_agent(&self, id: Uuid) -> Result<()> {
477        self.db.agent_cards().delete(id)
478    }
479
480    /// List agent cards with optional filtering
481    pub fn list_agents(&self, filter: AgentCardFilter) -> Result<Vec<AgentCard>> {
482        self.db.agent_cards().list(filter)
483    }
484
485    /// Count agent cards matching a filter
486    pub fn count_agents(&self, filter: AgentCardFilter) -> Result<u64> {
487        self.db.agent_cards().count(filter)
488    }
489
490    /// Verify an agent card (admin operation)
491    ///
492    /// Upgrades the agent's trust level to `Verified`.
493    pub fn verify_agent(&self, id: Uuid) -> Result<AgentCard> {
494        self.db.agent_cards().verify(id, TrustLevel::Verified, "system")
495    }
496
497    /// Suspend an agent card
498    ///
499    /// Temporarily disables the agent from participating in commerce.
500    pub fn suspend_agent(&self, id: Uuid, reason: &str) -> Result<AgentCard> {
501        self.db.agent_cards().suspend(id, reason)
502    }
503
504    /// Reactivate a suspended agent card
505    pub fn reactivate_agent(&self, id: Uuid) -> Result<AgentCard> {
506        self.db.agent_cards().reactivate(id)
507    }
508
509    /// Discover agents with specific capabilities
510    ///
511    /// Finds agents that support the specified network, asset, and skill.
512    ///
513    /// # Example
514    ///
515    /// ```rust,ignore
516    /// use stateset_core::{X402Network, X402Asset, A2ASkill};
517    ///
518    /// // Find all agents that can sell on Set Chain with USDC
519    /// let sellers = commerce.x402().discover_agents(
520    ///     Some(X402Network::SetChain),
521    ///     Some(X402Asset::Usdc),
522    ///     Some(A2ASkill::Sell),
523    ///     None,
524    /// )?;
525    /// ```
526    pub fn discover_agents(
527        &self,
528        network: Option<X402Network>,
529        asset: Option<X402Asset>,
530        skill: Option<stateset_core::A2ASkill>,
531        min_trust_level: Option<TrustLevel>,
532    ) -> Result<Vec<AgentCard>> {
533        self.db.agent_cards().discover(AgentCardFilter {
534            network,
535            asset,
536            skill,
537            trust_level: None,
538            min_trust_level,
539            active: Some(true),
540            ..Default::default()
541        })
542    }
543
544    /// Get all active agents
545    pub fn active_agents(&self) -> Result<Vec<AgentCard>> {
546        self.list_agents(AgentCardFilter { active: Some(true), ..Default::default() })
547    }
548
549    /// Get agents by trust level
550    pub fn agents_by_trust_level(&self, level: TrustLevel) -> Result<Vec<AgentCard>> {
551        self.list_agents(AgentCardFilter {
552            trust_level: Some(level),
553            active: Some(true),
554            ..Default::default()
555        })
556    }
557
558    /// Get verified agents only
559    pub fn verified_agents(&self) -> Result<Vec<AgentCard>> {
560        self.agents_by_trust_level(TrustLevel::Verified)
561    }
562
563    // ========================================================================
564    // Convenience Methods
565    // ========================================================================
566
567    /// Create a payment intent for a cart
568    ///
569    /// Convenience method that creates an intent linked to a specific cart.
570    /// The amount should be in the asset's decimal units (e.g., 100.00 for $100 USDC).
571    pub fn create_cart_payment(
572        &self,
573        cart_id: Uuid,
574        payer_address: &str,
575        payee_address: &str,
576        amount: rust_decimal::Decimal,
577        network: X402Network,
578        asset: X402Asset,
579    ) -> Result<X402PaymentIntent> {
580        use stateset_core::to_smallest_unit;
581        self.create_intent(CreateX402PaymentIntent {
582            payer_address: payer_address.to_string(),
583            payee_address: payee_address.to_string(),
584            amount: to_smallest_unit(amount, asset),
585            asset,
586            network,
587            cart_id: Some(cart_id),
588            ..Default::default()
589        })
590    }
591
592    /// Get the active payment intent for a cart (if any)
593    ///
594    /// Returns the most recent non-failed, non-expired intent for the cart.
595    pub fn active_intent_for_cart(&self, cart_id: Uuid) -> Result<Option<X402PaymentIntent>> {
596        let intents = self.intents_for_cart(cart_id)?;
597        Ok(intents.into_iter().find(|i| {
598            matches!(
599                i.status,
600                X402IntentStatus::Created
601                    | X402IntentStatus::Signed
602                    | X402IntentStatus::Sequenced
603                    | X402IntentStatus::Settled
604            )
605        }))
606    }
607
608    /// Check if an intent is ready for settlement
609    ///
610    /// An intent is ready when it has been signed and has not expired.
611    pub fn is_ready_for_settlement(&self, id: Uuid) -> Result<bool> {
612        if let Some(intent) = self.get_intent(id)? {
613            let now = chrono::Utc::now().timestamp() as u64;
614            Ok(intent.status == X402IntentStatus::Signed && intent.valid_until > now)
615        } else {
616            Ok(false)
617        }
618    }
619
620    /// Verify an intent's configured signature against its canonical signing hash.
621    ///
622    /// Returns `false` for missing, malformed, or invalid cryptographic fields.
623    pub fn has_valid_signature(&self, id: Uuid) -> Result<bool> {
624        if let Some(intent) = self.get_intent(id)? {
625            if !intent.is_signed() {
626                return Ok(false);
627            }
628            Ok(intent.verify_signature().unwrap_or(false))
629        } else {
630            Ok(false)
631        }
632    }
633}
634
635#[cfg(all(test, feature = "sqlite"))]
636mod tests {
637    use super::*;
638    use stateset_core::{
639        A2ASkill, CurrencyCode, ItemAvailability, QuotedItem, X402_DEFAULT_SIGNATURE_SCHEME,
640        X402SignatureScheme,
641    };
642    use stateset_crypto::pqc::generate_hybrid_signing_keypair;
643
644    fn setup_commerce() -> crate::Commerce {
645        crate::Commerce::in_memory().unwrap()
646    }
647
648    fn sign_locally_with_hybrid(intent: &mut X402PaymentIntent) {
649        let keypair = generate_hybrid_signing_keypair().unwrap();
650        intent.sign_with_hybrid(&keypair).unwrap();
651    }
652
653    #[test]
654    fn test_create_payment_intent() {
655        let commerce = setup_commerce();
656
657        // 100 USDC = 100_000_000 (6 decimals)
658        let intent = commerce
659            .x402()
660            .create_intent(CreateX402PaymentIntent {
661                payer_address: "0xPayer123".into(),
662                payee_address: "0xPayee456".into(),
663                amount: 100_000_000,
664                asset: X402Asset::Usdc,
665                network: X402Network::SetChain,
666                ..Default::default()
667            })
668            .unwrap();
669
670        assert_eq!(intent.payer_address, "0xPayer123");
671        assert_eq!(intent.payee_address, "0xPayee456");
672        assert_eq!(intent.amount, 100_000_000);
673        assert_eq!(intent.asset, X402Asset::Usdc);
674        assert_eq!(intent.network, X402Network::SetChain);
675        assert_eq!(intent.status, X402IntentStatus::Created);
676        assert_eq!(intent.payer_signature_scheme, Some(X402_DEFAULT_SIGNATURE_SCHEME));
677        assert!(intent.signing_hash.is_some());
678    }
679
680    #[test]
681    fn test_sign_payment_intent() {
682        let commerce = setup_commerce();
683
684        // 50 USDC = 50_000_000 (6 decimals)
685        let intent = commerce
686            .x402()
687            .create_intent(CreateX402PaymentIntent {
688                payer_address: "0xPayer123".into(),
689                payee_address: "0xPayee456".into(),
690                amount: 50_000_000,
691                ..Default::default()
692            })
693            .unwrap();
694
695        let mut locally_signed = commerce.x402().get_intent(intent.id).unwrap().unwrap();
696        sign_locally_with_hybrid(&mut locally_signed);
697        let signature = locally_signed.payer_signature.clone().unwrap();
698        let public_key = locally_signed.payer_public_key.clone().unwrap();
699        let signature_bundle = locally_signed.payer_signature_bundle.clone();
700        let public_key_bundle = locally_signed.payer_public_key_bundle.clone();
701
702        let signed = commerce
703            .x402()
704            .sign_intent(
705                intent.id,
706                SignX402PaymentIntent {
707                    intent_id: intent.id,
708                    signature_scheme: None,
709                    signature: signature.clone(),
710                    public_key: public_key.clone(),
711                    signature_bundle,
712                    public_key_bundle,
713                },
714            )
715            .unwrap();
716
717        assert_eq!(signed.status, X402IntentStatus::Signed);
718        assert_eq!(signed.payer_signature_scheme, Some(X402_DEFAULT_SIGNATURE_SCHEME));
719        assert_eq!(signed.payer_signature, Some(signature));
720        assert_eq!(signed.payer_public_key, Some(public_key));
721        assert!(signed.payer_signature_bundle.is_some());
722        assert!(signed.payer_public_key_bundle.is_some());
723    }
724
725    #[test]
726    fn test_sign_payment_intent_rejects_ed25519_downgrade_for_new_intents() {
727        let commerce = setup_commerce();
728
729        let intent = commerce
730            .x402()
731            .create_intent(CreateX402PaymentIntent {
732                payer_address: "0xHybridSigner".into(),
733                payee_address: "0xPayee456".into(),
734                amount: 50_000_000,
735                ..Default::default()
736            })
737            .unwrap();
738
739        let mut locally_signed = commerce.x402().get_intent(intent.id).unwrap().unwrap();
740        locally_signed.sign_with_ed25519(&[21u8; 32]).unwrap();
741
742        let err = commerce
743            .x402()
744            .sign_intent(
745                intent.id,
746                SignX402PaymentIntent {
747                    intent_id: intent.id,
748                    signature_scheme: Some(X402SignatureScheme::Ed25519),
749                    signature: locally_signed.payer_signature.unwrap(),
750                    public_key: locally_signed.payer_public_key.unwrap(),
751                    signature_bundle: None,
752                    public_key_bundle: None,
753                },
754            )
755            .unwrap_err();
756
757        assert!(err.to_string().contains("ed25519_ml_dsa65"));
758    }
759
760    #[test]
761    fn test_has_valid_signature_true_for_hybrid_signature() {
762        let commerce = setup_commerce();
763
764        let intent = commerce
765            .x402()
766            .create_intent(CreateX402PaymentIntent {
767                payer_address: "0xSigner".into(),
768                payee_address: "0xPayee".into(),
769                amount: 1_000_000,
770                ..Default::default()
771            })
772            .unwrap();
773
774        let mut to_sign = commerce.x402().get_intent(intent.id).unwrap().unwrap();
775        sign_locally_with_hybrid(&mut to_sign);
776
777        let signed = commerce
778            .x402()
779            .sign_intent(
780                intent.id,
781                SignX402PaymentIntent {
782                    intent_id: intent.id,
783                    signature_scheme: None,
784                    signature: to_sign.payer_signature.clone().unwrap(),
785                    public_key: to_sign.payer_public_key.clone().unwrap(),
786                    signature_bundle: to_sign.payer_signature_bundle.clone(),
787                    public_key_bundle: to_sign.payer_public_key_bundle.clone(),
788                },
789            )
790            .unwrap();
791
792        assert_eq!(signed.status, X402IntentStatus::Signed);
793        assert!(commerce.x402().has_valid_signature(intent.id).unwrap());
794    }
795
796    #[test]
797    fn test_sign_intent_rejects_malformed_signature() {
798        let commerce = setup_commerce();
799
800        let intent = commerce
801            .x402()
802            .create_intent(CreateX402PaymentIntent {
803                payer_address: "0xSigner".into(),
804                payee_address: "0xPayee".into(),
805                amount: 1_000_000,
806                ..Default::default()
807            })
808            .unwrap();
809
810        let result = commerce.x402().sign_intent(
811            intent.id,
812            SignX402PaymentIntent {
813                intent_id: intent.id,
814                signature_scheme: None,
815                signature: "not-hex-signature".into(),
816                public_key: "not-hex-public-key".into(),
817                signature_bundle: None,
818                public_key_bundle: None,
819            },
820        );
821
822        assert!(result.is_err());
823    }
824
825    #[test]
826    fn test_sign_intent_rejects_mismatched_intent_id() {
827        let commerce = setup_commerce();
828
829        let intent = commerce
830            .x402()
831            .create_intent(CreateX402PaymentIntent {
832                payer_address: "0xSigner".into(),
833                payee_address: "0xPayee".into(),
834                amount: 1_000_000,
835                ..Default::default()
836            })
837            .unwrap();
838
839        let mut locally_signed = commerce.x402().get_intent(intent.id).unwrap().unwrap();
840        sign_locally_with_hybrid(&mut locally_signed);
841
842        let result = commerce.x402().sign_intent(
843            intent.id,
844            SignX402PaymentIntent {
845                intent_id: Uuid::new_v4(),
846                signature_scheme: None,
847                signature: locally_signed.payer_signature.clone().unwrap(),
848                public_key: locally_signed.payer_public_key.clone().unwrap(),
849                signature_bundle: locally_signed.payer_signature_bundle.clone(),
850                public_key_bundle: locally_signed.payer_public_key_bundle.clone(),
851            },
852        );
853
854        assert!(result.is_err());
855    }
856
857    #[test]
858    fn test_mark_settled() {
859        let commerce = setup_commerce();
860
861        // 25 USDC = 25_000_000 (6 decimals)
862        let intent = commerce
863            .x402()
864            .create_intent(CreateX402PaymentIntent {
865                payer_address: "0xPayer123".into(),
866                payee_address: "0xPayee456".into(),
867                amount: 25_000_000,
868                ..Default::default()
869            })
870            .unwrap();
871
872        // Sign first
873        let mut locally_signed = commerce.x402().get_intent(intent.id).unwrap().unwrap();
874        sign_locally_with_hybrid(&mut locally_signed);
875
876        commerce
877            .x402()
878            .sign_intent(
879                intent.id,
880                SignX402PaymentIntent {
881                    intent_id: intent.id,
882                    signature_scheme: None,
883                    signature: locally_signed.payer_signature.clone().unwrap(),
884                    public_key: locally_signed.payer_public_key.clone().unwrap(),
885                    signature_bundle: locally_signed.payer_signature_bundle.clone(),
886                    public_key_bundle: locally_signed.payer_public_key_bundle.clone(),
887                },
888            )
889            .unwrap();
890
891        let sequenced = commerce.x402().mark_sequenced(intent.id, 7, Uuid::new_v4()).unwrap();
892        assert_eq!(sequenced.status, X402IntentStatus::Sequenced);
893
894        // Then settle
895        let settled = commerce.x402().mark_settled(intent.id, "0xTxHash123", 12345).unwrap();
896
897        assert_eq!(settled.status, X402IntentStatus::Settled);
898        assert_eq!(settled.tx_hash, Some("0xTxHash123".to_string()));
899        assert_eq!(settled.block_number, Some(12345));
900    }
901
902    #[test]
903    fn test_mark_settled_rejects_unsequenced_intent() {
904        let commerce = setup_commerce();
905
906        let intent = commerce
907            .x402()
908            .create_intent(CreateX402PaymentIntent {
909                payer_address: "0xPayer123".into(),
910                payee_address: "0xPayee456".into(),
911                amount: 25_000_000,
912                ..Default::default()
913            })
914            .unwrap();
915
916        let mut locally_signed = commerce.x402().get_intent(intent.id).unwrap().unwrap();
917        sign_locally_with_hybrid(&mut locally_signed);
918
919        commerce
920            .x402()
921            .sign_intent(
922                intent.id,
923                SignX402PaymentIntent {
924                    intent_id: intent.id,
925                    signature_scheme: None,
926                    signature: locally_signed.payer_signature.clone().unwrap(),
927                    public_key: locally_signed.payer_public_key.clone().unwrap(),
928                    signature_bundle: locally_signed.payer_signature_bundle.clone(),
929                    public_key_bundle: locally_signed.payer_public_key_bundle.clone(),
930                },
931            )
932            .unwrap();
933
934        let err = commerce.x402().mark_settled(intent.id, "0xTxHash123", 12345).unwrap_err();
935        assert!(err.to_string().contains("Cannot settle intent in signed status"));
936    }
937
938    #[test]
939    fn test_register_agent_card() {
940        let commerce = setup_commerce();
941
942        let card = commerce
943            .x402()
944            .register_agent(CreateAgentCard {
945                name: "Test Agent".into(),
946                wallet_address: "0xAgent123".into(),
947                public_key: "test_pubkey".into(),
948                supported_networks: Some(vec![X402Network::SetChain]),
949                supported_assets: Some(vec![X402Asset::Usdc]),
950                a2a_skills: Some(vec![A2ASkill::Sell, A2ASkill::Quote]),
951                endpoint_url: Some("https://api.example.com".into()),
952                ..Default::default()
953            })
954            .unwrap();
955
956        assert_eq!(card.name, "Test Agent");
957        assert_eq!(card.wallet_address, "0xAgent123");
958        assert!(card.active);
959        assert_eq!(card.trust_level, TrustLevel::Standard);
960    }
961
962    #[test]
963    fn test_discover_agents() {
964        let commerce = setup_commerce();
965
966        // Register a few agents
967        commerce
968            .x402()
969            .register_agent(CreateAgentCard {
970                name: "Seller 1".into(),
971                wallet_address: "0xSeller1".into(),
972                public_key: "pk1".into(),
973                supported_networks: Some(vec![X402Network::SetChain]),
974                supported_assets: Some(vec![X402Asset::Usdc]),
975                a2a_skills: Some(vec![A2ASkill::Sell]),
976                ..Default::default()
977            })
978            .unwrap();
979
980        commerce
981            .x402()
982            .register_agent(CreateAgentCard {
983                name: "Buyer 1".into(),
984                wallet_address: "0xBuyer1".into(),
985                public_key: "pk2".into(),
986                supported_networks: Some(vec![X402Network::Base]),
987                supported_assets: Some(vec![X402Asset::Usdc]),
988                a2a_skills: Some(vec![A2ASkill::Buy]),
989                ..Default::default()
990            })
991            .unwrap();
992
993        // Discover agents on SetChain
994        let set_chain_agents =
995            commerce.x402().discover_agents(Some(X402Network::SetChain), None, None, None).unwrap();
996
997        assert_eq!(set_chain_agents.len(), 1);
998        assert_eq!(set_chain_agents[0].name, "Seller 1");
999    }
1000
1001    #[test]
1002    fn test_verify_agent() {
1003        let commerce = setup_commerce();
1004
1005        let card = commerce
1006            .x402()
1007            .register_agent(CreateAgentCard {
1008                name: "Verified Agent".into(),
1009                wallet_address: "0xVerified".into(),
1010                public_key: "pk".into(),
1011                supported_networks: Some(vec![X402Network::SetChain]),
1012                supported_assets: Some(vec![X402Asset::Usdc]),
1013                ..Default::default()
1014            })
1015            .unwrap();
1016
1017        assert_eq!(card.trust_level, TrustLevel::Standard);
1018
1019        let verified = commerce.x402().verify_agent(card.id).unwrap();
1020        assert_eq!(verified.trust_level, TrustLevel::Verified);
1021    }
1022
1023    #[test]
1024    fn test_create_and_track_cart_payment() {
1025        let commerce = setup_commerce();
1026        let cart_id = Uuid::new_v4();
1027
1028        let intent = commerce
1029            .x402()
1030            .create_cart_payment(
1031                cart_id,
1032                "0xPayerCart",
1033                "0xPayeeCart",
1034                rust_decimal_macros::dec!(12.50),
1035                X402Network::SetChain,
1036                X402Asset::Usdc,
1037            )
1038            .unwrap();
1039
1040        assert_eq!(intent.cart_id, Some(cart_id));
1041        assert_eq!(intent.status, X402IntentStatus::Created);
1042
1043        let intents = commerce.x402().intents_for_cart(cart_id).unwrap();
1044        assert_eq!(intents.len(), 1);
1045        assert_eq!(intents[0].id, intent.id);
1046
1047        let active = commerce.x402().active_intent_for_cart(cart_id).unwrap();
1048        assert!(active.is_some());
1049        assert_eq!(active.expect("active intent").id, intent.id);
1050    }
1051
1052    #[test]
1053    fn test_a2a_quote_and_purchase_flow() {
1054        let commerce = setup_commerce();
1055
1056        let seller = commerce
1057            .x402()
1058            .register_agent(CreateAgentCard {
1059                name: "A2A Seller".into(),
1060                wallet_address: "0xSellerA2A".into(),
1061                public_key: "seller_pub".into(),
1062                supported_networks: Some(vec![X402Network::SetChain]),
1063                supported_assets: Some(vec![X402Asset::Usdc]),
1064                a2a_skills: Some(vec![A2ASkill::Sell]),
1065                trust_level: Some(TrustLevel::Verified),
1066                endpoint_url: Some("https://agent.example.com/".into()),
1067                ..Default::default()
1068            })
1069            .unwrap();
1070
1071        let buyer_id = Uuid::new_v4();
1072        let quote = commerce
1073            .x402()
1074            .create_quote(CreateA2AQuote {
1075                buyer_agent_id: buyer_id,
1076                seller_agent_id: seller.id,
1077                items: vec![QuotedItem {
1078                    line_number: 1,
1079                    sku: Some("SKU-1".to_string()),
1080                    name: "Service Plan".to_string(),
1081                    quantity: 1,
1082                    unit_price: rust_decimal_macros::dec!(19.99),
1083                    total: rust_decimal_macros::dec!(19.99),
1084                    availability: ItemAvailability::InStock,
1085                    lead_time_days: Some(1),
1086                }],
1087                subtotal: rust_decimal_macros::dec!(19.99),
1088                total: rust_decimal_macros::dec!(19.99),
1089                currency: Some(CurrencyCode::USD),
1090                tax_amount: Some(rust_decimal::Decimal::ZERO),
1091                shipping_amount: Some(rust_decimal::Decimal::ZERO),
1092                discount_amount: Some(rust_decimal::Decimal::ZERO),
1093                valid_until: chrono::Utc::now() + chrono::Duration::hours(1),
1094                payment_network: Some(X402Network::SetChain),
1095                payment_asset: Some(X402Asset::Usdc),
1096                notes: Some("unit test quote".into()),
1097                ..Default::default()
1098            })
1099            .unwrap();
1100
1101        assert_eq!(quote.status, QuoteStatus::Pending);
1102
1103        let quoted = commerce.x402().update_quote_status(quote.id, QuoteStatus::Quoted).unwrap();
1104        assert_eq!(quoted.status, QuoteStatus::Quoted);
1105
1106        let no_op_quote =
1107            commerce.x402().update_quote_status(quote.id, QuoteStatus::Quoted).unwrap();
1108        assert_eq!(no_op_quote.status, QuoteStatus::Quoted);
1109
1110        let purchase = commerce
1111            .x402()
1112            .create_purchase(CreateA2APurchase {
1113                buyer_agent_id: buyer_id,
1114                seller_agent_id: seller.id,
1115                quote_id: Some(quoted.id),
1116                items: quoted.items,
1117                total: quoted.total,
1118                currency: Some(quoted.currency),
1119                fulfillment_type: Some("digital".to_string()),
1120                notes: Some("unit test purchase".into()),
1121                metadata: None,
1122                payment_intent_id: None,
1123            })
1124            .unwrap();
1125
1126        assert_eq!(purchase.status, PurchaseStatus::Initiated);
1127        assert_eq!(purchase.quote_id, Some(quoted.id));
1128
1129        let payment_pending = commerce
1130            .x402()
1131            .update_purchase_status(purchase.id, PurchaseStatus::PaymentPending)
1132            .unwrap();
1133        assert_eq!(payment_pending.status, PurchaseStatus::PaymentPending);
1134
1135        let shipped =
1136            commerce.x402().update_purchase_status(purchase.id, PurchaseStatus::Shipped).unwrap();
1137        assert_eq!(shipped.status, PurchaseStatus::Shipped);
1138
1139        let refreshed_quote = commerce.x402().get_quote(quote.id).unwrap();
1140        assert!(
1141            matches!(refreshed_quote.as_ref(), Some(value) if value.status == QuoteStatus::Purchased)
1142        );
1143
1144        let listed_quotes = commerce
1145            .x402()
1146            .list_quotes(SkillQuoteFilter { buyer_agent_id: Some(buyer_id), ..Default::default() })
1147            .unwrap();
1148        assert!(listed_quotes.iter().any(|item| item.id == quoted.id));
1149
1150        let quote_by_number = commerce
1151            .x402()
1152            .get_quote_by_number(&quoted.quote_number)
1153            .unwrap()
1154            .expect("quote by number");
1155        assert_eq!(quote_by_number.id, quoted.id);
1156
1157        let counted_quotes = commerce
1158            .x402()
1159            .count_quotes(SkillQuoteFilter {
1160                seller_agent_id: Some(seller.id),
1161                ..Default::default()
1162            })
1163            .unwrap();
1164        assert!(counted_quotes >= 1);
1165
1166        let completed = commerce
1167            .x402()
1168            .confirm_delivery(purchase.id, "delivery_signature", Some(5), Some("good"))
1169            .unwrap();
1170        assert_eq!(completed.status, PurchaseStatus::Completed);
1171        assert_eq!(
1172            completed.delivery_confirmation_signature,
1173            Some("delivery_signature".to_string())
1174        );
1175
1176        let no_op_purchase =
1177            commerce.x402().update_purchase_status(purchase.id, PurchaseStatus::Completed).unwrap();
1178        assert_eq!(no_op_purchase.status, PurchaseStatus::Completed);
1179
1180        let listed_purchases = commerce
1181            .x402()
1182            .list_purchases(A2APurchaseFilter {
1183                buyer_agent_id: Some(buyer_id),
1184                ..Default::default()
1185            })
1186            .unwrap();
1187        assert!(listed_purchases.iter().any(|item| item.id == purchase.id));
1188
1189        let counted_purchases = commerce
1190            .x402()
1191            .count_purchases(A2APurchaseFilter {
1192                buyer_agent_id: Some(buyer_id),
1193                ..Default::default()
1194            })
1195            .unwrap();
1196        assert!(counted_purchases >= 1);
1197    }
1198
1199    #[test]
1200    fn test_a2a_quote_and_purchase_state_guards() {
1201        let commerce = setup_commerce();
1202
1203        let seller = commerce
1204            .x402()
1205            .register_agent(CreateAgentCard {
1206                name: "A2A Guard Seller".into(),
1207                wallet_address: "0xSellerA2AGuard".into(),
1208                public_key: "seller_guard_pub".into(),
1209                supported_networks: Some(vec![X402Network::SetChain]),
1210                supported_assets: Some(vec![X402Asset::Usdc]),
1211                a2a_skills: Some(vec![A2ASkill::Sell]),
1212                trust_level: Some(TrustLevel::Verified),
1213                endpoint_url: Some("https://agent.example.com/guard".into()),
1214                ..Default::default()
1215            })
1216            .unwrap();
1217
1218        let other_seller = commerce
1219            .x402()
1220            .register_agent(CreateAgentCard {
1221                name: "A2A Wrong Seller".into(),
1222                wallet_address: "0xSellerWrong".into(),
1223                public_key: "wrong_seller_pub".into(),
1224                supported_networks: Some(vec![X402Network::SetChain]),
1225                supported_assets: Some(vec![X402Asset::Usdc]),
1226                a2a_skills: Some(vec![A2ASkill::Sell]),
1227                ..Default::default()
1228            })
1229            .unwrap();
1230
1231        let buyer_id = Uuid::new_v4();
1232        let quote = commerce
1233            .x402()
1234            .create_quote(CreateA2AQuote {
1235                buyer_agent_id: buyer_id,
1236                seller_agent_id: seller.id,
1237                items: vec![QuotedItem {
1238                    line_number: 1,
1239                    sku: Some("SKU-2".to_string()),
1240                    name: "Guarded service".to_string(),
1241                    quantity: 1,
1242                    unit_price: rust_decimal_macros::dec!(30.00),
1243                    total: rust_decimal_macros::dec!(30.00),
1244                    availability: ItemAvailability::InStock,
1245                    lead_time_days: Some(2),
1246                }],
1247                subtotal: rust_decimal_macros::dec!(30.00),
1248                total: rust_decimal_macros::dec!(30.00),
1249                currency: Some(CurrencyCode::USD),
1250                tax_amount: Some(rust_decimal::Decimal::ZERO),
1251                shipping_amount: Some(rust_decimal::Decimal::ZERO),
1252                discount_amount: Some(rust_decimal::Decimal::ZERO),
1253                valid_until: chrono::Utc::now() + chrono::Duration::hours(1),
1254                payment_network: Some(X402Network::SetChain),
1255                payment_asset: Some(X402Asset::Usdc),
1256                notes: Some("guard quote".into()),
1257                ..Default::default()
1258            })
1259            .unwrap();
1260
1261        assert!(commerce.x402().update_quote_status(quote.id, QuoteStatus::Accepted).is_err());
1262
1263        let quoted = commerce.x402().update_quote_status(quote.id, QuoteStatus::Quoted).unwrap();
1264
1265        assert!(
1266            commerce
1267                .x402()
1268                .create_purchase(CreateA2APurchase {
1269                    buyer_agent_id: buyer_id,
1270                    seller_agent_id: other_seller.id,
1271                    quote_id: Some(quoted.id),
1272                    items: quoted.items.clone(),
1273                    total: quoted.total,
1274                    currency: Some(CurrencyCode::USD),
1275                    fulfillment_type: Some("digital".to_string()),
1276                    notes: Some("mismatched seller".into()),
1277                    metadata: None,
1278                    payment_intent_id: None,
1279                })
1280                .is_err()
1281        );
1282
1283        assert!(
1284            commerce
1285                .x402()
1286                .create_purchase(CreateA2APurchase {
1287                    buyer_agent_id: buyer_id,
1288                    seller_agent_id: seller.id,
1289                    quote_id: Some(quoted.id),
1290                    items: quoted.items.clone(),
1291                    total: quoted.total,
1292                    currency: Some(CurrencyCode::EUR),
1293                    fulfillment_type: Some("digital".to_string()),
1294                    notes: Some("mismatched currency".into()),
1295                    metadata: None,
1296                    payment_intent_id: None,
1297                })
1298                .is_err()
1299        );
1300
1301        assert!(
1302            commerce
1303                .x402()
1304                .create_purchase(CreateA2APurchase {
1305                    buyer_agent_id: buyer_id,
1306                    seller_agent_id: seller.id,
1307                    quote_id: Some(quoted.id),
1308                    items: quoted.items.clone(),
1309                    total: quoted.total + rust_decimal::Decimal::ONE,
1310                    currency: Some(CurrencyCode::USD),
1311                    fulfillment_type: Some("digital".to_string()),
1312                    notes: Some("mismatched total".into()),
1313                    metadata: None,
1314                    payment_intent_id: None,
1315                })
1316                .is_err()
1317        );
1318
1319        let purchase = commerce
1320            .x402()
1321            .create_purchase(CreateA2APurchase {
1322                buyer_agent_id: buyer_id,
1323                seller_agent_id: seller.id,
1324                quote_id: Some(quoted.id),
1325                items: quoted.items,
1326                total: quoted.total,
1327                currency: Some(quoted.currency),
1328                fulfillment_type: Some("digital".to_string()),
1329                notes: Some("valid purchase".into()),
1330                metadata: None,
1331                payment_intent_id: None,
1332            })
1333            .unwrap();
1334
1335        assert_eq!(purchase.status, PurchaseStatus::Initiated);
1336
1337        assert!(
1338            commerce.x402().update_purchase_status(purchase.id, PurchaseStatus::Completed).is_err()
1339        );
1340
1341        assert!(
1342            commerce
1343                .x402()
1344                .confirm_delivery(purchase.id, "delivery_signature", Some(5), Some("blocked"))
1345                .is_err()
1346        );
1347    }
1348
1349    #[test]
1350    fn test_a2a_purchase_rejected_for_expired_quote() {
1351        let commerce = setup_commerce();
1352
1353        let seller = commerce
1354            .x402()
1355            .register_agent(CreateAgentCard {
1356                name: "Expired Quote Seller".into(),
1357                wallet_address: "0xSellerA2AExpired".into(),
1358                public_key: "expired_seller_pub".into(),
1359                supported_networks: Some(vec![X402Network::SetChain]),
1360                supported_assets: Some(vec![X402Asset::Usdc]),
1361                a2a_skills: Some(vec![A2ASkill::Sell]),
1362                trust_level: Some(TrustLevel::Verified),
1363                endpoint_url: Some("https://agent.example.com/".into()),
1364                ..Default::default()
1365            })
1366            .unwrap();
1367
1368        let buyer_id = Uuid::new_v4();
1369        assert!(
1370            commerce
1371                .x402()
1372                .create_quote(CreateA2AQuote {
1373                    buyer_agent_id: buyer_id,
1374                    seller_agent_id: seller.id,
1375                    items: vec![QuotedItem {
1376                        line_number: 1,
1377                        sku: Some("SKU-EX".to_string()),
1378                        name: "Expired service".to_string(),
1379                        quantity: 1,
1380                        unit_price: rust_decimal_macros::dec!(15.00),
1381                        total: rust_decimal_macros::dec!(15.00),
1382                        availability: ItemAvailability::InStock,
1383                        lead_time_days: Some(1),
1384                    }],
1385                    subtotal: rust_decimal_macros::dec!(15.00),
1386                    total: rust_decimal_macros::dec!(15.00),
1387                    currency: Some(CurrencyCode::USD),
1388                    tax_amount: Some(rust_decimal::Decimal::ZERO),
1389                    shipping_amount: Some(rust_decimal::Decimal::ZERO),
1390                    discount_amount: Some(rust_decimal::Decimal::ZERO),
1391                    valid_until: chrono::Utc::now() - chrono::Duration::hours(1),
1392                    payment_network: Some(X402Network::SetChain),
1393                    payment_asset: Some(X402Asset::Usdc),
1394                    notes: Some("expired quote".into()),
1395                    ..Default::default()
1396                })
1397                .is_err()
1398        );
1399
1400        let quote = commerce
1401            .x402()
1402            .create_quote(CreateA2AQuote {
1403                buyer_agent_id: buyer_id,
1404                seller_agent_id: seller.id,
1405                items: vec![QuotedItem {
1406                    line_number: 1,
1407                    sku: Some("SKU-EX".to_string()),
1408                    name: "Expired service".to_string(),
1409                    quantity: 1,
1410                    unit_price: rust_decimal_macros::dec!(15.00),
1411                    total: rust_decimal_macros::dec!(15.00),
1412                    availability: ItemAvailability::InStock,
1413                    lead_time_days: Some(1),
1414                }],
1415                subtotal: rust_decimal_macros::dec!(15.00),
1416                total: rust_decimal_macros::dec!(15.00),
1417                currency: Some(CurrencyCode::USD),
1418                tax_amount: Some(rust_decimal::Decimal::ZERO),
1419                shipping_amount: Some(rust_decimal::Decimal::ZERO),
1420                discount_amount: Some(rust_decimal::Decimal::ZERO),
1421                valid_until: chrono::Utc::now() + chrono::Duration::hours(1),
1422                payment_network: Some(X402Network::SetChain),
1423                payment_asset: Some(X402Asset::Usdc),
1424                notes: Some("expired quote".into()),
1425                ..Default::default()
1426            })
1427            .unwrap();
1428
1429        let quoted = commerce.x402().update_quote_status(quote.id, QuoteStatus::Quoted).unwrap();
1430        let expired = commerce.x402().update_quote_status(quoted.id, QuoteStatus::Expired).unwrap();
1431        assert_eq!(quoted.status, QuoteStatus::Quoted);
1432        assert_eq!(expired.status, QuoteStatus::Expired);
1433
1434        assert!(
1435            commerce
1436                .x402()
1437                .create_purchase(CreateA2APurchase {
1438                    buyer_agent_id: buyer_id,
1439                    seller_agent_id: seller.id,
1440                    quote_id: Some(expired.id),
1441                    items: expired.items,
1442                    total: expired.total,
1443                    currency: Some(expired.currency),
1444                    fulfillment_type: Some("digital".to_string()),
1445                    notes: Some("expired quote blocked".into()),
1446                    metadata: None,
1447                    payment_intent_id: None,
1448                })
1449                .is_err()
1450        );
1451    }
1452
1453    #[test]
1454    fn test_a2a_purchase_state_lifecycle_controls() {
1455        let commerce = setup_commerce();
1456
1457        let seller = commerce
1458            .x402()
1459            .register_agent(CreateAgentCard {
1460                name: "A2A Lifecycle Seller".into(),
1461                wallet_address: "0xSellerLifecycle".into(),
1462                public_key: "lifecycle_pub".into(),
1463                supported_networks: Some(vec![X402Network::SetChain]),
1464                supported_assets: Some(vec![X402Asset::Usdc]),
1465                a2a_skills: Some(vec![A2ASkill::Sell]),
1466                trust_level: Some(TrustLevel::Verified),
1467                endpoint_url: Some("https://agent.example.com/lifecycle".into()),
1468                ..Default::default()
1469            })
1470            .unwrap();
1471
1472        let buyer_id = Uuid::new_v4();
1473        let now = chrono::Utc::now();
1474
1475        let make_quote = |buyer_id: Uuid, seller_id: Uuid| CreateA2AQuote {
1476            buyer_agent_id: buyer_id,
1477            seller_agent_id: seller_id,
1478            items: vec![QuotedItem {
1479                line_number: 1,
1480                sku: Some("SKU-LC-1".to_string()),
1481                name: "Lifecycle service".to_string(),
1482                quantity: 1,
1483                unit_price: rust_decimal_macros::dec!(12.00),
1484                total: rust_decimal_macros::dec!(12.00),
1485                availability: ItemAvailability::InStock,
1486                lead_time_days: Some(1),
1487            }],
1488            subtotal: rust_decimal_macros::dec!(12.00),
1489            tax_amount: Some(rust_decimal::Decimal::ZERO),
1490            shipping_amount: Some(rust_decimal::Decimal::ZERO),
1491            discount_amount: Some(rust_decimal::Decimal::ZERO),
1492            total: rust_decimal_macros::dec!(12.00),
1493            currency: Some(CurrencyCode::USD),
1494            payment_network: Some(X402Network::SetChain),
1495            payment_asset: Some(X402Asset::Usdc),
1496            shipping_address: None,
1497            valid_until: now + chrono::Duration::hours(1),
1498            notes: Some("lifecycle quote".to_string()),
1499            metadata: None,
1500        };
1501
1502        let cancelled_quote =
1503            commerce.x402().create_quote(make_quote(buyer_id, seller.id)).unwrap();
1504        let cancelled_quote =
1505            commerce.x402().update_quote_status(cancelled_quote.id, QuoteStatus::Quoted).unwrap();
1506
1507        let cancelled_purchase = commerce
1508            .x402()
1509            .create_purchase(CreateA2APurchase {
1510                buyer_agent_id: buyer_id,
1511                seller_agent_id: seller.id,
1512                quote_id: Some(cancelled_quote.id),
1513                items: cancelled_quote.items.clone(),
1514                total: cancelled_quote.total,
1515                currency: Some(cancelled_quote.currency),
1516                fulfillment_type: Some("digital".to_string()),
1517                notes: Some("cancel path".into()),
1518                metadata: None,
1519                payment_intent_id: None,
1520            })
1521            .unwrap();
1522
1523        let cancelled = commerce
1524            .x402()
1525            .update_purchase_status(cancelled_purchase.id, PurchaseStatus::Cancelled)
1526            .unwrap();
1527        assert_eq!(cancelled.status, PurchaseStatus::Cancelled);
1528
1529        let no_op = commerce
1530            .x402()
1531            .update_purchase_status(cancelled_purchase.id, PurchaseStatus::Cancelled)
1532            .unwrap();
1533        assert_eq!(no_op.status, PurchaseStatus::Cancelled);
1534
1535        assert!(
1536            commerce
1537                .x402()
1538                .update_purchase_status(cancelled_purchase.id, PurchaseStatus::PaymentPending)
1539                .is_err()
1540        );
1541        assert!(
1542            commerce
1543                .x402()
1544                .confirm_delivery(cancelled_purchase.id, "sig", Some(4), Some("should fail"))
1545                .is_err()
1546        );
1547
1548        let disputed_quote =
1549            commerce.x402().create_quote(make_quote(Uuid::new_v4(), seller.id)).unwrap();
1550        let disputed_quote =
1551            commerce.x402().update_quote_status(disputed_quote.id, QuoteStatus::Quoted).unwrap();
1552
1553        let disputed_purchase = commerce
1554            .x402()
1555            .create_purchase(CreateA2APurchase {
1556                buyer_agent_id: disputed_quote.buyer_agent_id,
1557                seller_agent_id: seller.id,
1558                quote_id: Some(disputed_quote.id),
1559                items: disputed_quote.items.clone(),
1560                total: disputed_quote.total,
1561                currency: Some(disputed_quote.currency),
1562                fulfillment_type: Some("digital".to_string()),
1563                notes: Some("dispute path".into()),
1564                metadata: None,
1565                payment_intent_id: None,
1566            })
1567            .unwrap();
1568
1569        let disputed = commerce
1570            .x402()
1571            .update_purchase_status(disputed_purchase.id, PurchaseStatus::Disputed)
1572            .unwrap();
1573        assert_eq!(disputed.status, PurchaseStatus::Disputed);
1574
1575        assert!(
1576            commerce
1577                .x402()
1578                .update_purchase_status(disputed_purchase.id, PurchaseStatus::Shipped)
1579                .is_err()
1580        );
1581        assert!(
1582            commerce
1583                .x402()
1584                .confirm_delivery(disputed_purchase.id, "sig", Some(4), Some("blocked"))
1585                .is_err()
1586        );
1587        assert!(
1588            commerce
1589                .x402()
1590                .update_purchase_status(disputed_purchase.id, PurchaseStatus::Disputed)
1591                .is_ok()
1592        );
1593    }
1594}