Commit 99a511c7 authored by frekk's avatar frekk
Browse files

use database to manage payments, remove digital wallet stuff

parent 04e2cfd1
from django.contrib import admin
from django.utils.html import format_html
# Register your models here.
from gms import admin
from .models import CardPayment
class CardPaymentAdmin(admin.ModelAdmin):
list_display = ['amount', 'url_field', 'date_created', 'is_paid']
readonly_fields = ['token', 'idempotency_key']
def url_field(self, obj):
return format_html('<a href="{}">Goto payment page</a>', obj.get_absolute_url())
url_field.short_description = 'Payment URL'
url_field.allow_tags = True
admin.site.register(CardPayment, CardPaymentAdmin)
\ No newline at end of file
import uuid
from django.core.management.utils import get_random_secret_key
from django.db import models
from django.urls import reverse
# Create your models here.
class CardPayment(models.Model):
token = models.CharField('Unique payment token', max_length=64, editable=False, default=get_random_secret_key)
description = models.CharField('Description', max_length=255)
amount = models.IntegerField('Amount in cents', null=False, blank=False)
idempotency_key = models.CharField('Square Transactions API idempotency key', max_length=64, editable=False, default=uuid.uuid1)
is_paid = models.BooleanField('Has been paid', blank=True, default=False)
completed_url = models.CharField('Redirect URL on success', max_length=255, null=True, editable=False)
dispense_synced = models.BooleanField('Payment lodged in dispense', blank=True, default=False)
date_created = models.DateTimeField('Date created', auto_now_add=True)
date_paid = models.DateTimeField('Date paid (payment captured)', null=True, blank=True)
def save(self, *args, **kwargs):
# generate a token by default. maybe possible using default=...?
if (self.token is None):
self.token = get_random_secret_key()
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('squarepay:pay', kwargs={ 'pk': self.pk, 'token': self.token })
\ No newline at end of file
......@@ -29,30 +29,36 @@ fieldset {
text-transform: uppercase;
}
.third {
.half {
float: left;
width: calc((100% - 32px) / 3);
width: calc((100% - 16px) / 2);
padding: 0;
margin: 0 16px 16px 0;
margin-right: 16px;
}
.third:last-of-type {
.half:last-of-type {
margin-right: 0;
}
/* Define how SqPaymentForm iframes should look */
.sq-input {
.sq-input,
#sq-expiration-date, #sq-card-number, #sq-cvv {
box-sizing: border-box;
border: 1px solid #E0E2E3;
border-radius: 4px;
outline-offset: -2px;
display: inline-block;
/*display: inline-block;*/
height: 56px;
-webkit-transition: border-color .2s ease-in-out, background .2s ease-in-out;
-moz-transition: border-color .2s ease-in-out, background .2s ease-in-out;
-ms-transition: border-color .2s ease-in-out, background .2s ease-in-out;
transition: border-color .2s ease-in-out, background .2s ease-in-out;
}
div#sq-expiration-date, div#sq-card-number, div#sq-cvv {
background-color: #f2f2f2;
}
/* Define how SqPaymentForm iframes should look when they have focus */
.sq-input--focus {
border: 1px solid #4A90E2;
......@@ -72,6 +78,22 @@ fieldset {
/* Customize the "Pay with Credit Card" button */
.button-credit-card {
width: 100%;
height: 56px;
margin-top: 10px;
background: #ccf;
border-radius: 4px;
cursor: default;
display: block;
color: #FFFFFF;
font-size: 16px;
line-height: 24px;
font-weight: 700;
letter-spacing: 0;
text-align: center;
}
.button-credit-card:enabled {
width: 100%;
height: 56px;
margin-top: 10px;
......@@ -91,11 +113,28 @@ fieldset {
transition: background .2s ease-in-out;
}
.button-credit-card:hover {
.button-credit-card:hover:enabled {
background-color: #4281CB;
}
#form-container {
position: relative;
width: 380px;
margin: 20px auto;
box-sizing: content-box;
border: 2px solid #eee;
background-color: white;
padding: 16px ;
}
.form-row {
margin: 0;
}
/*#content, #container, body {
height: 100%;
}
*/
/* Customize the Apple Pay on the Web button */
#sq-apple-pay {
width: 100%;
......@@ -120,7 +159,7 @@ fieldset {
margin: 24px 0 24px;
background-image: url("https://masterpass.com/dyn/img/acc/global/mp_mark_hor_wht.svg");
background-color: black;
background-size: 100% 60%;
background-size: 60% 60%;
background-repeat: no-repeat;
background-position: calc((100% - 32px) / 2) 50%;
border-radius: 4px;
......@@ -128,7 +167,7 @@ fieldset {
display: none;
}
#sq-masterpass::after {
/*#sq-masterpass::after {
box-sizing: border-box;
float: right;
width: 32px;
......@@ -137,7 +176,7 @@ fieldset {
content: url("data:image/svg+xml; utf8, <svg width='14' height='24' viewBox='0 0 14 24' xmlns='http://www.w3.org/2000/svg'><path d='M1.891 23.485c-.389 0-.778-.144-1.075-.436a1.46 1.46 0 0 1 0-2.102l9.141-8.944L.817 3.06a1.463 1.463 0 0 1 0-2.104 1.544 1.544 0 0 1 2.15 0l10.217 9.994a1.464 1.464 0 0 1 0 2.105L2.966 23.049a1.525 1.525 0 0 1-1.075.436' fill='#FFF' fill-rule='evenodd'/></svg>");
background-color: #E6761F;
border-radius: 0 4px 4px 0;
}
}*/
/* Customize the Google Pay button */
.button-google-pay {
......@@ -159,39 +198,6 @@ fieldset {
display: none;
}
#sq-walletbox {
margin: 0;
padding: 0;
text-align: center;
vertical-align: top;
width:100%;
height:123;
display:none;
}
#sq-walletbox-divider {
height: 24px;
margin: 24px 0 24px;
color: #CCC;
font-size: 14px;
font-weight: bold;
line-height: 24px;
letter-spacing: 0.5;
text-align: center;
text-transform: uppercase;
width: 100%;
}
#sq-walletbox-divider-label {
padding: 0 24px;
background-color: #FFF;
}
#sq-walletbox-divider hr {
margin-top: -12px;
}
#error {
width: 100%;
margin-top: 16px;
......@@ -200,4 +206,5 @@ fieldset {
font-weight: 500;
text-align: center;
opacity: 0.8;
display: none;
}
/** Javascript to handle the Square payments form.
* Adapted from https://docs.connect.squareup.com/payments/sqpaymentform/setup */
* Adapted from https://docs.connect.squareup.com/payments/sqpaymentform/setup
* SqPaymentForm documentation at https://docs.connect.squareup.com/api/paymentform */
/*
* function: requestCardNonce
......@@ -41,20 +42,10 @@ var paymentForm = new SqPaymentForm({
_mozOsxFontSmoothing: 'grayscale'
}],
// Initialize Apple Pay placeholder ID
applePay: {
elementId: 'sq-apple-pay'
},
// Initialize Masterpass placeholder ID
masterpass: {
elementId: 'sq-masterpass'
},
// Initialize Google Pay placeholder ID
googlePay: {
elementId: 'sq-google-pay'
},
/* digital wallets are only supported by Square for accounts in the US :'( */
applePay: false,
masterpass: false,
googlePay: false,
// Initialize the credit card placeholders
cardNumber: {
......@@ -69,43 +60,15 @@ var paymentForm = new SqPaymentForm({
elementId: 'sq-expiration-date',
placeholder: 'MM/YY'
},
postalCode: {
elementId: 'sq-postal-code',
placeholder: '6009'
},
/* postal code is not required in AU */
postalCode: false,
// SqPaymentForm callback functions
callbacks: {
/* callback function: methodsSupported
* Triggered when: the page is loaded. */
methodsSupported: function (methods) {
var walletBox = document.getElementById('sq-walletbox');
var applePayBtn = document.getElementById('sq-apple-pay');
var googlePayBtn = document.getElementById('sq-google-pay');
var masterpassBtn = document.getElementById('sq-masterpass');
// Only show the button if Apple Pay for Web is enabled
// Otherwise, display the wallet not enabled message.
if (methods.applePay === true) {
walletBox.style.display = 'block';
applePayBtn.style.display = 'block';
}
// Only show the button if Masterpass is enabled
// Otherwise, display the wallet not enabled message.
if (methods.masterpass === true) {
walletBox.style.display = 'block';
masterpassBtn.style.display = 'block';
}
// Only show the button if Google Pay is enabled
if (methods.googlePay === true) {
walletBox.style.display = 'block';
googlePayBtn.style.display = 'inline-block';
}
},
/* callback function: createPaymentRequest
* Triggered when: a digital wallet payment button is clicked. */
createPaymentRequest: function () {
/* createPaymentRequest: function () {
return {
requestShippingAddress: false,
......@@ -125,27 +88,16 @@ var paymentForm = new SqPaymentForm({
}
]
}
},
/* callback function: validateShippingContact
* Triggered when: a shipping address is selected/changed in a digital
* wallet UI that supports address selection. */
validateShippingContact: function (contact) {
var validationErrorObj;
/* ADD CODE TO SET validationErrorObj IF ERRORS ARE FOUND */
return validationErrorObj;
},
},*/
/* callback function: cardNonceResponseReceived
* Triggered when: SqPaymentForm completes a card nonce request */
cardNonceResponseReceived: function (errors, nonce, cardData) {
if (errors) {
// Log errors from nonce generation to the Javascript console
console.log("Encountered errors:");
console.log("cardNonceResponseReceived encountered errors:");
errors.forEach(function (error) {
console.log(' ' + error.message);
alert(error.message);
});
return;
......@@ -155,7 +107,6 @@ var paymentForm = new SqPaymentForm({
// POST the nonce form to the payment processing page
document.getElementById('nonce-form').submit();
},
/* callback function: unsupportedBrowserDetected
......@@ -167,6 +118,7 @@ var paymentForm = new SqPaymentForm({
/* callback function: inputEventReceived
* Triggered when: visitors interact with SqPaymentForm iframe elements. */
inputEventReceived: function (inputEvent) {
var e = document.getElementById("error");
switch (inputEvent.eventType) {
case 'focusClassAdded':
/* HANDLE AS DESIRED */
......@@ -175,11 +127,12 @@ var paymentForm = new SqPaymentForm({
/* HANDLE AS DESIRED */
break;
case 'errorClassAdded':
document.getElementById("error").innerHTML = "Please fix card information errors before continuing.";
e.innerHTML = "Please fix card information errors before continuing.";
e.style.display = "block";
break;
case 'errorClassRemoved':
/* HANDLE AS DESIRED */
document.getElementById("error").style.display = "none";
e.style.display = "none";
break;
case 'cardBrandChanged':
/* HANDLE AS DESIRED */
......@@ -195,13 +148,19 @@ var paymentForm = new SqPaymentForm({
paymentFormLoaded: function () {
/* HANDLE AS DESIRED */
console.log("The form loaded!");
btn = document.getElementById("sq-creditcard");
btn.disabled = false;
}
}
});
document.addEventListener("DOMContentLoaded", function(event) {
if (SqPaymentForm.isSupportedBrowser()) {
/* for testing, you can add ...?unsupported to the URL */
if (SqPaymentForm.isSupportedBrowser() && !window.location.href.includes("unsupported")) {
console.log("loading Square payment form...");
paymentForm.build();
paymentForm.recalculateSize();
} else {
console.log("not loading form: unsupported browser!");
}
});
\ No newline at end of file
{% extends "base.html" %}
{% load static %}
{% block title %}UCC Payment Gateway{% endblock %}
{% block extrahead %}
<!-- link to the SqPaymentForm library -->
<script type="text/javascript" src="https://js.squareup.com/v2/paymentform"></script>
......@@ -9,74 +11,67 @@
<script type="text/javascript">
var applicationId = "{{ app_id }}";
var locationId = "{{ loc_id }}";
var amount = {{ payment.amount }};
</script>
<script type="text/javascript" src="{% static 'squarepay.js' %}"></script>
<link rel="stylesheet" type="text/css" href="{% static 'squarepay.css' %}">
{% endblock %}
{% block content %}
{% if response %}
<h1>{{ response }}</h1>
{% endif %}
{% block content_title %}<h1>Pay with card</h1>{% endblock %}
{% block content %}
<div class="form-container">
<div class="form-header">
<span class="tips">{% block tips %}<b><i>Please fill in your card details below.</i></b>{% endblock %}</span>
{% if payment.is_paid %}<span class="tips">
<p><b>It appears you have already successfully attempted this payment.</b><br><br>
Note that you will not be charged twice if even you submit again.</p>
</span>{% endif %}
<noscript>
<span class="tips error">Please enable javascript to use the payment form.</span>
</noscript>
</div>
<!-- the element #form-container is used to create the Square payment form (hardcoded) -->
<div id="form-container">
<div id="sq-walletbox">
<!-- Placeholder for Apple Pay for Web button -->
<button id="sq-apple-pay"></button>
<!-- Placeholder for Masterpass button -->
<button id="sq-masterpass"></button>
<!-- Placeholder for Google Pay button-->
<button id="sq-google-pay" class="button-google-pay"></button>
<div id="sq-walletbox-divider">
<span id="sq-walletbox-divider-label">Or</span>
<hr />
</div>
</div>
<div id="sq-ccbox">
<!--
Be sure to replace the action attribute of the form with the path of
the Transaction API charge endpoint URL you want to POST the nonce to
(for example, "/process-card")
-->
<form id="nonce-form" novalidate action="{% url 'squarepay:pay' %}" method="post">
{% csrf_token %}
<fieldset>
<div class="form-row">
<span class="label">Card Number</span>
<div id="sq-card-number"></div>
</div>
<div id="sq-ccbox">
<!--
Be sure to replace the action attribute of the form with the path of
the Transaction API charge endpoint URL you want to POST the nonce to
(for example, "/process-card")
-->
<form id="nonce-form" novalidate action="#" method="post">
{% csrf_token %}
<div id="error"></div>
<div class="form-row">
<div class="third">
<span class="label">Expiration</span>
<div id="sq-expiration-date"></div>
<fieldset>
<div class="form-row">
<span class="label">Card Number</span>
<div id="sq-card-number"></div>
</div>
<div class="third">
<span class="label">CVV</span>
<div id="sq-cvv"></div>
</div>
<div class="form-row">
<div class="half">
<span class="label">Expiration</span>
<div id="sq-expiration-date"></div>
</div>
<div class="third">
<span class="label">Postcode</span>
<div id="sq-postal-code"></div>
<div class="half">
<span class="label">CVV</span>
<div id="sq-cvv"></div>
</div>
</div>
</div>
</fieldset>
<button id="sq-creditcard" class="button-credit-card" onclick="requestCardNonce(event)">Pay $1.00</button>
</fieldset>
<button id="sq-creditcard" class="button-credit-card" onclick="requestCardNonce(event)" disabled>Pay with card</button>
<div id="error"></div>
<!--
After a nonce is generated it will be assigned to this hidden input field.
-->
<input type="hidden" id="card-nonce" name="nonce">
</form>
</div> <!-- end #sq-ccbox -->
<!--
After a nonce is generated it will be assigned to this hidden input field.
-->
<input type="hidden" id="card-nonce" name="nonce">
</form>
</div> <!-- end #sq-ccbox -->
</div> <!-- end #form-container -->
</div>
</div>
{% endblock %}
\ No newline at end of file
......@@ -4,5 +4,5 @@ from .views import PaymentFormView
app_name = 'squarepay'
urlpatterns = [
path('pay/', PaymentFormView.as_view(), name='pay'),
path('pay/<int:pk>/<str:token>/', PaymentFormView.as_view(), name='pay'),
]
\ No newline at end of file
import uuid
from django.shortcuts import render
from django.views.generic.base import TemplateView
from django.views.generic.detail import DetailView
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.contrib import messages
from django.conf import settings
......@@ -9,26 +9,28 @@ from squareconnect.rest import ApiException
from squareconnect.apis.transactions_api import TransactionsApi
from squareconnect.apis.locations_api import LocationsApi
class PaymentFormView(TemplateView):
from .models import CardPayment
class PaymentFormView(DetailView):
"""
Handles the backend stuff for the Square payment form.
See https://docs.connect.squareup.com/payments/sqpaymentform/setup
"""
template_name = 'payment_form.html'
methods = ['get', 'post']
app_id = None
loc_id = None
access_key = None
amount = None
idempotency_key = None
sqapi = None
charge_response = None
app_id = None # square app ID (can be accessed by clients)
loc_id = None # square location key (can also be accessed by clients)
access_key = None # this is secret
sqapi = None # keep an instance of the Square API handy
model = CardPayment
slug_field = 'token'
slug_url_kwarg = 'token'
query_pk_and_slug = True
context_object_name = 'payment'
def __init__(self, *args, **kwargs):
self.amount = kwargs.pop('payment_amount', 100)
super().__init__(*args, **kwargs)
# get things from settings
......@@ -45,40 +47,36 @@ class PaymentFormView(TemplateView):
context.update({
'app_id': self.app_id,
'loc_id': self.loc_id,
'response': self.charge_response,
})
return context
def post(self, request, *args, **kwargs):
nonce = request.POST.get('nonce', None)
if (nonce is None or nonce == ""):
messages.error(request, "No nonce was passed or invalid card data.")
messages.error(request, "No nonce was generated! Please try reloading the page and submit again.")
return self.get(request)
api_inst = TransactionsApi(self.sqapi)
# this can be reused so we don't double charge the customer
if (self.idempotency_key is None):
self.idempotency_key = str(uuid.uuid1())
body = {
'idempotency_key': self.idempotency_key,
'card_nonce': nonce,
'amount_money': {
'amount': self.amount,
'amount': amount,
'currency': 'AUD'
}
}
try:
api_response = api_inst.charge(self.loc_id, body)
self.charge_response = api_response.transaction
messages.success(request, "Your payment of %1.2f was successful.", amount)
except ApiException as e:
self.charge_response = None
messages.error(request, "Exception while calling TransactionApi::charge: %s" % e)
return self.get(request)
# redirect to success URL
if (self.object.completed_url is None):
return self.get(request)
return HttpResponseRedirect(self.object.completed_url)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment