This commit is contained in:
2026-10-27 19:25:46 -04:00
parent aa18f6b895
commit 27a6305b22
9 changed files with 76 additions and 38 deletions
+1 -1
View File
@@ -56,4 +56,4 @@ MEMBERSHIP_TIERS_FILE=/app/tiers.eom.json
# Set MEMBERSHIP_TIERS_JSON instead to override the file ({} disables paid tiers).
# Each configured paid tier needs a unique positive level (level 0 is free).
# Example shape (names/prices/group DNs are your choice, not created by the app):
# MEMBERSHIP_TIERS_JSON={"supporter":{"level":1,"label":"Supporter","price_id":"price_REPLACE","price_label":"$5 / month","group_dn":"cn=www-supporter,ou=groups,dc=eom,dc=dev","description":"Support independent projects and community services."}}
# MEMBERSHIP_TIERS_JSON={"level-1":{"level":1,"label":"Bronze","price_id":"price_REPLACE","price_label":"$5 / month","group_dn":"cn=www-level-1,ou=groups,dc=eom,dc=dev","description":"Back the next experiment in the eom.dev lab."}}
+36
View File
@@ -82,6 +82,42 @@
"isBackground": false,
"problemMatcher": [],
"group": "test"
},
{
"label": "Test membership tier rename",
"type": "shell",
"command": "C:/Users/ermeehan/AppData/Local/Microsoft/WindowsApps/python3.12.exe",
"args": [
"${workspaceFolder}/membership/manage.py",
"test",
"members.tests.test_membership",
"members.tests.test_oidc",
"members.tests.test_postgres",
"members.tests.test_shkeeper",
"--settings=config.test_settings",
"--verbosity=1"
],
"isBackground": false,
"problemMatcher": [],
"group": "test"
},
{
"label": "Test ranked membership tier names",
"type": "shell",
"command": "C:/Users/ermeehan/AppData/Local/Microsoft/WindowsApps/python3.12.exe",
"args": [
"${workspaceFolder}/membership/manage.py",
"test",
"members.tests.test_membership",
"members.tests.test_oidc",
"members.tests.test_postgres",
"members.tests.test_shkeeper",
"--settings=config.test_settings",
"--verbosity=1"
],
"isBackground": false,
"problemMatcher": [],
"group": "test"
}
]
}
+6 -6
View File
@@ -48,12 +48,12 @@ metadata, Discourse integration, or existing subscriptions.
| Membership | Monthly USD | Existing live Price ID | Proposed dedicated LDAP group |
| --- | ---: | --- | --- |
| Level 0 · Community | Free | — | — |
| Level 1 · Supporter | $4 | `price_1Rgv4xC5KPXqTvVLfMd6z7wn` | `www-level-1` |
| Level 2 · Insider | $8 | `price_1Rgv5QC5KPXqTvVLv1PaHWb5` | `www-level-2` |
| Level 3 · Collaborator | $16 | `price_1Rgv5rC5KPXqTvVL8Ol8sMUf` | `www-level-3` |
| Level 4 · Sustainer | $32 | `price_1Rgv6FC5KPXqTvVL39mAxYjm` | `www-level-4` |
| Level 5 · Patron | $64 | `price_1Rgv6mC5KPXqTvVLkcmznWZa` | `www-level-5` |
| Level 0 · Open Lab | Free | — | — |
| Level 1 · Bronze | $4 | `price_1Rgv4xC5KPXqTvVLfMd6z7wn` | `www-level-1` |
| Level 2 · Silver | $8 | `price_1Rgv5QC5KPXqTvVLv1PaHWb5` | `www-level-2` |
| Level 3 · Gold | $16 | `price_1Rgv5rC5KPXqTvVL8Ol8sMUf` | `www-level-3` |
| Level 4 · Platinum | $32 | `price_1Rgv6FC5KPXqTvVL39mAxYjm` | `www-level-4` |
| Level 5 · Diamond | $64 | `price_1Rgv6mC5KPXqTvVLkcmznWZa` | `www-level-5` |
Every account has a non-null `membership_level`: new/free accounts start at 0,
and synchronization records the highest currently valid paid level. The former
+3 -3
View File
@@ -22,8 +22,8 @@ BASE = "ou=people,dc=example,dc=org"
DN = "uid=alice," + BASE
SENTINEL = "cn=sentinel,dc=example,dc=org"
TIERS = {
"supporter": {"label": "Supporter", "level": 1, "price_id": "price_supporter", "price_label": "$5 / month", "group_dn": GROUP},
"patron": {"label": "Patron", "level": 2, "price_id": "price_patron", "price_label": "$15 / month", "group_dn": OTHER_GROUP},
"supporter": {"label": "Bronze", "level": 1, "price_id": "price_supporter", "price_label": "$5 / month", "group_dn": GROUP},
"patron": {"label": "Diamond", "level": 2, "price_id": "price_patron", "price_label": "$15 / month", "group_dn": OTHER_GROUP},
}
@@ -42,7 +42,7 @@ class RegistrationTests(TestCase):
def test_public_home_and_registration(self):
response = self.client.get("/members/")
self.assertContains(response, "Create a free account")
self.assertContains(response, "Supporter")
self.assertContains(response, "Bronze")
self.assertEqual(response.headers["Cache-Control"], "no-store")
self.assertContains(self.client.get("/members/register/"), "Send verification email")
+3 -1
View File
@@ -104,7 +104,9 @@ class SHKeeperTests(TestCase):
def test_invoice_is_account_scoped_and_get_never_calls_provider(self):
invoice = self.prepared()
with patch("members.shkeeper.request") as request:
self.assertContains(self.client.get(f"/members/crypto/invoice/{invoice.pk}/"), "bc1-test-wallet")
response = self.client.get(f"/members/crypto/invoice/{invoice.pk}/")
self.assertContains(response, "bc1-test-wallet")
self.assertContains(response, "Bronze invoice")
request.assert_not_called()
self.login_other()
self.assertEqual(self.client.get(f"/members/crypto/invoice/{invoice.pk}/").status_code, 404)
+1 -1
View File
@@ -347,7 +347,7 @@ def crypto_checkout(request, tier):
@account_required
def crypto_invoice(request, invoice_id):
invoice = get_object_or_404(CryptoInvoice, pk=invoice_id, account=request.member)
return render(request, "crypto_invoice.html", {"invoice": invoice, "now": timezone.now()})
return render(request, "crypto_invoice.html", {"invoice": invoice, "tiers": settings.MEMBERSHIP_TIERS, "now": timezone.now()})
@csrf_exempt
+1 -1
View File
@@ -1,6 +1,6 @@
{% extends "crypto_base.html" %}
{% block content %}
<h1>{{ invoice.tier|title }} invoice</h1>
<h1>{% for key, tier in tiers.items %}{% if key == invoice.tier %}{{ tier.label }}{% endif %}{% empty %}Membership{% endfor %} invoice</h1>
<p class="member-notice" role="status">SHKeeper status: <strong>{{ invoice.status }}</strong></p>
<dl class="crypto-details">
<dt>Invoice reference</dt><dd>{{ invoice.id }}</dd>
+5 -5
View File
@@ -54,10 +54,10 @@
</div>
{% if request.GET.checkout %}<p role="status" class="member-notice">Thanks for supporting the work. Stripe is confirming your subscription. Access appears after payment is verified and the membership worker has synchronized it.</p>{% endif %}
{% for subscription in subscriptions %}
<p class="member-notice">{{ subscription.tier|default:"Membership"|title }} · {{ subscription.status }}{% if subscription.paid_until > now %} · Paid access through {{ subscription.paid_until|date:"M j, Y H:i T" }}{% else %} · No current paid access{% endif %}{% if subscription.cancel_at_period_end %} · Does not renew{% endif %}</p>
<p class="member-notice">{% for key, tier in tiers.items %}{% if key == subscription.tier %}{{ tier.label }}{% endif %}{% empty %}Membership{% endfor %} · {{ subscription.status }}{% if subscription.paid_until > now %} · Paid access through {{ subscription.paid_until|date:"M j, Y H:i T" }}{% else %} · No current paid access{% endif %}{% if subscription.cancel_at_period_end %} · Does not renew{% endif %}</p>
{% endfor %}
{% for invoice in crypto_invoices %}
<p class="member-notice">SHKeeper · {{ invoice.tier|title }} · {{ invoice.status }}{% if invoice.access_end > now %} · Prepaid term {{ invoice.access_start|date:"M j, Y" }}–{{ invoice.access_end|date:"M j, Y" }}{% endif %} · <a href="{% url 'members:crypto_invoice' invoice.id %}">View crypto invoice</a></p>
<p class="member-notice">SHKeeper · {% for key, tier in tiers.items %}{% if key == invoice.tier %}{{ tier.label }}{% endif %}{% empty %}Membership{% endfor %} · {{ invoice.status }}{% if invoice.access_end > now %} · Prepaid term {{ invoice.access_start|date:"M j, Y" }}–{{ invoice.access_end|date:"M j, Y" }}{% endif %} · <a href="{% url 'members:crypto_invoice' invoice.id %}">View crypto invoice</a></p>
{% endfor %}
<p class="member-small">Membership access updates automatically, usually within a few minutes. Signing out here does not sign you out of other eom.dev services.</p>
{% else %}
@@ -71,7 +71,7 @@
<div class="section-heading"><p class="eyebrow">Your support, your choice</p><h2 id="tiers-heading">Find your membership</h2><p>Public projects and ordinary community participation stay free. Membership adds a closer connection to the work, not a barrier to joining in.</p></div>
<div class="member-grid">
<article class="member-panel tier-card free-tier">
<p class="card-kicker">Level 0 · The starting point</p><h3>Community</h3><p class="tier-price">Free <span>/ always</span></p>
<p class="card-kicker">Level 0 · The starting point</p><h3>Open Lab</h3><p class="tier-price">Free <span>/ always</span></p>
<p>One account for eom.dev, its community, and its services.</p>
<ul><li>No payment required</li><li>One account for compatible community services</li><li>Upgrade whenever you choose</li></ul>
{% if not account %}<a class="button button-secondary" href="{% url 'members:register' %}">Join the community</a>{% else %}<p class="tier-footnote">Your free account is ready.</p>{% endif %}
@@ -86,14 +86,14 @@
</article>
{% empty %}<div class="member-panel"><h3>Paid memberships are coming soon.</h3><p>You can create your free account now. Paid tiers will appear when their prices and groups are configured.</p></div>{% endfor %}
</div>
<p class="member-small">One shared members' space, not a separate community at every price. Sustainer and Patron include the same access as Collaborator; choose them if you would like to contribute more.</p>
<p class="member-small">One shared members' space, not a separate community at every price. Platinum and Diamond include the same access as Gold; choose them if you would like to contribute more.</p>
<p class="member-small">Previews are shared when available, and playtests, listening sessions and feedback gatherings are occasional. Membership does not include storage quotas, personal consulting, guaranteed support or promised feature development. Recognition is always optional.</p>
</section>
<section class="member-faq" aria-labelledby="membership-faq">
<h2 id="membership-faq">Simple, by design.</h2>
<details><summary>What happens when a membership ends?</summary><p>Your free eom.dev account stays. Member-only access ends when your paid membership expires. An immediate cancellation removes that access once your membership status updates.</p></details>
<details><summary>Where do members connect?</summary><p>The shared members' Matrix space is the home for informal conversations and work in progress. Private forum discussions can complement it as they become available, without splitting everyone into separate rooms for each tier.</p></details>
<details><summary>What can I preview or take part in?</summary><p>Depending on what is being made, Insider members and above may preview music, game builds, videos or new experiments and join playtests or listening sessions. Collaborator members and above can also take part in project-topic polls and occasional small-group feedback sessions. Polls help inform the work; they are not promises to build requested features.</p></details>
<details><summary>What can I preview or take part in?</summary><p>Depending on what is being made, Silver members and above may preview music, game builds, videos or new experiments and join playtests or listening sessions. Gold members and above can also take part in project-topic polls and occasional small-group feedback sessions. Polls help inform the work; they are not promises to build requested features.</p></details>
<details><summary>Can I change or cancel a plan?</summary><p>Use Manage billing after signing in. The billing portal offers the changes enabled for your plan. Cancellation at period end keeps access through the paid expiry date.</p></details>
{% if shkeeper_enabled %}<details><summary>How do crypto memberships renew?</summary><p>SHKeeper payments prepay one calendar month, without automatic charges. Create a new invoice for each renewal. Confirmed payment on the same tier extends its prepaid term. Partial or unconfirmed payments do not grant access. Do not send renewal funds to an old invoice address.</p></details>{% endif %}
</section>
+20 -20
View File
@@ -1,72 +1,72 @@
{
"level-1": {
"level": 1,
"label": "Supporter",
"label": "Bronze",
"price_id": "price_1Rgv4xC5KPXqTvVLfMd6z7wn",
"price_label": "$4 USD / month",
"usd_amount": "4.00",
"group_dn": "cn=www-level-1,ou=groups,dc=eom,dc=dev",
"description": "A private place to connect and a closer look at what is being made.",
"group_dn": "cn=Level 1,ou=Members,dc=eom,dc=dev",
"description": "Kick off the experiment with a private place to connect and a closer look at what is being made.",
"benefits": [
"Access to the shared members' Matrix space",
"Behind-the-scenes notes and work-in-progress updates",
"Optional recognition on the supporters page"
"Optional credit on the community supporters page"
]
},
"level-2": {
"level": 2,
"label": "Insider",
"label": "Silver",
"price_id": "price_1Rgv5QC5KPXqTvVLv1PaHWb5",
"price_label": "$8 USD / month",
"usd_amount": "8.00",
"group_dn": "cn=www-level-2,ou=groups,dc=eom,dc=dev",
"description": "See new experiments take shape, and try things before their public release.",
"group_dn": "cn=Level 2,ou=Members,dc=eom,dc=dev",
"description": "Venture behind the scenes and explore new experiments before their public release.",
"benefits": [
"Everything in Supporter",
"Everything in Bronze",
"Early previews of music, games, videos and experiments when available",
"Invitations to member playtests and listening sessions"
]
},
"level-3": {
"level": 3,
"label": "Collaborator",
"label": "Gold",
"price_id": "price_1Rgv5rC5KPXqTvVL8Ol8sMUf",
"price_label": "$16 USD / month",
"usd_amount": "16.00",
"group_dn": "cn=www-level-3,ou=groups,dc=eom,dc=dev",
"description": "Share your perspective and help shape the next community experiment.",
"group_dn": "cn=Level 3,ou=Members,dc=eom,dc=dev",
"description": "Bring your perspective and help shape the next community experiment.",
"benefits": [
"Everything in Insider",
"Everything in Silver",
"Member polls on future project topics and experiments",
"Invitations to occasional small-group feedback sessions"
]
},
"level-4": {
"level": 4,
"label": "Sustainer",
"label": "Platinum",
"price_id": "price_1Rgv6FC5KPXqTvVL39mAxYjm",
"price_label": "$32 USD / month",
"usd_amount": "32.00",
"group_dn": "cn=www-level-4,ou=groups,dc=eom,dc=dev",
"group_dn": "cn=Level 4,ou=Members,dc=eom,dc=dev",
"description": "The same community access, with a larger contribution to keeping it independent.",
"benefits": [
"Everything in Collaborator, with the same access",
"Everything in Gold, with the same access",
"A larger contribution toward servers and public projects",
"Choose this level if you would like to give more"
]
},
"level-5": {
"level": 5,
"label": "Patron",
"label": "Diamond",
"price_id": "price_1Rgv6mC5KPXqTvVLkcmznWZa",
"price_label": "$64 USD / month",
"usd_amount": "64.00",
"group_dn": "cn=www-level-5,ou=groups,dc=eom,dc=dev",
"group_dn": "cn=Level 5,ou=Members,dc=eom,dc=dev",
"description": "Help underwrite the public work and the community around it.",
"benefits": [
"Everything in Collaborator, with the same access",
"Optional patron credit on the supporters page",
"Your highest level of support for independent public projects"
"Everything in Gold, with the same access",
"Optional Diamond credit on the supporters page",
"Your biggest boost for independent public projects"
]
}
}