Skip to content

Commit

Permalink
✨(backend) admin api refund and cancel endpoints
Browse files Browse the repository at this point in the history
Added new endpoints for the admin API to allow
to cancel and refund an order.
  • Loading branch information
jonathanreveille committed Oct 2, 2024
1 parent 5a9519d commit 0bd8a1e
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 13 deletions.
44 changes: 44 additions & 0 deletions src/backend/joanie/core/api/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@
generate_certificates_task,
update_organization_signatories_contracts_task,
)
from joanie.core.tasks.payment_schedule import refund_installments_task
from joanie.core.utils.course_product_relation import (
get_generated_certificates,
get_orders,
)
from joanie.core.utils.payment_schedule import (
has_installment_paid,
)

from .enrollment import EnrollmentViewSet

Expand Down Expand Up @@ -636,6 +640,46 @@ def generate_certificate(self, request, pk=None): # pylint:disable=unused-argum

return Response(certificate, status=HTTPStatus.CREATED)

@action(methods=["POST"], detail=True)
def cancel(self, request, pk=None): # pylint:disable=unused-argument
"""
Cancel an order.
"""
order = self.get_object()

if order.state == enums.ORDER_STATE_COMPLETED:
return Response(
"Cannot cancel a completed order.",
status=HTTPStatus.UNPROCESSABLE_ENTITY,
)

order.flow.cancel()
return Response(status=HTTPStatus.OK)

@action(methods=["POST"], detail=True)
def refund(self, request, pk=None): # pylint:disable=unused-argument
"""
Refund an order only if the order is in state 'cancel' and at least 1 installment
has been paid in the payment schedule.
"""
order = self.get_object()

if order.state != enums.ORDER_STATE_CANCELED:
return Response(
"Cannot refund an order not canceled.",
status=HTTPStatus.UNPROCESSABLE_ENTITY,
)

if not has_installment_paid(order):
return Response(
"Cannot refund an order without paid installments in payment schedule.",
status=HTTPStatus.BAD_REQUEST,
)

refund_installments_task.delay(order_id=order.id)

return Response(status=HTTPStatus.ACCEPTED)


class OrganizationAddressViewSet(
mixins.CreateModelMixin,
Expand Down
18 changes: 18 additions & 0 deletions src/backend/joanie/core/tasks/payment_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

from logging import getLogger

from django.core.exceptions import ValidationError

from joanie.celery_app import app
from joanie.core.models import Order
from joanie.core.utils.payment_schedule import (
is_installment_to_debit,
refund_paid_installments,
send_mail_reminder_for_installment_debit,
)
from joanie.payment import get_payment_backend
Expand Down Expand Up @@ -50,3 +53,18 @@ def send_mail_reminder_installment_debit_task(order_id, installment_id):
None,
)
send_mail_reminder_for_installment_debit(order, installment)


@app.task
def refund_installments_task(order_id):
"""
Task to refund the installments paid in a payment schedule of an order.
"""
try:
order = Order.objects.get(id=order_id)
except Order.DoesNotExist as error:
raise ValidationError("The given `order_id` does not exist.") from error

logger.info("Starting Celery task, refunding order id: %s...", order_id)
refund_paid_installments(order)
logger.info("Starting Celery task, done refunding order id: %s", order_id)
124 changes: 124 additions & 0 deletions src/backend/joanie/tests/core/test_api_admin_orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -1525,3 +1525,127 @@ def test_api_admin_orders_generate_certificate_when_order_is_not_ready_for_gradi
self.assertDictEqual(
response.json(), {"details": "This order is not ready for gradation."}
)

def test_api_admin_orders_cancel_request_without_authentication(self):
"""
Anonymous users should not be able to request cancel order endpoint.
"""
order = factories.OrderGeneratorFactory(state=enums.ORDER_STATE_PENDING_PAYMENT)

response = self.client.post(f"/api/v1.0/admin/orders/{order.id}/cancel/")

self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED)

def test_api_admin_orders_cancel_request_with_lambda_user(self):
"""
Lambda users should not be able to request cancel order endpoint.
"""
admin = factories.UserFactory(is_staff=False, is_superuser=False)
self.client.login(username=admin.username, password="password")
order = factories.OrderGeneratorFactory(state=enums.ORDER_STATE_PENDING_PAYMENT)

response = self.client.post(f"/api/v1.0/admin/orders/{order.id}/cancel/")

self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)

def test_api_admin_orders_cancel_with_an_invalid_order_id(self):
"""
Authenticated admin user should not be able to cancel an order by passing
an invalid order id.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")

response = self.client.post("/api/v1.0/admin/orders/invalid_id/cancel/")

self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND)

def test_api_admin_orders_cancel_with_get_method_is_not_allowed(self):
"""
Authenticated admin users should not be able to use the get method to request to cancel
an order endpoint.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
order = factories.OrderGeneratorFactory(state=enums.ORDER_STATE_PENDING_PAYMENT)

response = self.client.get(f"/api/v1.0/admin/orders/{order.id}/cancel/")

self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED)
self.assertEqual(order.state, enums.ORDER_STATE_PENDING_PAYMENT)

def test_api_admin_orders_cancel_with_put_method_is_not_allowed(self):
"""
Authenticated admin users should not be able to use the put method to update the request to
to cancel an order endpoint.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
order = factories.OrderGeneratorFactory(state=enums.ORDER_STATE_PENDING_PAYMENT)

response = self.client.put(f"/api/v1.0/admin/orders/{order.id}/cancel/")

self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED)
self.assertEqual(order.state, enums.ORDER_STATE_PENDING_PAYMENT)

def test_api_admin_orders_cancel_with_patch_method_is_not_allowed(self):
"""
Authenticated admin users should not be able to use the patch method to update a request
to cancel an order endpoint.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
order = factories.OrderGeneratorFactory(state=enums.ORDER_STATE_PENDING_PAYMENT)

response = self.client.patch(f"/api/v1.0/admin/orders/{order.id}/cancel/")

self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED)
self.assertEqual(order.state, enums.ORDER_STATE_PENDING_PAYMENT)

def test_api_admin_orders_cancel_with_delete_method_is_not_allowed(self):
"""
Authenticated admin users should not be able to use the delete method to delete an order on
the cancel an order endpoint.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
order = factories.OrderGeneratorFactory(state=enums.ORDER_STATE_PENDING_PAYMENT)

response = self.client.delete(f"/api/v1.0/admin/orders/{order.id}/cancel/")

self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED)
self.assertEqual(order.state, enums.ORDER_STATE_PENDING_PAYMENT)

def test_api_admin_orders_cancel_with_post_method_success(self):
"""
Authenticated admin users should be able to use the post method to request to cancel an
order endpoint. The order's state should be 'canceled' after calling the endpoint.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
order = factories.OrderGeneratorFactory(state=enums.ORDER_STATE_PENDING_PAYMENT)

response = self.client.post(f"/api/v1.0/admin/orders/{order.id}/cancel/")

order.refresh_from_db()

self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertEqual(order.state, enums.ORDER_STATE_CANCELED)

def test_api_admin_orders_cancel_an_order_that_is_completed_should_not_cancel_the_order(
self,
):
"""
Authenticated admin users should not be able to cancel an order that is in state
`completed`.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
order = factories.OrderGeneratorFactory(state=enums.ORDER_STATE_COMPLETED)

response = self.client.post(f"/api/v1.0/admin/orders/{order.id}/cancel/")

order.refresh_from_db()

self.assertEqual(response.status_code, HTTPStatus.UNPROCESSABLE_ENTITY)
self.assertEqual(order.state, enums.ORDER_STATE_COMPLETED)
Loading

0 comments on commit 0bd8a1e

Please sign in to comment.