Skip to content

Commit

Permalink
proposition of solution on Create Order route
Browse files Browse the repository at this point in the history
I try to use your proposed request and response serializers for the
creation of an Order.
All the cases keeps the same returned response, except the error on billing_address,
now it return a dict of error and not a single string, because it's a field with multiple
values.
All the old test past (modification of the billing_address error).
The generation of openAPI also works.
The usage of new request serializer add also check on billing_address
and credit_card_id values that was not done before.

I have also a question on the existing code, if paiement fail because we
didn't find the card, we didn't cancel the order ? (see commentary in the code)
  • Loading branch information
MorganeAD committed Mar 7, 2023
1 parent e49484d commit dc4e122
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 62 deletions.
51 changes: 15 additions & 36 deletions src/backend/joanie/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,14 @@ class OrderViewSet(
lookup_field = "pk"
pagination_class = Pagination
permission_classes = [permissions.IsAuthenticated]
serializer_class = serializers.OrderSerializer
filterset_class = filters.OrderViewSetFilter
ordering = ["-created_on"]

def get_serializer_class(self):
if self.action == 'create':
return serializers.OrderCreateBodySerializer
return serializers.OrderSerializer

def get_queryset(self):
"""Custom queryset to limit to orders owned by the logged-in user."""
user = User.update_or_create_from_request_user(request_user=self.request.user)
Expand All @@ -182,7 +186,7 @@ def get_queryset(self):
def perform_create(self, serializer):
"""Force the order's "owner" field to the logged-in user."""
owner = User.update_or_create_from_request_user(request_user=self.request.user)
serializer.save(owner=owner)
return serializer.save(owner=owner)

@swagger_auto_schema(
request_body=serializers.OrderCreateBodySerializer,
Expand All @@ -192,37 +196,19 @@ def perform_create(self, serializer):
def create(self, request, *args, **kwargs):
"""Try to create an order and a related payment if the payment is fee."""
serializer = self.get_serializer(data=request.data)

# - Validate data
if not serializer.is_valid():
return Response(serializer.errors, status=400)

product = serializer.validated_data.get("product")
course = serializer.validated_data.get("course")
billing_address = serializer.validated_data.get("billing_address")
credit_card_id = serializer.validated_data.get("credit_card_id")

# Populate organization field if it is not set and there is only one
# on the product
if not serializer.validated_data.get("organization"):
try:
organization = product.course_relations.get(
course=course
).organizations.get()
except (
models.Course.DoesNotExist,
models.Organization.DoesNotExist,
models.Organization.MultipleObjectsReturned,
):
pass
else:
serializer.validated_data["organization"] = organization

# If product is not free, we have to create a payment.
# To create one, a billing address is mandatory
if product.price.amount > 0 and not billing_address:
return Response({"billing_address": "This field is required."}, status=400)

# - Validate data then create an order
# then create an order
try:
self.perform_create(serializer)
order = self.perform_create(serializer)
except (DRFValidationError, IntegrityError):
return Response(
(
Expand All @@ -234,9 +220,7 @@ def create(self, request, *args, **kwargs):

# Once order has been created, if product is not free, create a payment
if product.price.amount > 0:
order = serializer.instance
payment_backend = get_payment_backend()
credit_card_id = serializer.initial_data.get("credit_card_id")

# if payment in one click
if credit_card_id:
Expand All @@ -251,25 +235,20 @@ def create(self, request, *args, **kwargs):
credit_card_token=credit_card.token,
)
except (CreditCard.DoesNotExist, NotImplementedError):
# TODO question: if paiement fail because we didn't find the card, we didn't cancel the order ?
pass
else:
payment_info = payment_backend.create_payment(
request=request, order=order, billing_address=billing_address
)

# Return the fresh new order with payment_info
order.payment_info = payment_info
return Response(
# FIXME: this work but we'll need to make several change in test and clients
serializers.OrderCreateResponseSerializer(
{
"order": serializer.data,
"payment_info": payment_info,
}
).data,
status=201,
serializers.OrderCreateResponseSerializer(order).data, status=201
)
# Else return the fresh new order
return Response(serializer.data, status=201)
return Response(serializers.OrderCreateResponseSerializer(order).data, status=201)

@swagger_auto_schema(
request_body=serializers.OrderAbortBodySerializer,
Expand Down
3 changes: 1 addition & 2 deletions src/backend/joanie/core/serializers/model_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,6 @@ class OrderSerializer(serializers.ModelSerializer):
Order model serializer
"""

id = serializers.CharField()
owner = serializers.CharField(
source="owner.username", read_only=True, required=False
)
Expand Down Expand Up @@ -470,7 +469,7 @@ class Meta:
"certificate",
"created_on",
"enrollments",
# "id",
"id",
"main_invoice",
"owner",
"total",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,45 @@
"""Serializers for core.api.OrderViewSet.create Body"""

from rest_framework import serializers
from joanie.core import models
from .model_serializers import OrderSerializer, AddressSerializer


class OrderCreateBodySerializer(OrderSerializer):
billing_address = AddressSerializer(required=False)
credit_card_id = serializers.UUIDField(required=False)

from .model_serializers import OrderSerializer, OrderCreateSerializer, AddressSerializer
class Meta(OrderSerializer.Meta):
fields = OrderSerializer.Meta.fields + ["billing_address", "credit_card_id"]
read_only_fields = OrderSerializer.Meta.fields + ["billing_address", "credit_card_id"]

def validate(self, data):
cleaned_data = super().validate(data)
# Populate organization field if it is not set and there is only one
# on the product
if "organization" not in cleaned_data or not cleaned_data["organization"]:
try:
organization = cleaned_data["product"].course_relations.get(
course=cleaned_data["course"]
).organizations.get()
except (
models.Organization.DoesNotExist,
models.Organization.MultipleObjectsReturned,
):
pass
else:
cleaned_data["organization"] = organization

class OrderCreateBodySerializer:
credit_card_id = serializers.CharField(required=True)
course = serializers.CharField(required=True)
product = serializers.CharField(required=True)
billing_address = AddressSerializer(required=True)
# If product is not free, we have to create a payment.
# To create one, a billing address is mandatory
if cleaned_data['product'].price.amount > 0 \
and ("billing_address" not in cleaned_data or not cleaned_data['billing_address']):
raise serializers.ValidationError({"billing_address": "This field is required."})
return cleaned_data

class Meta(OrderCreateSerializer.Meta):
fields = ["billing_address"]
def create(self, validated_data):
order_data = validated_data.copy()
if 'billing_address' in order_data:
order_data.pop('billing_address')
if 'credit_card_id' in order_data:
order_data.pop('credit_card_id')
return super().create(order_data)
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
"""Serializers for core.api.OrderViewSet.create Response"""
from .model_serializers import OrderSerializer, PaymentSerializer

from rest_framework import serializers
from djmoney.contrib.django_rest_framework import MoneyField
from drf_yasg.utils import swagger_serializer_method

from joanie.core import models
from .model_serializers import OrderSerializer, PaymentSerializer, EnrollmentSerializer


class OrderCreateResponseSerializer(serializers.Serializer):
order = OrderSerializer(required=True)
class OrderCreateResponseSerializer(OrderSerializer):
payment_info = PaymentSerializer(required=False)

class Meta(OrderSerializer.Meta):
fields = ["order", "payment_info"]
read_only_fields = ["order", "payment_info"]
fields = OrderSerializer.Meta.fields + ["payment_info"]
read_only_fields = OrderSerializer.Meta.fields + ["payment_info"]
1 change: 1 addition & 0 deletions src/backend/joanie/payment/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class BillingAddressDictFactory(factory.DictFactory):
Return a billing address dictionary
"""

title = factory.Faker("name")
address = factory.Faker("street_address")
city = factory.Faker("city")
country = factory.Faker("country_code")
Expand Down
2 changes: 1 addition & 1 deletion src/backend/joanie/tests/core/test_api_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,7 @@ def test_api_order_create_payment_requires_billing_address(self):
self.assertFalse(models.Order.objects.exists())
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.json(), {"billing_address": "This field is required."}
response.json(), {"billing_address": ["This field is required."]}
)

@mock.patch.object(
Expand Down
2 changes: 2 additions & 0 deletions src/backend/joanie/tests/payment/test_backend_payplug.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ def test_payment_backend_payplug_handle_notification_payment(
order = OrderFactory(product=product)
backend = PayplugBackend(self.configuration)
billing_address = BillingAddressDictFactory()
del billing_address["title"]
payplug_billing_address = billing_address.copy()
payplug_billing_address["address1"] = payplug_billing_address["address"]
del payplug_billing_address["address"]
Expand Down Expand Up @@ -485,6 +486,7 @@ def test_payment_backend_payplug_handle_notification_payment_register_card(
order = OrderFactory(product=product)
backend = PayplugBackend(self.configuration)
billing_address = BillingAddressDictFactory()
del billing_address["title"]
payplug_billing_address = billing_address.copy()
payplug_billing_address["address1"] = payplug_billing_address["address"]
del payplug_billing_address["address"]
Expand Down
2 changes: 1 addition & 1 deletion src/openApiClientJs/gen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type { ErrorResponse } from './models/ErrorResponse';
export { Order } from './models/Order';
export type { OrderAbortBody } from './models/OrderAbortBody';
export { OrderCreateBody } from './models/OrderCreateBody';
export type { OrderCreateResponse } from './models/OrderCreateResponse';
export { OrderCreateResponse } from './models/OrderCreateResponse';
export type { Payment } from './models/Payment';
export { Product } from './models/Product';

Expand Down
3 changes: 3 additions & 0 deletions src/openApiClientJs/gen/models/Order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export type Order = {
readonly created_on?: string;
readonly certificate?: string;
readonly enrollments?: string;
/**
* primary key for the record as UUID
*/
readonly id?: string;
readonly main_invoice?: string;
organization?: string;
Expand Down
4 changes: 4 additions & 0 deletions src/openApiClientJs/gen/models/OrderCreateBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export type OrderCreateBody = {
readonly created_on?: string;
readonly certificate?: string;
readonly enrollments?: string;
/**
* primary key for the record as UUID
*/
readonly id?: string;
readonly main_invoice?: string;
organization?: string;
Expand All @@ -22,6 +25,7 @@ export type OrderCreateBody = {
readonly state?: OrderCreateBody.state;
readonly target_courses?: string;
billing_address?: Address;
credit_card_id?: string;
};

export namespace OrderCreateBody {
Expand Down
33 changes: 31 additions & 2 deletions src/openApiClientJs/gen/models/OrderCreateResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,40 @@
/* tslint:disable */
/* eslint-disable */

import type { Order } from './Order';
import type { Payment } from './Payment';

export type OrderCreateResponse = {
order: Order;
course: string;
/**
* date and time at which a record was created
*/
readonly created_on?: string;
readonly certificate?: string;
readonly enrollments?: string;
/**
* primary key for the record as UUID
*/
readonly id?: string;
readonly main_invoice?: string;
organization?: string;
readonly owner?: string;
readonly total?: number;
readonly total_currency?: string;
product: string;
readonly state?: OrderCreateResponse.state;
readonly target_courses?: string;
payment_info?: Payment;
};

export namespace OrderCreateResponse {

export enum state {
PENDING = 'pending',
CANCELED = 'canceled',
FAILED = 'failed',
VALIDATED = 'validated',
}


}

0 comments on commit dc4e122

Please sign in to comment.