diff --git a/breathecode/activity/actions.py b/breathecode/activity/actions.py index 4311b3eec..8f5058ef4 100644 --- a/breathecode/activity/actions.py +++ b/breathecode/activity/actions.py @@ -39,6 +39,9 @@ 'mentorship_session_checkin', 'mentorship_session_checkout', ], + 'payments.Invoice': [ + 'checkout_completed', + ], } @@ -124,8 +127,8 @@ def answer(cls, 'user_username': instance.user.username if instance.user else None, 'user_first_name': instance.user.first_name if instance.user else None, 'user_last_name': instance.user.last_name if instance.user else None, - 'opened_at': instance.opened_at.isoformat() if instance.opened_at else None, - 'sent_at': instance.sent_at.isoformat() if instance.sent_at else None, + 'opened_at': instance.opened_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.opened_at else None, + 'sent_at': instance.sent_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.sent_at else None, } @classmethod @@ -166,8 +169,9 @@ def cohort(cls, 'id': instance.id, 'slug': instance.slug, 'name': instance.name, - 'kickoff_date': instance.kickoff_date.isoformat(), - 'ending_date': instance.ending_date.isoformat() if instance.ending_date else None, + 'kickoff_date': instance.kickoff_date.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'ending_date': + instance.ending_date.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.ending_date else None, 'current_day': instance.current_day, 'current_module': instance.current_module, 'stage': instance.stage, @@ -212,7 +216,7 @@ def task(cls, 'task_type': instance.task_type, 'github_url': instance.github_url, 'live_url': instance.live_url, - 'opened_at': instance.opened_at.isoformat() if instance.opened_at else None, + 'opened_at': instance.opened_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.opened_at else None, 'cohort': instance.cohort.slug if instance.cohort else None, } @@ -230,16 +234,26 @@ def event_checkin(cls, raise AbortTask(f'EventCheckin {related_id or related_slug} not found') return { - 'id': instance.id, - 'email': instance.email, - 'attendee_email': instance.attendee.email if instance.attendee else None, - 'attendee_username': instance.attendee.username if instance.attendee else None, - 'attendee_first_name': instance.attendee.first_name if instance.attendee else None, - 'attendee_last_name': instance.attendee.last_name if instance.attendee else None, - 'event_id': instance.event.id, - 'event_slug': instance.event.slug, - 'status': instance.status, - 'attended_at': instance.attended_at.isoformat() if instance.attended_at else None, + 'id': + instance.id, + 'email': + instance.email, + 'attendee_email': + instance.attendee.email if instance.attendee else None, + 'attendee_username': + instance.attendee.username if instance.attendee else None, + 'attendee_first_name': + instance.attendee.first_name if instance.attendee else None, + 'attendee_last_name': + instance.attendee.last_name if instance.attendee else None, + 'event_id': + instance.event.id, + 'event_slug': + instance.event.slug, + 'status': + instance.status, + 'attended_at': + instance.attended_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.attended_at else None, } @classmethod @@ -255,34 +269,93 @@ def mentorship_session(cls, if not instance: raise AbortTask(f'MentorshipSession {related_id or related_slug} not found') + return { + 'id': + instance.id, + 'name': + instance.name, + 'is_online': + instance.is_online, + 'latitude': + instance.latitude, + 'longitude': + instance.longitude, + 'mentor_id': + instance.mentor.id if instance.mentor else None, + 'mentor_slug': + instance.mentor.slug if instance.mentor else None, + 'mentor_name': + instance.mentor.name if instance.mentor else None, + 'service': + instance.service.slug if instance.service else None, + 'mentee_email': + instance.mentee.email if instance.mentee else None, + 'mentee_username': + instance.mentee.username if instance.mentee else None, + 'mentee_first_name': + instance.mentee.first_name if instance.mentee else None, + 'mentee_last_name': + instance.mentee.last_name if instance.mentee else None, + 'online_meeting_url': + instance.online_meeting_url, + 'online_recording_url': + instance.online_recording_url, + 'status': + instance.status, + 'allow_billing': + instance.allow_billing, + 'bill': + instance.bill.id if instance.bill else None, + 'suggested_accounted_duration': + instance.suggested_accounted_duration, + 'accounted_duration': + instance.accounted_duration, + 'starts_at': + instance.starts_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.starts_at else None, + 'ends_at': + instance.ends_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.ends_at else None, + 'started_at': + instance.started_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.started_at else None, + 'ended_at': + instance.ended_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.ended_at else None, + 'mentor_joined_at': + instance.mentor_joined_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') + if instance.mentor_joined_at else None, + 'mentor_left_at': + instance.mentor_left_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.mentor_left_at else None, + 'mentee_left_at': + instance.mentee_left_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.mentee_left_at else None, + } + + @classmethod + def invoice(cls, + kind: str, + related_id: Optional[str | int] = None, + related_slug: Optional[str] = None) -> dict[str, Any]: + from breathecode.payments.models import Invoice + + kwargs = cls._get_query(related_id, related_slug) + instance = Invoice.objects.filter(**kwargs).first() + + if not instance: + raise AbortTask(f'Invoice {related_id or related_slug} not found') + return { 'id': instance.id, - 'name': instance.name, - 'is_online': instance.is_online, - 'latitude': instance.latitude, - 'longitude': instance.longitude, - 'mentor_id': instance.mentor.id if instance.mentor else None, - 'mentor_slug': instance.mentor.slug if instance.mentor else None, - 'mentor_name': instance.mentor.name if instance.mentor else None, - 'service': instance.service.slug if instance.service else None, - 'mentee_email': instance.mentee.email if instance.mentee else None, - 'mentee_username': instance.mentee.username if instance.mentee else None, - 'mentee_first_name': instance.mentee.first_name if instance.mentee else None, - 'mentee_last_name': instance.mentee.last_name if instance.mentee else None, - 'online_meeting_url': instance.online_meeting_url, - 'online_recording_url': instance.online_recording_url, + 'amount': instance.amount, + 'currency': instance.currency.code, + 'paid_at': instance.paid_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'refunded_at': + instance.refunded_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.refunded_at else None, + 'refunded_at': + instance.refunded_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.refunded_at else None, 'status': instance.status, - 'allow_billing': instance.allow_billing, - 'bill': instance.bill.id if instance.bill else None, - 'suggested_accounted_duration': instance.suggested_accounted_duration, - 'accounted_duration': instance.accounted_duration, - 'starts_at': instance.starts_at.isoformat() if instance.starts_at else None, - 'ends_at': instance.ends_at.isoformat() if instance.ends_at else None, - 'started_at': instance.started_at.isoformat() if instance.started_at else None, - 'ended_at': instance.ended_at.isoformat() if instance.ended_at else None, - 'mentor_joined_at': instance.mentor_joined_at.isoformat() if instance.mentor_joined_at else None, - 'mentor_left_at': instance.mentor_left_at.isoformat() if instance.mentor_left_at else None, - 'mentee_left_at': instance.mentee_left_at.isoformat() if instance.mentee_left_at else None, + 'bag': instance.bag.id, + 'academy': instance.academy.slug, + 'user_email': instance.user.email, + 'user_username': instance.user.username, + 'user_first_name': instance.user.first_name, + 'user_last_name': instance.user.last_name, } @classmethod @@ -311,7 +384,8 @@ def bag(cls, 'user_last_name': instance.user.last_name, 'is_recurrent': instance.is_recurrent, 'was_delivered': instance.was_delivered, - 'expires_at': instance.expires_at.isoformat() if instance.expires_at else None, + 'expires_at': + instance.expires_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.expires_at else None, } @classmethod @@ -343,10 +417,11 @@ def subscription(cls, 'selected_cohort': instance.selected_cohort.slug if instance.selected_cohort else None, 'selected_mentorship_service_set': selected_mentorship_service_set, 'selected_event_type_set': selected_event_type_set, - 'paid_at': instance.paid_at.isoformat(), + 'paid_at': instance.paid_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'is_refundable': instance.is_refundable, - 'next_payment_at': instance.next_payment_at.isoformat(), - 'valid_until': instance.valid_until.isoformat() if instance.valid_until else None, + 'next_payment_at': instance.next_payment_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'valid_until': + instance.valid_until.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.valid_until else None, 'pay_every': instance.pay_every, 'pay_every_unit': instance.pay_every_unit, } @@ -370,20 +445,34 @@ def plan_financing(cls, selected_event_type_set = (instance.selected_event_type_set.slug if instance.selected_event_type_set else None) return { - 'id': instance.id, - 'status': instance.status, - 'user_email': instance.user.email, - 'user_username': instance.user.username, - 'user_first_name': instance.user.first_name, - 'user_last_name': instance.user.last_name, - 'academy': instance.academy.slug, - 'selected_cohort': instance.selected_cohort.slug if instance.selected_cohort else None, - 'selected_mentorship_service_set': selected_mentorship_service_set, - 'selected_event_type_set': selected_event_type_set, - 'next_payment_at': instance.next_payment_at.isoformat(), - 'valid_until': instance.valid_until.isoformat(), - 'plan_expires_at': instance.plan_expires_at.isoformat() if instance.plan_expires_at else None, - 'monthly_price': instance.monthly_price, + 'id': + instance.id, + 'status': + instance.status, + 'user_email': + instance.user.email, + 'user_username': + instance.user.username, + 'user_first_name': + instance.user.first_name, + 'user_last_name': + instance.user.last_name, + 'academy': + instance.academy.slug, + 'selected_cohort': + instance.selected_cohort.slug if instance.selected_cohort else None, + 'selected_mentorship_service_set': + selected_mentorship_service_set, + 'selected_event_type_set': + selected_event_type_set, + 'next_payment_at': + instance.next_payment_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'valid_until': + instance.valid_until.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'plan_expires_at': + instance.plan_expires_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if instance.plan_expires_at else None, + 'monthly_price': + instance.monthly_price, } @@ -434,4 +523,7 @@ def get_activity_meta(kind: str, 'mentorship.MentorshipSession']: return FillActivityMeta.mentorship_session(*args) + if related_type == 'payments.Invoice' and kind in ALLOWED_TYPES['payments.Invoice']: + return FillActivityMeta.invoice(*args) + raise AbortTask(f'kind {kind} is not supported by {related_type} yet') diff --git a/breathecode/commons/migrations/0003_taskmanager_attemps.py b/breathecode/commons/migrations/0003_taskmanager_attemps.py new file mode 100644 index 000000000..a5df535de --- /dev/null +++ b/breathecode/commons/migrations/0003_taskmanager_attemps.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.21 on 2023-10-11 07:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('commons', '0002_auto_20230811_0645'), + ] + + operations = [ + migrations.AddField( + model_name='taskmanager', + name='attemps', + field=models.IntegerField(default=1), + ), + ] diff --git a/breathecode/commons/models.py b/breathecode/commons/models.py index f8a5d2484..3f6658447 100644 --- a/breathecode/commons/models.py +++ b/breathecode/commons/models.py @@ -21,6 +21,7 @@ class TaskManager(models.Model): current_page = models.IntegerField(default=0, blank=True, null=True) total_pages = models.IntegerField(default=0, blank=True, null=True) + attemps = models.IntegerField(default=1) task_module = models.CharField(max_length=200) task_name = models.CharField(max_length=200) diff --git a/breathecode/notify/tasks.py b/breathecode/notify/tasks.py index 98a5bf45e..3600a9b27 100644 --- a/breathecode/notify/tasks.py +++ b/breathecode/notify/tasks.py @@ -4,7 +4,7 @@ from django.core.serializers.json import DjangoJSONEncoder from django.utils import timezone -from breathecode.utils.decorators import task +from breathecode.utils.decorators import task, AbortTask from .actions import sync_slack_team_channel, sync_slack_team_users from breathecode.services.slack.client import Slack from breathecode.mentorship.models import MentorshipSession @@ -126,7 +126,6 @@ def parse_payload(payload: dict): payload[key], set): l = [] for item in payload[key]: - print(item) l.append(parse_payload(item)) payload[key] = l @@ -182,5 +181,6 @@ def parse_payload(payload: dict): hook.save() except Exception: - logger.exception(f'Error while trying to save hook call with status code {response.status_code}.') logger.error(payload) + raise AbortTask( + f'Error while trying to save hook call with status code {response.status_code}. {payload}') diff --git a/breathecode/payments/tasks.py b/breathecode/payments/tasks.py index 6ea4480d2..97f6e1fb9 100644 --- a/breathecode/payments/tasks.py +++ b/breathecode/payments/tasks.py @@ -12,7 +12,7 @@ from breathecode.payments.services.stripe import Stripe from dateutil.relativedelta import relativedelta from breathecode.payments.signals import consume_service -from breathecode.utils.decorators import task, AbortTask +from breathecode.utils.decorators import task, AbortTask, RetryTask from breathecode.utils.i18n import translation from django.db.models import Q @@ -22,18 +22,11 @@ logger = logging.getLogger(__name__) -class BaseTaskWithRetry(Task): - autoretry_for = (Exception, ) - # seconds - retry_kwargs = {'max_retries': 5, 'countdown': 60 * 5} - retry_backoff = True - - def get_app_url(): return os.getenv('APP_URL', '') -@task(bind=True, base=BaseTaskWithRetry) +@task(bind=True) def renew_consumables(self, scheduler_id: int, **_: Any): """Renew consumables.""" @@ -50,8 +43,7 @@ def get_resource_lookup(i_owe_you: AbstractIOweYou, service: Service): logger.info(f'Starting renew_consumables for service stock scheduler {scheduler_id}') if not (scheduler := ServiceStockScheduler.objects.filter(id=scheduler_id).first()): - logger.error(f'ServiceStockScheduler with id {scheduler_id} not found') - return + raise RetryTask(f'ServiceStockScheduler with id {scheduler_id} not found') utc_now = timezone.now() @@ -59,30 +51,26 @@ def get_resource_lookup(i_owe_you: AbstractIOweYou, service: Service): if (scheduler.plan_handler and scheduler.plan_handler.subscription and scheduler.plan_handler.subscription.valid_until and scheduler.plan_handler.subscription.valid_until < utc_now): - logger.error(f'The subscription {scheduler.plan_handler.subscription.id} is over') - return + raise AbortTask(f'The subscription {scheduler.plan_handler.subscription.id} is over') # it needs to be paid if (scheduler.plan_handler and scheduler.plan_handler.subscription and scheduler.plan_handler.subscription.next_payment_at < utc_now): - logger.error( + raise AbortTask( f'The subscription {scheduler.plan_handler.subscription.id} needs to be paid to renew the ' 'consumables') - return # is over if (scheduler.plan_handler and scheduler.plan_handler.plan_financing and scheduler.plan_handler.plan_financing.valid_until < utc_now): - logger.error(f'The plan financing {scheduler.plan_handler.plan_financing.id} is over') - return + raise AbortTask(f'The plan financing {scheduler.plan_handler.plan_financing.id} is over') # it needs to be paid if (scheduler.plan_handler and scheduler.plan_handler.plan_financing and scheduler.plan_handler.plan_financing.next_payment_at < utc_now): - logger.error( + raise AbortTask( f'The plan financing {scheduler.plan_handler.plan_financing.id} needs to be paid to renew ' 'the consumables') - return if (scheduler.plan_handler and scheduler.plan_handler.plan_financing and scheduler.plan_handler.handler.plan.time_of_life @@ -98,16 +86,14 @@ def get_resource_lookup(i_owe_you: AbstractIOweYou, service: Service): # is over if (scheduler.subscription_handler and scheduler.subscription_handler.subscription and scheduler.subscription_handler.subscription.valid_until < utc_now): - logger.error(f'The subscription {scheduler.subscription_handler.subscription.id} is over') - return + raise AbortTask(f'The subscription {scheduler.subscription_handler.subscription.id} is over') # it needs to be paid if (scheduler.subscription_handler and scheduler.subscription_handler.subscription and scheduler.subscription_handler.subscription.next_payment_at < utc_now): - logger.error( + raise AbortTask( f'The subscription {scheduler.subscription_handler.subscription.id} needs to be paid to renew ' 'the consumables') - return if (scheduler.valid_until and scheduler.valid_until - timedelta(days=1) < utc_now): logger.info(f'The scheduler {scheduler.id} don\'t needs to be renewed') @@ -175,24 +161,21 @@ def get_resource_lookup(i_owe_you: AbstractIOweYou, service: Service): logger.info(f'The scheduler {scheduler.id} was renewed') -@task(bind=True, base=BaseTaskWithRetry) +@task(bind=True) def renew_subscription_consumables(self, subscription_id: int, **_: Any): """Renew consumables belongs to a subscription.""" logger.info(f'Starting renew_subscription_consumables for id {subscription_id}') if not (subscription := Subscription.objects.filter(id=subscription_id).first()): - logger.error(f'Subscription with id {subscription_id} not found') - return + raise RetryTask(f'Subscription with id {subscription_id} not found') utc_now = timezone.now() if subscription.valid_until and subscription.valid_until < utc_now: - logger.error(f'The subscription {subscription.id} is over') - return + raise AbortTask(f'The subscription {subscription.id} is over') if subscription.next_payment_at < utc_now: - logger.error(f'The subscription {subscription.id} needs to be paid to renew the consumables') - return + raise AbortTask(f'The subscription {subscription.id} needs to be paid to renew the consumables') for scheduler in ServiceStockScheduler.objects.filter(subscription_handler__subscription=subscription): renew_consumables.delay(scheduler.id) @@ -201,24 +184,21 @@ def renew_subscription_consumables(self, subscription_id: int, **_: Any): renew_consumables.delay(scheduler.id) -@task(bind=True, base=BaseTaskWithRetry) +@task(bind=True) def renew_plan_financing_consumables(self, plan_financing_id: int, **_: Any): """Renew consumables belongs to a plan financing.""" logger.info(f'Starting renew_plan_financing_consumables for id {plan_financing_id}') if not (plan_financing := PlanFinancing.objects.filter(id=plan_financing_id).first()): - logger.error(f'PlanFinancing with id {plan_financing_id} not found') - return + raise RetryTask(f'PlanFinancing with id {plan_financing_id} not found') utc_now = timezone.now() if plan_financing.valid_until and plan_financing.valid_until < utc_now: - logger.error(f'The plan financing {plan_financing.id} is over') - return + raise AbortTask(f'The plan financing {plan_financing.id} is over') if plan_financing.next_payment_at < utc_now: - logger.error(f'The PlanFinancing {plan_financing.id} needs to be paid to renew the consumables') - return + raise AbortTask(f'The PlanFinancing {plan_financing.id} needs to be paid to renew the consumables') if plan_financing.plan_expires_at and plan_financing.plan_expires_at < utc_now: logger.info(f'The services related to PlanFinancing {plan_financing.id} is over') @@ -250,7 +230,7 @@ def fallback_charge_subscription(self, subscription_id: int, exception: Exceptio s.refund_payment(invoice) -@task(bind=True, base=BaseTaskWithRetry, transaction=True, fallback=fallback_charge_subscription) +@task(bind=True, transaction=True, fallback=fallback_charge_subscription) def charge_subscription(self, subscription_id: int, **_: Any): """Renews a subscription.""" @@ -358,7 +338,7 @@ def fallback_charge_plan_financing(self, plan_financing_id: int, exception: Exce s.refund_payment(invoice) -@task(bind=True, base=BaseTaskWithRetry, transaction=True, fallback=fallback_charge_plan_financing) +@task(bind=True, transaction=True, fallback=fallback_charge_plan_financing) def charge_plan_financing(self, plan_financing_id: int, **_: Any): """Renew a plan financing.""" @@ -443,7 +423,7 @@ def charge_plan_financing(self, plan_financing_id: int, **_: Any): renew_plan_financing_consumables.delay(plan_financing.id) -@task(bind=True, base=BaseTaskWithRetry) +@task(bind=True) def build_service_stock_scheduler_from_subscription(self, subscription_id: int, user_id: Optional[int] = None, @@ -479,8 +459,7 @@ def build_service_stock_scheduler_from_subscription(self, if not (subscription := Subscription.objects.filter(id=subscription_id, ** additional_args['subscription']).first()): - logger.error(f'Subscription with id {subscription_id} not found') - return + raise RetryTask(f'Subscription with id {subscription_id} not found') utc_now = timezone.now() @@ -519,7 +498,7 @@ def build_service_stock_scheduler_from_subscription(self, renew_subscription_consumables.delay(subscription.id) -@task(bind=True, base=BaseTaskWithRetry) +@task(bind=True) def build_service_stock_scheduler_from_plan_financing(self, plan_financing_id: int, user_id: Optional[int] = None, @@ -552,8 +531,7 @@ def build_service_stock_scheduler_from_plan_financing(self, if not (plan_financing := PlanFinancing.objects.filter(id=plan_financing_id, **additional_args['plan_financing']).first()): - logger.error(f'PlanFinancing with id {plan_financing_id} not found') - return + raise RetryTask(f'PlanFinancing with id {plan_financing_id} not found') for plan in plan_financing.plans.all(): for handler in PlanServiceItem.objects.filter(plan=plan): @@ -579,17 +557,15 @@ def build_service_stock_scheduler_from_plan_financing(self, renew_plan_financing_consumables.delay(plan_financing.id) -@task(bind=True, base=BaseTaskWithRetry) +@task(bind=True) def build_subscription(self, bag_id: int, invoice_id: int, start_date: Optional[datetime] = None, **_: 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()): - logger.error(f'Bag with id {bag_id} not found') - return + raise RetryTask(f'Bag with id {bag_id} not found') if not (invoice := Invoice.objects.filter(id=invoice_id, status='FULFILLED').first()): - logger.error(f'Invoice with id {invoice_id} not found') - return + raise RetryTask(f'Invoice with id {invoice_id} not found') months = 1 @@ -640,21 +616,18 @@ def build_subscription(self, bag_id: int, invoice_id: int, start_date: Optional[ logger.info(f'Subscription was created with id {subscription.id}') -@task(bind=True, base=BaseTaskWithRetry) +@task(bind=True) def build_plan_financing(self, bag_id: int, invoice_id: int, is_free: bool = False, **_: 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()): - logger.error(f'Bag with id {bag_id} not found') - return + raise RetryTask(f'Bag with id {bag_id} not found') if not (invoice := Invoice.objects.filter(id=invoice_id, status='FULFILLED').first()): - logger.error(f'Invoice with id {invoice_id} not found') - return + raise RetryTask(f'Invoice with id {invoice_id} not found') if not is_free and not invoice.amount: - logger.error(f'An invoice without amount is prohibited (id: {invoice_id})') - return + raise AbortTask(f'An invoice without amount is prohibited (id: {invoice_id})') utc_now = timezone.now() months = bag.how_many_installments @@ -708,27 +681,23 @@ def build_plan_financing(self, bag_id: int, invoice_id: int, is_free: bool = Fal logger.info(f'PlanFinancing was created with id {financing.id}') -@task(bind=True, base=BaseTaskWithRetry) +@task(bind=True) def build_free_subscription(self, bag_id: int, invoice_id: int, **_: 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()): - logger.error(f'Bag with id {bag_id} not found') - return + raise RetryTask(f'Bag with id {bag_id} not found') if not (invoice := Invoice.objects.filter(id=invoice_id, status='FULFILLED').first()): - logger.error(f'Invoice with id {invoice_id} not found') - return + raise RetryTask(f'Invoice with id {invoice_id} not found') if invoice.amount != 0: - logger.error(f'The invoice with id {invoice_id} is invalid for a free subscription') - return + raise AbortTask(f'The invoice with id {invoice_id} is invalid for a free subscription') plans = bag.plans.all() if not plans: - logger.error(f'Not have plans to associated to this free subscription in the bag {bag_id}') - return + raise AbortTask(f'Not have plans to associated to this free subscription in the bag {bag_id}') for plan in plans: is_free_trial = True @@ -794,18 +763,16 @@ def build_free_subscription(self, bag_id: int, invoice_id: int, **_: Any): bag.save() -@task(bind=True, base=BaseTaskWithRetry) +@task(bind=True) def end_the_consumption_session(self, consumption_session_id: int, how_many: float = 1.0, **_: Any): logger.info(f'Starting end_the_consumption_session for ConsumptionSession {consumption_session_id}') session = ConsumptionSession.objects.filter(id=consumption_session_id).first() if not session: - logger.error(f'ConsumptionSession with id {consumption_session_id} not found') - return + raise AbortTask(f'ConsumptionSession with id {consumption_session_id} not found') if session.status != 'PENDING': - logger.error(f'ConsumptionSession with id {consumption_session_id} already processed') - return + raise AbortTask(f'ConsumptionSession with id {consumption_session_id} already processed') consumable = session.consumable consume_service.send(instance=consumable, sender=consumable.__class__, how_many=how_many) @@ -817,20 +784,18 @@ def end_the_consumption_session(self, consumption_session_id: int, how_many: flo # TODO: this task is not being used, if you will use this task, you need to take in consideration # you need fix the logic about the consumable valid until, maybe this must be removed -@task(bind=True, base=BaseTaskWithRetry) +@task(bind=True) def build_consumables_from_bag(bag_id: int, **_: Any): logger.info(f'Starting build_consumables_from_bag for bag {bag_id}') if not (bag := Bag.objects.filter(id=bag_id, status='PAID', was_delivered=False).first()): - logger.error(f'Bag with id {bag_id} not found') - return + raise RetryTask(f'Bag with id {bag_id} not found') mentorship_service_set = bag.selected_mentorship_service_sets.first() event_type_set = bag.selected_event_type_sets.first() if [mentorship_service_set, event_type_set].count(None) != 1: - logger.error(f'Bag with id {bag_id} not have a resource associated') - return + raise AbortTask(f'Bag with id {bag_id} not have a resource associated') consumables = [] for service_item in bag.service_items.all(): @@ -842,8 +807,8 @@ def build_consumables_from_bag(bag_id: int, **_: Any): kwargs['event_type_set'] = event_type_set if not kwargs: - logger.error(f'Bag with id {bag_id} have a resource associated opposite to the service item type') - return + raise AbortTask( + f'Bag with id {bag_id} have a resource associated opposite to the service item type') consumables.append( Consumable(service_item=service_item, @@ -859,7 +824,7 @@ def build_consumables_from_bag(bag_id: int, **_: Any): bag.save() -@task(bind=False, base=BaseTaskWithRetry) +@task(bind=False) def refund_mentoring_session(session_id: int, **_: Any): from breathecode.mentorship.models import MentorshipSession @@ -868,8 +833,7 @@ def refund_mentoring_session(session_id: int, **_: Any): if not (mentorship_session := MentorshipSession.objects.filter( id=session_id, mentee__isnull=False, service__isnull=False, status__in=['FAILED', 'IGNORED' ]).first()): - logger.error(f'MentoringSession with id {session_id} not found or is invalid') - return + raise AbortTask(f'MentoringSession with id {session_id} not found or is invalid') mentee = mentorship_session.mentee service = mentorship_session.service @@ -879,12 +843,10 @@ def refund_mentoring_session(session_id: int, **_: Any): consumable__mentorship_service_set__mentorship_services=service).exclude(status='CANCELLED').first() if not consumption_session: - logger.error(f'ConsumptionSession not found for mentorship session {session_id}') - return + raise AbortTask(f'ConsumptionSession not found for mentorship session {session_id}') if consumption_session.status == 'CANCELLED': - logger.error(f'ConsumptionSession already cancelled for mentorship session {session_id}') - return + raise AbortTask(f'ConsumptionSession already cancelled for mentorship session {session_id}') if consumption_session.status == 'DONE': logger.info('Refunding consumption session because it was discounted') @@ -897,7 +859,7 @@ def refund_mentoring_session(session_id: int, **_: Any): consumption_session.save() -@task(bind=False, base=BaseTaskWithRetry) +@task(bind=False) def add_cohort_set_to_subscription(subscription_id: int, cohort_set_id: int, **_: Any): logger.info( f'Starting add_cohort_set_to_subscription for subscription {subscription_id} cohort_set {cohort_set_id}' @@ -907,7 +869,7 @@ def add_cohort_set_to_subscription(subscription_id: int, cohort_set_id: int, **_ status__in=['CANCELLED', 'DEPRECATED']).first() if not subscription: - raise AbortTask(f'Subscription with id {subscription_id} not found') + raise RetryTask(f'Subscription with id {subscription_id} not found') if subscription.valid_until and subscription.valid_until < timezone.now(): raise AbortTask(f'The subscription {subscription.id} is over') @@ -917,7 +879,7 @@ def add_cohort_set_to_subscription(subscription_id: int, cohort_set_id: int, **_ cohort_set = CohortSet.objects.filter(id=cohort_set_id).first() if not cohort_set: - raise AbortTask(f'CohortSet with id {cohort_set_id} not found') + raise RetryTask(f'CohortSet with id {cohort_set_id} not found') subscription.selected_cohort_set = cohort_set subscription.save() diff --git a/breathecode/payments/tests/tasks/tests_add_cohort_set_to_subscription.py b/breathecode/payments/tests/tasks/tests_add_cohort_set_to_subscription.py index 62e074852..fa4b7534c 100644 --- a/breathecode/payments/tests/tasks/tests_add_cohort_set_to_subscription.py +++ b/breathecode/payments/tests/tasks/tests_add_cohort_set_to_subscription.py @@ -59,6 +59,8 @@ def test_subscription_set_not_found(bc: Breathecode, reset_mock_calls): assert Logger.info.call_args_list == [ call('Starting add_cohort_set_to_subscription for subscription 1 cohort_set 1'), + # retry + call('Starting add_cohort_set_to_subscription for subscription 1 cohort_set 1'), ] assert Logger.error.call_args_list == [call('Subscription with id 1 not found', exc_info=True)] @@ -78,6 +80,8 @@ def test_cohort_set_not_found(bc: Breathecode, reset_mock_calls): assert Logger.info.call_args_list == [ call('Starting add_cohort_set_to_subscription for subscription 1 cohort_set 1'), + # retry + call('Starting add_cohort_set_to_subscription for subscription 1 cohort_set 1'), ] assert Logger.error.call_args_list == [call('CohortSet with id 1 not found', exc_info=True)] diff --git a/breathecode/payments/tests/tasks/tests_build_free_subscription.py b/breathecode/payments/tests/tasks/tests_build_free_subscription.py index ae0a516d6..8b4b0a4b6 100644 --- a/breathecode/payments/tests/tasks/tests_build_free_subscription.py +++ b/breathecode/payments/tests/tasks/tests_build_free_subscription.py @@ -56,9 +56,16 @@ def test_bag_not_found(self): 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')]) - self.assertEqual(logging.Logger.error.call_args_list, [call('Bag with id 1 not found')]) + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting build_free_subscription for bag 1'), + # retry + call('Starting build_free_subscription for bag 1'), + ], + ) + self.assertEqual(logging.Logger.error.call_args_list, + [call('Bag with id 1 not found', exc_info=True)]) self.assertEqual(self.bc.database.list_of('payments.Bag'), []) self.assertEqual(self.bc.database.list_of('payments.Invoice'), []) @@ -83,9 +90,17 @@ def test_invoice_not_found(self): 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')]) - self.assertEqual(logging.Logger.error.call_args_list, [call('Invoice with id 1 not found')]) + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting build_free_subscription for bag 1'), + # retry + call('Starting build_free_subscription for bag 1'), + ], + ) + self.assertEqual(logging.Logger.error.call_args_list, [ + call('Invoice with id 1 not found', exc_info=True), + ]) self.assertEqual(self.bc.database.list_of('payments.Bag'), [self.bc.format.to_dict(model.bag)]) self.assertEqual(self.bc.database.list_of('payments.Invoice'), []) @@ -124,7 +139,7 @@ def test_without_plan(self): call('Starting build_free_subscription for bag 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('Not have plans to associated to this free subscription in the bag 1'), + call('Not have plans to associated to this free subscription in the bag 1', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('payments.Bag'), [ @@ -255,7 +270,7 @@ def test_invoice_with_amount(self): ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The invoice with id 1 is invalid for a free subscription'), + call('The invoice with id 1 is invalid for a free subscription', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('payments.Bag'), [ diff --git a/breathecode/payments/tests/tasks/tests_build_plan_financing.py b/breathecode/payments/tests/tasks/tests_build_plan_financing.py index 6c9311cfc..02e39c281 100644 --- a/breathecode/payments/tests/tasks/tests_build_plan_financing.py +++ b/breathecode/payments/tests/tasks/tests_build_plan_financing.py @@ -56,9 +56,15 @@ def test_bag_not_found(self): self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) - self.assertEqual(logging.Logger.info.call_args_list, - [call('Starting build_plan_financing for bag 1')]) - self.assertEqual(logging.Logger.error.call_args_list, [call('Bag with id 1 not found')]) + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting build_plan_financing for bag 1'), + # retrying + call('Starting build_plan_financing for bag 1'), + ]) + self.assertEqual(logging.Logger.error.call_args_list, + [call('Bag with id 1 not found', exc_info=True)]) self.assertEqual(self.bc.database.list_of('payments.Bag'), []) self.assertEqual(self.bc.database.list_of('payments.Invoice'), []) @@ -83,9 +89,15 @@ def test_invoice_not_found(self): self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) - self.assertEqual(logging.Logger.info.call_args_list, - [call('Starting build_plan_financing for bag 1')]) - self.assertEqual(logging.Logger.error.call_args_list, [call('Invoice with id 1 not found')]) + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting build_plan_financing for bag 1'), + # retrying + call('Starting build_plan_financing for bag 1'), + ]) + self.assertEqual(logging.Logger.error.call_args_list, + [call('Invoice with id 1 not found', exc_info=True)]) self.assertEqual(self.bc.database.list_of('payments.Bag'), [self.bc.format.to_dict(model.bag)]) self.assertEqual(self.bc.database.list_of('payments.Invoice'), []) @@ -134,7 +146,7 @@ def test_invoice_with_wrong_amount(self): call('Starting build_plan_financing for bag 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('An invoice without amount is prohibited (id: 1)'), + call('An invoice without amount is prohibited (id: 1)', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('payments.Bag'), [ diff --git a/breathecode/payments/tests/tasks/tests_build_service_stock_scheduler_from_plan_financing.py b/breathecode/payments/tests/tasks/tests_build_service_stock_scheduler_from_plan_financing.py index 232330125..613818e00 100644 --- a/breathecode/payments/tests/tasks/tests_build_service_stock_scheduler_from_plan_financing.py +++ b/breathecode/payments/tests/tasks/tests_build_service_stock_scheduler_from_plan_financing.py @@ -40,10 +40,16 @@ def test_subscription_not_found(self): self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) - self.assertEqual(logging.Logger.info.call_args_list, [ - call('Starting build_service_stock_scheduler_from_plan_financing for subscription 1'), + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting build_service_stock_scheduler_from_plan_financing for subscription 1'), + # retrying + call('Starting build_service_stock_scheduler_from_plan_financing for subscription 1'), + ]) + self.assertEqual(logging.Logger.error.call_args_list, [ + call('PlanFinancing with id 1 not found', exc_info=True), ]) - self.assertEqual(logging.Logger.error.call_args_list, [call('PlanFinancing with id 1 not found')]) self.assertEqual(self.bc.database.list_of('payments.PlanFinancing'), []) diff --git a/breathecode/payments/tests/tasks/tests_build_service_stock_scheduler_from_subscription.py b/breathecode/payments/tests/tasks/tests_build_service_stock_scheduler_from_subscription.py index 46acca993..509965e96 100644 --- a/breathecode/payments/tests/tasks/tests_build_service_stock_scheduler_from_subscription.py +++ b/breathecode/payments/tests/tasks/tests_build_service_stock_scheduler_from_subscription.py @@ -40,10 +40,16 @@ def test_subscription_not_found(self): self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) - self.assertEqual(logging.Logger.info.call_args_list, [ - call('Starting build_service_stock_scheduler_from_subscription for subscription 1'), + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting build_service_stock_scheduler_from_subscription for subscription 1'), + # retrying + call('Starting build_service_stock_scheduler_from_subscription for subscription 1'), + ]) + self.assertEqual(logging.Logger.error.call_args_list, [ + call('Subscription with id 1 not found', exc_info=True), ]) - self.assertEqual(logging.Logger.error.call_args_list, [call('Subscription with id 1 not found')]) self.assertEqual(self.bc.database.list_of('payments.Subscription'), []) diff --git a/breathecode/payments/tests/tasks/tests_build_subscription.py b/breathecode/payments/tests/tasks/tests_build_subscription.py index 97afcef00..188009eec 100644 --- a/breathecode/payments/tests/tasks/tests_build_subscription.py +++ b/breathecode/payments/tests/tasks/tests_build_subscription.py @@ -56,8 +56,16 @@ def test_bag_not_found(self): self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) - self.assertEqual(logging.Logger.info.call_args_list, [call('Starting build_subscription for bag 1')]) - self.assertEqual(logging.Logger.error.call_args_list, [call('Bag with id 1 not found')]) + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting build_subscription for bag 1'), + # retrying + call('Starting build_subscription for bag 1'), + ]) + self.assertEqual(logging.Logger.error.call_args_list, [ + call('Bag with id 1 not found', exc_info=True), + ]) self.assertEqual(self.bc.database.list_of('payments.Bag'), []) self.assertEqual(self.bc.database.list_of('payments.Invoice'), []) @@ -82,8 +90,16 @@ def test_invoice_not_found(self): self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) - self.assertEqual(logging.Logger.info.call_args_list, [call('Starting build_subscription for bag 1')]) - self.assertEqual(logging.Logger.error.call_args_list, [call('Invoice with id 1 not found')]) + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting build_subscription for bag 1'), + # retrying + call('Starting build_subscription for bag 1'), + ]) + self.assertEqual(logging.Logger.error.call_args_list, [ + call('Invoice with id 1 not found', exc_info=True), + ]) self.assertEqual(self.bc.database.list_of('payments.Bag'), [self.bc.format.to_dict(model.bag)]) self.assertEqual(self.bc.database.list_of('payments.Invoice'), []) diff --git a/breathecode/payments/tests/tasks/tests_refund_mentoring_session.py b/breathecode/payments/tests/tasks/tests_refund_mentoring_session.py index cdb5bf01b..3ae193b37 100644 --- a/breathecode/payments/tests/tasks/tests_refund_mentoring_session.py +++ b/breathecode/payments/tests/tasks/tests_refund_mentoring_session.py @@ -31,7 +31,7 @@ def test_0_items(self, enable_signals): call('Starting refund_mentoring_session for mentoring session 1'), ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('MentoringSession with id 1 not found or is invalid'), + call('MentoringSession with id 1 not found or is invalid', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), []) @@ -61,7 +61,7 @@ def test_1_mentoring_session__nothing_provide(self, enable_signals): call('Starting refund_mentoring_session for mentoring session 1'), ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('MentoringSession with id 1 not found or is invalid'), + call('MentoringSession with id 1 not found or is invalid', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ @@ -94,7 +94,7 @@ def test_1_mentoring_session__just_with_mentee(self, enable_signals): call('Starting refund_mentoring_session for mentoring session 1'), ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('MentoringSession with id 1 not found or is invalid'), + call('MentoringSession with id 1 not found or is invalid', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ @@ -128,7 +128,7 @@ def test_1_mentoring_session__just_with_service(self, enable_signals): call('Starting refund_mentoring_session for mentoring session 1'), ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('MentoringSession with id 1 not found or is invalid'), + call('MentoringSession with id 1 not found or is invalid', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ @@ -161,7 +161,7 @@ def test_1_mentoring_session__just_with_right_status(self, enable_signals): call('Starting refund_mentoring_session for mentoring session 1'), ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('MentoringSession with id 1 not found or is invalid'), + call('MentoringSession with id 1 not found or is invalid', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ @@ -200,7 +200,7 @@ def test_1_mentoring_session__all_elements_given(self, enable_signals): call('Starting refund_mentoring_session for mentoring session 1'), ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('ConsumptionSession not found for mentorship session 1'), + call('ConsumptionSession not found for mentorship session 1', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ @@ -361,7 +361,7 @@ def test_consumption_session_is_cancelled(self, enable_signals): call('Starting refund_mentoring_session for mentoring session 1'), ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('ConsumptionSession not found for mentorship session 1'), + call('ConsumptionSession not found for mentorship session 1', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ diff --git a/breathecode/payments/tests/tasks/tests_renew_consumables.py b/breathecode/payments/tests/tasks/tests_renew_consumables.py index 3b58a8f1d..07ea1549f 100644 --- a/breathecode/payments/tests/tasks/tests_renew_consumables.py +++ b/breathecode/payments/tests/tasks/tests_renew_consumables.py @@ -44,11 +44,15 @@ class PaymentsTestSuite(PaymentsTestCase): def test_scheduler_not_found(self): renew_consumables.delay(1) - self.assertEqual(logging.Logger.info.call_args_list, [ - call('Starting renew_consumables for service stock scheduler 1'), - ]) + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting renew_consumables for service stock scheduler 1'), + # retrying + call('Starting renew_consumables for service stock scheduler 1'), + ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('ServiceStockScheduler with id 1 not found'), + call('ServiceStockScheduler with id 1 not found', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) @@ -82,7 +86,7 @@ def test_plan_financing_is_over(self): call('Starting renew_consumables for service stock scheduler 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The plan financing 1 is over'), + call('The plan financing 1 is over', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) @@ -116,7 +120,7 @@ def test_plan_financing_without_be_paid(self): call('Starting renew_consumables for service stock scheduler 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The plan financing 1 needs to be paid to renew the consumables'), + call('The plan financing 1 needs to be paid to renew the consumables', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) @@ -369,7 +373,7 @@ def test_subscription__plan__is_over(self): call('Starting renew_consumables for service stock scheduler 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The subscription 1 is over'), + call('The subscription 1 is over', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) @@ -403,7 +407,7 @@ def test_subscription__plan__without_be_paid(self): call('Starting renew_consumables for service stock scheduler 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The subscription 1 needs to be paid to renew the consumables'), + call('The subscription 1 needs to be paid to renew the consumables', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) @@ -618,7 +622,7 @@ def test_subscription__service_item__is_over(self): call('Starting renew_consumables for service stock scheduler 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The subscription 1 is over'), + call('The subscription 1 is over', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) @@ -650,7 +654,7 @@ def test_subscription__service_item__without_be_paid(self): call('Starting renew_consumables for service stock scheduler 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The subscription 1 needs to be paid to renew the consumables'), + call('The subscription 1 needs to be paid to renew the consumables', exc_info=True), ]) self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) diff --git a/breathecode/payments/tests/tasks/tests_renew_plan_financing_consumables.py b/breathecode/payments/tests/tasks/tests_renew_plan_financing_consumables.py index b8ab4228d..018533cb4 100644 --- a/breathecode/payments/tests/tasks/tests_renew_plan_financing_consumables.py +++ b/breathecode/payments/tests/tasks/tests_renew_plan_financing_consumables.py @@ -41,10 +41,16 @@ def test_subscription_not_found(self): self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) - self.assertEqual(logging.Logger.info.call_args_list, [ - call('Starting renew_plan_financing_consumables for id 1'), + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting renew_plan_financing_consumables for id 1'), + # retrying + call('Starting renew_plan_financing_consumables for id 1'), + ]) + self.assertEqual(logging.Logger.error.call_args_list, [ + call('PlanFinancing with id 1 not found', exc_info=True), ]) - self.assertEqual(logging.Logger.error.call_args_list, [call('PlanFinancing with id 1 not found')]) self.assertEqual(self.bc.database.list_of('payments.PlanFinancing'), []) @@ -77,7 +83,7 @@ def test_subscription_was_not_paid(self): call('Starting renew_plan_financing_consumables for id 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The PlanFinancing 1 needs to be paid to renew the consumables'), + call('The PlanFinancing 1 needs to be paid to renew the consumables', exc_info=True), ]) self.assertEqual(tasks.renew_consumables.delay.call_args_list, []) @@ -153,7 +159,7 @@ def test_subscription_is_over(self): call('Starting renew_plan_financing_consumables for id 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The plan financing 1 is over'), + call('The plan financing 1 is over', exc_info=True), ]) self.assertEqual(tasks.renew_consumables.delay.call_args_list, []) diff --git a/breathecode/payments/tests/tasks/tests_renew_subscription_consumables.py b/breathecode/payments/tests/tasks/tests_renew_subscription_consumables.py index aec7fca57..c6c6377b5 100644 --- a/breathecode/payments/tests/tasks/tests_renew_subscription_consumables.py +++ b/breathecode/payments/tests/tasks/tests_renew_subscription_consumables.py @@ -41,10 +41,16 @@ def test_subscription_not_found(self): self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) - self.assertEqual(logging.Logger.info.call_args_list, [ - call('Starting renew_subscription_consumables for id 1'), - ]) - self.assertEqual(logging.Logger.error.call_args_list, [call('Subscription with id 1 not found')]) + self.assertEqual( + logging.Logger.info.call_args_list, + [ + call('Starting renew_subscription_consumables for id 1'), + # retrying + call('Starting renew_subscription_consumables for id 1'), + ], + ) + self.assertEqual(logging.Logger.error.call_args_list, + [call('Subscription with id 1 not found', exc_info=True)]) self.assertEqual(self.bc.database.list_of('payments.Subscription'), []) @@ -70,7 +76,7 @@ def test_subscription_was_not_paid(self): call('Starting renew_subscription_consumables for id 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The subscription 1 needs to be paid to renew the consumables'), + call('The subscription 1 needs to be paid to renew the consumables', exc_info=True), ]) self.assertEqual(tasks.renew_consumables.delay.call_args_list, []) @@ -137,7 +143,7 @@ def test_subscription_is_over(self): call('Starting renew_subscription_consumables for id 1'), ]) self.assertEqual(logging.Logger.error.call_args_list, [ - call('The subscription 1 is over'), + call('The subscription 1 is over', exc_info=True), ]) self.assertEqual(tasks.renew_consumables.delay.call_args_list, []) diff --git a/breathecode/payments/urls.py b/breathecode/payments/urls.py index fcbae0e89..f967b74d1 100644 --- a/breathecode/payments/urls.py +++ b/breathecode/payments/urls.py @@ -1,10 +1,11 @@ from django.urls import path -from .views import (AcademyPlanCohortView, AcademyPlanView, AcademyServiceView, AcademyAcademyServiceView, - AcademySubscriptionView, BagView, CardView, CheckingView, ConsumableCheckoutView, - EventTypeSetView, MeConsumableView, MeInvoiceView, AcademyInvoiceView, - MeSubscriptionCancelView, MeSubscriptionChargeView, MentorshipServiceSetView, PayView, - PlanOfferView, PlanView, ServiceItemView, ServiceView, MeSubscriptionView) +from .views import (AcademyCohortSetCohortView, AcademyPlanView, AcademyServiceView, + AcademyAcademyServiceView, AcademySubscriptionView, BagView, CardView, CheckingView, + ConsumableCheckoutView, EventTypeSetView, MeConsumableView, MeInvoiceView, + AcademyInvoiceView, MeSubscriptionCancelView, MeSubscriptionChargeView, + MentorshipServiceSetView, PayView, PlanOfferView, PlanView, ServiceItemView, ServiceView, + MeSubscriptionView) app_name = 'payments' urlpatterns = [ @@ -14,8 +15,8 @@ path('academy/plan', AcademyPlanView.as_view(), name='academy_plan'), path('academy/plan/', AcademyPlanView.as_view(), name='academy_plan_id'), path('academy/plan/', AcademyPlanView.as_view(), name='academy_plan_slug'), - path('academy/plan//cohort', AcademyPlanCohortView.as_view()), - path('academy/plan//cohort', AcademyPlanCohortView.as_view()), + path('academy/cohortset//cohort', AcademyCohortSetCohortView.as_view()), + path('academy/cohortset//cohort', AcademyCohortSetCohortView.as_view()), path('service', ServiceView.as_view()), path('service/', ServiceView.as_view()), path('service//items', ServiceItemView.as_view()), diff --git a/breathecode/payments/views.py b/breathecode/payments/views.py index 025ed26b8..32e29f04f 100644 --- a/breathecode/payments/views.py +++ b/breathecode/payments/views.py @@ -31,6 +31,7 @@ from breathecode.utils.generate_lookups_mixin import GenerateLookupsMixin from breathecode.utils.i18n import translation from breathecode.utils.payment_exception import PaymentException +from breathecode.utils.shorteners import C from breathecode.utils.validation_exception import ValidationException from django.db import transaction from breathecode.utils import getLogger @@ -245,15 +246,29 @@ def delete(self, request, plan_id=None, plan_slug=None, academy_id=None): return Response(status=status.HTTP_204_NO_CONTENT) -class AcademyPlanCohortView(APIView, GenerateLookupsMixin): +class AcademyCohortSetCohortView(APIView): extensions = APIViewExtensions(sort='-id', paginate=True) @capable_of('crud_plan') - def put(self, request, plan_id=None, plan_slug=None, academy_id=None): - lookups = self.generate_lookups(request, many_fields=['id', 'slug']) + def put(self, request, cohort_set_id=None, cohort_set_slug=None, academy_id=None): lang = get_user_language(request) - if not (plan := Plan.objects.filter(Q(id=plan_id) | Q(slug=plan_slug), + handler = self.extensions(request) + query = handler.lookup.build(lang, + ints={ + 'in': [ + 'id', + ], + }, + strings={ + 'in': [ + 'slug', + ], + }, + fix={'lower': 'slug'}) + + if not (cohort_set + := CohortSet.objects.filter(Q(id=cohort_set_id) | Q(slug=cohort_set_slug), owner__id=academy_id).exclude(status='DELETED').first()): raise ValidationException(translation(lang, en='Plan not found', @@ -261,19 +276,24 @@ def put(self, request, plan_id=None, plan_slug=None, academy_id=None): slug='not-found'), code=404) - if not (items := Cohort.objects.filter(**lookups)): + if not (items := Cohort.objects.filter(query)): raise ValidationException(translation(lang, en='Cohort not found', es='Cohort no encontrada', slug='cohort-not-found'), code=404) - created = False + raise ValidationException(C(f'This invite don\'t have email, contact to admin', + slug=f'without-email')) + data = [] for item in items: - if item not in plan.available_cohorts.all(): - created = True + if item in cohort_set.cohorts.all(): + data.append({'cohort': item.id, 'cohort_set': cohort_set.id}) plan.available_cohorts.add(item) + else: + ... + return Response({'status': 'ok'}, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK) diff --git a/breathecode/provisioning/tasks.py b/breathecode/provisioning/tasks.py index 8fa7f0c3b..c193a1cac 100644 --- a/breathecode/provisioning/tasks.py +++ b/breathecode/provisioning/tasks.py @@ -9,7 +9,7 @@ from celery import Task import pandas as pd from breathecode.payments.services.stripe import Stripe -from breathecode.utils.decorators import task, AbortTask +from breathecode.utils.decorators import task, AbortTask, RetryTask from breathecode.provisioning import actions from breathecode.provisioning.models import ProvisioningBill, ProvisioningConsumptionEvent, ProvisioningUserConsumption @@ -58,7 +58,7 @@ def calculate_bill_amounts(hash: str, *, force: bool = False, **_: Any): bills = bills.exclude(status__in=['DISPUTED', 'IGNORED', 'PAID']) if not bills.exists(): - raise AbortTask(f'Does not exists bills for hash {hash}') + raise RetryTask(f'Does not exists bills for hash {hash}') if bills[0].vendor.name == 'Gitpod': fields = ['id', 'credits', 'startTime', 'endTime', 'kind', 'userName', 'contextURL'] @@ -173,7 +173,7 @@ def upload(hash: str, *, page: int = 0, force: bool = False, task_manager_id: in storage = Storage() cloud_file = storage.file(os.getenv('PROVISIONING_BUCKET', None), hash) if not cloud_file.exists(): - raise AbortTask(f'File {hash} not found') + raise RetryTask(f'File {hash} not found') bills = ProvisioningBill.objects.filter(hash=hash).exclude(status='PENDING') if bills.exists() and not force: diff --git a/breathecode/provisioning/tests/tasks/tests_calculate_bill_amounts.py b/breathecode/provisioning/tests/tasks/tests_calculate_bill_amounts.py index 3ed418f13..653a30df9 100644 --- a/breathecode/provisioning/tests/tasks/tests_calculate_bill_amounts.py +++ b/breathecode/provisioning/tests/tasks/tests_calculate_bill_amounts.py @@ -123,8 +123,13 @@ def test_no_bills(self): self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), []) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), []) - self.bc.check.calls(logging.Logger.info.call_args_list, - [call(f'Starting calculate_bill_amounts for hash {slug}')]) + self.bc.check.calls( + logging.Logger.info.call_args_list, + [ + call(f'Starting calculate_bill_amounts for hash {slug}'), + # retried + call(f'Starting calculate_bill_amounts for hash {slug}'), + ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ call(f'Does not exists bills for hash {slug}', exc_info=True), ]) @@ -155,8 +160,13 @@ def test_bill_but_hash_does_not_match(self): self.bc.format.to_dict(model.provisioning_bill), ]) - self.bc.check.calls(logging.Logger.info.call_args_list, - [call(f'Starting calculate_bill_amounts for hash {bad_slug}')]) + self.bc.check.calls( + logging.Logger.info.call_args_list, + [ + call(f'Starting calculate_bill_amounts for hash {bad_slug}'), + # retried + call(f'Starting calculate_bill_amounts for hash {bad_slug}'), + ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ call(f'Does not exists bills for hash {bad_slug}', exc_info=True), ]) @@ -538,9 +548,13 @@ def test_academy_reacted_to_bill(self): }, ]) - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call(f'Starting calculate_bill_amounts for hash {slug}'), - ]) + self.bc.check.calls( + logging.Logger.info.call_args_list, + [ + call(f'Starting calculate_bill_amounts for hash {slug}'), + # retried + call(f'Starting calculate_bill_amounts for hash {slug}'), + ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ call(f'Does not exists bills for hash {slug}', exc_info=True), ]) @@ -707,9 +721,13 @@ def test_academy_reacted_to_bill__paid__force(self): }, ]) - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call(f'Starting calculate_bill_amounts for hash {slug}'), - ]) + self.bc.check.calls( + logging.Logger.info.call_args_list, + [ + call(f'Starting calculate_bill_amounts for hash {slug}'), + # retried + call(f'Starting calculate_bill_amounts for hash {slug}'), + ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ call(f'Does not exists bills for hash {slug}', exc_info=True), ]) diff --git a/breathecode/provisioning/tests/tasks/tests_upload.py b/breathecode/provisioning/tests/tasks/tests_upload.py index ced30e77c..7fde55bfd 100644 --- a/breathecode/provisioning/tests/tasks/tests_upload.py +++ b/breathecode/provisioning/tests/tasks/tests_upload.py @@ -271,7 +271,13 @@ def test_random_csv__file_does_not_exists(self): self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), []) self.assertEqual(self.bc.database.list_of('authenticate.GithubAcademyUser'), []) - self.bc.check.calls(logging.Logger.info.call_args_list, [call(f'Starting upload for hash {slug}')]) + self.bc.check.calls( + logging.Logger.info.call_args_list, + [ + call(f'Starting upload for hash {slug}'), + # retrying + call(f'Starting upload for hash {slug}'), + ]) self.bc.check.calls(logging.Logger.error.call_args_list, [ call(f'File {slug} not found', exc_info=True), ]) diff --git a/breathecode/utils/api_view_extensions/extensions/lookup_extension.py b/breathecode/utils/api_view_extensions/extensions/lookup_extension.py index 761f36a46..6a9b3bf79 100644 --- a/breathecode/utils/api_view_extensions/extensions/lookup_extension.py +++ b/breathecode/utils/api_view_extensions/extensions/lookup_extension.py @@ -282,6 +282,9 @@ def _to_frozenset(self, value: Optional[dict]) -> frozenset: return frozenset(result.items()) + def _fixer(self, querystring: dict[str, str], fix) -> dict[str, str]: + return querystring + def build(self, lang: str, overwrite: dict = dict(), **kwargs: dict | tuple) -> tuple[tuple, dict]: # foreign @@ -296,6 +299,7 @@ def build(self, lang: str, overwrite: dict = dict(), **kwargs: dict | tuple) -> # opts custom_fields = kwargs.get('custom_fields', dict()) + fix = kwargs.get('custom_fields', dict()) # serialize foreign ids = tuple(ids) @@ -317,6 +321,9 @@ def build(self, lang: str, overwrite: dict = dict(), **kwargs: dict | tuple) -> datetimes=datetimes, bools=bools) + if fix: + querystring = self._fixer(querystring, fix) + return self._build_lookup(lang, lookup, querystring, custom_fields, overwrite) def _can_modify_queryset(self) -> bool: diff --git a/breathecode/utils/decorators/task.py b/breathecode/utils/decorators/task.py index d924447ea..43feeb249 100644 --- a/breathecode/utils/decorators/task.py +++ b/breathecode/utils/decorators/task.py @@ -1,23 +1,30 @@ -from datetime import datetime +from datetime import datetime, timedelta from decimal import Decimal +import importlib import inspect import logging -from typing import Callable +from typing import Any, Callable from breathecode.utils.exceptions import ProgrammingError import celery from django.db import transaction from django.utils import timezone import copy -__all__ = ['task', 'AbortTask'] +__all__ = ['task', 'AbortTask', 'RetryTask'] logger = logging.getLogger(__name__) +RETRIES_LIMIT = 10 +RETRY_AFTER = timedelta(seconds=5) class AbortTask(Exception): pass +class RetryTask(Exception): + pass + + def parse_payload(payload: dict): if not isinstance(payload, dict): return payload @@ -69,6 +76,19 @@ def get_fn_desc(self, function: Callable) -> tuple[str, str] or tuple[None, None return module_name, function_name + def reattemp_settings(self) -> dict[str, datetime]: + """ + Return a dict with the settings to reattemp the task. + """ + return {'eta': timezone.now() + RETRY_AFTER} + + def reattemp(self, task_module: str, task_name: str, attemps: int, args: tuple[Any], kwargs: dict[str, + Any]): + module = importlib.import_module(task_module) + x = getattr(module, task_name, None) + + x.apply_async(args=args, kwargs={**kwargs, 'attemps': attemps}, **self.reattemp_settings()) + def __call__(self, function): from breathecode.commons.models import TaskManager @@ -84,6 +104,7 @@ def wrapper(*args, **kwargs): page = kwargs.get('page', 0) total_pages = kwargs.get('total_pages', 1) + attemps = kwargs.get('attemps', None) task_manager_id = kwargs.get('task_manager_id', None) last_run = timezone.now() @@ -96,6 +117,7 @@ def wrapper(*args, **kwargs): created = True x = TaskManager.objects.create(task_module=task_module, task_name=task_name, + attemps=1, reverse_module=reverse_module, reverse_name=reverse_name, arguments=arguments, @@ -109,6 +131,10 @@ def wrapper(*args, **kwargs): if not created: x.current_page = page + 1 x.last_run = last_run + + if attemps: + x.attemps = attemps + 1 + x.save() if x.status in ['CANCELLED', 'REVERSED', 'PAUSED', 'ABORTED', 'DONE']: @@ -117,10 +143,32 @@ def wrapper(*args, **kwargs): return if self.is_transaction == True: + error = None with transaction.atomic(): sid = transaction.savepoint() try: - return function(*args, **kwargs) + x.status_message = '' + x.save() + + res = function(*args, **kwargs) + + except RetryTask as e: + x.status_message = str(e)[:255] + + if x.attemps >= RETRIES_LIMIT: + logger.exception(str(e)) + x.status = 'ERROR' + x.save() + + else: + logger.warn(str(e)) + x.save() + + self.reattemp(x.task_module, x.task_name, x.attemps, arguments['args'], + arguments['kwargs']) + + # it don't raise anything to manage the reattems with the task manager + return except AbortTask as e: x.status = 'ABORTED' @@ -137,41 +185,62 @@ def wrapper(*args, **kwargs): error = str(e)[:255] exception = e - x.status = 'ERROR' - x.status_message = error - x.save() - # fallback - if self.fallback: - return self.fallback(*args, **kwargs, exception=exception) + if error: + x.status = 'ERROR' + x.status_message = error + x.save() - # behavior by default - raise exception + # fallback + if self.fallback: + return self.fallback(*args, **kwargs, exception=exception) - try: - res = function(*args, **kwargs) + # behavior by default + raise exception - except AbortTask as e: - x.status = 'ABORTED' - x.status_message = str(e)[:255] - x.save() + else: + try: + res = function(*args, **kwargs) - logger.exception(str(e)) + except RetryTask as e: + x.status_message = str(e)[:255] - # avoid reattempts - return + if x.attemps >= RETRIES_LIMIT: + logger.exception(str(e)) + x.status = 'ERROR' + x.save() - except Exception as e: - x.status = 'ERROR' - x.status_message = str(e)[:255] - x.save() + else: + logger.warn(str(e)) + x.save() + + self.reattemp(x.task_module, x.task_name, x.attemps, arguments['args'], + arguments['kwargs']) + + # it don't raise anything to manage the reattems with the task manager + return + + except AbortTask as e: + x.status = 'ABORTED' + x.status_message = str(e)[:255] + x.save() + + logger.exception(str(e)) + + # avoid reattempts + return + + except Exception as e: + x.status = 'ERROR' + x.status_message = str(e)[:255] + x.save() - # fallback - if self.fallback: - return self.fallback(*args, **kwargs, exception=e) + # fallback + if self.fallback: + return self.fallback(*args, **kwargs, exception=e) - # behavior by default - raise e + # behavior by default + raise e if x.total_pages == x.current_page: x.status = 'DONE' diff --git a/breathecode/utils/tests/decorators/tests_task.py b/breathecode/utils/tests/decorators/tests_task.py index 8029806f0..7cef1fd73 100644 --- a/breathecode/utils/tests/decorators/tests_task.py +++ b/breathecode/utils/tests/decorators/tests_task.py @@ -92,6 +92,7 @@ def db_item(data={}): 'task_manager_id': 1 } }, + 'attemps': 1, 'current_page': 1, 'id': 1, 'killed': False, diff --git a/conftest.py b/conftest.py index 64b41ae0a..6067fa142 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,7 @@ from datetime import datetime import os import random +from unittest.mock import patch import pytest from scripts.utils.environment import reset_environment, test_environment from breathecode.utils.exceptions import TestError @@ -121,6 +122,18 @@ def enable(): yield enable +@pytest.fixture(autouse=True) +def dont_wait_for_rescheduling_tasks(monkeypatch): + """ + Don't wait for rescheduling tasks by default. You can re-enable it within a test by calling the provided wrapper. + """ + + with patch('breathecode.utils.decorators.task.RETRIES_LIMIT', 2): + with patch('breathecode.utils.decorators.task.Task.reattemp_settings', + lambda *args, **kwargs: dict()): + yield + + from django.dispatch.dispatcher import Signal