Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 198f23e

Browse files
move create-setup-intent to graphql api
1 parent 0d80364 commit 198f23e

File tree

17 files changed

+207
-53
lines changed

17 files changed

+207
-53
lines changed

api/internal/owner/serializers.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,8 @@ class StripeCardSerializer(serializers.Serializer):
109109

110110

111111
class StripeUSBankAccountSerializer(serializers.Serializer):
112-
account_holder_type = serializers.CharField()
113-
account_type = serializers.CharField()
114112
bank_name = serializers.CharField()
115113
last4 = serializers.CharField()
116-
routing_number = serializers.CharField()
117114

118115

119116
class StripePaymentMethodSerializer(serializers.Serializer):

api/internal/owner/views.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,31 @@ def update_payment(self, request, *args, **kwargs):
8080
@action(detail=False, methods=["patch"])
8181
@stripe_safe
8282
def update_email(self, request, *args, **kwargs):
83+
"""
84+
Update the email address associated with the owner's billing account.
85+
86+
Args:
87+
request: The HTTP request object containing:
88+
- new_email: The new email address to update to
89+
- should_propagate_to_payment_methods: Optional boolean flag to update email on payment methods (default False)
90+
91+
Returns:
92+
Response with serialized owner data
93+
94+
Raises:
95+
ValidationError: If no new_email is provided in the request
96+
"""
8397
new_email = request.data.get("new_email")
8498
if not new_email:
8599
raise ValidationError(detail="No new_email sent")
86100
owner = self.get_object()
87101
billing = BillingService(requesting_user=request.current_owner)
88-
should_propagate = request.data.get("should_propagate_to_payment_methods", False)
89-
billing.update_email_address(owner, new_email, should_propagate_to_payment_methods=should_propagate)
102+
should_propagate = request.data.get(
103+
"should_propagate_to_payment_methods", False
104+
)
105+
billing.update_email_address(
106+
owner, new_email, should_propagate_to_payment_methods=should_propagate
107+
)
90108
return Response(self.get_serializer(owner).data)
91109

92110
@action(detail=False, methods=["patch"])
@@ -113,16 +131,21 @@ def update_billing_address(self, request, *args, **kwargs):
113131
billing.update_billing_address(owner, name, billing_address=formatted_address)
114132
return Response(self.get_serializer(owner).data)
115133

116-
117-
@action(detail=False, methods=["get"])
134+
@action(detail=False, methods=["post"])
118135
@stripe_safe
119136
def setup_intent(self, request, *args, **kwargs):
120137
"""
121-
GET a Stripe setupIntent clientSecret for updating payment method
138+
Create a Stripe SetupIntent to securely collect payment details.
139+
140+
Returns:
141+
Response with SetupIntent client_secret for frontend payment method setup.
142+
143+
Raises:
144+
ValidationError: If SetupIntent creation fails
122145
"""
123146
try:
124147
billing = BillingService(requesting_user=request.current_owner)
125-
client_secret = billing.get_setup_intent(self.owner)
148+
client_secret = billing.create_setup_intent(self.owner)
126149
return Response({"client_secret": client_secret})
127150
except Exception as e:
128151
log.error(
@@ -131,6 +154,7 @@ def setup_intent(self, request, *args, **kwargs):
131154
)
132155
raise ValidationError(detail="Unable to create setup intent")
133156

157+
134158
class UsersOrderingFilter(filters.OrderingFilter):
135159
def get_valid_fields(self, queryset, view, context=None):
136160
fields = super().get_valid_fields(queryset, view, context=context or {})

billing/views.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,6 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
312312
self._log_updated([owner])
313313

314314
def customer_subscription_updated(self, subscription: stripe.Subscription) -> None:
315-
print("CUSTOMER SUBSCRIPTION UPDATED", subscription)
316315
owners: QuerySet[Owner] = Owner.objects.filter(
317316
stripe_subscription_id=subscription.id,
318317
stripe_customer_id=subscription.customer,
@@ -409,8 +408,6 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
409408
)
410409

411410
def customer_updated(self, customer: stripe.Customer) -> None:
412-
print("CUSTOMER UPDATED", customer)
413-
414411
new_default_payment_method = customer["invoice_settings"][
415412
"default_payment_method"
416413
]

codecov/settings_base.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@
9898
"setup", "graphql", "query_cost_threshold", default=10000
9999
)
100100

101-
GRAPHQL_RATE_LIMIT_ENABLED = get_config("setup", "graphql", "rate_limit_enabled", default=True)
101+
GRAPHQL_RATE_LIMIT_ENABLED = get_config(
102+
"setup", "graphql", "rate_limit_enabled", default=True
103+
)
102104

103105
GRAPHQL_RATE_LIMIT_RPM = get_config("setup", "graphql", "rate_limit_rpm", default=300)
104106

@@ -426,7 +428,9 @@
426428
SHELTER_PUBSUB_PROJECT_ID = get_config("setup", "shelter", "pubsub_project_id")
427429
SHELTER_PUBSUB_SYNC_REPO_TOPIC_ID = get_config("setup", "shelter", "sync_repo_topic_id")
428430

429-
STRIPE_PAYMENT_METHOD_CONFIGURATION_ID = get_config("setup", "stripe", "payment_method_configuration", default=None)
431+
STRIPE_PAYMENT_METHOD_CONFIGURATION_ID = get_config(
432+
"setup", "stripe", "payment_method_configuration", default=None
433+
)
430434

431435
# Allows to do migrations from another module
432436
MIGRATION_MODULES = {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import logging
2+
3+
import stripe
4+
5+
from codecov.commands.base import BaseInteractor
6+
from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError
7+
from codecov.db import sync_to_async
8+
from codecov_auth.helpers import current_user_part_of_org
9+
from codecov_auth.models import Owner
10+
from services.billing import BillingService
11+
12+
log = logging.getLogger(__name__)
13+
14+
class CreateStripeSetupIntentInteractor(BaseInteractor):
15+
def validate(self, owner_obj: Owner) -> None:
16+
if not self.current_user.is_authenticated:
17+
raise Unauthenticated()
18+
if not owner_obj:
19+
raise ValidationError("Owner not found")
20+
if not current_user_part_of_org(self.current_owner, owner_obj):
21+
raise Unauthorized()
22+
23+
def create_setup_intent(self, owner_obj: Owner) -> stripe.SetupIntent:
24+
try:
25+
billing = BillingService(requesting_user=self.current_owner)
26+
return billing.create_setup_intent(owner_obj)
27+
except Exception as e:
28+
log.error(
29+
f"Error getting setup intent for owner {owner_obj.ownerid}",
30+
extra={"error": str(e)},
31+
)
32+
raise ValidationError("Unable to create setup intent")
33+
34+
@sync_to_async
35+
def execute(self, owner: str) -> stripe.SetupIntent:
36+
owner_obj = Owner.objects.filter(username=owner, service=self.service).first()
37+
self.validate(owner_obj)
38+
return self.create_setup_intent(owner_obj)

codecov_auth/commands/owner/owner.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .interactors.cancel_trial import CancelTrialInteractor
44
from .interactors.create_api_token import CreateApiTokenInteractor
5+
from .interactors.create_stripe_setup_intent import CreateStripeSetupIntentInteractor
56
from .interactors.create_user_token import CreateUserTokenInteractor
67
from .interactors.delete_session import DeleteSessionInteractor
78
from .interactors.fetch_owner import FetchOwnerInteractor
@@ -28,6 +29,9 @@ class OwnerCommands(BaseCommand):
2829
def create_api_token(self, name):
2930
return self.get_interactor(CreateApiTokenInteractor).execute(name)
3031

32+
def create_stripe_setup_intent(self, owner):
33+
return self.get_interactor(CreateStripeSetupIntentInteractor).execute(owner)
34+
3135
def delete_session(self, sessionid: int):
3236
return self.get_interactor(DeleteSessionInteractor).execute(sessionid)
3337

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from unittest.mock import patch
2+
from django.test import TransactionTestCase
3+
from shared.django_apps.core.tests.factories import OwnerFactory
4+
5+
from codecov_auth.models import Session
6+
from graphql_api.tests.helper import GraphQLTestHelper
7+
8+
query = """
9+
mutation($input: CreateStripeSetupIntentInput!) {
10+
createStripeSetupIntent(input: $input) {
11+
error {
12+
__typename
13+
}
14+
clientSecret
15+
}
16+
}
17+
"""
18+
19+
20+
class CreateStripeSetupIntentTestCase(GraphQLTestHelper, TransactionTestCase):
21+
def setUp(self):
22+
self.owner = OwnerFactory(username="codecov-user")
23+
24+
def test_when_unauthenticated(self):
25+
data = self.gql_request(query, variables={"input": {"owner": "somename"}})
26+
assert data["createStripeSetupIntent"]["error"]["__typename"] == "UnauthenticatedError"
27+
28+
@patch("services.billing.stripe.SetupIntent.create")
29+
def test_when_authenticated(self, setup_intent_create_mock):
30+
setup_intent_create_mock.return_value = {"client_secret": "test-client-secret"}
31+
data = self.gql_request(
32+
query, owner=self.owner, variables={"input": {"owner": self.owner.username}}
33+
)
34+
assert data["createStripeSetupIntent"]["clientSecret"] == "test-client-secret"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
input CreateStripeSetupIntentInput {
2+
owner: String!
3+
}

graphql_api/types/invoice/invoice.graphql

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Period {
3434
type PaymentMethod {
3535
billingDetails: BillingDetails
3636
card: Card
37+
usBankAccount: USBankAccount
3738
}
3839

3940
type Card {
@@ -43,12 +44,9 @@ type Card {
4344
last4: String
4445
}
4546

46-
type BankAccount {
47-
accountHolderType: String
48-
accountType: String
47+
type USBankAccount {
4948
bankName: String
5049
last4: String
51-
routingNumber: String
5250
}
5351

5452
type BillingDetails {

graphql_api/types/mutation/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .activate_measurements import gql_activate_measurements
44
from .cancel_trial import gql_cancel_trial
55
from .create_api_token import gql_create_api_token
6+
from .create_stripe_setup_intent import gql_create_stripe_setup_intent
67
from .create_user_token import gql_create_user_token
78
from .delete_component_measurements import gql_delete_component_measurements
89
from .delete_flag import gql_delete_flag
@@ -30,6 +31,7 @@
3031

3132
mutation = ariadne_load_local_graphql(__file__, "mutation.graphql")
3233
mutation = mutation + gql_create_api_token
34+
mutation = mutation + gql_create_stripe_setup_intent
3335
mutation = mutation + gql_sync_with_git_provider
3436
mutation = mutation + gql_delete_session
3537
mutation = mutation + gql_set_yaml_on_owner

0 commit comments

Comments
 (0)