Merge pull request #666 from pcoder/task/5690/generic-payment-page

Task/5690/generic payment page
This commit is contained in:
Pcoder 2018-10-05 09:16:08 +02:00 committed by GitHub
commit e557a777c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 955 additions and 293 deletions

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-05 23:11+0000\n" "POT-Creation-Date: 2018-09-26 20:44+0000\n"
"PO-Revision-Date: 2018-03-30 23:22+0000\n" "PO-Revision-Date: 2018-03-30 23:22+0000\n"
"Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n" "Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -293,6 +293,9 @@ msgstr "Registrieren"
msgid "Billing Address" msgid "Billing Address"
msgstr "Rechnungsadresse" msgstr "Rechnungsadresse"
msgid "Make a payment"
msgstr ""
msgid "Your Order" msgid "Your Order"
msgstr "Deine Bestellung" msgstr "Deine Bestellung"
@ -336,9 +339,9 @@ msgid ""
"database." "database."
msgstr "" msgstr ""
"Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine " "Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine "
"Kreditkartendetails unten an. Die Bezahlung wird über " "Kreditkartendetails unten an. Die Bezahlung wird über <a href=\"https://"
"<a href=\"https://stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. " "stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. Wir speichern Deine "
"Wir speichern Deine Kreditkartendetails nicht in unserer Datenbank." "Kreditkartendetails nicht in unserer Datenbank."
msgid "" msgid ""
"Please fill in your credit card information below. We are using <a href=" "Please fill in your credit card information below. We are using <a href="
@ -395,12 +398,35 @@ msgstr "Bestellungsübersicht"
msgid "Product" msgid "Product"
msgstr "Produkt" msgstr "Produkt"
msgid "Amount"
msgstr ""
msgid "Description"
msgstr ""
msgid "Recurring"
msgstr ""
msgid "Subtotal" msgid "Subtotal"
msgstr "Zwischensumme" msgstr "Zwischensumme"
msgid "VAT" msgid "VAT"
msgstr "Mehrwertsteuer" msgstr "Mehrwertsteuer"
msgid ""
"By clicking \"Place order\" this plan will charge your credit card account "
"with %(total_price)s CHF/month"
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
"%(vm_total_price)s CHF pro Monat belastet"
msgid ""
"By clicking \"Place order\" this payment will charge your credit card "
"account with a one time amount of %(total_price)s CHF"
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
"%(vm_total_price)s CHF pro Monat belastet"
#, python-format #, python-format
msgid "" msgid ""
"By clicking \"Place order\" this plan will charge your credit card account " "By clicking \"Place order\" this plan will charge your credit card account "
@ -541,8 +567,33 @@ msgstr ""
#, python-brace-format #, python-brace-format
msgid "An error occurred while associating the card. Details: {details}" msgid "An error occurred while associating the card. Details: {details}"
msgstr "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: " msgstr ""
"{details}" "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
msgid "Confirmation of your payment"
msgstr ""
msgid " This is a monthly recurring plan."
msgstr ""
#, python-brace-format
msgid ""
"Hi {name},\n"
"\n"
"thank you for your order!\n"
"We have just received a payment of CHF {amount:.2f} from you.{recurring}\n"
"\n"
"Cheers,\n"
"Your Data Center Light team"
msgstr ""
msgid "Thank you for the payment."
msgstr "Danke für Deine Bestellung."
msgid ""
"You will soon receive a confirmation email of the payment. You can always "
"contact us at info@ungleich.ch for any question that you may have."
msgstr ""
msgid "Thank you for the order." msgid "Thank you for the order."
msgstr "Danke für Deine Bestellung." msgstr "Danke für Deine Bestellung."

View file

@ -180,3 +180,9 @@ footer .dcl-link-separator::before {
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;
} }
.input-no-border {
border: none !important;
background: transparent !important;
resize: none;
}

View file

@ -67,6 +67,18 @@
</div> </div>
<div class="dcl-payment-box"> <div class="dcl-payment-box">
<div class="dcl-payment-section"> <div class="dcl-payment-section">
{% if generic_payment_form %}
<h3>{%trans "Make a payment" %}</h3>
<hr class="top-hr">
<form role="form" id="generic-payment-form" method="post" action="" novalidate>
{% csrf_token %}
<input type="hidden" name="product" value="1" />
{% for field in generic_payment_form %}
{% bootstrap_field field type='fields'%}
{% endfor %}
<p class="text-danger">{{generic_payment_form.non_field_errors|striptags}}</p>
</form>
{% else %}
<h3>{%trans "Your Order" %}</h3> <h3>{%trans "Your Order" %}</h3>
<hr class="top-hr"> <hr class="top-hr">
<div class="dcl-payment-order"> <div class="dcl-payment-order">
@ -97,6 +109,7 @@
</p> </p>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</div> </div>
</div> </div>
<div class="dcl-payment-box"> <div class="dcl-payment-box">

View file

@ -47,6 +47,32 @@
<hr> <hr>
<div> <div>
<h4>{% trans "Order summary" %}</h4> <h4>{% trans "Order summary" %}</h4>
{% if generic_payment_details %}
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{{ generic_payment_details.product_name }}
</p>
<div class="row">
<div class="col-sm-6">
<p>
<span>{% trans "Amount" %}: </span>
<strong class="pull-right">CHF {{generic_payment_details.amount|floatformat:2|intcomma}}</strong>
</p>
{% if generic_payment_details.description %}
<p>
<span>{% trans "Description" %}: </span>
<strong class="pull-right">{{generic_payment_details.description}}</strong>
</p>
{% endif %}
{% if generic_payment_details.recurring %}
<p>
<span>{% trans "Recurring" %}: </span>
<strong class="pull-right">Yes</strong>
</p>
{% endif %}
</div>
</div>
{% else %}
<p> <p>
<strong>{% trans "Product" %}:</strong>&nbsp; <strong>{% trans "Product" %}:</strong>&nbsp;
{{ request.session.template.name }} {{ request.session.template.name }}
@ -102,6 +128,7 @@
</p> </p>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
<hr class="thin-hr"> <hr class="thin-hr">
</div> </div>
@ -109,7 +136,15 @@
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="row">
<div class="col-sm-8"> <div class="col-sm-8">
{% if generic_payment_details %}
{% if generic_payment_details.recurring %}
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/month{% endblocktrans %}.</div>
{% else %}
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this payment will charge your credit card account with a one time amount of {{total_price}} CHF{% endblocktrans %}.</div>
{% endif %}
{% else %}
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.</div> <div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.</div>
{% endif %}
</div> </div>
<div class="col-sm-4 order-confirm-btn text-right"> <div class="col-sm-4 order-confirm-btn text-right">
<button class="btn choice-btn" id="btn-create-vm" data-toggle="modal" data-target="#createvm-modal"> <button class="btn choice-btn" id="btn-create-vm" data-toggle="modal" data-target="#createvm-modal">
@ -151,16 +186,5 @@
<script type="text/javascript"> <script type="text/javascript">
{% trans "Some problem encountered. Please try again later." as err_msg %} {% trans "Some problem encountered. Please try again later." as err_msg %}
var create_vm_error_message = '{{err_msg|safe}}'; var create_vm_error_message = '{{err_msg|safe}}';
window.onload = function () {
var locale_dates = document.getElementsByClassName("locale_date");
var formats = ['YYYY-MM-DD hh:mm a']
var i;
for (i = 0; i < locale_dates.length; i++) {
var oldDate = moment.utc(locale_dates[i].textContent, formats);
var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f;
locale_dates[i].innerHTML = oldDate.local().format(outputFormat);
locale_dates[i].className += ' done';
}
};
</script> </script>
{%endblock%} {%endblock%}

View file

@ -6,17 +6,24 @@ from django.contrib import messages
from django.contrib.auth import login, authenticate from django.contrib.auth import login, authenticate
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, JsonResponse from django.http import HttpResponseRedirect, JsonResponse, Http404
from django.shortcuts import render from django.shortcuts import render
from django.utils.translation import get_language, ugettext_lazy as _ from django.utils.translation import get_language, ugettext_lazy as _
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from django.views.generic import FormView, CreateView, DetailView from django.views.generic import FormView, CreateView, DetailView
from hosting.forms import HostingUserLoginForm from hosting.forms import (
from hosting.models import HostingOrder, UserCardDetail HostingUserLoginForm, GenericPaymentForm, ProductPaymentForm
)
from hosting.models import (
HostingBill, HostingOrder, UserCardDetail, GenericProduct
)
from membership.models import CustomUser, StripeCustomer from membership.models import CustomUser, StripeCustomer
from opennebula_api.serializers import VMTemplateSerializer from opennebula_api.serializers import VMTemplateSerializer
from utils.forms import BillingAddressForm, BillingAddressFormSignup from utils.forms import (
BillingAddressForm, BillingAddressFormSignup, UserBillingAddressForm,
BillingAddress
)
from utils.hosting_utils import get_vm_price_with_vat from utils.hosting_utils import get_vm_price_with_vat
from utils.stripe_utils import StripeUtils from utils.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task from utils.tasks import send_plain_email_task
@ -242,19 +249,93 @@ class PaymentOrderView(FormView):
'login_form': HostingUserLoginForm(prefix='login_form'), 'login_form': HostingUserLoginForm(prefix='login_form'),
'billing_address_form': billing_address_form, 'billing_address_form': billing_address_form,
'cms_integration': get_cms_integration('default'), 'cms_integration': get_cms_integration('default'),
})
if ('generic_payment_type' in self.request.session and
self.request.session['generic_payment_type'] == 'generic'):
if 'product_id' in self.request.session:
product = GenericProduct.objects.get(
id=self.request.session['product_id']
)
context.update({'generic_payment_form': ProductPaymentForm(
prefix='generic_payment_form',
initial={'product_name': product.product_name,
'amount': float(product.get_actual_price()),
'recurring': product.product_is_subscription,
'description': product.product_description,
},
product_id=product.id
), })
else:
context.update({'generic_payment_form': GenericPaymentForm(
prefix='generic_payment_form',
), })
else:
context.update({
'vm_pricing': VMPricing.get_vm_pricing_by_name( 'vm_pricing': VMPricing.get_vm_pricing_by_name(
self.request.session['specs']['pricing_name'] self.request.session['specs']['pricing_name']
) )
}) })
return context return context
@cache_control(no_cache=True, must_revalidate=True, no_store=True) @cache_control(no_cache=True, must_revalidate=True, no_store=True)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'specs' not in request.session: if (('type' in request.GET and request.GET['type'] == 'generic')
or 'product_slug' in kwargs):
request.session['generic_payment_type'] = 'generic'
if 'generic_payment_details' in request.session:
request.session.pop('generic_payment_details')
request.session.pop('product_id')
if 'product_slug' in kwargs:
logger.debug("Product slug is " + kwargs['product_slug'])
try:
product = GenericProduct.objects.get(
product_slug=kwargs['product_slug']
)
except GenericProduct.DoesNotExist as dne:
logger.error(
"Product '{}' does "
"not exist".format(kwargs['product_slug'])
)
raise Http404()
request.session['product_id'] = product.id
elif 'specs' not in request.session:
return HttpResponseRedirect(reverse('datacenterlight:index')) return HttpResponseRedirect(reverse('datacenterlight:index'))
return self.render_to_response(self.get_context_data()) return self.render_to_response(self.get_context_data())
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if 'product' in request.POST:
# query for the supplied product
product = None
try:
product = GenericProduct.objects.get(
id=request.POST['generic_payment_form-product_name']
)
except GenericProduct.DoesNotExist as dne:
logger.error(
"The requested product '{}' does not exist".format(
request.POST['generic_payment_form-product_name']
)
)
except GenericProduct.MultipleObjectsReturned as mpe:
logger.error(
"There seem to be more than one product with "
"the name {}".format(
request.POST['generic_payment_form-product_name']
)
)
product = GenericProduct.objects.all(
product_name=request.
POST['generic_payment_form-product_name']
).first()
if product is None:
return JsonResponse({})
else:
return JsonResponse({
'amount': product.get_actual_price(),
'isSubscription': product.product_is_subscription
})
if 'login_form' in request.POST: if 'login_form' in request.POST:
login_form = HostingUserLoginForm( login_form = HostingUserLoginForm(
data=request.POST, prefix='login_form' data=request.POST, prefix='login_form'
@ -265,6 +346,13 @@ class PaymentOrderView(FormView):
auth_user = authenticate(email=email, password=password) auth_user = authenticate(email=email, password=password)
if auth_user: if auth_user:
login(self.request, auth_user) login(self.request, auth_user)
if 'product_slug' in kwargs:
return HttpResponseRedirect(
reverse('show_product',
kwargs={
'product_slug': kwargs['product_slug']}
)
)
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('datacenterlight:payment') reverse('datacenterlight:payment')
) )
@ -281,6 +369,49 @@ class PaymentOrderView(FormView):
data=request.POST, data=request.POST,
) )
if address_form.is_valid(): if address_form.is_valid():
# Check if we are in a generic payment case and handle the generic
# payment details form before we go on to verify payment
if ('generic_payment_type' in request.session and
self.request.session['generic_payment_type'] == 'generic'):
if 'product_id' in request.session:
generic_payment_form = ProductPaymentForm(
data=request.POST, prefix='generic_payment_form',
product_id=request.session['product_id']
)
else:
generic_payment_form = GenericPaymentForm(
data=request.POST, prefix='generic_payment_form'
)
if generic_payment_form.is_valid():
logger.debug("Generic payment form is valid.")
if 'product_id' in request.session:
product = generic_payment_form.product
else:
product = generic_payment_form.cleaned_data.get(
'product_name'
)
gp_details = {
"product_name": product.product_name,
"amount": generic_payment_form.cleaned_data.get(
'amount'
),
"recurring": generic_payment_form.cleaned_data.get(
'recurring'
),
"description": generic_payment_form.cleaned_data.get(
'description'
),
"product_id": product.id
}
request.session["generic_payment_details"] = (
gp_details
)
else:
logger.debug("Generic payment form invalid")
context = self.get_context_data()
context['generic_payment_form'] = generic_payment_form
context['billing_address_form'] = address_form
return self.render_to_response(context)
token = address_form.cleaned_data.get('token') token = address_form.cleaned_data.get('token')
if token is '': if token is '':
card_id = address_form.cleaned_data.get('card') card_id = address_form.cleaned_data.get('card')
@ -386,7 +517,8 @@ class OrderConfirmationView(DetailView):
@cache_control(no_cache=True, must_revalidate=True, no_store=True) @cache_control(no_cache=True, must_revalidate=True, no_store=True)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
context = {} context = {}
if 'specs' not in request.session or 'user' not in request.session: if (('specs' not in request.session or 'user' not in request.session)
and 'generic_payment_type' not in request.session):
return HttpResponseRedirect(reverse('datacenterlight:index')) return HttpResponseRedirect(reverse('datacenterlight:index'))
if 'token' in self.request.session: if 'token' in self.request.session:
token = self.request.session['token'] token = self.request.session['token']
@ -404,9 +536,19 @@ class OrderConfirmationView(DetailView):
card_detail = UserCardDetail.objects.get(id=card_id) card_detail = UserCardDetail.objects.get(id=card_id)
context['cc_last4'] = card_detail.last4 context['cc_last4'] = card_detail.last4
context['cc_brand'] = card_detail.brand context['cc_brand'] = card_detail.brand
if ('generic_payment_type' in request.session and
self.request.session['generic_payment_type'] == 'generic'):
context.update({
'generic_payment_details':
request.session['generic_payment_details'],
})
else:
context.update({
'vm': request.session.get('specs'),
})
context.update({ context.update({
'site_url': reverse('datacenterlight:index'), 'site_url': reverse('datacenterlight:index'),
'vm': request.session.get('specs'),
'page_header_text': _('Confirm Order'), 'page_header_text': _('Confirm Order'),
'billing_address_data': ( 'billing_address_data': (
request.session.get('billing_address_data') request.session.get('billing_address_data')
@ -416,11 +558,8 @@ class OrderConfirmationView(DetailView):
return render(request, self.template_name, context) return render(request, self.template_name, context)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
template = request.session.get('template')
specs = request.session.get('specs')
user = request.session.get('user') user = request.session.get('user')
stripe_api_cus_id = request.session.get('customer') stripe_api_cus_id = request.session.get('customer')
vm_template_id = template.get('id', 1)
stripe_utils = StripeUtils() stripe_utils = StripeUtils()
if 'token' in request.session: if 'token' in request.session:
@ -450,7 +589,8 @@ class OrderConfirmationView(DetailView):
'brand': card_details_response['brand'], 'brand': card_details_response['brand'],
'card_id': card_details_response['card_id'] 'card_id': card_details_response['card_id']
} }
stripe_customer_obj = StripeCustomer.objects.filter(stripe_id=stripe_api_cus_id).first() stripe_customer_obj = StripeCustomer.objects.filter(
stripe_id=stripe_api_cus_id).first()
if stripe_customer_obj: if stripe_customer_obj:
ucd = UserCardDetail.get_user_card_details( ucd = UserCardDetail.get_user_card_details(
stripe_customer_obj, card_details_response stripe_customer_obj, card_details_response
@ -504,6 +644,59 @@ class OrderConfirmationView(DetailView):
} }
return JsonResponse(response) return JsonResponse(response)
if ('generic_payment_type' in request.session and
self.request.session['generic_payment_type'] == 'generic'):
gp_details = self.request.session['generic_payment_details']
if gp_details['recurring']:
# generic recurring payment
logger.debug("Commencing a generic recurring payment")
else:
# generic one time payment
logger.debug("Commencing a one time payment")
charge_response = stripe_utils.make_charge(
amount=gp_details['amount'],
customer=stripe_api_cus_id
)
stripe_onetime_charge = charge_response.get('response_object')
# Check if the payment was approved
if not stripe_onetime_charge:
msg = charge_response.get('error')
messages.add_message(self.request, messages.ERROR, msg,
extra_tags='failed_payment')
response = {
'status': False,
'redirect': "{url}#{section}".format(
url=(reverse('datacenterlight:payment') +
"?type=generic"),
section='payment_error'),
'msg_title': str(_('Error.')),
'msg_body': str(
_('There was a payment related error.'
' On close of this popup, you will be redirected'
' back to the payment page.'))
}
return JsonResponse(response)
if ('generic_payment_type' not in request.session or
(request.session['generic_payment_details']['recurring'])):
if 'generic_payment_details' in request.session:
amount_to_be_charged = (
round(
request.session['generic_payment_details']['amount'],
2
)
)
plan_name = "generic-{0}-{1:.2f}".format(
request.session['generic_payment_details']['product_id'],
amount_to_be_charged
)
stripe_plan_id = plan_name
else:
template = request.session.get('template')
specs = request.session.get('specs')
vm_template_id = template.get('id', 1)
cpu = specs.get('cpu') cpu = specs.get('cpu')
memory = specs.get('memory') memory = specs.get('memory')
disk_size = specs.get('disk_size') disk_size = specs.get('disk_size')
@ -624,6 +817,123 @@ class OrderConfirmationView(DetailView):
'user': custom_user.id 'user': custom_user.id
}) })
if 'generic_payment_type' in request.session:
stripe_cus = StripeCustomer.objects.filter(
stripe_id=stripe_api_cus_id
).first()
billing_address = BillingAddress(
cardholder_name=billing_address_data['cardholder_name'],
street_address=billing_address_data['street_address'],
city=billing_address_data['city'],
postal_code=billing_address_data['postal_code'],
country=billing_address_data['country']
)
billing_address.save()
order = HostingOrder.create(
price=self.request
.session['generic_payment_details']['amount'],
customer=stripe_cus,
billing_address=billing_address,
vm_pricing=VMPricing.get_default_pricing()
)
# Create a Hosting Bill
HostingBill.create(customer=stripe_cus,
billing_address=billing_address)
# Create Billing Address for User if he does not have one
if not stripe_cus.user.billing_addresses.count():
billing_address_data.update({
'user': stripe_cus.user.id
})
billing_address_user_form = UserBillingAddressForm(
billing_address_data
)
billing_address_user_form.is_valid()
billing_address_user_form.save()
if self.request.session['generic_payment_details']['recurring']:
# Associate the given stripe subscription with the order
order.set_subscription_id(
stripe_subscription_obj.id, card_details_dict
)
else:
# Associate the given stripe charge id with the order
order.set_stripe_charge(stripe_onetime_charge)
# Set order status approved
order.set_approved()
order.generic_payment_description = gp_details["description"]
order.generic_product_id = gp_details["product_id"]
order.save()
# send emails
context = {
'name': user.get('name'),
'email': user.get('email'),
'amount': gp_details['amount'],
'description': gp_details['description'],
'recurring': gp_details['recurring'],
'product_name': gp_details['product_name'],
'product_id': gp_details['product_id'],
'order_id': order.id
}
email_data = {
'subject': (settings.DCL_TEXT +
" Payment received from %s" % context['email']),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['info@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in context.items()]),
'reply_to': [context['email']],
}
send_plain_email_task.delay(email_data)
email_data = {
'subject': _("Confirmation of your payment"),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': [user.get('email')],
'body': _("Hi {name},\n\n"
"thank you for your order!\n"
"We have just received a payment of CHF {amount:.2f}"
" from you.{recurring}\n\n"
"Cheers,\nYour Data Center Light team".format(
name=user.get('name'),
amount=gp_details['amount'],
recurring=(
_(' This is a monthly recurring plan.')
if gp_details['recurring'] else ''
)
)
),
'reply_to': ['info@ungleich.ch'],
}
send_plain_email_task.delay(email_data)
response = {
'status': True,
'redirect': (
reverse('hosting:orders')
if request.user.is_authenticated()
else reverse('datacenterlight:index')
),
'msg_title': str(_('Thank you for the payment.')),
'msg_body': str(
_('You will soon receive a confirmation email of the '
'payment. You can always contact us at '
'info@ungleich.ch for any question that you may have.')
)
}
for session_var in ['specs', 'template', 'billing_address',
'billing_address_data', 'card_id',
'token', 'customer', 'generic_payment_type',
'generic_payment_details', 'product_id']:
if session_var in request.session:
del request.session[session_var]
return JsonResponse(response)
user = { user = {
'name': custom_user.name, 'name': custom_user.name,
'email': custom_user.email, 'email': custom_user.email,

View file

@ -10,6 +10,7 @@ from django.conf import settings
from hosting.views import ( from hosting.views import (
RailsHostingView, DjangoHostingView, NodeJSHostingView RailsHostingView, DjangoHostingView, NodeJSHostingView
) )
from datacenterlight.views import PaymentOrderView
from membership import urls as membership_urls from membership import urls as membership_urls
from ungleich_page.views import LandingView from ungleich_page.views import LandingView
from django.views.generic import RedirectView from django.views.generic import RedirectView
@ -29,6 +30,9 @@ urlpatterns = [
url(r'^nosystemd/', include('nosystemd.urls', namespace="nosystemd")), url(r'^nosystemd/', include('nosystemd.urls', namespace="nosystemd")),
url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')), url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')),
url(r'^jsi18n/(?P<packages>\S+?)/$', i18n.javascript_catalog), url(r'^jsi18n/(?P<packages>\S+?)/$', i18n.javascript_catalog),
url(r'^product/(?P<product_slug>[\w-]+)/$',
PaymentOrderView.as_view(),
name='show_product'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(

View file

@ -1,8 +1,8 @@
from django.contrib import admin from django.contrib import admin
from .models import HostingOrder, HostingBill, HostingPlan from .models import HostingOrder, HostingBill, HostingPlan, GenericProduct
admin.site.register(HostingOrder) admin.site.register(HostingOrder)
admin.site.register(HostingBill) admin.site.register(HostingBill)
admin.site.register(HostingPlan) admin.site.register(HostingPlan)
admin.site.register(GenericProduct)

View file

@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from membership.models import CustomUser from membership.models import CustomUser
from utils.hosting_utils import get_all_public_keys from utils.hosting_utils import get_all_public_keys
from .models import UserHostingKey from .models import UserHostingKey, GenericProduct
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -52,6 +52,93 @@ class HostingUserLoginForm(forms.Form):
raise forms.ValidationError(_("User does not exist")) raise forms.ValidationError(_("User does not exist"))
class ProductModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return obj.product_name
class GenericPaymentForm(forms.Form):
product_name = ProductModelChoiceField(
queryset=GenericProduct.objects.all().order_by('product_name'),
empty_label=_("Choose a product"),
)
amount = forms.FloatField(
widget=forms.TextInput(
attrs={'placeholder': _('Amount in CHF'),
'readonly': 'readonly', }
),
max_value=999999,
min_value=1,
label=_('Amount in CHF')
)
recurring = forms.BooleanField(required=False,
label=_("Recurring monthly"), )
description = forms.CharField(
widget=forms.Textarea(attrs={'style': "height: 60px;"}),
required=False
)
class Meta:
model = GenericProduct
fields = ['product_name', 'amount', 'recurring', 'description']
def clean_amount(self):
amount = self.cleaned_data.get('amount')
if (float(self.cleaned_data.get('product_name').get_actual_price()) !=
amount):
raise forms.ValidationError(_("Amount field does not match"))
return amount
def clean_recurring(self):
recurring = self.cleaned_data.get('recurring')
if (self.cleaned_data.get('product_name').product_is_subscription !=
(True if recurring else False)):
raise forms.ValidationError(_("Recurring field does not match"))
return recurring
class ProductPaymentForm(GenericPaymentForm):
def __init__(self, *args, **kwargs):
product_id = kwargs.pop('product_id', None)
if product_id is not None:
self.product = GenericProduct.objects.get(id=product_id)
super(ProductPaymentForm, self).__init__(*args, **kwargs)
self.fields['product_name'] = forms.CharField(
widget=forms.TextInput(
attrs={'placeholder': _('Product name'),
'readonly': 'readonly'}
)
)
if self.product.product_is_subscription:
self.fields['amount'].label = "{amt} ({payment_type})".format(
amt=_('Amount in CHF'),
payment_type=_('Monthly subscription')
)
else:
self.fields['amount'].label = "{amt} ({payment_type})".format(
amt=_('Amount in CHF'),
payment_type=_('One time payment')
)
self.fields['recurring'].widget = forms.HiddenInput()
self.fields['product_name'].widget.attrs['class'] = 'input-no-border'
self.fields['amount'].widget.attrs['class'] = 'input-no-border'
self.fields['description'].widget.attrs['class'] = 'input-no-border'
def clean_amount(self):
amount = self.cleaned_data.get('amount')
if (self.product is None or
float(self.product.get_actual_price()) != amount):
raise forms.ValidationError(_("Amount field does not match"))
return amount
def clean_recurring(self):
recurring = self.cleaned_data.get('recurring')
if (self.product.product_is_subscription !=
(True if recurring else False)):
raise forms.ValidationError(_("Recurring field does not match"))
return recurring
class HostingUserSignupForm(forms.ModelForm): class HostingUserSignupForm(forms.ModelForm):
confirm_password = forms.CharField(label=_("Confirm Password"), confirm_password = forms.CharField(label=_("Confirm Password"),
widget=forms.PasswordInput()) widget=forms.PasswordInput())

View file

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2018-10-03 07:57
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import utils.mixins
class Migration(migrations.Migration):
dependencies = [
('hosting', '0047_auto_20180821_1240'),
]
operations = [
migrations.CreateModel(
name='GenericProduct',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product_name', models.CharField(default='', max_length=128)),
('product_slug', models.SlugField(help_text='An optional html id for the Section. Required to set as target of a link on page', unique=True)),
('product_description', models.CharField(default='', max_length=500)),
('created_at', models.DateTimeField(auto_now_add=True)),
('product_price', models.DecimalField(decimal_places=2, max_digits=6)),
('product_vat', models.DecimalField(decimal_places=4, default=0, max_digits=6)),
('product_is_subscription', models.BooleanField(default=True)),
],
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
),
migrations.AddField(
model_name='hostingorder',
name='generic_payment_description',
field=models.CharField(max_length=500, null=True),
),
migrations.AddField(
model_name='hostingorder',
name='generic_product',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hosting.GenericProduct'),
),
]

View file

@ -9,8 +9,8 @@ from django.utils.functional import cached_property
from datacenterlight.models import VMPricing, VMTemplate from datacenterlight.models import VMPricing, VMTemplate
from membership.models import StripeCustomer, CustomUser from membership.models import StripeCustomer, CustomUser
from utils.models import BillingAddress
from utils.mixins import AssignPermissionsMixin from utils.mixins import AssignPermissionsMixin
from utils.models import BillingAddress
from utils.stripe_utils import StripeUtils from utils.stripe_utils import StripeUtils
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -61,6 +61,31 @@ class OrderDetail(AssignPermissionsMixin, models.Model):
) )
class GenericProduct(AssignPermissionsMixin, models.Model):
permissions = ('view_genericproduct',)
product_name = models.CharField(max_length=128, default="")
product_slug = models.SlugField(
unique=True,
help_text=(
'An optional html id for the Section. Required to set as target '
'of a link on page'
)
)
product_description = models.CharField(max_length=500, default="")
created_at = models.DateTimeField(auto_now_add=True)
product_price = models.DecimalField(max_digits=6, decimal_places=2)
product_vat = models.DecimalField(max_digits=6, decimal_places=4, default=0)
product_is_subscription = models.BooleanField(default=True)
def __str__(self):
return self.product_name
def get_actual_price(self):
return round(
self.product_price + (self.product_price * self.product_vat), 2
)
class HostingOrder(AssignPermissionsMixin, models.Model): class HostingOrder(AssignPermissionsMixin, models.Model):
ORDER_APPROVED_STATUS = 'Approved' ORDER_APPROVED_STATUS = 'Approved'
ORDER_DECLINED_STATUS = 'Declined' ORDER_DECLINED_STATUS = 'Declined'
@ -80,7 +105,13 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
OrderDetail, null=True, blank=True, default=None, OrderDetail, null=True, blank=True, default=None,
on_delete=models.SET_NULL on_delete=models.SET_NULL
) )
generic_product = models.ForeignKey(
GenericProduct, null=True, blank=True, default=None,
on_delete=models.SET_NULL
)
generic_payment_description = models.CharField(
max_length=500, null=True
)
permissions = ('view_hostingorder',) permissions = ('view_hostingorder',)
class Meta: class Meta:
@ -89,11 +120,18 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
) )
def __str__(self): def __str__(self):
return ("Order Nr: #{} - VM_ID: {} - {} - {} - " hosting_order_str = ("Order Nr: #{} - VM_ID: {} - {} - {} - "
"Specs: {} - Price: {}").format( "Specs: {} - Price: {}").format(
self.id, self.vm_id, self.customer.user.email, self.created_at, self.id, self.vm_id, self.customer.user.email, self.created_at,
self.order_detail, self.price self.order_detail, self.price
) )
if self.generic_product_id is not None:
hosting_order_str += " - Generic Payment"
if self.stripe_charge_id is not None:
hosting_order_str += " - One time charge"
else:
hosting_order_str += " - Recurring"
return hosting_order_str
@cached_property @cached_property
def status(self): def status(self):

View file

@ -22,6 +22,39 @@ function setBrandIcon(brand) {
$(document).ready(function () { $(document).ready(function () {
$(function () {
$("select#id_generic_payment_form-product_name").change(function () {
var gp_form = $('#generic-payment-form');
$.ajax({
url: gp_form.attr('action'),
type: 'POST',
data: gp_form.serialize(),
init: function () {
console.log("init")
},
success: function (data) {
if (data.amount !== undefined) {
$("#id_generic_payment_form-amount").val(data.amount);
if (data.isSubscription) {
$('#id_generic_payment_form-recurring').prop('checked', true);
} else {
$('#id_generic_payment_form-recurring').prop('checked', false);
}
} else {
$("#id_generic_payment_form-amount").val('');
$('#id_generic_payment_form-recurring').prop('checked', false);
console.log("No product found")
}
},
error: function (xmlhttprequest, textstatus, message) {
$("#id_generic_payment_form-amount").val('');
$('#id_generic_payment_form-recurring').prop('checked', false);
console.log("Error fetching product")
}
});
})
});
$.ajaxSetup({ $.ajaxSetup({
beforeSend: function (xhr, settings) { beforeSend: function (xhr, settings) {
function getCookie(name) { function getCookie(name) {
@ -124,17 +157,35 @@ $(document).ready(function () {
$('#billing-form').submit(); $('#billing-form').submit();
} }
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length === 2) return parts.pop().split(";").shift();
}
function submitBillingForm() {
var billing_form = $('#billing-form');
var recurring_input = $('#id_generic_payment_form-recurring');
billing_form.append('<input type="hidden" name="generic_payment_form-product_name" value="' + $('#id_generic_payment_form-product_name').val() + '" />');
billing_form.append('<input type="hidden" name="generic_payment_form-amount" value="' + $('#id_generic_payment_form-amount').val() + '" />');
if (recurring_input.attr('type') === 'hidden') {
billing_form.append('<input type="hidden" name="generic_payment_form-recurring" value="' + (recurring_input.val() === 'True' ? 'on' : '') + '" />');
} else {
billing_form.append('<input type="hidden" name="generic_payment_form-recurring" value="' + (recurring_input.prop('checked') ? 'on' : '') + '" />');
}
billing_form.append('<input type="hidden" name="generic_payment_form-description" value="' + $('#id_generic_payment_form-description').val() + '" />');
billing_form.submit();
}
var $form_new = $('#payment-form-new'); var $form_new = $('#payment-form-new');
$form_new.submit(payWithStripe_new); $form_new.submit(payWithStripe_new);
function payWithStripe_new(e) { function payWithStripe_new(e) {
e.preventDefault(); e.preventDefault();
function stripeTokenHandler(token) { function stripeTokenHandler(token) {
// Insert the token ID into the form so it gets submitted to the server // Insert the token ID into the form so it gets submitted to the server
$('#id_token').val(token.id); $('#id_token').val(token.id);
$('#billing-form').submit(); submitBillingForm();
} }
@ -196,10 +247,10 @@ $(document).ready(function () {
} }
}); });
$('.credit-card-info .btn.choice-btn').click(function(){ $('.credit-card-info .btn.choice-btn').click(function () {
var id = this.dataset['id_card']; var id = this.dataset['id_card'];
$('#id_card').val(id); $('#id_card').val(id);
$('#billing-form').submit(); submitBillingForm();
}); });
}); });

View file

@ -134,3 +134,15 @@ $(document).ready(function() {
$(this).find('.modal-footer .btn').addClass('hide'); $(this).find('.modal-footer .btn').addClass('hide');
}) })
}); });
window.onload = function () {
var locale_dates = document.getElementsByClassName("locale_date");
var formats = ['YYYY-MM-DD hh:mm a'];
var i;
for (i = 0; i < locale_dates.length; i++) {
var oldDate = moment.utc(locale_dates[i].textContent, formats);
var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f;
locale_dates[i].innerHTML = oldDate.local().format(outputFormat);
locale_dates[i].className += ' done';
}
};

View file

@ -39,7 +39,7 @@
{% endif %} {% endif %}
</span> </span>
</p> </p>
{% if order %} {% if order and vm %}
<p> <p>
<strong>{% trans "Status" %}: </strong> <strong>{% trans "Status" %}: </strong>
<strong> <strong>
@ -93,6 +93,7 @@
<hr> <hr>
<div> <div>
<h4>{% trans "Order summary" %}</h4> <h4>{% trans "Order summary" %}</h4>
{% if vm %}
<p> <p>
<strong>{% trans "Product" %}:</strong>&nbsp; <strong>{% trans "Product" %}:</strong>&nbsp;
{% if vm.name %} {% if vm.name %}
@ -164,6 +165,32 @@
</p> </p>
</div> </div>
</div> </div>
{% else %}
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{{ product_name }}
</p>
<div class="row">
<div class="col-sm-6">
<p>
<span>{% trans "Amount" %}: </span>
<strong class="pull-right">{{order.price|floatformat:2|intcomma}} CHF</strong>
</p>
{% if order.generic_payment_description %}
<p>
<span>{% trans "Description" %}: </span>
<strong class="pull-right">{{order.generic_payment_description}}</strong>
</p>
{% endif %}
{% if order.subscription_id %}
<p>
<span>{% trans "Recurring" %}: </span>
<strong class="pull-right">{{order.created_at|date:'d'|ordinal}} {% trans "of every month" %}</strong>
</p>
{% endif %}
</div>
</div>
{% endif %}
</div> </div>
<hr class="thin-hr"> <hr class="thin-hr">
</div> </div>
@ -229,17 +256,6 @@
<script type="text/javascript"> <script type="text/javascript">
{% trans "Some problem encountered. Please try again later." as err_msg %} {% trans "Some problem encountered. Please try again later." as err_msg %}
var create_vm_error_message = '{{err_msg|safe}}'; var create_vm_error_message = '{{err_msg|safe}}';
window.onload = function () {
var locale_dates = document.getElementsByClassName("locale_date");
var formats = ['YYYY-MM-DD hh:mm a']
var i;
for (i = 0; i < locale_dates.length; i++) {
var oldDate = moment.utc(locale_dates[i].textContent, formats);
var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f;
locale_dates[i].innerHTML = oldDate.local().format(outputFormat);
locale_dates[i].className += ' done';
}
};
</script> </script>
{%endblock%} {%endblock%}

View file

@ -28,7 +28,7 @@
{% for order in orders %} {% for order in orders %}
<tr> <tr>
<td class="xs-td-inline" data-header="{% trans 'Order Nr.' %}">{{ order.id }}</td> <td class="xs-td-inline" data-header="{% trans 'Order Nr.' %}">{{ order.id }}</td>
<td class="xs-td-bighalf" data-header="{% trans 'Date' %}">{{ order.created_at | date:"M d, Y H:i" }}</td> <td class="xs-td-bighalf locale_date" data-header="{% trans 'Date' %}">{{ order.created_at | date:'Y-m-d h:i a' }}</td>
<td class="xs-td-smallhalf" data-header="{% trans 'Amount' %}">{{ order.price|floatformat:2|intcomma }}</td> <td class="xs-td-smallhalf" data-header="{% trans 'Amount' %}">{{ order.price|floatformat:2|intcomma }}</td>
<td class="text-right last-td"> <td class="text-right last-td">
<a class="btn btn-order-detail" href="{% url 'hosting:orders' order.pk %}">{% trans 'See Invoice' %}</a> <a class="btn btn-order-detail" href="{% url 'hosting:orders' order.pk %}">{% trans 'See Invoice' %}</a>

View file

@ -18,8 +18,8 @@
<h3>{%trans "Billing Address" %}</h3> <h3>{%trans "Billing Address" %}</h3>
<hr> <hr>
<form role="form" id="billing-form" method="post" action="" novalidate> <form role="form" id="billing-form" method="post" action="" novalidate>
{% for field in form %}
{% csrf_token %} {% csrf_token %}
{% for field in form %}
{% bootstrap_field field show_label=False type='fields' bound_css_class='' %} {% bootstrap_field field show_label=False type='fields' bound_css_class='' %}
{% endfor %} {% endfor %}
<div class="form-group text-right"> <div class="form-group text-right">

View file

@ -59,7 +59,8 @@ from .forms import (
) )
from .mixins import ProcessVMSelectionMixin, HostingContextMixin from .mixins import ProcessVMSelectionMixin, HostingContextMixin
from .models import ( from .models import (
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail,
GenericProduct
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -862,7 +863,15 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
raise Http404 raise Http404
if obj is not None: if obj is not None:
if obj.generic_product_id is not None:
# generic payment case
logger.debug("Generic payment case")
context['product_name'] = GenericProduct.objects.get(
id=obj.generic_product_id
).product_name
else:
# invoice for previous order # invoice for previous order
logger.debug("Invoice of VM order")
try: try:
vm_detail = VMDetail.objects.get(vm_id=obj.vm_id) vm_detail = VMDetail.objects.get(vm_id=obj.vm_id)
context['vm'] = vm_detail.__dict__ context['vm'] = vm_detail.__dict__