Skip to content

Commit

Permalink
Merge pull request breatheco-de#1382 from gustavomm19/subscription-co…
Browse files Browse the repository at this point in the history
…nversion-info

add session_info to plan financings
  • Loading branch information
jefer94 authored Jun 27, 2024
2 parents 9ca3e94 + ad57a86 commit beec74b
Show file tree
Hide file tree
Showing 11 changed files with 694 additions and 58 deletions.
45 changes: 3 additions & 42 deletions breathecode/authenticate/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from breathecode.authenticate.actions import get_app_url, get_user_settings
from breathecode.events.models import Event
from breathecode.registry.models import Asset
from breathecode.utils import serpy
from breathecode.utils import serpy, validate_conversion_info
from breathecode.utils.i18n import translation
from capyc.rest_framework.exceptions import ValidationException

Expand Down Expand Up @@ -649,26 +649,7 @@ def validate(self, data):
code=400)

conversion_info = data.get('conversion_info', None)
if conversion_info is not None:
if not isinstance(conversion_info, dict):
raise ValidationException(translation(lang,
en='conversion_info should be a JSON object',
es='conversion_info debería ser un objeto de JSON',
slug='conversion-info-json-type'),
code=400)

expected_keys = [
'utm_placement', 'utm_medium', 'utm_source', 'utm_term', 'utm_content', 'utm_campaign',
'conversion_url', 'landing_url', 'user_agent', 'plan', 'location', 'translations'
]

for key in conversion_info.keys():
if key not in expected_keys:
raise ValidationException(translation(lang,
en=f'Invalid key {key} provided in the conversion_info',
es=f'Clave inválida {key} agregada en el conversion_info',
slug='conversion-info-invalid-key'),
code=400)
validate_conversion_info(conversion_info, lang)

return data

Expand Down Expand Up @@ -1401,27 +1382,7 @@ def validate(self, data: dict[str, str]):
code=400)

conversion_info = data.get('conversion_info', None)
if conversion_info is not None:
if not isinstance(conversion_info, dict):
raise ValidationException(translation(lang,
en='conversion_info should be a JSON object',
es='conversion_info debería ser un objeto de JSON',
slug='conversion-info-json-type'),
code=400)

expected_keys = [
'utm_placement', 'utm_medium', 'utm_source', 'utm_term', 'utm_content', 'utm_campaign',
'conversion_url', 'landing_url', 'user_agent', 'plan', 'location', 'internal_cta_placement',
'internal_cta_content', 'internal_cta_campaign'
]

for key in conversion_info.keys():
if key not in expected_keys:
raise ValidationException(translation(lang,
en=f'Invalid key {key} provided in the conversion_info',
es=f'Clave inválida {key} agregada en el conversion_info',
slug='conversion-info-invalid-key'),
code=400)
validate_conversion_info(conversion_info, lang)

return data

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 5.0.6 on 2024-06-27 00:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('payments', '0049_paymentmethod_is_credit_card'),
]

operations = [
migrations.AddField(
model_name='planfinancing',
name='conversion_info',
field=models.JSONField(blank=True,
default=None,
help_text='UTMs and other conversion information.',
null=True),
),
migrations.AddField(
model_name='subscription',
name='conversion_info',
field=models.JSONField(blank=True,
default=None,
help_text='UTMs and other conversion information.',
null=True),
),
]
4 changes: 4 additions & 0 deletions breathecode/payments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,10 @@ class AbstractIOweYou(models.Model):

# this reminds the plans to change the stock scheduler on change
plans = models.ManyToManyField(Plan, blank=True, help_text='Plans to be supplied')
conversion_info = models.JSONField(default=None,
blank=True,
null=True,
help_text='UTMs and other conversion information.')

created_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)
Expand Down
30 changes: 25 additions & 5 deletions breathecode/payments/tasks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import ast
from datetime import datetime, timedelta
from typing import Any, Optional

Expand Down Expand Up @@ -613,7 +614,12 @@ def build_service_stock_scheduler_from_plan_financing(self,


@task(bind=True, priority=TaskPriority.WEB_SERVICE_PAYMENT.value)
def build_subscription(self, bag_id: int, invoice_id: int, start_date: Optional[datetime] = None, **_: Any):
def build_subscription(self,
bag_id: int,
invoice_id: int,
start_date: Optional[datetime] = None,
conversion_info: Optional[str] = '',
**_: Any):
logger.info(f'Starting build_subscription for bag {bag_id}')

if not (bag := Bag.objects.filter(id=bag_id, status='PAID', was_delivered=False).first()):
Expand Down Expand Up @@ -646,6 +652,8 @@ def build_subscription(self, bag_id: int, invoice_id: int, start_date: Optional[
mentorship_service_set = None

subscription_start_at = start_date or invoice.paid_at

parsed_conversion_info = ast.literal_eval(conversion_info) if conversion_info != '' else None
subscription = Subscription.objects.create(user=bag.user,
paid_at=invoice.paid_at,
academy=bag.academy,
Expand All @@ -654,7 +662,8 @@ def build_subscription(self, bag_id: int, invoice_id: int, start_date: Optional[
selected_mentorship_service_set=mentorship_service_set,
valid_until=None,
next_payment_at=subscription_start_at + relativedelta(months=months),
status='ACTIVE')
status='ACTIVE',
conversion_info=parsed_conversion_info)

subscription.plans.set(bag.plans.all())
subscription.service_items.set(bag.service_items.all())
Expand All @@ -671,7 +680,12 @@ def build_subscription(self, bag_id: int, invoice_id: int, start_date: Optional[


@task(bind=True, priority=TaskPriority.WEB_SERVICE_PAYMENT.value)
def build_plan_financing(self, bag_id: int, invoice_id: int, is_free: bool = False, **_: Any):
def build_plan_financing(self,
bag_id: int,
invoice_id: int,
is_free: bool = False,
conversion_info: Optional[str] = '',
**_: Any):
logger.info(f'Starting build_plan_financing for bag {bag_id}')

if not (bag := Bag.objects.filter(id=bag_id, status='PAID', was_delivered=False).first()):
Expand Down Expand Up @@ -711,6 +725,9 @@ def build_plan_financing(self, bag_id: int, invoice_id: int, is_free: bool = Fal
event_type_set = None
mentorship_service_set = None

print('conversion_info')
print(conversion_info)
parsed_conversion_info = ast.literal_eval(conversion_info) if conversion_info != '' else None
financing = PlanFinancing.objects.create(user=bag.user,
next_payment_at=invoice.paid_at + relativedelta(months=1),
academy=bag.academy,
Expand All @@ -720,7 +737,8 @@ def build_plan_financing(self, bag_id: int, invoice_id: int, is_free: bool = Fal
valid_until=invoice.paid_at + relativedelta(months=months - 1),
plan_expires_at=invoice.paid_at + delta,
monthly_price=invoice.amount,
status='ACTIVE')
status='ACTIVE',
conversion_info=parsed_conversion_info)

financing.plans.set(plans)

Expand All @@ -736,7 +754,7 @@ def build_plan_financing(self, bag_id: int, invoice_id: int, is_free: bool = Fal


@task(bind=True, priority=TaskPriority.WEB_SERVICE_PAYMENT.value)
def build_free_subscription(self, bag_id: int, invoice_id: int, **_: Any):
def build_free_subscription(self, bag_id: int, invoice_id: int, conversion_info: Optional[str] = '', **_: Any):
logger.info(f'Starting build_free_subscription for bag {bag_id}')

if not (bag := Bag.objects.filter(id=bag_id, status='PAID', was_delivered=False).first()):
Expand Down Expand Up @@ -795,13 +813,15 @@ def build_free_subscription(self, bag_id: int, invoice_id: int, **_: Any):
'valid_until': until,
}

parsed_conversion_info = ast.literal_eval(conversion_info) if conversion_info != '' else None
subscription = Subscription.objects.create(user=bag.user,
paid_at=invoice.paid_at,
academy=bag.academy,
selected_cohort_set=cohort_set,
selected_event_type_set=event_type_set,
selected_mentorship_service_set=mentorship_service_set,
next_payment_at=until,
conversion_info=parsed_conversion_info,
**extra)

subscription.plans.add(plan)
Expand Down
82 changes: 82 additions & 0 deletions breathecode/payments/tests/tasks/tests_build_free_subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ def test_subscription_was_created__is_free_trial(self):
unit_type = plan.trial_duration_unit
db.append(
subscription_item({
'conversion_info': None,
'id': plan.id,
'status': 'FREE_TRIAL',
'paid_at': model.invoice.paid_at,
Expand Down Expand Up @@ -341,6 +342,7 @@ def test_subscription_was_created__bag_with_cohort(self):
unit_type = plan.trial_duration_unit
db.append(
subscription_item({
'conversion_info': None,
'id': plan.id,
'selected_cohort_set_id': 1,
'status': 'FREE_TRIAL',
Expand Down Expand Up @@ -413,6 +415,7 @@ def test_subscription_was_created__bag_with_event_type_set(self):
unit_type = plan.trial_duration_unit
db.append(
subscription_item({
'conversion_info': None,
'id': plan.id,
'selected_event_type_set_id': 1,
'status': 'FREE_TRIAL',
Expand Down Expand Up @@ -485,6 +488,7 @@ def test_subscription_was_created__bag_with_mentorship_service_set(self):
unit_type = plan.trial_duration_unit
db.append(
subscription_item({
'conversion_info': None,
'id': plan.id,
'selected_mentorship_service_set_id': 1,
'status': 'FREE_TRIAL',
Expand Down Expand Up @@ -559,6 +563,7 @@ def test_subscription_was_created__is_free__is_not_renewable(self):
unit_type = plan.time_of_life_unit
db.append(
subscription_item({
'conversion_info': None,
'id': plan.id,
'status': 'ACTIVE',
'paid_at': model.invoice.paid_at,
Expand Down Expand Up @@ -632,6 +637,83 @@ def test_subscription_was_created__is_free__is_renewable(self):
unit_type = plan.time_of_life_unit
db.append(
subscription_item({
'conversion_info': None,
'id': plan.id,
'status': 'ACTIVE',
'paid_at': model.invoice.paid_at,
'next_payment_at': model.invoice.paid_at + calculate_relative_delta(unit, unit_type),
'valid_until': None,
}))

self.assertEqual(self.bc.database.list_of('payments.Subscription'), db)
self.assertEqual(tasks.build_service_stock_scheduler_from_subscription.delay.call_args_list, [
call(1),
call(2),
])
self.bc.check.calls(activity_tasks.add_activity.delay.call_args_list, [
call(1, 'bag_created', related_type='payments.Bag', related_id=1),
])

"""
🔽🔽🔽 With Bag, Invoice and Plan with is_renewable=True
"""

@patch('logging.Logger.info', MagicMock())
@patch('logging.Logger.error', MagicMock())
@patch.object(timezone, 'now', MagicMock(return_value=UTC_NOW))
@patch('breathecode.payments.tasks.build_service_stock_scheduler_from_subscription.delay', MagicMock())
def test_subscription_was_created__is_free__is_renewable_with_conversion_info(self):
bag = {
'status': 'PAID',
'was_delivered': False,
'chosen_period': 'NO_SET',
}
invoice = {'status': 'FULFILLED'}

plans = [{
'is_renewable': True,
'trial_duration': 0,
'trial_duration_unit': random.choice(['DAY', 'WEEK', 'MONTH', 'YEAR']),
'time_of_life': random.randint(1, 100),
'time_of_life_unit': random.choice(['DAY', 'WEEK', 'MONTH', 'YEAR']),
} for _ in range(2)]

model = self.bc.database.create(bag=bag, invoice=invoice, plan=plans)

# remove prints from mixer
logging.Logger.info.call_args_list = []
logging.Logger.error.call_args_list = []

build_free_subscription.delay(1, 1, conversion_info='{"landing_url": "/"}')

self.assertEqual(self.bc.database.list_of('admissions.Cohort'), [])

self.assertEqual(logging.Logger.info.call_args_list, [
call('Starting build_free_subscription for bag 1'),
call('Free subscription was created with id 1 for plan 1'),
call('Free subscription was created with id 2 for plan 2'),
])
self.assertEqual(logging.Logger.error.call_args_list, [])

self.assertEqual(self.bc.database.list_of('payments.Bag'), [
{
**self.bc.format.to_dict(model.bag),
'was_delivered': True,
},
])
self.assertEqual(self.bc.database.list_of('payments.Invoice'), [
self.bc.format.to_dict(model.invoice),
])

db = []
for plan in model.plan:
unit = plan.time_of_life
unit_type = plan.time_of_life_unit
db.append(
subscription_item({
'conversion_info': {
'landing_url': '/'
},
'id': plan.id,
'status': 'ACTIVE',
'paid_at': model.invoice.paid_at,
Expand Down
Loading

0 comments on commit beec74b

Please sign in to comment.