diff --git a/api/internal/owner/views.py b/api/internal/owner/views.py index 96fe636844..9b7ba0730e 100644 --- a/api/internal/owner/views.py +++ b/api/internal/owner/views.py @@ -113,6 +113,23 @@ def update_billing_address(self, request, *args, **kwargs): return Response(self.get_serializer(owner).data) + @action(detail=False, methods=["get"]) + @stripe_safe + def setup_intent(self, request, *args, **kwargs): + """ + GET a Stripe setupIntent clientSecret for updating payment method + """ + try: + billing = BillingService(requesting_user=request.current_owner) + client_secret = billing.get_setup_intent(self.owner) + return Response({"client_secret": client_secret}) + except Exception as e: + log.error( + f"Error getting setup intent for owner {self.owner.ownerid}", + extra={"error": str(e)}, + ) + raise ValidationError(detail="Unable to create setup intent") + class UsersOrderingFilter(filters.OrderingFilter): def get_valid_fields(self, queryset, view, context=None): fields = super().get_valid_fields(queryset, view, context=context or {}) diff --git a/services/billing.py b/services/billing.py index 4568f26d40..db419c19c2 100644 --- a/services/billing.py +++ b/services/billing.py @@ -59,6 +59,10 @@ def modify_subscription(self, owner, plan): def create_checkout_session(self, owner, plan): pass + @abstractmethod + def create_setup_intent(self, owner): + pass + @abstractmethod def get_subscription(self, owner): pass @@ -539,6 +543,23 @@ def create_checkout_session(self, owner: Owner, desired_plan): ) return session["id"] + @_log_stripe_error + def create_setup_intent(self, owner: Owner): + log.info( + "Stripe create setup intent for owner", + extra=dict( + owner_id=owner.ownerid, + user_id=self.requesting_user.ownerid, + subscription_id=owner.stripe_subscription_id, + customer_id=owner.stripe_customer_id, + ), + ) + setup_intent = stripe.SetupIntent.create( + payment_method_types=['card', 'us_bank_account'], + customer=owner.stripe_customer_id, + ) + return setup_intent.client_secret + @_log_stripe_error def update_payment_method(self, owner: Owner, payment_method): log.info( @@ -722,6 +743,9 @@ def __init__(self, payment_service=None, requesting_user=None): def get_subscription(self, owner): return self.payment_service.get_subscription(owner) + def get_setup_intent(self, owner): + return self.payment_service.create_setup_intent(owner) + def get_schedule(self, owner): return self.payment_service.get_schedule(owner) diff --git a/services/tests/test_billing.py b/services/tests/test_billing.py index 66b3b1b383..eaed0adb6b 100644 --- a/services/tests/test_billing.py +++ b/services/tests/test_billing.py @@ -1825,6 +1825,15 @@ def test_apply_cancellation_discount_existing_coupon( assert not customer_modify_mock.called assert not coupon_create_mock.called + @patch("services.billing.stripe.SetupIntent.create") + def test_get_setup_intent(self, setup_intent_create_mock): + owner = OwnerFactory(stripe_customer_id="test-customer-id") + setup_intent_create_mock.return_value = {"client_secret": "test-client-secret"} + resp = self.stripe.get_setup_intent(owner) + self.stripe.payment_service.get_setup_intent.assert_called_once_with(owner) + + assert resp.client_secret == "test-client-secret" + class MockPaymentService(AbstractPaymentService): def list_filtered_invoices(self, owner, limit=10): @@ -2022,3 +2031,4 @@ def test_get_invoice(self, get_invoice_mock): owner = OwnerFactory() self.billing_service.get_invoice(owner, "abc") get_invoice_mock.assert_called_once_with(owner, "abc") +