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

This commit is contained in:
Disco DeDisco
2026-03-09 01:07:16 -04:00
parent ad0caa7c17
commit bd72135a2f
27 changed files with 397 additions and 33 deletions

View 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);

View 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")

View File

@@ -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'),
]

View File

@@ -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})