1use serde::{Deserialize, Serialize};
13
14use crate::error::CoreError;
15
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct SpendIntent {
18 pub delegation_id: String,
20 pub nonce: u64,
22 pub amount_cents: u64,
24 pub merchant_id: String,
26 pub category: String,
28 pub memo: String,
30}
31
32impl SpendIntent {
33 pub fn new(
34 delegation_id: impl Into<String>,
35 nonce: u64,
36 amount_cents: u64,
37 merchant_id: impl Into<String>,
38 category: impl Into<String>,
39 memo: impl Into<String>,
40 ) -> Self {
41 Self {
42 delegation_id: delegation_id.into(),
43 nonce,
44 amount_cents,
45 merchant_id: merchant_id.into(),
46 category: category.into(),
47 memo: memo.into(),
48 }
49 }
50
51 pub fn validate(&self) -> Result<(), CoreError> {
53 if self.delegation_id.trim().is_empty() {
54 return Err(CoreError::InvalidIntent("delegation_id 不能为空".into()));
55 }
56 if self.merchant_id.trim().is_empty() {
57 return Err(CoreError::InvalidIntent("merchant_id 不能为空".into()));
58 }
59 if self.amount_cents == 0 {
60 return Err(CoreError::InvalidIntent(
61 "amount_cents 不能为 0(单位是分)".into(),
62 ));
63 }
64 if self.nonce == 0 {
65 return Err(CoreError::InvalidIntent(
66 "nonce 不能为 0(防重放 nonce 从 1 起,对齐 mist-core 断言 7)".into(),
67 ));
68 }
69 Ok(())
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 fn sample() -> SpendIntent {
78 SpendIntent::new("d1", 1, 500, "jd:shop-1", "grocery", "早饭")
79 }
80
81 #[test]
82 fn validate_accepts_well_formed_intent() {
83 assert_eq!(sample().validate(), Ok(()));
84 }
85
86 #[test]
87 fn validate_rejects_zero_amount() {
88 let i = SpendIntent {
89 amount_cents: 0,
90 ..sample()
91 };
92 assert!(matches!(i.validate(), Err(CoreError::InvalidIntent(_))));
93 }
94
95 #[test]
96 fn validate_rejects_zero_nonce() {
97 let i = SpendIntent {
98 nonce: 0,
99 ..sample()
100 };
101 assert!(matches!(i.validate(), Err(CoreError::InvalidIntent(_))));
102 }
103
104 #[test]
105 fn validate_rejects_empty_fields() {
106 for bad in [
107 SpendIntent {
108 delegation_id: "".into(),
109 ..sample()
110 },
111 SpendIntent {
112 merchant_id: " ".into(),
113 ..sample()
114 },
115 ] {
116 assert!(
117 matches!(bad.validate(), Err(CoreError::InvalidIntent(_))),
118 "应拒收空字段: {bad:?}"
119 );
120 }
121 }
122
123 #[test]
124 fn serde_roundtrip() {
125 let i = sample();
126 let json = serde_json::to_string(&i).expect("序列化");
127 let back: SpendIntent = serde_json::from_str(&json).expect("反序列化");
128 assert_eq!(back, i);
129 assert!(json.contains("\"amount_cents\":500"));
130 assert!(json.contains("\"nonce\":1"));
131 }
132}