solid_pod_rs/wac/
payment.rs1use serde::{Deserialize, Serialize};
18
19use crate::wac::conditions::{ConditionOutcome, RequestContext};
20
21#[derive(Debug, Clone, Default, Deserialize, Serialize)]
27pub struct PaymentConditionBody {
28 #[serde(
30 rename = "acl:costSats",
31 default,
32 deserialize_with = "deserialize_cost_sats"
33 )]
34 pub cost_sats: u64,
35}
36
37fn deserialize_cost_sats<'de, D: serde::Deserializer<'de>>(de: D) -> Result<u64, D::Error> {
41 let v: serde_json::Value = Deserialize::deserialize(de)?;
42 match &v {
43 serde_json::Value::Number(n) => n
44 .as_u64()
45 .ok_or_else(|| serde::de::Error::custom("acl:costSats must be a non-negative integer")),
46 serde_json::Value::String(s) => s
47 .parse::<u64>()
48 .map_err(|_| serde::de::Error::custom("acl:costSats string must parse as u64")),
49 _ => Err(serde::de::Error::custom(
50 "acl:costSats must be a number or string",
51 )),
52 }
53}
54
55#[derive(Debug, Default, Clone, Copy)]
61pub struct PaymentConditionEvaluator;
62
63impl PaymentConditionEvaluator {
64 pub fn evaluate(
71 &self,
72 body: &PaymentConditionBody,
73 ctx: &RequestContext<'_>,
74 ) -> ConditionOutcome {
75 match ctx.payment_balance_sats {
76 Some(balance) if balance >= body.cost_sats => ConditionOutcome::Satisfied,
77 _ => ConditionOutcome::Denied,
78 }
79 }
80}
81
82pub fn total_payment_cost(conditions: &[crate::wac::conditions::Condition]) -> u64 {
86 conditions
87 .iter()
88 .filter_map(|c| match c {
89 crate::wac::conditions::Condition::Payment(body) => Some(body.cost_sats),
90 _ => None,
91 })
92 .sum()
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn evaluator_satisfied_when_balance_sufficient() {
101 let body = PaymentConditionBody { cost_sats: 100 };
102 let ctx = RequestContext {
103 web_id: Some("did:nostr:alice"),
104 client_id: None,
105 issuer: None,
106 payment_balance_sats: Some(100),
107 };
108 assert_eq!(
109 PaymentConditionEvaluator.evaluate(&body, &ctx),
110 ConditionOutcome::Satisfied
111 );
112 }
113
114 #[test]
115 fn evaluator_satisfied_when_balance_exceeds_cost() {
116 let body = PaymentConditionBody { cost_sats: 50 };
117 let ctx = RequestContext {
118 web_id: Some("did:nostr:alice"),
119 client_id: None,
120 issuer: None,
121 payment_balance_sats: Some(1000),
122 };
123 assert_eq!(
124 PaymentConditionEvaluator.evaluate(&body, &ctx),
125 ConditionOutcome::Satisfied
126 );
127 }
128
129 #[test]
130 fn evaluator_denied_when_balance_insufficient() {
131 let body = PaymentConditionBody { cost_sats: 100 };
132 let ctx = RequestContext {
133 web_id: Some("did:nostr:alice"),
134 client_id: None,
135 issuer: None,
136 payment_balance_sats: Some(50),
137 };
138 assert_eq!(
139 PaymentConditionEvaluator.evaluate(&body, &ctx),
140 ConditionOutcome::Denied
141 );
142 }
143
144 #[test]
145 fn evaluator_denied_when_no_payment_context() {
146 let body = PaymentConditionBody { cost_sats: 1 };
147 let ctx = RequestContext {
148 web_id: Some("did:nostr:alice"),
149 client_id: None,
150 issuer: None,
151 payment_balance_sats: None,
152 };
153 assert_eq!(
154 PaymentConditionEvaluator.evaluate(&body, &ctx),
155 ConditionOutcome::Denied
156 );
157 }
158
159 #[test]
160 fn evaluator_zero_cost_always_satisfied() {
161 let body = PaymentConditionBody { cost_sats: 0 };
162 let ctx = RequestContext {
163 web_id: Some("did:nostr:alice"),
164 client_id: None,
165 issuer: None,
166 payment_balance_sats: Some(0),
167 };
168 assert_eq!(
169 PaymentConditionEvaluator.evaluate(&body, &ctx),
170 ConditionOutcome::Satisfied
171 );
172 }
173
174 #[test]
175 fn total_payment_cost_sums_conditions() {
176 use crate::wac::conditions::Condition;
177
178 let conditions = vec![
179 Condition::Payment(PaymentConditionBody { cost_sats: 100 }),
180 Condition::Client(crate::wac::client::ClientConditionBody::default()),
181 Condition::Payment(PaymentConditionBody { cost_sats: 50 }),
182 ];
183 assert_eq!(total_payment_cost(&conditions), 150);
184 }
185
186 #[test]
187 fn total_payment_cost_zero_when_no_payment_conditions() {
188 use crate::wac::conditions::Condition;
189
190 let conditions = vec![Condition::Client(
191 crate::wac::client::ClientConditionBody::default(),
192 )];
193 assert_eq!(total_payment_cost(&conditions), 0);
194 }
195
196 #[test]
197 fn deserialize_from_json() {
198 let json = r#"{"@type": "acl:PaymentCondition", "acl:costSats": 42}"#;
199 let body: PaymentConditionBody =
200 serde_json::from_str(json).expect("deserialize PaymentConditionBody");
201 assert_eq!(body.cost_sats, 42);
202 }
203
204 #[test]
205 fn deserialize_cost_as_string() {
206 let json = r#"{"@type": "acl:PaymentCondition", "acl:costSats": "100"}"#;
207 let body: PaymentConditionBody =
208 serde_json::from_str(json).expect("deserialize from string cost");
209 assert_eq!(body.cost_sats, 100);
210 }
211
212 #[test]
213 fn serialize_roundtrip() {
214 let body = PaymentConditionBody { cost_sats: 500 };
215 let json = serde_json::to_string(&body).unwrap();
216 assert!(json.contains("500"));
217 let parsed: PaymentConditionBody = serde_json::from_str(&json).unwrap();
218 assert_eq!(parsed.cost_sats, 500);
219 }
220}