Skip to content

Commit 1193314

Browse files
renzonrenzon
authored andcommitted
Created action to activate user on Memberkit, Discourse and Active Campaign
close #3777
1 parent 62195ea commit 1193314

13 files changed

Lines changed: 391 additions & 20 deletions

pythonpro/domain/checkout_domain.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
from django_pagarme import facade as django_pagarme_facade
44

55
from pythonpro.core import facade as core_facade
6-
from pythonpro.domain import user_domain
6+
from pythonpro.domain import user_domain, subscription_domain
77
from pythonpro.domain.hotzapp_domain import verify_purchase, send_purchase_notification
88
from pythonpro.email_marketing import facade as email_marketing_facade
9-
from pythonpro.memberkit import facade as memberkit_facade
109

1110
__all__ = ['contact_info_listener', 'user_factory', 'payment_handler_task', 'payment_change_handler']
1211

@@ -57,8 +56,8 @@ def payment_handler_task(payment_id):
5756
else:
5857
status = payment.status()
5958
if status == django_pagarme_facade.PAID:
60-
memberkit_facade.create_new_subscription(payment, 'Criação como resposta de pagamento no Pagarme')
6159
user = payment.user
60+
subscription_domain.create_subscription_and_activate_services(payment)
6261
if payment.payment_method == django_pagarme_facade.BOLETO:
6362
email_marketing_facade.remove_tags.delay(user.email, user.id, f'{slug}-boleto', f'{slug}-refused')
6463
else:
Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,96 @@
11
from json.decoder import JSONDecodeError
2+
from logging import Logger
23

4+
import requests as requests
35
from celery import shared_task
6+
from django.conf import settings
7+
from django_pagarme import facade as pagarme_facade
8+
from django_pagarme.models import PagarmePayment
49

5-
from pythonpro.analytics.facade import posthog_alias
10+
from pythonpro.discourse import facade as discourse_facade
11+
from pythonpro.discourse.facade import MissingDiscourseAPICredentials
12+
from pythonpro.email_marketing import facade as email_marketing_facade
613
from pythonpro.email_marketing.facade import create_or_update_with_no_role
14+
from pythonpro.memberkit import facade as memberkit_facade
15+
from pythonpro.memberkit.models import Subscription
16+
from pythonpro.cohorts import facade as cohort_facade
717

18+
_logger = Logger(__file__)
819
run_until_available = shared_task(autoretry_for=(JSONDecodeError,), retry_backoff=True, max_retries=None)
920

1021

1122
@run_until_available
1223
def subscribe_with_no_role(session_id, name: str, email: str, *tags, id='0', phone=None):
1324
result = create_or_update_with_no_role(name, email, *tags, id=id, phone=phone)
14-
# order matter, better register client on email marketing before
15-
# posthog registration
16-
posthog_alias(session_id, email)
1725
return result
26+
27+
28+
def sync_user_on_discourse(subscription: Subscription):
29+
"""
30+
Synchronize user data on forum if API is configured
31+
:param user_or_id: Django user or his id
32+
:return: returns result of hitting Discourse api
33+
"""
34+
can_make_api_call = bool(settings.DISCOURSE_API_KEY and settings.DISCOURSE_API_USER)
35+
can_work_without_sync = not (settings.DISCOURSE_BASE_URL or can_make_api_call)
36+
if can_work_without_sync:
37+
_logger.info('Discourse Integration not available')
38+
return
39+
elif not can_make_api_call:
40+
raise MissingDiscourseAPICredentials('Must define both DISCOURSE_API_KEY and DISCOURSE_API_USER configs')
41+
42+
# https://meta.discourse.org/t/sync-sso-user-data-with-the-sync-sso-route/84398
43+
subscriber = subscription.subscriber
44+
params = {
45+
'email': subscriber.email,
46+
'external_id': subscriber.id,
47+
'require_actisubscription.discourse_groups': 'false',
48+
'groups': ','.join(subscription.discourse_groups)
49+
}
50+
sso_payload, signature = discourse_facade.generate_sso_payload_and_signature(params)
51+
# query_string = parse.urlencode()
52+
url = f'{settings.DISCOURSE_BASE_URL}/admin/users/sync_sso'
53+
headers = {
54+
'content-type': 'multipart/form-data',
55+
'Api-Key': settings.DISCOURSE_API_KEY,
56+
'Api-Username': settings.DISCOURSE_API_USER,
57+
}
58+
59+
requests.post(url, data={'sso': sso_payload, 'sig': signature}, headers=headers)
60+
61+
62+
def create_subscription_and_activate_services(payment: PagarmePayment) -> Subscription:
63+
subscription = memberkit_facade.create_new_subscription(payment, 'Criação como resposta de pagamento no Pagarme')
64+
phone = None
65+
try:
66+
phone = pagarme_facade.get_user_payment_profile(subscription.subscriber)
67+
except pagarme_facade.UserPaymentProfileDoesNotExist:
68+
phone = None
69+
return activate_subscription_on_all_services(
70+
subscription,
71+
observation='Ativados serviços no Memberkit, Discourse e Active Campaign',
72+
phone=phone
73+
)
74+
75+
76+
def activate_subscription_on_all_services(subscription: Subscription, responsible=None, observation='',
77+
phone=None) -> Subscription:
78+
"""
79+
Create or activate user account on Memberkit, Active Campaign and Discourse
80+
:param subscription:
81+
:return:
82+
"""
83+
memberkit_facade.activate(subscription, responsible, observation)
84+
sync_user_on_discourse(subscription)
85+
subscriber = subscription.subscriber
86+
email_marketing_facade.create_or_update_user.delay(
87+
subscriber.get_full_name(),
88+
subscriber.email,
89+
None,
90+
*subscription.email_marketing_tags,
91+
id=subscription.id,
92+
phone=phone
93+
)
94+
if subscription.include_on_cohort:
95+
cohort_facade.subscribe_to_last_cohort(subscriber)
96+
return subscription

pythonpro/domain/tests/test_checkout/test_boleto_generation.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ def create_or_update_lead_mock(mocker):
3535

3636
@pytest.fixture
3737
def payment_handler_task_mock(mocker):
38+
mocker.patch(
39+
'pythonpro.domain.checkout_domain.subscription_domain.create_subscription_and_activate_services'
40+
)
41+
3842
return mocker.patch(
3943
'pythonpro.domain.checkout_domain.payment_handler_task.delay',
4044
side_effect=checkout_domain.payment_handler_task

pythonpro/domain/tests/test_checkout/test_credit_card_payment.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ def create_or_update_lead_mock(mocker):
3939

4040
@pytest.fixture
4141
def payment_handler_task_mock(mocker):
42+
mocker.patch(
43+
'pythonpro.domain.checkout_domain.subscription_domain.create_subscription_and_activate_services'
44+
)
45+
4246
return mocker.patch(
4347
'pythonpro.domain.checkout_domain.payment_handler_task.delay',
4448
side_effect=checkout_domain.payment_handler_task

pythonpro/domain/tests/test_checkout/test_payment_handler.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from django_pagarme.models import PagarmePayment, PagarmeNotification, PagarmeItemConfig
44
from model_bakery import baker
55

6+
from pythonpro.cohorts.models import Cohort
67
from pythonpro.domain import checkout_domain
8+
from pythonpro.memberkit import api
79
from pythonpro.memberkit.models import SubscriptionType, Subscription, PaymentItemConfigToSubscriptionType
810

911

@@ -28,7 +30,7 @@ def send_purchase_notification_mock(mocker):
2830

2931

3032
def test_pagarme_payment_paid_boleto(db, tag_as_mock, remove_tags_mock, promote_user_mock, logged_user,
31-
send_purchase_notification_mock):
33+
send_purchase_notification_mock, mock_subscription_creation):
3234
payment = baker.make(PagarmePayment, payment_method=facade.BOLETO, user=logged_user)
3335
baker.make(PagarmeNotification, status=facade.PAID, payment=payment)
3436
config = baker.make(PagarmeItemConfig, payments=[payment])
@@ -44,25 +46,80 @@ def test_pagarme_payment_paid_boleto(db, tag_as_mock, remove_tags_mock, promote_
4446
send_purchase_notification_mock.asser_called_once_with(payment.id)
4547

4648

49+
@pytest.fixture
50+
def api_key():
51+
key = 'some_key'
52+
api.set_default_api_key(key)
53+
return key
54+
55+
56+
@pytest.fixture(autouse=True)
57+
def enable_memberkit(settings):
58+
settings.MEMBERKIT_ON = True
59+
60+
61+
@pytest.fixture
62+
def mock_email_marketing_facade(mocker):
63+
return mocker.patch(
64+
'pythonpro.domain.subscription_domain.email_marketing_facade.create_or_update_user.delay'
65+
)
66+
67+
4768
def test_subscription_creation(db, tag_as_mock, remove_tags_mock, promote_user_mock, logged_user,
48-
send_purchase_notification_mock):
69+
send_purchase_notification_mock, responses, api_key, mock_email_marketing_facade):
4970
payment = baker.make(PagarmePayment, payment_method=facade.BOLETO, user=logged_user)
71+
cohort = baker.make(Cohort)
5072
baker.make(PagarmeNotification, status=facade.PAID, payment=payment)
5173
config = baker.make(PagarmeItemConfig, payments=[payment])
52-
subscription_type = baker.make(SubscriptionType)
74+
subscription_type = baker.make(SubscriptionType, include_on_cohort=True)
5375
PaymentItemConfigToSubscriptionType.objects.create(payment_item=config, subscription_type=subscription_type)
76+
user_response = {
77+
'bio': None,
78+
'blocked': False,
79+
'email': logged_user.email,
80+
'enrollments': [],
81+
'full_name': logged_user.get_full_name(),
82+
'id': 1,
83+
'memberships': [
84+
{
85+
'expire_date': '2200-01-01',
86+
'membership_level_id': 4424,
87+
'status': 'active'
88+
}
89+
],
90+
'profile_image_url': None,
91+
'unlimited': True
92+
}
93+
responses.add(
94+
responses.POST,
95+
f'https://memberkit.com.br/api/v1/users?api_key={api_key}',
96+
)
97+
responses.add(
98+
responses.GET,
99+
f'https://memberkit.com.br/api/v1/users/{logged_user.email}?api_key={api_key}',
100+
json=user_response
101+
)
54102
checkout_domain.payment_handler_task(payment.id)
55103
subscription = Subscription.objects.first()
56104
assert subscription is not None
57105
assert subscription.subscriber_id == payment.user_id
58-
assert subscription.status == Subscription.Status.INACTIVE
106+
assert subscription.status == Subscription.Status.ACTIVE
59107
assert subscription.payment_id == payment.id
60108
assert list(subscription.subscription_types.all()) == [subscription_type]
61109
assert subscription.responsible is None
62-
assert subscription.observation == 'Criação como resposta de pagamento no Pagarme'
110+
assert 'Criação como resposta de pagamento no Pagarme' in subscription.observation
111+
assert cohort.students.filter(id=logged_user.id).exists()
112+
assert mock_email_marketing_facade.call_count == 1
113+
114+
115+
@pytest.fixture
116+
def mock_subscription_creation(mocker):
117+
return mocker.patch(
118+
'pythonpro.domain.checkout_domain.subscription_domain.create_subscription_and_activate_services')
63119

64120

65-
def test_pagarme_payment_paid_credit_card(db, tag_as_mock, remove_tags_mock, promote_user_mock, logged_user,
121+
def test_pagarme_payment_paid_credit_card(mock_subscription_creation, db, tag_as_mock, remove_tags_mock,
122+
promote_user_mock, logged_user,
66123
send_purchase_notification_mock):
67124
payment = baker.make(PagarmePayment, payment_method=facade.CREDIT_CARD, user=logged_user)
68125
baker.make(PagarmeNotification, status=facade.PAID, payment=payment)
@@ -78,8 +135,8 @@ def test_pagarme_payment_paid_credit_card(db, tag_as_mock, remove_tags_mock, pro
78135
send_purchase_notification_mock.asser_called_once_with(payment.id)
79136

80137

81-
def test_pagarme_payment_waiting_payment_boleto(db, tag_as_mock, remove_tags_mock, promote_user_mock, logged_user,
82-
send_purchase_notification_mock):
138+
def test_pagarme_payment_waiting_payment_boleto(mock_subscription_creation, db, tag_as_mock, remove_tags_mock,
139+
promote_user_mock, logged_user, send_purchase_notification_mock):
83140
payment = baker.make(PagarmePayment, payment_method=facade.BOLETO, user=logged_user)
84141
baker.make(PagarmeNotification, status=facade.WAITING_PAYMENT, payment=payment)
85142
config = baker.make(PagarmeItemConfig, payments=[payment])

pythonpro/memberkit/admin.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django_pagarme.models import PagarmeItemConfig
77

88
# Register your models here.
9+
from pythonpro.domain import subscription_domain
910
from pythonpro.memberkit import facade
1011
from pythonpro.memberkit.models import SubscriptionType, PaymentItemConfigToSubscriptionType, Subscription
1112

@@ -17,10 +18,10 @@ class PaymentItemConfigInline(admin.TabularInline):
1718

1819
@admin.register(SubscriptionType)
1920
class SubscriptionTypeAdmin(admin.ModelAdmin):
20-
change_list_template = "memberkit/subscriptiontype/synchronize_button.html"
21-
fields = ['id', 'name']
21+
change_list_template = 'memberkit/subscriptiontype/synchronize_button.html'
22+
fields = ['id', 'name', 'email_marketing_tags', 'discourse_groups', 'include_on_cohort']
2223
list_display = fields
23-
readonly_fields = fields
24+
readonly_fields = ['id', 'name']
2425
inlines = [PaymentItemConfigInline]
2526

2627
def get_urls(self):
@@ -66,11 +67,15 @@ def queryset(self, request, queryset):
6667
@admin.register(Subscription)
6768
class SubscriptionAdmin(admin.ModelAdmin):
6869
fields = ['payment', 'subscriber', 'subscription_types', 'observation']
69-
list_display = ['subscriber', 'pagarme_url_field', 'responsible', 'status', 'created_at', 'updated_at']
70+
list_display = ['subscriber', 'pagarme_url_field', 'memberkit_user_url_field', 'responsible', 'status',
71+
'created_at', 'updated_at',
72+
'activated_at']
7073
autocomplete_fields = ['subscriber']
7174
search_fields = ['subscriber__email', 'payment__transaction_id']
7275
list_filter = ['status', PaymentListFilter, 'subscription_types']
7376
ordering = ['-updated_at']
77+
readonly_fields = ['activated_at', 'memberkit_user_id']
78+
actions = ['activate']
7479

7580
def get_queryset(self, request):
7681
return Subscription.objects.select_related('payment').select_related('subscriber').select_related('responsible')
@@ -85,7 +90,30 @@ def pagarme_url_field(self, obj):
8590
return format_html(link)
8691

8792
pagarme_url_field.allow_tags = True
88-
pagarme_url_field.short_description = 'Link Pagarme'
93+
pagarme_url_field.short_description = 'Pagarme'
94+
95+
def memberkit_user_url_field(self, obj):
96+
if obj.memberkit_user_id is None:
97+
return '----'
98+
link = (
99+
f'<a href="https://plataforma.dev.pro.br/members/{obj.memberkit_user_id}/edit">'
100+
f'{obj.subscriber.short_name}</a>'
101+
)
102+
return format_html(link)
103+
104+
memberkit_user_url_field.allow_tags = True
105+
memberkit_user_url_field.short_description = 'Memberkit'
106+
107+
def activate(self, request, queryset):
108+
responsible = request.user
109+
for subscription in queryset:
110+
subscription_domain.activate_subscription_on_all_services(
111+
subscription,
112+
responsible,
113+
f'Ativada via admin por Usuário com id {responsible.id} e email {responsible.email}'
114+
)
115+
116+
activate.short_descriptions = 'Ativar'
89117

90118
def has_delete_permission(self, request, obj=None):
91119
return False

pythonpro/memberkit/api.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
"""
2+
Wrapper of Memberkit api.
3+
4+
See doc: https://gist.github.com/rainerborene/26bc6b66bbc5dd4f78a1141df31ef718
5+
"""
6+
from datetime import date
7+
18
import requests
29
from decorator import decorator
310
from django.conf import settings
@@ -39,3 +46,27 @@ def _configure_api_key(func, *args, api_key=_ApiKeyNone, **kwargs):
3946
def list_membership_levels(*, api_key=_ApiKeyNone):
4047
response = requests.get(f'{_base_url}/api/v1/membership_levels?api_key={api_key}')
4148
return response.json()
49+
50+
51+
@_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}')
54+
return response.json()
55+
56+
57+
@_configure_api_key
58+
def activate_user(full_name: str, email: str, subscription_type_id: int, expires_at: date = None, *,
59+
api_key=_ApiKeyNone):
60+
if expires_at is None:
61+
expires_at = date(2200, 1, 1)
62+
data = {
63+
'full_name': full_name,
64+
'email': email,
65+
'status': 'active',
66+
'blocked': False,
67+
'membership_level_id': subscription_type_id,
68+
'unlimited': False,
69+
'expires_at': expires_at.strftime('%d/%m/%Y'),
70+
}
71+
requests.post(f'{_base_url}/api/v1/users?api_key={api_key}', json=data)
72+
return user_detail(email)

0 commit comments

Comments
 (0)