full passing test suite w. new stripe integration across multiple project nodes; new gameboard django app; stripe in test mode on staging
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
43
src/apps/dashboard/static/apps/scripts/wallet.js
Normal file
43
src/apps/dashboard/static/apps/scripts/wallet.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const initWallet = () => {
|
||||
let stripe, elements;
|
||||
|
||||
const addBtn = document.getElementById('id_add_payment_method');
|
||||
const saveBtn = document.getElementById('id_save_payment_method');
|
||||
if (!addBtn) return;
|
||||
|
||||
const getCsrf = () => document.cookie.match(/csrftoken=([^;]+)/)[1];
|
||||
|
||||
addBtn.addEventListener('click', async () => {
|
||||
const res = await fetch('/dashboard/wallet/setup-intent', {
|
||||
method: 'POST',
|
||||
headers: {'X-CSRFToken': getCsrf()},
|
||||
});
|
||||
const {client_secret, publishable_key} = await res.json();
|
||||
stripe = Stripe(publishable_key);
|
||||
elements = stripe.elements({clientSecret: client_secret});
|
||||
elements.create('payment').mount('#id_stripe_payment_element');
|
||||
saveBtn.hidden = false;
|
||||
});
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
const {error, setupIntent} = await stripe.confirmSetup({
|
||||
elements,
|
||||
redirect: 'if_required',
|
||||
});
|
||||
if (error) { console.error(error); return; }
|
||||
const res = await fetch('/dashboard/wallet/save-payment-method', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrf(),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `payment_method_id=${setupIntent.payment_method}`,
|
||||
});
|
||||
const {last4, brand} = await res.json();
|
||||
const pm = document.createElement('div');
|
||||
pm.textContent = `${brand} ····${last4}`;
|
||||
document.getElementById('id_payment_methods').appendChild(pm);
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initWallet);
|
||||
79
src/apps/dashboard/tests/integrated/test_stripe_views.py
Normal file
79
src/apps/dashboard/tests/integrated/test_stripe_views.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from unittest import mock
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.lyric.models import PaymentMethod, User
|
||||
|
||||
|
||||
class SetupIntentViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="capman@test.io")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_setup_intent_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.post("/dashboard/wallet/setup-intent")
|
||||
self.assertRedirects(
|
||||
response, "/?next=/dashboard/wallet/setup-intent",
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
@mock.patch("apps.dashboard.views.stripe")
|
||||
def test_returns_client_secret(self, mock_stripe):
|
||||
mock_stripe.Customer.create.return_value = mock.Mock(id="cus_test123")
|
||||
mock_stripe.SetupIntent.create.return_value = mock.Mock(client_secret="seti_secret")
|
||||
response = self.client.post("/dashboard/wallet/setup-intent")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()["client_secret"], "seti_secret")
|
||||
self.assertIn("publishable_key", response.json())
|
||||
|
||||
@mock.patch("apps.dashboard.views.stripe")
|
||||
def test_reuses_existing_stripe_customer(self, mock_stripe):
|
||||
self.user.stripe_customer_id = "cus_existing"
|
||||
self.user.save()
|
||||
mock_stripe.SetupIntent.create.return_value = mock.Mock(client_secret="seti_secret")
|
||||
self.client.post("/dashboard/wallet/setup-intent")
|
||||
mock_stripe.Customer.create.assert_not_called()
|
||||
mock_stripe.SetupIntent.create.assert_called_once_with(customer="cus_existing")
|
||||
|
||||
class SavePaymentMethodViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="capman@test.io")
|
||||
self.user.stripe_customer_id = "cus_test123"
|
||||
self.user.save()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_save_payment_method_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(
|
||||
"/dashboard/wallet/save-payment-method", {"payment_method_id": "pm_test"}
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, "/?next=/dashboard/wallet/save-payment-method",
|
||||
fetch_redirect_response=False,
|
||||
)
|
||||
|
||||
@mock.patch("apps.dashboard.views.stripe")
|
||||
def test_creates_payment_method_record(self, mock_stripe):
|
||||
mock_stripe.PaymentMethod.retrieve.return_value = mock.Mock(
|
||||
card=mock.Mock(last4="4242", brand="visa")
|
||||
)
|
||||
self.client.post(
|
||||
"/dashboard/wallet/save-payment-method",
|
||||
{"payment_method_id": "pm_test123"},
|
||||
)
|
||||
pm = PaymentMethod.objects.get(user=self.user)
|
||||
self.assertEqual(pm.last4, "4242")
|
||||
self.assertEqual(pm.brand, "visa")
|
||||
|
||||
@mock.patch("apps.dashboard.views.stripe")
|
||||
def test_returns_json_with_last4_and_brand(self, mock_stripe):
|
||||
mock_stripe.PaymentMethod.retrieve.return_value = mock.Mock(
|
||||
card=mock.Mock(last4="4242", brand="visa")
|
||||
)
|
||||
response = self.client.post(
|
||||
"/dashboard/wallet/save-payment-method",
|
||||
{"payment_method_id": "pm_test123"},
|
||||
)
|
||||
data = response.json()
|
||||
self.assertEqual(data["last4"], "4242")
|
||||
self.assertEqual(data["brand"], "visa")
|
||||
@@ -10,4 +10,6 @@ urlpatterns = [
|
||||
path('users/<uuid:user_id>/', views.my_lists, name='my_lists'),
|
||||
path('toggle_applets', views.toggle_applets, name="toggle_applets"),
|
||||
path('wallet/', views.wallet, name='wallet'),
|
||||
path('wallet/setup-intent', views.setup_intent, name='setup_intent'),
|
||||
path('wallet/save-payment-method', views.save_payment_method, name='save_payment_method'),
|
||||
]
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import stripe
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Max, Q
|
||||
from django.http import HttpResponse, HttpResponseForbidden
|
||||
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from apps.dashboard.forms import ExistingListItemForm, ItemForm
|
||||
from apps.dashboard.models import Applet, Item, List, UserApplet
|
||||
from apps.lyric.models import Token, User, Wallet
|
||||
from apps.lyric.models import PaymentMethod, Token, User, Wallet
|
||||
|
||||
|
||||
APPLET_ORDER = ["wallet", "new-list", "my-lists", "username", "palette"]
|
||||
@@ -146,6 +150,7 @@ def toggle_applets(request):
|
||||
return redirect("home")
|
||||
|
||||
@login_required(login_url="/")
|
||||
@ensure_csrf_cookie
|
||||
def wallet(request):
|
||||
wallet = request.user.wallet
|
||||
coin = request.user.tokens.filter(token_type=Token.COIN).first()
|
||||
@@ -155,3 +160,31 @@ def wallet(request):
|
||||
"coin": coin,
|
||||
"free_tokens": free_tokens,
|
||||
})
|
||||
|
||||
@login_required(login_url="/")
|
||||
def setup_intent(request):
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
user = request.user
|
||||
if not user.stripe_customer_id:
|
||||
customer = stripe.Customer.create(email=user.email)
|
||||
user.stripe_customer_id = customer.id
|
||||
user.save(update_fields=["stripe_customer_id"])
|
||||
intent = stripe.SetupIntent.create(customer=user.stripe_customer_id)
|
||||
return JsonResponse({
|
||||
"client_secret": intent.client_secret,
|
||||
"publishable_key": settings.STRIPE_PUBLISHABLE_KEY,
|
||||
})
|
||||
|
||||
@login_required(login_url="/")
|
||||
def save_payment_method(request):
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
pm_id = request.POST.get("payment_method_id")
|
||||
pm = stripe.PaymentMethod.retrieve(pm_id)
|
||||
stripe.PaymentMethod.attach(pm_id, customer=request.user.stripe_customer_id)
|
||||
PaymentMethod.objects.create(
|
||||
user=request.user,
|
||||
stripe_pm_id=pm_id,
|
||||
last4=pm.card.last4,
|
||||
brand=pm.card.brand,
|
||||
)
|
||||
return JsonResponse({"last4": pm.card.last4, "brand": pm.card.brand})
|
||||
|
||||
Reference in New Issue
Block a user