diff --git a/gms/gms/settings.py b/gms/gms/settings.py index 4c34c04ea0f446a64e487601c9e15a40f1b2123e..e620d088986454e171b94d64bff9232c4ddfcda1 100644 --- a/gms/gms/settings.py +++ b/gms/gms/settings.py @@ -14,6 +14,7 @@ from gms.settings_local import * # Application definition INSTALLED_APPS = ( + 'sslserver', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -22,6 +23,7 @@ INSTALLED_APPS = ( 'django.contrib.staticfiles', 'memberdb', 'import_members', + 'squarepay', ) MIDDLEWARE = [ diff --git a/gms/gms/settings_local.example.py b/gms/gms/settings_local.example.py index f8984ca920cd8bfd05cc5fb8c02477bfb1b333a9..08919b4c64caaed4fb86cea24b51777bfeac1ad6 100644 --- a/gms/gms/settings_local.example.py +++ b/gms/gms/settings_local.example.py @@ -84,4 +84,9 @@ AUTH_LDAP_USER_FLAGS_BY_GROUP = { # superusers have all permissions (but also need staff to login to admin site) "is_superuser": ADMIN_ACCESS_QUERY, -} \ No newline at end of file +} + +# the Square app and location data (set to sandbox unless you want it to charge people) +SQUARE_APP_ID = 'maybe-sandbox-something-something-here' +SQUARE_LOCATION = 'CBASEDE-this-is-probably-somewhere-in-Sydney' +SQUARE_ACCESS_TOKEN = 'keep-this-very-secret' \ No newline at end of file diff --git a/gms/gms/urls.py b/gms/gms/urls.py index 1a35ea5e625c0b1d05709d3f48a6cc920405f489..1252591997000e343e058180c6ccf32587bdd9e1 100644 --- a/gms/gms/urls.py +++ b/gms/gms/urls.py @@ -5,4 +5,5 @@ from . import admin urlpatterns = [ path('', include('memberdb.urls')), path('admin/', admin.site.urls), + path('payment/', include('squarepay.urls')), ] diff --git a/gms/memberdb/static/main.css b/gms/memberdb/static/main.css index 7c4859e4ca3861caacdb97eb362594171cba56aa..306edcd1620aea08033817def251394b615aa6b3 100644 --- a/gms/memberdb/static/main.css +++ b/gms/memberdb/static/main.css @@ -26,7 +26,7 @@ body { transform: translate(-50%, -50%); } -.form-container { +.form-container, #form-container { background-color: #ffe; border: 2px solid #8fc; border-radius: 5px; diff --git a/gms/squarepay/__init__.py b/gms/squarepay/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c7f1520ed64128702e574269b609c4f258e954b1 --- /dev/null +++ b/gms/squarepay/__init__.py @@ -0,0 +1 @@ +default_app_config = 'squarepay.apps.SquarePayConfig' \ No newline at end of file diff --git a/gms/squarepay/admin.py b/gms/squarepay/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/gms/squarepay/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/gms/squarepay/apps.py b/gms/squarepay/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..9a6550339c2c805b2bd7638d72f39efa1b547989 --- /dev/null +++ b/gms/squarepay/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SquarePayConfig(AppConfig): + name = 'squarepay' + verbose_name = 'Square Payments' diff --git a/gms/squarepay/migrations/__init__.py b/gms/squarepay/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/gms/squarepay/models.py b/gms/squarepay/models.py new file mode 100644 index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91 --- /dev/null +++ b/gms/squarepay/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/gms/squarepay/static/sqpaymentform-basic.css b/gms/squarepay/static/sqpaymentform-basic.css new file mode 100644 index 0000000000000000000000000000000000000000..4bd6a854862a6b6237c07d384044e9491a2b723d --- /dev/null +++ b/gms/squarepay/static/sqpaymentform-basic.css @@ -0,0 +1,222 @@ +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body, html { + color: #373F4A; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: normal; +} + +iframe { + margin: 0; + padding: 0; + border: 0; +} + +button { + border: 0; +} + +hr { + height: 1px; + border: 0; + background-color: #CCC; +} + +fieldset { + margin: 0; + padding: 0; + border: 0; +} + + +/*#form-container { + position: relative; + width: 380px; + margin: 0 auto; + top: 50%; + transform: translateY(-50%); +}*/ + +.label { + font-size: 14px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.5; + text-transform: uppercase; +} + +.third { + float: left; + width: calc((100% - 32px) / 3); + padding: 0; + margin: 0 16px 16px 0; +} + +.third:last-of-type { + margin-right: 0; +} + +/* Define how SqPaymentForm iframes should look */ +.sq-input { + box-sizing: border-box; + border: 1px solid #E0E2E3; + border-radius: 4px; + outline-offset: -2px; + display: inline-block; + -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; +} + +/* Define how SqPaymentForm iframes should look when they have focus */ +.sq-input--focus { + border: 1px solid #4A90E2; + background-color: rgba(74,144,226,0.02); +} + + +/* Define how SqPaymentForm iframes should look when they contain invalid values */ +.sq-input--error { + border: 1px solid #E02F2F; + background-color: rgba(244,47,47,0.02); +} + +#sq-card-number { + margin-bottom: 16px; +} + +/* Customize the "Pay with Credit Card" button */ +.button-credit-card { + width: 100%; + height: 56px; + margin-top: 10px; + background: #4A90E2; + border-radius: 4px; + cursor: pointer; + display: block; + color: #FFFFFF; + font-size: 16px; + line-height: 24px; + font-weight: 700; + letter-spacing: 0; + text-align: center; + -webkit-transition: background .2s ease-in-out; + -moz-transition: background .2s ease-in-out; + -ms-transition: background .2s ease-in-out; + transition: background .2s ease-in-out; +} + +.button-credit-card:hover { + background-color: #4281CB; +} + + +/* Customize the Apple Pay on the Web button */ +#sq-apple-pay { + width: 100%; + height: 48px; + padding: 0; + margin: 24px 0 16px 0; + background-image: -webkit-named-image(apple-pay-logo-white); + background-color: black; + background-size: 100% 60%; + background-repeat: no-repeat; + background-position: 50% 50%; + border-radius: 4px; + cursor: pointer; + display: none; +} + +/* Customize the Masterpass button */ +#sq-masterpass { + width: 100%; + height: 48px; + padding: 0; + 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-repeat: no-repeat; + background-position: calc((100% - 32px) / 2) 50%; + border-radius: 4px; + cursor: pointer; + display: none; +} + +#sq-masterpass::after { + box-sizing: border-box; + float: right; + width: 32px; + height: 48px; + padding-top: 12px; + 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 { + min-width: 200px; + min-height: 40px; + padding: 11px 24px; + margin: 10px; + background-color: #000; + background-image: url(data:image/svg+xml,%3Csvg%20width%3D%22103%22%20height%3D%2217%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M.148%202.976h3.766c.532%200%201.024.117%201.477.35.453.233.814.555%201.085.966.27.41.406.863.406%201.358%200%20.495-.124.924-.371%201.288s-.572.64-.973.826v.084c.504.177.912.471%201.225.882.313.41.469.891.469%201.442a2.6%202.6%200%200%201-.427%201.47c-.285.43-.667.763-1.148%201.001A3.5%203.5%200%200%201%204.082%2013H.148V2.976zm3.696%204.2c.448%200%20.81-.14%201.085-.42.275-.28.413-.602.413-.966s-.133-.684-.399-.959c-.266-.275-.614-.413-1.043-.413H1.716v2.758h2.128zm.238%204.368c.476%200%20.856-.15%201.141-.448.285-.299.427-.644.427-1.036%200-.401-.147-.749-.441-1.043-.294-.294-.688-.441-1.183-.441h-2.31v2.968h2.366zm5.379.903c-.453-.518-.679-1.239-.679-2.163V5.86h1.54v4.214c0%20.579.138%201.013.413%201.302.275.29.637.434%201.085.434.364%200%20.686-.096.966-.287.28-.191.495-.446.644-.763a2.37%202.37%200%200%200%20.224-1.022V5.86h1.54V13h-1.456v-.924h-.084c-.196.336-.5.611-.91.826-.41.215-.845.322-1.302.322-.868%200-1.528-.259-1.981-.777zm9.859.161L16.352%205.86h1.722l2.016%204.858h.056l1.96-4.858H23.8l-4.41%2010.164h-1.624l1.554-3.416zm8.266-6.748h1.666l1.442%205.11h.056l1.61-5.11h1.582l1.596%205.11h.056l1.442-5.11h1.638L36.392%2013h-1.624L33.13%207.876h-.042L31.464%2013h-1.596l-2.282-7.14zm12.379-1.337a1%201%200%200%201-.301-.735%201%201%200%200%201%20.301-.735%201%201%200%200%201%20.735-.301%201%201%200%200%201%20.735.301%201%201%200%200%201%20.301.735%201%201%200%200%201-.301.735%201%201%200%200%201-.735.301%201%201%200%200%201-.735-.301zM39.93%205.86h1.54V13h-1.54V5.86zm5.568%207.098a1.967%201.967%200%200%201-.686-.406c-.401-.401-.602-.947-.602-1.638V7.218h-1.246V5.86h1.246V3.844h1.54V5.86h1.736v1.358H45.75v3.36c0%20.383.075.653.224.812.14.187.383.28.728.28.159%200%20.299-.021.42-.063.121-.042.252-.11.392-.203v1.498c-.308.14-.681.21-1.12.21-.317%200-.616-.051-.896-.154zm3.678-9.982h1.54v2.73l-.07%201.092h.07c.205-.336.511-.614.917-.833.406-.22.842-.329%201.309-.329.868%200%201.53.254%201.988.763.457.509.686%201.202.686%202.079V13h-1.54V8.688c0-.541-.142-.947-.427-1.218-.285-.27-.656-.406-1.113-.406-.345%200-.656.098-.931.294a2.042%202.042%200%200%200-.651.777%202.297%202.297%200%200%200-.238%201.029V13h-1.54V2.976zm32.35-.341v4.083h2.518c.6%200%201.096-.202%201.488-.605.403-.402.605-.882.605-1.437%200-.544-.202-1.018-.605-1.422-.392-.413-.888-.62-1.488-.62h-2.518zm0%205.52v4.736h-1.504V1.198h3.99c1.013%200%201.873.337%202.582%201.012.72.675%201.08%201.497%201.08%202.466%200%20.991-.36%201.819-1.08%202.482-.697.665-1.559.996-2.583.996h-2.485v.001zm7.668%202.287c0%20.392.166.718.499.98.332.26.722.391%201.168.391.633%200%201.196-.234%201.692-.701.497-.469.744-1.019.744-1.65-.469-.37-1.123-.555-1.962-.555-.61%200-1.12.148-1.528.442-.409.294-.613.657-.613%201.093m1.946-5.815c1.112%200%201.989.297%202.633.89.642.594.964%201.408.964%202.442v4.932h-1.439v-1.11h-.065c-.622.914-1.45%201.372-2.486%201.372-.882%200-1.621-.262-2.215-.784-.594-.523-.891-1.176-.891-1.96%200-.828.313-1.486.94-1.976s1.463-.735%202.51-.735c.892%200%201.629.163%202.206.49v-.344c0-.522-.207-.966-.621-1.33a2.132%202.132%200%200%200-1.455-.547c-.84%200-1.504.353-1.995%201.062l-1.324-.834c.73-1.045%201.81-1.568%203.238-1.568m11.853.262l-5.02%2011.53H96.42l1.864-4.034-3.302-7.496h1.635l2.387%205.749h.032l2.322-5.75z%22%20fill%3D%22%23FFF%22%2F%3E%3Cpath%20d%3D%22M75.448%207.134c0-.473-.04-.93-.116-1.366h-6.344v2.588h3.634a3.11%203.11%200%200%201-1.344%202.042v1.68h2.169c1.27-1.17%202.001-2.9%202.001-4.944%22%20fill%3D%22%234285F4%22%2F%3E%3Cpath%20d%3D%22M68.988%2013.7c1.816%200%203.344-.595%204.459-1.621l-2.169-1.681c-.603.406-1.38.643-2.29.643-1.754%200-3.244-1.182-3.776-2.774h-2.234v1.731a6.728%206.728%200%200%200%206.01%203.703%22%20fill%3D%22%2334A853%22%2F%3E%3Cpath%20d%3D%22M65.212%208.267a4.034%204.034%200%200%201%200-2.572V3.964h-2.234a6.678%206.678%200%200%200-.717%203.017c0%201.085.26%202.11.717%203.017l2.234-1.731z%22%20fill%3D%22%23FABB05%22%2F%3E%3Cpath%20d%3D%22M68.988%202.921c.992%200%201.88.34%202.58%201.008v.001l1.92-1.918c-1.165-1.084-2.685-1.75-4.5-1.75a6.728%206.728%200%200%200-6.01%203.702l2.234%201.731c.532-1.592%202.022-2.774%203.776-2.774%22%20fill%3D%22%23E94235%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E); + background-origin: content-box; + background-position: center; + background-repeat: no-repeat; + background-size: contain; + border: 0; + border-radius: 4px; + box-shadow: 0 1px 1px 0 rgba(60, 64, 67, 0.30), 0 1px 3px 1px rgba(60, 64, 67, 0.15); + outline: 0; + cursor: pointer; + 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; + font-size: 14px; + color: red; + font-weight: 500; + text-align: center; + opacity: 0.8; +} diff --git a/gms/squarepay/static/sqpaymentform-basic.js b/gms/squarepay/static/sqpaymentform-basic.js new file mode 100644 index 0000000000000000000000000000000000000000..7b264ce5760d9d8a7a33c783584db1c030019fd7 --- /dev/null +++ b/gms/squarepay/static/sqpaymentform-basic.js @@ -0,0 +1,207 @@ +/** Javascript to handle the Square payments form. + * Adapted from https://docs.connect.squareup.com/payments/sqpaymentform/setup */ + +/* + * function: requestCardNonce + * + * requestCardNonce is triggered when the "Pay with credit card" button is + * clicked + * + * Modifying this function is not required, but can be customized if you + * wish to take additional action when the form button is clicked. + */ +function requestCardNonce(event) { + + // Don't submit the form until SqPaymentForm returns with a nonce + event.preventDefault(); + + // Request a nonce from the SqPaymentForm object + paymentForm.requestCardNonce(); +} + +// Create and initialize a payment form object +var paymentForm = new SqPaymentForm({ + + // Initialize the payment form elements + applicationId: applicationId, + locationId: locationId, + inputClass: 'sq-input', + autoBuild: false, + + // Customize the CSS for SqPaymentForm iframe elements + inputStyles: [{ + fontSize: '16px', + fontFamily: 'Helvetica Neue', + padding: '16px', + color: '#373F4A', + backgroundColor: 'transparent', + lineHeight: '24px', + placeholderColor: '#CCC', + _webkitFontSmoothing: 'antialiased', + _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' + }, + + // Initialize the credit card placeholders + cardNumber: { + elementId: 'sq-card-number', + placeholder: '• • • • • • • • • • • • • • • •' + }, + cvv: { + elementId: 'sq-cvv', + placeholder: 'CVV' + }, + expirationDate: { + elementId: 'sq-expiration-date', + placeholder: 'MM/YY' + }, + postalCode: { + elementId: 'sq-postal-code', + placeholder: '6009' + }, + + // 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 () { + + return { + requestShippingAddress: false, + requestBillingInfo: false, + currencyCode: "AUD", + countryCode: "AU", + total: { + label: "University Computer Club Inc.", + amount: "100", + pending: false + }, + lineItems: [ + { + label: "Subtotal", + amount: "100", + pending: false + } + ] + } + }, + + /* 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:"); + errors.forEach(function (error) { + console.log(' ' + error.message); + alert(error.message); + }); + + return; + } + // Assign the nonce value to the hidden form field + document.getElementById('card-nonce').value = nonce; + + // POST the nonce form to the payment processing page + document.getElementById('nonce-form').submit(); + + }, + + /* callback function: unsupportedBrowserDetected + * Triggered when: the page loads and an unsupported browser is detected */ + unsupportedBrowserDetected: function () { + /* PROVIDE FEEDBACK TO SITE VISITORS */ + }, + + /* callback function: inputEventReceived + * Triggered when: visitors interact with SqPaymentForm iframe elements. */ + inputEventReceived: function (inputEvent) { + switch (inputEvent.eventType) { + case 'focusClassAdded': + /* HANDLE AS DESIRED */ + break; + case 'focusClassRemoved': + /* HANDLE AS DESIRED */ + break; + case 'errorClassAdded': + document.getElementById("error").innerHTML = "Please fix card information errors before continuing."; + break; + case 'errorClassRemoved': + /* HANDLE AS DESIRED */ + document.getElementById("error").style.display = "none"; + break; + case 'cardBrandChanged': + /* HANDLE AS DESIRED */ + break; + case 'postalCodeChanged': + /* HANDLE AS DESIRED */ + break; + } + }, + + /* callback function: paymentFormLoaded + * Triggered when: SqPaymentForm is fully loaded */ + paymentFormLoaded: function () { + /* HANDLE AS DESIRED */ + console.log("The form loaded!"); + } + } +}); + +document.addEventListener("DOMContentLoaded", function(event) { + if (SqPaymentForm.isSupportedBrowser()) { + paymentForm.build(); + paymentForm.recalculateSize(); + } +}); \ No newline at end of file diff --git a/gms/squarepay/templates/payment_form.html b/gms/squarepay/templates/payment_form.html new file mode 100644 index 0000000000000000000000000000000000000000..37d58066f03cbd0ae65ff6e69be8237a3e79db6d --- /dev/null +++ b/gms/squarepay/templates/payment_form.html @@ -0,0 +1,81 @@ +{% extends "base.html" %} +{% load static %} + +{% block extrahead %} +<!-- link to the SqPaymentForm library --> +<script type="text/javascript" src="https://js.squareup.com/v2/paymentform"></script> + +<script type="text/javascript"> +/* {# bring the location IDs into javascript so the next bits know about them #} */ +var applicationId = "{{ app_id }}"; +var locationId = "{{ loc_id }}"; +</script> + +<!-- link to the local SqPaymentForm initialization --> +<script type="text/javascript" src="{% static 'sqpaymentform-basic.js' %}"></script> + +<!-- link to the custom styles for SqPaymentForm --> +<link rel="stylesheet" type="text/css" href="{% static 'sqpaymentform-basic.css' %}"> +{% endblock %} + +{% block content %} +{% if response %} +<h1>{{ response }}</h1> +{% endif %} + +<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> + <span class="label">Card Number</span> + <div id="sq-card-number"></div> + + <div class="third"> + <span class="label">Expiration</span> + <div id="sq-expiration-date"></div> + </div> + + <div class="third"> + <span class="label">CVV</span> + <div id="sq-cvv"></div> + </div> + + <div class="third"> + <span class="label">Postal</span> + <div id="sq-postal-code"></div> + </div> + </fieldset> + + <button id="sq-creditcard" class="button-credit-card" onclick="requestCardNonce(event)">Pay $1.00</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 --> +</div> <!-- end #form-container --> +{% endblock %} \ No newline at end of file diff --git a/gms/squarepay/tests.py b/gms/squarepay/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/gms/squarepay/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/gms/squarepay/urls.py b/gms/squarepay/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..59c1b711f2de2402322302532cdfab7fc20ad612 --- /dev/null +++ b/gms/squarepay/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import PaymentFormView + +app_name = 'squarepay' +urlpatterns = [ + path('pay/', PaymentFormView.as_view(), name='pay'), +] \ No newline at end of file diff --git a/gms/squarepay/views.py b/gms/squarepay/views.py new file mode 100644 index 0000000000000000000000000000000000000000..959d6ea547e616e27faaa41c7515ad7b18fc1540 --- /dev/null +++ b/gms/squarepay/views.py @@ -0,0 +1,85 @@ +import uuid +from django.shortcuts import render +from django.views.generic.base import TemplateView +from django.contrib import messages +from django.conf import settings + +import squareconnect +from squareconnect.rest import ApiException +from squareconnect.apis.transactions_api import TransactionsApi +from squareconnect.apis.locations_api import LocationsApi + +class PaymentFormView(TemplateView): + """ + 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 + + + def __init__(self, *args, **kwargs): + self.amount = kwargs.pop('payment_amount', 100) + super().__init__(*args, **kwargs) + + # get things from settings + self.app_id = getattr(settings, 'SQUARE_APP_ID', 'bad_config') + self.loc_id = getattr(settings, 'SQUARE_LOCATION', 'bad_config') + self.access_key = getattr(settings, 'SQUARE_ACCESS_TOKEN') + + # do some square API client stuff + self.sqapi = squareconnect.ApiClient() + self.sqapi.configuration.access_token = self.access_key + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + 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.") + 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, + 'currency': 'AUD' + } + } + + try: + api_response = api_inst.charge(self.loc_id, body) + self.charge_response = api_response.transaction + except ApiException as e: + self.charge_response = None + messages.error(request, "Exception while calling TransactionApi::charge: %s" % e) + + return self.get(request) + + + + +