From bd72135a2fa285cebf553bfb96774b4d622aa0ec Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Mon, 9 Mar 2026 01:07:16 -0400 Subject: [PATCH] full passing test suite w. new stripe integration across multiple project nodes; new gameboard django app; stripe in test mode on staging --- infra/gamearray.env.j2 | 2 + infra/group_vars/staging/vault.yaml | 68 +++++++++------- requirements.dev.txt | 2 + requirements.txt | 1 + .../dashboard/static/apps/scripts/wallet.js | 43 ++++++++++ .../tests/integrated/test_stripe_views.py | 79 +++++++++++++++++++ src/apps/dashboard/urls.py | 2 + src/apps/dashboard/views.py | 37 ++++++++- src/apps/gameboard/__init__.py | 0 src/apps/gameboard/admin.py | 3 + src/apps/gameboard/apps.py | 5 ++ src/apps/gameboard/models.py | 3 + src/apps/gameboard/tests/__init__.py | 0 .../gameboard/tests/integrated/__init__.py | 0 .../gameboard/tests/integrated/test_views.py | 52 ++++++++++++ src/apps/gameboard/tests/unit/__init__.py | 0 src/apps/gameboard/urls.py | 9 +++ src/apps/gameboard/views.py | 16 ++++ src/apps/lyric/admin.py | 3 +- ...7_user_stripe_customer_id_paymentmethod.py | 30 +++++++ src/apps/lyric/models.py | 10 +++ src/core/settings.py | 11 ++- src/core/urls.py | 1 + src/functional_tests/test_wallet.py | 8 +- src/manage.py | 6 ++ src/templates/apps/dashboard/wallet.html | 4 + src/templates/apps/gameboard/gameboard.html | 35 ++++++++ 27 files changed, 397 insertions(+), 33 deletions(-) create mode 100644 src/apps/dashboard/static/apps/scripts/wallet.js create mode 100644 src/apps/dashboard/tests/integrated/test_stripe_views.py create mode 100644 src/apps/gameboard/__init__.py create mode 100644 src/apps/gameboard/admin.py create mode 100644 src/apps/gameboard/apps.py create mode 100644 src/apps/gameboard/models.py create mode 100644 src/apps/gameboard/tests/__init__.py create mode 100644 src/apps/gameboard/tests/integrated/__init__.py create mode 100644 src/apps/gameboard/tests/integrated/test_views.py create mode 100644 src/apps/gameboard/tests/unit/__init__.py create mode 100644 src/apps/gameboard/urls.py create mode 100644 src/apps/gameboard/views.py create mode 100644 src/apps/lyric/migrations/0007_user_stripe_customer_id_paymentmethod.py create mode 100644 src/templates/apps/gameboard/gameboard.html diff --git a/infra/gamearray.env.j2 b/infra/gamearray.env.j2 index 0b990ef..0b671e5 100644 --- a/infra/gamearray.env.j2 +++ b/infra/gamearray.env.j2 @@ -5,6 +5,8 @@ DJANGO_SUPERUSER_EMAIL={{ django_superuser_email }} DJANGO_SUPERUSER_PASSWORD={{ django_superuser_password }} DATABASE_URL=postgresql://gamearray:{{ postgres_password }}@gamearray_postgres/gamearray MAILGUN_API_KEY={{ mailgun_api_key }} +STRIPE_PUBLISHABLE_KEY={{ stripe_publishable_key }} +STRIPE_SECRET_KEY={{ stripe_secret_key }} CELERY_BROKER_URL=redis://gamearray_redis:6379/0 REDIS_URL=redis://gamearray_redis:6379/1 diff --git a/infra/group_vars/staging/vault.yaml b/infra/group_vars/staging/vault.yaml index ee7f134..7aa48b7 100644 --- a/infra/group_vars/staging/vault.yaml +++ b/infra/group_vars/staging/vault.yaml @@ -1,28 +1,42 @@ $ANSIBLE_VAULT;1.1;AES256 -65383061626464353936363564313761663834646361326362613934363565623234636337313363 -3933313962643261353830333463336166393030313936370a616234626135633432613366633363 -61633265363937326231623365646336333737306634646335376135633031643564666164336230 -3435353764383936620a396165386538666433356166383661323037333861373632376432313332 -66666236373462363236663335623734633364653539323331396361613738636166323134386466 -66656431663261633036333537373336643866623236643139656662333831366435373837656262 -36333734376363373462643239623437623735373935633732343639313666663436616630363933 -61396530336461393064323161666537646135383462383532363932326132363331633438313138 -61623431326537313637626239653038353263313731303262653537316134383264616661623962 -32333564366362383431336432303964663835363365636434303332613161363930333065336637 -33343466343062306434663765613837343635386630326439303739616166396134393939626434 -62336634303963653230626630636363343730623734626336363039623231633532653330646366 -66613432633834393133386666623466326131386633303264333766306135623337353433306632 -66323733373232383862646661313966366465333463366361366337656537623562613964666631 -65373566316432383134666434393338626138363632633766636561383263333636623530326664 -63333265366132376437396431393535323931383637323833303839336635633735333565333530 -65343263373630633063383931646163323237643436366566363932646566323539373136646433 -37623638333834373537316164633166633738333363656431356163623332396631353864333333 -33306666646532626636376239326438373737383432663539333736363866663938396136383035 -32343534613862653538346430313338326435356230636535343464666262626663376635363835 -65363862663461353464313533313333323863313539643533343431643130383663656161616131 -33323639333564383830346163386362386238323936393832623961646565613961356263356365 -65376431666130356564666236383764316136326366666661326538653133343165326431393564 -36303065366263316232663230343137333231346538633036613066643365616331336135376461 -35613265623134663633303238366363336137383436663836353863623533396236666433303738 -38356361653633323065303035376664326238633066623731623436333332373363636634323433 -393631303539373234386465663630316335 +38383061343764656262613934313230656462366163363263653462333338333863326338343838 +3664646437643462346636623231633639396239333532340a363338313839353734326238643735 +39343237396433336436366430626332343666666461613636656433363838613432393539386266 +3237336434346333350a663530623334633438616135376437666631313064333735653633396461 +31306163343838336465626663373661343839653037333235313361633335646337353339616333 +35343233346562346236636364316265313936646235373866636333353866623161663935626637 +31633864366339653930626365373237326531366632626337636163333266656434323063333365 +38373437383261613439306666373764633737623466626235356465636365646337306534326535 +36633866663161613632613434666134343465383663633165663330376535653537333763376232 +61653265303134656338393033303834663630653064666134633638393235346631346461633030 +35343332393961363361613661633633613262663231366236396663636239326534373134623762 +30653139333134616236666238616466633733656633326331386138363839653566333434346534 +63326539333461383265316332336333656365386531393630663537363365643061363263313738 +37633564363533633762393736636333306433306534393539636231656162343562383232663932 +62646339363266303564383438636636373661656465666663613863396639633732636635326166 +39323738303338373466366236623665633538363134616565326665386564613735393638656630 +31326431316163376132623064376634643737313864336464623431333834663361336133353838 +32303635663261333732306137383133623134373363613837306637663566303634653863343766 +33613936626362653466333537666462373633313038376565623363666631353162643634653730 +30323532623261643136666237316561353038323265303930336364633731333533386563623133 +31343965643336613933663431626435333235366639363334653065303434386165333739336632 +61363030376664643638653365626365623936623864666663326534343863613962616431376666 +39363837386639393235316339323932326466616330303165613032663637616232656162653335 +61613266376262626234383135306238313366346330656333383465383861663962653638303362 +34353833646461383839386238626661346263363131643438343461393739336132386466373665 +32646238633161363064666335626639653335306236613866333934646366323564306133396131 +36343032623964316138386538333863363530396330646431373466646538663063326330663639 +32323762356632336364333162336133336335623865323861663131626232633066643238333237 +32343938353166353037316162653832663433343534626331633936633866356666653932656665 +38396533356131326262633431653435306362633966383531356236396639376437396333616130 +35666435393461316232323234653865346338326330623065373461323961393663306262313066 +30313430353065616230356135333565333338373663643434353561363438656233383739663233 +35653832353062396634613832353837333835636461616234343462626239636634613430373931 +31656534343764643065643733326637343631356633653531313062633362663461313732633331 +35626364393563373339636466346339383032383635303865306636623737343237333863353238 +63306132396262656365323833323635633563653735366630313363386236613231346339643430 +63396230353566633830383932666335373665356434656438336338633035653465613665613862 +31663565653338376662323866613538363566306635333735646363363730646331306234353839 +30346363393231623563646439623261643634663831313338393761343865303930373133633733 +31656466303365316164396463373335396464643130643337656361333339653238333633373662 +6539 diff --git a/requirements.dev.txt b/requirements.dev.txt index bc6309d..7911e5b 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -21,11 +21,13 @@ outcome==1.3.0.post0 packaging==25.0 pycparser==2.23 PySocks==1.7.1 +python-dotenv requests==2.32.5 selenium==4.39.0 sniffio==1.3.1 sortedcontainers==2.4.0 sqlparse==0.5.5 +stripe trio==0.32.0 trio-websocket==0.12.2 types-PyYAML==6.0.12.20250915 diff --git a/requirements.txt b/requirements.txt index 0295e4f..c975982 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,5 @@ lxml==6.0.2 psycopg2-binary redis requests==2.31.0 +stripe whitenoise==6.11.0 diff --git a/src/apps/dashboard/static/apps/scripts/wallet.js b/src/apps/dashboard/static/apps/scripts/wallet.js new file mode 100644 index 0000000..a5ddcc3 --- /dev/null +++ b/src/apps/dashboard/static/apps/scripts/wallet.js @@ -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); \ No newline at end of file diff --git a/src/apps/dashboard/tests/integrated/test_stripe_views.py b/src/apps/dashboard/tests/integrated/test_stripe_views.py new file mode 100644 index 0000000..ec73d60 --- /dev/null +++ b/src/apps/dashboard/tests/integrated/test_stripe_views.py @@ -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") diff --git a/src/apps/dashboard/urls.py b/src/apps/dashboard/urls.py index 39a8e96..fe34e6f 100644 --- a/src/apps/dashboard/urls.py +++ b/src/apps/dashboard/urls.py @@ -10,4 +10,6 @@ urlpatterns = [ path('users//', 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'), ] diff --git a/src/apps/dashboard/views.py b/src/apps/dashboard/views.py index 7c34c8d..2c52657 100644 --- a/src/apps/dashboard/views.py +++ b/src/apps/dashboard/views.py @@ -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}) diff --git a/src/apps/gameboard/__init__.py b/src/apps/gameboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/gameboard/admin.py b/src/apps/gameboard/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/src/apps/gameboard/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/src/apps/gameboard/apps.py b/src/apps/gameboard/apps.py new file mode 100644 index 0000000..4b786d4 --- /dev/null +++ b/src/apps/gameboard/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class GameboardConfig(AppConfig): + name = 'apps.gameboard' diff --git a/src/apps/gameboard/models.py b/src/apps/gameboard/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/src/apps/gameboard/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/src/apps/gameboard/tests/__init__.py b/src/apps/gameboard/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/gameboard/tests/integrated/__init__.py b/src/apps/gameboard/tests/integrated/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py new file mode 100644 index 0000000..b4c4ed6 --- /dev/null +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -0,0 +1,52 @@ +import lxml.html + +from django.test import TestCase + +from apps.lyric.models import User + + +class GameboardViewTest(TestCase): + def setUp(self): + self.user = User.objects.create(email="capman@test.io") + self.client.force_login(self.user) + response = self.client.get("/gameboard/") + self.parsed = lxml.html.fromstring(response.content) + + def test_gameboard_requires_login(self): + self.client.logout() + response = self.client.get("/gameboard/") + self.assertRedirects( + response, "/?next=/gameboard/", fetch_redirect_response=False + ) + + def test_gameboard_renders(self): + response = self.client.get("/gameboard/") + self.assertEqual(response.status_code, 200) + + def test_gameboard_shows_my_games_applet(self): + [_] = self.parsed.cssselect("#id_applet_my_games") + + def test_gameboard_shows_new_game_applet(self): + [_] = self.parsed.cssselect("#id_applet_new_game") + + def test_gameboard_shows_game_kit_btn(self): + [_] = self.parsed.cssselect("#id_game_kit_btn") + + def test_gameboard_shows_game_gear(self): + [_] = self.parsed.cssselect("#id_game_gear") + + def test_my_games_has_no_game_items_for_new_user(self): + game_items = self.parsed.cssselect("#id_applet_my_games .game-item") + self.assertEqual(len(game_items), 0) + + def test_game_kit_has_coin_on_a_string(self): + [_] = self.parsed.cssselect("#id_game_kit #id_kit_coin_on_a_string") + + def test_game_kit_has_free_token(self): + [_] = self.parsed.cssselect("#id_game_kit #id_kit_free_token_0") + + def test_game_kit_has_card_deck_placeholder(self): + [_] = self.parsed.cssselect("#id_game_kit #id_kit_card_deck") + + def test_game_kit_has_dice_set_placeholder(self): + [_] = self.parsed.cssselect("#id_game_kit #id_kit_dice_set") diff --git a/src/apps/gameboard/tests/unit/__init__.py b/src/apps/gameboard/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/gameboard/urls.py b/src/apps/gameboard/urls.py new file mode 100644 index 0000000..47b068d --- /dev/null +++ b/src/apps/gameboard/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from . import views + + +urlpatterns = [ + path('', views.gameboard, name='gameboard'), +] + diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py new file mode 100644 index 0000000..b401b86 --- /dev/null +++ b/src/apps/gameboard/views.py @@ -0,0 +1,16 @@ +from django.contrib.auth.decorators import login_required +from django.shortcuts import render + +from apps.lyric.models import Token + + +@login_required(login_url="/") +def gameboard(request): + coin = request.user.tokens.filter(token_type=Token.COIN).first() + free_tokens = list(request.user.tokens.filter(token_type=Token.FREE)) + return render( + request, "apps/gameboard/gameboard.html", { + "coin": coin, + "free_tokens": free_tokens, + } + ) diff --git a/src/apps/lyric/admin.py b/src/apps/lyric/admin.py index ceeabd9..65adafe 100644 --- a/src/apps/lyric/admin.py +++ b/src/apps/lyric/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import LoginToken, User +from .models import LoginToken, Token, User class UserAdmin(admin.ModelAdmin): @@ -9,3 +9,4 @@ class UserAdmin(admin.ModelAdmin): admin.site.register(User, UserAdmin) admin.site.register(LoginToken) +admin.site.register(Token) diff --git a/src/apps/lyric/migrations/0007_user_stripe_customer_id_paymentmethod.py b/src/apps/lyric/migrations/0007_user_stripe_customer_id_paymentmethod.py new file mode 100644 index 0000000..d0c0419 --- /dev/null +++ b/src/apps/lyric/migrations/0007_user_stripe_customer_id_paymentmethod.py @@ -0,0 +1,30 @@ +# Generated by Django 6.0 on 2026-03-08 20:26 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lyric', '0006_token_wallet'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='stripe_customer_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.CreateModel( + name='PaymentMethod', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_pm_id', models.CharField(max_length=255)), + ('last4', models.CharField(max_length=4)), + ('brand', models.CharField(max_length=32)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_methods', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/apps/lyric/models.py b/src/apps/lyric/models.py index 2bb12a4..cd8cb57 100644 --- a/src/apps/lyric/models.py +++ b/src/apps/lyric/models.py @@ -31,6 +31,7 @@ class User(AbstractBaseUser): username = models.CharField(max_length=35, unique=True, null=True, blank=True) searchable = models.BooleanField(default=False) palette = models.CharField(max_length=32, default="palette-default") + stripe_customer_id = models.CharField(max_length=255, null=True, blank=True) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) @@ -78,6 +79,15 @@ class Token(models.Model): ) return self.get_token_type_display() +class PaymentMethod(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payment_methods") + stripe_pm_id = models.CharField(max_length=255) + last4 = models.CharField(max_length=4) + brand = models.CharField(max_length=32) + + def __str__(self): + return f"{self.brand} ....{self.last4}" + @receiver(post_save, sender=User) def create_wallet_and_tokens(sender, instance, created, **kwargs): if not created: diff --git a/src/core/settings.py b/src/core/settings.py index def6231..a50b85e 100644 --- a/src/core/settings.py +++ b/src/core/settings.py @@ -55,9 +55,12 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - # Custom apps + # Board apps 'apps.dashboard', + 'apps.gameboard', + # Gamer apps 'apps.lyric', + # Custom apps 'apps.api', 'functional_tests', # Depend apps @@ -196,4 +199,8 @@ LOGGING = { # Mailgun API settings (for HTTP API instead of SMTP) MAILGUN_API_KEY = os.environ.get("MAILGUN_API_KEY") -MAILGUN_DOMAIN = "howdy.earthmanrpg.com" # Your Mailgun domain \ No newline at end of file +MAILGUN_DOMAIN = "howdy.earthmanrpg.com" # Your Mailgun domain + +# Stripe payment settings +STRIPE_PUBLISHABLE_KEY = os.environ.get("STRIPE_PUBLISHABLE_KEY", "") +STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY", "") diff --git a/src/core/urls.py b/src/core/urls.py index 2442a55..3831b91 100644 --- a/src/core/urls.py +++ b/src/core/urls.py @@ -11,6 +11,7 @@ urlpatterns = [ path('api/', include('apps.api.urls')), path('dashboard/', include('apps.dashboard.urls')), path('lyric/', include('apps.lyric.urls')), + path('gameboard/', include('apps.gameboard.urls')), ] # Please remove the following urlpattern diff --git a/src/functional_tests/test_wallet.py b/src/functional_tests/test_wallet.py index c3470b9..4f31a4a 100644 --- a/src/functional_tests/test_wallet.py +++ b/src/functional_tests/test_wallet.py @@ -104,6 +104,12 @@ class WalletDisplayTest(FunctionalTest): By.CSS_SELECTOR, "#id_stripe_payment_element iframe" ) self.browser.switch_to.frame(stripe_frame) + # 4a. Wait for card inputs to render inside iframe + self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, 'input[placeholder="1234 1234 1234 1234"]' + ) + ) # 5. Fill in Stripe test card details self.browser.find_element( By.CSS_SELECTOR, 'input[placeholder="1234 1234 1234 1234"]' @@ -115,7 +121,7 @@ class WalletDisplayTest(FunctionalTest): By.CSS_SELECTOR, 'input[placeholder="CVC"]' ).send_keys("424") self.browser.find_element( - By.CSS_SELECTOR, 'input[placeholder="ZIP"]' + By.CSS_SELECTOR, 'input[placeholder="12345"]' ).send_keys("42424") # 6. Return to main doc & submit form self.browser.switch_to.default_content() diff --git a/src/manage.py b/src/manage.py index f2a662c..8eba45d 100644 --- a/src/manage.py +++ b/src/manage.py @@ -5,6 +5,12 @@ import sys def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + try: + from dotenv import load_dotenv + load_dotenv() + except ImportError: + pass """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') try: diff --git a/src/templates/apps/dashboard/wallet.html b/src/templates/apps/dashboard/wallet.html index 1ff0e25..1b8ec01 100644 --- a/src/templates/apps/dashboard/wallet.html +++ b/src/templates/apps/dashboard/wallet.html @@ -1,4 +1,5 @@ {% extends "core/base.html" %} +{% load static %} {% block content %}
@@ -27,6 +28,7 @@
+
@@ -38,4 +40,6 @@
+ + {% endblock content %} \ No newline at end of file diff --git a/src/templates/apps/gameboard/gameboard.html b/src/templates/apps/gameboard/gameboard.html new file mode 100644 index 0000000..469fcb0 --- /dev/null +++ b/src/templates/apps/gameboard/gameboard.html @@ -0,0 +1,35 @@ +{% extends "core/base.html" %} + +{% block content %} +
+
+

My Games

+
+ +
+

New Game

+
+ +
+ + +
+{% endblock content %} \ No newline at end of file