Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions contrib/env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ RECAPTCHA_PRIVATE_KEY=

# Make it False to disable cache. Default is True
CACHE_TURNED_ON=false

# Hotzapp Integration

HOTZAPP_API_URL=https://hotzapp.me
2 changes: 2 additions & 0 deletions pythonpro/core/facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from pythonpro.core.forms import UserSignupForm
from pythonpro.core.models import User, UserInteraction

UserDoesNotExist = User.DoesNotExist


class UserCreationException(Exception):

Expand Down
14 changes: 13 additions & 1 deletion pythonpro/domain/checkout_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pythonpro.core import facade as core_facade
from pythonpro.domain import user_facade
from pythonpro.domain.hotzapp_domain import verify_purchase, send_purchase_notification
from pythonpro.email_marketing import facade as email_marketing_facade

__all__ = ['contact_info_listener', 'user_factory', 'payment_handler_task', 'payment_change_handler']
Expand All @@ -18,10 +19,18 @@ def contact_info_listener(name: str, email: str, phone: str, payment_item_slug:
core_facade.webdev_checkout_form(user)
else:
user_id = None
phone = str(phone)
email_marketing_facade.create_or_update_with_no_role.delay(
name, email, f'{payment_item_slug}-form', id=user_id, phone=str(phone)
name, email, f'{payment_item_slug}-form', id=user_id, phone=phone
)

verify_purchase_after_30_minutes(name, email, phone, payment_item_slug)


def verify_purchase_after_30_minutes(name, email, phone, payment_item_slug):
THIRTY_MINUTES_IN_SECONDS = 1800
verify_purchase.apply_async((name, email, phone, payment_item_slug), countdown=THIRTY_MINUTES_IN_SECONDS)


django_pagarme_facade.add_contact_info_listener(contact_info_listener)

Expand Down Expand Up @@ -52,12 +61,15 @@ def payment_handler_task(payment_id):
else:
email_marketing_facade.remove_tags.delay(user.email, user.id, f'{slug}-refused')
_promote(user, slug)
send_purchase_notification.delay(payment.id)
elif status == django_pagarme_facade.REFUSED:
user = payment.user
email_marketing_facade.tag_as.delay(user.email, user.id, f'{slug}-refused')
send_purchase_notification.delay(payment.id)
elif status == django_pagarme_facade.WAITING_PAYMENT:
user = payment.user
email_marketing_facade.tag_as.delay(user.email, user.id, f'{slug}-boleto')
send_purchase_notification.delay(payment.id)


def _promote(user, slug: str):
Expand Down
108 changes: 108 additions & 0 deletions pythonpro/domain/hotzapp_domain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from datetime import datetime

import requests
from celery import shared_task
from django_pagarme import facade as django_pagarme_facade
from django_pagarme.models import PagarmePayment

from pythonpro import settings
from pythonpro.core import facade


def total_price(purchased_items):
"""
Sum all items prices of purchase to return the total price
:param purchased_items: itens of a givem purch
:return total
"""
total = 0
for item in purchased_items:
total += item.price
return total / 100


@shared_task
def send_abandoned_cart(name, email, phone, payment_item_slug):
"""
Send a potential client who filled their data but not complete the buy to hotzapp
:param name: name filled at the form
:param phone: phone filled at the form
:param email: email filled at the form
:param payment_item_slug: slug of the item of purchase
"""
payment_config_item = django_pagarme_facade.get_payment_item(payment_item_slug)
price = total_price([payment_config_item])
potential_customer = {
'created_at': datetime.now().isoformat(),
'name': name,
'phone': phone,
'email': email,
'currency_code_from': 'R$',
'total_price': price,
'line_items': [
{
'product_name': payment_config_item.name,
'quantity': 1,
'price': price
}
],
}
return requests.post(settings.HOTZAPP_API_URL, potential_customer)


PAYMENT_METHOD_DCT = {
django_pagarme_facade.CREDIT_CARD: 'credit',
django_pagarme_facade.BOLETO: 'billet',
}
STATUS_DCT = {
django_pagarme_facade.REFUSED: 'refused',
django_pagarme_facade.PAID: 'paid',
django_pagarme_facade.WAITING_PAYMENT: 'issued',
}


@shared_task
def send_purchase_notification(payment_id):
"""
Send a potential client who filled their data but not complete the buy-in face of a refused credit card
:params payment_id: id of payment
"""
payment = django_pagarme_facade.find_payment(payment_id)
last_notification = payment.notifications.order_by('-creation').values('status', 'creation').first()
payment_profile = django_pagarme_facade.get_user_payment_profile(payment.user_id)
payment_config_items = payment.items.all()
purchased_items = [{"product_name": item.name, "quantity": '1', "price": str(item.price / 100), } for item in
payment_config_items]

purchase = {
"created_at": last_notification['creation'],
"transaction_id": payment.transaction_id,
"name": payment_profile.name,
"email": payment_profile.email,
"phone": str(payment_profile.phone),
"total_price": total_price(payment_config_items),
"line_items": purchased_items,
"payment_method": PAYMENT_METHOD_DCT[payment.payment_method],
"financial_status": STATUS_DCT[last_notification['status']],
}
return requests.post(settings.HOTZAPP_API_URL, purchase)


@shared_task
def verify_purchase(name, email, phone, payment_item_slug):
"""
Verify each buy interaction to see if it succeeded and depending on each situation take an action
:param name: name filled at the form
:param phone: phone filled at the form
:param email: email filled at the form
:param payment_item_slug: slug of the item of purchase
"""
try:
user = facade.find_user_by_email(email=email)
except facade.UserDoesNotExist:
return send_abandoned_cart(name, phone, email, payment_item_slug)
else:
payment = PagarmePayment.objects.filter(user__id=user.id, items__slug__exact=payment_item_slug).order_by(
'-id').first()
if payment is None or payment.status() != django_pagarme_facade.PAID:
return send_abandoned_cart(name, phone, email, payment_item_slug)
15 changes: 13 additions & 2 deletions pythonpro/domain/tests/test_checkout/test_boleto_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,14 @@ def sync_on_discourse_mock(mocker):
return mocker.patch('pythonpro.domain.user_facade.sync_user_on_discourse.delay')


@pytest.fixture
def send_purchase_notification_mock(mocker):
return mocker.patch('pythonpro.domain.checkout_domain.send_purchase_notification.delay')


@pytest.fixture
def resp(client, pagarme_responses, create_or_update_lead_mock, payment_handler_task_mock, tag_as_mock,
active_product_item, sync_on_discourse_mock):
active_product_item, sync_on_discourse_mock, send_purchase_notification_mock):
return client.get(
reverse('django_pagarme:capture', kwargs={'token': TRANSACTION_ID, 'slug': active_product_item.slug})
)
Expand All @@ -61,6 +66,12 @@ def test_status_code(resp):
assert resp.status_code == 200


def test_send_purchase_notification(resp, send_purchase_notification_mock):
send_purchase_notification_mock.assert_called_once_with(
django_pagarme_facade.find_payment_by_transaction(TRANSACTION_ID).id
)


def test_user_is_created(resp, django_user_model):
User = django_user_model
assert User.objects.exists()
Expand Down Expand Up @@ -93,7 +104,7 @@ def test_created_user_tagged_with_boleto(resp, django_user_model, tag_as_mock, a

@pytest.fixture
def resp_logged_user(client_with_lead, pagarme_responses, payment_handler_task_mock, tag_as_mock, active_product_item,
remove_tags_mock):
remove_tags_mock, send_purchase_notification_mock):
return client_with_lead.get(
reverse('django_pagarme:capture', kwargs={'token': TRANSACTION_ID, 'slug': active_product_item.slug}),
secure=True
Expand Down
17 changes: 16 additions & 1 deletion pythonpro/domain/tests/test_checkout/test_contact_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pythonpro.core import facade as core_facade
from pythonpro.core.models import UserInteraction
from pythonpro.django_assertions import dj_assert_template_used
from pythonpro.domain import hotzapp_domain

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


@pytest.fixture(autouse=True)
def verify_purchase_mock(mocker):
return mocker.patch('pythonpro.domain.checkout_domain.verify_purchase_after_30_minutes',
side_effetc=hotzapp_domain.verify_purchase)


@pytest.fixture
def valid_data():
return {'name': 'Foo Bar Baz', 'email': 'foo@email.com', 'phone': '12999999999'}
Expand All @@ -37,11 +44,19 @@ def make_post(client, contact_info, slug):


@all_slugs
def test_status_code(resp, client, valid_data, slug):
def test_status_code(client, valid_data, slug):
resp = make_post(client, valid_data, slug)
assert resp.status_code == 302


@all_slugs
def test_verify_purchase_called(client, valid_data, slug, verify_purchase_mock):
make_post(client, valid_data, slug)
phone = valid_data['phone']
verify_purchase_mock.assert_called_once_with(
valid_data['name'], valid_data['email'], f'+55{phone}', slug)


membership_slugs = pytest.mark.parametrize(
'slug',
[
Expand Down
17 changes: 14 additions & 3 deletions pythonpro/domain/tests/test_checkout/test_credit_card_payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,18 @@ def create_or_update_pythonist_mock(mocker):
)


# test user not logged
@pytest.fixture
def send_purchase_notification_mock(mocker):
return mocker.patch('pythonpro.domain.checkout_domain.send_purchase_notification.delay')


# tests for user not logged

@pytest.fixture
def resp(client, pagarme_responses, payment_handler_task_mock, create_or_update_lead_mock,
create_or_update_member_mock, create_or_update_webdev_mock, create_or_update_data_scientist_mock,
create_or_update_bootcamper_mock, active_product_item, remove_tags_mock, sync_on_discourse_mock,
create_or_update_pythonist_mock):
create_or_update_pythonist_mock, send_purchase_notification_mock):
return client.get(
reverse('django_pagarme:capture', kwargs={'token': TRANSACTION_ID, 'slug': active_product_item.slug})
)
Expand All @@ -102,6 +107,12 @@ def test_status_code(resp, active_product_item):
assert resp.url == reverse('django_pagarme:thanks', kwargs={'slug': active_product_item.slug})


def test_send_purchase_notification(resp, send_purchase_notification_mock):
send_purchase_notification_mock.assert_called_once_with(
django_pagarme_facade.find_payment_by_transaction(TRANSACTION_ID).id
)


def test_user_is_created(resp, django_user_model):
User = django_user_model
assert User.objects.exists()
Expand Down Expand Up @@ -168,7 +179,7 @@ def resp_logged_user(client_with_user, pagarme_responses, payment_handler_task_m
remove_tags_mock,
sync_on_discourse_mock, create_or_update_member_mock, create_or_update_webdev_mock,
create_or_update_data_scientist_mock, create_or_update_bootcamper_mock,
create_or_update_pythonist_mock):
create_or_update_pythonist_mock, send_purchase_notification_mock):
return client_with_user.get(
reverse('django_pagarme:capture', kwargs={'token': TRANSACTION_ID, 'slug': active_product_item.slug})
)
Expand Down
75 changes: 75 additions & 0 deletions pythonpro/domain/tests/test_checkout/test_hotzapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import pytest
import responses
from django_pagarme import facade
from django_pagarme.models import PagarmePayment, PagarmeNotification, UserPaymentProfile
from model_bakery import baker

from pythonpro.domain.hotzapp_domain import verify_purchase, send_purchase_notification

succes_body = 'ok_5f24555e4dec7f1927bddddb'


@pytest.fixture
def resp_success_mock(settings):
with responses.RequestsMock() as r:
r.add(
r.POST,
settings.HOTZAPP_API_URL,
body=succes_body,
status=200
)
yield r


def test_verify_purchase_not_existing_user(active_product_item, resp_success_mock):
resp = verify_purchase(name='User test', email='asser@test.com', phone=5563432343,
payment_item_slug=active_product_item.slug)
assert resp.status_code == 200


@pytest.fixture
def payment_profile(logged_user):
return baker.make(UserPaymentProfile, user=logged_user, email=logged_user.email, phone='12999999999')


@pytest.fixture(params=[facade.CREDIT_CARD, facade.BOLETO])
def payment(logged_user, active_product_item, payment_profile, request):
return baker.make(
PagarmePayment, payment_method=request.param, user=logged_user, items=[active_product_item]
)


@pytest.fixture(params=[facade.REFUSED, facade.PAID, facade.WAITING_PAYMENT])
def pagarme_notification(payment, request):
return baker.make(PagarmeNotification, status=request.param, payment=payment)


def test_send_purchase_notification(payment, pagarme_notification, resp_success_mock):
resp = send_purchase_notification(payment.id)
assert resp.status_code == 200


@pytest.fixture(params=[facade.REFUSED, facade.WAITING_PAYMENT])
def pagarme_notification_not_paid(payment, request):
return baker.make(PagarmeNotification, status=request.param, payment=payment)


def test_verify_purchase_existing_user(active_product_item, resp_success_mock, payment_profile,
pagarme_notification_not_paid):
resp = verify_purchase(name=payment_profile.name, email=payment_profile.email,
phone=str(payment_profile.phone),
payment_item_slug=active_product_item.slug)
assert resp.status_code == 200


@pytest.fixture
def pagarme_notification_paid(payment):
return baker.make(PagarmeNotification, status=facade.PAID, payment=payment)


def test_dont_verify_purchase_existing_user_paid_before_30_minutes(active_product_item, payment_profile,
pagarme_notification_paid, logged_user):
with responses.RequestsMock():
verify_purchase(name=payment_profile.name, email=payment_profile.email,
phone=str(payment_profile.phone),
payment_item_slug=active_product_item.slug)
Loading