max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01WFBzMprSmNCfvdj2cGZyka
4 files changed,
+483 insertions,
-481 deletions
| @@ -66,114 +66,6 @@ | |||
| 66 | 66 | Ok(cents_f as i32) | |
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | - | #[cfg(test)] | |
| 70 | - | mod parse_dollars_tests { | |
| 71 | - | use super::*; | |
| 72 | - | ||
| 73 | - | #[test] | |
| 74 | - | fn empty_or_missing_is_zero() { | |
| 75 | - | assert_eq!(parse_dollars_to_cents("Price", None).unwrap(), 0); | |
| 76 | - | assert_eq!(parse_dollars_to_cents("Price", Some("")).unwrap(), 0); | |
| 77 | - | assert_eq!(parse_dollars_to_cents("Price", Some(" ")).unwrap(), 0); | |
| 78 | - | } | |
| 79 | - | ||
| 80 | - | #[test] | |
| 81 | - | fn rounds_to_nearest_cent() { | |
| 82 | - | assert_eq!(parse_dollars_to_cents("Price", Some("9.99")).unwrap(), 999); | |
| 83 | - | assert_eq!(parse_dollars_to_cents("Price", Some("1.234")).unwrap(), 123); | |
| 84 | - | assert_eq!(parse_dollars_to_cents("Price", Some("1.236")).unwrap(), 124); | |
| 85 | - | } | |
| 86 | - | ||
| 87 | - | #[test] | |
| 88 | - | fn rejects_nan() { | |
| 89 | - | assert!(parse_dollars_to_cents("Price", Some("NaN")).is_err()); | |
| 90 | - | assert!(parse_dollars_to_cents("Price", Some("nan")).is_err()); | |
| 91 | - | } | |
| 92 | - | ||
| 93 | - | #[test] | |
| 94 | - | fn rejects_infinity() { | |
| 95 | - | assert!(parse_dollars_to_cents("Price", Some("inf")).is_err()); | |
| 96 | - | assert!(parse_dollars_to_cents("Price", Some("Infinity")).is_err()); | |
| 97 | - | } | |
| 98 | - | ||
| 99 | - | #[test] | |
| 100 | - | fn rejects_negative() { | |
| 101 | - | assert!(parse_dollars_to_cents("Price", Some("-1")).is_err()); | |
| 102 | - | assert!(parse_dollars_to_cents("Price", Some("-0.01")).is_err()); | |
| 103 | - | } | |
| 104 | - | ||
| 105 | - | #[test] | |
| 106 | - | fn rejects_overflow() { | |
| 107 | - | assert!(parse_dollars_to_cents("Price", Some("100000000000")).is_err()); | |
| 108 | - | assert!(parse_dollars_to_cents("Price", Some("1e20")).is_err()); | |
| 109 | - | } | |
| 110 | - | ||
| 111 | - | #[test] | |
| 112 | - | fn rejects_garbage() { | |
| 113 | - | assert!(parse_dollars_to_cents("Price", Some("abc")).is_err()); | |
| 114 | - | assert!(parse_dollars_to_cents("Price", Some("free")).is_err()); | |
| 115 | - | } | |
| 116 | - | ||
| 117 | - | #[test] | |
| 118 | - | fn strips_clipboard_decoration() { | |
| 119 | - | // Clipboard pastes from invoices / price lists shouldn't 422. | |
| 120 | - | assert_eq!(parse_dollars_to_cents("Price", Some("$5")).unwrap(), 500); | |
| 121 | - | assert_eq!( | |
| 122 | - | parse_dollars_to_cents("Price", Some("1,000")).unwrap(), | |
| 123 | - | 100_000 | |
| 124 | - | ); | |
| 125 | - | assert_eq!( | |
| 126 | - | parse_dollars_to_cents("Price", Some("$ 1,250.00")).unwrap(), | |
| 127 | - | 125_000 | |
| 128 | - | ); | |
| 129 | - | assert_eq!( | |
| 130 | - | parse_dollars_to_cents("Price", Some(" $9.99 ")).unwrap(), | |
| 131 | - | 999 | |
| 132 | - | ); | |
| 133 | - | // Decoration-only input still parses as garbage (no digits → fails) | |
| 134 | - | assert!(parse_dollars_to_cents("Price", Some("$$")).is_err()); | |
| 135 | - | } | |
| 136 | - | ||
| 137 | - | // `validate_dollars_f64` is the JSON-handler entry point and had no test of | |
| 138 | - | // its own: every case above reached it only through the string parser, so | |
| 139 | - | // mutation testing found all twelve of its mutants alive (Phase 0 run, | |
| 140 | - | // 2026-08-16). The boundary cases are the point — an off-by-one on the | |
| 141 | - | // overflow guard is the difference between a $21,474,836.47 price and a | |
| 142 | - | // wrapped negative one. | |
| 143 | - | ||
| 144 | - | #[test] | |
| 145 | - | fn validate_f64_rejects_non_finite() { | |
| 146 | - | assert!(validate_dollars_f64("Price", f64::NAN).is_err()); | |
| 147 | - | assert!(validate_dollars_f64("Price", f64::INFINITY).is_err()); | |
| 148 | - | assert!(validate_dollars_f64("Price", f64::NEG_INFINITY).is_err()); | |
| 149 | - | } | |
| 150 | - | ||
| 151 | - | #[test] | |
| 152 | - | fn validate_f64_rejects_negative_but_accepts_zero() { | |
| 153 | - | assert!(validate_dollars_f64("Price", -0.01).is_err()); | |
| 154 | - | assert!(validate_dollars_f64("Price", -1.0).is_err()); | |
| 155 | - | assert_eq!(validate_dollars_f64("Price", 0.0).unwrap(), 0); | |
| 156 | - | } | |
| 157 | - | ||
| 158 | - | #[test] | |
| 159 | - | fn validate_f64_multiplies_by_a_hundred_and_rounds() { | |
| 160 | - | assert_eq!(validate_dollars_f64("Price", 9.99).unwrap(), 999); | |
| 161 | - | assert_eq!(validate_dollars_f64("Price", 1.234).unwrap(), 123); | |
| 162 | - | assert_eq!(validate_dollars_f64("Price", 1.236).unwrap(), 124); | |
| 163 | - | assert_eq!(validate_dollars_f64("Price", 2.0).unwrap(), 200); | |
| 164 | - | } | |
| 165 | - | ||
| 166 | - | #[test] | |
| 167 | - | fn validate_f64_overflow_guard_is_inclusive_at_i32_max_cents() { | |
| 168 | - | // Exactly `i32::MAX` cents is the largest representable price and must | |
| 169 | - | // be accepted; one cent more must not be. | |
| 170 | - | let at_max = f64::from(i32::MAX) / 100.0; | |
| 171 | - | assert_eq!(validate_dollars_f64("Price", at_max).unwrap(), i32::MAX); | |
| 172 | - | assert!(validate_dollars_f64("Price", at_max + 0.01).is_err()); | |
| 173 | - | assert!(validate_dollars_f64("Price", 1e20).is_err()); | |
| 174 | - | } | |
| 175 | - | } | |
| 176 | - | ||
| 177 | 69 | /// Pre-fetched access state for a user viewing a priced resource. | |
| 178 | 70 | /// | |
| 179 | 71 | /// Routes populate this from DB lookups, then pass it to `PricingModel::can_access()`. |
| @@ -545,379 +545,7 @@ | |||
| 545 | 545 | } | |
| 546 | 546 | ||
| 547 | 547 | #[cfg(test)] | |
| 548 | - | pub(crate) mod test_provider { | |
| 549 | - | //! A crate-visible [`PaymentProvider`] double for lib tests. | |
| 550 | - | //! | |
| 551 | - | //! The integration suite already has `MockPaymentProvider` | |
| 552 | - | //! (`tests/harness/stripe.rs`), which is richer: it captures checkout | |
| 553 | - | //! sessions and signs webhooks. It lives in a separate test binary, so a | |
| 554 | - | //! `--lib` test cannot reach it, and this is deliberately the smaller | |
| 555 | - | //! thing. It answers the subscription-lifecycle calls and panics on | |
| 556 | - | //! everything else, which is enough to test the code that fans those out | |
| 557 | - | //! without a database, a router or a Stripe key. | |
| 558 | - | //! | |
| 559 | - | //! Implement a method here when a lib test needs it. Growing this toward | |
| 560 | - | //! the harness's copy would give the crate two mocks to keep in agreement, | |
| 561 | - | //! which is the imitation-oracle failure wiki `testing-posture` describes. | |
| 562 | - | ||
| 563 | - | use std::collections::HashSet; | |
| 564 | - | use std::sync::Mutex; | |
| 565 | - | ||
| 566 | - | use super::*; | |
| 567 | - | ||
| 568 | - | /// Records every subscription op it is asked for, and fails the ones whose | |
| 569 | - | /// subscription id was listed as failing. | |
| 570 | - | #[derive(Default)] | |
| 571 | - | pub(crate) struct ScriptedProvider { | |
| 572 | - | failing: HashSet<String>, | |
| 573 | - | calls: Mutex<Vec<(&'static str, String)>>, | |
| 574 | - | } | |
| 575 | - | ||
| 576 | - | impl ScriptedProvider { | |
| 577 | - | /// Every call succeeds. | |
| 578 | - | pub(crate) fn healthy() -> Self { | |
| 579 | - | Self::default() | |
| 580 | - | } | |
| 581 | - | ||
| 582 | - | /// Every call succeeds except those naming one of `sub_ids`. | |
| 583 | - | pub(crate) fn failing(sub_ids: impl IntoIterator<Item = &'static str>) -> Self { | |
| 584 | - | Self { | |
| 585 | - | failing: sub_ids.into_iter().map(str::to_owned).collect(), | |
| 586 | - | calls: Mutex::new(Vec::new()), | |
| 587 | - | } | |
| 588 | - | } | |
| 589 | - | ||
| 590 | - | /// `(op, subscription id)` in the order they were applied. | |
| 591 | - | pub(crate) fn calls(&self) -> Vec<(&'static str, String)> { | |
| 592 | - | self.calls | |
| 593 | - | .lock() | |
| 594 | - | .expect("no test panics while holding this") | |
| 595 | - | .clone() | |
| 596 | - | } | |
| 597 | - | ||
| 598 | - | fn record(&self, op: &'static str, sub_id: &str) -> crate::error::Result<()> { | |
| 599 | - | self.calls | |
| 600 | - | .lock() | |
| 601 | - | .expect("no test panics while holding this") | |
| 602 | - | .push((op, sub_id.to_string())); | |
| 603 | - | if self.failing.contains(sub_id) { | |
| 604 | - | return Err(crate::error::AppError::BadRequest(format!( | |
| 605 | - | "scripted failure for {sub_id}" | |
| 606 | - | ))); | |
| 607 | - | } | |
| 608 | - | Ok(()) | |
| 609 | - | } | |
| 610 | - | } | |
| 611 | - | ||
| 612 | - | /// The methods no lib test drives yet. A call is a bug in the test, not a | |
| 613 | - | /// condition to handle, so it panics rather than returning an error the | |
| 614 | - | /// code under test would quietly count as a Stripe failure. | |
| 615 | - | macro_rules! unused { | |
| 616 | - | ($($name:ident),+ $(,)?) => { | |
| 617 | - | $( | |
| 618 | - | #[allow(unused_variables)] | |
| 619 | - | fn $name(&self) -> ! { | |
| 620 | - | unimplemented!( | |
| 621 | - | "ScriptedProvider::{} is not implemented; add it if a lib test needs it", | |
| 622 | - | stringify!($name) | |
| 623 | - | ) | |
| 624 | - | } | |
| 625 | - | )+ | |
| 626 | - | }; | |
| 627 | - | } | |
| 628 | - | ||
| 629 | - | impl ScriptedProvider { | |
| 630 | - | unused!( | |
| 631 | - | create_checkout_session, | |
| 632 | - | create_guest_checkout_session, | |
| 633 | - | create_subscription_checkout_session, | |
| 634 | - | create_tip_checkout_session, | |
| 635 | - | create_fan_plus_checkout_session, | |
| 636 | - | create_creator_tier_checkout_session, | |
| 637 | - | create_synckit_app_sub_checkout_session, | |
| 638 | - | create_cart_checkout_session, | |
| 639 | - | create_connect_account, | |
| 640 | - | create_account_link, | |
| 641 | - | fetch_account, | |
| 642 | - | create_subscription_product_and_price, | |
| 643 | - | get_balance, | |
| 644 | - | cancel_platform_subscription, | |
| 645 | - | set_platform_cancel_at_period_end, | |
| 646 | - | create_billing_portal_session, | |
| 647 | - | create_refund_for_transaction, | |
| 648 | - | create_platform_credit_transfer, | |
| 649 | - | create_platform_credit_reversal, | |
| 650 | - | verify_webhook, | |
| 651 | - | verify_webhook_v2, | |
| 652 | - | normalize_webhook, | |
| 653 | - | create_synckit_customer, | |
| 654 | - | create_synckit_subscription, | |
| 655 | - | update_synckit_subscription_price, | |
| 656 | - | update_synckit_app_sub_price, | |
| 657 | - | cancel_synckit_subscription, | |
| 658 | - | create_synckit_billing_portal, | |
| 659 | - | ); | |
| 660 | - | } | |
| 661 | - | ||
| 662 | - | #[async_trait::async_trait] | |
| 663 | - | impl PaymentProvider for ScriptedProvider { | |
| 664 | - | // ── what the fan-out drives ── | |
| 665 | - | ||
| 666 | - | async fn pause_subscription(&self, sub: &str, _account: &str) -> crate::error::Result<()> { | |
| 667 | - | self.record("pause", sub) | |
| 668 | - | } | |
| 669 | - | ||
| 670 | - | async fn resume_subscription(&self, sub: &str, _account: &str) -> crate::error::Result<()> { | |
| 671 | - | self.record("resume", sub) | |
| 672 | - | } | |
| 673 | - | ||
| 674 | - | async fn cancel_subscription(&self, sub: &str, _account: &str) -> crate::error::Result<()> { | |
| 675 | - | self.record("cancel", sub) | |
| 676 | - | } | |
| 677 | - | ||
| 678 | - | async fn set_cancel_at_period_end( | |
| 679 | - | &self, | |
| 680 | - | sub: &str, | |
| 681 | - | _account: &str, | |
| 682 | - | cancel: bool, | |
| 683 | - | ) -> crate::error::Result<()> { | |
| 684 | - | self.record( | |
| 685 | - | if cancel { | |
| 686 | - | "set_cancel_at_period_end" | |
| 687 | - | } else { | |
| 688 | - | "clear_cancel_at_period_end" | |
| 689 | - | }, | |
| 690 | - | sub, | |
| 691 | - | ) | |
| 692 | - | } | |
| 693 | - | ||
| 694 | - | // ── everything else ── | |
| 695 | - | ||
| 696 | - | async fn create_checkout_session( | |
| 697 | - | &self, | |
| 698 | - | _params: &CheckoutParams<'_>, | |
| 699 | - | ) -> crate::error::Result<CheckoutResult> { | |
| 700 | - | ScriptedProvider::create_checkout_session(self) | |
| 701 | - | } | |
| 702 | - | async fn create_guest_checkout_session( | |
| 703 | - | &self, | |
| 704 | - | _params: &GuestCheckoutParams<'_>, | |
| 705 | - | ) -> crate::error::Result<CheckoutResult> { | |
| 706 | - | ScriptedProvider::create_guest_checkout_session(self) | |
| 707 | - | } | |
| 708 | - | async fn create_subscription_checkout_session( | |
| 709 | - | &self, | |
| 710 | - | _params: &SubscriptionCheckoutParams<'_>, | |
| 711 | - | ) -> crate::error::Result<CheckoutResult> { | |
| 712 | - | ScriptedProvider::create_subscription_checkout_session(self) | |
| 713 | - | } | |
| 714 | - | async fn create_tip_checkout_session( | |
| 715 | - | &self, | |
| 716 | - | _params: &TipCheckoutParams<'_>, | |
| 717 | - | ) -> crate::error::Result<CheckoutResult> { | |
| 718 | - | ScriptedProvider::create_tip_checkout_session(self) | |
| 719 | - | } | |
| 720 | - | async fn create_fan_plus_checkout_session( | |
| 721 | - | &self, | |
| 722 | - | _price_id: &str, | |
| 723 | - | _user_id: crate::db::UserId, | |
| 724 | - | _success_url: &str, | |
| 725 | - | _cancel_url: &str, | |
| 726 | - | ) -> crate::error::Result<CheckoutResult> { | |
| 727 | - | ScriptedProvider::create_fan_plus_checkout_session(self) | |
| 728 | - | } | |
| 729 | - | async fn create_creator_tier_checkout_session( | |
| 730 | - | &self, | |
| 731 | - | _price_id: &str, | |
| 732 | - | _user_id: crate::db::UserId, | |
| 733 | - | _tier: &str, | |
| 734 | - | _success_url: &str, | |
| 735 | - | _cancel_url: &str, | |
| 736 | - | _trial_days: Option<i32>, | |
| 737 | - | ) -> crate::error::Result<CheckoutResult> { | |
| 738 | - | ScriptedProvider::create_creator_tier_checkout_session(self) | |
| 739 | - | } | |
| 740 | - | async fn create_synckit_app_sub_checkout_session( | |
| 741 | - | &self, | |
| 742 | - | _params: &SynckitAppSubCheckoutParams<'_>, | |
| 743 | - | ) -> crate::error::Result<CheckoutResult> { | |
| 744 | - | ScriptedProvider::create_synckit_app_sub_checkout_session(self) | |
| 745 | - | } | |
| 746 | - | async fn create_cart_checkout_session( | |
| 747 | - | &self, | |
| 748 | - | _params: &CartCheckoutParams<'_>, | |
| 749 | - | ) -> crate::error::Result<CheckoutResult> { | |
| 750 | - | ScriptedProvider::create_cart_checkout_session(self) | |
| 751 | - | } | |
| 752 | - | async fn create_connect_account( | |
| 753 | - | &self, | |
| 754 | - | _email: &str, | |
| 755 | - | ) -> crate::error::Result<ProviderAccountId> { | |
| 756 | - | ScriptedProvider::create_connect_account(self) | |
| 757 | - | } | |
| 758 | - | async fn get_balance( | |
| 759 | - | &self, | |
| 760 | - | _account_id: &str, | |
| 761 | - | _currency: crate::currency::SettlementCurrency, | |
| 762 | - | ) -> crate::error::Result<BalanceSummary> { | |
| 763 | - | ScriptedProvider::get_balance(self) | |
| 764 | - | } | |
| 765 | - | async fn cancel_platform_subscription(&self, _sub: &str) -> crate::error::Result<()> { | |
| 766 | - | ScriptedProvider::cancel_platform_subscription(self) | |
| 767 | - | } | |
| 768 | - | async fn set_platform_cancel_at_period_end( | |
| 769 | - | &self, | |
| 770 | - | _sub: &str, | |
| 771 | - | _cancel: bool, | |
| 772 | - | ) -> crate::error::Result<()> { | |
| 773 | - | ScriptedProvider::set_platform_cancel_at_period_end(self) | |
| 774 | - | } | |
| 775 | - | fn verify_webhook( | |
| 776 | - | &self, | |
| 777 | - | _payload: &str, | |
| 778 | - | _signature: &str, | |
| 779 | - | ) -> crate::error::Result<UntypedEvent> { | |
| 780 | - | ScriptedProvider::verify_webhook(self) | |
| 781 | - | } | |
| 782 | - | fn verify_webhook_v2( | |
| 783 | - | &self, | |
| 784 | - | _payload: &str, | |
| 785 | - | _signature: &str, | |
| 786 | - | ) -> crate::error::Result<serde_json::Value> { | |
| 787 | - | ScriptedProvider::verify_webhook_v2(self) | |
| 788 | - | } | |
| 789 | - | fn normalize_webhook(&self, _event: UntypedEvent) -> crate::error::Result<MnwEvent> { | |
| 790 | - | ScriptedProvider::normalize_webhook(self) | |
| 791 | - | } | |
| 792 | - | async fn update_synckit_subscription_price( | |
| 793 | - | &self, | |
| 794 | - | _subscription_id: &str, | |
| 795 | - | _new_price_cents: i64, | |
| 796 | - | _app_name: &str, | |
| 797 | - | ) -> crate::error::Result<()> { | |
| 798 | - | ScriptedProvider::update_synckit_subscription_price(self) | |
| 799 | - | } | |
| 800 | - | async fn update_synckit_app_sub_price( | |
| 801 | - | &self, | |
| 802 | - | _subscription_id: &str, | |
| 803 | - | _new_price_cents: i64, | |
| 804 | - | _interval: SyncBillingInterval, | |
| 805 | - | _product_name: &str, | |
| 806 | - | ) -> crate::error::Result<()> { | |
| 807 | - | ScriptedProvider::update_synckit_app_sub_price(self) | |
| 808 | - | } | |
| 809 | - | async fn cancel_synckit_subscription(&self, _sub: &str) -> crate::error::Result<()> { | |
| 810 | - | ScriptedProvider::cancel_synckit_subscription(self) | |
| 811 | - | } | |
| 812 | - | } | |
| 813 | - | ||
| 814 | - | // ── The capability extensions, every one of them a panic: no lib test | |
| 815 | - | // drives an extension yet, and `ScriptedProvider` implements them so the | |
| 816 | - | // double stays wirable wherever a full provider is expected. | |
| 817 | - | ||
| 818 | - | #[async_trait::async_trait] | |
| 819 | - | impl HostedPortal for ScriptedProvider { | |
| 820 | - | async fn create_billing_portal_session( | |
| 821 | - | &self, | |
| 822 | - | _customer_id: &str, | |
| 823 | - | _return_url: &str, | |
| 824 | - | ) -> crate::error::Result<String> { | |
| 825 | - | ScriptedProvider::create_billing_portal_session(self) | |
| 826 | - | } | |
| 827 | - | async fn create_synckit_billing_portal( | |
| 828 | - | &self, | |
| 829 | - | _customer_id: &str, | |
| 830 | - | _return_url: &str, | |
| 831 | - | ) -> crate::error::Result<String> { | |
| 832 | - | ScriptedProvider::create_synckit_billing_portal(self) | |
| 833 | - | } | |
| 834 | - | } | |
| 835 | - | ||
| 836 | - | #[async_trait::async_trait] | |
| 837 | - | impl ConnectOnboarding for ScriptedProvider { | |
| 838 | - | async fn create_account_link( | |
| 839 | - | &self, | |
| 840 | - | _account_id: &str, | |
| 841 | - | _return_url: &str, | |
| 842 | - | _refresh_url: &str, | |
| 843 | - | ) -> crate::error::Result<String> { | |
| 844 | - | ScriptedProvider::create_account_link(self) | |
| 845 | - | } | |
| 846 | - | async fn fetch_account(&self, _account_id: &str) -> crate::error::Result<AccountUpdate> { | |
| 847 | - | ScriptedProvider::fetch_account(self) | |
| 848 | - | } | |
| 849 | - | } | |
| 850 | - | ||
| 851 | - | #[async_trait::async_trait] | |
| 852 | - | impl Catalogue for ScriptedProvider { | |
| 853 | - | async fn create_subscription_product_and_price( | |
| 854 | - | &self, | |
| 855 | - | _connected_account_id: &str, | |
| 856 | - | _tier_name: &str, | |
| 857 | - | _tier_description: Option<&str>, | |
| 858 | - | _price_cents: i64, | |
| 859 | - | _currency: crate::currency::SettlementCurrency, | |
| 860 | - | ) -> crate::error::Result<(String, String)> { | |
| 861 | - | ScriptedProvider::create_subscription_product_and_price(self) | |
| 862 | - | } | |
| 863 | - | } | |
| 864 | - | ||
| 865 | - | #[async_trait::async_trait] | |
| 866 | - | impl Refundable for ScriptedProvider { | |
| 867 | - | async fn create_refund_for_transaction( | |
| 868 | - | &self, | |
| 869 | - | _payment_intent_id: &str, | |
| 870 | - | _connected_account_id: &str, | |
| 871 | - | _amount_cents: i64, | |
| 872 | - | _transaction_id: crate::db::TransactionId, | |
| 873 | - | ) -> crate::error::Result<()> { | |
| 874 | - | ScriptedProvider::create_refund_for_transaction(self) | |
| 875 | - | } | |
| 876 | - | } | |
| 877 | - | ||
| 878 | - | #[async_trait::async_trait] | |
| 879 | - | impl PlatformTransfers for ScriptedProvider { | |
| 880 | - | async fn create_platform_credit_transfer( | |
| 881 | - | &self, | |
| 882 | - | _connected_account_id: &str, | |
| 883 | - | _amount_cents: i64, | |
| 884 | - | _transaction_id: crate::db::TransactionId, | |
| 885 | - | _currency: crate::currency::SettlementCurrency, | |
| 886 | - | ) -> crate::error::Result<String> { | |
| 887 | - | ScriptedProvider::create_platform_credit_transfer(self) | |
| 888 | - | } | |
| 889 | - | async fn create_platform_credit_reversal( | |
| 890 | - | &self, | |
| 891 | - | _transfer_id: &str, | |
| 892 | - | _amount_cents: i64, | |
| 893 | - | _transaction_id: crate::db::TransactionId, | |
| 894 | - | ) -> crate::error::Result<()> { | |
| 895 | - | ScriptedProvider::create_platform_credit_reversal(self) | |
| 896 | - | } | |
| 897 | - | } | |
| 898 | - | ||
| 899 | - | #[async_trait::async_trait] | |
| 900 | - | impl CustodialCustomers for ScriptedProvider { | |
| 901 | - | async fn create_synckit_customer( | |
| 902 | - | &self, | |
| 903 | - | _developer_user_id: crate::db::UserId, | |
| 904 | - | _app_id: crate::db::SyncAppId, | |
| 905 | - | _email: &str, | |
| 906 | - | _app_name: &str, | |
| 907 | - | ) -> crate::error::Result<String> { | |
| 908 | - | ScriptedProvider::create_synckit_customer(self) | |
| 909 | - | } | |
| 910 | - | async fn create_synckit_subscription( | |
| 911 | - | &self, | |
| 912 | - | _customer_id: &str, | |
| 913 | - | _app_id: crate::db::SyncAppId, | |
| 914 | - | _app_name: &str, | |
| 915 | - | _price_cents: i64, | |
| 916 | - | ) -> crate::error::Result<SynckitSubResult> { | |
| 917 | - | ScriptedProvider::create_synckit_subscription(self) | |
| 918 | - | } | |
| 919 | - | } | |
| 920 | - | } | |
| 548 | + | pub(crate) mod test_provider; | |
| 921 | 549 | ||
| 922 | 550 | #[async_trait::async_trait] | |
| 923 | 551 | impl PaymentProvider for StripeClient { |
| @@ -847,3 +847,108 @@ | |||
| 847 | 847 | proptest::prop_assert!(SubscriptionPricing.validate_amount(amount, SettlementCurrency::Usd).is_err()); | |
| 848 | 848 | } | |
| 849 | 849 | } | |
| 850 | + | ||
| 851 | + | // ── parse_dollars_to_cents ── | |
| 852 | + | ||
| 853 | + | #[test] | |
| 854 | + | fn empty_or_missing_is_zero() { | |
| 855 | + | assert_eq!(parse_dollars_to_cents("Price", None).unwrap(), 0); | |
| 856 | + | assert_eq!(parse_dollars_to_cents("Price", Some("")).unwrap(), 0); | |
| 857 | + | assert_eq!(parse_dollars_to_cents("Price", Some(" ")).unwrap(), 0); | |
| 858 | + | } | |
| 859 | + | ||
| 860 | + | #[test] | |
| 861 | + | fn rounds_to_nearest_cent() { | |
| 862 | + | assert_eq!(parse_dollars_to_cents("Price", Some("9.99")).unwrap(), 999); | |
| 863 | + | assert_eq!(parse_dollars_to_cents("Price", Some("1.234")).unwrap(), 123); | |
| 864 | + | assert_eq!(parse_dollars_to_cents("Price", Some("1.236")).unwrap(), 124); | |
| 865 | + | } | |
| 866 | + | ||
| 867 | + | #[test] | |
| 868 | + | fn rejects_nan() { | |
| 869 | + | assert!(parse_dollars_to_cents("Price", Some("NaN")).is_err()); | |
| 870 | + | assert!(parse_dollars_to_cents("Price", Some("nan")).is_err()); | |
| 871 | + | } | |
| 872 | + | ||
| 873 | + | #[test] | |
| 874 | + | fn rejects_infinity() { | |
| 875 | + | assert!(parse_dollars_to_cents("Price", Some("inf")).is_err()); | |
| 876 | + | assert!(parse_dollars_to_cents("Price", Some("Infinity")).is_err()); | |
| 877 | + | } | |
| 878 | + | ||
| 879 | + | #[test] | |
| 880 | + | fn rejects_negative() { | |
| 881 | + | assert!(parse_dollars_to_cents("Price", Some("-1")).is_err()); | |
| 882 | + | assert!(parse_dollars_to_cents("Price", Some("-0.01")).is_err()); | |
| 883 | + | } | |
| 884 | + | ||
| 885 | + | #[test] | |
| 886 | + | fn rejects_overflow() { | |
| 887 | + | assert!(parse_dollars_to_cents("Price", Some("100000000000")).is_err()); | |
| 888 | + | assert!(parse_dollars_to_cents("Price", Some("1e20")).is_err()); | |
| 889 | + | } | |
| 890 | + | ||
| 891 | + | #[test] | |
| 892 | + | fn rejects_garbage() { | |
| 893 | + | assert!(parse_dollars_to_cents("Price", Some("abc")).is_err()); | |
| 894 | + | assert!(parse_dollars_to_cents("Price", Some("free")).is_err()); | |
| 895 | + | } | |
| 896 | + | ||
| 897 | + | #[test] | |
| 898 | + | fn strips_clipboard_decoration() { | |
| 899 | + | // Clipboard pastes from invoices / price lists shouldn't 422. | |
| 900 | + | assert_eq!(parse_dollars_to_cents("Price", Some("$5")).unwrap(), 500); | |
| 901 | + | assert_eq!( | |
| 902 | + | parse_dollars_to_cents("Price", Some("1,000")).unwrap(), | |
| 903 | + | 100_000 | |
| 904 | + | ); | |
| 905 | + | assert_eq!( | |
| 906 | + | parse_dollars_to_cents("Price", Some("$ 1,250.00")).unwrap(), | |
| 907 | + | 125_000 | |
| 908 | + | ); | |
| 909 | + | assert_eq!( | |
| 910 | + | parse_dollars_to_cents("Price", Some(" $9.99 ")).unwrap(), | |
| 911 | + | 999 | |
| 912 | + | ); | |
| 913 | + | // Decoration-only input still parses as garbage (no digits → fails) | |
| 914 | + | assert!(parse_dollars_to_cents("Price", Some("$$")).is_err()); | |
| 915 | + | } | |
| 916 | + | ||
| 917 | + | // `validate_dollars_f64` is the JSON-handler entry point and had no test of | |
| 918 | + | // its own: every case above reached it only through the string parser, so | |
| 919 | + | // mutation testing found all twelve of its mutants alive (Phase 0 run, | |
| 920 | + | // 2026-08-16). The boundary cases are the point — an off-by-one on the | |
| 921 | + | // overflow guard is the difference between a $21,474,836.47 price and a | |
| 922 | + | // wrapped negative one. | |
| 923 | + | ||
| 924 | + | #[test] | |
| 925 | + | fn validate_f64_rejects_non_finite() { | |
| 926 | + | assert!(validate_dollars_f64("Price", f64::NAN).is_err()); | |
| 927 | + | assert!(validate_dollars_f64("Price", f64::INFINITY).is_err()); | |
| 928 | + | assert!(validate_dollars_f64("Price", f64::NEG_INFINITY).is_err()); | |
| 929 | + | } | |
| 930 | + | ||
| 931 | + | #[test] | |
| 932 | + | fn validate_f64_rejects_negative_but_accepts_zero() { | |
| 933 | + | assert!(validate_dollars_f64("Price", -0.01).is_err()); | |
| 934 | + | assert!(validate_dollars_f64("Price", -1.0).is_err()); | |
| 935 | + | assert_eq!(validate_dollars_f64("Price", 0.0).unwrap(), 0); | |
| 936 | + | } | |
| 937 | + | ||
| 938 | + | #[test] | |
| 939 | + | fn validate_f64_multiplies_by_a_hundred_and_rounds() { | |
| 940 | + | assert_eq!(validate_dollars_f64("Price", 9.99).unwrap(), 999); | |
| 941 | + | assert_eq!(validate_dollars_f64("Price", 1.234).unwrap(), 123); | |
| 942 | + | assert_eq!(validate_dollars_f64("Price", 1.236).unwrap(), 124); | |
| 943 | + | assert_eq!(validate_dollars_f64("Price", 2.0).unwrap(), 200); | |
| 944 | + | } | |
| 945 | + | ||
| 946 | + | #[test] | |
| 947 | + | fn validate_f64_overflow_guard_is_inclusive_at_i32_max_cents() { | |
| 948 | + | // Exactly `i32::MAX` cents is the largest representable price and must | |
| 949 | + | // be accepted; one cent more must not be. | |
| 950 | + | let at_max = f64::from(i32::MAX) / 100.0; | |
| 951 | + | assert_eq!(validate_dollars_f64("Price", at_max).unwrap(), i32::MAX); | |
| 952 | + | assert!(validate_dollars_f64("Price", at_max + 0.01).is_err()); | |
| 953 | + | assert!(validate_dollars_f64("Price", 1e20).is_err()); | |
| 954 | + | } |
| @@ -1,0 +1,377 @@ | |||
| 1 | + | //! Scripted payment providers, for tests that need one that behaves on cue. | |
| 2 | + | //! | |
| 3 | + | //! `pub(crate)` rather than private: `payments/fan_ops.rs` builds a | |
| 4 | + | //! `ScriptedProvider` too, and a fixture two modules share is a module, not a | |
| 5 | + | //! copy in each. | |
| 6 | + | ||
| 7 | + | //! A crate-visible [`PaymentProvider`] double for lib tests. | |
| 8 | + | //! | |
| 9 | + | //! The integration suite already has `MockPaymentProvider` | |
| 10 | + | //! (`tests/harness/stripe.rs`), which is richer: it captures checkout | |
| 11 | + | //! sessions and signs webhooks. It lives in a separate test binary, so a | |
| 12 | + | //! `--lib` test cannot reach it, and this is deliberately the smaller | |
| 13 | + | //! thing. It answers the subscription-lifecycle calls and panics on | |
| 14 | + | //! everything else, which is enough to test the code that fans those out | |
| 15 | + | //! without a database, a router or a Stripe key. | |
| 16 | + | //! | |
| 17 | + | //! Implement a method here when a lib test needs it. Growing this toward | |
| 18 | + | //! the harness's copy would give the crate two mocks to keep in agreement, | |
| 19 | + | //! which is the imitation-oracle failure wiki `testing-posture` describes. | |
| 20 | + | ||
| 21 | + | use std::collections::HashSet; | |
| 22 | + | use std::sync::Mutex; | |
| 23 | + | ||
| 24 | + | use super::*; | |
| 25 | + | ||
| 26 | + | /// Records every subscription op it is asked for, and fails the ones whose | |
| 27 | + | /// subscription id was listed as failing. | |
| 28 | + | #[derive(Default)] | |
| 29 | + | pub(crate) struct ScriptedProvider { | |
| 30 | + | failing: HashSet<String>, | |
| 31 | + | calls: Mutex<Vec<(&'static str, String)>>, | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | impl ScriptedProvider { | |
| 35 | + | /// Every call succeeds. | |
| 36 | + | pub(crate) fn healthy() -> Self { | |
| 37 | + | Self::default() | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | /// Every call succeeds except those naming one of `sub_ids`. | |
| 41 | + | pub(crate) fn failing(sub_ids: impl IntoIterator<Item = &'static str>) -> Self { | |
| 42 | + | Self { | |
| 43 | + | failing: sub_ids.into_iter().map(str::to_owned).collect(), | |
| 44 | + | calls: Mutex::new(Vec::new()), | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | /// `(op, subscription id)` in the order they were applied. | |
| 49 | + | pub(crate) fn calls(&self) -> Vec<(&'static str, String)> { | |
| 50 | + | self.calls | |
| 51 | + | .lock() | |
| 52 | + | .expect("no test panics while holding this") | |
| 53 | + | .clone() | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | fn record(&self, op: &'static str, sub_id: &str) -> crate::error::Result<()> { | |
| 57 | + | self.calls | |
| 58 | + | .lock() | |
| 59 | + | .expect("no test panics while holding this") | |
| 60 | + | .push((op, sub_id.to_string())); | |
| 61 | + | if self.failing.contains(sub_id) { | |
| 62 | + | return Err(crate::error::AppError::BadRequest(format!( | |
| 63 | + | "scripted failure for {sub_id}" | |
| 64 | + | ))); | |
| 65 | + | } | |
| 66 | + | Ok(()) | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | /// The methods no lib test drives yet. A call is a bug in the test, not a | |
| 71 | + | /// condition to handle, so it panics rather than returning an error the | |
| 72 | + | /// code under test would quietly count as a Stripe failure. | |
| 73 | + | macro_rules! unused { | |
| 74 | + | ($($name:ident),+ $(,)?) => { | |
| 75 | + | $( | |
| 76 | + | #[allow(unused_variables)] | |
| 77 | + | fn $name(&self) -> ! { | |
| 78 | + | unimplemented!( | |
| 79 | + | "ScriptedProvider::{} is not implemented; add it if a lib test needs it", | |
| 80 | + | stringify!($name) | |
| 81 | + | ) | |
| 82 | + | } | |
| 83 | + | )+ | |
| 84 | + | }; | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | impl ScriptedProvider { | |
| 88 | + | unused!( | |
| 89 | + | create_checkout_session, | |
| 90 | + | create_guest_checkout_session, | |
| 91 | + | create_subscription_checkout_session, | |
| 92 | + | create_tip_checkout_session, | |
| 93 | + | create_fan_plus_checkout_session, | |
| 94 | + | create_creator_tier_checkout_session, | |
| 95 | + | create_synckit_app_sub_checkout_session, | |
| 96 | + | create_cart_checkout_session, | |
| 97 | + | create_connect_account, | |
| 98 | + | create_account_link, | |
| 99 | + | fetch_account, | |
| 100 | + | create_subscription_product_and_price, | |
| 101 | + | get_balance, | |
| 102 | + | cancel_platform_subscription, | |
| 103 | + | set_platform_cancel_at_period_end, | |
| 104 | + | create_billing_portal_session, | |
| 105 | + | create_refund_for_transaction, | |
| 106 | + | create_platform_credit_transfer, | |
| 107 | + | create_platform_credit_reversal, | |
| 108 | + | verify_webhook, | |
| 109 | + | verify_webhook_v2, | |
| 110 | + | normalize_webhook, | |
| 111 | + | create_synckit_customer, | |
| 112 | + | create_synckit_subscription, | |
| 113 | + | update_synckit_subscription_price, | |
| 114 | + | update_synckit_app_sub_price, | |
| 115 | + | cancel_synckit_subscription, | |
| 116 | + | create_synckit_billing_portal, | |
| 117 | + | ); | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | #[async_trait::async_trait] | |
| 121 | + | impl PaymentProvider for ScriptedProvider { | |
| 122 | + | // ── what the fan-out drives ── | |
| 123 | + | ||
| 124 | + | async fn pause_subscription(&self, sub: &str, _account: &str) -> crate::error::Result<()> { | |
| 125 | + | self.record("pause", sub) | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | async fn resume_subscription(&self, sub: &str, _account: &str) -> crate::error::Result<()> { | |
| 129 | + | self.record("resume", sub) | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | async fn cancel_subscription(&self, sub: &str, _account: &str) -> crate::error::Result<()> { | |
| 133 | + | self.record("cancel", sub) | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | async fn set_cancel_at_period_end( | |
| 137 | + | &self, | |
| 138 | + | sub: &str, | |
| 139 | + | _account: &str, | |
| 140 | + | cancel: bool, | |
| 141 | + | ) -> crate::error::Result<()> { | |
| 142 | + | self.record( | |
| 143 | + | if cancel { | |
| 144 | + | "set_cancel_at_period_end" | |
| 145 | + | } else { | |
| 146 | + | "clear_cancel_at_period_end" | |
| 147 | + | }, | |
| 148 | + | sub, | |
| 149 | + | ) | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // ── everything else ── | |
| 153 | + | ||
| 154 | + | async fn create_checkout_session( | |
| 155 | + | &self, | |
| 156 | + | _params: &CheckoutParams<'_>, | |
| 157 | + | ) -> crate::error::Result<CheckoutResult> { | |
| 158 | + | ScriptedProvider::create_checkout_session(self) | |
| 159 | + | } | |
| 160 | + | async fn create_guest_checkout_session( | |
| 161 | + | &self, | |
| 162 | + | _params: &GuestCheckoutParams<'_>, | |
| 163 | + | ) -> crate::error::Result<CheckoutResult> { | |
| 164 | + | ScriptedProvider::create_guest_checkout_session(self) | |
| 165 | + | } | |
| 166 | + | async fn create_subscription_checkout_session( | |
| 167 | + | &self, | |
| 168 | + | _params: &SubscriptionCheckoutParams<'_>, | |
| 169 | + | ) -> crate::error::Result<CheckoutResult> { | |
| 170 | + | ScriptedProvider::create_subscription_checkout_session(self) | |
| 171 | + | } | |
| 172 | + | async fn create_tip_checkout_session( | |
| 173 | + | &self, | |
| 174 | + | _params: &TipCheckoutParams<'_>, | |
| 175 | + | ) -> crate::error::Result<CheckoutResult> { | |
| 176 | + | ScriptedProvider::create_tip_checkout_session(self) | |
| 177 | + | } | |
| 178 | + | async fn create_fan_plus_checkout_session( | |
| 179 | + | &self, | |
| 180 | + | _price_id: &str, | |
| 181 | + | _user_id: crate::db::UserId, | |
| 182 | + | _success_url: &str, | |
| 183 | + | _cancel_url: &str, | |
| 184 | + | ) -> crate::error::Result<CheckoutResult> { | |
| 185 | + | ScriptedProvider::create_fan_plus_checkout_session(self) | |
| 186 | + | } | |
| 187 | + | async fn create_creator_tier_checkout_session( | |
| 188 | + | &self, | |
| 189 | + | _price_id: &str, | |
| 190 | + | _user_id: crate::db::UserId, | |
| 191 | + | _tier: &str, | |
| 192 | + | _success_url: &str, | |
| 193 | + | _cancel_url: &str, | |
| 194 | + | _trial_days: Option<i32>, | |
| 195 | + | ) -> crate::error::Result<CheckoutResult> { | |
| 196 | + | ScriptedProvider::create_creator_tier_checkout_session(self) | |
| 197 | + | } | |
| 198 | + | async fn create_synckit_app_sub_checkout_session( | |
| 199 | + | &self, | |
| 200 | + | _params: &SynckitAppSubCheckoutParams<'_>, | |
| 201 | + | ) -> crate::error::Result<CheckoutResult> { | |
| 202 | + | ScriptedProvider::create_synckit_app_sub_checkout_session(self) | |
| 203 | + | } | |
| 204 | + | async fn create_cart_checkout_session( | |
| 205 | + | &self, | |
| 206 | + | _params: &CartCheckoutParams<'_>, | |
| 207 | + | ) -> crate::error::Result<CheckoutResult> { | |
| 208 | + | ScriptedProvider::create_cart_checkout_session(self) | |
| 209 | + | } | |
| 210 | + | async fn create_connect_account( | |
| 211 | + | &self, | |
| 212 | + | _email: &str, | |
| 213 | + | ) -> crate::error::Result<ProviderAccountId> { | |
| 214 | + | ScriptedProvider::create_connect_account(self) | |
| 215 | + | } | |
| 216 | + | async fn get_balance( | |
| 217 | + | &self, | |
| 218 | + | _account_id: &str, | |
| 219 | + | _currency: crate::currency::SettlementCurrency, | |
| 220 | + | ) -> crate::error::Result<BalanceSummary> { | |
| 221 | + | ScriptedProvider::get_balance(self) | |
| 222 | + | } | |
| 223 | + | async fn cancel_platform_subscription(&self, _sub: &str) -> crate::error::Result<()> { | |
| 224 | + | ScriptedProvider::cancel_platform_subscription(self) | |
| 225 | + | } | |
| 226 | + | async fn set_platform_cancel_at_period_end( | |
| 227 | + | &self, | |
| 228 | + | _sub: &str, | |
| 229 | + | _cancel: bool, | |
| 230 | + | ) -> crate::error::Result<()> { | |
| 231 | + | ScriptedProvider::set_platform_cancel_at_period_end(self) | |
| 232 | + | } | |
| 233 | + | fn verify_webhook( | |
| 234 | + | &self, | |
| 235 | + | _payload: &str, | |
| 236 | + | _signature: &str, | |
| 237 | + | ) -> crate::error::Result<UntypedEvent> { | |
| 238 | + | ScriptedProvider::verify_webhook(self) | |
| 239 | + | } | |
| 240 | + | fn verify_webhook_v2( | |
| 241 | + | &self, | |
| 242 | + | _payload: &str, | |
| 243 | + | _signature: &str, | |
| 244 | + | ) -> crate::error::Result<serde_json::Value> { | |
| 245 | + | ScriptedProvider::verify_webhook_v2(self) | |
| 246 | + | } | |
| 247 | + | fn normalize_webhook(&self, _event: UntypedEvent) -> crate::error::Result<MnwEvent> { | |
| 248 | + | ScriptedProvider::normalize_webhook(self) | |
| 249 | + | } | |
| 250 | + | async fn update_synckit_subscription_price( | |
| 251 | + | &self, | |
| 252 | + | _subscription_id: &str, | |
| 253 | + | _new_price_cents: i64, | |
| 254 | + | _app_name: &str, | |
| 255 | + | ) -> crate::error::Result<()> { | |
| 256 | + | ScriptedProvider::update_synckit_subscription_price(self) | |
| 257 | + | } | |
| 258 | + | async fn update_synckit_app_sub_price( | |
| 259 | + | &self, | |
| 260 | + | _subscription_id: &str, | |
| 261 | + | _new_price_cents: i64, | |
| 262 | + | _interval: SyncBillingInterval, | |
| 263 | + | _product_name: &str, | |
| 264 | + | ) -> crate::error::Result<()> { | |
| 265 | + | ScriptedProvider::update_synckit_app_sub_price(self) | |
| 266 | + | } | |
| 267 | + | async fn cancel_synckit_subscription(&self, _sub: &str) -> crate::error::Result<()> { | |
| 268 | + | ScriptedProvider::cancel_synckit_subscription(self) | |
| 269 | + | } | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | // ── The capability extensions, every one of them a panic: no lib test | |
| 273 | + | // drives an extension yet, and `ScriptedProvider` implements them so the | |
| 274 | + | // double stays wirable wherever a full provider is expected. | |
| 275 | + | ||
| 276 | + | #[async_trait::async_trait] | |
| 277 | + | impl HostedPortal for ScriptedProvider { | |
| 278 | + | async fn create_billing_portal_session( | |
| 279 | + | &self, | |
| 280 | + | _customer_id: &str, | |
| 281 | + | _return_url: &str, | |
| 282 | + | ) -> crate::error::Result<String> { | |
| 283 | + | ScriptedProvider::create_billing_portal_session(self) | |
| 284 | + | } | |
| 285 | + | async fn create_synckit_billing_portal( | |
| 286 | + | &self, | |
| 287 | + | _customer_id: &str, | |
| 288 | + | _return_url: &str, | |
| 289 | + | ) -> crate::error::Result<String> { | |
| 290 | + | ScriptedProvider::create_synckit_billing_portal(self) | |
| 291 | + | } | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | #[async_trait::async_trait] | |
| 295 | + | impl ConnectOnboarding for ScriptedProvider { | |
| 296 | + | async fn create_account_link( | |
| 297 | + | &self, | |
| 298 | + | _account_id: &str, | |
| 299 | + | _return_url: &str, | |
| 300 | + | _refresh_url: &str, | |
| 301 | + | ) -> crate::error::Result<String> { | |
| 302 | + | ScriptedProvider::create_account_link(self) | |
| 303 | + | } | |
| 304 | + | async fn fetch_account(&self, _account_id: &str) -> crate::error::Result<AccountUpdate> { | |
| 305 | + | ScriptedProvider::fetch_account(self) | |
| 306 | + | } | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | #[async_trait::async_trait] | |
| 310 | + | impl Catalogue for ScriptedProvider { | |
| 311 | + | async fn create_subscription_product_and_price( | |
| 312 | + | &self, | |
| 313 | + | _connected_account_id: &str, | |
| 314 | + | _tier_name: &str, | |
| 315 | + | _tier_description: Option<&str>, | |
| 316 | + | _price_cents: i64, | |
| 317 | + | _currency: crate::currency::SettlementCurrency, | |
| 318 | + | ) -> crate::error::Result<(String, String)> { | |
| 319 | + | ScriptedProvider::create_subscription_product_and_price(self) | |
| 320 | + | } | |
| 321 | + | } | |
| 322 | + | ||
| 323 | + | #[async_trait::async_trait] | |
| 324 | + | impl Refundable for ScriptedProvider { | |
| 325 | + | async fn create_refund_for_transaction( | |
| 326 | + | &self, | |
| 327 | + | _payment_intent_id: &str, | |
| 328 | + | _connected_account_id: &str, | |
| 329 | + | _amount_cents: i64, | |
| 330 | + | _transaction_id: crate::db::TransactionId, | |
| 331 | + | ) -> crate::error::Result<()> { | |
| 332 | + | ScriptedProvider::create_refund_for_transaction(self) | |
| 333 | + | } | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | #[async_trait::async_trait] | |
| 337 | + | impl PlatformTransfers for ScriptedProvider { | |
| 338 | + | async fn create_platform_credit_transfer( | |
| 339 | + | &self, | |
| 340 | + | _connected_account_id: &str, | |
| 341 | + | _amount_cents: i64, | |
| 342 | + | _transaction_id: crate::db::TransactionId, | |
| 343 | + | _currency: crate::currency::SettlementCurrency, | |
| 344 | + | ) -> crate::error::Result<String> { | |
| 345 | + | ScriptedProvider::create_platform_credit_transfer(self) | |
| 346 | + | } | |
| 347 | + | async fn create_platform_credit_reversal( | |
| 348 | + | &self, | |
| 349 | + | _transfer_id: &str, | |
| 350 | + | _amount_cents: i64, | |
| 351 | + | _transaction_id: crate::db::TransactionId, | |
| 352 | + | ) -> crate::error::Result<()> { | |
| 353 | + | ScriptedProvider::create_platform_credit_reversal(self) | |
| 354 | + | } | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | #[async_trait::async_trait] | |
| 358 | + | impl CustodialCustomers for ScriptedProvider { | |
| 359 | + | async fn create_synckit_customer( | |
| 360 | + | &self, | |
| 361 | + | _developer_user_id: crate::db::UserId, | |
| 362 | + | _app_id: crate::db::SyncAppId, | |
| 363 | + | _email: &str, | |
| 364 | + | _app_name: &str, | |
| 365 | + | ) -> crate::error::Result<String> { | |
| 366 | + | ScriptedProvider::create_synckit_customer(self) | |
| 367 | + | } | |
| 368 | + | async fn create_synckit_subscription( | |
| 369 | + | &self, | |
| 370 | + | _customer_id: &str, | |
| 371 | + | _app_id: crate::db::SyncAppId, | |
| 372 | + | _app_name: &str, | |
| 373 | + | _price_cents: i64, | |
| 374 | + | ) -> crate::error::Result<SynckitSubResult> { | |
| 375 | + | ScriptedProvider::create_synckit_subscription(self) | |
| 376 | + | } | |
| 377 | + | } |