Skip to content

Commit ba77d1a

Browse files
renzonrenzon
authored andcommitted
Created command to syncronize subscriptions
Part of #4764
1 parent 27e4580 commit ba77d1a

File tree

11 files changed

+227
-15
lines changed

11 files changed

+227
-15
lines changed

Pipfile.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pythonpro/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
from .celery import app # noqa
1+
from .celery import app as celery_app # noqa
2+
3+
__all__ = ('celery_app',)

pythonpro/celery.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55

66
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pythonpro.settings')
77
app = Celery('pythonpro.celery')
8-
app.config_from_object('django.conf:settings')
8+
app.config_from_object('django.conf:settings', namespace='CELERY')
99
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

pythonpro/memberkit/api.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,25 @@ def activate_user(full_name: str, email: str, subscription_type_id: int, expires
7272
return user_detail(email)
7373

7474

75+
@_configure_api_key
76+
def update_user_subscription(memberkit_user_id: int, subscription_type_id: int, status: str, expires_at: date = None, *,
77+
api_key=_ApiKeyNone):
78+
user_json = user_detail(memberkit_user_id, api_key=api_key)
79+
if expires_at is None:
80+
expires_at = date(2200, 1, 1)
81+
data = {
82+
'full_name': user_json['full_name'],
83+
'email': user_json['email'],
84+
'status': status,
85+
'blocked': False,
86+
'membership_level_id': subscription_type_id,
87+
'unlimited': False,
88+
'expires_at': expires_at.strftime('%d/%m/%Y'),
89+
}
90+
response = requests.post(f'{_base_url}/api/v1/users?api_key={api_key}', json=data)
91+
return response.json()
92+
93+
7594
@_configure_api_key
7695
def inactivate_user(memberkit_user_id: int, subscription_type_id: int, *,
7796
api_key=_ApiKeyNone):
@@ -89,6 +108,19 @@ def inactivate_user(memberkit_user_id: int, subscription_type_id: int, *,
89108
return response.json()
90109

91110

111+
@_configure_api_key
112+
def delete_user(memberkit_user_id: int, *, api_key=_ApiKeyNone):
113+
response = requests.delete(f'{_base_url}/api/v1/users/{memberkit_user_id}?api_key={api_key}')
114+
response.raise_for_status()
115+
116+
117+
@_configure_api_key
118+
def list_users(page=0, *, api_key=_ApiKeyNone):
119+
response = requests.get(f'{_base_url}/api/v1/users?api_key={api_key}&page={page}')
120+
response.raise_for_status()
121+
return response.json()
122+
123+
92124
@_configure_api_key
93125
def generate_token(memberkit_user_id: int, *, api_key=_ApiKeyNone):
94126
user_json = user_detail(memberkit_user_id, api_key=api_key)

pythonpro/memberkit/facade.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
from builtins import Exception
22
from datetime import timedelta
3+
from itertools import count
4+
from logging import Logger
35
from typing import List
46

7+
from celery import shared_task
58
from django.utils import timezone
69

710
from pythonpro.memberkit import api
8-
from pythonpro.memberkit.models import SubscriptionType, Subscription, YEAR_IN_DAYS
11+
from pythonpro.memberkit.models import SubscriptionType, Subscription, YEAR_IN_DAYS, UserSubscriptionsSummary
12+
13+
_logger = Logger(__file__)
914

1015

1116
def synchronize_subscription_types() -> List[SubscriptionType]:
@@ -57,7 +62,8 @@ def create_new_subscription(payment, observation: str = '') -> Subscription:
5762

5863
def activate(subscription, responsible=None, observation=''):
5964
user = subscription.subscriber
60-
subscription.activated_at = timezone.now()
65+
if subscription.status == Subscription.Status.INACTIVE or subscription.activated_at is None:
66+
subscription.activated_at = timezone.now()
6167
for subscription_type in subscription.subscription_types.all():
6268
expires_at = subscription.activated_at + timedelta(days=subscription_type.days_of_access)
6369
if subscription_type.id in IDS_COMUNIDADE_SUBSCRIPTION:
@@ -103,6 +109,26 @@ def inactivate(subscription, responsible=None, observation=''):
103109
return subscription
104110

105111

112+
def clean_memberkit_users_up():
113+
total = 0
114+
for page in count(1):
115+
memberkit_users = api.list_users(page)
116+
if len(memberkit_users) == 0:
117+
break
118+
for memberkit_user in memberkit_users:
119+
total += 1
120+
memberkit_user_id = int(memberkit_user['id'])
121+
has_active_subscription = Subscription.objects.filter(
122+
memberkit_user_id=memberkit_user_id, status=Subscription.Status.ACTIVE
123+
).exists()
124+
if not has_active_subscription:
125+
api.delete_user(memberkit_user_id)
126+
print(f'Desativado: {memberkit_user_id} ############################################')
127+
else:
128+
print(f'Ativo: {memberkit_user_id}')
129+
return total
130+
131+
106132
class InactiveUserException(Exception):
107133
pass
108134

@@ -133,3 +159,38 @@ def migrate_when_status_active(user):
133159
).exclude(activated_at__isnull=False)
134160
for subscription in status_active_but_not_activated:
135161
activate(subscription, observation='Migrado automaticamente da plataforma antiga para nova')
162+
163+
164+
@shared_task
165+
def process_expired_subscriptions(user_id):
166+
now = timezone.now()
167+
summary = UserSubscriptionsSummary(user_id)
168+
active_subscriptions = list(summary.active_subscriptions())
169+
for subscription in active_subscriptions:
170+
if subscription.expires_at < now:
171+
subscription.status = Subscription.Status.INACTIVE
172+
subscription.save()
173+
inactive_subscriptions = [s for s in active_subscriptions if s.status == Subscription.Status.INACTIVE]
174+
active_subscriptions = [s for s in active_subscriptions if s.status == Subscription.Status.ACTIVE]
175+
if len(active_subscriptions) == 0:
176+
for memberkit_user_id in summary.memberkit_user_ids():
177+
_logger.info(f'Deleted memberkit account for user_id: {user_id}')
178+
api.delete_user(memberkit_user_id)
179+
else:
180+
for inactive_subscription in inactive_subscriptions:
181+
_logger.info(f'Inactivated {inactive_subscription.name} for user_id: {user_id}')
182+
inactivate(inactive_subscription, observation='Inativada por processo de inativação')
183+
for active_subscription in active_subscriptions:
184+
for subscription_type in active_subscription.subscription_types.all().only('id'):
185+
_logger.info(f'Activated {active_subscription.name} for user_id: {user_id}')
186+
api.update_user_subscription(
187+
active_subscription.memberkit_user_id,
188+
subscription_type,
189+
'activate'
190+
)
191+
192+
193+
def inactivate_expired_subscriptions():
194+
for user_id in UserSubscriptionsSummary.users_with_active_subscriptions().values_list('id', flat=True):
195+
_logger.info(f'Adding task to process subscriptions expiration for user_id: {user_id}')
196+
process_expired_subscriptions.delay(user_id)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.core.management.base import BaseCommand
2+
3+
from pythonpro.memberkit import facade
4+
5+
6+
class Command(BaseCommand):
7+
help = 'Busca todas assinaturas ativas e inativa as que estão expiradas'
8+
9+
def add_arguments(self, parser):
10+
pass
11+
12+
def handle(self, *args, **options):
13+
facade.inactivate_expired_subscriptions()

pythonpro/memberkit/models.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,39 @@ def discourse_groups(self):
9696

9797
def __str__(self):
9898
return f'Assinatura: {self.id} de {self.subscriber}'
99+
100+
101+
class UserSubscriptionsSummary:
102+
"""
103+
This class provides summary data about user subscriptions.
104+
This is not a model, but a utility class to handle user subscriptions
105+
"""
106+
107+
def __init__(self, django_user_or_user_id):
108+
if isinstance(django_user_or_user_id, get_user_model()):
109+
self.user = django_user_or_user_id
110+
self.user_id = django_user_or_user_id.id
111+
else:
112+
self.user = None
113+
self.user_id = django_user_or_user_id
114+
115+
def has_active_subscriptions(self):
116+
return self.active_subscriptions().exists()
117+
118+
def active_subscriptions(self):
119+
return Subscription.objects.filter(
120+
subscriber_id=self.user_id, status=Subscription.Status.ACTIVE
121+
)
122+
123+
@classmethod
124+
def users_with_active_subscriptions(cls):
125+
"""
126+
Returns query set with user with at least one active subscription
127+
:return: Django Use Query Set
128+
"""
129+
return get_user_model().objects.filter(subscriptions__status=Subscription.Status.ACTIVE).distinct()
130+
131+
def memberkit_user_ids(self) -> set[int]:
132+
return set(Subscription.objects.filter(
133+
subscriber_id=self.user_id
134+
).values_list('memberkit_user_id', flat=True))

pythonpro/memberkit/tests/test_commands.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,11 @@ def test_subscription_creation_another_subscription_type(role, subscription_type
165165
assert Subscription.objects.count() == 2, 'New Subscription should be created'
166166
management.call_command('create_subscriptions_for_roles')
167167
assert Subscription.objects.count() == 2, 'New Subscription should be created only once'
168+
169+
170+
def test_inactivate_expired_subscriptions(django_user_model, mocker):
171+
process_expired_subscriptions_mock = mocker.patch('pythonpro.memberkit.facade.process_expired_subscriptions.delay')
172+
active_user = baker.make(django_user_model)
173+
baker.make(Subscription, status=Subscription.Status.ACTIVE, subscriber=active_user)
174+
management.call_command('inactivate_expired_subscriptions')
175+
process_expired_subscriptions_mock.assert_called_once_with(active_user.id)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pytest
2+
from django.utils import timezone
3+
from model_bakery import baker
4+
5+
from pythonpro.memberkit import facade
6+
from pythonpro.memberkit.models import Subscription
7+
8+
9+
@pytest.mark.freeze_time('2023-03-22')
10+
def test_process_valid_subscriptions(django_user_model):
11+
active_user = baker.make(django_user_model)
12+
now = timezone.now()
13+
subscription = baker.make(
14+
Subscription,
15+
status=Subscription.Status.ACTIVE,
16+
subscriber=active_user,
17+
activated_at=now,
18+
days_of_access=1
19+
)
20+
facade.process_expired_subscriptions(active_user.id)
21+
subscription.refresh_from_db()
22+
assert subscription.status == Subscription.Status.ACTIVE
23+
24+
# @pytest.mark.freeze_time('2023-03-22')
25+
# def test_process_expired_subscriptions(django_user_model):
26+
# active_user = baker.make(django_user_model)
27+
# now = timezone.now()
28+
# tow_days_ago = now - timedelta(days=2)
29+
# subscription = baker.make(
30+
# Subscription,
31+
# status=Subscription.Status.ACTIVE,
32+
# subscriber=active_user,
33+
# activated_at=tow_days_ago,
34+
# days_of_access=1
35+
# )
36+
# facade.process_expired_subscriptions(active_user.id)
37+
# subscription.refresh_from_db()
38+
# assert subscription.status == Subscription.Status.INACTIVE
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from model_bakery import baker
2+
3+
from pythonpro.memberkit.models import UserSubscriptionsSummary, Subscription
4+
5+
6+
def test_user_summary_with_no_subscriptions(logged_user):
7+
summary = UserSubscriptionsSummary(logged_user)
8+
assert not summary.has_active_subscriptions()
9+
10+
11+
def test_user_with_active_subscriptions(django_user_model):
12+
active_users = baker.make(django_user_model, 5)
13+
for active_user in active_users:
14+
baker.make(Subscription, status=Subscription.Status.ACTIVE, subscriber=active_user)
15+
16+
inactive_users = baker.make(django_user_model, 5)
17+
for inactive_user in inactive_users:
18+
baker.make(Subscription, status=Subscription.Status.INACTIVE, subscriber=inactive_user)
19+
20+
active_user_ids_from_db = set(
21+
UserSubscriptionsSummary.users_with_active_subscriptions().values_list('id', flat=True)
22+
)
23+
assert active_user_ids_from_db == set(active_user.id for active_user in active_users)

0 commit comments

Comments
 (0)