Skip to content

Commit

Permalink
Support paying with revolut
Browse files Browse the repository at this point in the history
This gives us apple and google pay. We can later remove direct paypal
payments if unused.
  • Loading branch information
mariocj89 committed Mar 9, 2025
1 parent 1a30c66 commit 140a9d4
Show file tree
Hide file tree
Showing 18 changed files with 355 additions and 16 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ install-deps:

.PHONY: test
test:
$(PYTEST) eas --cov=eas --cov-report=term-missing --cov-fail-under=100 -vv
$(PYTEST) eas --cov=eas --cov-report=term-missing --cov-fail-under=100 --cov-exclude=**/__main__.py -vv

.PHONY: lint
lint:
DJANGO_SETTINGS_MODULE=eas.settings.local $(PYTHON) -m pylint eas
$(PYTHON) -m isort eas --check --recursive
$(PYTHON) -m isort eas --check
$(PYTHON) -m black eas --check

.PHONY: format
format:
$(PYTHON) -m isort eas --recursive
$(PYTHON) -m isort eas
$(PYTHON) -m black eas

.PHONY: runlocal
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ The following env vars enable additional services:
- `EAS_LAMADAVA_APIK`: Instagram (Get from hikerapi console).
- `EAS_LAMATOK_APIK`: TikTok (Get from hikerapi console).
- `EAS_PAYPAL_SECRET`: Sandbox KEY for paypal payments.
- `EAS_REVOLUT_SECRET`: Sandbox KEY for revolut payments.

All keys are in lastpass.

Expand Down
7 changes: 7 additions & 0 deletions eas/api/instagram/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import sys

from . import get_comments

if __name__ == "__main__":
for comment in get_comments(sys.argv[1]):
print(comment)
18 changes: 18 additions & 0 deletions eas/api/migrations/0021_payment_revolut_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2025-03-09 18:45

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0020_payment_secret_santa_alter_payment_draw"),
]

operations = [
migrations.AddField(
model_name="payment",
name="revolut_id",
field=models.CharField(db_index=True, max_length=500, null=True),
),
]
1 change: 1 addition & 0 deletions eas/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ class Options(enum.Enum):
payed = models.BooleanField(default=False)
draw_url = models.URLField(null=True)
paypal_id = models.CharField(max_length=500, db_index=True, null=True)
revolut_id = models.CharField(max_length=500, db_index=True, null=True)

option_certified = models.BooleanField(default=False)
option_support = models.BooleanField(default=False)
Expand Down
2 changes: 2 additions & 0 deletions eas/api/paypal.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ def accept_payment(payment_id, payer_id): # pragma: no cover
)
if response.status_code == 201:
LOG.info("Payment[%s] execute successfully", payment_id)
return True
else:
error = response.json()
if error.get("name") == "INSTRUMENT_DECLINED":
LOG.info("Payment[%s] declined", payment_id)
else:
LOG.error("Payment[%s] failed: %r", payment_id, error)
raise Exception("Failed to process PayPal Payment")
return False
52 changes: 52 additions & 0 deletions eas/api/revolut.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import logging

import requests
from django.conf import settings

LOG = logging.getLogger(__name__)

REVOLUT_API_V = "2024-09-01"
REVOLUT_API_BASE_URL = (
"https://sandbox-merchant.revolut.com"
if settings.REVOLUT_MODE == "sandbox"
else "https://merchant.revolut.com"
)

bearer = settings.REVOLUT_SECRET


def create_payment(draw_url, accept_url, amount):
url = f"{REVOLUT_API_BASE_URL}/api/orders"
payload = {
"amount": amount * 100,
"currency": "EUR",
"description": f"Payment for {draw_url}",
"redirect_url": accept_url,
}
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Revolut-Api-Version": REVOLUT_API_V,
"Authorization": f"Bearer {bearer}",
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
payment = response.json()
redirect_url = payment["checkout_url"]
LOG.info("Created new payment with id %r and url %r", payment["id"], redirect_url)
return payment["id"], redirect_url


def accept_payment(payment_id):
url = f"{REVOLUT_API_BASE_URL}/api/orders/{payment_id}"
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Revolut-Api-Version": REVOLUT_API_V,
"Authorization": f"Bearer {bearer}",
}
response = requests.get(url, headers=headers)
response.raise_for_status()
status = response.json()["state"]
LOG.info("Payment with id %s is in status %s", payment_id, status)
return status == "completed"
11 changes: 11 additions & 0 deletions eas/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,17 @@ class PayPalCreateSerialzier(serializers.Serializer):
draw_url = serializers.URLField()


class RevolutCreateSerialzier(serializers.Serializer):
options = serializers.ListField(
child=serializers.ChoiceField(
choices=[v.value for v in models.Payment.Options]
),
min_length=1,
)
draw_id = serializers.CharField(max_length=100)
draw_url = serializers.URLField()


class PromoCodeSerializer(serializers.Serializer):
code = serializers.CharField(max_length=8)
draw_id = serializers.CharField(max_length=100)
Expand Down
128 changes: 128 additions & 0 deletions eas/api/tests/int/test_revolut.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import os
from unittest import mock

import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APILiveServerTestCase

from eas.api import models

from .. import factories


class RevolutTestEnd2End(APILiveServerTestCase):
def setUp(self):
self.client.default_format = "json"
self.draw_id = factories.RaffleFactory().id
self.create_url = reverse("revolut-create")
self.accept_url = reverse("revolut-accept", kwargs={"draw_id": self.draw_id})

@pytest.mark.skipif(
"EAS_REVOLUT_SECRET" not in os.environ, reason="EAS_REVOLUT_SECRET unset"
)
def test_create_payment_end_to_end(self):
response = self.client.post(
self.create_url,
{
"options": ["CERTIFIED"],
"draw_id": self.draw_id,
"draw_url": "https://test.com",
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK, response.content)
assert response.json()["redirect_url"]
response = self.client.get(self.accept_url)
self.assertEqual(response.status_code, status.HTTP_302_FOUND, response.content)


class RevolutTestPublicDraw(APILiveServerTestCase):
def setUp(self):
self.client.default_format = "json"
self.draw_id = factories.RaffleFactory().id
self.create_url = reverse("revolut-create")
self.accept_url = reverse("revolut-accept", kwargs={"draw_id": self.draw_id})

@property
def draw(self):
return models.Raffle.objects.get(id=self.draw_id)

@mock.patch("eas.api.revolut.accept_payment")
@mock.patch("eas.api.revolut.create_payment")
def test_full_payment_public_draw(self, create_payment, _):
assert self.draw.payments == []
url = reverse("raffle-detail", kwargs=dict(pk=self.draw.id))
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK, response.content)
assert response.json()["payments"] == []

create_payment.return_value = "revolut-id", "fake-url"
response = self.client.post(
self.create_url,
{
"options": ["CERTIFIED", "ADFREE", "SUPPORT"],
"draw_id": self.draw.id,
"draw_url": "http://test.com",
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK, response.content)
assert response.json()["redirect_url"] == "fake-url"

response = self.client.get(
self.accept_url, {"token": "revolut-id", "PayerID": "payer-id"}
)
self.assertEqual(response.status_code, status.HTTP_302_FOUND, response.content)
assert response.url == "http://test.com"
assert self.draw.payments == ["CERTIFIED", "ADFREE", "SUPPORT"]

url = reverse("raffle-detail", kwargs=dict(pk=self.draw.id))
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK, response.content)
assert response.json()["payments"] == ["CERTIFIED", "ADFREE", "SUPPORT"]


class RevolutTestSecretSanta(APILiveServerTestCase):
def setUp(self):
self.client.default_format = "json"
self.draw = models.SecretSanta()
self.draw.save()
for i in range(3):
result = models.SecretSantaResult(
draw=self.draw, source=f"Person {i}", target=f"Person {2-i}"
)
result.save()
self.create_url = reverse("revolut-create")
self.accept_url = reverse("revolut-accept", kwargs={"draw_id": self.draw.id})

@mock.patch("eas.api.revolut.accept_payment")
@mock.patch("eas.api.revolut.create_payment")
def test_full_payment_secret_santa(self, create_payment, _):
assert self.draw.payments == []
url = reverse("secret-santa-admin", kwargs=dict(pk=self.draw.id))
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK, response.content)
assert response.json()["payments"] == []

create_payment.return_value = "revolut-id", "fake-url"
response = self.client.post(
self.create_url,
{
"options": ["CERTIFIED", "ADFREE", "SUPPORT"],
"draw_id": self.draw.id,
"draw_url": "http://test.com",
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK, response.content)
assert response.json()["redirect_url"] == "fake-url"

response = self.client.get(
self.accept_url, {"token": "revolut-id", "PayerID": "payer-id"}
)
self.assertEqual(response.status_code, status.HTTP_302_FOUND, response.content)
assert response.url == "http://test.com"
assert self.draw.payments == ["CERTIFIED", "ADFREE", "SUPPORT"]

url = reverse("secret-santa-admin", kwargs=dict(pk=self.draw.id))
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK, response.content)
assert response.json()["payments"] == ["CERTIFIED", "ADFREE", "SUPPORT"]
6 changes: 6 additions & 0 deletions eas/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
),
re_path(r"paypal/create/", views.paypal_create, name="paypal-create"),
re_path(r"paypal/accept/", views.paypal_accept, name="paypal-accept"),
re_path(r"revolut/create/", views.revolut_create, name="revolut-create"),
re_path(
r"revolut/accept/(?P<draw_id>[^/]+)/$",
views.revolut_accept,
name="revolut-accept",
),
re_path(
r"secret-santa-admin/(?P<pk>[^/]+)/$",
views.secret_santa_admin,
Expand Down
Loading

0 comments on commit 140a9d4

Please sign in to comment.