Skip to content

Commit

Permalink
Updated Coinify api (#1745)
Browse files Browse the repository at this point in the history
* Updated Coinify api

* Fixed small bug, added payment intents to admin page

* Updated a few things after testing against coinify sandbox

* use respones not .json()

* Add user email to payment intent

* Update src/shop/models.py

* Update src/shop/views.py

* Remove old callback endpoint

* Update src/shop/views.py

* Update src/shop/models.py

* Remove unused property

* Update src/shop/coinify.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update src/shop/views.py

* maybe fix CI

* remove broken linter, remove unused import

* use apt to install deps

* use sudo

* libffi7 not found in apt

* Added Coinify Payment Intent import to backoffice

* Update src/backoffice/templates/includes/coinify_payment_intent_list_table.html

* Add class list-group-item-action to Coinify dashboard payment intent link

---------

Co-authored-by: Thomas Steen Rasmussen <tykling@bornhack.org>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 16, 2025
1 parent a4a4dd5 commit 81ea920
Show file tree
Hide file tree
Showing 22 changed files with 432 additions and 132 deletions.
6 changes: 2 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@ jobs:
with:
submodules: recursive

- uses: awalsh128/cache-apt-pkgs-action@latest
with:
version: 1.3.0
packages: libgdal-dev libpq-dev libffi7 libmagic1 git
- name: "Install OS deps"
run: "sudo apt -y install libgdal-dev libpq-dev libmagic1 git binutils libproj-dev gdal-bin"

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
Expand Down
5 changes: 0 additions & 5 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,5 +0,0 @@

[submodule "src/vendor/coinify"]
path = src/vendor/coinify
url = https://github.com/tykling/python-sdk
branch = python3-support
4 changes: 0 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@ repos:
rev: v3.1.0
hooks:
- id: add-trailing-comma
- repo: https://github.com/hadialqattan/pycln
rev: v2.5.0
hooks:
- id: pycln
- repo: https://github.com/psf/black
rev: 25.1.0
hooks:
Expand Down
4 changes: 4 additions & 0 deletions src/backoffice/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ def __init__(self, bank, *args, **kwargs):


class CoinifyCSVForm(forms.Form):
payment_intents = forms.FileField(
help_text="CSV file with Coinify Payment Intents. Leave empty if no invoices need to be imported.",
required=False,
)
invoices = forms.FileField(
help_text="CSV file with Coinify invoices. Leave empty if no invoices need to be imported.",
required=False,
Expand Down
4 changes: 4 additions & 0 deletions src/backoffice/templates/coinify_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
{% endif %}

<div class="list-group">
<a class="list-group-item list-group-item-action" href="{% url 'backoffice:coinifypayment_intent_list' camp_slug=camp.slug %}">
<h4 class="list-group-item-heading">Coinify Payment Intents</h4>
<p class="list-group-item-text">Use this view to see {{ payment_intents }} Coinify Payment Intents.</p>
</a>
<a class="list-group-item" href="{% url 'backoffice:coinifyinvoice_list' camp_slug=camp.slug %}">
<h4 class="list-group-item-heading">Coinify Invoices</h4>
<p class="list-group-item-text">Use this view to see {{ invoices }} Coinify invoices.</p>
Expand Down
28 changes: 28 additions & 0 deletions src/backoffice/templates/coinifypayment_intent_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% extends 'base.html' %}
{% load bornhack %}

{% block title %}
Coinify Payment Intent List | Backoffice | {{ block.super }}
{% endblock %}

{% block content %}
<div class="card">
<div class="card-header"><h3 class="card-title">Coinify Invoice List - BackOffice</h3></div>
<div class="card-body">
<p>A list of coinify payment intents imported from their system via CSV files.</p>
{% if not object_list %}
<p class="lead">No Coinify Payment Intents found. Go import a CSV!</p>
{% else %}
<p>
<a class="btn btn-secondary" href="{% url 'backoffice:coinify_dashboard' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Coinify Dashboard</a>
<a class="btn btn-secondary" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
{% include "includes/coinify_payment_intent_list_table.html" %}
</p>
{% endif %}
<p>
<a class="btn btn-secondary" href="{% url 'backoffice:coinify_dashboard' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Coinify Dashboard</a>
<a class="btn btn-secondary" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
</p>
</div>
</div>
{% endblock content %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% load bornhack %}
<table class="table table-striped table-hover datatable">
<thead>
<tr>
{% thh "coinify_id" "ID" %}
{% thh "coinify_created" "Created" %}
{% thh "requested_amount" %}
{% thh "amount" %}
{% thh "state (state_reason)" %}
{% thh "reference_type" %}
{% thh "original_order_id" %}
{% thh "Shop order" %}
{% thh "api_payment_intent" %}
{% thh "created" "Imported" "The date this Coinify payment intent was imported" %}
</tr>
</thead>
<tbody>
{% for ci in object_list %}
<tr>
<td>{{ ci.coinify_id }}</td>
<td data-order="{{ ci.coinify_created|sortable }}">{{ ci.coinify_created }}</td>
<td data-order="{{ ci.requested_amount }}">{{ ci.requested_amount }}&nbsp;{{ ci.requested_currency }}</td>
<td data-order="{{ ci.amount }}">{{ ci.amount }}&nbsp;{{ ci.currency }}</td>
<td>{{ ci.state }} ({{ ci.state_reason }})</td>
<td>{{ ci.reference_type }}</td>
<td>{{ ci.original_order_id }}</td>
<td>{{ ci.order }}</td>
<td>{{ ci.api_payment_intent }}</td>
<td data-order="{{ ci.created|sortable }}">{{ ci.created }}</td>
</tr>
{% endfor %}
</tbody>
</table>
6 changes: 6 additions & 0 deletions src/backoffice/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from .views import CoinifyCSVImportView
from .views import CoinifyDashboardView
from .views import CoinifyInvoiceListView
from .views import CoinifyPaymentIntentListView
from .views import TeamPermissionIndexView
from .views import TeamPermissionManageView
from .views import CoinifyPayoutListView
Expand Down Expand Up @@ -1010,6 +1011,11 @@
CoinifyDashboardView.as_view(),
name="coinify_dashboard",
),
path(
"payment_intents/",
CoinifyPaymentIntentListView.as_view(),
name="coinifypayment_intent_list",
),
path(
"invoices/",
CoinifyInvoiceListView.as_view(),
Expand Down
22 changes: 22 additions & 0 deletions src/backoffice/views/economy.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from economy.models import CoinifyBalance
from economy.models import CoinifyInvoice
from economy.models import CoinifyPayout
from economy.models import CoinifyPaymentIntent
from economy.models import Credebtor
from economy.models import EpayTransaction
from economy.models import Expense
Expand Down Expand Up @@ -458,6 +459,7 @@ def get_context_data(self, *args, **kwargs):
except CoinifyBalance.DoesNotExist:
context["balance"] = None

context["payment_intents"] = CoinifyPaymentIntent.objects.count()
context["invoices"] = CoinifyInvoice.objects.count()
context["payouts"] = CoinifyPayout.objects.count()
context["balances"] = CoinifyBalance.objects.count()
Expand All @@ -469,6 +471,11 @@ class CoinifyInvoiceListView(CampViewMixin, EconomyTeamPermissionMixin, ListView
template_name = "coinifyinvoice_list.html"


class CoinifyPaymentIntentListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = CoinifyPaymentIntent
template_name = "coinifypayment_intent_list.html"


class CoinifyPayoutListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = CoinifyPayout
template_name = "coinifypayout_list.html"
Expand All @@ -484,6 +491,21 @@ class CoinifyCSVImportView(CampViewMixin, EconomyTeamPermissionMixin, FormView):
template_name = "coinify_csv_upload_form.html"

def form_valid(self, form):
if "payment_intents" in form.files:
csvdata = form.files["payment_intents"].read().decode("utf-8-sig")
reader = csv.reader(StringIO(csvdata), delimiter=",", quotechar='"')
created = CoinifyCSVImporter.import_coinify_payment_intent_csv(reader)
if created:
messages.success(
self.request,
f"Payment Intent CSV processed OK. Successfully imported {created} new Coinify payment intents.",
)
else:
messages.info(
self.request,
"Payment Intent CSV processed OK. No new Coinify payment intents were created.",
)

if "invoices" in form.files:
csvdata = form.files["invoices"].read().decode("utf-8-sig")
reader = csv.reader(StringIO(csvdata), delimiter=",", quotechar='"')
Expand Down
3 changes: 1 addition & 2 deletions src/bornhack/environment_settings.py.dist
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ PDF_ARCHIVE_PATH='{{ pdf_archive_path }}'
QUICKPAY_API_KEY="{{ quickpay_api_key }}"
QUICKPAY_PRIVATE_KEY="{{ quickpay_private_key }}"

COINIFY_API_URL='{{ coinify_api_url }}'
COINIFY_API_KEY='{{ coinify_api_key }}'
COINIFY_API_SECRET='{{ coinify_api_secret }}'
COINIFY_IPN_SECRET='{{ coinify_ipn_secret }}'
COINIFY_CALLBACK_HOSTNAME='{{ coinify_callback_hostname | default('') }}' # leave empty or comment out to use hostname from request

# shop settings
BANKACCOUNT_BANK='{{ bank_name }}'
Expand Down
19 changes: 19 additions & 0 deletions src/economy/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .models import Chain
from .models import CoinifyBalance
from .models import CoinifyInvoice
from .models import CoinifyPaymentIntent
from .models import CoinifyPayout
from .models import Credebtor
from .models import EpayTransaction
Expand Down Expand Up @@ -234,6 +235,24 @@ class CoinifyInvoiceAdmin(admin.ModelAdmin):
]


@admin.register(CoinifyPaymentIntent)
class CoinifyPaymentIntentAdmin(admin.ModelAdmin):
list_display = [
"coinify_id",
"coinify_created",
"requested_amount",
"requested_currency",
"amount",
"currency",
"state",
"state_reason",
"reference_type",
"original_order_id",
"order",
"api_payment_intent",
]


@admin.register(CoinifyPayout)
class CoinifyPayoutAdmin(admin.ModelAdmin):
list_display = [
Expand Down
47 changes: 47 additions & 0 deletions src/economy/migrations/0042_coinifypaymentintent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 4.2.16 on 2025-02-16 10:44

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


class Migration(migrations.Migration):

dependencies = [
('shop', '0087_coinifyapipaymentintent'),
('economy', '0041_alter_expense_responsible_team'),
]

operations = [
migrations.CreateModel(
name='CoinifyPaymentIntent',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('coinify_id', models.UUIDField(help_text='Coinifys internal ID for this invoice')),
('coinify_created', models.DateTimeField(help_text='Created datetime in Coinifys end')),
('reference_type', models.CharField(help_text='Coinifys reference type', max_length=100)),
('merchant_id', models.UUIDField(help_text="The Merchant ID in Coinify's system.")),
('merchant_name', models.TextField(help_text="The Merchant name as set in Coinify's system.")),
('subaccount_id', models.CharField(blank=True, help_text='Unique identifier of a created sub-account.', max_length=32, null=True)),
('subaccount_name', models.TextField(blank=True, help_text='Name given when creating the sub-account.', null=True)),
('state', models.CharField(help_text='Coinify intent state', max_length=100)),
('state_reason', models.CharField(help_text='Coinify intent state reason', max_length=100)),
('original_order_id', models.CharField(blank=True, help_text='Order id', max_length=100, null=True)),
('customer_email', models.TextField()),
('requested_amount', models.DecimalField(decimal_places=2, help_text='The requested amount', max_digits=12)),
('requested_currency', models.CharField(help_text='The requested currency.', max_length=3)),
('amount', models.DecimalField(decimal_places=2, help_text='The payment amount', max_digits=12, null=True)),
('currency', models.CharField(blank=True, help_text='The payment currency.', max_length=3, null=True)),
('api_payment_intent', models.ForeignKey(blank=True, help_text='The original api payment intent', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='economy_coinify_payment_intents', to='shop.coinifyapipaymentintent')),
('order', models.ForeignKey(blank=True, help_text='The Order this payment intent is for', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='economy_coinify_payment_intents', to='shop.order')),
],
options={
'ordering': ['-coinify_created'],
'get_latest_by': ['coinify_created'],
},
bases=(django_prometheus.models.ExportModelOperationsMixin('coinify_payment_intent'), models.Model),
),
]
85 changes: 85 additions & 0 deletions src/economy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1451,6 +1451,91 @@ class Meta:
)


class CoinifyPaymentIntent(
ExportModelOperationsMixin("coinify_payment_intent"),
CreatedUpdatedUUIDModel,
):
"""Coinify Payment intents a intent is created for each payment."""

class Meta:
get_latest_by = ["coinify_created"]
ordering = ["-coinify_created"]

coinify_id = models.UUIDField(help_text="Coinifys internal ID for this invoice")
coinify_created = models.DateTimeField(help_text="Created datetime in Coinifys end")
reference_type = models.CharField(
max_length=100,
help_text="Coinifys reference type",
)
merchant_id = models.UUIDField(help_text="The Merchant ID in Coinify's system.")
merchant_name = models.TextField(
help_text="The Merchant name as set in Coinify's system.",
)
subaccount_id = models.CharField(
max_length=32,
help_text="Unique identifier of a created sub-account.",
blank=True,
null=True,
)
subaccount_name = models.TextField(
help_text="Name given when creating the sub-account.",
blank=True,
null=True,
)
state = models.CharField(
max_length=100,
help_text="Coinify intent state",
)
state_reason = models.CharField(
max_length=100,
help_text="Coinify intent state reason",
)
original_order_id = models.CharField(
null=True,
blank=True,
max_length=100,
help_text="Order id",
)
order = models.ForeignKey(
"shop.Order",
on_delete=models.PROTECT,
related_name="economy_coinify_payment_intents",
help_text="The Order this payment intent is for",
blank=True,
null=True,
)
api_payment_intent = models.ForeignKey(
"shop.CoinifyAPIPaymentIntent",
on_delete=models.PROTECT,
related_name="economy_coinify_payment_intents",
help_text="The original api payment intent",
blank=True,
null=True,
)
customer_email = models.TextField()
requested_amount = models.DecimalField(
max_digits=12,
decimal_places=2,
help_text="The requested amount",
)
requested_currency = models.CharField(
max_length=3,
help_text="The requested currency.",
)
amount = models.DecimalField(
max_digits=12,
decimal_places=2,
help_text="The payment amount",
null=True,
)
currency = models.CharField(
max_length=3,
help_text="The payment currency.",
null=True,
blank=True,
)


class CoinifyPayout(
ExportModelOperationsMixin("coinify_payout"),
CreatedUpdatedUUIDModel,
Expand Down
Loading

0 comments on commit 81ea920

Please sign in to comment.