Skip to content

Commit e708654

Browse files
committed
Updated Coinify api
1 parent 079805d commit e708654

File tree

8 files changed

+148
-112
lines changed

8 files changed

+148
-112
lines changed

.gitmodules

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +0,0 @@
1-
2-
[submodule "src/vendor/coinify"]
3-
path = src/vendor/coinify
4-
url = https://github.com/tykling/python-sdk
5-
branch = python3-support

src/bornhack/environment_settings.py.dist

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,9 @@ PDF_ARCHIVE_PATH='{{ pdf_archive_path }}'
4949
QUICKPAY_API_KEY="{{ quickpay_api_key }}"
5050
QUICKPAY_PRIVATE_KEY="{{ quickpay_private_key }}"
5151

52+
COINIFY_API_URL='{{ coinify_api_url }}'
5253
COINIFY_API_KEY='{{ coinify_api_key }}'
53-
COINIFY_API_SECRET='{{ coinify_api_secret }}'
5454
COINIFY_IPN_SECRET='{{ coinify_ipn_secret }}'
55-
COINIFY_CALLBACK_HOSTNAME='{{ coinify_callback_hostname | default('') }}' # leave empty or comment out to use hostname from request
5655

5756
# shop settings
5857
BANKACCOUNT_BANK='{{ bank_name }}'

src/shop/coinify.py

Lines changed: 43 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,26 @@
55
from django.conf import settings
66

77
from .models import CoinifyAPICallback
8-
from .models import CoinifyAPIInvoice
8+
from .models import CoinifyAPIPaymentIntent
99
from .models import CoinifyAPIRequest
10-
from vendor.coinify.coinify_api import CoinifyAPI
1110

1211
logger = logging.getLogger("bornhack.%s" % __name__)
1312

1413

15-
def process_coinify_invoice_json(invoicejson, order, request):
16-
# create or update the invoice object in our database
17-
coinifyinvoice, created = CoinifyAPIInvoice.objects.update_or_create(
18-
coinify_id=invoicejson["id"],
14+
def process_coinify_payment_intent_json(intentjson, order, request):
15+
# create or update the intent object in our database
16+
coinifyintent, created = CoinifyAPIPaymentIntent.objects.update_or_create(
17+
coinify_id=intentjson["id"],
1918
order=order,
20-
defaults={"invoicejson": invoicejson},
19+
defaults={"paymentintentjson": intentjson},
2120
)
2221

2322
# if the order is paid in full call the mark as paid method now
24-
if invoicejson["state"] == "complete" and not coinifyinvoice.order.paid:
25-
coinifyinvoice.order.mark_as_paid(request=request)
23+
if "state" in intentjson:
24+
if intentjson["state"] == "complete" and not coinifyintent.order.paid:
25+
coinifyintent.order.mark_as_paid(request=request)
2626

27-
return coinifyinvoice
27+
return coinifyintent
2828

2929

3030
def save_coinify_callback(request, order):
@@ -51,55 +51,49 @@ def save_coinify_callback(request, order):
5151
return callbackobject
5252

5353

54-
def coinify_api_request(api_method, order, **kwargs):
55-
# Initiate coinify API
56-
coinifyapi = CoinifyAPI(settings.COINIFY_API_KEY, settings.COINIFY_API_SECRET)
57-
58-
# is this a supported method?
59-
if not hasattr(coinifyapi, api_method):
60-
logger.error("coinify api method not supported" % api_method)
61-
return False
62-
63-
# get and run the API call using the SDK
64-
method = getattr(coinifyapi, api_method)
65-
66-
# catch requests exceptions as described in https://github.com/CoinifySoftware/python-sdk#catching-errors and
67-
# http://docs.python-requests.org/en/latest/user/quickstart/#errors-and-exceptions
54+
def coinify_api_request(api_method, order, payload):
55+
url = f"{settings.COINIFY_API_URL}{api_method}"
56+
headers = {
57+
"accept": "application/json",
58+
"content-type": "application/json",
59+
"X-API-KEY": settings.COINIFY_API_KEY,
60+
}
6861
try:
69-
response = method(**kwargs)
62+
response = requests.post(url, data=json.dumps(payload), headers=headers)
7063
except requests.exceptions.RequestException as E:
7164
logger.error("requests exception during coinify api request: %s" % E)
7265
return False
7366

67+
logger.error(response.text)
7468
# save this API request to the database
7569
req = CoinifyAPIRequest.objects.create(
7670
order=order,
7771
method=api_method,
78-
payload=kwargs,
79-
response=response,
72+
payload=payload,
73+
response=response.json(),
8074
)
8175
logger.debug("saved coinify api request %s in db" % req.id)
8276

8377
return req
8478

8579

8680
def handle_coinify_api_response(apireq, order, request):
87-
if apireq.method == "invoice_create" or apireq.method == "invoice_get":
81+
if apireq.method == "payment-intents":
8882
# Parse api response
89-
if apireq.response["success"]:
90-
# save this new coinify invoice to the DB
91-
coinifyinvoice = process_coinify_invoice_json(
92-
invoicejson=apireq.response["data"],
83+
if "paymentWindowUrl" in apireq.response:
84+
# save this new coinify intent to the DB
85+
coinifyintent = process_coinify_payment_intent_json(
86+
intentjson=apireq.response,
9387
order=order,
9488
request=request,
9589
)
96-
return coinifyinvoice
90+
return coinifyintent
9791
else:
98-
api_error = apireq.response["error"]
92+
api_error = apireq.json()
9993
logger.error(
10094
"coinify API error: {} ({})".format(
101-
api_error["message"],
102-
api_error["code"],
95+
api_error["errorMessage"],
96+
api_error["errorCode"],
10397
),
10498
)
10599
return False
@@ -112,36 +106,26 @@ def handle_coinify_api_response(apireq, order, request):
112106
# API CALLS
113107

114108

115-
def get_coinify_invoice(coinify_invoiceid, order, request):
116-
# put args for API request together
117-
invoicedict = {"invoice_id": coinify_invoiceid}
118-
119-
# perform the api request
120-
apireq = coinify_api_request(api_method="invoice_get", order=order, **invoicedict)
121-
122-
coinifyinvoice = handle_coinify_api_response(apireq, order, request)
123-
return coinifyinvoice
124-
125-
126-
def create_coinify_invoice(order, request):
109+
def create_coinify_payment_intent(order, request):
127110
# put args for API request together
128-
invoicedict = {
111+
intentdict = {
129112
"amount": float(order.total),
130113
"currency": "DKK",
131-
"plugin_name": "BornHack webshop",
132-
"plugin_version": "1.0",
133-
"description": "BornHack order id #%s" % order.id,
134-
"callback_url": order.get_coinify_callback_url(request),
135-
"return_url": order.get_coinify_thanks_url(request),
136-
"cancel_url": order.get_cancel_url(request),
114+
"pluginIdentifier": "BornHack webshop",
115+
"orderId": order.id,
116+
"customerId": "bbca76fa-1337-439a-ae29-a3c2c2c84c4b",
117+
"customerEmail": "coinifycustomer@bornhack.example",
118+
"memo": "BornHack order id #%s" % order.id,
119+
"successUrl": order.get_coinify_thanks_url(request),
120+
"failureUrl": order.get_cancel_url(request),
137121
}
138122

139123
# perform the API request
140124
apireq = coinify_api_request(
141-
api_method="invoice_create",
125+
api_method="payment-intents",
142126
order=order,
143-
**invoicedict,
127+
payload=intentdict,
144128
)
145129

146-
coinifyinvoice = handle_coinify_api_response(apireq, order, request)
147-
return coinifyinvoice
130+
coinifyintent = handle_coinify_api_response(apireq, order, request)
131+
return coinifyintent
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 4.2.16 on 2025-02-15 15:42
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import django_prometheus.models
6+
import uuid
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('shop', '0086_alter_product_price'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='CoinifyAPIPaymentIntent',
18+
fields=[
19+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('created', models.DateTimeField(auto_now_add=True)),
21+
('updated', models.DateTimeField(auto_now=True)),
22+
('coinify_id', models.UUIDField(default=uuid.uuid4, editable=False)),
23+
('paymentintentjson', models.JSONField()),
24+
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='coinify_api_payment_intents', to='shop.order')),
25+
],
26+
options={
27+
'abstract': False,
28+
},
29+
bases=(django_prometheus.models.ExportModelOperationsMixin('coinify_api_payment_intent'), models.Model),
30+
),
31+
]

src/shop/models.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import logging
2+
import uuid
23
from datetime import timedelta
34
from decimal import Decimal
45
from enum import Enum
56
from itertools import chain
67
from typing import Optional
78

8-
from django.conf import settings
99
from django.contrib import messages
1010
from django.contrib.auth.models import User
1111
from django.contrib.postgres.fields import DateTimeRangeField
@@ -166,21 +166,6 @@ def total(self):
166166
else:
167167
return False
168168

169-
def get_coinify_callback_url(self, request):
170-
"""Check settings for an alternative COINIFY_CALLBACK_HOSTNAME otherwise use the one from the request"""
171-
if (
172-
hasattr(settings, "COINIFY_CALLBACK_HOSTNAME")
173-
and settings.COINIFY_CALLBACK_HOSTNAME
174-
):
175-
host = settings.COINIFY_CALLBACK_HOSTNAME
176-
else:
177-
host = request.get_host()
178-
return (
179-
"https://"
180-
+ host
181-
+ str(reverse_lazy("shop:coinify_callback", kwargs={"pk": self.pk}))
182-
)
183-
184169
def get_coinify_thanks_url(self, request):
185170
return (
186171
"https://"
@@ -265,6 +250,20 @@ def coinifyapiinvoice(self):
265250
# nope
266251
return False
267252

253+
@property
254+
def coinifyapipaymentintent(self):
255+
if not self.coinify_api_payment_intents.exists():
256+
return False
257+
258+
for tempinvoice in self.coinify_api_payment_intents.all():
259+
# we already have a coinifypaymentintent for this order
260+
if "paymentWindowUrl" in tempinvoice.paymentintentjson:
261+
# this invoice is still active, we are good to go
262+
return tempinvoice
263+
264+
# nope
265+
return False
266+
268267
@property
269268
def filename(self):
270269
return "bornhack_proforma_invoice_order_%s.pdf" % self.pk
@@ -1153,6 +1152,22 @@ def expired(self):
11531152
return parse_datetime(self.invoicejson["expire_time"]) < timezone.now()
11541153

11551154

1155+
class CoinifyAPIPaymentIntent(
1156+
ExportModelOperationsMixin("coinify_api_payment_intent"),
1157+
CreatedUpdatedModel,
1158+
):
1159+
coinify_id = models.UUIDField(default=uuid.uuid4, editable=False)
1160+
paymentintentjson = models.JSONField()
1161+
order = models.ForeignKey(
1162+
"shop.Order",
1163+
related_name="coinify_api_payment_intents",
1164+
on_delete=models.PROTECT,
1165+
)
1166+
1167+
def __str__(self):
1168+
return "coinifypaymentintent for order #%s" % self.order.id
1169+
1170+
11561171
class CoinifyAPICallback(
11571172
ExportModelOperationsMixin("coinify_api_callback"),
11581173
CreatedUpdatedModel,

src/shop/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@
8787
],
8888
),
8989
),
90+
path(
91+
"blockchain/callback/",
92+
CoinifyCallbackView.as_view(),
93+
name="coinify_intent_callback",
94+
),
9095
path("creditnotes/", CreditNoteListView.as_view(), name="creditnote_list"),
9196
path(
9297
"creditnotes/<int:pk>/pdf/",

0 commit comments

Comments
 (0)