Releases: Wordpressistic/memberistic
Release list
Memberistic 2.1.0 payment-integrity
Changelog
All notable changes are tracked here. The format follows Keep a Changelog.
2.1.0 - Payment integrity - 2026-08-12
Every payment-provider event now passes through a single integrity gate before
it may change a membership. The governing rule: payment-provider events are
evidence to verify, never commands to trust. See
docs/PAYMENT-INTEGRITY.md.
Upgrading changes nobody's access on its own. Both configurable parts of the new
billing-to-access mapping default to the behaviour 2.0.1 already had.
Security
- A renewal could be granted by an unverified invoice event. The
invoice.payment_succeededhandler found a membership by subscription id or,
failing that, by provider metadata; set its status to active; advanced the
renewal date; wrote a payment row and sent a receipt — without checking who
paid, how much, in what currency, on which Stripe account, or whether the
invoice had been paid at all. The invoice is now re-read from the provider and
every one of those questions is answered before a field moves. A mismatch goes
to manual review and does not reactivate the membership. - A stale cancellation could cancel a replacement subscription. The
customer.subscription.deletedhandler fell back tometadata.membership_id
whenever the subscription lookup missed, so a cancellation for a subscription
a member had already replaced would find their membership through that
fallback and cancel it — removing access from someone who had just
re-subscribed and paid. The event's subscription id is now compared against
the membership's current authoritative subscription, and metadata can no
longer override a conflicting one. - A late payment failure could revoke access from a member who had paid.
invoice.payment_failedmoved a membership topast_duewithout checking the
failure still stood. The gate re-reads the subscription and does nothing when
the provider says it is paid. - Webhook deduplication no longer depends on a lock. The capped 500-id
option is replaced by aUNIQUEkey over(provider, account, event_id);
the failedINSERTis the deduplication. A retry storm could previously
push an id off the end of the list while its retry was in flight, charging the
member twice and emailing them twice. - Stripe signature verification hardened. Per-mode signing secrets;
digits-only timestamps; tolerance enforced in both directions; everyv1
candidate compared rather than the last one parsed winning (which broke
signing-secret rotation); a bounded header so an unauthenticated request
cannot buy unlimited HMAC work; one uniform error message so a forger learns
nothing from which check they failed. - Provider account and environment are checked. A test-mode event cannot
change live records, and an event from another Stripe account is refused. - Payment rows are unique per provider transaction, enforced by the
database rather than by a precedingSELECT.
Added
- Payment Integrity Gate, subscription state machine with an explicit
fail-closed transition matrix, an event ledger, and an immutable audit trail
with reason codes (includes/payments/). billing_statuson memberships, separate fromstatus. Payment events own
the first; staff decisions in the second (comped,paused, …) are never
overwritten by a card failure.- Configurable dunning: a stored grace deadline (default 7 days), a daily sweep
that movespast_due → grace_period → expired, and settings for whether a
trial or a grace window retains access — both defaulting to 2.0.1 behaviour. wp memberistic stripe reconcile(report by default,--applyto correct)
andwp memberistic stripe health.- Admin payment health reporting that names the problem and the remedy, with
every secret masked. - WooCommerce order transitions routed through the same gate, with plan/basket
verification and order-derived event ids so repeated hook fires deduplicate. docs/PAYMENT-INTEGRITY.md.
Changed
- The compatibility matrix is a gate rather than a report. Targets are
declared once in.github/wordpress-targets.jsonand validated against
wordpress.org before any job runs; a version that is not published fails the
run instead of printing a note and continuing.Tested up tois asserted
against the highest blocking target, so a WordPress line cannot be claimed
before it is tested. - Emails, hooks and activity now fire only after state is committed, and never
for a duplicate or rejected event. - New datetime columns are UTC, so event ordering survives a site time-zone
change. Existing columns are unchanged.
Database
- DB version
1.12.0. Additive and idempotent: new columns and two new tables,
nothing renamed or dropped,stripe_*columns retained and still written.
Pre-existing duplicate transaction ids are reported rather than deleted, and
the unique key is skipped until they are resolved.
2.0.1 - Compatibility and the init fatal (2026-08-08)
First release cut through the automated release pipeline, and the first whose
Tested up to is a statement about what was tested rather than what shipped.
Fixed
- Fatal error on
initfor every install.includes/integrations/class-booking-adapter.phpshipped in 2.0.0 but was never added to the manual require list inPlugin::load_dependencies(), and nothing autoloads.Waiver_Booking_Bridge::register()callsBooking_Adapter::hook()as its first statement and runs oninitpriority 4 whenever the Waiver Manager integration is enabled — and that integration's default is'yes'. So a stock install with no configuration at all fatalled oninitwithClass "WordPressistic\Memberistic\Integrations\Booking_Adapter" not found.Booking_Engine,POS_BridgeandStaff_Dashboardreach the same class. The file is now required ahead of every consumer.
Changed
Tested up toraised from 6.8 to 7.0, on evidence. The integration matrix exercises WordPress 6.8, 6.9 and 7.0.3 against PHP 8.2, 8.3 and 8.4 — nine jobs, each installing a real WordPress against a real MySQL and running the suite — plus a non-blocking trunk canary.readme.txtsays 7.0 because wordpress.org accepts only major.minor there. A green canary is never a claim of support for an unreleased version.
Added
- WordPress integration test harness (
bin/install-wp-tests.sh,phpunit-integration.xml,tests/integration/). The unit suite stubs WordPress; nothing before this loaded it, which is how theinitfatal below survived a green CI for the whole life of 2.0.0. Pinned to PHPUnit 9.6 because the WordPress core test library still callsPHPUnit\Util\Test::parseTestMethodAnnotations(), removed in PHPUnit 10; the unit suite stays on 10.5 and never loads WordPress. - Compatibility matrix in CI (
.github/workflows/integration.yml), covering activation, schema creation, DB version, capabilities, roles, scheduled tasks, fresh-install defaults, and a deprecation check that asserts WordPress reported nothing about the plugin during load or activation. A further test asserts the running WordPress matches the version the job installed, so the matrix cannot report green for a version it never exercised. - Release automation (
.github/workflows/release.yml): verifies the version string across its homes against the tag, lints, builds the distributable from.distignore, asserts no dev or internal files leaked in, computes SHA-256, and drafts — never publishes — the GitHub Release. - Dependency-manifest guard test (
tests/unit/DependencyManifestTest.php). Asserts that every.phpfile underincludes/appears inPlugin::load_dependencies(), that every listed path exists, that the list has no duplicates, and thatBooking_Adapterprecedes its consumers. Because there is no autoloader, an omission there is a fatal at load time that no existing check can see —php -lproves each file parses, and it does parse perfectly on its own, while the unit suite never boots the plugin. CI was green for the whole life of the 2.0.0 release.
2.0.0 - Public release (2026-08-08)
Brand-neutral packaging, security and privacy hardening, and safe defaults.
No new product features. Upgrading from 1.x: there is no data migration —
tables, option keys, meta keys, hooks, capabilities, and the REST namespace are
all unchanged. See docs/UPGRADE-2.0.md.
Added
- Privacy tooling. GDPR exporter and eraser registered with WordPress core, so Tools → Export/Erase Personal Data covers Memberistic records. Suggested privacy-policy text registered with the core policy editor. Erasure anonymizes in place and reports what it retains, with the reason: signed waivers (legal-claims basis, overridable via
memberistic_privacy_erase_waivers) and payment records (statutory financial retention). - Data retention. Configurable windows for check-in and activity history, defaulting to keep-indefinitely so no update can start silently deleting a business's records.
- Complete uninstall. Covers every table including the corporate module, all options and transients, user meta, cron events, dynamically created member and per-plan roles, capabilities left on surviving roles, and the generated check-in page. Multisite-aware. Still opt-in, still off by default.
- Theme template overrides. Copy any file from
templates/into amemberistic/directory in the theme to replace it. Newmemberistic_locate_templatefilter, with the resolved path validated to stay inside the theme or plugin template roots. - Plan template library (
templates/plans/) — importable JSON sets for gym, studio, club, association, range, and generic tiered catalogues. Import as inactiv...
Fixed issues
Changelog
All notable changes are tracked here. The format follows Keep a Changelog.
Unreleased
Fixed
- Fatal error on
initfor every install.includes/integrations/class-booking-adapter.phpshipped in 2.0.0 but was never added to the manual require list inPlugin::load_dependencies(), and nothing autoloads.Waiver_Booking_Bridge::register()callsBooking_Adapter::hook()as its first statement and runs oninitpriority 4 whenever the Waiver Manager integration is enabled — and that integration's default is'yes'. So a stock install with no configuration at all fatalled oninitwithClass "WordPressistic\Memberistic\Integrations\Booking_Adapter" not found.Booking_Engine,POS_BridgeandStaff_Dashboardreach the same class. The file is now required ahead of every consumer.
Added
- Dependency-manifest guard test (
tests/unit/DependencyManifestTest.php). Asserts that every.phpfile underincludes/appears inPlugin::load_dependencies(), that every listed path exists, that the list has no duplicates, and thatBooking_Adapterprecedes its consumers. Because there is no autoloader, an omission there is a fatal at load time that no existing check can see —php -lproves each file parses, and it does parse perfectly on its own, while the unit suite never boots the plugin. CI was green for the whole life of the 2.0.0 release.
2.0.0 - Public release (2026-08-08)
Brand-neutral packaging, security and privacy hardening, and safe defaults.
No new product features. Upgrading from 1.x: there is no data migration —
tables, option keys, meta keys, hooks, capabilities, and the REST namespace are
all unchanged. See docs/UPGRADE-2.0.md.
Added
- Privacy tooling. GDPR exporter and eraser registered with WordPress core, so Tools → Export/Erase Personal Data covers Memberistic records. Suggested privacy-policy text registered with the core policy editor. Erasure anonymizes in place and reports what it retains, with the reason: signed waivers (legal-claims basis, overridable via
memberistic_privacy_erase_waivers) and payment records (statutory financial retention). - Data retention. Configurable windows for check-in and activity history, defaulting to keep-indefinitely so no update can start silently deleting a business's records.
- Complete uninstall. Covers every table including the corporate module, all options and transients, user meta, cron events, dynamically created member and per-plan roles, capabilities left on surviving roles, and the generated check-in page. Multisite-aware. Still opt-in, still off by default.
- Theme template overrides. Copy any file from
templates/into amemberistic/directory in the theme to replace it. Newmemberistic_locate_templatefilter, with the resolved path validated to stay inside the theme or plugin template roots. - Plan template library (
templates/plans/) — importable JSON sets for gym, studio, club, association, range, and generic tiered catalogues. Import as inactive so example pricing can never go live unreviewed. - Booking and POS adapters.
Booking_AdapterandPOS_Bridge::adapter()resolve a third-party plugin's hook, table, and CSS names from a single declared preset, auto-detected. Any booking or POS plugin can now be mapped withmemberistic_booking_adapter/memberistic_pos_adapter. Nothing registers and no foreign table is queried when nothing is mapped. - Requirements gate. Unsupported PHP or WordPress shows an admin notice and leaves the plugin inactive, instead of a fatal error that would lock the user out of the screen they need to fix it.
- WooCommerce HPOS and cart/checkout blocks compatibility declared.
- Licensing seam (
includes/class-licensing.php) — the extension points and policy a licence add-on must follow. Contract only: registers no hooks, makes no request. languages/memberistic.pot— 1500 strings across 80 PHP and 11 JS files, with translator comments and plural forms.docs/INTEGRATIONS.md(integration matrix and adapter contracts),docs/UPGRADE-2.0.md,THIRD-PARTY-LICENSES.md.
Changed
- Requires PHP 8.2 (was 8.0) and WordPress 6.8. PHP 8.0 and 8.1 are both past end of security support.
- New installs seed no membership plans.
Plans_Repository::seed_default_plans()is now purely an extension point driven bymemberistic_default_plans; the hard-coded plan arrays are deleted, not commented out. Existing plans are untouched. - Every integration that touches a third-party plugin or off-site service now defaults to off. Email Automation and Waiver Manager remain on: both are built-in, send nothing off-site, and disabling them would stop members receiving their own account notices.
- Frontend design tokens fall back to a neutral palette.
token-bridge.csspreviously mapped--memberistic-*straight onto one theme's tokens with no fallbacks, so the frontend rendered unstyled on any other theme. Every token now prefers a matching theme token and falls back to a contrast-checked default. All fallback pairs verified at ≥ 4.5:1. - Corporate module CSS renamed from
.g2a-corp-*/--g2a-*to.memberistic-corp-*/--memberistic-corp-*. - Email header logo resolves through the logo setting → the site's Custom Logo → the Site Icon →
memberistic_email_logo_url, replacing a hard-coded theme path. - Checkout failure messages name the configured business, falling back to generic support wording.
- Member card ID prefix is now
MEM, filterable viamemberistic_member_id_prefix. Display only — never stored, never a lookup key. - Walk-in role stripping is filterable (
memberistic_walkin_roles) instead of hard-coded to one role slug. - Guest-pass audit tags users with
_memberistic_customer_segmentinstead of a third-party-namespaced meta key. - Plans screen gets a real first-run empty state with a primary action.
- CI matrix moved to PHP 8.2 / 8.3 / 8.4.
Removed
- All partner branding from code, comments, templates, assets, and documentation.
- A background image hotlinked from an external domain.
docs/PARTNERS.md, the internal audit report, and a checkout incident postmortem — internal documents, not product documentation.- The workflow that synced this repository to a private monorepo.
Security
- Verified every REST route carries a real capability check. No route in the plugin uses
permission_callback => __return_true. - Confirmed Stripe webhook signature verification uses
hash_equalswith a 300-second replay window, and rejects unsigned requests before parsing the payload. - Template override resolution validates the filtered path against the allowed roots, so the filter cannot be turned into an arbitrary-file include.
Upgrade notes
- The booking integration's default flipped from on to off. The upgrade writes the previous value explicitly for any site that never saved that screen, so behaviour is preserved.
memberistic_lane_included_plan_slugsno longer has a built-in default. Sites that never set it explicitly get an admin notice asking which plans include bookings; until then, standard booking pricing applies. This fails closed deliberately rather than guessing an entitlement.
1.20.0 - Single membership authority (2026-08-08)
Added
Membership_Service(includes/class-membership-service.php) — the public façade every other plugin calls for membership state. Exposesget_user_membership_status(),user_has_active_plan(),get_user_plan(),is_guest_user(),can_book_lane(),requires_payment_for_booking(),assign_plan_after_payment(),assign_plan_manually()andremove_plan(). Lane-entitlement rules are delegated toIntegrations\Entitlement_Service, never restated, so there is exactly one rulebook.- Global helpers for cross-plugin use:
memberistic_get_membership_status(),memberistic_user_has_active_membership(),memberistic_can_user_book(),memberistic_booking_requires_payment(),memberistic_is_guest_user(). - Plan-assignment guard rails:
assign_plan_after_payment()refuses withoutpayment_verifiedand an order/transaction reference;assign_plan_manually()requires the management capability and a non-empty reason. Both write an activity entry and an audit-log row.remove_plan()expires the membership while preserving all history (bookings, payments, waivers, QR). - docs/PMPRO-REMOVAL.md (in the booking engine) documents the removal, the replacement architecture, the assignment rules and the migration path.
Changed
- Paid Memberships Pro is no longer referenced by any runtime code path anywhere in the system. The only remaining mentions are the CSV importer — one-way migration of a legacy PMPro member base into Memberistic — plus two explanatory comments. This is enforced in CI by
PmproRemovalTest, which carries an explicit allowlist and fails if an allow-listed file stops mentioning PMPro.
1.19.0 - Lane entitlement policy, Guest Pass cleanup (2026-08-08)
Changed
- BREAKING (policy): Only memberships on the included plan slugs (documented setting
memberistic_lane_included_plan_slugs) in an eligible status (defaultactive,comped—memberistic_lane_eligible_statuses) resolve lane bookings to $0. NewEntitlement_Serviceanswers the booking engine'sg2ab_lane_entitlementfilter with a structured snapshot (membership id, plan slug, status, eligibility reason code, pricing type, timestamp). Guest Pass never includes free lane time, even when sold intentionally. g2ab_user_is_memberandg2ab_booking_pricingnow resolve through the entitlement service: trials, past-due, suspended, expired and Guest Pass holders no longer count as members for booking purposes. Linked/family members qualify only through their own authenticated account.
###...
Public Release Version 2.0.0
Memberistic Membership Solutions
A membership operations engine for service businesses running on WordPress.
Plans, members, linked family accounts, digital waivers, check-ins, payments,
and staff workflows — in one admin, backed by its own database tables rather
than bent out of posts and post meta.
- Version: 2.0.0
- Requires: WordPress 6.8+, PHP 8.2+
- Licence: GPL-2.0-or-later
- Author: WordPressistic
Contents
- What Memberistic is
- Who it is for
- Features
- Requirements
- Installation
- Getting started
- Shortcodes
- Developer hooks
- REST API
- Template overrides
- Integrations
- Third-party services
- Privacy and data
- Licensing and support
- Changelog
What Memberistic is
Memberistic runs the operational side of a membership business: who is a
member, what they pay for, who else is on their account, whether their waiver
is current, and whether they are allowed through the door today.
It is built around a few decisions worth knowing up front:
- Its own tables. Members, plans, people, payments, check-ins, and waivers
live in dedicated tables. A membership is not a post, and a member is not a
post-meta bag — so member queries stay fast on a site with tens of thousands
of records, and nothing collides with the rest of the site's content. - A membership can cover several people. Plans declare how many people
they include. Linked family members get their own profile, their own waiver,
and their own check-in history, under one billing relationship. - Waivers are first-class. Signing, expiry, re-consent on a new waiver
version, an immutable signature archive, kiosk and guest signing surfaces. - Staff-facing, not just member-facing. A front-desk dashboard for
check-ins and lookups, alongside the member's own account area.
Who it is for
Any business where people pay for ongoing access and staff need to verify that
access at a counter: gyms and fitness studios, yoga and dance studios, social
and sports clubs, professional associations, climbing walls, makerspaces,
shooting ranges, and similar.
It is a poor fit for content-only paywalls — if all you need is "subscribers
can read this post", a content-restriction plugin will be simpler.
Features
Members and plans
- Unlimited plans with monthly and annual pricing and per-plan included-people
limits - Linked family and additional members under one membership
- Statuses covering active, trial, comped, past due, paused, suspended,
expired, cancelled, and needs-review - Notes, activity timeline, and per-member history
- Bulk actions and saved views in the admin
Waivers and documents
- Digital waiver signing with tokenised member links, guest forms, and a kiosk
mode - Waiver versioning with re-consent when terms change
- Expiry tracking and automated follow-up reminders
- Immutable signature archive with a separate document store
- Import for historical waivers from a previous system
Check-in
- Front-desk staff dashboard with member lookup
- Self-service kiosk page
- QR code verification on the member's digital card
- Check-in history per person
Payments
- Stripe Checkout for subscriptions, with a hosted billing portal for members
- Signature-verified webhooks with replay protection and idempotent handling
- WooCommerce order sync as an alternative path
- Manual and counter payment recording
- Per-plan WooCommerce member discounts
- Transactional templates for the whole membership lifecycle
- Merge tags, per-template overrides, and a delivery log
- HTML layout that picks up the site's own logo and colours
Admin
- React-free vanilla JS admin apps built on
wp.element - Dashboard with revenue history, expiring memberships, and recent activity
- CSV import for members and payments from a previous system
- Corporate/group memberships with invoicing and payment links
Requirements
| WordPress | 6.8 or newer |
| PHP | 8.2 or newer |
| MySQL | 5.7+ / MariaDB 10.3+ |
| WooCommerce | Optional. Any currently supported version. HPOS and cart/checkout blocks are declared compatible. |
PHP 8.0 and 8.1 are both past end of security support and are not supported.
The plugin checks both floors at load and deactivates itself with an admin
notice rather than fataling if they are not met.
Installation
- Upload the plugin folder to
wp-content/plugins/, or install the zip via
Plugins → Add New → Upload Plugin. - Activate it.
- Go to Memberistic → Settings and set your business name, currency, and
email sender details.
On activation the plugin creates its tables, adds its roles, and creates a
single /check-in/ page. It does not create any membership plans, does not
enable any integration, and makes no outbound network request.
Getting started
- Create a plan. Memberistic → Plans starts empty. Add your first
plan, or start from one of the bundled templates in
templates/plans/(gym, studio, club,
association, range, or generic tiered). Templates import as inactive so
you can review the pricing before anything goes on sale. - Set up payments. Settings → Payments, add your Stripe test keys
first. Stripe starts disabled and in test mode. Add the webhook endpoint
(/wp-json/memberistic/v1/webhooks/stripe) and paste the signing secret —
without it, webhooks are rejected. - Write your waiver. Memberistic → Waivers, set the text and the
validity period. - Publish the member pages. Add
[memberistic_plans],
[memberistic_account], and[memberistic_checkout]to pages. - Give staff access. Assign the Memberistic Staff or Manager role to the
people who work the counter, and point them at[memberistic_staff_dashboard]. - Go live. Switch Stripe to live mode and swap in your live keys.
Shortcodes
Member-facing
| Shortcode | Renders |
|---|---|
[memberistic_plans] |
Plan grid with a monthly/annual toggle |
[memberistic_plan] |
A single plan card |
[memberistic_checkout] |
Checkout form |
[memberistic_account] |
Member dashboard — membership, billing, people, waivers, digital card |
[memberistic_login] |
Login, lost password, and password reset |
[memberistic_renewal] |
Renewal prompt |
[memberistic_thank_you] |
Post-purchase confirmation |
[memberistic_payment_failed] |
Failed-payment recovery |
[memberistic_payment_history] |
The member's payments |
[memberistic_booking_history] |
The member's bookings, when a booking engine is mapped |
[memberistic_people] |
Linked/additional members management |
[memberistic_profile_summary] |
Compact profile block |
[memberistic_status] |
Current membership status |
[memberistic_expiring_notice] |
Notice shown when a membership is near expiry |
Staff and public terminals
| Shortcode | Renders |
|---|---|
[memberistic_staff_dashboard] |
Front-desk dashboard |
[memberistic_frontdesk] |
Compact check-in lookup |
[memberistic_kiosk] |
Self-service check-in kiosk |
[memberistic_guest_waiver] |
Guest waiver form |
[memberistic_guest_pass] |
Guest pass issuance |
[memberistic_group_portal] |
Corporate/group member portal |
Developer hooks
Full reference: docs/HOOKS.md.
The ones most people need first:
// Seed plans on first install (the plugin ships none).
add_filter( 'memberistic_default_plans', function ( $plans ) { /* ... */ } );
// Change the customer-facing business name used in emails, waivers, and PDFs.
add_filter( 'memberistic_brand_label', function ( $label ) { /* ... */ } );
// Connect a booking plugin.
add_filter( 'memberistic_booking_adapter', function () { /* ... */ } );
// Connect a point-of-sale plugin.
add_filter( 'memberistic_pos_adapter', function () { /* ... */ } );
// React to lifecycle events.
add_action( 'memberistic_membership_activated', function ( $membership_id ) { /* ... */ } );REST API
Namespace: memberistic/v1. Every route has a real capability check —
there is no permission_callback => __return_true anywhere in the plugin.
Authenticated requests from the admin need the X-WP-Nonce header.
Main route groups:
| Group | Routes |
|---|---|
| Plans | /plans, /plans/{id}, /plans/stats |
| Memberships | /memberships, /memberships/{id}, /memberships/stats, /memberships/bulk-waiver |
| Membership detail | /memberships/{id}/people, /payments, /checkins, /notes, /activity, /emails, /bookings |
| People | /people/{id} |
| Payments | /payments, /payments/stats |
| Check-ins | /checkins |
| Activity | /activity, /activity/types |
| Dashboard | /dashboard/stats, /dashboard/revenue-history, /dashboard/expiring-soon, /dashboard/recent-activity |
/email-templates, /emails/stats, /emails/directory |
|
| Settings | /settings, /settings/pages, /settings/pages-options |
| Saved views | /saved-views, /saved-views/{id} |
| Webhooks | /webhooks/stripe, /webhooks/woocommerce |
Webhook routes are necessarily public but are not unauthenticated: Stripe
requests are verified against the endpoint signing secret with a timing-safe
comparison and a 300-second replay window, and WooCommerce requests against an
HMAC shared secret. Unsigned or stale requests are rejected before the payload
is parsed.
##...