diff --git a/CHANGELOG.md b/CHANGELOG.md index c21be1702..090e9eefc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to ### Changed +- Update round robin logic to favor author organizations - Reassign organization for pending orders ### Fixed diff --git a/src/backend/joanie/core/api/client/__init__.py b/src/backend/joanie/core/api/client/__init__.py index 2e129d2e9..84ad8f2e4 100644 --- a/src/backend/joanie/core/api/client/__init__.py +++ b/src/backend/joanie/core/api/client/__init__.py @@ -10,7 +10,18 @@ from django.core.exceptions import ValidationError from django.core.files.storage import storages from django.db import IntegrityError, transaction -from django.db.models import Count, OuterRef, Prefetch, Q, Subquery +from django.db.models import ( + BooleanField, + Case, + Count, + ExpressionWrapper, + OuterRef, + Prefetch, + Q, + Subquery, + Value, + When, +) from django.http import FileResponse, Http404, HttpResponse, JsonResponse from django.urls import reverse from django.utils import timezone @@ -323,36 +334,35 @@ def _get_organization_with_least_active_orders( Return the organization with the least not canceled order count for a given product and course. """ - if enrollment: - clause = Q(order__enrollment=enrollment) - else: - clause = Q(order__course=course) - - order_count = Count( - "order", - filter=clause - & Q(order__product=product) - & ~Q( - order__state__in=[ - enums.ORDER_STATE_CANCELED, - enums.ORDER_STATE_PENDING, - ] - ), - ) + course_id = course.id if course else enrollment.course_run.course_id try: - course_relation = product.course_relations.get( - course_id=course.id if course else enrollment.course_run.course_id - ) + course_relation = product.course_relations.get(course_id=course_id) except models.CourseProductRelation.DoesNotExist: return None + order_count_filter = Q(order__product=product) & ~Q( + order__state__in=[ + enums.ORDER_STATE_CANCELED, + enums.ORDER_STATE_PENDING, + ] + ) + if enrollment: + order_count_filter &= Q(order__enrollment=enrollment) + else: + order_count_filter &= Q(order__course=course) + try: - return ( - course_relation.organizations.annotate(order_count=order_count) - .order_by("order_count") - .first() + organizations = course_relation.organizations.annotate( + order_count=Count("order", filter=order_count_filter), + is_author=Case( + When(Q(courses__id=course_id), then=Value(True)), + default=Value(False), + output_field=BooleanField(), + ), ) + + return organizations.order_by("order_count", "-is_author", "?").first() except models.Organization.DoesNotExist: return None diff --git a/src/backend/joanie/tests/core/api/order/test_submit.py b/src/backend/joanie/tests/core/api/order/test_submit.py index 5a8341281..6a502e22b 100644 --- a/src/backend/joanie/tests/core/api/order/test_submit.py +++ b/src/backend/joanie/tests/core/api/order/test_submit.py @@ -308,3 +308,70 @@ def test_api_order_submit_auto_assign_organization_with_least_orders(self): order.refresh_from_db() self.assertEqual(order.organization, expected_organization) + + def test_api_order_submit_get_organization_with_least_active_orders_prefer_author( + self, + ): + """ + In case of order count equality, the method _get_organization_with_least_orders should + return first organization which is also an author of the course. + """ + user = factories.UserFactory() + token = self.generate_token_from_user(user) + + organization, expected_organization = ( + factories.OrganizationFactory.create_batch(2) + ) + + relation = factories.CourseProductRelationFactory( + organizations=[organization, expected_organization] + ) + + relation.course.organizations.set([expected_organization]) + + # Create 3 orders for the first organization (1 draft, 1 pending, 1 canceled) + factories.OrderFactory( + organization=organization, + product=relation.product, + course=relation.course, + state=enums.ORDER_STATE_PENDING, + ) + factories.OrderFactory( + organization=organization, + product=relation.product, + course=relation.course, + state=enums.ORDER_STATE_CANCELED, + ) + + # 2 ignored orders for the second organization (1 pending, 1 canceled) + factories.OrderFactory( + organization=expected_organization, + product=relation.product, + course=relation.course, + state=enums.ORDER_STATE_PENDING, + ) + factories.OrderFactory( + organization=expected_organization, + product=relation.product, + course=relation.course, + state=enums.ORDER_STATE_CANCELED, + ) + + # Then create an order without organization + order = factories.OrderFactory( + owner=user, + product=relation.product, + course=relation.course, + organization=None, + ) + + self.client.patch( + f"/api/v1.0/orders/{order.id}/submit/", + content_type="application/json", + data={"billing_address": BillingAddressDictFactory()}, + HTTP_AUTHORIZATION=f"Bearer {token}", + ) + + order.refresh_from_db() + + self.assertEqual(order.organization, expected_organization)