Payment flow

This commit is contained in:
Kumi 2021-04-18 16:46:57 +02:00
parent 5c8c570589
commit e85074dc9f
14 changed files with 162 additions and 38 deletions

View file

@ -16,4 +16,4 @@ class InquiryProcessForm(forms.ModelForm):
class Meta:
model = Inquiry
fields = ["destination_radius", "arrival", "min_nights", "budget", "adults", "children", "comment"]
fields = ["gateway"]

View file

@ -4,12 +4,14 @@ from django.contrib import messages
from django.urls import reverse
from django.contrib.gis.geos import Point
from django.contrib.gis.db.models.functions import Distance
from django.conf import settings
from public.mixins import InConstructionMixin
from partners.mixins import PartnerProfileRequiredMixin
from localauth.helpers import name_to_coords
from partners.models import Establishment
from payment.models import BillingAddress, Invoice, InvoiceItem, InvoicePayment
from clients.models import ClientProfile
from .models import Inquiry, Offer
from .forms import InquiryProcessForm
@ -59,7 +61,7 @@ class InquiryProcessView(InConstructionMixin, UpdateView):
return initial
def form_valid(self, form):
profile = self.request.user.clientprofile
profile, _ = ClientProfile.objects.get_or_create(user=self.request.user)
profile.first_name = form.cleaned_data["first_name"]
profile.last_name = form.cleaned_data["last_name"]
profile.street = form.cleaned_data["street"]
@ -69,12 +71,30 @@ class InquiryProcessView(InConstructionMixin, UpdateView):
profile.country = form.cleaned_data["country"]
profile.save()
form.instance.client = profile
return super().form_valid(form)
def form_invalid(self, form, *args, **kwargs):
for field in form:
for error in field.errors:
messages.error(self.request, f"{field.name}: {error}")
return redirect("/")
def get_success_url(self):
return reverse("auction:inquiry_payment", args=(self.object.uuid,))
class InquiryPaymentView(InConstructionMixin, View):
def get(self, request, *args, **kwargs):
inquiry = Inquiry.objects.get(uuid=kwargs["uuid"])
try:
invoice = inquiry.invoice
except Invoice.DoesNotExist:
invoice = Invoice.from_inquiry(inquiry)
payment_url = InvoicePayment.from_invoice(invoice, inquiry.gateway)
return redirect(payment_url)
class OfferSelectionView(InConstructionMixin, DetailView):
model = Inquiry

View file

@ -100,12 +100,17 @@ class PhoneMixin(models.Model):
class Meta:
abstract = True
class Profile(AddressMixin, PhoneMixin):
user = models.OneToOneField(User, models.CASCADE)
class PersonMixin(models.Model):
company = models.CharField("Firma", max_length=64, null=True, blank=True)
vat_id = models.CharField("UID-Nummer", max_length=32, null=True, blank=True)
first_name = models.CharField("Vorname", max_length=64)
last_name = models.CharField("Nachname", max_length=64)
class Meta:
abstract = True
class Profile(PersonMixin, AddressMixin, PhoneMixin):
user = models.OneToOneField(User, models.CASCADE)
verified = models.BooleanField(default=False)
enabled = models.BooleanField(default=True)

View file

@ -29,3 +29,9 @@ AWS_STORAGE_BUCKET_NAME = "AWS Bucket"
# Countries the app can be used in (currently no more than 5 due to Google Maps restrictions)
JOKER_COUNTRIES = ["AT"]
# Currency to use
CURRENCY_SYMBOL = ""
CURRENCY_CODE = "EUR"
CURRENCY_NAME = "Euro"

View file

@ -1,15 +1,30 @@
from payment.models import InvoicePayment, Invoice
from payment.signals import initiate_payment
from django.db import models
from django.urls import reverse_lazy
from django.dispatch import receiver
import uuid
class DemoInvoicePayment(InvoicePayment):
@property
def gateway(self):
return "Paypal"
return "Demo"
@staticmethod
def initiate(invoice):
self.objects.create(invoice=invoice, amount=invoice.amount, gateway_id=uuid.uuid4())
@classmethod
def initiate(cls, invoice):
payment = cls.objects.create(invoice=invoice, amount=invoice.balance * -1, gateway_id=uuid.uuid4())
invoice.generate_invoice()
return reverse_lazy("payment:status", args=[payment.uuid])
@property
def status(self):
return 0
@receiver(initiate_payment)
def from_signal(sender, **kwargs):
if kwargs["gateway"] == "demo":
return {"redirect": DemoInvoicePayment.initiate(kwargs["invoice"])}
else:
return {}

View file

@ -2,6 +2,7 @@ from django.db import models
from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.utils import timezone
from django.conf import settings
from dateutil.relativedelta import relativedelta
@ -13,27 +14,38 @@ from django_countries.fields import CountryField
from io import BytesIO
import uuid
import urllib.request
from dbsettings.functions import getValue
from .functions import invoice_upload_path
from .signals import initiate_payment
from auction.models import Inquiry
from localauth.models import PersonMixin, AddressMixin
class BillingAddress(models.Model):
class BillingAddress(PersonMixin, AddressMixin):
user = models.ForeignKey(get_user_model(), models.CASCADE)
company = models.CharField(max_length=64, null=True, blank=True)
vat_id = models.CharField(max_length=32, null=True, blank=True)
first_name = models.CharField(max_length=64)
last_name = models.CharField(max_length=64)
street = models.CharField(max_length=128)
city = models.CharField(max_length=64)
zip = models.CharField(max_length=16)
state = models.CharField(max_length=32, null=True, blank=True)
country = CountryField()
@classmethod
def from_profile(cls, profile):
return cls.objects.create(
company = profile.company,
vat_id = profile.vat_id,
first_name = profile.first_name,
last_name = profile.last_name,
street = profile.street,
city = profile.city,
zip = profile.zip,
state = profile.state,
country = profile.country,
user = profile.user
)
class Invoice(models.Model):
uuid = models.UUIDField(default=uuid.uuid4)
user = models.ForeignKey(get_user_model(), models.PROTECT)
billing_address = models.ForeignKey(BillingAddress, models.PROTECT)
currency = models.CharField(max_length=3)
@ -45,7 +57,7 @@ class Invoice(models.Model):
def price_net(self):
price = 0
for item in self.invoiceitem_set().all():
for item in self.invoiceitem_set.all():
price += item.net_total
return price
@ -67,15 +79,17 @@ class Invoice(models.Model):
return False
@property
def is_paid(self):
def balance(self):
paid_amount = 0
for payment in self.invoicepayment_set().all():
for payment in self.invoicepayment_set.all():
paid_amount += payment.amount
if paid_amount >= price_gross:
return True
return False
return paid_amount - self.price_gross
@property
def is_paid(self):
return self.balance >= 0
def generate_invoice(self):
output = BytesIO()
@ -116,21 +130,21 @@ class Invoice(models.Model):
"state": profile.state,
"country": profile.country,
"post_code": profile.zip,
"vat_tax_number": profile.vat_id,
"email": self.user.email,
}.items() if value}
doc.client_info = ClientInfo(**client_data)
for item in self.invoiceitem_set().all():
for item in self.invoiceitem_set.all():
doc.add_item(Item(item.name, item.description, item.count, item.net_each))
doc.set_item_tax_rate(self.tax_rate)
for payment in self.invoicepayment_set().all():
for payment in self.invoicepayment_set.all():
doc.add_transaction(Transaction(payment.gateway, payment.gateway_id, payment.timestamp, payment.amount))
bottom_tip = (f"{ self.payment_instructions }<br /><br />" if self.payment_instructions else "") + getValue("billing.bottom_tip", "")
bottom_tip = ("Kunden-UID: %s<br /><br />" % profile.vat_id) if profile.vat_id else ""
bottom_tip += (f"{ self.payment_instructions }<br /><br />" if self.payment_instructions else "") + getValue("billing.bottom_tip", "")
bottom_tip += "<br /><br />" if bottom_tip else ""
bottom_tip += "Rechnung erstellt: %s" % str(timezone.now())
@ -141,6 +155,26 @@ class Invoice(models.Model):
self.invoice = ContentFile(output.getvalue(), "invoice.pdf")
self.save()
@classmethod
def from_inquiry(cls, inquiry):
invoice = cls.objects.create(
user = inquiry.client.user,
billing_address = BillingAddress.from_profile(inquiry.client),
currency = settings.CURRENCY_CODE,
tax_rate = 0,
inquiry = inquiry
)
InvoiceItem.objects.create(
invoice = invoice,
name = "Sicherheitsleistung",
description = "Rückzahlbare Sicherheitsleistung zu JourneyJoker-Anfrage #%i" % inquiry.id,
count = 1,
net_each = inquiry.budget
)
return invoice
class InvoiceItem(models.Model):
invoice = models.ForeignKey(Invoice, models.CASCADE)
name = models.CharField(max_length=64)
@ -148,7 +182,12 @@ class InvoiceItem(models.Model):
count = models.IntegerField()
net_each = models.DecimalField(max_digits=11, decimal_places=2)
@property
def net_total(self):
return self.net_each * self.count
class InvoicePayment(PolymorphicModel):
uuid = models.UUIDField(default=uuid.uuid4)
invoice = models.ForeignKey(Invoice, models.PROTECT)
amount = models.DecimalField(max_digits=9, decimal_places=2)
gateway_id = models.CharField(max_length=256)
@ -158,6 +197,23 @@ class InvoicePayment(PolymorphicModel):
def gateway(self):
raise NotImplementedError("%s does not implement gateway" % type(self))
@property
def status(self):
raise NotImplementedError("%s does not implement status" % type(self))
@classmethod
def initiate(cls, invoice):
raise NotImplementedError("%s does not implement initiate()" % cls.__name__)
@classmethod
def from_invoice(cls, invoice, gateway):
if invoice.balance < 0:
responses = initiate_payment.send_robust(sender=cls, invoice=invoice, gateway=gateway)
for handler, response in responses:
try:
return response["redirect"]
except:
continue
return False

3
payment/signals.py Normal file
View file

@ -0,0 +1,3 @@
import django.dispatch
initiate_payment = django.dispatch.Signal()

View file

@ -1,8 +1,11 @@
from django.urls import path, include
from .views import PaymentStatusView
app_name = "payment"
urlpatterns = [
path('gateways/paypal/', include("payment.paypal.urls"), name="paypal"),
path('gateways/sepa/', include("payment.sepa.urls"), name="sepa"),
path('status/<slug:uuid>/', PaymentStatusView.as_view(), name="status"),
]

View file

@ -0,0 +1,11 @@
from django.views.generic import DetailView
from django.shortcuts import get_object_or_404
from .models import InvoicePayment
class PaymentStatusView(DetailView):
model = InvoicePayment
template_name = "payment/status.html"
def get_object(self):
return get_object_or_404(InvoicePayment, uuid=self.kwargs["uuid"])

View file

@ -1,3 +1,3 @@
function setPaymentMethod() {
$('#id_payment').val($(".nav-link.active")[0].href.split("#tab-")[1])
$('#id_gateway').val($(".nav-link.active")[0].href.split("#tab-")[1])
}

View file

@ -0,0 +1 @@
var language="DE";

View file

@ -90,7 +90,7 @@
<div class="col-12 col-md-12 col-lg-7 col-xl-8 content-side">
<form class="lg-booking-form" id="frm_booking" name="frm_booking" method="post" action="{% url "auction:inquiry_payment" object.uuid %}">
<form class="lg-booking-form" id="frm_booking" name="frm_booking" method="post" %}">
{% csrf_token %}
{% if not request.user.is_authenticated %}
<div class="lg-booking-form-heading">
@ -295,7 +295,7 @@
<label><input type="checkbox" {% if request.user.is_authenticated %}required{% else %}disabled{% endif %} id="id_terms" name="terms"> {% trans "Ich erkläre mich einverstanden mit den" %} <a href="#">{% trans "Allgemeinen Geschäftsbedingungen" %}</a> {% trans "und der" %} <a href="#">{% trans "Datenschutzerklärung" %}</a>.</label>
</div><!-- end checkbox -->
<div class="col-md-12 text-center" id="result_msg"></div>
<input type="hidden" id="id_payment" name="payment">
<input type="hidden" id="id_gateway" name="gateway">
<button type="submit" onclick="setPaymentMethod(); return true;" class="btn btn-orange" name="submit" id="submit">{% trans "Zahlung durchführen" %}</button>
</form>

View file

@ -340,7 +340,7 @@
</div>
<!-- Page Scripts Starts -->
<script>var language="{{ request.LANGUAGE_CODE }}";</script>
<script src="{% static "frontend/js/language.js" %}"></script>
<script src="{% static "vendor/js/jquery-3.3.1.min.js" %}"></script>
<script src="{% static "vendor/js/jquery.magnific-popup.min.js" %}"></script>
<script src="{% static "vendor/js/jquery.mCustomScrollbar.concat.min.js" %}"></script>

View file

@ -9,7 +9,7 @@
<div class="col-md-12 col-lg-8 col-lg-offset-2">
<div class="payment-success-text">
<h2>{% trans "Zahlung" %} {% if object.status == 0 %}{% trans "erfolgreich" %}{% elif object.status == -1 %}{% trans "autorisiert" %}{% endif %}</h2>
<p>{% blocktrans with currency=object.invoice.currency|upper amount=object.invoice.amount %}Die Zahlung über {{ currency }} {{ amount }} wurde{% endblocktrans %} {% if object.status == 0 %}{% trans "erfolgreich durchgeführt" %}{% elif object.status == -1 %}{% trans "autorisiert" %}{% endif %}</p>
<p>{% blocktrans with currency=object.invoice.currency|upper amount=object.amount %}Die Zahlung über {{ currency }} {{ amount }} wurde{% endblocktrans %} {% if object.status == 0 %}{% trans "erfolgreich durchgeführt" %}{% elif object.status == -1 %}{% trans "autorisiert" %}{% endif %}</p>
<span><i class="fa fa-check-circle"></i></span>
<div class="table-responsive">
@ -22,16 +22,20 @@
</tr>
</thead>
<tbody>
{% for line in object.invoice.invoiceitem_set.all %}
<tr>
<td><span><i class="fa fa-building"></i></span>{% trans "Gebot" %}</td>
<td>{{ object.invoice.destination_name }}<span class="t-date">{{ object.first_date|date:"SHORT_DATE_FORMAT" }} {{ object.last_date|date:"SHORT_DATE_FORMAT" }}</span></td>
<td>{{ object.invoice.currency|upper }} {{ object.invoice.amount }}</td>
<td><span><i class="fa fa-building"></i></span>{% if object.invoice.inquiry %}{% trans "Gebot" %}{% else %}{% trans "Gebühren" %}{% endif %}</td>
<td>{{ line.description }}</td>
<td>{{ object.invoice.currency|upper }} {{ line.net_total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{% url "auction:offer_selection" object.invoice.inquiry.uuid %}" class="btn btn-orange">Angebote erhalten!</a><br /><br />
<a href="{{ object.invoice.invoice.url }}" class="btn">PDF-Beleg herunterladen</a>
{% if object.status == -1 %}
<p>{% trans "Die Zahlung wurde autorisiert." %} <a href="/">{% trans "Was bedeutet das?" %}</a></p>
<p>{% trans "Die Zahlung wurde autorisiert." %} <a href="#">{% trans "Was bedeutet das?" %}</a></p>
{% endif %}
<p>{% trans "Die Zahlungsdaten wurden auch per E-Mail versendet." %}</p>
</div>