Payment flow
This commit is contained in:
parent
5c8c570589
commit
e85074dc9f
14 changed files with 162 additions and 38 deletions
|
@ -16,4 +16,4 @@ class InquiryProcessForm(forms.ModelForm):
|
|||
|
||||
class Meta:
|
||||
model = Inquiry
|
||||
fields = ["destination_radius", "arrival", "min_nights", "budget", "adults", "children", "comment"]
|
||||
fields = ["gateway"]
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
|
@ -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 {}
|
|
@ -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
3
payment/signals.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
import django.dispatch
|
||||
|
||||
initiate_payment = django.dispatch.Signal()
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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"])
|
|
@ -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])
|
||||
}
|
1
static/frontend/js/language.js
Normal file
1
static/frontend/js/language.js
Normal file
|
@ -0,0 +1 @@
|
|||
var language="DE";
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue