From 368c03d122412a6bc3400cfa6ee4348025ea2924 Mon Sep 17 00:00:00 2001 From: Pawan Verma Date: Thu, 5 Dec 2024 13:46:04 +0530 Subject: [PATCH 1/5] Fix remaining budget calculation for managed opportunities --- commcare_connect/opportunity/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index 6214d3fc..722a0ae8 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -108,7 +108,12 @@ def minimum_budget_per_visit(self): @property def remaining_budget(self) -> int: - return self.total_budget - self.claimed_budget + if not self.managed: + return self.total_budget - self.claimed_budget + + org_pay = self.managedopportunity.org_pay_per_visit * self.allotted_visits + total_user_budget = self.total_budget - org_pay + return total_user_budget - self.claimed_budget @property def claimed_budget(self): From 722313853cb3d47a3dbd844d6f34b1fe3382fa64 Mon Sep 17 00:00:00 2001 From: Pawan Verma Date: Thu, 5 Dec 2024 13:46:21 +0530 Subject: [PATCH 2/5] Add test for managed opportunities --- .../opportunity/tests/test_models.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/commcare_connect/opportunity/tests/test_models.py b/commcare_connect/opportunity/tests/test_models.py index c5a3b494..3b263a4f 100644 --- a/commcare_connect/opportunity/tests/test_models.py +++ b/commcare_connect/opportunity/tests/test_models.py @@ -27,18 +27,19 @@ def test_learn_progress(opportunity: Opportunity): @pytest.mark.django_db +@pytest.mark.parametrize("opportunity", [{}, {"opp_options": {"managed": True}}], indirect=True) def test_opportunity_stats(opportunity: Opportunity, user: User): - payment_unit_sub = PaymentUnitFactory( + payment_unit_sub = PaymentUnitFactory.create( opportunity=opportunity, max_total=100, max_daily=10, amount=5, parent_payment_unit=None ) - payment_unit1 = PaymentUnitFactory( + payment_unit1 = PaymentUnitFactory.create( opportunity=opportunity, max_total=100, max_daily=10, amount=3, parent_payment_unit=payment_unit_sub, ) - payment_unit2 = PaymentUnitFactory( + payment_unit2 = PaymentUnitFactory.create( opportunity=opportunity, max_total=100, max_daily=10, amount=5, parent_payment_unit=None ) assert set(list(opportunity.paymentunit_set.values_list("id", flat=True))) == { @@ -46,10 +47,17 @@ def test_opportunity_stats(opportunity: Opportunity, user: User): payment_unit2.id, payment_unit_sub.id, } + payment_units = [payment_unit_sub, payment_unit1, payment_unit2] + budget_per_user = sum(pu.max_total * pu.amount for pu in payment_units) + org_pay = 0 + if opportunity.managed: + org_pay = opportunity.managedopportunity.org_pay_per_visit + budget_per_user += sum(pu.max_total * org_pay for pu in payment_units) + opportunity.total_budget = budget_per_user * 3 payment_units = [payment_unit1, payment_unit2, payment_unit_sub] assert opportunity.budget_per_user == sum([p.amount * p.max_total for p in payment_units]) - assert opportunity.number_of_users == opportunity.total_budget / opportunity.budget_per_user + assert opportunity.number_of_users == 3 assert opportunity.allotted_visits == sum([pu.max_total for pu in payment_units]) * opportunity.number_of_users assert opportunity.max_visits_per_user_new == sum([pu.max_total for pu in payment_units]) assert opportunity.daily_max_visits_per_user_new == sum([pu.max_daily for pu in payment_units]) @@ -61,7 +69,12 @@ def test_opportunity_stats(opportunity: Opportunity, user: User): ocl1 = OpportunityClaimLimitFactory(opportunity_claim=claim, payment_unit=payment_unit1) ocl2 = OpportunityClaimLimitFactory(opportunity_claim=claim, payment_unit=payment_unit2) - opportunity.claimed_budget == (ocl1.max_visits * payment_unit1.amount) + (ocl2.max_visits * payment_unit2.amount) + assert opportunity.claimed_budget == (ocl1.max_visits * payment_unit1.amount) + ( + ocl2.max_visits * payment_unit2.amount + ) + assert opportunity.remaining_budget == opportunity.total_budget - opportunity.claimed_budget - ( + opportunity.allotted_visits * org_pay + ) @pytest.mark.django_db From 87c9caddf6bb6fe9132d9ad5e78b6632f34a4e69 Mon Sep 17 00:00:00 2001 From: Pawan Verma Date: Thu, 12 Dec 2024 15:51:29 +0530 Subject: [PATCH 3/5] Add org_pay to claimed budget --- commcare_connect/opportunity/models.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index 722a0ae8..89ab28e7 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -108,18 +108,16 @@ def minimum_budget_per_visit(self): @property def remaining_budget(self) -> int: - if not self.managed: - return self.total_budget - self.claimed_budget - - org_pay = self.managedopportunity.org_pay_per_visit * self.allotted_visits - total_user_budget = self.total_budget - org_pay - return total_user_budget - self.claimed_budget + return self.total_budget - self.claimed_budget @property def claimed_budget(self): opp_access = OpportunityAccess.objects.filter(opportunity=self) opportunity_claim = OpportunityClaim.objects.filter(opportunity_access__in=opp_access) claim_limits = OpportunityClaimLimit.objects.filter(opportunity_claim__in=opportunity_claim) + org_pay = 0 + if self.managed: + org_pay = self.managedopportunity.org_pay_per_visit payment_unit_counts = claim_limits.values("payment_unit").annotate( visits_count=Sum("max_visits"), amount=F("payment_unit__amount") @@ -128,7 +126,7 @@ def claimed_budget(self): for count in payment_unit_counts: visits_count = count["visits_count"] amount = count["amount"] - claimed += visits_count * amount + claimed += visits_count * (amount + org_pay) return claimed From 23d7092ce1cb63aca5f646e30a110cc9fd3f207d Mon Sep 17 00:00:00 2001 From: Pawan Verma Date: Thu, 12 Dec 2024 15:51:49 +0530 Subject: [PATCH 4/5] Fix tests for opportunity stats --- commcare_connect/opportunity/tests/test_models.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/commcare_connect/opportunity/tests/test_models.py b/commcare_connect/opportunity/tests/test_models.py index 3b263a4f..526b83d2 100644 --- a/commcare_connect/opportunity/tests/test_models.py +++ b/commcare_connect/opportunity/tests/test_models.py @@ -69,12 +69,10 @@ def test_opportunity_stats(opportunity: Opportunity, user: User): ocl1 = OpportunityClaimLimitFactory(opportunity_claim=claim, payment_unit=payment_unit1) ocl2 = OpportunityClaimLimitFactory(opportunity_claim=claim, payment_unit=payment_unit2) - assert opportunity.claimed_budget == (ocl1.max_visits * payment_unit1.amount) + ( - ocl2.max_visits * payment_unit2.amount - ) - assert opportunity.remaining_budget == opportunity.total_budget - opportunity.claimed_budget - ( - opportunity.allotted_visits * org_pay + assert opportunity.claimed_budget == (ocl1.max_visits * (payment_unit1.amount + org_pay)) + ( + ocl2.max_visits * (payment_unit2.amount + org_pay) ) + assert opportunity.remaining_budget == opportunity.total_budget - opportunity.claimed_budget @pytest.mark.django_db From d2138ec4bc10bbee395b1b7c0d0c74d1c16fe8bc Mon Sep 17 00:00:00 2001 From: Pawan Verma Date: Thu, 12 Dec 2024 16:30:14 +0530 Subject: [PATCH 5/5] Add exhausted budget test for managed opportunities --- .../opportunity/tests/test_api_views.py | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/commcare_connect/opportunity/tests/test_api_views.py b/commcare_connect/opportunity/tests/test_api_views.py index 0471ebbd..021fc2c8 100644 --- a/commcare_connect/opportunity/tests/test_api_views.py +++ b/commcare_connect/opportunity/tests/test_api_views.py @@ -29,7 +29,7 @@ UserVisitFactory, ) from commcare_connect.users.models import User -from commcare_connect.users.tests.factories import ConnectIdUserLinkFactory +from commcare_connect.users.tests.factories import ConnectIdUserLinkFactory, MobileUserFactory def _setup_opportunity_and_access(mobile_user: User, total_budget, end_date, budget_per_visit=10): @@ -56,11 +56,37 @@ def test_claim_endpoint_success(mobile_user: User, api_client: APIClient): 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) +@pytest.mark.django_db +@pytest.mark.parametrize("opportunity", [{}, {"opp_options": {"managed": True}}], indirect=["opportunity"]) +def test_claim_endpoint_budget_exhausted(opportunity: Opportunity, api_client: APIClient): + PaymentUnitFactory(opportunity=opportunity, amount=10, max_total=100) + opportunity.total_budget = 10 * 100 + if opportunity.managed: + opportunity.total_budget += 100 * opportunity.managedopportunity.org_pay_per_visit + opportunity.end_date = datetime.date.today() + datetime.timedelta(days=100) + opportunity.save() + + mobile_user_1 = MobileUserFactory() + opportunity_access_1 = OpportunityAccessFactory(opportunity=opportunity, user=mobile_user_1) + ConnectIdUserLinkFactory( + user=mobile_user_1, + commcare_username="test_1@ccc-test.commcarehq.org", + domain=opportunity.deliver_app.cc_domain, ) - api_client.force_authenticate(mobile_user) + api_client.force_authenticate(mobile_user_1) + response = api_client.post(f"/api/opportunity/{opportunity.id}/claim") + assert response.status_code == 201 + claim = OpportunityClaim.objects.filter(opportunity_access=opportunity_access_1) + assert claim.exists() + + mobile_user_2 = MobileUserFactory() + OpportunityAccessFactory(opportunity=opportunity, user=mobile_user_2) + ConnectIdUserLinkFactory( + user=mobile_user_2, + commcare_username="test_2@ccc-test.commcarehq.org", + domain=opportunity.deliver_app.cc_domain, + ) + api_client.force_authenticate(mobile_user_2) response = api_client.post(f"/api/opportunity/{opportunity.id}/claim") assert response.status_code == 400 assert response.data == "Opportunity cannot be claimed. (Budget Exhausted)"