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

Commit de011b5

Browse files
authored
Merge branch 'main' into feb_06_public_api_fix
2 parents 51a6b84 + faecc38 commit de011b5

33 files changed

+192
-1118
lines changed

billing/tests/test_views.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,6 +1643,132 @@ class MockPaymentMethod:
16431643
"sub_123", default_payment_method=payment_method_retrieve_mock.return_value
16441644
)
16451645

1646+
@patch("logging.Logger.error")
1647+
@patch("services.billing.stripe.PaymentMethod.attach")
1648+
@patch("services.billing.stripe.Customer.modify")
1649+
@patch("services.billing.stripe.Subscription.modify")
1650+
@patch("services.billing.stripe.PaymentMethod.retrieve")
1651+
def test_check_and_handle_delayed_notification_payment_methods_no_subscription(
1652+
self,
1653+
payment_method_retrieve_mock,
1654+
subscription_modify_mock,
1655+
customer_modify_mock,
1656+
payment_method_attach_mock,
1657+
log_error_mock,
1658+
):
1659+
class MockPaymentMethod:
1660+
type = "us_bank_account"
1661+
us_bank_account = {}
1662+
id = "pm_123"
1663+
1664+
payment_method_retrieve_mock.return_value = MockPaymentMethod()
1665+
1666+
self.owner.stripe_subscription_id = None
1667+
self.owner.stripe_customer_id = "cus_123"
1668+
self.owner.save()
1669+
1670+
handler = StripeWebhookHandler()
1671+
handler._check_and_handle_delayed_notification_payment_methods(
1672+
"cus_123", "pm_123"
1673+
)
1674+
1675+
payment_method_retrieve_mock.assert_called_once_with("pm_123")
1676+
payment_method_attach_mock.assert_not_called()
1677+
customer_modify_mock.assert_not_called()
1678+
subscription_modify_mock.assert_not_called()
1679+
1680+
log_error_mock.assert_called_once_with(
1681+
"No owners found with that customer_id, something went wrong",
1682+
extra=dict(customer_id="cus_123"),
1683+
)
1684+
1685+
@patch("logging.Logger.error")
1686+
@patch("services.billing.stripe.PaymentMethod.attach")
1687+
@patch("services.billing.stripe.Customer.modify")
1688+
@patch("services.billing.stripe.Subscription.modify")
1689+
@patch("services.billing.stripe.PaymentMethod.retrieve")
1690+
def test_check_and_handle_delayed_notification_payment_methods_no_customer(
1691+
self,
1692+
payment_method_retrieve_mock,
1693+
subscription_modify_mock,
1694+
customer_modify_mock,
1695+
payment_method_attach_mock,
1696+
log_error_mock,
1697+
):
1698+
class MockPaymentMethod:
1699+
type = "us_bank_account"
1700+
us_bank_account = {}
1701+
id = "pm_123"
1702+
1703+
payment_method_retrieve_mock.return_value = MockPaymentMethod()
1704+
1705+
handler = StripeWebhookHandler()
1706+
handler._check_and_handle_delayed_notification_payment_methods(
1707+
"cus_1", "pm_123"
1708+
)
1709+
1710+
payment_method_retrieve_mock.assert_called_once_with("pm_123")
1711+
payment_method_attach_mock.assert_not_called()
1712+
customer_modify_mock.assert_not_called()
1713+
subscription_modify_mock.assert_not_called()
1714+
1715+
log_error_mock.assert_called_once_with(
1716+
"No owners found with that customer_id, something went wrong",
1717+
extra=dict(customer_id="cus_1"),
1718+
)
1719+
1720+
@patch("services.billing.stripe.PaymentMethod.attach")
1721+
@patch("services.billing.stripe.Customer.modify")
1722+
@patch("services.billing.stripe.Subscription.modify")
1723+
@patch("services.billing.stripe.PaymentMethod.retrieve")
1724+
def test_check_and_handle_delayed_notification_payment_methods_multiple_subscriptions(
1725+
self,
1726+
payment_method_retrieve_mock,
1727+
subscription_modify_mock,
1728+
customer_modify_mock,
1729+
payment_method_attach_mock,
1730+
):
1731+
class MockPaymentMethod:
1732+
type = "us_bank_account"
1733+
us_bank_account = {}
1734+
id = "pm_123"
1735+
1736+
payment_method_retrieve_mock.return_value = MockPaymentMethod()
1737+
1738+
self.owner.stripe_subscription_id = "sub_123"
1739+
self.owner.stripe_customer_id = "cus_123"
1740+
self.owner.save()
1741+
1742+
OwnerFactory(stripe_subscription_id="sub_124", stripe_customer_id="cus_123")
1743+
1744+
handler = StripeWebhookHandler()
1745+
handler._check_and_handle_delayed_notification_payment_methods(
1746+
"cus_123", "pm_123"
1747+
)
1748+
1749+
payment_method_retrieve_mock.assert_called_once_with("pm_123")
1750+
payment_method_attach_mock.assert_called_once_with(
1751+
payment_method_retrieve_mock.return_value, customer="cus_123"
1752+
)
1753+
customer_modify_mock.assert_called_once_with(
1754+
"cus_123",
1755+
invoice_settings={
1756+
"default_payment_method": payment_method_retrieve_mock.return_value
1757+
},
1758+
)
1759+
subscription_modify_mock.assert_has_calls(
1760+
[
1761+
call(
1762+
"sub_123",
1763+
default_payment_method=payment_method_retrieve_mock.return_value,
1764+
),
1765+
call(
1766+
"sub_124",
1767+
default_payment_method=payment_method_retrieve_mock.return_value,
1768+
),
1769+
]
1770+
)
1771+
16461772
@patch(
16471773
"billing.views.StripeWebhookHandler._check_and_handle_delayed_notification_payment_methods"
16481774
)

billing/views.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,6 @@ def _check_and_handle_delayed_notification_payment_methods(
539539
When verification succeeds, this attaches the payment method to the customer and sets
540540
it as the default payment method for both the customer and subscription.
541541
"""
542-
owner = Owner.objects.get(stripe_customer_id=customer_id)
543542
payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
544543

545544
is_us_bank_account = payment_method.type == "us_bank_account" and hasattr(
@@ -548,19 +547,34 @@ def _check_and_handle_delayed_notification_payment_methods(
548547

549548
should_set_as_default = is_us_bank_account
550549

550+
# attach the payment method + set as default on the invoice and subscription
551551
if should_set_as_default:
552-
# attach the payment method + set as default on the invoice and subscription
553-
stripe.PaymentMethod.attach(
554-
payment_method, customer=owner.stripe_customer_id
555-
)
556-
stripe.Customer.modify(
557-
owner.stripe_customer_id,
558-
invoice_settings={"default_payment_method": payment_method},
559-
)
560-
stripe.Subscription.modify(
561-
owner.stripe_subscription_id, default_payment_method=payment_method
552+
# retrieve the number of owners to update
553+
owners = Owner.objects.filter(
554+
stripe_customer_id=customer_id, stripe_subscription_id__isnull=False
562555
)
563556

557+
if owners.exists():
558+
# Even if multiple results are returned, these two stripe calls are
559+
# just for a single customer
560+
stripe.PaymentMethod.attach(payment_method, customer=customer_id)
561+
stripe.Customer.modify(
562+
customer_id,
563+
invoice_settings={"default_payment_method": payment_method},
564+
)
565+
566+
# But this one is for each subscription an owner may have
567+
for owner in owners:
568+
stripe.Subscription.modify(
569+
owner.stripe_subscription_id,
570+
default_payment_method=payment_method,
571+
)
572+
else:
573+
log.error(
574+
"No owners found with that customer_id, something went wrong",
575+
extra=dict(customer_id=customer_id),
576+
)
577+
564578
def payment_intent_succeeded(self, payment_intent: stripe.PaymentIntent) -> None:
565579
"""
566580
Stripe payment_intent.succeeded webhook event is emitted when a

codecov/settings_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@
443443
"profiling": "shared.django_apps.profiling.migrations",
444444
"reports": "shared.django_apps.reports.migrations",
445445
"staticanalysis": "shared.django_apps.staticanalysis.migrations",
446+
"timeseries": "shared.django_apps.timeseries.migrations",
446447
}
447448

448449
# to aid in debugging, print out this info on startup. If no license, prints nothing

core/admin.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from codecov.admin import AdminMixin
88
from codecov_auth.models import RepositoryToken
99
from core.models import Pull, Repository
10+
from services.task.task import TaskService
1011

1112

1213
class RepositoryTokenInline(admin.TabularInline):
@@ -90,12 +91,19 @@ class RepositoryAdmin(AdminMixin, admin.ModelAdmin):
9091
"webhook_secret",
9192
)
9293

93-
def has_delete_permission(self, request, obj=None):
94-
return False
95-
9694
def has_add_permission(self, _, obj=None):
9795
return False
9896

97+
def has_delete_permission(self, request, obj=None):
98+
return bool(request.user and request.user.is_superuser)
99+
100+
def delete_queryset(self, request, queryset) -> None:
101+
for repo in queryset:
102+
TaskService().flush_repo(repository_id=repo.repoid)
103+
104+
def delete_model(self, request, obj) -> None:
105+
TaskService().flush_repo(repository_id=obj.repoid)
106+
99107

100108
@admin.register(Pull)
101109
class PullsAdmin(AdminMixin, admin.ModelAdmin):

core/apps.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22

33
from django.apps import AppConfig
4-
from django.core.management import call_command
54
from shared.helpers.cache import RedisBackend
65

76
from services.redis_configuration import get_redis_connection
@@ -17,24 +16,6 @@ class CoreConfig(AppConfig):
1716
def ready(self):
1817
import core.signals # noqa: F401
1918

20-
if RUN_ENV == "DEV":
21-
try:
22-
# Call your management command here
23-
call_command(
24-
"insert_data_to_db_from_csv",
25-
"core/management/commands/codecovTiers-Jan25.csv",
26-
"--model",
27-
"tiers",
28-
)
29-
call_command(
30-
"insert_data_to_db_from_csv",
31-
"core/management/commands/codecovPlans-Jan25.csv",
32-
"--model",
33-
"plans",
34-
)
35-
except Exception as e:
36-
logger.error(f"Failed to run startup command: {e}")
37-
3819
if RUN_ENV not in ["DEV", "TESTING"]:
3920
cache_backend = RedisBackend(get_redis_connection())
4021
cache.configure(cache_backend)

core/management/commands/codecovPlans-Jan25.csv

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
10,2025-01-16 04:40:55.162 -0800,2025-01-24 11:33:46.043 -0800,0,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories""}",,true,Developer,,,false,users-trial,6,
33
9,2025-01-16 04:39:59.759 -0800,2025-01-24 11:34:10.038 -0800,10,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",annually,true,Enterprise Cloud,,,true,users-enterprisey,4,price_1LmjzwGlVGuVgOrkIwlM46EU
44
8,2025-01-16 04:39:15.877 -0800,2025-01-24 11:34:31.904 -0800,12,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",monthly,true,Enterprise Cloud,,,true,users-enterprisem,4,price_1LmjypGlVGuVgOrkzKtNqhwW
5-
7,2025-01-16 04:38:12.544 -0800,2025-01-24 11:34:53.935 -0800,4,"{""Up to 10 users"",""Unlimited repositories"",""2500 private repo uploads"",""Patch coverage analysis""}",annually,true,Team,10,2500,true,users-teamy,2,price_1NrlXiGlVGuVgOrkgMTw5yno
6-
6,2025-01-16 04:37:08.918 -0800,2025-01-24 11:35:15.346 -0800,5,"{""Up to 10 users"",""Unlimited repositories"",""2500 private repo uploads"",""Patch coverage analysis""}",monthly,true,Team,10,2500,true,users-teamm,2,price_1NqPKdGlVGuVgOrkm9OFvtz8
7-
5,2025-01-16 04:35:34.152 -0800,2025-01-24 11:35:42.724 -0800,10,"{""Includes 5 seats"",""$10 per additional seat"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",annually,true,Sentry Pro,5,,true,users-sentryy,5,price_1MlYAYGlVGuVgOrke9SdbBUn
8-
4,2025-01-16 04:34:33.867 -0800,2025-01-24 11:35:48.218 -0800,12,"{""Includes 5 seats"",""$10 per additional seat"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",monthly,true,Sentry Pro,5,,true,users-sentrym,5,price_1MlY9yGlVGuVgOrkHluurBtJ
9-
3,2025-01-16 04:32:44.655 -0800,2025-01-24 11:36:09.660 -0800,10,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",annually,true,Pro,,,true,users-pr-inappy,3,price_1Gv2COGlVGuVgOrkuOYVLIj7
10-
2,2025-01-16 04:30:42.897 -0800,2025-01-24 11:36:14.651 -0800,12,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",monthly,true,Pro,,,true,users-pr-inappm,3,price_1Gv2B8GlVGuVgOrkFnLunCgc
5+
7,2025-01-16 04:38:12.544 -0800,2025-01-24 11:34:53.935 -0800,4,"{""Up to 10 users"",""Unlimited repositories"",""2500 private repo uploads"",""Patch coverage analysis""}",annually,true,Team,10,2500,true,users-teamy,2,price_1OCM2cGlVGuVgOrkMWUFjPFz
6+
6,2025-01-16 04:37:08.918 -0800,2025-01-24 11:35:15.346 -0800,5,"{""Up to 10 users"",""Unlimited repositories"",""2500 private repo uploads"",""Patch coverage analysis""}",monthly,true,Team,10,2500,true,users-teamm,2,price_1OCM0gGlVGuVgOrkWDYEBtSL
7+
5,2025-01-16 04:35:34.152 -0800,2025-01-24 11:35:42.724 -0800,10,"{""Includes 5 seats"",""$10 per additional seat"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",annually,true,Sentry Pro,5,,true,users-sentryy,5,price_1Mj1mMGlVGuVgOrkC0ORc6iW
8+
4,2025-01-16 04:34:33.867 -0800,2025-01-24 11:35:48.218 -0800,12,"{""Includes 5 seats"",""$10 per additional seat"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",monthly,true,Sentry Pro,5,,true,users-sentrym,5,price_1Mj1kYGlVGuVgOrk7jucaZAa
9+
3,2025-01-16 04:32:44.655 -0800,2025-01-24 11:36:09.660 -0800,10,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",annually,true,Pro,,,true,users-pr-inappy,3,plan_H6P16wij3lUuxg
10+
2,2025-01-16 04:30:42.897 -0800,2025-01-24 11:36:14.651 -0800,12,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",monthly,true,Pro,,,true,users-pr-inappm,3,plan_H6P3KZXwmAbqPS
1111
13,2025-01-23 14:25:04.793 -0800,2025-01-23 14:25:04.793 -0800,12,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories""}",,true,Github Marketplace,,,false,users,3,
1212
12,2025-01-16 04:44:51.064 -0800,2025-01-24 11:33:14.405 -0800,0,"{""Up to 1 user"",""Unlimited public repositories"",""Unlimited private repositories""}",,true,Developer,1,250,false,users-developer,2,
1313
11,2025-01-16 04:44:01.249 -0800,2025-01-24 11:33:28.532 -0800,0,"{""Up to 1 user"",""Unlimited public repositories"",""Unlimited private repositories""}",,true,Developer,1,250,false,users-basic,1,

core/management/commands/insert_data_to_db_from_csv.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,15 @@ def handle(self, *args, **kwargs):
5151
)
5252
continue
5353

54-
Model.objects.create(**model_data)
55-
self.stdout.write(self.style.SUCCESS(f"Inserted row: {row}"))
54+
try:
55+
Model.objects.update_or_create(
56+
defaults=model_data,
57+
id=row.get("id"),
58+
)
59+
self.stdout.write(self.style.SUCCESS(f"Inserted row: {row}"))
60+
except Exception as e:
61+
self.stdout.write(self.style.ERROR(f"Error inserting row: {e}"))
62+
continue
5663

5764
self.stdout.write(
5865
self.style.SUCCESS(

dev.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# starts the development server using gunicorn
44
# NEVER run production with the --reload option command
5-
echo "Starting gunicorn in dev mode"
5+
echo "API: Starting gunicorn in dev mode"
66

77
_start_gunicorn() {
88
if [ -n "$PROMETHEUS_MULTIPROC_DIR" ]; then
@@ -19,6 +19,8 @@ _start_gunicorn() {
1919
python manage.py migrate
2020
python manage.py migrate --database "timeseries" timeseries
2121
python manage.py pgpartition --yes --skip-delete
22+
python manage.py insert_data_to_db_from_csv core/management/commands/codecovTiers-Jan25.csv --model tiers
23+
python manage.py insert_data_to_db_from_csv core/management/commands/codecovPlans-Jan25.csv --model plans
2224
fi
2325
if [[ "$DEBUGPY" ]]; then
2426
pip install debugpy

graphql_api/types/plan/plan.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def resolve_trial_end_date(plan_service: PlanService, info) -> Optional[datetime
3030

3131
@plan_bindable.field("trialStatus")
3232
def resolve_trial_status(plan_service: PlanService, info) -> TrialStatus:
33-
if plan_service.trial_status is None:
33+
if not plan_service.trial_status:
3434
return TrialStatus.NOT_STARTED
3535
return TrialStatus(plan_service.trial_status)
3636

requirements.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ freezegun
2525
google-cloud-pubsub
2626
gunicorn>=22.0.0
2727
https://github.com/codecov/opentelem-python/archive/refs/tags/v0.0.4a1.tar.gz#egg=codecovopentelem
28-
https://github.com/codecov/shared/archive/7ba099fa0552244c77a8cc3a4b772216613c09c8.tar.gz#egg=shared
28+
https://github.com/codecov/shared/archive/abf6180de5cee3ac99b7fbf909bd82c7325063fd.tar.gz#egg=shared
2929
https://github.com/photocrowd/django-cursor-pagination/archive/f560902696b0c8509e4d95c10ba0d62700181d84.tar.gz
3030
idna>=3.7
3131
minio
@@ -58,4 +58,4 @@ starlette==0.40.0
5858
stripe>=11.4.1
5959
urllib3>=1.26.19
6060
vcrpy
61-
whitenoise
61+
whitenoise

0 commit comments

Comments
 (0)