| 22 |
22 |
|
pub(in crate::routes::stripe) struct ProjectCheckoutForm {
|
| 23 |
23 |
|
#[serde(default)]
|
| 24 |
24 |
|
share_contact: bool,
|
| 25 |
|
- |
/// PWYW: buyer-chosen amount in cents.
|
| 26 |
|
- |
amount_cents: Option<i32>,
|
|
25 |
+ |
/// PWYW: buyer-chosen amount in dollars, exactly as typed into the paywall
|
|
26 |
+ |
/// box. The wire unit is dollars because the label the buyer reads says
|
|
27 |
+ |
/// dollars; the conversion to cents happens once, in
|
|
28 |
+ |
/// [`ProjectCheckoutForm::amount_cents`], right before the amount is
|
|
29 |
+ |
/// validated and charged.
|
|
30 |
+ |
amount_dollars: Option<String>,
|
|
31 |
+ |
}
|
|
32 |
+ |
|
|
33 |
+ |
impl ProjectCheckoutForm {
|
|
34 |
+ |
/// The buyer-chosen PWYW amount in cents, or `None` when the field was not
|
|
35 |
+ |
/// submitted at all.
|
|
36 |
+ |
///
|
|
37 |
+ |
/// Goes through `pricing::parse_dollars_to_cents`, the one canonical
|
|
38 |
+ |
/// dollars-to-cents conversion, so "5" is 500 cents rather than 5.
|
|
39 |
+ |
fn amount_cents(&self) -> Result<Option<i32>> {
|
|
40 |
+ |
self.amount_dollars
|
|
41 |
+ |
.as_deref()
|
|
42 |
+ |
.map(|raw| pricing::parse_dollars_to_cents("Amount", Some(raw)))
|
|
43 |
+ |
.transpose()
|
|
44 |
+ |
}
|
| 27 |
45 |
|
}
|
| 28 |
46 |
|
|
| 29 |
47 |
|
/// POST /stripe/checkout/project/{project_id}: Purchase project-level access.
|
| 80 |
98 |
|
|
| 81 |
99 |
|
// Determine price
|
| 82 |
100 |
|
let base_price_cents = if project_pricing.checkout_type() == CheckoutType::PayWhatYouWant {
|
| 83 |
|
- |
let amount = form.amount_cents.ok_or_else(|| {
|
|
101 |
+ |
let amount = form.amount_cents()?.ok_or_else(|| {
|
| 84 |
102 |
|
AppError::BadRequest("Amount is required for pay-what-you-want projects".to_string())
|
| 85 |
103 |
|
})?;
|
| 86 |
104 |
|
project_pricing
|
| 198 |
216 |
|
|
| 199 |
217 |
|
Ok(Redirect::to(&checkout_url).into_response())
|
| 200 |
218 |
|
}
|
|
219 |
+ |
|
|
220 |
+ |
#[cfg(test)]
|
|
221 |
+ |
mod tests {
|
|
222 |
+ |
//! The unit conversion on the PWYW paywall. The box is labelled dollars and
|
|
223 |
+ |
//! the wire carries dollars, so the handler owns the one multiplication that
|
|
224 |
+ |
//! turns what the buyer typed into what Stripe charges. Cents on the wire
|
|
225 |
+ |
//! under a dollars label charges a buyer who types 100 a single dollar.
|
|
226 |
+ |
|
|
227 |
+ |
use super::*;
|
|
228 |
+ |
|
|
229 |
+ |
fn form(amount: Option<&str>) -> ProjectCheckoutForm {
|
|
230 |
+ |
ProjectCheckoutForm {
|
|
231 |
+ |
share_contact: false,
|
|
232 |
+ |
amount_dollars: amount.map(str::to_string),
|
|
233 |
+ |
}
|
|
234 |
+ |
}
|
|
235 |
+ |
|
|
236 |
+ |
#[test]
|
|
237 |
+ |
fn a_whole_dollar_figure_becomes_cents() {
|
|
238 |
+ |
assert_eq!(form(Some("100")).amount_cents().unwrap(), Some(10_000));
|
|
239 |
+ |
assert_eq!(form(Some("5")).amount_cents().unwrap(), Some(500));
|
|
240 |
+ |
assert_eq!(form(Some("1")).amount_cents().unwrap(), Some(100));
|
|
241 |
+ |
}
|
|
242 |
+ |
|
|
243 |
+ |
#[test]
|
|
244 |
+ |
fn cents_typed_after_the_point_survive() {
|
|
245 |
+ |
assert_eq!(form(Some("9.99")).amount_cents().unwrap(), Some(999));
|
|
246 |
+ |
assert_eq!(form(Some("0.50")).amount_cents().unwrap(), Some(50));
|
|
247 |
+ |
assert_eq!(form(Some("1250.05")).amount_cents().unwrap(), Some(125_005));
|
|
248 |
+ |
}
|
|
249 |
+ |
|
|
250 |
+ |
#[test]
|
|
251 |
+ |
fn a_missing_field_is_distinct_from_an_empty_one() {
|
|
252 |
+ |
// Absent: the caller raises "Amount is required". Empty: zero, which the
|
|
253 |
+ |
// pricing model accepts only when the minimum is $0.
|
|
254 |
+ |
assert_eq!(form(None).amount_cents().unwrap(), None);
|
|
255 |
+ |
assert_eq!(form(Some("")).amount_cents().unwrap(), Some(0));
|
|
256 |
+ |
}
|
|
257 |
+ |
|
|
258 |
+ |
#[test]
|
|
259 |
+ |
fn junk_is_refused_rather_than_charged() {
|
|
260 |
+ |
for raw in ["abc", "-5", "NaN", "inf"] {
|
|
261 |
+ |
assert!(
|
|
262 |
+ |
form(Some(raw)).amount_cents().is_err(),
|
|
263 |
+ |
"{raw} must not reach the charge"
|
|
264 |
+ |
);
|
|
265 |
+ |
}
|
|
266 |
+ |
}
|
|
267 |
+ |
}
|