From 27a6305b22e4222e68061fb932ef0173046af05c Mon Sep 17 00:00:00 2001 From: Eric Meehan Date: Tue, 27 Oct 2026 19:25:46 -0400 Subject: [PATCH] Tiers --- .env.example | 2 +- .vscode/tasks.json | 36 +++++++++++++++++++ README.md | 12 +++---- membership/members/tests/test_membership.py | 6 ++-- membership/members/tests/test_shkeeper.py | 4 ++- membership/members/views.py | 2 +- membership/templates/crypto_invoice.html | 2 +- membership/templates/members.html | 10 +++--- membership/tiers.eom.json | 40 ++++++++++----------- 9 files changed, 76 insertions(+), 38 deletions(-) diff --git a/.env.example b/.env.example index 0d31e52..7e2ec80 100755 --- a/.env.example +++ b/.env.example @@ -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."}} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6985836..4867050 100755 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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" } ] } \ No newline at end of file diff --git a/README.md b/README.md index 80cbf0c..11ee9c7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/membership/members/tests/test_membership.py b/membership/members/tests/test_membership.py index 22b9f05..eef9c63 100755 --- a/membership/members/tests/test_membership.py +++ b/membership/members/tests/test_membership.py @@ -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") diff --git a/membership/members/tests/test_shkeeper.py b/membership/members/tests/test_shkeeper.py index f68f65a..92fb112 100755 --- a/membership/members/tests/test_shkeeper.py +++ b/membership/members/tests/test_shkeeper.py @@ -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) diff --git a/membership/members/views.py b/membership/members/views.py index 2d56bf4..9b240e5 100755 --- a/membership/members/views.py +++ b/membership/members/views.py @@ -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 diff --git a/membership/templates/crypto_invoice.html b/membership/templates/crypto_invoice.html index 34c7aae..dfe5a05 100755 --- a/membership/templates/crypto_invoice.html +++ b/membership/templates/crypto_invoice.html @@ -1,6 +1,6 @@ {% extends "crypto_base.html" %} {% block content %} -

{{ invoice.tier|title }} invoice

+

{% for key, tier in tiers.items %}{% if key == invoice.tier %}{{ tier.label }}{% endif %}{% empty %}Membership{% endfor %} invoice

SHKeeper status: {{ invoice.status }}

Invoice reference
{{ invoice.id }}
diff --git a/membership/templates/members.html b/membership/templates/members.html index f1bbe60..60f68e1 100755 --- a/membership/templates/members.html +++ b/membership/templates/members.html @@ -54,10 +54,10 @@ {% if request.GET.checkout %}

Thanks for supporting the work. Stripe is confirming your subscription. Access appears after payment is verified and the membership worker has synchronized it.

{% endif %} {% for subscription in subscriptions %} -

{{ 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 %}

+

{% 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 %}

{% endfor %} {% for invoice in crypto_invoices %} -

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 %} · View crypto invoice

+

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 %} · View crypto invoice

{% endfor %}

Membership access updates automatically, usually within a few minutes. Signing out here does not sign you out of other eom.dev services.

{% else %} @@ -71,7 +71,7 @@

Your support, your choice

Find your membership

Public projects and ordinary community participation stay free. Membership adds a closer connection to the work, not a barrier to joining in.

-

Level 0 · The starting point

Community

Free / always

+

Level 0 · The starting point

Open Lab

Free / always

One account for eom.dev, its community, and its services.

  • No payment required
  • One account for compatible community services
  • Upgrade whenever you choose
{% if not account %}Join the community{% else %}

Your free account is ready.

{% endif %} @@ -86,14 +86,14 @@
{% empty %}

Paid memberships are coming soon.

You can create your free account now. Paid tiers will appear when their prices and groups are configured.

{% endfor %}
-

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.

+

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.

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.

Simple, by design.

What happens when a membership ends?

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.

Where do members connect?

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.

-
What can I preview or take part in?

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.

+
What can I preview or take part in?

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.

Can I change or cancel a plan?

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.

{% if shkeeper_enabled %}
How do crypto memberships renew?

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.

{% endif %}
diff --git a/membership/tiers.eom.json b/membership/tiers.eom.json index b72f7c2..c087e0c 100755 --- a/membership/tiers.eom.json +++ b/membership/tiers.eom.json @@ -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" ] } }