diff --git a/gms/gms/settings.py b/gms/gms/settings.py index e620d088986454e171b94d64bff9232c4ddfcda1..27aec658323ac20aee3e26def72a2297cf1b1093 100644 --- a/gms/gms/settings.py +++ b/gms/gms/settings.py @@ -33,6 +33,7 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'memberdb.views.MemberMiddleware', ] ROOT_URLCONF = 'gms.urls' @@ -139,5 +140,9 @@ LOGGING = { 'level': LOG_LEVEL, 'handlers': ['logfile', 'console'], }, + 'squarepay': { + 'level': LOG_LEVEL, + 'handlers': ['logfile', 'console'], + } }, -} \ No newline at end of file +} diff --git a/gms/gms/settings_local.example.py b/gms/gms/settings_local.example.py index 08919b4c64caaed4fb86cea24b51777bfeac1ad6..89de59e13e31173bb65d2d114a6d06f7f5bd7b62 100644 --- a/gms/gms/settings_local.example.py +++ b/gms/gms/settings_local.example.py @@ -89,4 +89,13 @@ AUTH_LDAP_USER_FLAGS_BY_GROUP = { # 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 +SQUARE_ACCESS_TOKEN = 'keep-this-very-secret' + +DISPENSE_BIN = '/usr/local/bin/dispense' + +# configure the email backend (see https://docs.djangoproject.com/en/2.1/topics/email/) +EMAIL_HOST = "secure.ucc.asn.au" +EMAIL_PORT = 465 +EMAIL_USE_SSL = True +EMAIL_HOST_USER = "uccportal" +EMAIL_HOST_PASSWORD = "changeme" diff --git a/gms/squarepay/dispense.py b/gms/squarepay/dispense.py new file mode 100644 index 0000000000000000000000000000000000000000..42b623461e8f51c3f55658829475f4150628ca87 --- /dev/null +++ b/gms/squarepay/dispense.py @@ -0,0 +1,48 @@ +""" +this file contains utilities for wrapping the opendispense2 CLI utility `dispense` +It is essentially a hack to avoid having to write an actual dispense client here. +""" + +import subprocess +from subprocess import CalledProcessError, TimeoutExpired +from django.conf import settings + +from .payments import log + +DISPENSE_BIN = getattr(settings, 'DISPENSE_BIN', None) + +if DISPENSE_BIN is None: + log.warning("DISPENSE_BIN is not defined! Lookups for prices will fail!") + +def run_dispense(*args): + if DISPENSE_BIN is None: + return None + + cmd = [DISPENSE_BIN] + args + log.info("run_dispense: " + cmd) + try: + # get a string containing the output of the program + res = subprocess.check_output(cmd, timeout=4, universal_newlines=True) + except CalledProcessError as e: + log.warning("dispense returned error code %d, output: '%s'" % (e.returncode, e.output)) + return None + except TimeoutExpired as e: + log.error(e) + return None + return res + +def get_item_price(itemid): + """ gets the price of the given dispense item in cents """ + if (itemid is None or itemid == ""): + return None + out = run_dispense('iteminfo', itemid) + if (out is None): + return 123456 + + s = out.split() # get something like ['pseudo:7', '25.00', 'membership', '(non-student', 'and', 'non-guild)'] + if (s[0] != itemid): + log.warning("get_item_price: got result for incorrect item: %s" + s) + return None + else: + # return the price as a number of cents + return int(float(s[0]) * 100) \ No newline at end of file diff --git a/gms/squarepay/payments.py b/gms/squarepay/payments.py new file mode 100644 index 0000000000000000000000000000000000000000..f64b6d9b824d94f5de311d1e64595c2163ede11f --- /dev/null +++ b/gms/squarepay/payments.py @@ -0,0 +1,62 @@ +""" +This file contains functions for dealing with payments (although that's fairly obvious) +""" +import logging + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.utils import timezone + +import squareconnect +from squareconnect.rest import ApiException +from squareconnect.apis.transactions_api import TransactionsApi + +log = logging.getLogger('squarepay') + +# load the configuration values +app_id = getattr(settings, 'SQUARE_APP_ID', None) +loc_id = getattr(settings, 'SQUARE_LOCATION', None) +access_key = getattr(settings, 'SQUARE_ACCESS_TOKEN', None) + +# make sure the configuration values exist +if (app_id is None) or (loc_id is None) or (access_key is None): + raise ImproperlyConfigured("Please define SQUARE_APP_ID, SQUARE_LOCATION and SQUARE_ACCESS_TOKEN in settings.py") + +# instantiate the global squareconnect ApiClient instance (only needs to be done once) +_sqapi_inst = squareconnect.ApiClient() +_sqapi_inst.configuration.access_token = access_key + +def get_transactions_api(): + return TransactionsApi(_sqapi_inst) + +def try_capture_payment(card_payment, nonce): + """ + attempt to charge the customer associated with the given card nonce (created by the PaymentForm in JS) + Note: this can be called multiple times with the same CardPayment instance but the customer will not + be charged multiple times (using the Square idempotency key feature) + Returns either True on success or False on failure. + """ + api_inst = get_transactions_api() + + request_body = { + 'idempotency_key': card_payment.idempotency_key, + 'card_nonce': nonce, + 'amount_money': { + 'amount': card_payment.amount, + 'currency': 'AUD' + } + } + + try: + api_response = api_inst.charge(loc_id, request_body) + set_paid(card_payment) + log.info("TransactionApi response without error, charge $%1.2f" % (float(card_payment.amount) / 100.0)) + return True + except ApiException as e: + log.error("Exception while calling TransactionApi::charge: %s" % e) + return False + +def set_paid(card_payment): + card_payment.is_paid = True + card_payment.date_paid = timezone.now() + card_payment.save()