Streamline billing page to single payment form

- Replace multiple forms with single unified payment form
- Remove deprecated pay_what_you_can preferences (no longer needed with Stripe Checkout)
- Remove pay-what-you-can route and view
- Simplify setup-namespace to just link to billing
- Update tests for new billing UI
This commit is contained in:
Russell Ballestrini 2025-12-19 21:40:12 -05:00
parent f4baebedc6
commit de1e7ca83b
7 changed files with 18 additions and 226 deletions

View file

@ -9,7 +9,6 @@ def includeme(config):
# stripe: payment processing via Stripe Checkout
config.add_route("billing", "/billing")
config.add_route("pay-what-you-can", "/pay-what-you-can")
config.add_route("create-checkout", "/billing/checkout")
config.add_route("billing-success", "/billing/success")
config.add_route("stripe-webhook", "/webhook/stripe")

View file

@ -1,5 +1,4 @@
{% extends request.base_funnel_template -%}
{% import 'snippets/forms.j2' as forms with context %}
{% block title %}{{ the_title }} | {{ request.domain }}{%- endblock -%}
{% block content -%}
@ -8,92 +7,37 @@
<h4>{{ the_title }}</h4>
</center>
<p>Support Remarkbox with a contribution. Set your preferences below, then pay when you're ready.</p>
<p>Support Remarkbox with a contribution. Choose an amount and pay securely with Stripe.</p>
<br>
{# Pay What You Can Preferences #}
{{ forms.pay_what_you_can() }}
<br>
{# Pay Now Button - uses saved preferences or custom amount #}
<form method="post" action="/billing/checkout">
<fieldset>
<legend>Pay Now</legend>
{% if request.user.pay_what_you_can and request.user.pay_what_you_can.amount %}
<p>Pay your configured amount of <strong>${{ request.user.pay_what_you_can.amount }}</strong> now.</p>
<input type="hidden" name="payment_type" value="pay_what_you_want">
<input type="hidden" name="amount" value="{{ request.user.pay_what_you_can.amount }}">
<input type="hidden" name="duration_months" value="{% if request.user.pay_what_you_can.frequency == 'yearly' %}12{% else %}0{% endif %}">
{% include 'snippets/csrf.j2' %}
<button type="submit" class="green-button">Pay ${{ request.user.pay_what_you_can.amount }} Now</button>
{% else %}
<p>Set your contribution amount above first, or enter a custom amount:</p>
<input type="hidden" name="payment_type" value="pay_what_you_want">
<input type="hidden" name="duration_months" value="0">
<label for="custom-amount">Amount (USD)</label>
<input type="text"
id="custom-amount"
name="amount"
placeholder="$10.00"
required
style="width: 100%; max-width: 200px;">
<br><br>
{% include 'snippets/csrf.j2' %}
<button type="submit" class="green-button">Pay Now</button>
{% endif %}
</fieldset>
</form>
<legend>Make a Payment</legend>
<br>
{# Annual Subscription #}
<form method="post" action="/billing/checkout">
<fieldset>
<legend>Annual Subscription</legend>
<p>Support Remarkbox with a yearly contribution.</p>
<input type="hidden" name="payment_type" value="annual">
<input type="hidden" name="duration_months" value="12">
<label for="annual-amount">Annual Amount (USD)</label>
<select id="annual-amount" name="amount" style="width: 100%; max-width: 200px;">
<option value="36">$36/year ($3/month)</option>
<option value="60">$60/year ($5/month)</option>
<option value="120" selected>$120/year ($10/month)</option>
<option value="240">$240/year ($20/month)</option>
<option value="360">$360/year ($30/month)</option>
<label>Payment Type</label>
<select name="payment_type" id="payment-type" style="width: 100%; max-width: 300px;">
<option value="pay_what_you_want" selected>One-time (Pay What You Want)</option>
<option value="annual">Annual Subscription</option>
<option value="top_up">Top Up</option>
</select>
<br><br>
{% include 'snippets/csrf.j2' %}
<button type="submit" class="green-button">Subscribe Annually</button>
</fieldset>
</form>
<br>
{# Top Up #}
<form method="post" action="/billing/checkout">
<fieldset>
<legend>Top Up</legend>
<p>Make an additional contribution anytime.</p>
<input type="hidden" name="payment_type" value="top_up">
<input type="hidden" name="duration_months" value="0">
<label for="topup-amount">Amount (USD)</label>
<label for="amount">Amount (USD)</label>
<input type="text"
id="topup-amount"
id="amount"
name="amount"
placeholder="$25.00"
placeholder="$10.00"
{% if request.user.pay_what_you_can and request.user.pay_what_you_can.amount %}value="{{ request.user.pay_what_you_can.amount }}"{% endif %}
required
style="width: 100%; max-width: 200px;">
style="width: 100%; max-width: 300px;">
<br><br>
<input type="hidden" name="duration_months" value="12">
{% include 'snippets/csrf.j2' %}
<button type="submit" class="green-button">Top Up</button>
<button type="submit" class="green-button">Pay with Stripe</button>
</fieldset>
</form>

View file

@ -111,10 +111,6 @@ There's no obligation to pay anything. : )
<br>
<br>
{{ forms.pay_what_you_can() }}
<br>
<a href="{{ request.link_prefix }}/billing" class="button green-button">Go to Billing</a>
<br>
@ -122,15 +118,11 @@ There's no obligation to pay anything. : )
Or PayPal <a href="https://www.paypal.me/russellbal/" target="_blank">@russellbal</a>
<br>
<br>
<b>Thank you so much!</b>
<br>
</section>

View file

@ -73,29 +73,3 @@
{% endmacro %}
{% macro pay_what_you_can() %}
<form method="post" action="/pay-what-you-can" onsubmit="submit.disabled = true; return true;">
<fieldset style="text-align: left;">
<legend>Pay What You Can</legend>
Safely adjust frequency or amount anytime.
<br>
<br>
<input type="radio" name="frequency" value="once" {% if request.user.pay_what_you_can.frequency == "once"%}checked="checked"{% endif %} required><b>Once</b><br>
<input type="radio" name="frequency" value="yearly" {% if request.user.pay_what_you_can.frequency == "yearly"%}checked="checked"{% endif %} required><b>Yearly</b><br>
<br>
<input type="text" id="amount" name="amount" placeholder="Amount: $USD" style="width: 94%;" {% if request.user.pay_what_you_can.amount %}value="{{ request.user.pay_what_you_can.amount }}"{% endif %} required>
<br>
<br>
We charge cards on the <b>1st</b> of each month to prevent double charges.
<br>
<br>
<b>Once</b> is <b>cumulative</b>, for example let's say you configured amount to $15 and months later adjust to $25, on the 1st of the next month you will be charged $10.
<br>
<br>
<b>Yearly</b> acts similar but also reoccurs each anniversary.
<br>
{% include 'snippets/csrf.j2' %}
<input type="submit" value="Save" style="float: right;">
</fieldset>
</form>
{% endmacro %}

View file

@ -251,36 +251,3 @@ class TestPaymentModel(unittest.TestCase):
payment.mark_failed()
self.assertEqual(payment.status, "failed")
class TestPayWhatYouCanModel(unittest.TestCase):
"""Unit tests for PayWhatYouCan model."""
@patch("remarkbox.models.user.is_user_name_available", MagicMock(return_value=True))
def setUp(self):
self.user = User("test@example.com")
def test_pay_what_you_can_creation(self):
"""Test creating a PayWhatYouCan preference."""
from remarkbox.models import PayWhatYouCan
pwc = PayWhatYouCan(self.user, "yearly", 100)
self.assertEqual(pwc.frequency, "yearly")
self.assertEqual(pwc.amount, 100)
# contributions defaults to 0 in DB but may be None before flush
self.assertIn(pwc.contributions, [0, None])
self.assertIsNotNone(pwc.created_timestamp)
def test_pay_what_you_can_update(self):
"""Test updating PayWhatYouCan preferences."""
from remarkbox.models import PayWhatYouCan
pwc = PayWhatYouCan(self.user, "once", 50)
original_timestamp = pwc.updated_timestamp
pwc.update("yearly", 100)
self.assertEqual(pwc.frequency, "yearly")
self.assertEqual(pwc.amount, 100)
self.assertGreaterEqual(pwc.updated_timestamp, original_timestamp)

View file

@ -409,73 +409,8 @@ class AuthenticatedFunctionalTests(FunctionalTests):
"""Test that the billing page loads for authenticated users."""
self._log_in_test_user(self.test_creds1)
res = self.testapp.get("/billing", status=200)
self.assertIn(b"Pay What You Can", res.body)
self.assertIn(b"Annual Subscription", res.body)
self.assertIn(b"Top Up", res.body)
def test_pay_what_you_can_preference(self):
"""Test saving pay-what-you-can preferences."""
self._log_in_test_user(self.test_creds1)
# Save preferences
redirect_res = self.testapp.post(
"/pay-what-you-can",
{
"frequency": "yearly",
"amount": "50",
"csrf_token": self.csrf,
},
status=302,
)
res = redirect_res.follow()
self.assertIn(b"Your contribution preferences have been saved", res.body)
# Verify billing page loads successfully
billing_res = self.testapp.get("/billing", status=200)
self.assertIn(b"Pay What You Can", billing_res.body)
def test_pay_what_you_can_update_preference(self):
"""Test updating pay-what-you-can preferences."""
self._log_in_test_user(self.test_creds1)
# Set initial preferences
self.testapp.post(
"/pay-what-you-can",
{
"frequency": "once",
"amount": "25",
"csrf_token": self.csrf,
},
)
# Update preferences
redirect_res = self.testapp.post(
"/pay-what-you-can",
{
"frequency": "yearly",
"amount": "100",
"csrf_token": self.csrf,
},
status=302,
)
res = redirect_res.follow()
self.assertIn(b"Your contribution preferences have been saved", res.body)
def test_pay_what_you_can_missing_fields(self):
"""Test pay-what-you-can with missing fields."""
self._log_in_test_user(self.test_creds1)
# Missing amount
redirect_res = self.testapp.post(
"/pay-what-you-can",
{
"frequency": "yearly",
"csrf_token": self.csrf,
},
status=302,
)
res = redirect_res.follow()
self.assertIn(b"You must set both frequency and amount", res.body)
self.assertIn(b"Make a Payment", res.body)
self.assertIn(b"Pay with Stripe", res.body)
@patch("remarkbox.stripe.checkout.stripe.checkout.Session.create")
def test_create_checkout_redirects_to_stripe(self, mock_create):

View file

@ -19,7 +19,7 @@ from remarkbox.stripe.checkout import (
retrieve_checkout_session,
verify_webhook_signature,
)
from remarkbox.models import Payment, PayWhatYouCan, get_payment_by_session_id, create_payment
from remarkbox.models import Payment, get_payment_by_session_id, create_payment
import logging
@ -43,25 +43,6 @@ def billing(request):
}
@view_config(route_name="pay-what-you-can", request_method="POST")
@user_required()
def pay_what_you_can(request):
"""Save user's pay-what-you-can preferences (frequency and amount)."""
frequency = request.params.get("frequency", None)
amount = request.params.get("amount", None)
if frequency is None or amount is None:
request.session.flash(("You must set both frequency and amount.", "error"))
elif request.user.pay_what_you_can:
request.user.pay_what_you_can.update(frequency, amount)
request.session.flash(("Your contribution preferences have been saved.", "success"))
else:
request.user.pay_what_you_can = PayWhatYouCan(request.user, frequency, amount)
request.session.flash(("Your contribution preferences have been saved.", "success"))
return HTTPFound("/billing")
@view_config(route_name="create-checkout", request_method="POST")
@user_required()
def create_checkout(request):