Skip to content

Commit

Permalink
Merge pull request #1543 from jefer94/development
Browse files Browse the repository at this point in the history
fix subscriptions
  • Loading branch information
jefer94 authored Jan 30, 2025
2 parents 45b4339 + 03effe0 commit 2bff715
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 6 deletions.
2 changes: 1 addition & 1 deletion breathecode/assignments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def post(self, request, academy_id=None):
if isinstance(request.query_params, QueryDict):
for key, value in request.query_params.items():
merged_data[key] = value

webhook = LearnPack.add_webhook_to_log(merged_data)

if webhook:
Expand Down
8 changes: 7 additions & 1 deletion breathecode/payments/management/commands/make_charges.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import timedelta

from django.core.management.base import BaseCommand
from django.db.models import F
from django.db.models.query_utils import Q
from django.utils import timezone

Expand Down Expand Up @@ -47,7 +48,12 @@ def handle(self, *args, **options):
status="EXPIRED"
)

subscriptions = Subscription.objects.filter(*subscription_args, **params)
fix_subscriptions = Subscription.objects.filter(*subscription_args, paid_at=F("next_payment_at"), **params)

for subscription in fix_subscriptions:
tasks.fix_subscription_next_payment_at.delay(subscription.id)

subscriptions = Subscription.objects.filter(*subscription_args, **params).exclude(paid_at=F("next_payment_at"))
plan_financings = PlanFinancing.objects.filter(*financing_args, **params)

for status in statuses:
Expand Down
21 changes: 21 additions & 0 deletions breathecode/payments/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from capyc.core.i18n import translation
from dateutil.relativedelta import relativedelta
from django.core.cache import cache
from django.db.models import F
from django.utils import timezone
from django_redis import get_redis_connection
from redis.exceptions import LockError
Expand Down Expand Up @@ -239,6 +240,26 @@ def renew_plan_financing_consumables(self, plan_financing_id: int, **_: Any):
renew_consumables.delay(scheduler.id)


# this could be removed 01-29-2025
@task(bind=False, priority=TaskPriority.WEB_SERVICE_PAYMENT.value)
def fix_subscription_next_payment_at(subscription_id: int, **_: Any):
"""Fix a subscription next payment at."""

logger.info(f"Starting fix_subscription_next_payment_at for subscription {subscription_id}")

if not (
subscription := Subscription.objects.filter(id=subscription_id, paid_at=F("next_payment_at"))
.only("id", "paid_at", "next_payment_at")
.first()
):
raise AbortTask(f"Subscription with id {subscription_id} not found")

delta = actions.calculate_relative_delta(subscription.pay_every, subscription.pay_every_unit)

subscription.next_payment_at += delta
subscription.save()


def fallback_charge_subscription(self, subscription_id: int, exception: Exception, **_: Any):
if not (subscription := Subscription.objects.filter(id=subscription_id).first()):
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
def setup(db: None, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(tasks.charge_subscription, "delay", MagicMock())
monkeypatch.setattr(tasks.charge_plan_financing, "delay", MagicMock())
monkeypatch.setattr(tasks.fix_subscription_next_payment_at, "delay", MagicMock())


def test_with_zero_subscriptions(bc: Breathecode):
Expand All @@ -25,6 +26,32 @@ def test_with_zero_subscriptions(bc: Breathecode):
assert result == None
assert bc.database.list_of("payments.Subscription") == []
assert tasks.charge_subscription.delay.call_args_list == []
assert tasks.fix_subscription_next_payment_at.delay.call_args_list == []


def test_fix_next_payment_at_for_subscriptions(bc: Breathecode, utc_now):
model = bc.database.create(
subscription=[
{
"valid_until": None,
"next_payment_at": utc_now - relativedelta(seconds=1),
"paid_at": utc_now - relativedelta(seconds=1),
},
{
"valid_until": utc_now + relativedelta(months=1),
"next_payment_at": utc_now - relativedelta(days=1),
"paid_at": utc_now - relativedelta(days=1),
},
]
)

command = Command()
result = command.handle()

assert result == None
assert bc.database.list_of("payments.Subscription") == bc.format.to_dict(model.subscription)
assert tasks.charge_subscription.delay.call_args_list == []
assert tasks.fix_subscription_next_payment_at.delay.call_args_list == [call(1), call(2)]


@pytest.mark.parametrize(
Expand All @@ -50,6 +77,7 @@ def test_with_two_subscriptions__wrong_cases(bc: Breathecode, delta, status, utc
assert result == None
assert bc.database.list_of("payments.Subscription") == bc.format.to_dict(model.subscription)
assert tasks.charge_subscription.delay.call_args_list == []
assert tasks.fix_subscription_next_payment_at.delay.call_args_list == []


@pytest.mark.parametrize(
Expand Down Expand Up @@ -77,6 +105,7 @@ def test_with_two_subscriptions__expired(bc: Breathecode, delta, status, status_

assert bc.database.list_of("payments.Subscription") == db
assert tasks.charge_subscription.delay.call_args_list == []
assert tasks.fix_subscription_next_payment_at.delay.call_args_list == []


@pytest.mark.parametrize(
Expand Down Expand Up @@ -112,6 +141,7 @@ def test_with_two_subscriptions__payment_issue__gt_7_days(

assert bc.database.list_of("payments.Subscription") == db
assert tasks.charge_subscription.delay.call_args_list == []
assert tasks.fix_subscription_next_payment_at.delay.call_args_list == []


@pytest.mark.parametrize(
Expand Down Expand Up @@ -145,6 +175,7 @@ def test_with_two_subscriptions__payment_issue__lt_7_days(

assert bc.database.list_of("payments.Subscription") == db
assert tasks.charge_subscription.delay.call_args_list == [call(1), call(2)]
assert tasks.fix_subscription_next_payment_at.delay.call_args_list == []


@pytest.mark.parametrize(
Expand All @@ -171,6 +202,7 @@ def test_with_two_subscriptions__valid_cases(bc: Breathecode, delta, status, utc
call(model.subscription[0].id),
call(model.subscription[1].id),
]
assert tasks.fix_subscription_next_payment_at.delay.call_args_list == []


# 🔽🔽🔽 PlanFinancing cases
Expand Down Expand Up @@ -214,6 +246,7 @@ def test_with_two_plan_financings__wrong_cases(bc: Breathecode, delta, status, u
assert result == None
assert bc.database.list_of("payments.PlanFinancing") == bc.format.to_dict(model.plan_financing)
assert tasks.charge_plan_financing.delay.call_args_list == []
assert tasks.fix_subscription_next_payment_at.delay.call_args_list == []


@pytest.mark.parametrize(
Expand Down Expand Up @@ -248,6 +281,7 @@ def test_with_two_plan_financings__expired(bc: Breathecode, delta, status, statu

assert bc.database.list_of("payments.PlanFinancing") == db
assert tasks.charge_plan_financing.delay.call_args_list == []
assert tasks.fix_subscription_next_payment_at.delay.call_args_list == []


@pytest.mark.parametrize(
Expand Down Expand Up @@ -280,3 +314,4 @@ def test_with_two_plan_financings__valid_cases(bc: Breathecode, delta, status, u
call(model.plan_financing[0].id),
call(model.plan_financing[1].id),
]
assert tasks.fix_subscription_next_payment_at.delay.call_args_list == []
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""
Test /answer
"""

import random
from datetime import timedelta
from logging import Logger
from unittest.mock import MagicMock, call

import pytest
from dateutil.relativedelta import relativedelta
from django.utils import timezone

from breathecode.payments import tasks
from breathecode.tests.mixins.breathecode_mixin import Breathecode

UTC_NOW = timezone.now()

# enable this file to use the database
pytestmark = pytest.mark.usefixtures("db")


@pytest.fixture(autouse=True)
def setup(monkeypatch):
# mock logger with monkeypatch

monkeypatch.setattr("logging.Logger.info", MagicMock())
monkeypatch.setattr("logging.Logger.error", MagicMock())

yield


@pytest.fixture
def reset_mock_calls():

def wrapper():
Logger.info.call_args_list = []
Logger.error.call_args_list = []

yield wrapper


@pytest.mark.parametrize("subscription_number", [0, 1])
def test_subscription_not_found(bc: Breathecode, reset_mock_calls, subscription_number, utc_now):
if subscription_number:
model = bc.database.create(
subscription=(subscription_number, {"paid_at": utc_now, "next_payment_at": utc_now + timedelta(days=2)})
)
reset_mock_calls()

tasks.fix_subscription_next_payment_at(1)

assert bc.database.list_of("payments.CohortSet") == []
assert bc.database.list_of("payments.CohortSetCohort") == []

if subscription_number:
assert bc.database.list_of("payments.Subscription") == [bc.format.to_dict(model.subscription)]

else:
assert bc.database.list_of("payments.Subscription") == []

assert Logger.info.call_args_list == [
call("Starting fix_subscription_next_payment_at for subscription 1"),
]
assert Logger.error.call_args_list == [call("Subscription with id 1 not found", exc_info=True)]


@pytest.mark.parametrize(
"pay_every, pay_every_unit, next_payment_at_delta",
[
(1, "DAY", relativedelta(days=1)),
(3, "DAY", relativedelta(days=3)),
(1, "WEEK", relativedelta(weeks=1)),
(3, "WEEK", relativedelta(weeks=3)),
(1, "MONTH", relativedelta(months=1)),
(3, "MONTH", relativedelta(months=3)),
(1, "YEAR", relativedelta(years=1)),
(3, "YEAR", relativedelta(years=3)),
],
)
@pytest.mark.parametrize(
"delta",
[-timedelta(days=2), -timedelta(days=1), timedelta(days=0), timedelta(days=1), timedelta(days=2)],
)
def test_fix_payment_at_for_subscription(
bc: Breathecode, reset_mock_calls, utc_now, delta, pay_every, pay_every_unit, next_payment_at_delta
):
model = bc.database.create(
subscription=(
{
"paid_at": utc_now + delta,
"next_payment_at": utc_now + delta,
"pay_every": pay_every,
"pay_every_unit": pay_every_unit,
}
)
)
reset_mock_calls()

tasks.fix_subscription_next_payment_at(1)

assert bc.database.list_of("payments.CohortSet") == []
assert bc.database.list_of("payments.CohortSetCohort") == []

assert bc.database.list_of("payments.Subscription") == [
{**bc.format.to_dict(model.subscription), "next_payment_at": utc_now + delta + next_payment_at_delta}
]

assert Logger.info.call_args_list == [
call("Starting fix_subscription_next_payment_at for subscription 1"),
]
assert Logger.error.call_args_list == []
11 changes: 7 additions & 4 deletions breathecode/services/learnpack/actions/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
logger = logging.getLogger(__name__)
from breathecode.assignments.models import AssignmentTelemetry, LearnPackWebhook


def batch(self, webhook: LearnPackWebhook):
# lazyload to fix circular import
from breathecode.registry.models import Asset
from breathecode.assignments.models import Task

asset = None
if "asset_id" in webhook.payload:
if "asset_id" in webhook.payload:
_id = webhook.payload["asset_id"]
asset = Asset.objects.filter(id=_id).first()
if asset is None:

if asset is None:
_slug = webhook.payload["slug"]
asset = Asset.get_by_slug(_slug)

if asset is None:
raise Exception("Asset specified by learnpack telemetry was not found using either the payload 'asset_id' or 'slug'")
raise Exception(
"Asset specified by learnpack telemetry was not found using either the payload 'asset_id' or 'slug'"
)

telemetry = AssignmentTelemetry.objects.filter(asset_slug=asset.slug, user__id=webhook.payload["user_id"]).first()

Expand Down
8 changes: 8 additions & 0 deletions docs/infrastructure/journal.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,11 @@ Why:
## 06/10/2024

- `[dev]` `HEROKU_POSTGRESQL_TEAL` was replaced by `HEROKU_POSTGRESQL_GOLD`.

## 29/01/2025

- `[dev]` switch `CELERY_CONCURRENCY` from `8` to `4`.

Why:

- To reduce the memory usage of the celery workers.

0 comments on commit 2bff715

Please sign in to comment.