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 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.
Removed
- Automatic Guest Pass enrollment. Booking a lane (
g2ab_booking_created/g2ab_booking_paid) or buying a WooCommerce product no longer creates a Memberistic membership. The explicit[memberistic_guest_pass]registration form is the only remaining issue path. Non-member bookers are classified by the booking engine as therange_guestcustomer segment instead (user meta, no membership row). g2ab_advisory_membership_hinthandler: a typed email address no longer reveals whether it belongs to a member, in any form.
Added
wp memberistic guest-pass-auditWP-CLI command: dry-run by default, CSV/JSON report, confidence-bucketed classification (auto-created vs legitimate vs ambiguous), batched + resumable, transaction-safe--applythat expires only high-confidence auto-created memberships (users, bookings, payments, waivers, QR history preserved;range_guestsegment assigned), full audit journal, and--rollbacksupport. See docs/guest-pass-audit.md.- docs/entitlements.md — the lane-entitlement business rules and result contract.
1.18.6 - Branding split, notice scoping, plan entitlements (2026-07-31)
Fixed
- The admin menu now always reads "Memberistic". It previously used the customer-facing brand label, so shortening that label for emails also renamed the entire admin menu and left staff without a recognisable entry.
- Customer-facing branding now prefers the Business name setting over the shorthand Brand label, falling back to the site name. An abbreviation intended for the admin sidebar no longer goes out on membership emails, waivers and PDFs.
- The Stripe webhook health notice was hooked to global
admin_notices, so it rendered at the top of every wp-admin page — including other plugins' dashboards — with no dismiss control and no way to act on it. It is now dismissible, limited to the WordPress dashboard and Memberistic screens, and links to the payments screen. Dismissals are fingerprinted against the current warning set, so a new or worsening problem re-surfaces rather than staying hidden.
Added
- Plans can now mark booking types as included via
settings.included_booking_types(a list of booking type ids, or'all'). A Guest Pass sold at the counter — annually, monthly, or at a custom price — resolves lane bookings to $0 directly, instead of depending on a 100% discount rule being configured for every booking type. g2ab_advisory_membership_hintfilter: reports that a typed email matches an active membership so the booking engine can route the customer to the front desk for a member-rate check. It carries no price and no access, preserving the audit C27 rule that a typed address alone never grants member pricing.
1.18.5 - Public checkout lock hotfix (2026-07-28)
Fixed
- Fixed public checkout rate-limit advisory lock names so MySQL
GET_LOCK()/RELEASE_LOCK()never receive names longer than the 64-character limit. GET_LOCK()results now distinguish acquired locks, real contention, and database/compatibility failures instead of reporting every failure as "Checkout is busy."- Added a durable database-table fallback for checkout rate limiting when advisory locks are unavailable.
- Hardened the public checkout endpoint with no-cache headers, POST/action/nonce enforcement, same-origin referer checks when available, safe Stripe redirect host validation, and clearer customer-facing error messages.
- Prevented the checkout form from submitting twice client-side while preserving server-side idempotency as authoritative.
- Stopped sending the checkout-start membership email before Stripe Checkout Session creation succeeds; payment activation remains webhook/API-authoritative.
Added
memberistic_rate_limitstable and hourly cleanup for durable fallback rate limiting.- Developer incident note with confirmed failure path, cache exclusions, staging QA, deployment, and rollback steps.
1.18.4 - Account dashboard color-contrast fix (2026-07-20)
Fixed
- The member account dashboard's
--ma-*color variables were hardcoded hex snapshots instead of live theme tokens, so the "Welcome back" banner, member-since fields, status pills, sign-out link, and digital member card were structurally incapable of following the site's light/dark mode toggle. Rewired to the live token bridge and added light-mode-specific overrides where the digital card's face stays dark in both modes by design. - Fixed a CSS specificity bug that made member avatar initials nearly invisible (~1.05:1 contrast) in both modes.
1.18.3 - Stripe incident hotfix (2026-07-20)
Fixed
- Stripe webhook processing no longer marks an event processed before the handler finishes, and retryable failures now propagate to Stripe as 5xx instead of silent success.
- Pending signup retries reuse or reconcile the saved Checkout Session before any new Stripe Checkout Session is created.
- Checkout completion validates the authoritative Stripe session/subscription state against the local membership, plan, amount, currency, email, billing cycle, and site mode before activation.
- The thank-you shortcode no longer assumes success from query parameters; it confirms Stripe state server-side and shows honest active, processing, failed, or manual-review states.
- Renewal handlers support both legacy and nested Stripe invoice subscription references.
- Inconclusive Stripe API reconciliation no longer downgrades active recurring members.
- Membership role sync now runs after account provisioning.
Added
stripe_checkout_session_idandstripe_checkout_expires_atmembership columns for safe pending checkout reuse.- Admin webhook health notices and WP-CLI
memberistic stripe-audit/memberistic stripe-reconcilecommands.
1.13.1 — Waiver bridge hardening (2026-07-15)
- Security:
Waiver_Booking_Bridge(theg2ab_waiver_satisfiedhooker) now matches an on-file waiver by email only. The name fallback meant the public booking form's attacker-controlledcustomer_namecould satisfy the waiver requirement by matching any prior signer.Waivers_Archive::find_on_file()keeps name/DOB matching for authenticated staff screens.
1.13.0 — Guaranteed Stripe cancellation (2026-07-15)
- Stripe first, local status second. Cancelling a membership (members app REST action, admin edit screen, legacy wp-admin links) now confirms the Stripe subscription is stopped before the local record flips to cancelled. Previously the DB was marked cancelled first; if the Stripe call then failed, the site showed "cancelled" while Stripe kept billing the member.
- On Stripe failure the membership keeps its current status, the operator gets an explicit error, a persistent admin notice lists every affected member, and retries run automatically with backoff (5m, 30m, 2h, 6h, 24h, 48h). A successful retry completes the local cancellation automatically.
- Explicit override:
POST /memberships/{id}/cancelwithforce=truecancels locally even when Stripe is failing (retries keep running until Stripe confirms). - Already-cancelled / missing subscriptions at Stripe still count as success (idempotent), and inbound webhook cancellations are unaffected.
1.12.0 — Crossmatch unification (2026-07-13)
Changed
- Unified the two divergent copies of the plugin (a monorepo line, which had reached 1.10.7, and the dedicated memberistic-membership-solutions repo line, which had reached 1.11.0). Both locations now carry the identical 1.12.0 tree containing the newest code from each line: the monorepo's 1.10.1–1.10.7 fixes (Stripe cancel propagation, token-bridge stylesheet, audit hardening) and the dedicated repo's Advanced FFL Checkout bridge (1.11.0). No functional changes beyond the merge itself.
1.11.0 — Advanced FFL Checkout bridge
Added
- Advanced FFL Checkout integration. New read-only bridge (
FFL_Checkout_Bridge) surfaces a member's own online FFL firearm-transfer history — from the separateadvanced-ffl-checkoutWooCommerce storefront plugin — on their account dashboard (matched by email against that plugin'stransferstable, same-site direct read, the same pattern the POS Bridge and coreSTORE Bridge already use), plus a quick heads-up on the staff "Member Verification" QR card if they have an open transfer in flight. New "Advanced FFL Checkout" card on Memberistic → Integrations (off by default; gated on that plugin being active).
1.10.1 — Cancelling on the site now cancels the Stripe subscription
Fixed
- Cancelling a membership in WordPress never cancelled the Stripe subscription, so Stripe kept billing the member. Every on-site cancel path (the members app's Cancel action, an admin edit setting Status → Cancelled, and the legacy wp-admin members page) only flipped the local DB status.
Stripe_Servicenow has acancel_subscription()API call, and a listener onmemberistic_membership_status_changedcancels the member's Stripe subscription whenever a membership is cancelled on the WordPress side. The call is idempotent ("already cancelled"/"no such subscription" responses are treated as done), skipped while inbound Stripe webhooks are being processed (Stripe told us — no need to tell Stripe back), and a failed cancel is logged to the membership's activity feed and the PHP error log instead of silently leaving billing live. - The admin REST cancel action and edit-screen status changes bypassed the canonical
change_status()path, so thememberistic_membership_status_changedhook (Stripe propagation, coreSTORE bridge) never fired for them. Both now route status changes throughchange_status().
Added
memberistic_stripe_cancel_at_period_endfilter — returntrueto stop billing at the end of the paid period instead of immediately.
1.10.0 — coreSTORE bridge, WooCommerce member discounts, dashboard Shop tab
Added
- coreSTORE (Coreware) POS bridge. New integration — fully independent from the POS Bridge — that pushes membership state into the shop's hosted coreSTORE over its REST API (
x-api-key): per-plan price-tier mapping (coreSTORE's tier pricing then applies member discounts at the register automatically), optional custom fields for plan/status/expiry on the cashier's customer screen, live push on activation/renewal/lapse, daily reconciliation cron, Test Connection + Sync Now buttons, and a capped sync log. Credential-ready: configure URL/key on Integrations whenever the store issues them. - Automatic WooCommerce member discounts. Each plan carries its own discount %, category include/exclude rules, and a skip-sale-items switch (Integrations screen). Active members get a labeled negative fee at cart/checkout ("Member discount — (10%)"), orders are stamped with the saving (
_memberistic_member_discount), and product pages show the member price. Membership products are never discounted. - Shop tab on the member dashboard. The
/account/dashboard now includes the member's WooCommerce world: order count / total spent / lifetime member savings, recent orders with per-order savings and view links, downloads, billing & shipping addresses with edit links, and payment-methods / account-details links — no separate my-account visit needed. memberistic_membership_status_changedaction fires from the canonicalchange_status()path so integrations react to lapses/cancellations without polling.
1.9.9.5 — Post-login cache-buster
Fixed
- Sign-in could still appear dead behind an aggressive page cache/CDN. After a successful login,
Authnow redirects to the account page with a uniquemloginquery arg, forcing a cache miss so a stale logged-out copy of/account/(which is the login form again) can never be served back to the freshly signed-in member. Pair with a one-time full CDN cache purge after deploying.
1.9.9.4 — Sign-out
Fixed
- Members couldn't sign out. The theme filters
logout_urlto/login/?action=logout&redirect_to=…&_wpnonce=…, butAuthonly handled login / lostpassword / reset — so the logout link landed on the login page with the member still signed in.Auth::maybe_logout()now handles it: verifies the standardlog-outnonce, callswp_logout(), and redirects toredirect_to(default home) through thelogout_redirectfilter. A stale-nonce link falls through to a one-tap confirm view (render_logout_confirm()) with a fresh logout URL.retrievepasswordis also accepted as a legacy alias oflostpassword.
Changed
- The account template's "no active membership" state now includes a Sign Out link, so a signed-in visitor without a membership isn't stranded.
1.9.9.3 — Live-site hardening
Fixed
- Forgot-password still looked dead on the live site because a page cache / CDN served the cached login HTML for
/login/?action=lostpasswordand/login/?action=rp.Auth::prevent_login_cache()now sendsnocache_headers()and definesDONOTCACHEPAGE/DONOTCACHEOBJECTetc. on the login surface (detected by auth action, login/account page id, or the login/account shortcode), so caching plugins and CDNs keep it dynamic. Clear the site cache once after updating. - "Set up member logins" admin notice would not clear when a flagged membership had no email (the repair tool can't create a login without one), leaving a permanent banner.
count_missing_logins()is now email-aware (joins the people table and only counts memberships that have a real email and no linked user), andensure_user_for_membership()falls back to any linked person's email when the primary row has none — common in imported data — so imported members are reachable and the notice clears once the actionable work is done.
Changed
- Settings screen now has a single Save button. Removed the duplicate header "Save changes" button; the sticky footer save bar remains.
1.9.9.2 — Member login & password recovery
Fixed
- Forgot-password / reset-password did nothing. Themes that hide wp-login.php filter
login_url/lostpassword_urlto a branded/login/page, and WP's reset emails carrywp-login.php?action=rplinks — but[memberistic_login]only rendered the login form, so/login/?action=lostpasswordand/login/?action=rpdead-ended. A newFrontend\Authhandler turns/login/into a full, theme-independent auth surface: it processes login (viawp_signon), lost-password (viaretrieve_password) and reset-password (viacheck_password_reset_key+reset_password) ontemplate_redirect, with PRG redirects and inline notices. Works even when wp-login.php is blocked/redirected. - Password links bypassed the branded page. WP builds reset and "set your password" links with
network_site_url('wp-login.php?action=rp…'), side-stepping the theme filter.Authnow filtersretrieve_password_messageandwp_new_user_notification_emailto rewrite those URLs to/login/?action=rp, so every link lands on the working handler. - Staff-added and imported members had no WP account. Only the Stripe checkout and corporate paths created users; admin "Add Member" and CSV import left a membership with no
primary_user_id, so those members couldn't sign in or set a password. NewAccount_Provisionercreates (or links) a WP user for a membership's primary person and emails a set-password link — wired to run onmemberistic_membership_activated, on staff member creation (RESTcreate_item), and as a one-click bulk "Set up member logins" repair tool surfaced via an admin notice. Idempotent; the set-password email is sent at most once per member.
1.46.1 — Booking integration correctness
Fixed
- Member booking discounts now apply.
booking_discount()/discount_percent_for_booking_type()readbooking_discountsfrom$membership['settings'], but thememberistic_membershipstable has nosettingscolumn (onlymemberistic_plansdoes), so the rules were always empty and members only ever got the booking type's basemember_discount. Both now resolve the plan viaPlans_Repository::get($membership['plan_id'])and read the rules from the plan settings. - Timezone-correct expiry check.
membership_is_bookable()appended' 23:59:59'torenewal_date, but that column is aDATETIME(Y-m-d H:i:s), producing a "double time specification" that made every active member read as not bookable (breaking member booking, pricing and member content access). It now takes the date part only and builds the end-of-day inwp_timezone(), treating''/0000-00-00[ 00:00:00]as non-expiring. - Booking-form pages are never content-restricted.
Content_Restrictionsnow exempts any page containing[g2a_lane_booking]/[g2a_booking_form](filterable viamemberistic_restriction_exempt_post) across all four gates, so a mis-set required-plan can't hide the lane-booking UI from guests. - Member state cleared on expiry. A new
clear_roles_for_membership()onmemberistic_membership_expiredstrips the member + plan roles and thememberistic_active_plan_id/_namemeta, guarded so a user who still holds another active membership keeps their roles (the survivor check excludes the just-expired row). - Consistent member resolution for content gating.
current_user_has_any_plan()now resolves through the booking integration'sget_active_membership_for_user()(honors email-linked memberships + rechecks renewal) instead of a status-onlyprimary_user_idlookup. - Correct membership level id in booking metadata.
g2ab_booking_pricingnow carries the plan id (was the membership row id) so the booking engine's role/label assignment looks up the right plan. - Stripe checkout currency allowlist.
create_checkout_session()validates the configured currency against a known list and falls back tousd, so a corrupted setting can't make Stripe reject the session.
1.45.0 — Integrations toggle fix + POS Bridge + SMS module + waiver module
Fixed
- Integration toggles (Verifyistic et al.) finally persist.
register_setting's sanitize callback ran on everyupdate_option('memberistic_settings')— including the Integrations page save — and returned a fixed allowlist that stripped everyintegration_*key. Toggling Verifyistic ON, saving, and reloading showed it OFF again. The sanitizer now persists every Integrations Registry toggle, the Verifyistic sub-options,email_reply_to_address,verifyistic_max_age_days, and passes through unknown scalar keys instead of deleting them. - React Settings → Integrations tab rendered only the WooCommerce toggle; it now renders every registry module (with availability/coming-soon states) from a new
_integrationssnapshot onGET /settings.
Added
- POS Bridge module (Integrations toggle, requires a connected POS): answers the POS membership-lookup filter with live Memberistic status/plan/expiry/benefits at the counter, feeds upcoming range bookings to the POS dashboard via
g2a_pos_membership_bookings, and stampspos_customer_idon membership rows. - SMS Notifications (Messageistic) module (Integrations toggle, requires Messageistic): one switch controls membership + booking SMS sent through Messageistic (incl. the local SMS gateway).
- Waiver Manager module card replaces the "Waiver Provider — coming soon" placeholder: the built-in waiver system (tokenized member signing, guest + kiosk surfaces, immutable archive, expiry tracking) is now surfaced as a real module; the toggle gates the booking-engine check-in mirror.
- New lifecycle hooks for add-ons:
memberistic_membership_expiring( $membership_id, $days_out )(30/7/1-day windows, deduped) andmemberistic_membership_expired( $membership_id ).
1.43.5 — Stabilization & integration hardening
Fixed
- Duplicate Stripe payment row on first invoice (handle_invoice_succeeded now defers to handle_checkout_completed for the initial charge).
- Activity log mis-classified
payment_past_dueevents; added to the valid-types whitelist alongsidepayment_receiptandpayment_refunded. - WooCommerce bridge now respects the Integrations Registry toggle.
- WooCommerce webhook endpoint refuses requests when the shared secret is empty (was: skip verification → effectively open auth).
- Verifyistic bridge cookie reader enforces a 30-day max age (default; configurable).
- Stripe webhook dedupe persists across object-cache flush via a capped option list.
- Hardcoded "From $29.99/mo" on login shortcode now reads the minimum active plan monthly price.
- Mesa AZ (UTC-7) and other non-UTC sites: monthly KPI cutoffs use
wp_date()withcurrent_time('timestamp'). - Stripe checkout no longer creates duplicate pending memberships on form refresh.
- Generic fallback email "Status: {plan_name}" merge tag corrected to "{status}".
- Stripe subscription deletion: subscription_id is the primary lookup, metadata is the fallback.
1.40.0 — Public kiosk + profile image upload
- Auto-created
/check-in/page and[memberistic_kiosk]shortcode for front-desk QR scan workflows. - Profile image upload REST routes (
POST /profile/image).
1.38.0 — Stripe billing portal
- Customer-facing "Manage billing" link via Stripe's Billing Portal.
1.35.0 — Corporate Guest auto-enroll
- Buyers without a membership are auto-enrolled on a hidden Guest Pass plan on
woocommerce_payment_completeand ong2ab_booking_created/paid.
1.34.0 — Schema 1.4.0 / 1.5.0
- Added
memberistic_waiver_signatures,memberistic_documents,memberistic_waivers_archivetables.
1.33.0 — Automatic Guest members for non-members
Non-members are now remembered. Anyone who books a range lane or buys a product without a membership level is saved automatically as a Guest member — with their own WordPress login account, a Digital Card with dynamic QR, a waiver request, and their contact details on file — so the next visit the front desk can pull them up by QR scan and their information persists.
Added
- Auto Guest enrollment from WooCommerce purchases.
Corporate_Guest_Service::maybe_enroll_from_wc_order()hookswoocommerce_payment_complete+woocommerce_order_status_completed/processing. Any buyer whose billing email has no membership is enrolled on the hidden Guest Pass plan (login account + QR Digital Card + waiver email). Membership/group-invoice orders are skipped (they are not product sales), and orders without an email are skipped. - Auto Guest enrollment from range bookings.
Corporate_Guest_Service::maybe_enroll_from_booking()hooks bothg2ab_booking_created(passes a booking id) andg2ab_booking_paid(passes the booking row). The handler normalizes either payload, readscustomer_name/customer_email/customer_phone, and enrolls the booker as a Guest member if they are not already a member.
Changed
- Idempotent by design. A new
email_has_membership()guard means existing members and existing guests are never re-enrolled and never re-emailed on repeat purchases or bookings. A booker/buyer with no name falls back to the email local-part so a card can always be issued.
Notes
- No DB schema changes — guests reuse the existing membership, person, waiver, check-in, and QR verification infrastructure.
1.10.0 — Operations dashboard release
A broad admin operations upgrade: pagination, KPI cards across every list page, a card-based Plans console, a permissive importer that never drops a row, and a React-rebuilt Email Directory.
Added
- Members page — KPI cards & MoM growth. Eight cards: Total Members, Active, Pending, Past Due, Expired, Cancelled, New This Month (with month-over-month % growth indicator), and Waiver Missing. Powered by a new
GET /memberistic/v1/memberships/statsendpoint. - Members page — server-side pagination.
GET /memberistic/v1/membershipsacceptslimit+offsetand returnsX-WP-Total/X-WP-TotalPagesheaders. NewMemberShips_Repository::count_all()powers the totals. UI gains «/Prev/Next/» controls plus a per-page selector (25/50/100/200). - Members page — bulk waiver action. New
Change waiver status…bulk action with a waiver-status sub-dropdown. Posts to a newPOST /memberistic/v1/memberships/bulk-waiverendpoint that updates every person on each selected membership and writes awaiver_signed/waiver_expiredactivity row per membership. - Plans page — card UI. Replaces the table with an animated card grid. Each card surfaces price (monthly + annual), capacity, the top 4 benefits, status, "Featured" ribbon, and a footer with three live member-count tiles: Total / Active / Other. Powered by a new
GET /memberistic/v1/plans/statsendpoint. - Payments page — KPI cards. Six cards: Lifetime revenue, Revenue this month (with MoM growth %), New-member payments (first completed payment per membership), Renewal payments (second-or-later), Failed payments, Visible on page. Powered by a new
GET /memberistic/v1/payments/statsendpoint andPayments_Repository::stats_summary()+count_all(). - Payments page — pagination. Same headers + UI pattern as the Members page. Per-page picker, prev/next/first/last, with full filter context preserved.
- Payments page — richer CSV. Export now includes Payment ID and Created columns.
- Emails page — React directory. Brand-new
assets/admin-emails.jsconsole with KPI cards (Sent today, Sent this week, Sent this month, Delivery rate, People with email), filters by member status and waiver status, paginated list with status pills and "Open member" links. CSV export gathers the full filtered set (chunked at 1000/req) and lays it out in 13 properly labelled columns including waiver signed/expires dates. - Import — "No Plan" sentinel. Auto-created hidden plan that catches imported members with no recognised tier so historic rows are never dropped.
- Import — Instore stub creation for orphan payments. Orders whose email does not match an existing member create a stub Instore member; emailless orders attach to a single shared "Instore Walk-in" membership. Disposition is logged per row in the dry-run preview.
Changed
- Import — members analyzer / committer. Now imports every row including those with no email (kept as primary contact via full name), unrecognised levels (attached to No Plan), and expired memberships (status auto-set to
expired). Dry-run preview adds counters for expired, no-plan, and no-email rows. - Import — payments analyzer / committer. Replaces the silent skip path with the orphan-membership flow above. Status mapping now also recognises
failed/error/declined. Disposition column added to sample table. - Email Directory PHP page. Reduced to a React mount point; the legacy server-rendered
?memberistic_export=emailsURL still works (now exports 13 columns instead of 6) for bookmarks.
REST API additions (v1)
GET /memberships/statsPOST /memberships/bulk-waiverGET /plans/statsGET /payments/statsGET /emails/directory— paginated email directory with search + filters; setsX-WP-Total/X-WP-TotalPages.GET /emails/stats— sent today/week/month, delivery rate, contact coverage.GET /membershipsandGET /payments— now acceptoffsetand emitX-WP-Total/X-WP-TotalPages.
Notes
- No DB schema changes. DB version stays at
1.2.0. - Version bumped to
1.10.0.
1.9.0 — Admin-side waiver management
Added
- Per-person Edit action on the Members detail panel. The People tab in the slide-in detail panel now exposes an inline editor for every linked person on a membership. Admins can update full name, email, phone, relationship, waiver status (
missing/signed/expired/needs_review/rejected), waiver signed date, waiver expiry date, and the person's own active/inactive/removed status without leaving the page. - Remove action for non-primary linked members directly from the People row (the primary member is still protected server-side).
- Waiver expiry hint in the People table — the waiver pill now shows the expiry date underneath when one is set, so admins can see at a glance which signed waivers are aging out.
Changed
PUT /memberistic/v1/people/{id}now declares a typed argument schema coveringfull_name,email,phone,date_of_birth,relationship,waiver_status(enum),waiver_signed_at,waiver_expires_at,status, andnotes. Previously the route accepted those fields but did not validate them at the REST layer.- When
waiver_statusis set tosignedand nowaiver_signed_atis provided (and none was previously recorded), the API auto-stampswaiver_signed_atto the current time. - Whenever
waiver_statuschanges viaPUT /people/{id}, an activity row is written:waiver_signedwhen the new status issigned,waiver_expiredforexpired/rejected/needs_review, andmembership_status_changedotherwise. This makes the change visible on the Activity tab and Activity admin page.
Notes
- UI-only update. Database schema unchanged (
memberistic_peoplealready hadwaiver_status,waiver_signed_at,waiver_expires_at). - Version bumped to
1.9.0. DB schema version stays at1.2.0.
1.8.0 — Member import
Added
- Import page (
Memberistic → Import) — upload a Paid Memberships Pro (PMPro) members CSV, or a payment/orders export, and bring the data into Memberistic. Columns are auto-detected through an alias map; legacy PMPro levels are mapped onto plan slugs; "Additional Member" levels import as linked people attached to a primary membership with open capacity. - Two-step flow with a dry-run preview (row counts, plan breakdown, linked-member and duplicate detection, sample rows) before anything is committed.
- Imported members are linked to existing WordPress user accounts by email when one is found.
Changed
- Version bumped to
1.8.0. DB schema unchanged.
1.7.1 — Partnership documentation release
Documentation-only release. No functional code changes.
Added
- Plugin header —
DescriptionandAuthorfields updated to acknowledge the partnership.Plugin URIandAuthor URIcontinue to point to WordPressistic.
Changed
- Version bumped to
1.7.1. DB schema unchanged at1.2.0.
1.7.0 — Audit + spec-completion pass
Full audit against the canonical Memberistic feature spec. Fixes 20 bugs and gaps and adds the remaining spec-required features so the engine is feature-complete against CORE_MEMBERSHIP_PLAN_FEATURES.txt.
Added
- Default plans seed — a starter plan catalogue is created automatically on first install. (Removed in 2.0.0: new installs now start with an empty Plans screen.)
- Email automation overhaul
- Six new transactional templates:
membership_renewed,expiring_30_days,expiring_7_days,expiring_tomorrow,membership_expired,linked_member_added,staff_manual. The required-template set is now 12 of 12. - Full merge-tag rendering with all 20 documented tags (
{member_name}…{logo_url}) plus amemberistic_email_merge_tagsfilter. wp_memberistic_email_logstable records every send with template, recipient, subject, status, and any failure message.
- Six new transactional templates:
- Daily cron jobs via the new
Schedulerclass:- Renewal reminders at 30 / 7 / 1 day windows.
- Auto-expire of active memberships past their renewal date.
- Waiver follow-up nudges for active memberships still missing a signed waiver.
- REST API completions
PUT /people/{id}andDELETE /people/{id}(the primary member is protected).POST /webhooks/woocommercewith HMAC SHA-256 signature validation.GET /memberships/{id}/bookingsnow returns{ bookings, checkins }instead of just check-ins.GET /membershipsaccepts 9 filter args:billing_cycle,waiver_status,expiring_in_days,checked_in_today,created_from,created_to,limit(in addition to the existingsearch,status,plan_id).
- WooCommerce bridge
WooCommerce_Bridge::ensure_default_products()creates the hidden virtual products (one per plan × Monthly / Annual) and matches them to existing SKUs on re-run.- Refund and cancel hooks flip the linked membership to
cancelledand log the activity.
- New tables:
wp_memberistic_email_logs,wp_memberistic_integrations. Migration1.2.0creates them on existing installs. - New roles:
memberistic_kiosk_operator,memberistic_pos_staffplaceholder roles for the future KIOSK and POS modules. - New statuses:
suspended,needs_revieware now accepted everywhere and rendered as proper badges. - New activity event types:
membership_expired,membership_upgraded,membership_downgraded,waiver_expired. - Settings — added
timezone,logo_url,accent_brand_color,woocommerce_webhook_secretto the sanitizer.
Fixed
- Stripe
invoice.payment_succeedednow handled — recurring renewals advancerenewal_date, record a payment row, fire themembership_renewedactivity, and send the renewal receipt. - Stripe
customer.subscription.deletedfallback — looks up the membership bystripe_subscription_idif the metadata is absent. memberistic_validate_status()accepts the full 10-status set — previously it silently coercedsuspendedandneeds_reviewtopending.People_Repository::update()partial updates preserve existing values — previously a partial update would clobberstatus/waiver_statusback to defaults.- Member search now joins linked-member names and matches Stripe / Woo / POS customer IDs — previously search only matched the primary person.
- People sanitizer accepts
waiver_signed_atandwaiver_expires_at— schema columns are now writable.
Changed
Email_Servicerewritten around merge-tag substitution and email-log persistence.Scheduleris wired into activation (schedules) and deactivation (clears scheduled events).Activity_Repository::types()and the internal whitelist now cover the full 21 spec event types.
Documentation
docs/AUDIT_REPORT.md— complete, sectioned audit against the canonical spec. Every spec section reportsDone/Partial/Roadmapand lists fixes shipped in this release.README.mdrewritten for client delivery: what's in the box, install steps, REST surface, filter map, repo layout.CHANGELOG.mdintroduced as the durable history.
1.6.0 — Phase 5
- Retired the legacy server-rendered views in favour of React consoles.
- Email automation foundation.
- Saved-filter views per user.
- React Settings app on top of the new Settings REST controller.
1.5.6
- Membership user roles, plan-specific roles, and post / page restriction controls.
- Modern frontend restriction overlay for visitors without the required plan.
1.5.5
- Polished admin plan cards and smoother branded card motion.
- Expanded integrations panel with connected and coming-soon addon cards.
- Public-booking integration: active members detected by email for included bookings.
1.5.4
- Hardened public checkout redirects, restyled default login/result pages, and improved staff / admin operational screens.
1.5.3
- Staff frontend dashboard for walk-in membership creation, member search, check-ins, and recent booking visibility.
- Fixed Settings screen tabs.
- Added frontend dashboard page mapping.
1.5.2
- Routed public Stripe checkout submissions through the mapped Memberistic Checkout page to avoid login redirects before Stripe Checkout opens.
1.5.1
- Memberistic checkout form submits via a public frontend route so Stripe Checkout is not blocked by wp-admin/login rules.
- Connected to real booking engine hooks.
1.5.0
- Linked online Stripe checkout memberships to WordPress users or email-matched accounts.
- Real frontend account, people, payment history, and booking / check-in history shortcode views.
- Lifecycle email foundation for membership creation, activation, payment failure, cancellation, renewal, and waiver reminders.
- Booking Engine eligibility, discount, and activity hooks.
- Optional WooCommerce completed-order bridge foundation.
- Expanded REST endpoints for plan management, membership update / delete, payments, activity, bookings, renew, cancel, upgrade, and dashboard queues.
- Dashboard reporting with waiver / payment follow-up, expiring renewals, and revenue by plan.
1.4.3
- Ignored known legacy / common membership page mappings when branded Memberistic pages are available.
1.4.2
- Branded page URL fallbacks for checkout buttons and Stripe return URLs.
1.4.1
- Branded Memberistic URL slugs; remap action for existing installs.
1.4.0
- Stripe Checkout settings.
- Frontend membership checkout form.
- Pending membership creation before Stripe redirect.
- Stripe Checkout Session creation without the Stripe PHP SDK.
- Stripe webhook endpoint for checkout completion, failed invoices, and subscription cancellation.
1.3.0
- Frontend shortcode foundation.
- Public membership plan cards with monthly / annual toggle.
- Checkout, account, login, and renewal shortcode foundations.
- Page mapping settings and required-page creation action.
1.2.1
- Rounded dashboard revenue values to clean currency precision.
1.2.0
- Staff operation foundation: manual payments, check-ins, and staff notes.
- Functional Payments, Check-Ins, and Activity admin pages.
- Staff operation forms on the member profile screen.
- Authenticated REST endpoints for payment, check-in, and note creation.
1.1.0
- Phase 2 membership creation foundation.
- Primary person creation and linked member management.
- Member profile admin screen with people and activity sections.
- Authenticated memberships REST endpoints.
1.0.1
- Hardened REST controller compatibility with WordPress core inheritance.
- Removed PHP 8-only type syntax from runtime code for broader staging-host safety.
1.0.0
- Initial Phase 1 foundation build.