Skip to main content

max / makenotwork

21.7 KB · 667 lines History Blame Raw
1 //! Parsed metadata types for Stripe Checkout sessions, plus type-discriminator helpers.
2 //!
3 //! Each variant (`CheckoutMetadata`, `SubscriptionCheckoutMetadata`, etc.) corresponds
4 //! to one of the `CheckoutType` flavors set in the session's `metadata.checkout_type`
5 //! field at creation time. The `from_metadata` constructors parse the relevant fields
6 //! back out at webhook time.
7
8 use std::collections::HashMap;
9
10 use crate::db::{
11 CheckoutType, ItemId, ProjectId, PromoCodeId, SubscriptionTierId, SyncAppId, UserId,
12 };
13 use crate::error::{AppError, Result};
14
15 /// Convenience alias for the metadata pulled off a Stripe `CheckoutSession`.
16 pub type CheckoutMetaMap = HashMap<String, String>;
17
18 fn require<'a>(meta: Option<&'a CheckoutMetaMap>, key: &str) -> Result<&'a String> {
19 meta.and_then(|m| m.get(key))
20 .ok_or_else(|| AppError::BadRequest(format!("Missing {key} in metadata")))
21 }
22
23 fn parse_uuid_to<T: From<uuid::Uuid>>(value: &str, field: &'static str) -> Result<T> {
24 value
25 .parse::<uuid::Uuid>()
26 .map(T::from)
27 .map_err(|_| AppError::BadRequest(format!("Invalid {field} format")))
28 }
29
30 /// Parsed metadata from a one-time purchase checkout session.
31 #[derive(Debug)]
32 pub struct CheckoutMetadata {
33 pub buyer_id: UserId,
34 pub seller_id: UserId,
35 pub item_id: Option<ItemId>,
36 pub promo_code_id: Option<PromoCodeId>,
37 }
38
39 impl CheckoutMetadata {
40 pub fn from_metadata(meta: Option<&CheckoutMetaMap>) -> Result<Self> {
41 let buyer_id: UserId = parse_uuid_to(require(meta, "buyer_id")?, "buyer_id")?;
42 let seller_id: UserId = parse_uuid_to(require(meta, "seller_id")?, "seller_id")?;
43 let item_id = meta
44 .and_then(|m| m.get("item_id"))
45 .and_then(|v| v.parse::<uuid::Uuid>().ok().map(ItemId::from));
46 let promo_code_id = meta
47 .and_then(|m| m.get("promo_code_id"))
48 .and_then(|v| v.parse::<uuid::Uuid>().ok().map(PromoCodeId::from));
49 Ok(CheckoutMetadata {
50 buyer_id,
51 seller_id,
52 item_id,
53 promo_code_id,
54 })
55 }
56 }
57
58 /// Parsed metadata from a subscription checkout session.
59 #[derive(Debug)]
60 pub struct SubscriptionCheckoutMetadata {
61 pub subscriber_id: UserId,
62 pub project_id: ProjectId,
63 pub tier_id: SubscriptionTierId,
64 pub promo_code_id: Option<PromoCodeId>,
65 }
66
67 impl SubscriptionCheckoutMetadata {
68 pub fn from_metadata(meta: Option<&CheckoutMetaMap>) -> Result<Self> {
69 let subscriber_id: UserId =
70 parse_uuid_to(require(meta, "subscriber_id")?, "subscriber_id")?;
71 let project_id: ProjectId = parse_uuid_to(require(meta, "project_id")?, "project_id")?;
72 let tier_id: SubscriptionTierId = parse_uuid_to(require(meta, "tier_id")?, "tier_id")?;
73 let promo_code_id = meta
74 .and_then(|m| m.get("promo_code_id"))
75 .and_then(|v| v.parse::<uuid::Uuid>().ok().map(PromoCodeId::from));
76 Ok(SubscriptionCheckoutMetadata {
77 subscriber_id,
78 project_id,
79 tier_id,
80 promo_code_id,
81 })
82 }
83 }
84
85 /// Parsed metadata from a Fan+ checkout session.
86 #[derive(Debug)]
87 pub struct FanPlusCheckoutMetadata {
88 pub user_id: UserId,
89 }
90
91 impl FanPlusCheckoutMetadata {
92 pub fn from_metadata(meta: Option<&CheckoutMetaMap>) -> Result<Self> {
93 let user_id: UserId = parse_uuid_to(require(meta, "user_id")?, "user_id")?;
94 Ok(FanPlusCheckoutMetadata { user_id })
95 }
96 }
97
98 /// Parsed metadata from a creator tier checkout session.
99 #[derive(Debug)]
100 pub struct CreatorTierCheckoutMetadata {
101 pub user_id: UserId,
102 pub tier: String,
103 }
104
105 impl CreatorTierCheckoutMetadata {
106 pub fn from_metadata(meta: Option<&CheckoutMetaMap>) -> Result<Self> {
107 let user_id: UserId = parse_uuid_to(require(meta, "user_id")?, "user_id")?;
108 let tier = require(meta, "tier")?.clone();
109 Ok(CreatorTierCheckoutMetadata { user_id, tier })
110 }
111 }
112
113 /// Parsed metadata from a tip checkout session.
114 #[derive(Debug)]
115 pub struct TipCheckoutMetadata {
116 pub tipper_id: UserId,
117 pub recipient_id: UserId,
118 pub project_id: Option<ProjectId>,
119 pub message: Option<String>,
120 }
121
122 impl TipCheckoutMetadata {
123 pub fn from_metadata(meta: Option<&CheckoutMetaMap>) -> Result<Self> {
124 let tipper_id: UserId = parse_uuid_to(require(meta, "tipper_id")?, "tipper_id")?;
125 let recipient_id: UserId = parse_uuid_to(require(meta, "recipient_id")?, "recipient_id")?;
126 let project_id = meta
127 .and_then(|m| m.get("project_id"))
128 .and_then(|v| v.parse::<uuid::Uuid>().ok().map(ProjectId::from));
129 let message = meta.and_then(|m| m.get("message")).cloned();
130 Ok(TipCheckoutMetadata {
131 tipper_id,
132 recipient_id,
133 project_id,
134 message,
135 })
136 }
137 }
138
139 /// Parsed metadata from a cart (multi-item) checkout session.
140 #[derive(Debug)]
141 pub struct CartCheckoutMetadata {
142 pub buyer_id: UserId,
143 pub seller_id: UserId,
144 }
145
146 impl CartCheckoutMetadata {
147 pub fn from_metadata(meta: Option<&CheckoutMetaMap>) -> Result<Self> {
148 let buyer_id: UserId = parse_uuid_to(require(meta, "buyer_id")?, "buyer_id")?;
149 let seller_id: UserId = parse_uuid_to(require(meta, "seller_id")?, "seller_id")?;
150 Ok(CartCheckoutMetadata {
151 buyer_id,
152 seller_id,
153 })
154 }
155 }
156
157 /// Parsed metadata from a guest checkout session.
158 #[derive(Debug)]
159 pub struct GuestCheckoutMetadata {
160 pub seller_id: UserId,
161 pub item_id: ItemId,
162 pub promo_code_id: Option<PromoCodeId>,
163 }
164
165 impl GuestCheckoutMetadata {
166 pub fn from_metadata(meta: Option<&CheckoutMetaMap>) -> Result<Self> {
167 let seller_id: UserId = parse_uuid_to(require(meta, "seller_id")?, "seller_id")?;
168 let item_id: ItemId = parse_uuid_to(require(meta, "item_id")?, "item_id")?;
169 let promo_code_id = meta
170 .and_then(|m| m.get("promo_code_id"))
171 .and_then(|v| v.parse::<uuid::Uuid>().ok().map(PromoCodeId::from));
172 Ok(GuestCheckoutMetadata {
173 seller_id,
174 item_id,
175 promo_code_id,
176 })
177 }
178 }
179
180 /// Extract the checkout type from a Stripe session's metadata.
181 pub fn get_checkout_type(meta: Option<&CheckoutMetaMap>) -> Option<CheckoutType> {
182 meta.and_then(|m| m.get("checkout_type"))
183 .and_then(|t| t.parse().ok())
184 }
185
186 pub fn is_tip_checkout(meta: Option<&CheckoutMetaMap>) -> bool {
187 get_checkout_type(meta) == Some(CheckoutType::Tip)
188 }
189
190 pub fn is_fan_plus_checkout(meta: Option<&CheckoutMetaMap>) -> bool {
191 get_checkout_type(meta) == Some(CheckoutType::FanPlus)
192 }
193
194 pub fn is_creator_tier_checkout(meta: Option<&CheckoutMetaMap>) -> bool {
195 get_checkout_type(meta) == Some(CheckoutType::CreatorTier)
196 }
197
198 pub fn is_subscription_checkout(meta: Option<&CheckoutMetaMap>) -> bool {
199 get_checkout_type(meta) == Some(CheckoutType::Subscription)
200 }
201
202 pub fn is_guest_checkout(meta: Option<&CheckoutMetaMap>) -> bool {
203 get_checkout_type(meta) == Some(CheckoutType::Guest)
204 }
205
206 pub fn is_cart_checkout(meta: Option<&CheckoutMetaMap>) -> bool {
207 get_checkout_type(meta) == Some(CheckoutType::Cart)
208 }
209
210 pub fn is_synckit_app_sub_checkout(meta: Option<&CheckoutMetaMap>) -> bool {
211 get_checkout_type(meta) == Some(CheckoutType::SynckitAppSub)
212 }
213
214 /// Parsed metadata for an end-user subscribing to an app's cloud sync. The
215 /// billing cadence is carried as `interval` ("monthly"/"annual"); a SyncKit app
216 /// sub has no creator-tier concept.
217 #[derive(Debug)]
218 pub struct SynckitAppSubCheckoutMetadata {
219 pub user_id: UserId,
220 pub app_id: SyncAppId,
221 pub interval: String,
222 pub storage_limit_bytes: Option<i64>,
223 }
224
225 impl SynckitAppSubCheckoutMetadata {
226 pub fn from_metadata(meta: Option<&CheckoutMetaMap>) -> Result<Self> {
227 let user_id: UserId = parse_uuid_to(require(meta, "user_id")?, "user_id")?;
228 let app_id: SyncAppId = parse_uuid_to(require(meta, "app_id")?, "app_id")?;
229 // Read the billing cadence from `interval`, falling back to the legacy
230 // `tier` key that older checkout sessions (created before this key was
231 // corrected) still carry, so a session in flight across the deploy
232 // still settles.
233 let interval = match meta.and_then(|m| m.get("interval")) {
234 Some(v) => v.clone(),
235 None => require(meta, "tier")?.clone(),
236 };
237 let storage_limit_bytes = meta
238 .and_then(|m| m.get("storage_limit_bytes"))
239 .and_then(|v| v.parse::<i64>().ok());
240 Ok(SynckitAppSubCheckoutMetadata {
241 user_id,
242 app_id,
243 interval,
244 storage_limit_bytes,
245 })
246 }
247 }
248
249 #[cfg(test)]
250 mod tests {
251 use super::*;
252
253 fn meta_of(entries: &[(&str, &str)]) -> CheckoutMetaMap {
254 entries
255 .iter()
256 .map(|(k, v)| (k.to_string(), v.to_string()))
257 .collect()
258 }
259
260 // --- SynckitAppSubCheckoutMetadata: interval key + legacy `tier` fallback ---
261
262 #[test]
263 fn synckit_app_sub_reads_interval_key() {
264 let user = UserId::new();
265 let app = SyncAppId::new();
266 let m = meta_of(&[
267 ("user_id", &user.to_string()),
268 ("app_id", &app.to_string()),
269 ("interval", "annual"),
270 ("storage_limit_bytes", "1073741824"),
271 ]);
272 let parsed = SynckitAppSubCheckoutMetadata::from_metadata(Some(&m)).unwrap();
273 assert_eq!(parsed.interval, "annual");
274 assert_eq!(parsed.storage_limit_bytes, Some(1_073_741_824));
275 }
276
277 #[test]
278 fn synckit_app_sub_falls_back_to_legacy_tier_key() {
279 // Sessions created before the key was corrected carry the cadence under
280 // `tier`; those must still settle after the deploy.
281 let user = UserId::new();
282 let app = SyncAppId::new();
283 let m = meta_of(&[
284 ("user_id", &user.to_string()),
285 ("app_id", &app.to_string()),
286 ("tier", "monthly"),
287 ]);
288 let parsed = SynckitAppSubCheckoutMetadata::from_metadata(Some(&m)).unwrap();
289 assert_eq!(parsed.interval, "monthly");
290 }
291
292 // --- CheckoutMetadata ---
293
294 #[test]
295 fn from_metadata_valid() {
296 let buyer = UserId::new();
297 let seller = UserId::new();
298 let item = ItemId::new();
299 let m = meta_of(&[
300 ("buyer_id", &buyer.to_string()),
301 ("seller_id", &seller.to_string()),
302 ("item_id", &item.to_string()),
303 ]);
304 let r = CheckoutMetadata::from_metadata(Some(&m)).unwrap();
305 assert_eq!(r.buyer_id, buyer);
306 assert_eq!(r.seller_id, seller);
307 assert_eq!(r.item_id, Some(item));
308 }
309
310 #[test]
311 fn from_metadata_missing_metadata() {
312 assert!(CheckoutMetadata::from_metadata(None).is_err());
313 }
314
315 #[test]
316 fn from_metadata_missing_buyer_id() {
317 let m = meta_of(&[
318 ("seller_id", &UserId::new().to_string()),
319 ("item_id", &ItemId::new().to_string()),
320 ]);
321 assert!(CheckoutMetadata::from_metadata(Some(&m)).is_err());
322 }
323
324 #[test]
325 fn from_metadata_missing_seller_id() {
326 let m = meta_of(&[
327 ("buyer_id", &UserId::new().to_string()),
328 ("item_id", &ItemId::new().to_string()),
329 ]);
330 assert!(CheckoutMetadata::from_metadata(Some(&m)).is_err());
331 }
332
333 #[test]
334 fn from_metadata_invalid_uuid_format() {
335 let m = meta_of(&[
336 ("buyer_id", "not-a-uuid"),
337 ("seller_id", &UserId::new().to_string()),
338 ("item_id", &ItemId::new().to_string()),
339 ]);
340 assert!(CheckoutMetadata::from_metadata(Some(&m)).is_err());
341 }
342
343 #[test]
344 fn from_metadata_empty() {
345 let m = HashMap::new();
346 assert!(CheckoutMetadata::from_metadata(Some(&m)).is_err());
347 }
348
349 // --- SubscriptionCheckoutMetadata ---
350
351 #[test]
352 fn sub_metadata_valid() {
353 let subscriber = UserId::new();
354 let project = ProjectId::new();
355 let tier = SubscriptionTierId::new();
356 let meta = meta_of(&[
357 ("subscriber_id", &subscriber.to_string()),
358 ("project_id", &project.to_string()),
359 ("tier_id", &tier.to_string()),
360 ("checkout_type", "subscription"),
361 ]);
362 let parsed = SubscriptionCheckoutMetadata::from_metadata(Some(&meta)).unwrap();
363 assert_eq!(parsed.subscriber_id, subscriber);
364 assert_eq!(parsed.project_id, project);
365 assert_eq!(parsed.tier_id, tier);
366 }
367
368 #[test]
369 fn sub_metadata_missing_tier_id() {
370 let m = meta_of(&[
371 ("subscriber_id", &UserId::new().to_string()),
372 ("project_id", &ProjectId::new().to_string()),
373 ]);
374 assert!(SubscriptionCheckoutMetadata::from_metadata(Some(&m)).is_err());
375 }
376
377 #[test]
378 fn sub_metadata_missing_all() {
379 assert!(SubscriptionCheckoutMetadata::from_metadata(None).is_err());
380 }
381
382 // --- is_*_checkout ---
383
384 #[test]
385 fn is_subscription_checkout_true() {
386 let m = meta_of(&[("checkout_type", "subscription")]);
387 assert!(is_subscription_checkout(Some(&m)));
388 }
389
390 #[test]
391 fn is_subscription_checkout_false_no_metadata() {
392 assert!(!is_subscription_checkout(None));
393 }
394
395 #[test]
396 fn is_subscription_checkout_false_purchase() {
397 let m = meta_of(&[("buyer_id", &UserId::new().to_string())]);
398 assert!(!is_subscription_checkout(Some(&m)));
399 }
400
401 // --- CartCheckoutMetadata ---
402
403 #[test]
404 fn cart_metadata_valid() {
405 let b = UserId::new();
406 let s = UserId::new();
407 let m = meta_of(&[
408 ("checkout_type", "cart"),
409 ("buyer_id", &b.to_string()),
410 ("seller_id", &s.to_string()),
411 ]);
412 let r = CartCheckoutMetadata::from_metadata(Some(&m)).unwrap();
413 assert_eq!(r.buyer_id, b);
414 assert_eq!(r.seller_id, s);
415 }
416
417 #[test]
418 fn cart_metadata_missing_buyer_id() {
419 let m = meta_of(&[
420 ("checkout_type", "cart"),
421 ("seller_id", &UserId::new().to_string()),
422 ]);
423 assert!(CartCheckoutMetadata::from_metadata(Some(&m)).is_err());
424 }
425
426 #[test]
427 fn cart_metadata_missing_metadata() {
428 assert!(CartCheckoutMetadata::from_metadata(None).is_err());
429 }
430
431 // --- GuestCheckoutMetadata ---
432
433 #[test]
434 fn guest_metadata_valid_with_promo() {
435 let seller = UserId::new();
436 let item = ItemId::new();
437 let promo = PromoCodeId::new();
438 let meta = meta_of(&[
439 ("checkout_type", "guest"),
440 ("seller_id", &seller.to_string()),
441 ("item_id", &item.to_string()),
442 ("promo_code_id", &promo.to_string()),
443 ]);
444 let parsed = GuestCheckoutMetadata::from_metadata(Some(&meta)).unwrap();
445 assert_eq!(parsed.seller_id, seller);
446 assert_eq!(parsed.item_id, item);
447 assert_eq!(parsed.promo_code_id, Some(promo));
448 }
449
450 #[test]
451 fn guest_metadata_valid_without_promo() {
452 let s = UserId::new();
453 let i = ItemId::new();
454 let m = meta_of(&[
455 ("checkout_type", "guest"),
456 ("seller_id", &s.to_string()),
457 ("item_id", &i.to_string()),
458 ]);
459 let r = GuestCheckoutMetadata::from_metadata(Some(&m)).unwrap();
460 assert_eq!(r.seller_id, s);
461 assert_eq!(r.item_id, i);
462 assert_eq!(r.promo_code_id, None);
463 }
464
465 #[test]
466 fn guest_metadata_missing_seller_id() {
467 let m = meta_of(&[
468 ("checkout_type", "guest"),
469 ("item_id", &ItemId::new().to_string()),
470 ]);
471 assert!(GuestCheckoutMetadata::from_metadata(Some(&m)).is_err());
472 }
473
474 // --- TipCheckoutMetadata ---
475
476 #[test]
477 fn tip_metadata_valid_full() {
478 let t = UserId::new();
479 let r2 = UserId::new();
480 let p = ProjectId::new();
481 let m = meta_of(&[
482 ("checkout_type", "tip"),
483 ("tipper_id", &t.to_string()),
484 ("recipient_id", &r2.to_string()),
485 ("project_id", &p.to_string()),
486 ("message", "Great work!"),
487 ]);
488 let r = TipCheckoutMetadata::from_metadata(Some(&m)).unwrap();
489 assert_eq!(r.tipper_id, t);
490 assert_eq!(r.recipient_id, r2);
491 assert_eq!(r.project_id, Some(p));
492 assert_eq!(r.message.as_deref(), Some("Great work!"));
493 }
494
495 #[test]
496 fn tip_metadata_valid_minimal() {
497 let t = UserId::new();
498 let r2 = UserId::new();
499 let m = meta_of(&[
500 ("checkout_type", "tip"),
501 ("tipper_id", &t.to_string()),
502 ("recipient_id", &r2.to_string()),
503 ]);
504 let r = TipCheckoutMetadata::from_metadata(Some(&m)).unwrap();
505 assert_eq!(r.project_id, None);
506 assert_eq!(r.message, None);
507 }
508
509 #[test]
510 fn tip_metadata_missing_tipper_id() {
511 let m = meta_of(&[
512 ("checkout_type", "tip"),
513 ("recipient_id", &UserId::new().to_string()),
514 ]);
515 assert!(TipCheckoutMetadata::from_metadata(Some(&m)).is_err());
516 }
517
518 // --- is_* combinations ---
519
520 #[test]
521 fn is_cart_checkout_true() {
522 let m = meta_of(&[("checkout_type", "cart")]);
523 assert!(is_cart_checkout(Some(&m)));
524 }
525
526 #[test]
527 fn is_cart_checkout_false_for_item() {
528 let m = meta_of(&[("buyer_id", &UserId::new().to_string())]);
529 assert!(!is_cart_checkout(Some(&m)));
530 }
531
532 #[test]
533 fn is_guest_checkout_true() {
534 let m = meta_of(&[("checkout_type", "guest")]);
535 assert!(is_guest_checkout(Some(&m)));
536 }
537
538 #[test]
539 fn is_tip_checkout_true() {
540 let m = meta_of(&[("checkout_type", "tip")]);
541 assert!(is_tip_checkout(Some(&m)));
542 }
543
544 #[test]
545 fn is_fan_plus_checkout_true() {
546 let m = meta_of(&[("checkout_type", "fan_plus")]);
547 assert!(is_fan_plus_checkout(Some(&m)));
548 }
549
550 #[test]
551 fn is_creator_tier_checkout_true() {
552 let m = meta_of(&[("checkout_type", "creator_tier")]);
553 assert!(is_creator_tier_checkout(Some(&m)));
554 }
555
556 // --- FanPlusCheckoutMetadata ---
557
558 #[test]
559 fn fan_plus_metadata_valid() {
560 let u = UserId::new();
561 let m = meta_of(&[("checkout_type", "fan_plus"), ("user_id", &u.to_string())]);
562 let r = FanPlusCheckoutMetadata::from_metadata(Some(&m)).unwrap();
563 assert_eq!(r.user_id, u);
564 }
565
566 #[test]
567 fn fan_plus_metadata_missing_user_id() {
568 let m = meta_of(&[("checkout_type", "fan_plus")]);
569 let err = FanPlusCheckoutMetadata::from_metadata(Some(&m)).unwrap_err();
570 assert!(format!("{err:?}").contains("user_id"));
571 }
572
573 // --- CreatorTierCheckoutMetadata ---
574
575 #[test]
576 fn creator_tier_metadata_valid() {
577 let u = UserId::new();
578 let m = meta_of(&[
579 ("checkout_type", "creator_tier"),
580 ("user_id", &u.to_string()),
581 ("tier", "everything"),
582 ]);
583 let r = CreatorTierCheckoutMetadata::from_metadata(Some(&m)).unwrap();
584 assert_eq!(r.user_id, u);
585 assert_eq!(r.tier, "everything");
586 }
587
588 #[test]
589 fn creator_tier_metadata_preserves_tier_string_verbatim() {
590 let u = UserId::new();
591 let m = meta_of(&[("user_id", &u.to_string()), ("tier", "Big Files")]);
592 let r = CreatorTierCheckoutMetadata::from_metadata(Some(&m)).unwrap();
593 assert_eq!(r.tier, "Big Files");
594 }
595
596 #[test]
597 fn creator_tier_metadata_missing_tier() {
598 let m = meta_of(&[("user_id", &UserId::new().to_string())]);
599 let err = CreatorTierCheckoutMetadata::from_metadata(Some(&m)).unwrap_err();
600 assert!(format!("{err:?}").contains("tier"));
601 }
602
603 // --- get_checkout_type ---
604
605 #[test]
606 fn get_checkout_type_recognises_each_variant() {
607 for (s, expected) in [
608 ("tip", CheckoutType::Tip),
609 ("fan_plus", CheckoutType::FanPlus),
610 ("creator_tier", CheckoutType::CreatorTier),
611 ("subscription", CheckoutType::Subscription),
612 ("guest", CheckoutType::Guest),
613 ("cart", CheckoutType::Cart),
614 ("synckit_app_sub", CheckoutType::SynckitAppSub),
615 ] {
616 let m = meta_of(&[("checkout_type", s)]);
617 assert_eq!(get_checkout_type(Some(&m)), Some(expected));
618 }
619 }
620
621 #[test]
622 fn get_checkout_type_unknown_string_is_none() {
623 let m = meta_of(&[("checkout_type", "not-a-known-type")]);
624 assert_eq!(get_checkout_type(Some(&m)), None);
625 }
626
627 #[test]
628 fn get_checkout_type_missing_field_is_none() {
629 let m = HashMap::new();
630 assert_eq!(get_checkout_type(Some(&m)), None);
631 }
632
633 #[test]
634 fn is_predicates_dont_cross_match() {
635 let m = meta_of(&[("checkout_type", "tip")]);
636 let meta = Some(&m);
637 assert!(is_tip_checkout(meta));
638 assert!(!is_fan_plus_checkout(meta));
639 assert!(!is_creator_tier_checkout(meta));
640 assert!(!is_subscription_checkout(meta));
641 assert!(!is_guest_checkout(meta));
642 assert!(!is_cart_checkout(meta));
643 assert!(!is_synckit_app_sub_checkout(meta));
644 }
645
646 // The SynckitAppSub predicate had no test of its own and no false case:
647 // both of its stub mutants and its comparison survived Phase 0. It decides
648 // whether a settled session bills an end user for app sync, so answering
649 // `true` for every session is the expensive direction.
650 #[test]
651 fn is_synckit_app_sub_checkout_matches_only_its_own_type() {
652 let own = meta_of(&[("checkout_type", "synckit_app_sub")]);
653 assert!(is_synckit_app_sub_checkout(Some(&own)));
654
655 let other = meta_of(&[("checkout_type", "subscription")]);
656 assert!(!is_synckit_app_sub_checkout(Some(&other)));
657 assert!(!is_synckit_app_sub_checkout(None));
658 }
659
660 #[test]
661 fn is_tip_checkout_false_for_another_type_and_for_no_metadata() {
662 let other = meta_of(&[("checkout_type", "cart")]);
663 assert!(!is_tip_checkout(Some(&other)));
664 assert!(!is_tip_checkout(None));
665 }
666 }
667