paykit_lib/allowance/
evaluation.rs1use super::{
4 allowance_period_window,
5 amounts::{add_decimals, compare_decimals},
6 AllowanceEvaluation, AllowanceEvaluationBlock, AllowanceEvaluationInput, AllowanceTerms,
7};
8use crate::{validation::parse_utc_timestamp, PaymentEndpointIdentifier, PaymentRequestTerms};
9use chrono::{DateTime, Utc};
10
11pub fn match_allowance_request(
16 terms: &AllowanceTerms,
17 request: &PaymentRequestTerms,
18) -> std::result::Result<Vec<PaymentEndpointIdentifier>, AllowanceEvaluationBlock> {
19 request
20 .validate()
21 .map_err(|_| AllowanceEvaluationBlock::InvalidRequest)?;
22 if request.amount().asset() != terms.asset() {
23 return Err(AllowanceEvaluationBlock::AssetMismatch);
24 }
25 if let Some(range) = terms.per_payment_amount() {
26 if compare_decimals(request.amount().value(), range.minimum()).is_lt()
27 || compare_decimals(request.amount().value(), range.maximum()).is_gt()
28 {
29 return Err(AllowanceEvaluationBlock::AmountOutsideRange);
30 }
31 }
32 let mut endpoints = Vec::new();
33 for endpoint in request.accepted_payment_endpoint_identifiers() {
34 if let Some(crate::PaymentConversion::Fixed { rates }) = request.conversion() {
37 if crate::ConversionRate::for_endpoint(rates, endpoint)
38 .map_err(|_| AllowanceEvaluationBlock::InvalidRequest)?
39 .is_some()
40 {
41 continue;
42 }
43 }
44 if terms
45 .allowed_payment_endpoint_identifiers()
46 .is_none_or(|allowed| allowed.contains(endpoint))
47 && !endpoints.contains(endpoint)
48 {
49 endpoints.push(endpoint.clone());
50 }
51 }
52 if endpoints.is_empty() {
53 return Err(AllowanceEvaluationBlock::NoEligibleEndpoint);
54 }
55 Ok(endpoints)
56}
57
58pub fn evaluate_allowance(
65 input: &AllowanceEvaluationInput<'_>,
66) -> std::result::Result<AllowanceEvaluation, AllowanceEvaluationBlock> {
67 let endpoints = match_allowance_request(input.terms, input.request)?;
68 check_allowance_time(input.terms, input.trusted_time, input.watermark)?;
69 validate_usage(input)?;
70 check_lifetime_limit(input)?;
71 check_period_limits(input)?;
72 Ok(AllowanceEvaluation {
73 eligible_payment_endpoint_identifiers: endpoints,
74 })
75}
76
77pub fn check_allowance_time(
83 terms: &AllowanceTerms,
84 trusted_time: DateTime<Utc>,
85 watermark: DateTime<Utc>,
86) -> std::result::Result<(), AllowanceEvaluationBlock> {
87 if trusted_time < watermark {
88 return Err(AllowanceEvaluationBlock::ClockRollback);
89 }
90 if trusted_time.timestamp_subsec_nanos() >= 1_000_000_000
91 || watermark.timestamp_subsec_nanos() >= 1_000_000_000
92 {
93 return Err(AllowanceEvaluationBlock::ArithmeticOverflow);
94 }
95 if parse_time_bound(terms.active_from())?.is_some_and(|start| trusted_time < start) {
96 return Err(AllowanceEvaluationBlock::NotActive);
97 }
98 if parse_time_bound(terms.expires_at())?.is_some_and(|end| trusted_time >= end) {
99 return Err(AllowanceEvaluationBlock::Expired);
100 }
101 Ok(())
102}
103
104fn parse_time_bound(
105 bound: Option<&str>,
106) -> std::result::Result<Option<chrono::DateTime<chrono::Utc>>, AllowanceEvaluationBlock> {
107 bound
108 .map(|value| {
109 let time = parse_utc_timestamp(value, "Allowance time bound")
110 .map_err(|_| AllowanceEvaluationBlock::ArithmeticOverflow)?;
111 if time.timestamp_subsec_nanos() >= 1_000_000_000 {
112 return Err(AllowanceEvaluationBlock::ArithmeticOverflow);
113 }
114 Ok(time.with_timezone(&chrono::Utc))
115 })
116 .transpose()
117}
118
119fn validate_usage(
120 input: &AllowanceEvaluationInput<'_>,
121) -> std::result::Result<(), AllowanceEvaluationBlock> {
122 for entry in input.usage {
123 if entry.admitted_at() > input.trusted_time {
124 return Err(AllowanceEvaluationBlock::FutureUsage);
125 }
126 if entry.amount().asset() != input.terms.asset() {
127 return Err(AllowanceEvaluationBlock::UsageAssetMismatch);
128 }
129 }
130 Ok(())
131}
132
133fn check_lifetime_limit(
134 input: &AllowanceEvaluationInput<'_>,
135) -> std::result::Result<(), AllowanceEvaluationBlock> {
136 if let Some(limit) = input.terms.lifetime_amount_limit() {
137 let total = input
138 .usage
139 .iter()
140 .fold(input.request.amount().value().to_owned(), |sum, entry| {
141 add_decimals(&sum, entry.amount().value())
142 });
143 if compare_decimals(&total, limit).is_gt() {
144 return Err(AllowanceEvaluationBlock::LifetimeAmountLimit);
145 }
146 }
147 Ok(())
148}
149
150fn check_period_limits(
151 input: &AllowanceEvaluationInput<'_>,
152) -> std::result::Result<(), AllowanceEvaluationBlock> {
153 for (index, limit) in input.terms.period_limits().iter().enumerate() {
154 let window = allowance_period_window(limit.period(), input.trusted_time)?;
155 let mut total = input.request.amount().value().to_owned();
156 let mut count = 1_u64;
157 for entry in input
158 .usage
159 .iter()
160 .filter(|entry| window.contains(entry.admitted_at()))
161 {
162 total = add_decimals(&total, entry.amount().value());
163 count = count
164 .checked_add(1)
165 .ok_or(AllowanceEvaluationBlock::ArithmeticOverflow)?;
166 }
167 if limit
168 .amount_limit()
169 .is_some_and(|amount| compare_decimals(&total, amount).is_gt())
170 {
171 return Err(AllowanceEvaluationBlock::PeriodAmountLimit { index });
172 }
173 if limit
174 .payment_count_limit()
175 .is_some_and(|maximum| count > maximum)
176 {
177 return Err(AllowanceEvaluationBlock::PeriodCountLimit { index });
178 }
179 }
180 Ok(())
181}