Source code for payments.views.players

import csv
from datetime import timedelta

import stripe
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone, dateformat

from accounts.models import User, TeamMate
from cobalt.settings import (
    STRIPE_SECRET_KEY,
    AUTO_TOP_UP_DEFAULT_AMT,
    AUTO_TOP_UP_MAX_AMT,
    GLOBAL_CURRENCY_SYMBOL,
    BRIDGE_CREDITS,
)
from logs.views import log_event
from notifications.views.core import contact_member
from payments.forms import MemberTransfer, ManualTopup
from payments.models import (
    MemberTransaction,
    PaymentStatic,
    StripeTransaction,
    UserPendingPayment,
)
from payments.views.admin import refund_stripe_transaction_sub
from payments.views.core import (
    get_balance,
    auto_topup_member,
    stripe_current_balance,
    TZ,
    statement_common,
)
from payments.views.payments_api import payment_api_interactive
from rbac.core import rbac_user_has_role
from rbac.views import rbac_forbidden
from utils.utils import cobalt_paginator


[docs] @login_required() def statement(request): """Member statement view. Basic view of statement showing transactions in a web page. Args: request - standard request object Returns: HTTPResponse """ (summary, club, balance, auto_button, events_list) = statement_common(request.user) things = cobalt_paginator(request, events_list) # Check for refund eligible items payment_static = PaymentStatic.objects.filter(active=True).last() ref_date = timezone.now() - timedelta(weeks=payment_static.stripe_refund_weeks) for thing in things: if ( thing.stripe_transaction and thing.stripe_transaction.stripe_receipt_url and thing.stripe_transaction.status != "Refunded" and balance - thing.amount >= 0.0 and thing.created_date > ref_date ): thing.show_refund = True # Show tour for this page? tour = request.GET.get("tour", None) return render( request, "payments/players/statement.html", { "things": things, "user": request.user, "summary": summary, "club": club, "balance": balance, "auto_button": auto_button, "auto_amount": request.user.auto_amount, "tour": tour, }, )
[docs] @login_required() def statement_csv(request, member_id=None): """Member statement view - csv download Generates a CSV of the statement. Args: request (HTTPRequest): standard request object member_id(int): id of member to view, defaults to logged in user Returns: HTTPResponse: CSV headed response with CSV statement data """ if member_id: if not rbac_user_has_role(request.user, "payments.global.view"): return rbac_forbidden(request, "payments.global.view") member = get_object_or_404(User, pk=member_id) else: member = request.user (summary, club, balance, auto_button, events_list) = statement_common(member) local_dt = timezone.localtime(timezone.now(), TZ) today = dateformat.format(local_dt, "Y-m-d H:i:s") response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = 'attachment; filename="statement.csv"' writer = csv.writer(response) writer.writerow([member.full_name, member.system_number, today]) writer.writerow( [ "Date", "Counterparty", "Reference", "Type", "Description", "Amount", "Balance", ] ) for row in events_list: counterparty = "" if row.other_member: counterparty = row.other_member if row.organisation: counterparty = row.organisation local_dt = timezone.localtime(row.created_date, TZ) writer.writerow( [ dateformat.format(local_dt, "Y-m-d H:i:s"), counterparty, row.reference_no, row.type, row.description, row.amount, row.balance, ] ) return response
[docs] @login_required() def statement_pdf(request): """Member statement view - csv download Generates a PDF of the statement. Args: request (HTTPRequest): standard request object Returns: HTTPResponse: PDF headed response with PDF statement data """ # (summary, club, balance, auto_button, events_list) = statement_common( # request # ) # pylint: disable=unused-variable # today = datetime.today().strftime("%-d %B %Y") # return render_to_pdf_response( # request, # "payments/statement_pdf.html", # { # "events": events_list, # "user": request.user, # "summary": summary, # "club": club, # "balance": balance, # "today": today, # }, # ) return
[docs] @login_required() def stripe_create_customer(request): """calls Stripe to register a customer. Creates a new customer entry with Stripe and sets this member's stripe_customer_id to match the customer created. Also sets the auto_amount for the member to the system default. Args: request (HTTPRequest): standard request object Returns: Nothing. """ stripe.api_key = STRIPE_SECRET_KEY customer = stripe.Customer.create( name=request.user, email=request.user.email, metadata={"cobalt_tran_type": "Auto"}, ) request.user.stripe_customer_id = customer.id request.user.auto_amount = AUTO_TOP_UP_DEFAULT_AMT request.user.save()
[docs] @login_required() def setup_autotopup(request): """view to sign up to auto top up. Creates Stripe customer if not already defined. Hands over to Stripe to process card. Args: request (HTTPRequest): standard request object Returns: HTTPResponse: Our page with Stripe code embedded. """ stripe.api_key = STRIPE_SECRET_KEY warn = "" # Already set up? if request.user.stripe_auto_confirmed == "On": try: paylist = stripe.PaymentMethod.list( customer=request.user.stripe_customer_id, type="card", ) except stripe.error.InvalidRequestError as error: log_event( user=request.user, severity="HIGH", source="Payments", sub_source="setup_autotopup", message="Stripe InvalidRequestError: %s" % error.error.message, ) stripe_create_customer(request) paylist = None except stripe.error.RateLimitError: log_event( user=request.user, severity="HIGH", source="Payments", sub_source="setup_autotopup", message="Stripe RateLimitError", ) except stripe.error.AuthenticationError: log_event( user=request.user, severity="CRITICAL", source="Payments", sub_source="setup_autotopup", message="Stripe AuthenticationError", ) except stripe.error.APIConnectionError: log_event( user=request.user, severity="HIGH", source="Payments", sub_source="setup_autotopup", message="Stripe APIConnectionError - likely network problems", ) except stripe.error.StripeError: log_event( user=request.user, severity="CRITICAL", source="Payments", sub_source="setup_autotopup", message="Stripe generic StripeError", ) if paylist: # if customer has a card associated card = paylist.data[0].card card_type = card.brand card_exp_month = card.exp_month card_exp_year = card.exp_year card_last4 = card.last4 warn = f"Changing card details will override your {card_type} card ending in {card_last4} \ with expiry {card_exp_month}/{card_exp_year}" else: stripe_create_customer(request) log_event( user=request.user, severity="INFO", source="Payments", sub_source="Auto top up", message="User setup auto top up with Stripe", ) balance = get_balance(request.user) topup = request.user.auto_amount return render( request, "payments/players/autotopup.html", {"warn": warn, "topup": topup, "balance": balance}, )
[docs] @login_required() def member_transfer(request): """view to transfer $ to another member This view allows a member to transfer money to another member. Args: request (HTTPRequest): standard request object Returns: HTTPResponse """ if request.method == "POST": form = MemberTransfer(request.POST, user=request.user) if form.is_valid(): description = form.cleaned_data["description"] amount = form.cleaned_data["amount"] other_member = form.cleaned_data["transfer_to"] return payment_api_interactive( request=request, description=description, amount=amount, member=request.user, other_member=other_member, payment_type="Member Transfer", route_code="M2M", ) else: print(form.errors) else: form = MemberTransfer(user=request.user) # get balance last_tran = MemberTransaction.objects.filter(member=request.user).last() balance = last_tran.balance if last_tran else "Nil" recents = ( MemberTransaction.objects.filter(member=request.user) .exclude(other_member=None) .values("other_member") .distinct() ) recent_transfer_to = [] for r in recents: member = User.objects.get(pk=r["other_member"]) recent_transfer_to.append(member) team_mates = TeamMate.objects.filter(user=request.user) for team_mate in team_mates: recent_transfer_to.append(team_mate.team_mate) # make unique - convert to set to be unique, then back to list to sort recent_transfer_to = list(set(recent_transfer_to)) recent_transfer_to.sort(key=lambda x: x.first_name) return render( request, "payments/players/member_transfer.html", {"form": form, "recents": recent_transfer_to, "balance": balance}, )
[docs] @login_required() def update_auto_amount(request): """Called by the auto top up page when a user changes the amount of the auto top up. The auto top up page has Stripe code on it so a standard form won't work for this. Instead we use a little Ajax code on the page to handle this. Args: request (HTTPRequest): standard request object Returns: HTTPResponse: "Successful" """ if request.method == "GET": amount = request.GET["amount"] request.user.auto_amount = amount request.user.save() log_event( user=request.user, severity="INFO", source="Payments", sub_source="Auto top up", message=f"User changed auto top up amount to {amount}", ) return HttpResponse("Successful")
[docs] @login_required() def manual_topup(request): """Page to allow credit card top up regardless of auto status. This page allows a member to add to their account using a credit card, they can do this even if they have already set up for auto top up. Args: request (HTTPRequest): standard request object Returns: HTTPResponse """ balance = get_balance(request.user) if request.method == "POST": form = ManualTopup(request.POST, balance=balance) if form.is_valid(): if form.cleaned_data["card_choice"] == "Existing": # Use Auto (return_code, msg) = auto_topup_member( request.user, topup_required=form.cleaned_data["amount"], payment_type="Manual Top Up", ) if return_code: # success messages.success(request, msg, extra_tags="cobalt-message-success") return redirect("payments:payments") else: # error messages.error(request, msg, extra_tags="cobalt-message-error") else: # Use Manual trans = StripeTransaction() trans.description = "Manual Top Up" trans.amount = form.cleaned_data["amount"] trans.member = request.user trans.save() msg = "Manual Top Up - Checkout" return render( request, "payments/players/checkout.html", {"trans": trans, "msg": msg}, ) # else: # print(form.errors) else: form = ManualTopup(balance=balance) return render( request, "payments/players/manual_topup.html", { "form": form, "balance": balance, "remaining_balance": AUTO_TOP_UP_MAX_AMT - balance, }, )
[docs] @login_required() def cancel_auto_top_up(request): """Cancel auto top up. Args: request (HTTPRequest): standard request object Returns: HTTPResponse """ if request.method == "POST": if request.POST.get("stop_auto"): request.user.auto_amount = None request.user.stripe_auto_confirmed = "Off" request.user.stripe_customer_id = None request.user.save() messages.info( request, "Auto top up disabled", extra_tags="cobalt-message-success" ) log_event( user=request.user, severity="INFO", source="Payments", sub_source="Auto top up", message="User disabled auto top up", ) return redirect("payments:payments") balance = get_balance(request.user) return render( request, "payments/players/cancel_autotopup.html", {"balance": balance} )
[docs] @login_required() def stripe_webpage_confirm(request, stripe_id): """User has been told by Stripe that transaction went through. This is called by the web page after Stripe confirms the transaction is approved. Because this originates from the client we do not trust it, but we do move the status to Pending unless it is already Confirmed (timing issues). Args: request(HTTPRequest): stasndard request object stripe_id(int): pk of stripe transaction Returns: Nothing. """ stripe = get_object_or_404(StripeTransaction, pk=stripe_id) if stripe.status == "Intent": print("Stripe status is intend - updating") stripe.status = "Pending" stripe.save() return HttpResponse("ok")
[docs] @login_required() def stripe_autotopup_confirm(request): """User has been told by Stripe that auto top up went through. This is called by the web page after Stripe confirms that auto top up is approved. Because this originates from the client we do not trust it, but we do move the status to Pending unless it is already Confirmed (timing issues). For manual payments we update the transaction, but for auto top up there is no transaction so we record this on the User object. Args: request(HTTPRequest): standard request object Returns: Nothing. """ if request.user.stripe_auto_confirmed == "Off": request.user.stripe_auto_confirmed = "Pending" request.user.save() return HttpResponse("ok")
[docs] @login_required() def stripe_autotopup_off(request): """Switch off auto top up This is called by the web page when a user submits new card details to Stripe. This is the latest point that we can turn it off in case the user aborts the change. Args: request(HTTPRequest): stasndard request object Returns: Nothing. """ request.user.stripe_auto_confirmed = "Off" request.user.save() return HttpResponse("ok")
[docs] @login_required() def refund_stripe_transaction(request, stripe_transaction_id): """Allows a user to refund a Stripe transaction Args: stripe_transaction_id: request (HTTPRequest): standard request object Returns: HTTPResponse """ stripe_item = get_object_or_404(StripeTransaction, pk=stripe_transaction_id) # Calculate how much refund is left in case already partly refunded stripe_item.refund_left = stripe_item.amount - stripe_item.refund_amount member_balance = get_balance(stripe_item.member) payment_static = PaymentStatic.objects.filter(active=True).last() balance_after = float(member_balance) - float(stripe_item.refund_left) bridge_credit_charge = float(stripe_item.refund_left) member_card_refund = ( bridge_credit_charge * (100.0 - float(payment_static.stripe_refund_percentage_charge)) / 100.0 ) # Is this allowed? if stripe_item.member != request.user: messages.error( request, "Action Prohibited - transaction is not yours", extra_tags="cobalt-message-error", ) return redirect("payments:statement") if not stripe_item.stripe_receipt_url: messages.error( request, "Invalid transaction for a refund", extra_tags="cobalt-message-error", ) return redirect("payments:statement") if stripe_item.status == "Refunded": messages.error( request, "Transaction already refunded", extra_tags="cobalt-message-error" ) return redirect("payments:statement") if balance_after < 0.0: messages.error( request, "Cannot refund. Balance will be negative", extra_tags="cobalt-message-error", ) return redirect("payments:statement") if stripe_current_balance() - bridge_credit_charge < 0.0: messages.error( request, "Cannot refund. We have insufficient funds available with Stripe. Please try again later.", extra_tags="cobalt-message-error", ) return redirect("payments:statement") ref_date = timezone.now() - timedelta(weeks=payment_static.stripe_refund_weeks) if stripe_item.created_date <= ref_date: messages.error( request, "Cannot refund. Transaction is too old.", extra_tags="cobalt-message-error", ) return redirect("payments:statement") if request.method == "POST": stripe_amount = int(member_card_refund * 100) stripe.api_key = STRIPE_SECRET_KEY try: rc = stripe.Refund.create( charge=stripe_item.stripe_reference, amount=stripe_amount, ) except stripe.error.InvalidRequestError as e: log_event( user=request.user, severity="HIGH", source="Payments", sub_source="User initiated refund", message=str(e), ) return render( request, "payments/admin/payments_refund_error.html", {"rc": e, "stripe_item": stripe_item}, ) if rc["status"] not in ["succeeded", "pending"]: log_event( user=request.user, severity="CRITICAL", source="Payments", sub_source="Admin refund", message=f"User Refund. Unknown status from stripe refund. Stripe Item:{stripe_item} Return Code{rc}", ) return render( request, "payments/admin/payments_refund_error.html", {"rc": rc, "stripe_item": stripe_item}, ) # Call atomic database update refund_stripe_transaction_sub( stripe_item, stripe_item.refund_left, "Card refund" ) # Notify member email_body = f"""You have requested to refund a card transaction. You will receive a refund of {GLOBAL_CURRENCY_SYMBOL}{member_card_refund:.2f} to your card.<br><br> Please note that It can take up to two weeks for the money to appear in your card statement.<br><br> Your {BRIDGE_CREDITS} account balance has been reduced to reflect this refund. You can check your new balance using the link below.<br><br> """ # send contact_member( member=stripe_item.member, msg="Card Refund - %s%s" % (GLOBAL_CURRENCY_SYMBOL, member_card_refund), contact_type="Email", html_msg=email_body, link="/payments", subject="Card Refund", link_text="View Statement", ) messages.success( request, "Refund Request Submitted", extra_tags="cobalt-message-success" ) return redirect("payments:statement") return render( request, "payments/players/refund_stripe_transaction.html", { "stripe_item": stripe_item, "payment_static": payment_static, "member_balance": member_balance, "balance_after": balance_after, "bridge_credit_charge": bridge_credit_charge, "member_card_refund": member_card_refund, }, )
[docs] @login_required() def pay_user_pending_payment(request, user_pending_payment_id): """Pay for an IOU to a club""" user_pending_payment = get_object_or_404( UserPendingPayment, pk=user_pending_payment_id ) # Add session if we can if user_pending_payment.session_entry: session = user_pending_payment.session_entry.session else: session = None return payment_api_interactive( request=request, member=request.user, description=user_pending_payment.description, amount=user_pending_payment.amount, organisation=user_pending_payment.organisation, route_code="UPP", route_payload=user_pending_payment_id, book_internals=True, session=session, )
[docs] def user_pending_payment_callback(status, user_pending_payment_id): """Callback for a user_pending_payment being made""" if status == "Success": user_pending_payment = UserPendingPayment.objects.filter( pk=user_pending_payment_id ).first() if not user_pending_payment: log_event( user="Stripe API", severity="CRITICAL", source="Payments", sub_source="user_pending_payment", message=f"Callback could not find matching UserPendingPayment for pk={user_pending_payment_id}", ) return user_pending_payment.delete()