Skip to content

Commit 385da32

Browse files
renzonrenzon
authored andcommitted
Implemented User Inactivation
close #3803 close #3819
1 parent 97cdaaa commit 385da32

7 files changed

Lines changed: 244 additions & 6 deletions

File tree

pythonpro/domain/checkout_domain.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ def payment_handler_task(payment_id):
7272
user = payment.user
7373
email_marketing_facade.tag_as.delay(user.email, user.id, f'{slug}-boleto')
7474
send_purchase_notification.delay(payment.id)
75+
elif status in {django_pagarme_facade.REFUNDED, django_pagarme_facade.PENDING_REFUND}:
76+
subscription_domain.inactivate_payment_subscription(payment)
7577

7678

7779
def _promote(user, slug: str):

pythonpro/domain/subscription_domain.py

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def subscribe_with_no_role(session_id, name: str, email: str, *tags, id='0', pho
2828
def sync_user_on_discourse(subscription: Subscription):
2929
"""
3030
Synchronize user data on forum if API is configured
31-
:param user_or_id: Django user or his id
31+
:param subscription
3232
:return: returns result of hitting Discourse api
3333
"""
3434
can_make_api_call = bool(settings.DISCOURSE_API_KEY and settings.DISCOURSE_API_USER)
@@ -59,6 +59,74 @@ def sync_user_on_discourse(subscription: Subscription):
5959
requests.post(url, data={'sso': sso_payload, 'sig': signature}, headers=headers)
6060

6161

62+
def remove_from_discourse(subscription: Subscription):
63+
"""
64+
Synchronize user data on forum if API is configured
65+
:param subscription
66+
:return: returns result of hitting Discourse api
67+
"""
68+
can_make_api_call = bool(settings.DISCOURSE_API_KEY and settings.DISCOURSE_API_USER)
69+
can_work_without_sync = not (settings.DISCOURSE_BASE_URL or can_make_api_call)
70+
if can_work_without_sync:
71+
_logger.info('Discourse Integration not available')
72+
return
73+
elif not can_make_api_call:
74+
raise MissingDiscourseAPICredentials('Must define both DISCOURSE_API_KEY and DISCOURSE_API_USER configs')
75+
76+
# https://meta.discourse.org/t/sync-sso-user-data-with-the-sync-sso-route/84398
77+
subscriber = subscription.subscriber
78+
params = {
79+
'email': subscriber.email,
80+
'external_id': subscriber.id,
81+
'require_actisubscription.discourse_groups': 'false',
82+
'remove_groups': ','.join(subscription.discourse_groups)
83+
}
84+
sso_payload, signature = discourse_facade.generate_sso_payload_and_signature(params)
85+
# query_string = parse.urlencode()
86+
url = f'{settings.DISCOURSE_BASE_URL}/admin/users/sync_sso'
87+
headers = {
88+
'content-type': 'multipart/form-data',
89+
'Api-Key': settings.DISCOURSE_API_KEY,
90+
'Api-Username': settings.DISCOURSE_API_USER,
91+
}
92+
93+
requests.post(url, data={'sso': sso_payload, 'sig': signature}, headers=headers)
94+
95+
96+
def remove_user_from_discourse(subscription: Subscription):
97+
"""
98+
Synchronize user data on forum if API is configured
99+
:param user_or_id: Django user or his id
100+
:return: returns result of hitting Discourse api
101+
"""
102+
can_make_api_call = bool(settings.DISCOURSE_API_KEY and settings.DISCOURSE_API_USER)
103+
can_work_without_sync = not (settings.DISCOURSE_BASE_URL or can_make_api_call)
104+
if can_work_without_sync:
105+
_logger.info('Discourse Integration not available')
106+
return
107+
elif not can_make_api_call:
108+
raise MissingDiscourseAPICredentials('Must define both DISCOURSE_API_KEY and DISCOURSE_API_USER configs')
109+
110+
# https://meta.discourse.org/t/sync-sso-user-data-with-the-sync-sso-route/84398
111+
subscriber = subscription.subscriber
112+
params = {
113+
'email': subscriber.email,
114+
'external_id': subscriber.id,
115+
'require_actisubscription.discourse_groups': 'false',
116+
'remove_groups': ','.join(subscription.discourse_groups)
117+
}
118+
sso_payload, signature = discourse_facade.generate_sso_payload_and_signature(params)
119+
# query_string = parse.urlencode()
120+
url = f'{settings.DISCOURSE_BASE_URL}/admin/users/sync_sso'
121+
headers = {
122+
'content-type': 'multipart/form-data',
123+
'Api-Key': settings.DISCOURSE_API_KEY,
124+
'Api-Username': settings.DISCOURSE_API_USER,
125+
}
126+
127+
requests.post(url, data={'sso': sso_payload, 'sig': signature}, headers=headers)
128+
129+
62130
def create_subscription_and_activate_services(payment: PagarmePayment) -> Subscription:
63131
subscription = memberkit_facade.create_new_subscription(payment, 'Criação como resposta de pagamento no Pagarme')
64132
phone = None
@@ -97,3 +165,26 @@ def activate_subscription_on_all_services(subscription: Subscription, responsibl
97165
phone=phone
98166
)
99167
return subscription
168+
169+
170+
def inactivate_subscription_on_all_services(subscription: Subscription, responsible=None,
171+
observation='') -> Subscription:
172+
"""
173+
Inactivate user account on Memberkit, Active Campaign and Discourse
174+
:param subscription:
175+
:return:
176+
"""
177+
remove_user_from_discourse(subscription)
178+
memberkit_facade.inactivate(subscription, responsible, observation)
179+
subscriber = subscription.subscriber
180+
tags = list(subscription.email_marketing_tags)
181+
email_marketing_facade.remove_tags.delay(
182+
subscriber.email,
183+
subscriber.id,
184+
*tags
185+
)
186+
return subscription
187+
188+
189+
def inactivate_payment_subscription(payment: PagarmePayment):
190+
inactivate_subscription_on_all_services(payment.subscription)

pythonpro/domain/tests/test_checkout/test_payment_handler.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,6 @@ def test_pagarme_payment_waiting_payment_boleto(mock_subscription_creation, db,
152152
[
153153
facade.PROCESSING,
154154
facade.AUTHORIZED,
155-
facade.REFUNDED,
156-
facade.PENDING_REFUND,
157155
]
158156
)
159157
def test_pagarme_payment_with_item_but_do_nothing_status(db, tag_as_mock, remove_tags_mock, promote_user_mock, status):
@@ -166,6 +164,37 @@ def test_pagarme_payment_with_item_but_do_nothing_status(db, tag_as_mock, remove
166164
assert promote_user_mock.called is False
167165

168166

167+
@pytest.fixture
168+
def inactivate_subscription_on_all_services(mocker):
169+
return mocker.patch(
170+
'pythonpro.domain.checkout_domain.subscription_domain.inactivate_subscription_on_all_services')
171+
172+
173+
@pytest.mark.parametrize(
174+
'status',
175+
[
176+
facade.REFUNDED,
177+
facade.PENDING_REFUND,
178+
]
179+
)
180+
def test_pagarme_subscription_inactivation(
181+
db, tag_as_mock, remove_tags_mock, promote_user_mock, status, inactivate_subscription_on_all_services):
182+
payment = baker.make(PagarmePayment)
183+
baker.make(PagarmeNotification, status=status, payment=payment)
184+
config = baker.make(PagarmeItemConfig, payments=[payment])
185+
subscription_type = baker.make(SubscriptionType)
186+
subscription = baker.make(Subscription, payment=payment)
187+
subscription.subscription_types.add(subscription_type)
188+
PaymentItemConfigToSubscriptionType.objects.create(payment_item=config, subscription_type=subscription_type)
189+
190+
checkout_domain.payment_handler_task(payment.id)
191+
192+
assert tag_as_mock.called is False
193+
assert remove_tags_mock.called is False
194+
assert promote_user_mock.called is False
195+
inactivate_subscription_on_all_services.assert_called_once_with(subscription)
196+
197+
169198
@pytest.mark.parametrize(
170199
'status',
171200
[

pythonpro/memberkit/admin.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from time import strftime
2+
13
from django.contrib import admin
24
from django.shortcuts import redirect
35
from django.urls import path
@@ -76,7 +78,7 @@ class SubscriptionAdmin(admin.ModelAdmin):
7678
list_filter = ['status', PaymentListFilter, 'subscription_types']
7779
ordering = ['-updated_at']
7880
readonly_fields = ['activated_at', 'memberkit_user_id']
79-
actions = ['activate']
81+
actions = ['activate', 'inactivate']
8082

8183
def get_queryset(self, request):
8284
return Subscription.objects.select_related('payment').select_related('subscriber').select_related('responsible')
@@ -120,5 +122,17 @@ def activate(self, request, queryset):
120122

121123
activate.short_descriptions = 'Ativar'
122124

125+
def inactivate(self, request, queryset):
126+
responsible = request.user
127+
strftime('d%/%m/%Y H%:%M:%S')
128+
for subscription in queryset:
129+
subscription_domain.inactivate_subscription_on_all_services(
130+
subscription,
131+
responsible,
132+
f'Desativada em via admin por Usuário com id {responsible.id} e email {responsible.email}'
133+
)
134+
135+
inactivate.short_descriptions = 'Desativar'
136+
123137
def has_delete_permission(self, request, obj=None):
124138
return False

pythonpro/memberkit/api.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ def list_membership_levels(*, api_key=_ApiKeyNone):
4949

5050

5151
@_configure_api_key
52-
def user_detail(email, *, api_key=_ApiKeyNone):
53-
response = requests.get(f'{_base_url}/api/v1/users/{email}?api_key={api_key}')
52+
def user_detail(email_or_memberkit_user_id, *, api_key=_ApiKeyNone):
53+
response = requests.get(f'{_base_url}/api/v1/users/{email_or_memberkit_user_id}?api_key={api_key}')
5454
return response.json()
5555

5656

@@ -70,3 +70,20 @@ def activate_user(full_name: str, email: str, subscription_type_id: int, expires
7070
}
7171
requests.post(f'{_base_url}/api/v1/users?api_key={api_key}', json=data)
7272
return user_detail(email)
73+
74+
75+
@_configure_api_key
76+
def inactivate_user(memberkit_user_id: int, subscription_type_id: int, *,
77+
api_key=_ApiKeyNone):
78+
user_json = user_detail(memberkit_user_id, api_key=api_key)
79+
data = {
80+
'full_name': user_json['full_name'],
81+
'email': user_json['email'],
82+
'status': 'expired',
83+
'blocked': False,
84+
'membership_level_id': subscription_type_id,
85+
'unlimited': False,
86+
'expires_at': date.today().strftime('%d/%m/%Y'),
87+
}
88+
response = requests.post(f'{_base_url}/api/v1/users?api_key={api_key}', json=data)
89+
return response.json()

pythonpro/memberkit/facade.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,20 @@ def activate(subscription, responsible=None, observation=''):
4848
subscription.responsible = responsible
4949
subscription.save()
5050
return subscription
51+
52+
53+
def inactivate(subscription, responsible=None, observation=''):
54+
for subscription_type in subscription.subscription_types.all().only('id'):
55+
api.inactivate_user(subscription.memberkit_user_id, subscription_type.id)
56+
subscription.status = Subscription.Status.INACTIVE
57+
subscription.activated_at = None
58+
if responsible is not None:
59+
subscription.responsible = responsible
60+
if subscription.observation:
61+
subscription.observation += f'\n\n {observation}'
62+
else:
63+
subscription.observation = observation
64+
subscription.save(update_fields=[
65+
'status', 'activated_at', 'responsible', 'observation'
66+
])
67+
return subscription

pythonpro/memberkit/tests/test_user_management.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,71 @@ def test_activate_on_membership(django_user_model, logged_user, responses, api_k
6565
assert subscription.memberkit_user_id == memberkit_user_id
6666
assert subscription.observation == msg
6767
assert subscription.responsible == logged_user
68+
69+
70+
def test_deactivate_on_membership(django_user_model, logged_user, api_key, responses):
71+
memberkit_user_id = 5047080
72+
user_response = {
73+
'bio': None,
74+
'blocked': False,
75+
'email': '[email protected]',
76+
'enrollments': [],
77+
'full_name': 'ktGsanrofXZhsxXakxkNtHTNiDRdQU',
78+
'id': memberkit_user_id,
79+
'memberships': [
80+
{
81+
'expire_date': '2200-01-01',
82+
'membership_level_id': 4424,
83+
'status': 'active'
84+
}
85+
],
86+
'profile_image_url': None,
87+
'unlimited': True
88+
}
89+
user = baker.make(django_user_model, email='[email protected]')
90+
responses.add(
91+
responses.GET,
92+
f'https://memberkit.com.br/api/v1/users/{memberkit_user_id}?api_key={api_key}',
93+
json=user_response
94+
)
95+
96+
inactive_user_response = {
97+
'bio': None,
98+
'blocked': False,
99+
'email': '[email protected]',
100+
'enrollments': [],
101+
'full_name': 'ktGsanrofXZhsxXakxkNtHTNiDRdQU',
102+
'id': memberkit_user_id,
103+
'memberships': [
104+
{
105+
'expire_date': '2200-01-01',
106+
'membership_level_id': 4424,
107+
'status': 'expired'
108+
}
109+
],
110+
'profile_image_url': None,
111+
'unlimited': True
112+
}
113+
responses.add(
114+
responses.POST,
115+
f'https://memberkit.com.br/api/v1/users?api_key={api_key}',
116+
json=inactive_user_response
117+
)
118+
119+
subscription_type = SubscriptionType.objects.create(id=4424, name='Membros')
120+
subscription = baker.make(
121+
Subscription,
122+
subscriber=user,
123+
status=Subscription.Status.ACTIVE,
124+
activated_at=timezone.now(),
125+
memberkit_user_id=memberkit_user_id
126+
)
127+
subscription.subscription_types.add(subscription_type)
128+
msg = 'Desativado por razão x'
129+
facade.inactivate(subscription, logged_user, msg)
130+
subscription.refresh_from_db()
131+
assert subscription.status == Subscription.Status.INACTIVE
132+
assert subscription.activated_at is None
133+
assert subscription.memberkit_user_id == memberkit_user_id
134+
assert subscription.observation == msg
135+
assert subscription.responsible == logged_user

0 commit comments

Comments
 (0)