Merge branch 'sync-old-meowpay-patches' into 'master'

Revamped payment pipeline (imported from nico/mew-cloud)

See merge request uncloud/uncloud!3
This commit is contained in:
nico14571 2020-04-09 11:31:49 +02:00
commit d6bdf5c991
10 changed files with 356 additions and 148 deletions

View file

@ -1,6 +1,3 @@
# Live/test key from stripe
STRIPE_KEY = ''
# XML-RPC interface of opennebula # XML-RPC interface of opennebula
OPENNEBULA_URL = 'https://opennebula.ungleich.ch:2634/RPC2' OPENNEBULA_URL = 'https://opennebula.ungleich.ch:2634/RPC2'
@ -15,6 +12,7 @@ LDAP_ADMIN_PASSWORD=""
LDAP_SERVER_URI = "" LDAP_SERVER_URI = ""
# Stripe (Credit Card payments) # Stripe (Credit Card payments)
STRIPE_API_key="" STRIPE_KEY=""
STRIPE_PUBLIC_KEY=""
SECRET_KEY="dx$iqt=lc&yrp^!z5$ay^%g5lhx1y3bcu=jg(jx0yj0ogkfqvf" SECRET_KEY="dx$iqt=lc&yrp^!z5$ay^%g5lhx1y3bcu=jg(jx0yj0ogkfqvf"

View file

@ -54,7 +54,6 @@ router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='paym
router.register(r'bill', payviews.BillViewSet, basename='bill') router.register(r'bill', payviews.BillViewSet, basename='bill')
router.register(r'order', payviews.OrderViewSet, basename='order') router.register(r'order', payviews.OrderViewSet, basename='order')
router.register(r'payment', payviews.PaymentViewSet, basename='payment') router.register(r'payment', payviews.PaymentViewSet, basename='payment')
router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='payment-methods')
# admin/staff urls # admin/staff urls

View file

@ -0,0 +1,27 @@
# Generated by Django 3.0.3 on 2020-03-05 15:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('uncloud_pay', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='paymentmethod',
old_name='stripe_card_id',
new_name='stripe_payment_method_id',
),
migrations.AddField(
model_name='paymentmethod',
name='stripe_setup_intent_id',
field=models.CharField(blank=True, max_length=32, null=True),
),
migrations.AlterUniqueTogether(
name='paymentmethod',
unique_together=set(),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.0.3 on 2020-03-05 13:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('uncloud_pay', '0002_auto_20200305_1524.py'),
]
operations = [
migrations.AlterField(
model_name='paymentmethod',
name='primary',
field=models.BooleanField(default=False, editable=False),
),
]

View file

@ -4,9 +4,7 @@ from django.contrib.auth import get_user_model
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.dispatch import receiver
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
import django.db.models.signals as signals
import uuid import uuid
from functools import reduce from functools import reduce
@ -106,57 +104,69 @@ class PaymentMethod(models.Model):
), ),
default='stripe') default='stripe')
description = models.TextField() description = models.TextField()
primary = models.BooleanField(default=True) primary = models.BooleanField(default=False, editable=False)
# Only used for "Stripe" source # Only used for "Stripe" source
stripe_card_id = models.CharField(max_length=32, blank=True, null=True) stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True)
stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True)
@property @property
def stripe_card_last4(self): def stripe_card_last4(self):
if self.source == 'stripe': if self.source == 'stripe' and self.active:
card_request = uncloud_pay.stripe.get_card( payment_method = uncloud_pay.stripe.get_payment_method(
StripeCustomer.objects.get(owner=self.owner).stripe_id, self.stripe_payment_method_id)
self.stripe_card_id) return payment_method.card.last4
if card_request['error'] == None:
return card_request['response_object']['last4']
else:
return None
else: else:
return None return None
@property
def active(self):
if self.source == 'stripe' and self.stripe_payment_method_id != None:
return True
else:
return False
def charge(self, amount): def charge(self, amount):
if amount > 0: # Make sure we don't charge negative amount by errors... if not self.active:
raise Exception('This payment method is inactive.')
if amount < 0: # Make sure we don't charge negative amount by errors...
raise Exception('Cannot charge negative amount.')
if self.source == 'stripe': if self.source == 'stripe':
stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id
charge_request = uncloud_pay.stripe.charge_customer(amount, stripe_customer, self.stripe_card_id) stripe_payment = uncloud_pay.stripe.charge_customer(
if charge_request['error'] == None: amount, stripe_customer, self.stripe_payment_method_id)
payment = Payment(owner=self.owner, source=self.source, amount=amount) if 'paid' in stripe_payment and stripe_payment['paid'] == False:
payment.save() # TODO: Check return status raise Exception(stripe_payment['error'])
else:
payment = Payment.objects.create(
owner=self.owner, source=self.source, amount=amount)
return payment return payment
else:
raise Exception('Stripe error: {}'.format(charge_request['error']))
else: else:
raise Exception('This payment method is unsupported/cannot be charged.') raise Exception('This payment method is unsupported/cannot be charged.')
else:
raise Exception('Cannot charge negative amount.')
def set_as_primary_for(self, user):
methods = PaymentMethod.objects.filter(owner=user, primary=True)
for method in methods:
print(method)
method.primary = False
method.save()
self.primary = True
self.save()
def get_primary_for(user): def get_primary_for(user):
methods = PaymentMethod.objects.filter(owner=user) methods = PaymentMethod.objects.filter(owner=user)
for method in methods: for method in methods:
# Do we want to do something with non-primary method?
if method.primary: if method.primary:
return method return method
return None return None
class Meta:
unique_together = [['owner', 'primary']]
### ###
# Bills & Payments. # Bills.
class Bill(models.Model): class Bill(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

View file

@ -8,30 +8,29 @@ from .models import *
class PaymentSerializer(serializers.ModelSerializer): class PaymentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Payment model = Payment
fields = ['owner', 'amount', 'source', 'timestamp'] fields = '__all__'
class PaymentMethodSerializer(serializers.ModelSerializer): class PaymentMethodSerializer(serializers.ModelSerializer):
stripe_card_last4 = serializers.IntegerField() stripe_card_last4 = serializers.IntegerField()
class Meta: class Meta:
model = PaymentMethod model = PaymentMethod
fields = ['uuid', 'source', 'description', 'primary', 'stripe_card_last4'] fields = ['uuid', 'source', 'description', 'primary', 'stripe_card_last4', 'active']
class UpdatePaymentMethodSerializer(serializers.ModelSerializer):
class Meta:
model = PaymentMethod
fields = ['description']
class ChargePaymentMethodSerializer(serializers.Serializer): class ChargePaymentMethodSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=10, decimal_places=2) amount = serializers.DecimalField(max_digits=10, decimal_places=2)
class CreditCardSerializer(serializers.Serializer):
number = serializers.IntegerField()
exp_month = serializers.IntegerField()
exp_year = serializers.IntegerField()
cvc = serializers.IntegerField()
class CreatePaymentMethodSerializer(serializers.ModelSerializer): class CreatePaymentMethodSerializer(serializers.ModelSerializer):
credit_card = CreditCardSerializer() please_visit = serializers.CharField(read_only=True)
class Meta: class Meta:
model = PaymentMethod model = PaymentMethod
fields = ['source', 'description', 'primary', 'credit_card'] fields = ['uuid', 'primary', 'source', 'description', 'please_visit']
read_only_field = ['uuid', 'primary']
### ###
# Orders & Products. # Orders & Products.

View file

@ -10,9 +10,14 @@ import uncloud.secrets
# Static stripe configuration used below. # Static stripe configuration used below.
CURRENCY = 'chf' CURRENCY = 'chf'
# README: We use the Payment Intent API as described on
# https://stripe.com/docs/payments/save-and-reuse
# For internal use only.
stripe.api_key = uncloud.secrets.STRIPE_KEY stripe.api_key = uncloud.secrets.STRIPE_KEY
# Helper (decorator) used to catch errors raised by stripe logic. # Helper (decorator) used to catch errors raised by stripe logic.
# Catch errors that should not be displayed to the end user, raise again.
def handle_stripe_error(f): def handle_stripe_error(f):
def handle_problems(*args, **kwargs): def handle_problems(*args, **kwargs):
response = { response = {
@ -21,108 +26,84 @@ def handle_stripe_error(f):
'error': None 'error': None
} }
common_message = "Currently it is not possible to make payments." common_message = "Currently it is not possible to make payments. Please try agin later."
try: try:
response_object = f(*args, **kwargs) response_object = f(*args, **kwargs)
response = { return response_object
'response_object': response_object,
'error': None
}
return response
except stripe.error.CardError as e: except stripe.error.CardError as e:
# Since it's a decline, stripe.error.CardError will be caught # Since it's a decline, stripe.error.CardError will be caught
body = e.json_body body = e.json_body
err = body['error']
response.update({'error': err['message']})
logging.error(str(e)) logging.error(str(e))
return response
raise e # For error handling.
except stripe.error.RateLimitError: except stripe.error.RateLimitError:
response.update( logging.error("Too many requests made to the API too quickly.")
{'error': "Too many requests made to the API too quickly"}) raise Exception(common_message)
return response
except stripe.error.InvalidRequestError as e: except stripe.error.InvalidRequestError as e:
logging.error(str(e)) logging.error(str(e))
response.update({'error': "Invalid parameters"}) raise Exception('Invalid parameters.')
return response
except stripe.error.AuthenticationError as e: except stripe.error.AuthenticationError as e:
# Authentication with Stripe's API failed # Authentication with Stripe's API failed
# (maybe you changed API keys recently) # (maybe you changed API keys recently)
logging.error(str(e)) logging.error(str(e))
response.update({'error': common_message}) raise Exception(common_message)
return response
except stripe.error.APIConnectionError as e: except stripe.error.APIConnectionError as e:
logging.error(str(e)) logging.error(str(e))
response.update({'error': common_message}) raise Exception(common_message)
return response
except stripe.error.StripeError as e: except stripe.error.StripeError as e:
# maybe send email # XXX: maybe send email
logging.error(str(e)) logging.error(str(e))
response.update({'error': common_message}) raise Exception(common_message)
return response
except Exception as e: except Exception as e:
# maybe send email # maybe send email
logging.error(str(e)) logging.error(str(e))
response.update({'error': common_message}) raise Exception(common_message)
return response
return handle_problems return handle_problems
# Convenience CC container, also used for serialization.
class CreditCard():
number = None
exp_year = None
exp_month = None
cvc = None
def __init__(self, number, exp_month, exp_year, cvc):
self.number=number
self.exp_year = exp_year
self.exp_month = exp_month
self.cvc = cvc
# Actual Stripe logic. # Actual Stripe logic.
def public_api_key():
return uncloud.secrets.STRIPE_PUBLIC_KEY
def get_customer_id_for(user): def get_customer_id_for(user):
try: try:
# .get() raise if there is no matching entry. # .get() raise if there is no matching entry.
return uncloud_pay.models.StripeCustomer.objects.get(owner=user).stripe_id return uncloud_pay.models.StripeCustomer.objects.get(owner=user).stripe_id
except ObjectDoesNotExist: except ObjectDoesNotExist:
# No entry yet - making a new one. # No entry yet - making a new one.
customer_request = create_customer(user.username, user.email) try:
if customer_request['error'] == None: customer = create_customer(user.username, user.email)
mapping = uncloud_pay.models.StripeCustomer.objects.create( uncloud_stripe_mapping = uncloud_pay.models.StripeCustomer.objects.create(
owner=user, owner=user, stripe_id=customer.id)
stripe_id=customer_request['response_object']['id'] return uncloud_stripe_mapping.stripe_id
) except Exception as e:
return mapping.stripe_id
else:
return None return None
@handle_stripe_error @handle_stripe_error
def create_card(customer_id, credit_card): def create_setup_intent(customer_id):
return stripe.Customer.create_source( return stripe.SetupIntent.create(customer=customer_id)
customer_id,
card={
'number': credit_card.number,
'exp_month': credit_card.exp_month,
'exp_year': credit_card.exp_year,
'cvc': credit_card.cvc
})
@handle_stripe_error @handle_stripe_error
def get_card(customer_id, card_id): def get_setup_intent(setup_intent_id):
return stripe.Customer.retrieve_source(customer_id, card_id) return stripe.SetupIntent.retrieve(setup_intent_id)
def get_payment_method(payment_method_id):
return stripe.PaymentMethod.retrieve(payment_method_id)
@handle_stripe_error @handle_stripe_error
def charge_customer(amount, customer_id, card_id): def charge_customer(amount, customer_id, card_id):
# Amount is in CHF but stripes requires smallest possible unit. # Amount is in CHF but stripes requires smallest possible unit.
# See https://stripe.com/docs/api/charges/create # https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount
adjusted_amount = int(amount * 100) adjusted_amount = int(amount * 100)
return stripe.Charge.create( return stripe.PaymentIntent.create(
amount=adjusted_amount, amount=adjusted_amount,
currency=CURRENCY, currency=CURRENCY,
customer=customer_id, customer=customer_id,
source=card_id) payment_method=card_id,
off_session=True,
confirm=True,
)
@handle_stripe_error @handle_stripe_error
def create_customer(name, email): def create_customer(name, email):

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>Error</title>
<style>
#content {
width: 400px;
margin: auto;
}
</style>
</head>
<body>
<div id="content">
<h1>Error</h1>
<p>{{ error }}</p>
</div>
</body>
</html>

View file

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<title>Stripe Card Registration</title>
<!-- https://stripe.com/docs/js/appendix/viewport_meta_requirements -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://js.stripe.com/v3/"></script>
<style>
#content {
width: 400px;
margin: auto;
}
#callback-form {
display: none;
}
</style>
</head>
<body>
<div id="content">
<h1>Registering Stripe Credit Card</h1>
<!-- Stripe form and messages -->
<span id="message"></span>
<form id="setup-form">
<div id="card-element"></div>
<button type='button' id="card-button">
Save
</button>
</form>
<!-- Dirty hack used for callback to API -->
<form id="callback-form" action="{{ callback }}" method="post"></form>
</div>
<!-- Enable Stripe from UI elements -->
<script>
var stripe = Stripe('{{ stripe_pk }}');
var elements = stripe.elements();
var cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>
<!-- Handle card submission -->
<script>
var cardButton = document.getElementById('card-button');
var messageContainer = document.getElementById('message');
var clientSecret = '{{ client_secret }}';
cardButton.addEventListener('click', function(ev) {
stripe.confirmCardSetup(
clientSecret,
{
payment_method: {
card: cardElement,
billing_details: {
},
},
}
).then(function(result) {
if (result.error) {
var message = document.createTextNode('Error:' + result.error.message);
messageContainer.appendChild(message);
} else {
// Return to API on success.
document.getElementById("callback-form").submit();
}
});
});
</script>
</body>
</html>

View file

@ -1,9 +1,12 @@
from django.shortcuts import render from django.shortcuts import render
from django.db import transaction from django.db import transaction
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import viewsets, permissions, status from rest_framework import viewsets, permissions, status, views
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.reverse import reverse
from rest_framework.decorators import renderer_classes
import json import json
@ -13,26 +16,7 @@ from datetime import datetime
import uncloud_pay.stripe as uncloud_stripe import uncloud_pay.stripe as uncloud_stripe
### ###
# Standard user views: # Payments and Payment Methods.
class BalanceViewSet(viewsets.ViewSet):
# here we return a number
# number = sum(payments) - sum(bills)
#bills = Bill.objects.filter(owner=self.request.user)
#payments = Payment.objects.filter(owner=self.request.user)
# sum_paid = sum([ amount for amount payments..,. ]) # you get the picture
# sum_to_be_paid = sum([ amount for amount bills..,. ]) # you get the picture
pass
class BillViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = BillSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Bill.objects.filter(owner=self.request.user)
class PaymentViewSet(viewsets.ReadOnlyModelViewSet): class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = PaymentSerializer serializer_class = PaymentSerializer
@ -48,19 +32,19 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self): def get_queryset(self):
return Order.objects.filter(owner=self.request.user) return Order.objects.filter(owner=self.request.user)
class PaymentMethodViewSet(viewsets.ModelViewSet): class PaymentMethodViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
def get_serializer_class(self): def get_serializer_class(self):
if self.action == 'create': if self.action == 'create':
return CreatePaymentMethodSerializer return CreatePaymentMethodSerializer
elif self.action == 'update':
return UpdatePaymentMethodSerializer
elif self.action == 'charge': elif self.action == 'charge':
return ChargePaymentMethodSerializer return ChargePaymentMethodSerializer
else: else:
return PaymentMethodSerializer return PaymentMethodSerializer
def get_queryset(self): def get_queryset(self):
return PaymentMethod.objects.filter(owner=self.request.user) return PaymentMethod.objects.filter(owner=self.request.user)
@ -70,6 +54,11 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
# Set newly created method as primary if no other method is.
if PaymentMethod.get_primary_for(request.user) == None:
serializer.validated_data['primary'] = True
if serializer.validated_data['source'] == "stripe":
# Retrieve Stripe customer ID for user. # Retrieve Stripe customer ID for user.
customer_id = uncloud_stripe.get_customer_id_for(request.user) customer_id = uncloud_stripe.get_customer_id_for(request.user)
if customer_id == None: if customer_id == None:
@ -77,21 +66,26 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
{'error': 'Could not resolve customer stripe ID.'}, {'error': 'Could not resolve customer stripe ID.'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR) status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# Register card under stripe customer. try:
credit_card = uncloud_stripe.CreditCard(**serializer.validated_data.pop('credit_card')) setup_intent = uncloud_stripe.create_setup_intent(customer_id)
card_request = uncloud_stripe.create_card(customer_id, credit_card) except Exception as e:
if card_request['error']: return Response({'error': str(e)},
return Response({'stripe_error': card_request['error']}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) status=status.HTTP_500_INTERNAL_SERVER_ERROR)
card_id = card_request['response_object']['id']
# Save payment method locally. payment_method = PaymentMethod.objects.create(
serializer.validated_data['stripe_card_id'] = card_request['response_object']['id'] owner=request.user,
payment_method = PaymentMethod.objects.create(owner=request.user, **serializer.validated_data) stripe_setup_intent_id=setup_intent.id,
**serializer.validated_data)
# We do not want to return the credit card details sent with the POST # TODO: find a way to use reverse properly:
# request. # https://www.django-rest-framework.org/api-guide/reverse/
output_serializer = PaymentMethodSerializer(payment_method) path = "payment-method/{}/register-stripe-cc".format(
return Response(output_serializer.data) payment_method.uuid)
stripe_registration_url = reverse('api-root', request=request) + path
return Response({'please_visit': stripe_registration_url})
else:
serializer.save(owner=request.user, **serializer.validated_data)
return Response(serializer.data)
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def charge(self, request, pk=None): def charge(self, request, pk=None):
@ -106,8 +100,96 @@ class PaymentMethodViewSet(viewsets.ModelViewSet):
except Exception as e: except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=True, methods=['get'], url_path='register-stripe-cc', renderer_classes=[TemplateHTMLRenderer])
def register_stripe_cc(self, request, pk=None):
payment_method = self.get_object()
if payment_method.source != 'stripe':
return Response(
{'error': 'This is not a Stripe-based payment method.'},
template_name='error.html.j2')
if payment_method.active:
return Response(
{'error': 'This payment method is already active'},
template_name='error.html.j2')
try:
setup_intent = uncloud_stripe.get_setup_intent(
payment_method.stripe_setup_intent_id)
except Exception as e:
return Response(
{'error': str(e)},
template_name='error.html.j2')
# TODO: find a way to use reverse properly:
# https://www.django-rest-framework.org/api-guide/reverse/
callback_path= "payment-method/{}/activate-stripe-cc/".format(
payment_method.uuid)
callback = reverse('api-root', request=request) + callback_path
# Render stripe card registration form.
template_args = {
'client_secret': setup_intent.client_secret,
'stripe_pk': uncloud_stripe.public_api_key,
'callback': callback
}
return Response(template_args, template_name='stripe-payment.html.j2')
@action(detail=True, methods=['post'], url_path='activate-stripe-cc')
def activate_stripe_cc(self, request, pk=None):
payment_method = self.get_object()
try:
setup_intent = uncloud_stripe.get_setup_intent(
payment_method.stripe_setup_intent_id)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# Card had been registered, fetching payment method.
print(setup_intent)
if setup_intent.payment_method:
payment_method.stripe_payment_method_id = setup_intent.payment_method
payment_method.save()
return Response({
'uuid': payment_method.uuid,
'activated': payment_method.active})
else:
error = 'Could not fetch payment method from stripe. Please try again.'
return Response({'error': error})
@action(detail=True, methods=['post'], url_path='set-as-primary')
def set_as_primary(self, request, pk=None):
payment_method = self.get_object()
payment_method.set_as_primary_for(request.user)
serializer = self.get_serializer(payment_method)
return Response(serializer.data)
### ###
# Admin views. # Bills and Orders.
class BillViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = BillSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Bill.objects.filter(owner=self.request.user)
def unpaid(self, request):
return Bill.objects.filter(owner=self.request.user, paid=False)
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Order.objects.filter(owner=self.request.user)
###
# Old admin stuff.
class AdminPaymentViewSet(viewsets.ModelViewSet): class AdminPaymentViewSet(viewsets.ModelViewSet):
serializer_class = PaymentSerializer serializer_class = PaymentSerializer