Skip to content

Commit

Permalink
server/subscriptions: implement benefit deletion logic and endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
frankie567 committed Nov 2, 2023
1 parent cb911e2 commit 733c870
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 1 deletion.
19 changes: 19 additions & 0 deletions server/polar/subscription/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,25 @@ async def update_subscription_benefit(
)


@router.delete("/benefits/{id}", status_code=204, tags=[Tags.PUBLIC])
async def delete_subscription_benefit(
id: UUID4,
auth: UserRequiredAuth,
authz: Authz = Depends(Authz.authz),
session: AsyncSession = Depends(get_db_session),
) -> None:
subscription_benefit = await subscription_benefit_service.get_by_id(
session, auth.subject, id
)

if subscription_benefit is None:
raise ResourceNotFound()

await subscription_benefit_service.user_delete(
session, authz, subscription_benefit, auth.user
)


@router.post(
"/subscribe-sessions/",
response_model=SubscribeSession,
Expand Down
32 changes: 31 additions & 1 deletion server/polar/subscription/service/subscription_benefit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from collections.abc import Sequence
from typing import Any

from sqlalchemy import Select, or_, select
from sqlalchemy import Select, delete, or_, select
from sqlalchemy.exc import InvalidRequestError
from sqlalchemy.orm import aliased, contains_eager

Expand All @@ -11,10 +11,12 @@
from polar.kit.db.postgres import AsyncSession
from polar.kit.pagination import PaginationParams, paginate
from polar.kit.services import ResourceService
from polar.kit.utils import utc_now
from polar.models import (
Organization,
Repository,
SubscriptionBenefit,
SubscriptionTierBenefit,
User,
UserOrganization,
)
Expand Down Expand Up @@ -166,6 +168,34 @@ async def user_update(

return updated_subscription_benefit

async def user_delete(
self,
session: AsyncSession,
authz: Authz,
subscription_benefit: SubscriptionBenefit,
user: User,
) -> SubscriptionBenefit:
subscription_benefit = await self._with_organization_or_repository(
session, subscription_benefit
)

if not await authz.can(user, AccessType.write, subscription_benefit):
raise NotPermitted()

subscription_benefit.deleted_at = utc_now()
session.add(subscription_benefit)
statement = delete(SubscriptionTierBenefit).where(
SubscriptionTierBenefit.subscription_benefit_id == subscription_benefit.id
)
await session.execute(statement)
await session.commit()

await subscription_benefit_grant_service.enqueue_benefit_grant_deletions(
session, subscription_benefit
)

return subscription_benefit

async def _with_organization_or_repository(
self, session: AsyncSession, subscription_benefit: SubscriptionBenefit
) -> SubscriptionBenefit:
Expand Down
34 changes: 34 additions & 0 deletions server/polar/subscription/service/subscription_benefit_grant.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,40 @@ async def update_benefit_grant(

return grant

async def enqueue_benefit_grant_deletions(
self, session: AsyncSession, subscription_benefit: SubscriptionBenefit
) -> None:
grants = await self._get_granted_by_benefit(session, subscription_benefit)
for grant in grants:
await enqueue_job(
"subscription.subscription_benefit.delete",
subscription_benefit_grant_id=grant.id,
)

async def delete_benefit_grant(
self,
session: AsyncSession,
grant: SubscriptionBenefitGrant,
) -> SubscriptionBenefitGrant:
# Already revoked, nothing to do
if grant.is_revoked:
return grant

await session.refresh(grant, {"subscription_benefit"})
subscription_benefit = grant.subscription_benefit

benefit_service = get_subscription_benefit_service(
subscription_benefit.type, session
)
await benefit_service.revoke(subscription_benefit)

grant.set_revoked()

session.add(grant)
await session.commit()

return grant

async def _get_by_subscription_and_benefit(
self,
session: AsyncSession,
Expand Down
18 changes: 18 additions & 0 deletions server/polar/subscription/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,21 @@ async def subscription_benefit_update(
await subscription_benefit_grant_service.update_benefit_grant(
session, subscription_benefit_grant
)


@task("subscription.subscription_benefit.delete")
async def subscription_benefit_delete(
ctx: JobContext,
subscription_benefit_grant_id: uuid.UUID,
polar_context: PolarWorkerContext,
) -> None:
async with AsyncSessionMaker(ctx) as session:
subscription_benefit_grant = await subscription_benefit_grant_service.get(
session, subscription_benefit_grant_id
)
if subscription_benefit_grant is None:
raise SubscriptionBenefitGrantDoesNotExist(subscription_benefit_grant_id)

await subscription_benefit_grant_service.delete_benefit_grant(
session, subscription_benefit_grant
)
38 changes: 38 additions & 0 deletions server/tests/subscription/service/test_subscription_benefit.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,41 @@ async def test_valid_description_change(
assert updated_subscription_benefit.description == "Description update"

enqueue_benefit_grant_updates_mock.assert_awaited_once()


@pytest.mark.asyncio
class TestUserDelete:
async def test_not_writable_subscription_benefit(
self,
session: AsyncSession,
authz: Authz,
user: User,
subscription_benefit_organization: SubscriptionBenefit,
) -> None:
with pytest.raises(NotPermitted):
await subscription_benefit_service.user_delete(
session, authz, subscription_benefit_organization, user
)

async def test_valid(
self,
mocker: MockerFixture,
session: AsyncSession,
authz: Authz,
user: User,
subscription_benefit_organization: SubscriptionBenefit,
user_organization_admin: UserOrganization,
) -> None:
enqueue_benefit_grant_updates_mock = mocker.patch.object(
subscription_benefit_grant_service,
"enqueue_benefit_grant_deletions",
spec=SubscriptionBenefitGrantService.enqueue_benefit_grant_updates,
)

updated_subscription_benefit = await subscription_benefit_service.user_delete(
session, authz, subscription_benefit_organization, user
)

assert updated_subscription_benefit.deleted_at is not None

enqueue_benefit_grant_updates_mock.assert_awaited_once()
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,98 @@ async def test_granted_grant(
assert updated_grant.id == grant.id
assert updated_grant.is_granted
subscription_benefit_service_mock.grant.assert_called_once()


@pytest.mark.asyncio
class TestEnqueueBenefitGrantDeletions:
async def test_valid(
self,
mocker: MockerFixture,
session: AsyncSession,
subscription: Subscription,
subscription_benefit_organization: SubscriptionBenefit,
subscription_benefit_repository: SubscriptionBenefit,
) -> None:
granted_grant = SubscriptionBenefitGrant(
subscription=subscription,
subscription_benefit=subscription_benefit_organization,
)
granted_grant.set_granted()
session.add(granted_grant)

revoked_grant = SubscriptionBenefitGrant(
subscription=subscription,
subscription_benefit=subscription_benefit_organization,
)
revoked_grant.set_revoked()
session.add(revoked_grant)

other_benefit_grant = SubscriptionBenefitGrant(
subscription=subscription,
subscription_benefit=subscription_benefit_repository,
)
other_benefit_grant.set_granted()
session.add(other_benefit_grant)

await session.commit()

enqueue_job_mock = mocker.patch(
"polar.subscription.service.subscription_benefit_grant.enqueue_job"
)

await subscription_benefit_grant_service.enqueue_benefit_grant_deletions(
session, subscription_benefit_organization
)

enqueue_job_mock.assert_called_once_with(
"subscription.subscription_benefit.delete",
subscription_benefit_grant_id=granted_grant.id,
)


@pytest.mark.asyncio
class TestDeleteBenefitGrant:
async def test_revoked_grant(
self,
session: AsyncSession,
subscription: Subscription,
subscription_benefit_organization: SubscriptionBenefit,
subscription_benefit_service_mock: MagicMock,
) -> None:
grant = SubscriptionBenefitGrant(
subscription=subscription,
subscription_benefit=subscription_benefit_organization,
)
grant.set_revoked()
session.add(grant)
await session.commit()

updated_grant = await subscription_benefit_grant_service.delete_benefit_grant(
session, grant
)

assert updated_grant.id == grant.id
subscription_benefit_service_mock.revoke.assert_not_called()

async def test_granted_grant(
self,
session: AsyncSession,
subscription: Subscription,
subscription_benefit_organization: SubscriptionBenefit,
subscription_benefit_service_mock: MagicMock,
) -> None:
grant = SubscriptionBenefitGrant(
subscription=subscription,
subscription_benefit=subscription_benefit_organization,
)
grant.set_granted()
session.add(grant)
await session.commit()

updated_grant = await subscription_benefit_grant_service.delete_benefit_grant(
session, grant
)

assert updated_grant.id == grant.id
assert updated_grant.is_revoked
subscription_benefit_service_mock.revoke.assert_called_once()
33 changes: 33 additions & 0 deletions server/tests/subscription/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,39 @@ async def test_valid(
assert "properties" in json


@pytest.mark.asyncio
class TestDeleteSubscriptionBenefit:
async def test_anonymous(
self,
client: AsyncClient,
subscription_benefit_organization: SubscriptionBenefit,
) -> None:
response = await client.delete(
f"/api/v1/subscriptions/benefits/{subscription_benefit_organization.id}"
)

assert response.status_code == 401

@pytest.mark.authenticated
async def test_not_existing(self, client: AsyncClient) -> None:
response = await client.delete(f"/api/v1/subscriptions/benefits/{uuid.uuid4()}")

assert response.status_code == 404

@pytest.mark.authenticated
async def test_valid(
self,
client: AsyncClient,
subscription_benefit_organization: SubscriptionBenefit,
user_organization_admin: UserOrganization,
) -> None:
response = await client.delete(
f"/api/v1/subscriptions/benefits/{subscription_benefit_organization.id}"
)

assert response.status_code == 204


@pytest.mark.asyncio
class TestCreateSubscribeSession:
async def test_not_existing(self, client: AsyncClient) -> None:
Expand Down
42 changes: 42 additions & 0 deletions server/tests/subscription/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
SubscriptionBenefitGrantDoesNotExist,
SubscriptionDoesNotExist,
enqueue_benefits_grants,
subscription_benefit_delete,
subscription_benefit_grant,
subscription_benefit_grant_service,
subscription_benefit_revoke,
Expand Down Expand Up @@ -194,3 +195,44 @@ async def test_existing_grant(
await subscription_benefit_update(job_context, grant.id, polar_worker_context)

update_benefit_grant_mock.assert_called_once()


@pytest.mark.asyncio
class TestSubscriptionBenefitDelete:
async def test_not_existing_grant(
self,
job_context: JobContext,
polar_worker_context: PolarWorkerContext,
subscription_benefit_organization: SubscriptionBenefit,
) -> None:
with pytest.raises(SubscriptionBenefitGrantDoesNotExist):
await subscription_benefit_delete(
job_context, uuid.uuid4(), polar_worker_context
)

async def test_existing_grant(
self,
session: AsyncSession,
mocker: MockerFixture,
job_context: JobContext,
polar_worker_context: PolarWorkerContext,
subscription: Subscription,
subscription_benefit_organization: SubscriptionBenefit,
) -> None:
grant = SubscriptionBenefitGrant(
subscription=subscription,
subscription_benefit=subscription_benefit_organization,
)
grant.set_granted()
session.add(grant)
await session.commit()

delete_benefit_grant_mock = mocker.patch.object(
subscription_benefit_grant_service,
"delete_benefit_grant",
spec=SubscriptionBenefitGrantService.delete_benefit_grant,
)

await subscription_benefit_delete(job_context, grant.id, polar_worker_context)

delete_benefit_grant_mock.assert_called_once()

1 comment on commit 733c870

@vercel
Copy link

@vercel vercel bot commented on 733c870 Nov 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./docs

docs-git-main-polar-sh.vercel.app
docs-sigma-puce.vercel.app
docs-polar-sh.vercel.app
docs.polar.sh

Please sign in to comment.