From 4ad1c3ded498d5585fdb45a0ad964909b727cf40 Mon Sep 17 00:00:00 2001 From: Pawan Verma Date: Tue, 3 Oct 2023 18:26:32 +0530 Subject: [PATCH 1/2] Add budget and end date check for opportunity claims --- commcare_connect/opportunity/api/views.py | 11 +++++++++-- commcare_connect/opportunity/models.py | 11 +++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/commcare_connect/opportunity/api/views.py b/commcare_connect/opportunity/api/views.py index aa8d6ed5..43725077 100644 --- a/commcare_connect/opportunity/api/views.py +++ b/commcare_connect/opportunity/api/views.py @@ -1,3 +1,5 @@ +import datetime + from rest_framework import viewsets from rest_framework.generics import ListAPIView, get_object_or_404 from rest_framework.permissions import IsAuthenticated @@ -54,14 +56,19 @@ def post(self, *args, **kwargs): opportunity_access = get_object_or_404(OpportunityAccess, user=self.request.user, opportunity=kwargs.get("pk")) opportunity = opportunity_access.opportunity + if opportunity.remaining_budget <= 0: + return Response(status=200, data="Opportunity cannot be claimed. (Budget Exhausted)") + if opportunity.end_date < datetime.date.today(): + return Response(status=200, data="Opportunity cannot be claimed. (End date reached)") + + max_payments = min(opportunity.remaining_budget, opportunity.daily_max_visits_per_user) claim, created = OpportunityClaim.objects.get_or_create( opportunity_access=opportunity_access, defaults={ - "max_payments": opportunity.daily_max_visits_per_user, + "max_payments": max_payments, "end_date": opportunity.end_date, }, ) - if not created: return Response(status=200, data="Opportunity is already claimed") diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index c2837153..e6906c40 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -1,6 +1,7 @@ from uuid import uuid4 from django.db import models +from django.db.models import Sum from django.utils.translation import gettext from commcare_connect.organization.models import Organization @@ -64,6 +65,16 @@ class Opportunity(BaseModel): def __str__(self): return self.name + @property + def remaining_budget(self) -> int: + opp_access = OpportunityAccess.objects.filter(opportunity=self) + used_budget = OpportunityClaim.objects.filter(opportunity_access__in=opp_access).aggregate( + Sum("max_payments") + )["max_payments__sum"] + if used_budget is None: + used_budget = 0 + return self.total_budget - used_budget + class DeliverForm(models.Model): app = models.ForeignKey( From 4cabd6b5d9fdcc552c81b445f47e8ded98306ce4 Mon Sep 17 00:00:00 2001 From: Pawan Verma Date: Tue, 3 Oct 2023 22:23:59 +0530 Subject: [PATCH 2/2] Add tests for claim endpoint --- .../opportunity/tests/factories.py | 8 +++ .../opportunity/tests/test_api_views.py | 63 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 commcare_connect/opportunity/tests/test_api_views.py diff --git a/commcare_connect/opportunity/tests/factories.py b/commcare_connect/opportunity/tests/factories.py index 0bc3520d..3ee5772e 100644 --- a/commcare_connect/opportunity/tests/factories.py +++ b/commcare_connect/opportunity/tests/factories.py @@ -90,3 +90,11 @@ class UserVisitFactory(DjangoModelFactory): class Meta: model = "opportunity.UserVisit" + + +class OpportunityAccessFactory(DjangoModelFactory): + opportunity = SubFactory(OpportunityFactory) + user = SubFactory("commcare_connect.users.tests.factories.UserFactory") + + class Meta: + model = "opportunity.OpportunityAccess" diff --git a/commcare_connect/opportunity/tests/test_api_views.py b/commcare_connect/opportunity/tests/test_api_views.py new file mode 100644 index 00000000..d9cb6cec --- /dev/null +++ b/commcare_connect/opportunity/tests/test_api_views.py @@ -0,0 +1,63 @@ +import datetime + +from rest_framework.test import APIClient + +from commcare_connect.opportunity.models import OpportunityClaim +from commcare_connect.opportunity.tests.factories import OpportunityAccessFactory, OpportunityFactory +from commcare_connect.users.models import User +from commcare_connect.users.tests.factories import ConnectIdUserLinkFactory + + +def _setup_opportunity_and_access(mobile_user: User, total_budget, end_date): + opportunity = OpportunityFactory(total_budget=total_budget, max_visits_per_user=100, end_date=end_date) + opportunity_access = OpportunityAccessFactory(opportunity=opportunity, user=mobile_user) + ConnectIdUserLinkFactory( + user=mobile_user, commcare_username="test@ccc-test.commcarehq.org", domain=opportunity.deliver_app.cc_domain + ) + return opportunity, opportunity_access + + +def test_claim_endpoint_success(mobile_user: User, api_client: APIClient): + opportunity, opportunity_access = _setup_opportunity_and_access( + mobile_user, total_budget=1000, end_date=datetime.date.today() + datetime.timedelta(days=100) + ) + api_client.force_authenticate(mobile_user) + response = api_client.post(f"/api/opportunity/{opportunity.id}/claim") + assert response.status_code == 201 + claim = OpportunityClaim.objects.filter(opportunity_access=opportunity_access) + assert claim.exists() + + +def test_claim_endpoint_budget_exhausted(mobile_user: User, api_client: APIClient): + opportunity, opportunity_access = _setup_opportunity_and_access( + mobile_user, total_budget=0, end_date=datetime.date.today() + datetime.timedelta(days=100) + ) + api_client.force_authenticate(mobile_user) + response = api_client.post(f"/api/opportunity/{opportunity.id}/claim") + assert response.status_code == 200 + assert response.data == "Opportunity cannot be claimed. (Budget Exhausted)" + + +def test_claim_endpoint_end_date_exceeded(mobile_user: User, api_client: APIClient): + opportunity, opportunity_access = _setup_opportunity_and_access( + mobile_user, total_budget=1000, end_date=datetime.date.today() - datetime.timedelta(days=100) + ) + api_client.force_authenticate(mobile_user) + response = api_client.post(f"/api/opportunity/{opportunity.id}/claim") + assert response.status_code == 200 + assert response.data == "Opportunity cannot be claimed. (End date reached)" + + +def test_claim_endpoint_already_claimed_opportunity(mobile_user: User, api_client: APIClient): + opportunity, opportunity_access = _setup_opportunity_and_access( + mobile_user, total_budget=1000, end_date=datetime.date.today() + datetime.timedelta(days=100) + ) + api_client.force_authenticate(mobile_user) + response = api_client.post(f"/api/opportunity/{opportunity.id}/claim") + assert response.status_code == 201 + claim = OpportunityClaim.objects.filter(opportunity_access=opportunity_access) + assert claim.exists() + + response = api_client.post(f"/api/opportunity/{opportunity.id}/claim") + assert response.status_code == 200 + assert response.data == "Opportunity is already claimed"