Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
UCC
uccportal
Commits
99a511c7
Commit
99a511c7
authored
Jan 13, 2019
by
frekk
Browse files
use database to manage payments, remove digital wallet stuff
parent
04e2cfd1
Changes
7
Hide whitespace changes
Inline
Side-by-side
gms/squarepay/admin.py
View file @
99a511c7
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
gms/squarepay/models.py
View file @
99a511c7
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
gms/squarepay/static/squarepay.css
View file @
99a511c7
...
...
@@ -29,30 +29,36 @@ fieldset {
text-transform
:
uppercase
;
}
.
third
{
.
half
{
float
:
left
;
width
:
calc
((
100%
-
32
px
)
/
3
);
width
:
calc
((
100%
-
16
px
)
/
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
:
10
0%
60%
;
background-size
:
6
0%
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
;
}
gms/squarepay/static/squarepay.js
View file @
99a511c7
/** 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
(
"
E
ncountered errors:
"
);
console
.
log
(
"
cardNonceResponseReceived e
ncountered 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
gms/squarepay/templates/payment_form.html
View file @
99a511c7
{% 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
gms/squarepay/urls.py
View file @
99a511c7
...
...
@@ -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
gms/squarepay/views.py
View file @
99a511c7
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
)
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment