Skip to content

Commit c3727ed

Browse files
diegomaranhaorenzon
authored andcommitted
Implemented hotzapp integration
close #2730
1 parent c55ec95 commit c3727ed

File tree

10 files changed

+265
-11
lines changed

10 files changed

+265
-11
lines changed

contrib/env-sample

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,7 @@ RECAPTCHA_PRIVATE_KEY=
5353

5454
# Make it False to disable cache. Default is True
5555
CACHE_TURNED_ON=false
56+
57+
# Hotzapp Integration
58+
59+
HOTZAPP_API_URL=https://hotzapp.me

pythonpro/core/facade.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from pythonpro.core.forms import UserSignupForm
1313
from pythonpro.core.models import User, UserInteraction
1414

15+
UserDoesNotExist = User.DoesNotExist
16+
1517

1618
class UserCreationException(Exception):
1719

pythonpro/domain/checkout_domain.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from pythonpro.core import facade as core_facade
66
from pythonpro.domain import user_facade
7+
from pythonpro.domain.hotzapp_domain import verify_purchase, send_purchase_notification
78
from pythonpro.email_marketing import facade as email_marketing_facade
89

910
__all__ = ['contact_info_listener', 'user_factory', 'payment_handler_task', 'payment_change_handler']
@@ -18,10 +19,18 @@ def contact_info_listener(name: str, email: str, phone: str, payment_item_slug:
1819
core_facade.webdev_checkout_form(user)
1920
else:
2021
user_id = None
22+
phone = str(phone)
2123
email_marketing_facade.create_or_update_with_no_role.delay(
22-
name, email, f'{payment_item_slug}-form', id=user_id, phone=str(phone)
24+
name, email, f'{payment_item_slug}-form', id=user_id, phone=phone
2325
)
2426

27+
verify_purchase_after_30_minutes(name, email, phone, payment_item_slug)
28+
29+
30+
def verify_purchase_after_30_minutes(name, email, phone, payment_item_slug):
31+
THIRTY_MINUTES_IN_SECONDS = 1800
32+
verify_purchase.apply_async((name, email, phone, payment_item_slug), countdown=THIRTY_MINUTES_IN_SECONDS)
33+
2534

2635
django_pagarme_facade.add_contact_info_listener(contact_info_listener)
2736

@@ -52,12 +61,15 @@ def payment_handler_task(payment_id):
5261
else:
5362
email_marketing_facade.remove_tags.delay(user.email, user.id, f'{slug}-refused')
5463
_promote(user, slug)
64+
send_purchase_notification.delay(payment.id)
5565
elif status == django_pagarme_facade.REFUSED:
5666
user = payment.user
5767
email_marketing_facade.tag_as.delay(user.email, user.id, f'{slug}-refused')
68+
send_purchase_notification.delay(payment.id)
5869
elif status == django_pagarme_facade.WAITING_PAYMENT:
5970
user = payment.user
6071
email_marketing_facade.tag_as.delay(user.email, user.id, f'{slug}-boleto')
72+
send_purchase_notification.delay(payment.id)
6173

6274

6375
def _promote(user, slug: str):

pythonpro/domain/hotzapp_domain.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from datetime import datetime
2+
3+
import requests
4+
from celery import shared_task
5+
from django_pagarme import facade as django_pagarme_facade
6+
from django_pagarme.models import PagarmePayment
7+
8+
from pythonpro import settings
9+
from pythonpro.core import facade
10+
11+
12+
def total_price(purchased_items):
13+
"""
14+
Sum all items prices of purchase to return the total price
15+
:param purchased_items: itens of a givem purch
16+
:return total
17+
"""
18+
total = 0
19+
for item in purchased_items:
20+
total += item.price
21+
return total / 100
22+
23+
24+
@shared_task
25+
def send_abandoned_cart(name, email, phone, payment_item_slug):
26+
"""
27+
Send a potential client who filled their data but not complete the buy to hotzapp
28+
:param name: name filled at the form
29+
:param phone: phone filled at the form
30+
:param email: email filled at the form
31+
:param payment_item_slug: slug of the item of purchase
32+
"""
33+
payment_config_item = django_pagarme_facade.get_payment_item(payment_item_slug)
34+
price = total_price([payment_config_item])
35+
potential_customer = {
36+
'created_at': datetime.now().isoformat(),
37+
'name': name,
38+
'phone': phone,
39+
'email': email,
40+
'currency_code_from': 'R$',
41+
'total_price': price,
42+
'line_items': [
43+
{
44+
'product_name': payment_config_item.name,
45+
'quantity': 1,
46+
'price': price
47+
}
48+
],
49+
}
50+
return requests.post(settings.HOTZAPP_API_URL, potential_customer)
51+
52+
53+
PAYMENT_METHOD_DCT = {
54+
django_pagarme_facade.CREDIT_CARD: 'credit',
55+
django_pagarme_facade.BOLETO: 'billet',
56+
}
57+
STATUS_DCT = {
58+
django_pagarme_facade.REFUSED: 'refused',
59+
django_pagarme_facade.PAID: 'paid',
60+
django_pagarme_facade.WAITING_PAYMENT: 'issued',
61+
}
62+
63+
64+
@shared_task
65+
def send_purchase_notification(payment_id):
66+
"""
67+
Send a potential client who filled their data but not complete the buy-in face of a refused credit card
68+
:params payment_id: id of payment
69+
"""
70+
payment = django_pagarme_facade.find_payment(payment_id)
71+
last_notification = payment.notifications.order_by('-creation').values('status', 'creation').first()
72+
payment_profile = django_pagarme_facade.get_user_payment_profile(payment.user_id)
73+
payment_config_items = payment.items.all()
74+
purchased_items = [{"product_name": item.name, "quantity": '1', "price": str(item.price / 100), } for item in
75+
payment_config_items]
76+
77+
purchase = {
78+
"created_at": last_notification['creation'],
79+
"transaction_id": payment.transaction_id,
80+
"name": payment_profile.name,
81+
"email": payment_profile.email,
82+
"phone": str(payment_profile.phone),
83+
"total_price": total_price(payment_config_items),
84+
"line_items": purchased_items,
85+
"payment_method": PAYMENT_METHOD_DCT[payment.payment_method],
86+
"financial_status": STATUS_DCT[last_notification['status']],
87+
}
88+
return requests.post(settings.HOTZAPP_API_URL, purchase)
89+
90+
91+
@shared_task
92+
def verify_purchase(name, email, phone, payment_item_slug):
93+
"""
94+
Verify each buy interaction to see if it succeeded and depending on each situation take an action
95+
:param name: name filled at the form
96+
:param phone: phone filled at the form
97+
:param email: email filled at the form
98+
:param payment_item_slug: slug of the item of purchase
99+
"""
100+
try:
101+
user = facade.find_user_by_email(email=email)
102+
except facade.UserDoesNotExist:
103+
return send_abandoned_cart(name, phone, email, payment_item_slug)
104+
else:
105+
payment = PagarmePayment.objects.filter(user__id=user.id, items__slug__exact=payment_item_slug).order_by(
106+
'-id').first()
107+
if payment is None or payment.status() != django_pagarme_facade.PAID:
108+
return send_abandoned_cart(name, phone, email, payment_item_slug)

pythonpro/domain/tests/test_checkout/test_boleto_generation.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,14 @@ def sync_on_discourse_mock(mocker):
4949
return mocker.patch('pythonpro.domain.user_facade.sync_user_on_discourse.delay')
5050

5151

52+
@pytest.fixture
53+
def send_purchase_notification_mock(mocker):
54+
return mocker.patch('pythonpro.domain.checkout_domain.send_purchase_notification.delay')
55+
56+
5257
@pytest.fixture
5358
def resp(client, pagarme_responses, create_or_update_lead_mock, payment_handler_task_mock, tag_as_mock,
54-
active_product_item, sync_on_discourse_mock):
59+
active_product_item, sync_on_discourse_mock, send_purchase_notification_mock):
5560
return client.get(
5661
reverse('django_pagarme:capture', kwargs={'token': TRANSACTION_ID, 'slug': active_product_item.slug})
5762
)
@@ -61,6 +66,12 @@ def test_status_code(resp):
6166
assert resp.status_code == 200
6267

6368

69+
def test_send_purchase_notification(resp, send_purchase_notification_mock):
70+
send_purchase_notification_mock.assert_called_once_with(
71+
django_pagarme_facade.find_payment_by_transaction(TRANSACTION_ID).id
72+
)
73+
74+
6475
def test_user_is_created(resp, django_user_model):
6576
User = django_user_model
6677
assert User.objects.exists()
@@ -93,7 +104,7 @@ def test_created_user_tagged_with_boleto(resp, django_user_model, tag_as_mock, a
93104

94105
@pytest.fixture
95106
def resp_logged_user(client_with_lead, pagarme_responses, payment_handler_task_mock, tag_as_mock, active_product_item,
96-
remove_tags_mock):
107+
remove_tags_mock, send_purchase_notification_mock):
97108
return client_with_lead.get(
98109
reverse('django_pagarme:capture', kwargs={'token': TRANSACTION_ID, 'slug': active_product_item.slug}),
99110
secure=True

pythonpro/domain/tests/test_checkout/test_contact_info.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pythonpro.core import facade as core_facade
55
from pythonpro.core.models import UserInteraction
66
from pythonpro.django_assertions import dj_assert_template_used
7+
from pythonpro.domain import hotzapp_domain
78

89
all_slugs = pytest.mark.parametrize(
910
'slug',
@@ -27,6 +28,12 @@ def create_or_update_with_no_role_mock(mocker):
2728
return mocker.patch('pythonpro.domain.checkout_domain.email_marketing_facade.create_or_update_with_no_role.delay')
2829

2930

31+
@pytest.fixture(autouse=True)
32+
def verify_purchase_mock(mocker):
33+
return mocker.patch('pythonpro.domain.checkout_domain.verify_purchase_after_30_minutes',
34+
side_effetc=hotzapp_domain.verify_purchase)
35+
36+
3037
@pytest.fixture
3138
def valid_data():
3239
return {'name': 'Foo Bar Baz', 'email': 'foo@email.com', 'phone': '12999999999'}
@@ -37,11 +44,19 @@ def make_post(client, contact_info, slug):
3744

3845

3946
@all_slugs
40-
def test_status_code(resp, client, valid_data, slug):
47+
def test_status_code(client, valid_data, slug):
4148
resp = make_post(client, valid_data, slug)
4249
assert resp.status_code == 302
4350

4451

52+
@all_slugs
53+
def test_verify_purchase_called(client, valid_data, slug, verify_purchase_mock):
54+
make_post(client, valid_data, slug)
55+
phone = valid_data['phone']
56+
verify_purchase_mock.assert_called_once_with(
57+
valid_data['name'], valid_data['email'], f'+55{phone}', slug)
58+
59+
4560
membership_slugs = pytest.mark.parametrize(
4661
'slug',
4762
[

pythonpro/domain/tests/test_checkout/test_credit_card_payment.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,18 @@ def create_or_update_pythonist_mock(mocker):
8585
)
8686

8787

88-
# test user not logged
88+
@pytest.fixture
89+
def send_purchase_notification_mock(mocker):
90+
return mocker.patch('pythonpro.domain.checkout_domain.send_purchase_notification.delay')
91+
92+
93+
# tests for user not logged
8994

9095
@pytest.fixture
9196
def resp(client, pagarme_responses, payment_handler_task_mock, create_or_update_lead_mock,
9297
create_or_update_member_mock, create_or_update_webdev_mock, create_or_update_data_scientist_mock,
9398
create_or_update_bootcamper_mock, active_product_item, remove_tags_mock, sync_on_discourse_mock,
94-
create_or_update_pythonist_mock):
99+
create_or_update_pythonist_mock, send_purchase_notification_mock):
95100
return client.get(
96101
reverse('django_pagarme:capture', kwargs={'token': TRANSACTION_ID, 'slug': active_product_item.slug})
97102
)
@@ -102,6 +107,12 @@ def test_status_code(resp, active_product_item):
102107
assert resp.url == reverse('django_pagarme:thanks', kwargs={'slug': active_product_item.slug})
103108

104109

110+
def test_send_purchase_notification(resp, send_purchase_notification_mock):
111+
send_purchase_notification_mock.assert_called_once_with(
112+
django_pagarme_facade.find_payment_by_transaction(TRANSACTION_ID).id
113+
)
114+
115+
105116
def test_user_is_created(resp, django_user_model):
106117
User = django_user_model
107118
assert User.objects.exists()
@@ -168,7 +179,7 @@ def resp_logged_user(client_with_user, pagarme_responses, payment_handler_task_m
168179
remove_tags_mock,
169180
sync_on_discourse_mock, create_or_update_member_mock, create_or_update_webdev_mock,
170181
create_or_update_data_scientist_mock, create_or_update_bootcamper_mock,
171-
create_or_update_pythonist_mock):
182+
create_or_update_pythonist_mock, send_purchase_notification_mock):
172183
return client_with_user.get(
173184
reverse('django_pagarme:capture', kwargs={'token': TRANSACTION_ID, 'slug': active_product_item.slug})
174185
)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import pytest
2+
import responses
3+
from django_pagarme import facade
4+
from django_pagarme.models import PagarmePayment, PagarmeNotification, UserPaymentProfile
5+
from model_bakery import baker
6+
7+
from pythonpro.domain.hotzapp_domain import verify_purchase, send_purchase_notification
8+
9+
succes_body = 'ok_5f24555e4dec7f1927bddddb'
10+
11+
12+
@pytest.fixture
13+
def resp_success_mock(settings):
14+
with responses.RequestsMock() as r:
15+
r.add(
16+
r.POST,
17+
settings.HOTZAPP_API_URL,
18+
body=succes_body,
19+
status=200
20+
)
21+
yield r
22+
23+
24+
def test_verify_purchase_not_existing_user(active_product_item, resp_success_mock):
25+
resp = verify_purchase(name='User test', email='asser@test.com', phone=5563432343,
26+
payment_item_slug=active_product_item.slug)
27+
assert resp.status_code == 200
28+
29+
30+
@pytest.fixture
31+
def payment_profile(logged_user):
32+
return baker.make(UserPaymentProfile, user=logged_user, email=logged_user.email, phone='12999999999')
33+
34+
35+
@pytest.fixture(params=[facade.CREDIT_CARD, facade.BOLETO])
36+
def payment(logged_user, active_product_item, payment_profile, request):
37+
return baker.make(
38+
PagarmePayment, payment_method=request.param, user=logged_user, items=[active_product_item]
39+
)
40+
41+
42+
@pytest.fixture(params=[facade.REFUSED, facade.PAID, facade.WAITING_PAYMENT])
43+
def pagarme_notification(payment, request):
44+
return baker.make(PagarmeNotification, status=request.param, payment=payment)
45+
46+
47+
def test_send_purchase_notification(payment, pagarme_notification, resp_success_mock):
48+
resp = send_purchase_notification(payment.id)
49+
assert resp.status_code == 200
50+
51+
52+
@pytest.fixture(params=[facade.REFUSED, facade.WAITING_PAYMENT])
53+
def pagarme_notification_not_paid(payment, request):
54+
return baker.make(PagarmeNotification, status=request.param, payment=payment)
55+
56+
57+
def test_verify_purchase_existing_user(active_product_item, resp_success_mock, payment_profile,
58+
pagarme_notification_not_paid):
59+
resp = verify_purchase(name=payment_profile.name, email=payment_profile.email,
60+
phone=str(payment_profile.phone),
61+
payment_item_slug=active_product_item.slug)
62+
assert resp.status_code == 200
63+
64+
65+
@pytest.fixture
66+
def pagarme_notification_paid(payment):
67+
return baker.make(PagarmeNotification, status=facade.PAID, payment=payment)
68+
69+
70+
def test_dont_verify_purchase_existing_user_paid_before_30_minutes(active_product_item, payment_profile,
71+
pagarme_notification_paid, logged_user):
72+
with responses.RequestsMock():
73+
verify_purchase(name=payment_profile.name, email=payment_profile.email,
74+
phone=str(payment_profile.phone),
75+
payment_item_slug=active_product_item.slug)

0 commit comments

Comments
 (0)