Skip to content

Commit

Permalink
Organization launch priorities (#13395)
Browse files Browse the repository at this point in the history
* Add explicit note about Owner (User) => Owner (Organization) transfers

* add notes about billing/invoices to the delete org box

* Explicit instructions on how to handle org owned projects before deleting org

* stripe webhook: be permissive when recieving hooks for customers/subscriptions

we share a stripe account with other PSF things (namely pycon) that use customers

* stripe billing: format the name of the organization to include display name

* billing: update customer names and descriptions in stripe

* fix customer_name

* lint

* fix tests, lint

nailed it on that review @miketheman.
  • Loading branch information
ewdurbin authored Apr 8, 2023
1 parent f29cda4 commit 4b954a2
Show file tree
Hide file tree
Showing 14 changed files with 222 additions and 76 deletions.
13 changes: 5 additions & 8 deletions tests/unit/api/test_billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import pytest
import stripe

from pyramid.httpexceptions import HTTPBadRequest, HTTPNoContent, HTTPNotFound
from pyramid.httpexceptions import HTTPBadRequest, HTTPNoContent

from warehouse.api import billing

Expand Down Expand Up @@ -238,7 +238,7 @@ def test_handle_billing_webhook_event_subscription_deleted_update(

billing.handle_billing_webhook_event(db_request, event)

def test_handle_billing_webhook_event_subscription_deleted_not_found(
def test_handle_billing_webhook_event_subscription_deleted(
self, db_request, subscription_service
):
organization = OrganizationFactory.create()
Expand All @@ -258,8 +258,7 @@ def test_handle_billing_webhook_event_subscription_deleted_not_found(
},
}

with pytest.raises(HTTPNotFound):
billing.handle_billing_webhook_event(db_request, event)
billing.handle_billing_webhook_event(db_request, event)

def test_handle_billing_webhook_event_subscription_deleted_invalid_status(
self, db_request
Expand Down Expand Up @@ -359,8 +358,7 @@ def test_handle_billing_webhook_event_subscription_updated_not_found(
},
}

with pytest.raises(HTTPNotFound):
billing.handle_billing_webhook_event(db_request, event)
billing.handle_billing_webhook_event(db_request, event)

def test_handle_billing_webhook_event_subscription_updated_no_change(
self, db_request
Expand Down Expand Up @@ -478,8 +476,7 @@ def test_handle_billing_webhook_event_customer_deleted_no_subscriptions(
},
}

with pytest.raises(HTTPNotFound):
billing.handle_billing_webhook_event(db_request, event)
billing.handle_billing_webhook_event(db_request, event)

def test_handle_billing_webhook_event_customer_deleted_invalid_customer(
self, db_request
Expand Down
34 changes: 33 additions & 1 deletion tests/unit/manage/views/test_organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,29 +553,47 @@ def test_manage_organization(
),
]

@pytest.mark.parametrize("orgtype", list(OrganizationType))
@pytest.mark.parametrize(
["orgtype", "has_customer"],
[(orgtype, True) for orgtype in list(OrganizationType)]
+ [(orgtype, False) for orgtype in list(OrganizationType)],
)
def test_save_organization(
self,
db_request,
pyramid_user,
orgtype,
has_customer,
billing_service,
organization_service,
enable_organizations,
monkeypatch,
):
organization = OrganizationFactory.create(orgtype=orgtype)
customer = StripeCustomerFactory.create()
if has_customer:
OrganizationStripeCustomerFactory.create(
organization=organization, customer=customer
)
db_request.POST = {
"display_name": organization.display_name,
"link_url": organization.link_url,
"description": organization.description,
"orgtype": organization.orgtype,
}

db_request.registry.settings["site.name"] = "PiePeaEye"

monkeypatch.setattr(
organization_service,
"update_organization",
pretend.call_recorder(lambda *a, **kw: None),
)
monkeypatch.setattr(
billing_service,
"update_customer",
pretend.call_recorder(lambda stripe_customer_id, name, description: None),
)

save_organization_obj = pretend.stub(
validate=lambda: True, data=db_request.POST
Expand All @@ -598,6 +616,20 @@ def test_save_organization(
assert organization_service.update_organization.calls == [
pretend.call(organization.id, **db_request.POST)
]
assert billing_service.update_customer.calls == (
[
pretend.call(
customer.customer_id,
(
f"PiePeaEye Organization - {organization.display_name} "
f"({organization.name})"
),
organization.description,
)
]
if has_customer
else []
)
assert send_email.calls == [
pretend.call(
db_request,
Expand Down
1 change: 1 addition & 0 deletions tests/unit/mock/test_billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def organization(self):
return OrganizationFactory.create()

def test_disable_organizations(self, db_request, organization):
db_request.organization_access = False
with pytest.raises(HTTPNotFound):
billing.MockBillingViews(organization, db_request)

Expand Down
13 changes: 13 additions & 0 deletions tests/unit/organizations/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ def test_traversal_cant_find(self, db_request):


class TestOrganization:
def test_customer_name(self, db_session):
organization = DBOrganizationFactory.create(
name="pypi", display_name="The Python Package Index"
)
assert (
organization.customer_name()
== "PyPI Organization - The Python Package Index (pypi)"
)
assert (
organization.customer_name("Test PyPI")
== "Test PyPI Organization - The Python Package Index (pypi)"
)

def test_acl(self, db_session):
organization = DBOrganizationFactory.create()
owner1 = DBOrganizationRoleFactory.create(organization=organization)
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/subscriptions/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,26 @@ def test_create_customer(self, billing_service, organization_service):
assert customer is not None
assert customer["id"]

def test_update_customer(self, billing_service, organization_service):
organization = OrganizationFactory.create()

customer = billing_service.create_customer(
name=organization.name,
description=organization.description,
)

assert customer is not None
assert customer["name"] == organization.name

customer = billing_service.update_customer(
customer_id=customer["id"],
name="wutangClan",
description=organization.description,
)

assert customer is not None
assert customer["name"] == "wutangClan"

def test_create_checkout_session(self, billing_service, subscription_service):
subscription_price = StripeSubscriptionPriceFactory.create()
success_url = "http://what.ever"
Expand Down
8 changes: 1 addition & 7 deletions warehouse/api/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import stripe

from pyramid.httpexceptions import HTTPBadRequest, HTTPNoContent, HTTPNotFound
from pyramid.httpexceptions import HTTPBadRequest, HTTPNoContent
from pyramid.view import view_config

from warehouse.subscriptions.interfaces import IBillingService, ISubscriptionService
Expand Down Expand Up @@ -76,8 +76,6 @@ def handle_billing_webhook_event(request, event):
subscription_service.update_subscription_status(
id, StripeSubscriptionStatus.Canceled
)
else:
raise HTTPNotFound("Subscription not found")
# Occurs whenever a subscription changes e.g. status changes.
case "customer.subscription.updated":
subscription = event["data"]["object"]
Expand All @@ -94,8 +92,6 @@ def handle_billing_webhook_event(request, event):
if id := subscription_service.find_subscriptionid(subscription_id):
# Update subscription status.
subscription_service.update_subscription_status(id, status)
else:
raise HTTPNotFound("Subscription not found")
# Occurs whenever a customer is deleted.
case "customer.deleted":
customer = event["data"]["object"]
Expand All @@ -105,8 +101,6 @@ def handle_billing_webhook_event(request, event):
if subscription_service.get_subscriptions_by_customer(customer_id):
# Delete the customer and all associated subscription data
subscription_service.delete_customer(customer_id)
else:
raise HTTPNotFound("Customer subscription data not found")
# Occurs whenever a customer is updated.
case "customer.updated":
customer = event["data"]["object"]
Expand Down
Loading

0 comments on commit 4b954a2

Please sign in to comment.