Source code for payments.views.payments_api

import logging

from django.contrib import messages
from django.shortcuts import render, redirect
from django.urls import reverse

from cobalt.settings import (
    AUTO_TOP_UP_LOW_LIMIT,
    BRIDGE_CREDITS,
    GLOBAL_CURRENCY_SYMBOL,
)
from logs.views import log_event
from notifications.views.core import send_cobalt_email_with_template
import payments.views.core as payments_core
from payments.models import StripeTransaction

logger = logging.getLogger("cobalt")


[docs] def payment_api_interactive( request, member, description, amount, organisation=None, other_member=None, payment_type="Miscellaneous", next_url=None, route_code=None, route_payload=None, book_internals=True, session=None, ): """Payments API when we have an attached user. This will try to make a payment and if need be take the user to the Stripe payment screen to handle a manual payment. For auto top up users, or users with enough money, this is a synchronous process and we will return the next_url to the user. For manual payments, the Stripe process is asynchronous. We hand the user off to Stripe and only know if their payment worked when Stripe calls us back through the webhook. We use a route_code to know what function to call when the webhook is triggered and the route_payload is passed so the calling module knows what this refers to. args: request (HttpRequest): Standard request object description (str): text description of the payment amount (float): A positive amount is a charge, a negative amount is an incoming payment. member (User): User object related to the payment organisation (organisations.models.Organisation): linked organisation other_member (User): User object payment_type (str): description of payment next_url (str): where to take the user next route_code (str): used by the callback to know which function to call upon the payment going through route_payload (str): identifier to pass when making the callback book_internals (bool): sometimes the calling module wants to book the internal deals (not Stripe) themselves for example, event entry may be booking a whole team of entries as part of this so we only want the stripe transaction to go through and the call back will book all of the individual deals. Default is to have us book the internals too. session (club_sessions.models.Session): optional club_session.session to link payment to returns: HttpResponse - either the Stripe manual payment screen or the next_url """ if not next_url: # where to next next_url = reverse("dashboard:dashboard") # First try to make the payment without the user's involvement if payment_api_batch( member=member, description=description, amount=amount, organisation=organisation, other_member=other_member, payment_type=payment_type, book_internals=book_internals, session=session, ): logger.info(f"{request.user} paid {amount:.2f} for {description}") # Call the callback payments_core.callback_router( route_code=route_code, route_payload=route_payload ) # Return msg = _success_msg_for_user(amount, other_member, organisation) messages.success(request, msg, extra_tags="cobalt-message-success") return redirect(next_url) # Didn't work automatically, we need to get the user to pay manually balance = float(payments_core.get_balance(member)) amount = float(amount) # Create Stripe Transaction trans = StripeTransaction() trans.description = description trans.amount = amount - balance trans.member = member trans.route_code = route_code trans.route_payload = route_payload trans.linked_amount = amount trans.linked_member = other_member trans.linked_organisation = organisation trans.linked_transaction_type = payment_type trans.save() msg = _manual_payment_description(balance, amount, other_member, description) next_url_name = _next_url_name_from_url(next_url) logger.info( f"{request.user} handed to Stripe manual screen to pay {amount:.2f} for {description}" ) return render( request, "payments/players/checkout.html", { "trans": trans, "msg": msg, "next_url": next_url, "next_url_name": next_url_name, }, )
def _manual_payment_description(balance, amount, other_member, description): """Format the message correctly""" if other_member: # transfer to another member if balance > 0.0: return f"""Partial payment for transfer to {other_member} ({description}). <br> Also using your current balance of {GLOBAL_CURRENCY_SYMBOL}{balance:.2f} to make a total payment of {GLOBAL_CURRENCY_SYMBOL}{amount:.2f}. """ return f"Payment to {other_member} ({description})" if balance > 0.0: # Use existing balance return f"""Partial payment for {description}. <br> Also using your current balance of {GLOBAL_CURRENCY_SYMBOL}{balance:.2f} to make a total payment of {GLOBAL_CURRENCY_SYMBOL}{amount:.2f}. """ return f"Payment for: {description}" def _next_url_name_from_url(next_url): """Try to work out what to describe the next_url as""" # TODO: Move this to the calling function to provide as a parameter if next_url.find("events") >= 0: return "Events" elif next_url.find("dashboard") >= 0: return "Dashboard" elif next_url.find("payments") >= 0: return "your statement" return "Next" def _success_msg_for_user(amount, other_member, organisation): """Build message to show on user's next screen""" if other_member: return f"Payment of {amount:.2f} to {other_member} successful" if organisation: return f"Payment of {amount:.2f} to {organisation} successful" # Shouldn't get here return f"Payment of {amount:.2f} successful"
[docs] def payment_api_batch( member, description, amount, organisation=None, other_member=None, payment_type=None, book_internals=True, session=None, event=None, ): """This API is used by other parts of the system to make payments or fail. It will use existing funds or try to initiate an auto top up. Use payment_api_interactive() if you wish the user to be taken to the manual payment screen. We accept either an organisation as the counterpart for this payment or other_member. If the calling function wishes to book their own transactions they can pass us neither parameter. For Events, the booking of the internal transactions is done in the callback so that we can have individual transactions that are easier to map. The optional parameter book_internals handles this. If this is set to false then only the necessary Stripe transactions are executed by payment_api. args: description - text description of the payment amount - A positive amount is a charge, a negative amount is an incoming payment. member - User object related to the payment organisation - linked organisation other_member - User object payment_type - description of payment book_internals - sometimes the calling module wants to book the internal deals (not Stripe) themselves for example, event entry may be booking a whole team of entries as part of this so we only want the stripe transaction to go through and the call back will book all of the individual deals. Default is to have us book the internals too. session (club_sessions.models.Session): optional club_session.session to link payment to event (event.models.Event): optional event to link payment to returns: bool - success or failure """ if other_member and organisation: # one or the other, not both log_event( user="Stripe API", severity="CRITICAL", source="Payments", sub_source="payments_api", message="Received both other_member and organisation. Code Error.", ) return False balance = float(payments_core.get_balance(member)) amount = float(amount) if not payment_type: payment_type = "Miscellaneous" if amount <= balance: return _payment_with_sufficient_funds( member, amount, description, organisation, other_member, payment_type, balance, book_internals, session, event, ) else: return _payment_with_insufficient_funds( member, amount, description, organisation, other_member, payment_type, balance, book_internals, session, event, )
def _payment_with_sufficient_funds( member, amount, description, organisation, other_member, payment_type, balance, book_internals, session, event, ): """Handle a payment when the user has enough money to cover it""" # Record the internal transactions unless asked not to by calling module if book_internals: _update_account_entries_for_member_payment( member, amount, description, organisation, other_member, payment_type, session, event, ) # For member to member transfers, we notify both parties if other_member: notify_member_to_member_transfer(member, other_member, amount, description) # check for auto top up required - or manual top up low balance _check_for_auto_topup_or_low_balance(member, amount, balance, book_internals) return True def _update_account_entries_for_member_payment( member, amount, description, organisation, other_member, payment_type, session, event, ): """Make the actual updates to the tables for a member transaction. Doesn't check anything, just does the work.""" payments_core.update_account( member=member, amount=-amount, organisation=organisation, other_member=other_member, description=description, payment_type=payment_type, session=session, ) # If we got an organisation then make their payment too if organisation: payments_core.update_organisation( organisation=organisation, amount=amount, description=description, payment_type=payment_type, member=member, session=session, event=event, ) # If we got an other_member then make their payment too if other_member: payments_core.update_account( amount=amount, description=description, payment_type=payment_type, other_member=member, member=other_member, session=session, )
[docs] def notify_member_to_member_transfer(member, other_member, amount, description): """For member to member transfers we email both members to confirm""" logger.info(f"{member} transfer to {other_member} {amount}") # Member email email_body = f"""You have transferred {amount:.2f} credits into the {BRIDGE_CREDITS} account of {other_member}. <br><br> The description was: {description}. <br><br>Please contact us immediately if you do not recognise this transaction. <br><br>""" context = { "name": member.first_name, "title": f"Transfer to {other_member.full_name}", "email_body": email_body, "link": "/payments", "link_text": "View Statement", "box_colour": "#007bff", } send_cobalt_email_with_template( to_address=member.email, context=context, priority="now" ) # Other Member email email_body = f"""<b>{member}</b> has transferred {amount:.2f} credits into your {BRIDGE_CREDITS} account. <br><br> The description was: {description}. <br><br> Please contact {member.first_name} directly if you have any queries.<br><br> """ context = { "name": other_member.first_name, "title": f"Transfer from {member.full_name}", "email_body": email_body, "link": "/payments", "link_text": "View Statement", "box_colour": "#007bff", } send_cobalt_email_with_template( to_address=other_member.email, context=context, priority="now" )
def _check_for_auto_topup_or_low_balance(member, amount, balance, book_internals): """Check if member needs to be filled up after this transaction or notified of low balance. We don't worry about whether auto top up works or not. If it fails then the auto_topup_member function will take care of it. If book_internals is False (currently only for events which book their own member transactions so players get to see each one separately), then we don't check for low balance. That must be done by the calling module after the payment is made (or the balance and recent transactions will be wrong). """ # Low balance warning - for now make it the same as auto top up low_balance_limit = AUTO_TOP_UP_LOW_LIMIT # Auto top up if member.stripe_auto_confirmed == "On": if balance - amount < AUTO_TOP_UP_LOW_LIMIT: payments_core.auto_topup_member(member) # low balance else: if book_internals and balance - amount < low_balance_limit: payments_core.low_balance_warning(member) def _payment_with_insufficient_funds( member, amount, description, organisation, other_member, payment_type, balance, book_internals, session, event, ): """Handle a member not having enough money to pay""" # We can't do anything if auto top up is off if member.stripe_auto_confirmed != "On": return False topup_required = calculate_auto_topup_amount(member, amount, balance) return_code, _ = payments_core.auto_topup_member( member, topup_required=topup_required ) if return_code: # We should now have sufficient funds but lets check just to be sure balance = float(payments_core.get_balance(member)) if amount <= balance and book_internals: _update_account_entries_for_member_payment( member, amount, description, organisation, other_member, payment_type, session, event, ) # For member to member transfers, we notify both parties if other_member: notify_member_to_member_transfer( member, other_member, amount, description ) return True return False
[docs] def calculate_auto_topup_amount(member, amount, balance): """calculate required top up amount Generally top by the largest of amount and auto_amount, BUT if the balance after that will be low enough to require another top up then we top up by increments of the top up amount. """ topup_required = amount # normal top up if balance < AUTO_TOP_UP_LOW_LIMIT: topup_required = member.auto_amount if member.auto_amount >= amount else amount # check if we will still be under threshold if balance + topup_required - amount < AUTO_TOP_UP_LOW_LIMIT: min_required_amt = amount - balance + AUTO_TOP_UP_LOW_LIMIT n = int(min_required_amt / member.auto_amount) + 1 topup_required = member.auto_amount * n elif member.auto_amount >= amount: # use biggest topup_required = member.auto_amount return topup_required