Skip to content

Commit

Permalink
Implement new Square checkout flow (#101)
Browse files Browse the repository at this point in the history
* Swap squareconnect for squareup

* Add migration for 8aefed6

* Implement Square checkout flow

* Custom admin rendering of webhooks

* Custom admin rendering of square payments
  • Loading branch information
binaryf0x committed Oct 18, 2023
1 parent 5206b2b commit 396c723
Show file tree
Hide file tree
Showing 15 changed files with 509 additions and 215 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
.DS_Store
/data/
con_specific
.env
.idea
.vscode
artshow-utils/*.csv
Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ django-formtools = "*"
django-ses = "*"
gunicorn = "*"
"num2words" = "*"
squareconnect = "*"
pdfrw = "*"
reportlab = "*"
requests = "*"
Expand All @@ -22,6 +21,7 @@ psycopg = {extras = ["binary"],version = "*"}
supervisor = "*"
environs = {extras = ["django"],version = "*"}
celery = {extras = ["sqs"],version = "*"}
squareup = "*"

[dev-packages]
"django-debug-toolbar" = "*"
Expand Down
274 changes: 198 additions & 76 deletions Pipfile.lock

Large diffs are not rendered by default.

52 changes: 51 additions & 1 deletion artshow/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@
from django.urls import reverse
from django.utils.html import format_html

import json

from . import email1
from . import processbatchscan
from .models import (
Agent, Allocation, Artist, BatchScan, Bid, Bidder, BidderId, Checkoff,
ChequePayment, EmailSignature, EmailTemplate, Invoice, InvoiceItem,
InvoicePayment, Payment, PaymentType, Piece, Location, Space
InvoicePayment, Payment, PaymentType, Piece, Location, Space, SquarePayment,
SquareWebhook
)

User = get_user_model()
Expand Down Expand Up @@ -591,3 +594,50 @@ def print_cheques(self, request, cheqs):


admin.site.register(ChequePayment, ChequePaymentAdmin)


@admin.register(SquarePayment)
class SquarePaymentAdmin(admin.ModelAdmin):
@admin.display(description='Artist')
def clickable_artist(self, obj):
return format_html('<a href="{}">{}</a>',
reverse('admin:artshow_artist_change',
args=(obj.artist.pk,)),
str(obj.artist))

list_display = ('id', 'clickable_artist', 'amount', 'payment_type', 'date')
list_filter = ('payment_type',)
raw_id_fields = ('artist',)
readonly_fields = ('payment_link_id', 'payment_link_url', 'order_id')


@admin.register(SquareWebhook)
class SquareWebhookAdmin(admin.ModelAdmin):
list_display = ('webhook_event_id', 'timestamp', 'webhook_type', 'webhook_data_id')
fields = ('timestamp', 'pretty_json')
readonly_fields = ('timestamp', 'pretty_json')

@admin.display(description='ID')
def webhook_event_id(self, webhook):
if 'event_id' in webhook.body:
return webhook.body['event_id']
return '(unknown)'

@admin.display(description='Type')
def webhook_type(self, webhook):
if 'type' in webhook.body:
return webhook.body['type']
return '(unknown)'

@admin.display(description='Object ID')
def webhook_data_id(self, webhook):
if 'data' in webhook.body and 'id' in webhook.body['data']:
return webhook.body['data']['id']
return '(unknown)'

@admin.display(description='Body')
def pretty_json(self, webhook):
return format_html(
'<pre>{}</pre>',
json.dumps(webhook.body, sort_keys=True, indent=2),
)
85 changes: 39 additions & 46 deletions artshow/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
from django.forms import HiddenInput, ModelChoiceField
from django.forms.formsets import formset_factory
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.timezone import now
from django.urls import reverse
from .models import (
Allocation, Artist, Location, Payment, Piece, Space, validate_space,
Allocation, Artist, Location, Piece, Space, SquarePayment, validate_space,
validate_space_increments
)
from django import forms
Expand Down Expand Up @@ -339,57 +338,45 @@ def person_details(request, artist_id):
"artshow_settings": artshow_settings})


class PaymentForm(forms.Form):
amount = forms.DecimalField(required=True, max_digits=7, decimal_places=2)
nonce = forms.CharField(required=False)

def clean_amount(self):
amount = self.cleaned_data["amount"]
if amount <= 0:
raise forms.ValidationError("Amount must be above zero")
return amount


@login_required
@user_edits_allowable
def make_payment(request, artist_id):
artist = get_object_or_404(Artist.objects.viewable_by(request.user), pk=artist_id)
total_requested_cost, deduction_to_date, deduction_remaining, payment_remaining = \
artist.payment_remaining_with_details()

payment_remaining = Decimal(payment_remaining).quantize(Decimal('1.00'))
if request.method == "POST":
form = PaymentForm(request.POST)
if form.is_valid():
payment = Payment(artist=artist,
amount=form.cleaned_data["amount"],
payment_type_id=settings.ARTSHOW_PAYMENT_PENDING_PK,
description="",
date=now())

transaction = square.charge(payment, form.cleaned_data["nonce"])
if transaction:
payment.payment_type_id = settings.ARTSHOW_PAYMENT_RECEIVED_PK
payment.description = "Square " + transaction
payment.save()
return redirect(reverse("artshow-manage-payment-square",
args=(artist_id,)))
else:
form.add_error(None, "Failed to charge payment.")
else:
form = PaymentForm(initial={'amount': payment_remaining})

context = {"form": form,
"artist": artist,
"allocations": artist.allocation_set.order_by("id"),
"total_requested_cost": total_requested_cost,
"deduction_to_date": deduction_to_date,
"deduction_remaining": deduction_remaining,
"account_balance": artist.balance(),
"payment_remaining": payment_remaining,
"sq_application_id": settings.ARTSHOW_SQUARE_APPLICATION_ID,
"sq_location_id": settings.ARTSHOW_SQUARE_LOCATION_ID,
}
if request.method == "POST" and payment_remaining > 0:
payment_url = square.create_payment_url(
artist,
f'Art Show space reservation for {artist}',
payment_remaining,
request.build_absolute_uri(reverse('artshow-manage-payment-square',
args=(artist_id,))),
)
if payment_url is not None:
return redirect(payment_url)
else:
return redirect(reverse('artshow-manage-payment-square-error',
args=(artist_id,)))

pending_square_payment = SquarePayment.objects.filter(
artist=artist,
payment_type_id=settings.ARTSHOW_PAYMENT_PENDING_PK,
).first()
pending_payment_url = None
if pending_square_payment is not None:
pending_payment_url = pending_square_payment.payment_link_url

context = {
"artist": artist,
"allocations": artist.allocation_set.order_by("id"),
"total_requested_cost": total_requested_cost,
"deduction_to_date": deduction_to_date,
"deduction_remaining": deduction_remaining,
"account_balance": artist.balance(),
"payment_remaining": payment_remaining,
"payment_url": pending_payment_url,
}

return render(request, "artshow/make_payment.html", context)

Expand All @@ -404,3 +391,9 @@ def payment_made_mail(request, artist_id):
def payment_made_square(request, artist_id):
artist = get_object_or_404(Artist.objects.viewable_by(request.user), pk=artist_id)
return render(request, "artshow/payment_made_square.html", {"artist": artist})


@login_required
def payment_error_square(request, artist_id):
artist = get_object_or_404(Artist.objects.viewable_by(request.user), pk=artist_id)
return render(request, "artshow/payment_error_square.html", {"artist": artist})
2 changes: 2 additions & 0 deletions artshow/manage_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
manage.payment_made_mail, name='artshow-manage-payment-mail'),
re_path(r'^artist/(?P<artist_id>\d+)/makepayment/complete/square/$',
manage.payment_made_square, name='artshow-manage-payment-square'),
re_path(r'^artist/(?P<artist_id>\d+)/makepayment/error/square/$',
manage.payment_error_square, name='artshow-manage-payment-square-error'),
re_path(r'^register/$', register.main, name='artshow-register'),
re_path(r'^announcement/$', announcement.index, name="view_announcements"),
re_path(r'^announcement/(?P<announcement_id>\d+)/$', announcement.show,
Expand Down
18 changes: 18 additions & 0 deletions artshow/migrations/0010_alter_invoicepayment_payment_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.6 on 2023-10-08 19:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('artshow', '0009_location'),
]

operations = [
migrations.AlterField(
model_name='invoicepayment',
name='payment_method',
field=models.IntegerField(choices=[(0, 'Not Paid'), (1, 'Cash'), (3, 'Card')], default=0),
),
]
25 changes: 25 additions & 0 deletions artshow/migrations/0011_squarepayment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.6 on 2023-10-08 19:30

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('artshow', '0010_alter_invoicepayment_payment_method'),
]

operations = [
migrations.CreateModel(
name='SquarePayment',
fields=[
('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='artshow.payment')),
('payment_link_id', models.CharField(max_length=192)),
('payment_link_url', models.CharField(max_length=255)),
('order_id', models.CharField(max_length=192)),
('payment_id', models.CharField(blank=True, max_length=192)),
],
bases=('artshow.payment',),
),
]
21 changes: 21 additions & 0 deletions artshow/migrations/0012_squarewebhook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 4.2.6 on 2023-10-11 04:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('artshow', '0011_squarepayment'),
]

operations = [
migrations.CreateModel(
name='SquareWebhook',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField()),
('body', models.JSONField()),
],
),
]
12 changes: 12 additions & 0 deletions artshow/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,13 @@ def amount_words(self):
return '%s dollars and %s cents' % (num2words(dollars), num2words(cents))


class SquarePayment (Payment):
payment_link_id = models.CharField(max_length=192)
payment_link_url = models.CharField(max_length=255)
order_id = models.CharField(max_length=192)
payment_id = models.CharField(max_length=192, blank=True)


class Invoice (models.Model):
payer = models.ForeignKey(Bidder, on_delete=models.CASCADE)
tax_paid = models.DecimalField(max_digits=7, decimal_places=2, blank=True, null=True)
Expand Down Expand Up @@ -759,3 +766,8 @@ class Agent(models.Model):
help_text="Person is allowed to retrieve pieces from the show")
can_arbitrate = models.BooleanField(default=False,
help_text="Person is allowed to make executive decisions regarding pieces")


class SquareWebhook(models.Model):
timestamp = models.DateTimeField()
body = models.JSONField()
Loading

0 comments on commit 396c723

Please sign in to comment.