diff --git a/src/squarepay/urls.py b/src/squarepay/urls.py
index 8ae3290fb55b3cfe6c402346039778f944a9e2c7..ac263a8c8dc1e77052e54570983f66dba65649df 100644
--- a/src/squarepay/urls.py
+++ b/src/squarepay/urls.py
@@ -2,7 +2,10 @@ from django.urls import path
 
 from .views import PaymentFormView, MembershipPaymentView
 
+# note that other apps (like memberdb) may have dependencies via the reverse URL
+# using something like reverse('squarepay:pay_membership', ...)
+
 app_name = 'squarepay'
 urlpatterns = [
-    path('pay/<int:pk>/<str:token>/', MembershipPaymentView.as_view(), name='pay'),
+    path('pay/<int:pk>/', MembershipPaymentView.as_view(), name='pay_membership'),
 ]
diff --git a/src/squarepay/views.py b/src/squarepay/views.py
index 467aae1cde4dd31edde1073595d66ef41a9d05d2..ffcc26050796ab73889de77f82cf03898608d0ed 100644
--- a/src/squarepay/views.py
+++ b/src/squarepay/views.py
@@ -1,33 +1,25 @@
 import uuid
-from django.views.generic.base import RedirectView
+from django.views.generic.base import RedirectView, View
 from django.views.generic.detail import DetailView
 from django.http import HttpResponse, HttpResponseRedirect, Http404
+from django.core.exceptions import ObjectDoesNotExist
 from django.contrib import messages
 from django.conf import settings
 from django.urls import reverse
 from django.utils import timezone
 
+from memberdb.views import MemberAccessMixin
+from memberdb.models import Membership, MEMBERSHIP_TYPES
+
 from .models import MembershipPayment, CardPayment
 from . import payments
-from .payments import try_capture_payment, set_paid
-
-class PaymentFormView(DetailView):
-    """
-    Handles the backend stuff for the Square payment form.
-    See https://docs.connect.squareup.com/payments/sqpaymentform/setup
-    """
+from .payments import try_capture_payment
+from .dispense import get_item_price
 
+class PaymentFormMixin:
     template_name = 'payment_form.html'
-
-    model = CardPayment
-    slug_field = 'token'
-    slug_url_kwarg = 'token'
-    query_pk_and_slug = True
     context_object_name = 'payment'
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         amount = "$%1.2f AUD" % (self.get_object().amount / 100.0)
@@ -40,9 +32,10 @@ class PaymentFormView(DetailView):
 
     def payment_success(self, payment):
         set_paid(payment)
+        messages.success(request, "Your payment of $%1.2f was successful." % amount_aud)
 
-    def get_completed_url(self):
-        return self.get_object().get_absolute_url()
+    def payment_error(self, payment):
+        messages.error(request, "Your payment of $%1.2f was unsuccessful. Please try again later." % amount_aud)
 
     def post(self, request, *args, **kwargs):
         nonce = request.POST.get('nonce', None)
@@ -55,20 +48,66 @@ class PaymentFormView(DetailView):
 
         if try_capture_payment(card_payment, nonce):
             payment_success(card_payment)
-            messages.success(request, "Your payment of $%1.2f was successful." % amount_aud)
         else:
-            messages.error(request, "Your payment of $%1.2f was unsuccessful. Please try again later." % amount_aud)
-        
+            payment_error(card_payment)
+
         # redirect to success URL, or redisplay the form with a success message if none is given
         return HttpResponseRedirect(self.get_completed_url())
 
-class MembershipPaymentView(PaymentFormView):
-    model = MembershipPayment
+class PaymentFormView(DetailView, PaymentFormMixin):
+    """
+    Handles the backend stuff for the Square payment form.
+    See https://docs.connect.squareup.com/payments/sqpaymentform/setup
+    Note: currently unused. see MembershipPaymentView
+    """
+    model = CardPayment
+    slug_field = 'token'
+    slug_url_kwarg = 'token'
+    query_pk_and_slug = True
+
+    def get_completed_url(self):
+        return self.get_object().get_absolute_url()
+
+class MembershipPaymentView(MemberAccessMixin, PaymentFormMixin, DetailView):
+    """ displays the payment form appropriate for the given membership ID for the currently logged in member """
+
+    def get_object(self):
+        """ return the appropriate payment for the current membership, or None if no payment should be made """
+        if self.request.member is None:
+            raise Http404("no member record associated with current session")
+
+        try:
+            # find the membership record we are dealing with
+            ms = Membership.objects.get(
+                id = self.kwargs['pk'], # get the membership with the given ID
+                member = self.request.member,
+                date_paid__exact = None, # make sure membership itself is not marked as paid
+            )
+
+            # try to find a corresponding MembershipPayment record which has not been paid
+            payment = MembershipPayment.objects.get(
+                date_paid = None, # CardPayment.date_paid
+                is_paid = False, # CardPayment.is_paid
+                membership = ms, # MembershipPayment.membership
+                membership__date_paid__exact = None, # MembershipPayment.membership.date_paid
+            )
+        except Membership.DoesNotExist as e:
+            # no unpaid membership found, return
+            return None
+        except MembershipPayment.DoesNotExist as e:
+            # found an unpaid membership, but no payment record exists yet
+            messages.success(self.request, "Created payment record for as-yet unpaid membership")
+            return create_membership_payment(ms)
+        return payment
 
     def dispatch(self, request, *args, **kwargs):
+        self.request = request
         self.object = self.get_object()
-        if (self.object.membership.date_paid is not None):
-            # the membership is already marked as paid, so we add an error and redirect to member home
+
+        # don't produce the payment form if get_object() decides we don't have anything to do
+        if (self.object is None):
+            # the membership is already marked as paid and no CardPayment exists
+            # so we add an error and redirect to member home
             messages.error(request, "Your membership is already paid. Check the cokelog (/home/other/coke/cokelog) for more details.")
             return HttpResponseRedirect(self.get_completed_url())
         else:
@@ -83,3 +122,16 @@ class MembershipPaymentView(PaymentFormView):
 
     def get_completed_url(self):
         return reverse('memberdb:home')
+
+def create_membership_payment(membership, commit=True):
+    """ creates a MembershipPayment object for the given membership """
+    # get the amount from dispense
+    price = get_item_price(membership.membership_type)
+    if (price is None or price == 0):
+        return None
+    desc = MEMBERSHIP_TYPES[membership.membership_type]['desc']
+    payment = MembershipPayment(description=desc, amount=price, membership=membership)
+
+    if (commit):
+        payment.save()
+    return payment