1use chrono::{DateTime, Duration, Utc};
29use rust_decimal::Decimal;
30use serde::{Deserialize, Serialize};
31use uuid::Uuid;
32
33use super::x402::{X402Asset, X402Network};
34
35#[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 #[default]
47 Pending,
48 Submitted,
50 Completed,
52 Failed,
54 Cancelled,
56 Refunded,
58}
59
60impl A2APaymentStatus {
61 #[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 #[must_use]
75 pub fn can_transition_to(self, target: Self) -> bool {
76 self.allowed_transitions().contains(&target)
77 }
78
79 #[must_use]
81 pub const fn is_terminal(self) -> bool {
82 matches!(self, Self::Cancelled | Self::Refunded)
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct A2APayment {
89 pub id: Uuid,
91
92 pub status: A2APaymentStatus,
94
95 pub sender_agent_id: Option<Uuid>,
100
101 pub sender_address: String,
103
104 pub recipient_agent_id: Option<Uuid>,
106
107 pub recipient_address: String,
109
110 pub amount: u64,
115
116 pub amount_decimal: Decimal,
118
119 pub asset: X402Asset,
121
122 pub network: X402Network,
124
125 pub memo: Option<String>,
130
131 pub reference_type: Option<A2AReferenceType>,
133
134 pub reference_id: Option<Uuid>,
136
137 pub idempotency_key: Option<String>,
139
140 pub intent_id: Option<Uuid>,
145
146 pub tx_hash: Option<String>,
148
149 pub block_number: Option<u64>,
151
152 pub metadata: Option<String>,
157
158 pub created_at: DateTime<Utc>,
160
161 pub updated_at: DateTime<Utc>,
163
164 pub completed_at: Option<DateTime<Utc>>,
166}
167
168impl A2APayment {
169 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 pub fn with_memo(mut self, memo: impl Into<String>) -> Self {
208 self.memo = Some(memo.into());
209 self
210 }
211
212 #[must_use]
214 pub const fn with_network(mut self, network: X402Network) -> Self {
215 self.network = network;
216 self
217 }
218
219 #[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 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#[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 Quote,
245 PaymentRequest,
247 Order,
249 Invoice,
251 ServiceCall,
253 Tip,
255 Refund,
257 Other,
259}
260
261#[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 #[default]
273 Pending,
274 Viewed,
276 Processing,
278 Paid,
280 Declined,
282 Expired,
284 Cancelled,
286}
287
288impl PaymentRequestStatus {
289 #[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 #[must_use]
304 pub fn can_transition_to(self, target: Self) -> bool {
305 self.allowed_transitions().contains(&target)
306 }
307
308 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct PaymentRequest {
318 pub id: Uuid,
320
321 pub status: PaymentRequestStatus,
323
324 pub requester_agent_id: Option<Uuid>,
329
330 pub requester_address: String,
332
333 pub payer_agent_id: Option<Uuid>,
335
336 pub payer_address: Option<String>,
338
339 pub amount: u64,
344
345 pub amount_decimal: Decimal,
347
348 pub asset: X402Asset,
350
351 pub accepted_networks: Vec<X402Network>,
353
354 pub description: String,
359
360 pub line_items: Option<String>,
362
363 pub reference_type: Option<A2AReferenceType>,
365
366 pub reference_id: Option<String>,
368
369 pub expires_at: DateTime<Utc>,
374
375 pub allow_partial: bool,
377
378 pub minimum_amount: Option<u64>,
380
381 pub amount_paid: u64,
386
387 pub payment_ids: Vec<Uuid>,
389
390 pub callback_url: Option<String>,
395
396 pub metadata: Option<String>,
401
402 pub created_at: DateTime<Utc>,
404
405 pub updated_at: DateTime<Utc>,
407
408 pub paid_at: Option<DateTime<Utc>>,
410}
411
412impl PaymentRequest {
413 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 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 #[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 #[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 #[must_use]
476 pub fn is_expired(&self) -> bool {
477 Utc::now() > self.expires_at
478 }
479
480 #[must_use]
482 pub const fn is_fully_paid(&self) -> bool {
483 self.amount_paid >= self.amount
484 }
485
486 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#[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 #[default]
511 Requested,
512 Quoted,
514 CounterOffered,
516 Accepted,
518 Declined,
520 Expired,
522 Fulfilled,
524 Cancelled,
526}
527
528impl A2AQuoteStatus {
529 #[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 #[must_use]
551 pub fn can_transition_to(self, target: Self) -> bool {
552 self.allowed_transitions().contains(&target)
553 }
554
555 #[must_use]
557 pub const fn is_terminal(self) -> bool {
558 matches!(self, Self::Declined | Self::Expired | Self::Fulfilled | Self::Cancelled)
559 }
560
561 #[must_use]
563 pub const fn allows_negotiation(self) -> bool {
564 matches!(self, Self::Quoted | Self::CounterOffered)
565 }
566}
567
568#[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 InitialQuote,
580 CounterOffer,
582 Revision,
584 Acceptance,
586 Decline,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct NegotiationEntry {
593 pub round: u32,
595 pub initiated_by: String,
597 pub negotiation_type: NegotiationType,
599 pub proposed_total: u64,
601 pub message: Option<String>,
603 pub timestamp: DateTime<Utc>,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct CounterA2AQuote {
610 pub quote_id: Uuid,
612 pub proposed_total: u64,
614 pub proposed_fees: Option<u64>,
616 pub message: Option<String>,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct ReviseA2AQuote {
623 pub quote_id: Uuid,
625 pub revised_total: u64,
627 pub revised_fees: Option<u64>,
629 pub revised_tax: Option<u64>,
631 pub expires_in_hours: Option<i64>,
633 pub message: Option<String>,
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
639pub struct DeclineA2AQuote {
640 pub quote_id: Uuid,
642 pub reason: Option<String>,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct A2AQuote {
649 pub id: Uuid,
651
652 pub status: A2AQuoteStatus,
654
655 pub buyer_agent_id: Option<Uuid>,
660
661 pub buyer_address: String,
663
664 pub seller_agent_id: Option<Uuid>,
666
667 pub seller_address: String,
669
670 pub items: Vec<A2AQuoteItem>,
675
676 pub subtotal: u64,
678
679 pub fees: u64,
681
682 pub tax: u64,
684
685 pub total: u64,
687
688 pub total_decimal: Decimal,
690
691 pub asset: X402Asset,
693
694 pub accepted_networks: Vec<X402Network>,
696
697 pub expires_at: DateTime<Utc>,
702
703 pub terms: Option<String>,
705
706 pub estimated_delivery: Option<String>,
711
712 pub delivery_method: Option<String>,
714
715 pub fulfillment_instructions: Option<String>,
717
718 pub payment_id: Option<Uuid>,
723
724 pub payment_request_id: Option<Uuid>,
726
727 pub counter_count: u32,
732
733 pub max_rounds: u32,
735
736 pub negotiation_history: Vec<NegotiationEntry>,
738
739 pub escrow_id: Option<Uuid>,
741
742 pub request_message: Option<String>,
747
748 pub response_message: Option<String>,
750
751 pub metadata: Option<String>,
753
754 pub created_at: DateTime<Utc>,
756
757 pub quoted_at: Option<DateTime<Utc>>,
759
760 pub accepted_at: Option<DateTime<Utc>>,
762
763 pub fulfilled_at: Option<DateTime<Utc>>,
765
766 pub updated_at: DateTime<Utc>,
768}
769
770impl A2AQuote {
771 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 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 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 #[must_use]
844 pub fn is_expired(&self) -> bool {
845 Utc::now() > self.expires_at
846 }
847
848 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 #[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 #[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 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 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 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 #[must_use]
945 pub const fn is_negotiation_limit_reached(&self) -> bool {
946 self.counter_count >= self.max_rounds
947 }
948}
949
950#[derive(Debug, Clone, Serialize, Deserialize)]
952pub struct A2AQuoteItem {
953 pub description: String,
955
956 pub sku: Option<String>,
958
959 pub quantity: u32,
961
962 pub unit_price: u64,
964
965 pub metadata: Option<String>,
967}
968
969impl A2AQuoteItem {
970 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 #[must_use]
977 pub const fn total(&self) -> u64 {
978 self.unit_price * self.quantity as u64
979 }
980}
981
982#[derive(Debug, Clone, Serialize, Deserialize)]
988pub struct A2AService {
989 pub id: Uuid,
991
992 pub agent_id: Uuid,
994
995 pub name: String,
997
998 pub description: String,
1000
1001 pub category: A2AServiceCategory,
1003
1004 pub pricing: A2APricing,
1006
1007 pub active: bool,
1009
1010 pub input_schema: Option<String>,
1012
1013 pub output_schema: Option<String>,
1015
1016 pub endpoint_url: Option<String>,
1018
1019 pub avg_response_time: Option<u32>,
1021
1022 pub success_rate: Option<f32>,
1024
1025 pub transaction_count: u64,
1027
1028 pub metadata: Option<String>,
1030
1031 pub created_at: DateTime<Utc>,
1033
1034 pub updated_at: DateTime<Utc>,
1036}
1037
1038#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1040#[serde(rename_all = "snake_case")]
1041#[non_exhaustive]
1042pub enum A2AServiceCategory {
1043 Data,
1045 Compute,
1047 Api,
1049 Content,
1051 Analysis,
1053 Goods,
1055 DigitalGoods,
1057 Other,
1059}
1060
1061#[derive(Debug, Clone, Serialize, Deserialize)]
1063#[serde(tag = "model", rename_all = "snake_case")]
1064#[non_exhaustive]
1065pub enum A2APricing {
1066 Fixed { amount: u64, asset: X402Asset, unit: String },
1068 PerUnit { amount_per_unit: u64, asset: X402Asset, unit: String },
1070 Tiered { tiers: Vec<PricingTier>, asset: X402Asset },
1072 Quote,
1074 Freemium { free_quota: u64, unit: String, overage_price: u64, asset: X402Asset },
1076}
1077
1078#[derive(Debug, Clone, Serialize, Deserialize)]
1080pub struct PricingTier {
1081 pub up_to: Option<u64>,
1082 pub price_per_unit: u64,
1083}
1084
1085#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1091pub struct CreateA2APayment {
1092 pub recipient_agent_id: Option<Uuid>,
1094 pub recipient_address: Option<String>,
1095
1096 pub amount: Option<u64>,
1098 pub amount_decimal: Option<Decimal>,
1099
1100 pub asset: Option<X402Asset>,
1102
1103 pub network: Option<X402Network>,
1105
1106 pub memo: Option<String>,
1108
1109 pub reference_type: Option<A2AReferenceType>,
1111 pub reference_id: Option<Uuid>,
1112
1113 pub idempotency_key: Option<String>,
1115
1116 pub metadata: Option<String>,
1118}
1119
1120#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1122pub struct CreatePaymentRequest {
1123 pub payer_agent_id: Option<Uuid>,
1125 pub payer_address: Option<String>,
1126
1127 pub amount: Option<u64>,
1129 pub amount_decimal: Option<Decimal>,
1130
1131 pub asset: Option<X402Asset>,
1133
1134 pub accepted_networks: Option<Vec<X402Network>>,
1136
1137 pub description: String,
1139
1140 pub line_items: Option<Vec<PaymentRequestLineItem>>,
1142
1143 pub expires_in_hours: Option<i64>,
1145
1146 pub allow_partial: Option<bool>,
1148 pub minimum_amount: Option<u64>,
1149
1150 pub callback_url: Option<String>,
1152
1153 pub metadata: Option<String>,
1155}
1156
1157#[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#[derive(Debug, Clone, Serialize, Deserialize)]
1168pub struct RequestA2AQuote {
1169 pub seller_agent_id: Option<Uuid>,
1171 pub seller_address: Option<String>,
1172
1173 pub items: Vec<QuoteItemRequest>,
1175
1176 pub asset: Option<X402Asset>,
1178
1179 pub message: Option<String>,
1181
1182 pub metadata: Option<String>,
1184}
1185
1186#[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#[derive(Debug, Clone, Serialize, Deserialize)]
1197pub struct ProvideA2AQuote {
1198 pub quote_id: Uuid,
1200
1201 pub items: Option<Vec<A2AQuoteItem>>,
1203
1204 pub total: u64,
1206
1207 pub fees: Option<u64>,
1209
1210 pub tax: Option<u64>,
1212
1213 pub expires_in_hours: Option<i64>,
1215
1216 pub terms: Option<String>,
1218
1219 pub estimated_delivery: Option<String>,
1221
1222 pub message: Option<String>,
1224}
1225
1226#[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#[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#[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#[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); 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 quote.accept();
1328 assert_eq!(quote.status, A2AQuoteStatus::Accepted);
1329 }
1330
1331 #[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 #[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 #[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 #[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 quote.provide_quote(1_100_000, 50_000, 50_000, 24);
1483 assert_eq!(quote.status, A2AQuoteStatus::Quoted);
1484
1485 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 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 assert!(quote.counter_offer(150, None));
1529 quote.revise(175, 0, 0, 24, None);
1530
1531 assert!(quote.counter_offer(160, None));
1533
1534 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 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 quote.provide_quote(12_000_000, 500_000, 500_000, 48);
1572
1573 quote.counter_offer(10_500_000, Some("Budget limited".into()));
1575
1576 quote.revise(11_000_000, 300_000, 200_000, 24, Some("Final offer".into()));
1578
1579 quote.accept();
1581 assert_eq!(quote.status, A2AQuoteStatus::Accepted);
1582 assert_eq!(quote.negotiation_history.len(), 2); quote.fulfill();
1586 assert_eq!(quote.status, A2AQuoteStatus::Fulfilled);
1587 assert!(quote.status.is_terminal());
1588 }
1589}