diff --git a/src/squarepay/dispense.py b/src/squarepay/dispense.py index 9a50d1f9d466b00e4b44bc72db37c8f412a799eb..84e7f623db8e342b557c6b86c7ad581fba786083 100644 --- a/src/squarepay/dispense.py +++ b/src/squarepay/dispense.py @@ -31,25 +31,43 @@ def run_dispense(*args): return None return res -def run_dispense_return_error(*args): +def dispense_add_balance(user, amount, id): """ - A variant of run_dispense that returns a tuple (output, error). - On success, error is None. On failure, output is None and error is the exception instance. + Adds `amount` (cents) to a `user`s dispense account and returns a tuple (output, error). + If an `id` is specified, it will be appended to the dispense reason, which is useful to trace back from the dispense log. + On success, error is None. On failure, output is None and error is a String. """ if DISPENSE_BIN is None: return None, RuntimeError("DISPENSE_BIN is not defined") - cmd = (DISPENSE_BIN, ) + args - log.info("run_dispense_return_error: " + str(cmd)) + reason = "via portal" if id is None else f"via portal (id: {id})" + + cmd = (DISPENSE_BIN, "acct", user, "+"+str(amount), reason) + log.info("dispense_add_balance: " + str(cmd)) try: res = subprocess.check_output(cmd, timeout=4, universal_newlines=True) return res, None except CalledProcessError as e: log.warning("dispense returned error code %d, output: '%s'" % (e.returncode, e.output)) - return None, e + err = e except TimeoutExpired as e: log.error(e) - return None, e + err = e + + # if dispense returned an error, coerce it into a reasonable string for storage. + parts = [] + if hasattr(err, 'output') and err.output: + parts.append(f"output: {err.output}") + if hasattr(err, 'stderr') and err.stderr: + parts.append(f"stderr: {err.stderr}") + if hasattr(err, 'message'): + parts.append(f"message: {err.message}") + + err_str = str(err) + if err_str and err_str not in parts: + parts.append(f"str: {err_str}") + + return None, " | ".join(parts) if parts else err_str def get_item_price(itemid): diff --git a/src/squarepay/views.py b/src/squarepay/views.py index a7fcd6b0447bdec1133144e9b52896e23914657f..d257a95a5a034b1dfb1cf0ebfee7868078c04e14 100644 --- a/src/squarepay/views.py +++ b/src/squarepay/views.py @@ -17,7 +17,9 @@ from .models import MembershipPayment, CardPayment, TopUpPayment from . import payments from .payments import try_capture_payment, log from .dispense import get_item_price -from .dispense import run_dispense, run_dispense_return_error +from .dispense import run_dispense, dispense_add_balance + +SQUARE_FEE = 0.022 # 2.2% as of 2025 class PaymentFormMixin: template_name = 'payment_form.html' @@ -183,22 +185,9 @@ class CustomPaymentView(MemberAccessMixin, PaymentFormMixin, DetailView): def payment_success(self, payment): super().payment_success(payment) - _, err = run_dispense_return_error("acct", payment.username, "+"+str(payment.amount), "via portal") + _, err = dispense_add_balance(payment.username, payment.amount, payment.idempotency_key) - # if dispense returned an error, coerce it into a reasonable string and store it. if err is not None: - parts = [] - if hasattr(err, 'output') and err.output: - parts.append(f"output: {err.output}") - if hasattr(err, 'stderr') and err.stderr: - parts.append(f"stderr: {err.stderr}") - if hasattr(err, 'message'): - parts.append(f"message: {err.message}") - - err_str = str(err) - if err_str and err_str not in parts: - parts.append(f"str: {err_str}") - payment.potential_error = " | ".join(parts) if parts else err_str payment.save(update_fields=['potential_error']) messages.error(self.request, f"We could not sync your payment with dispense. No money has been added to your account. Contact committee with your username for a refund. (ID: {payment.idempotency_key}))") else: @@ -218,21 +207,23 @@ class TopUpFormView(MemberAccessMixin, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['balance'] = self.get_balance() + context['square_fee'] = round(SQUARE_FEE * 100, 2) return context def post(self, request, *args, **kwargs): amount = request.POST.get('amount') - if not amount or not amount.isdigit(): + + try: + amount_cents = int(float(amount) * 100) + except: messages.error(request, "Please enter a valid amount") return self.get(request, *args, **kwargs) - amount_cents = int(float(amount) * 100) if amount_cents < 100: # Minimum $1 messages.error(request, "Minimum top-up amount is $1.00") return self.get(request, *args, **kwargs) - # add fee (2.2% as of 2025) - amount_cents = math.ceil(amount_cents / (1 - 0.022)) + amount_cents = math.ceil(amount_cents / (1 - SQUARE_FEE)) idempotency = uuid.uuid1() diff --git a/src/templates/topup_form.html b/src/templates/topup_form.html index f87b182c29c91a337af0c0167f77cb5fea41b20d..c2a856c2a7fb30a3f31bbe82b832793a28392993 100644 --- a/src/templates/topup_form.html +++ b/src/templates/topup_form.html @@ -25,7 +25,7 @@ <div class="form-row"> <input type="submit" value="Continue to payment"> </div> - <em>Square charges 2.2% on API payments, which will be added to your transaction.</em> + <em>Square charges {{square_fee}}% on API payments, which will be added to your transaction.</em> </form> </div> {% endblock %}