Skip to content

Releases: Wordpressistic/memberistic

Memberistic 2.1.0 payment-integrity

Choose a tag to compare

@Shubochandrosarker Shubochandrosarker released this 12 Aug 19:53
51b1de7

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_succeeded handler 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.deleted handler fell back to metadata.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_failed moved a membership to past_due without 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 a UNIQUE key over (provider, account, event_id);
    the failed INSERT is 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; every v1
    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 preceding SELECT.

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_status on memberships, separate from status. 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 moves past_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, --apply to correct)
    and wp 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.json and 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 to is 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 init for every install. includes/integrations/class-booking-adapter.php shipped in 2.0.0 but was never added to the manual require list in Plugin::load_dependencies(), and nothing autoloads. Waiver_Booking_Bridge::register() calls Booking_Adapter::hook() as its first statement and runs on init priority 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 on init with Class "WordPressistic\Memberistic\Integrations\Booking_Adapter" not found. Booking_Engine, POS_Bridge and Staff_Dashboard reach the same class. The file is now required ahead of every consumer.

Changed

  • Tested up to raised 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.txt says 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 the init fatal 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 calls PHPUnit\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 .php file under includes/ appears in Plugin::load_dependencies(), that every listed path exists, that the list has no duplicates, and that Booking_Adapter precedes its consumers. Because there is no autoloader, an omission there is a fatal at load time that no existing check can see — php -l proves 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 a memberistic/ directory in the theme to replace it. New memberistic_locate_template filter, 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...
Read more

Fixed issues

Choose a tag to compare

@Shubochandrosarker Shubochandrosarker released this 08 Aug 23:00
93190c0

Changelog

All notable changes are tracked here. The format follows Keep a Changelog.

Unreleased

Fixed

  • Fatal error on init for every install. includes/integrations/class-booking-adapter.php shipped in 2.0.0 but was never added to the manual require list in Plugin::load_dependencies(), and nothing autoloads. Waiver_Booking_Bridge::register() calls Booking_Adapter::hook() as its first statement and runs on init priority 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 on init with Class "WordPressistic\Memberistic\Integrations\Booking_Adapter" not found. Booking_Engine, POS_Bridge and Staff_Dashboard reach the same class. The file is now required ahead of every consumer.

Added

  • Dependency-manifest guard test (tests/unit/DependencyManifestTest.php). Asserts that every .php file under includes/ appears in Plugin::load_dependencies(), that every listed path exists, that the list has no duplicates, and that Booking_Adapter precedes its consumers. Because there is no autoloader, an omission there is a fatal at load time that no existing check can see — php -l proves 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 a memberistic/ directory in the theme to replace it. New memberistic_locate_template filter, 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_Adapter and POS_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 with memberistic_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 by memberistic_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.css previously 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 via memberistic_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_segment instead 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_equals with 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_slugs no 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. Exposes get_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() and remove_plan(). Lane-entitlement rules are delegated to Integrations\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 without payment_verified and 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 (default active, compedmemberistic_lane_eligible_statuses) resolve lane bookings to $0. New Entitlement_Service answers the booking engine's g2ab_lane_entitlement filter 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_member and g2ab_booking_pricing now 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.

###...

Read more

Public Release Version 2.0.0

Choose a tag to compare

@Shubochandrosarker Shubochandrosarker released this 08 Aug 13:52
18ebcb9

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

  1. What Memberistic is
  2. Who it is for
  3. Features
  4. Requirements
  5. Installation
  6. Getting started
  7. Shortcodes
  8. Developer hooks
  9. REST API
  10. Template overrides
  11. Integrations
  12. Third-party services
  13. Privacy and data
  14. Licensing and support
  15. 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

Email

  • 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

  1. Upload the plugin folder to wp-content/plugins/, or install the zip via
    Plugins → Add New → Upload Plugin.
  2. Activate it.
  3. 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

  1. 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.
  2. 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.
  3. Write your waiver. Memberistic → Waivers, set the text and the
    validity period.
  4. Publish the member pages. Add [memberistic_plans],
    [memberistic_account], and [memberistic_checkout] to pages.
  5. Give staff access. Assign the Memberistic Staff or Manager role to the
    people who work the counter, and point them at [memberistic_staff_dashboard].
  6. 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 /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.

##...

Read more