Skip to content

Commit dc4e122

Browse files
committed
proposition of solution on Create Order route
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)
1 parent e49484d commit dc4e122

File tree

11 files changed

+102
-62
lines changed

11 files changed

+102
-62
lines changed

src/backend/joanie/core/api.py

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,14 @@ class OrderViewSet(
170170
lookup_field = "pk"
171171
pagination_class = Pagination
172172
permission_classes = [permissions.IsAuthenticated]
173-
serializer_class = serializers.OrderSerializer
174173
filterset_class = filters.OrderViewSetFilter
175174
ordering = ["-created_on"]
176175

176+
def get_serializer_class(self):
177+
if self.action == 'create':
178+
return serializers.OrderCreateBodySerializer
179+
return serializers.OrderSerializer
180+
177181
def get_queryset(self):
178182
"""Custom queryset to limit to orders owned by the logged-in user."""
179183
user = User.update_or_create_from_request_user(request_user=self.request.user)
@@ -182,7 +186,7 @@ def get_queryset(self):
182186
def perform_create(self, serializer):
183187
"""Force the order's "owner" field to the logged-in user."""
184188
owner = User.update_or_create_from_request_user(request_user=self.request.user)
185-
serializer.save(owner=owner)
189+
return serializer.save(owner=owner)
186190

187191
@swagger_auto_schema(
188192
request_body=serializers.OrderCreateBodySerializer,
@@ -192,37 +196,19 @@ def perform_create(self, serializer):
192196
def create(self, request, *args, **kwargs):
193197
"""Try to create an order and a related payment if the payment is fee."""
194198
serializer = self.get_serializer(data=request.data)
199+
200+
# - Validate data
195201
if not serializer.is_valid():
196202
return Response(serializer.errors, status=400)
197203

198204
product = serializer.validated_data.get("product")
199205
course = serializer.validated_data.get("course")
200206
billing_address = serializer.validated_data.get("billing_address")
207+
credit_card_id = serializer.validated_data.get("credit_card_id")
201208

202-
# Populate organization field if it is not set and there is only one
203-
# on the product
204-
if not serializer.validated_data.get("organization"):
205-
try:
206-
organization = product.course_relations.get(
207-
course=course
208-
).organizations.get()
209-
except (
210-
models.Course.DoesNotExist,
211-
models.Organization.DoesNotExist,
212-
models.Organization.MultipleObjectsReturned,
213-
):
214-
pass
215-
else:
216-
serializer.validated_data["organization"] = organization
217-
218-
# If product is not free, we have to create a payment.
219-
# To create one, a billing address is mandatory
220-
if product.price.amount > 0 and not billing_address:
221-
return Response({"billing_address": "This field is required."}, status=400)
222-
223-
# - Validate data then create an order
209+
# then create an order
224210
try:
225-
self.perform_create(serializer)
211+
order = self.perform_create(serializer)
226212
except (DRFValidationError, IntegrityError):
227213
return Response(
228214
(
@@ -234,9 +220,7 @@ def create(self, request, *args, **kwargs):
234220

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

241225
# if payment in one click
242226
if credit_card_id:
@@ -251,25 +235,20 @@ def create(self, request, *args, **kwargs):
251235
credit_card_token=credit_card.token,
252236
)
253237
except (CreditCard.DoesNotExist, NotImplementedError):
238+
# TODO question: if paiement fail because we didn't find the card, we didn't cancel the order ?
254239
pass
255240
else:
256241
payment_info = payment_backend.create_payment(
257242
request=request, order=order, billing_address=billing_address
258243
)
259244

260245
# Return the fresh new order with payment_info
246+
order.payment_info = payment_info
261247
return Response(
262-
# FIXME: this work but we'll need to make several change in test and clients
263-
serializers.OrderCreateResponseSerializer(
264-
{
265-
"order": serializer.data,
266-
"payment_info": payment_info,
267-
}
268-
).data,
269-
status=201,
248+
serializers.OrderCreateResponseSerializer(order).data, status=201
270249
)
271250
# Else return the fresh new order
272-
return Response(serializer.data, status=201)
251+
return Response(serializers.OrderCreateResponseSerializer(order).data, status=201)
273252

274253
@swagger_auto_schema(
275254
request_body=serializers.OrderAbortBodySerializer,

src/backend/joanie/core/serializers/model_serializers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,6 @@ class OrderSerializer(serializers.ModelSerializer):
423423
Order model serializer
424424
"""
425425

426-
id = serializers.CharField()
427426
owner = serializers.CharField(
428427
source="owner.username", read_only=True, required=False
429428
)
@@ -470,7 +469,7 @@ class Meta:
470469
"certificate",
471470
"created_on",
472471
"enrollments",
473-
# "id",
472+
"id",
474473
"main_invoice",
475474
"owner",
476475
"total",
Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,45 @@
11
"""Serializers for core.api.OrderViewSet.create Body"""
2-
32
from rest_framework import serializers
3+
from joanie.core import models
4+
from .model_serializers import OrderSerializer, AddressSerializer
5+
6+
7+
class OrderCreateBodySerializer(OrderSerializer):
8+
billing_address = AddressSerializer(required=False)
9+
credit_card_id = serializers.UUIDField(required=False)
410

5-
from .model_serializers import OrderSerializer, OrderCreateSerializer, AddressSerializer
11+
class Meta(OrderSerializer.Meta):
12+
fields = OrderSerializer.Meta.fields + ["billing_address", "credit_card_id"]
13+
read_only_fields = OrderSerializer.Meta.fields + ["billing_address", "credit_card_id"]
614

15+
def validate(self, data):
16+
cleaned_data = super().validate(data)
17+
# Populate organization field if it is not set and there is only one
18+
# on the product
19+
if "organization" not in cleaned_data or not cleaned_data["organization"]:
20+
try:
21+
organization = cleaned_data["product"].course_relations.get(
22+
course=cleaned_data["course"]
23+
).organizations.get()
24+
except (
25+
models.Organization.DoesNotExist,
26+
models.Organization.MultipleObjectsReturned,
27+
):
28+
pass
29+
else:
30+
cleaned_data["organization"] = organization
731

8-
class OrderCreateBodySerializer:
9-
credit_card_id = serializers.CharField(required=True)
10-
course = serializers.CharField(required=True)
11-
product = serializers.CharField(required=True)
12-
billing_address = AddressSerializer(required=True)
32+
# If product is not free, we have to create a payment.
33+
# To create one, a billing address is mandatory
34+
if cleaned_data['product'].price.amount > 0 \
35+
and ("billing_address" not in cleaned_data or not cleaned_data['billing_address']):
36+
raise serializers.ValidationError({"billing_address": "This field is required."})
37+
return cleaned_data
1338

14-
class Meta(OrderCreateSerializer.Meta):
15-
fields = ["billing_address"]
39+
def create(self, validated_data):
40+
order_data = validated_data.copy()
41+
if 'billing_address' in order_data:
42+
order_data.pop('billing_address')
43+
if 'credit_card_id' in order_data:
44+
order_data.pop('credit_card_id')
45+
return super().create(order_data)
Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
"""Serializers for core.api.OrderViewSet.create Response"""
2+
from .model_serializers import OrderSerializer, PaymentSerializer
23

3-
from rest_framework import serializers
4-
from djmoney.contrib.django_rest_framework import MoneyField
5-
from drf_yasg.utils import swagger_serializer_method
64

7-
from joanie.core import models
8-
from .model_serializers import OrderSerializer, PaymentSerializer, EnrollmentSerializer
9-
10-
11-
class OrderCreateResponseSerializer(serializers.Serializer):
12-
order = OrderSerializer(required=True)
5+
class OrderCreateResponseSerializer(OrderSerializer):
136
payment_info = PaymentSerializer(required=False)
147

158
class Meta(OrderSerializer.Meta):
16-
fields = ["order", "payment_info"]
17-
read_only_fields = ["order", "payment_info"]
9+
fields = OrderSerializer.Meta.fields + ["payment_info"]
10+
read_only_fields = OrderSerializer.Meta.fields + ["payment_info"]

src/backend/joanie/payment/factories.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class BillingAddressDictFactory(factory.DictFactory):
8383
Return a billing address dictionary
8484
"""
8585

86+
title = factory.Faker("name")
8687
address = factory.Faker("street_address")
8788
city = factory.Faker("city")
8889
country = factory.Faker("country_code")

src/backend/joanie/tests/core/test_api_order.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1111,7 +1111,7 @@ def test_api_order_create_payment_requires_billing_address(self):
11111111
self.assertFalse(models.Order.objects.exists())
11121112
self.assertEqual(response.status_code, 400)
11131113
self.assertEqual(
1114-
response.json(), {"billing_address": "This field is required."}
1114+
response.json(), {"billing_address": ["This field is required."]}
11151115
)
11161116

11171117
@mock.patch.object(

src/backend/joanie/tests/payment/test_backend_payplug.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ def test_payment_backend_payplug_handle_notification_payment(
408408
order = OrderFactory(product=product)
409409
backend = PayplugBackend(self.configuration)
410410
billing_address = BillingAddressDictFactory()
411+
del billing_address["title"]
411412
payplug_billing_address = billing_address.copy()
412413
payplug_billing_address["address1"] = payplug_billing_address["address"]
413414
del payplug_billing_address["address"]
@@ -485,6 +486,7 @@ def test_payment_backend_payplug_handle_notification_payment_register_card(
485486
order = OrderFactory(product=product)
486487
backend = PayplugBackend(self.configuration)
487488
billing_address = BillingAddressDictFactory()
489+
del billing_address["title"]
488490
payplug_billing_address = billing_address.copy()
489491
payplug_billing_address["address1"] = payplug_billing_address["address"]
490492
del payplug_billing_address["address"]

src/openApiClientJs/gen/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export type { ErrorResponse } from './models/ErrorResponse';
2121
export { Order } from './models/Order';
2222
export type { OrderAbortBody } from './models/OrderAbortBody';
2323
export { OrderCreateBody } from './models/OrderCreateBody';
24-
export type { OrderCreateResponse } from './models/OrderCreateResponse';
24+
export { OrderCreateResponse } from './models/OrderCreateResponse';
2525
export type { Payment } from './models/Payment';
2626
export { Product } from './models/Product';
2727

src/openApiClientJs/gen/models/Order.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export type Order = {
1010
readonly created_on?: string;
1111
readonly certificate?: string;
1212
readonly enrollments?: string;
13+
/**
14+
* primary key for the record as UUID
15+
*/
1316
readonly id?: string;
1417
readonly main_invoice?: string;
1518
organization?: string;

src/openApiClientJs/gen/models/OrderCreateBody.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export type OrderCreateBody = {
1212
readonly created_on?: string;
1313
readonly certificate?: string;
1414
readonly enrollments?: string;
15+
/**
16+
* primary key for the record as UUID
17+
*/
1518
readonly id?: string;
1619
readonly main_invoice?: string;
1720
organization?: string;
@@ -22,6 +25,7 @@ export type OrderCreateBody = {
2225
readonly state?: OrderCreateBody.state;
2326
readonly target_courses?: string;
2427
billing_address?: Address;
28+
credit_card_id?: string;
2529
};
2630

2731
export namespace OrderCreateBody {

0 commit comments

Comments
 (0)