=== Rosterkeeper for Clubs ===
Contributors: bnraines
Tags: membership, club management, csv import, paypal, member directory
Requires at least: 6.2
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 3.14.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

A complete membership management system for clubs: configurable member numbers, online join/renewal with PayPal, and a public member roster shortcode.

== Description ==

Rosterkeeper for Clubs gives a club a full membership back office inside WordPress:

* A custom member roster (name, address, phone, email, emergency contact, expiration date) with a configurable, unique, sequential member number.
* An admin screen to add, edit, search, sort, and bulk-delete members.
* CSV import, with column mapping so it adapts to whatever spreadsheet you already have — fully functional: creates new members, and updates existing ones when a row's member number already matches. CSV export, and the option to choose skip-vs-update behavior per import, are available via the optional Pro add-on described below.
* A public `[rkfc_roster]` shortcode showing a sortable, searchable member directory, with configurable display rules for expired members and an admin-selectable choice of which fields appear as columns.
* Online joining and renewal via two front-end forms (`[rkfc_join_form]` and `[rkfc_renew_form]`), each collecting the required info, a waiver/terms acceptance, and then a PayPal payment. A successful, server-verified payment automatically creates or updates the member's roster record — no manual data entry required.
* A self-service "update my info" form (`[rkfc_update_profile]`) that lets a member update their own contact info without any admin involvement — secured by emailing a one-time link to the address already on file, so one member can never look up or edit another's record.
* A "resend my welcome email" form (`[rkfc_resend_welcome]`) so a member can trigger the configurable welcome email again by looking themselves up with member number + last name — no data exposure since it only resends an email, with a short cooldown to prevent repeated resends to the same person.
* A choice of three membership-year models, configurable from Settings: **fixed season** (a defined membership year with an independently configurable in-season/off-season pricing window), **rolling** (a single flat annual fee, always expiring exactly one year from the join/renewal date), or **free** (no fee and no PayPal step at all — visitors still complete the join/renewal form and agree to the waiver/terms).
* A "Join Requests" log for troubleshooting online payments. Bulk "Email Members" is available via the optional Pro add-on described below.
* A manual "Send Renewal Notice" button next to each member in the admin list (with a "Last Notified" column so you can see at a glance who's already been nudged), for a one-off configurable renewal reminder — real troubleshooting value on its own (e.g. a member says a notice went to spam), and every send is recorded on its own Renewal Notice Log page with CSV export. Want this fully automatic instead — up to four notices per member, timed automatically before and after expiration, running hands-off in the background? That's what the optional Pro add-on adds.
* Every club-specific label (the member number's name, the club's name used in payment descriptions and the waiver heading, the waiver/terms text itself) is editable from Settings — nothing requires touching code.
* Optional "Rosterkeeper for Clubs - CSV & Email Pro" add-on (sold separately, not on WordPress.org) adds CSV export, the choice of skip-vs-update behavior per CSV import, a Merge Supplemental Data tool, the bulk email tool, and fully automated multi-notice renewal reminders. Import & Export now includes a real, fully-functional CSV import on the free tier (see above — no restrictions); Email Members remains a Pro-only tool, showing a brief upsell message on the free tier in its place.

= Membership Year Models =

**Fixed Season**: choose a season-start month and day (e.g. April 1) — this governs when membership years begin and expire (memberships always expire the day before the next occurrence of that date, e.g. March 31). Separately, an independently configurable In-Season Window (a start month and end month, which can wrap around the year) determines which months use the In-Season Fee versus the Off-Season Fee — this doesn't need to align with the season-start date at all, so pricing can match your club's actual activity season regardless of your fiscal year.

**Rolling**: a single flat fee, no seasonal split. Every membership expires exactly one year from whenever it was purchased or renewed, with no fixed fiscal year.

**Free**: no fee is charged and the PayPal payment step is skipped entirely for both `[rkfc_join_form]` and `[rkfc_renew_form]`. Visitors still complete the full form and must agree to the waiver/terms — membership is required, it's just free. Dated the same way as Rolling (one year from signup/renewal).

Switching models only affects new signups/renewals going forward; it never retroactively changes any expiration date already on file.

== External services ==

This plugin connects to PayPal's API to process membership payments for the online join and renewal forms (`[rkfc_join_form]` and `[rkfc_renew_form]`). This only happens when a visitor actively submits one of those forms and proceeds to pay — it does not run in the background or on any other page.

What is sent, and to whom:

* When a visitor clicks the PayPal button, the plugin's server creates a PayPal order via PayPal's REST API (`api-m.paypal.com` for live transactions, `api-m.sandbox.paypal.com` in Sandbox/testing mode), sending the membership fee amount and a random, non-identifying reference token.
* The visitor's browser then loads PayPal's own Checkout JavaScript SDK directly from PayPal (`https://www.paypal.com/sdk/js`) to complete the payment on PayPal's site.
* After the visitor approves payment, the plugin's server calls PayPal's API again to capture (finalize) the transaction and confirm it succeeded, before creating or updating the corresponding member record.

No member contact information (name, address, email, etc.) is sent to PayPal by this plugin; only the payment amount and an anonymous reference token are sent as part of the order. PayPal's own checkout process, once the visitor is on PayPal's site, is governed entirely by PayPal's own privacy policy and terms of service:

* PayPal Privacy Policy: https://www.paypal.com/us/legalhub/privacy-full
* PayPal User Agreement: https://www.paypal.com/us/legalhub/useragreement-full

A site administrator must configure their own PayPal API credentials (Client ID and Secret) under Club Members > Settings for this feature to function; it does nothing with PayPal until that's done.

== Installation ==

1. Upload the plugin files to `/wp-content/plugins/rosterkeeper-for-clubs`, or install the zip directly via Plugins > Add New > Upload Plugin.
2. Activate the plugin. This creates the plugin's own database tables — no existing WordPress data is touched.
3. A "Club Members" menu appears in your WordPress admin sidebar. Visit Club Members > Settings to configure the member number label, club name, membership-year model, and (if you'll use online join/renewal) your PayPal credentials.
4. Place `[rkfc_roster]`, `[rkfc_join_form]`, and/or `[rkfc_renew_form]` on pages as needed.

== Frequently Asked Questions ==

= Does this require PayPal? =

No. The member roster, admin management, and online join/renewal are separate — the join/renewal forms are the only thing that needs PayPal configured.

= Can I rename "member number" to something else? =

Yes. Club Members > Settings has a "Member Number Label" and a "Member Number Short Label" field, used throughout the admin, the public roster, and the join/renewal forms.

= What happens to existing members if I switch membership-year models? =

Nothing retroactive. Changing between Fixed Season and Rolling only affects new signups and renewals going forward.

= Does this plugin track or phone home any usage data? =

No. The only external network calls this plugin makes are the PayPal API calls described above, and only when a visitor actively submits a payment form.

== Screenshots ==

1. Admin member list with search and sortable columns.
2. Add/Edit Member screen.
3. Public roster shortcode with search, status filter, and sortable columns.
4. Online join form with PayPal payment step.
5. Settings screen showing configurable member-number label, club name, and membership-year model.

== Changelog ==

= 3.14.2 =
* Two lingering references to "riding/activity season" (in the Help text
  and the live Settings page for the In-Season Window field) still leaned
  cycling-specific despite the rest of the plugin being genericized for any
  club type — caught while rebuilding the admin screenshot gallery.
  Shortened to just "activity season."

= 3.14.1 =
* Minor follow-up to 3.14.0's rename: the one-time migration's RENAME TABLE
  statement used raw string interpolation for the table names instead of
  $wpdb->prepare() with a placeholder. Not a real injection risk (the
  values come entirely from $wpdb->prefix plus hardcoded literals, never
  user input), but there's no reason not to use the sanctioned %i
  identifier placeholder here instead, so switched to that.

= 3.14.0 =
* Renamed the plugin from "Club Member Manager" to "Rosterkeeper for Clubs"
  (slug: rosterkeeper-for-clubs) following a WordPress.org review finding
  that the previous name was too generic and similar to existing plugin
  names in this space. The internal prefix used throughout the codebase
  also changed from bcm_ to rkfc_ (4+ characters, per directory
  requirements) — functions, classes, option keys, database tables, and
  shortcode tags.
* Existing installs upgrade transparently: a one-time migration on this
  version renames the plugin's three database tables and copies over all
  stored settings from their old names to the new ones automatically, and
  every shortcode ([bcm_roster], [bcm_join_form], [bcm_renew_form],
  [bcm_update_profile], [bcm_resend_welcome]) keeps working under its old
  name indefinitely alongside the new one, so no existing page content or
  member data is affected by the rename.

= 3.13.4 =
* Fixed a real WordPress.org compliance issue flagged during review: the CSV
  import previously skipped updating existing members on a duplicate member
  number even though the capability to do so (RKFC_DB::update_member())
  already existed and was used elsewhere in this same plugin, and the UI
  marketed that as a Pro-only upgrade. This is exactly the kind of
  artificially-restricted-feature pattern the directory guidelines prohibit,
  even when the underlying code is already present. The free import now
  genuinely creates or updates as appropriate, with no restriction. Pro's
  real remaining differentiators for this feature are CSV export, choosing
  skip-vs-update behavior per import, and Merge Supplemental Data — each a
  genuinely separate capability, not a hobbled version of this one.
* Also fixed three inline <script> tags flagged in the same review — moved
  to a properly enqueued assets/js/bcm-admin.js, loaded only on the two
  admin pages that actually use it, with translated strings passed via
  wp_localize_script() instead of embedded directly in the markup.

= 3.13.3 =
* Fixed several defaults that still referenced this plugin's original club (Solomons Island Cycling) instead of being genuinely generic: the Club Name field defaulted to "Solomons Island Cycling" instead of a neutral placeholder, the Member Number labels defaulted to "SIC Number"/"SIC #", the PayPal Business Email field pre-filled with a real-looking but non-functional address, and — most significantly — the default liability waiver text was a real, bicycling-specific legal document that named a real third-party organization and "Solomon Island Cycling" directly in the legal language itself. The waiver default is now a genuinely generic, activity-agnostic starting template that automatically substitutes the site's actual configured club name wherever it appears, rather than requiring the admin to manually find and edit a name buried in a wall of legal text.

= 3.13.2 =
* Renamed the "Renewal Notice Log" menu item to "Manual Renewal Log" — when the Pro add-on is also active, it registers its own separate log page under the same name, so an admin running both would see two identically-labeled menu items with no way to tell them apart. Pro's equivalent is being renamed to "Automated Renewal Log" in the same update for clear symmetry.

= 3.13.1 =
* Fixed a real, blocking issue Plugin Check caught: the new CSV import used move_uploaded_file(), which is on WordPress's forbidden-functions list (not just discouraged — an actual error). Replaced it with wp_handle_upload(), the sanctioned WordPress function for this exact purpose, redirected to the plugin's own protected uploads subdirectory via a temporary upload_dir filter. This also resolved a related unsanitized-input warning on the raw $_FILES access, since wp_handle_upload() is the recognized-safe way to consume it.

= 3.13.0 =
* Added a real, free CSV import to Import & Export (Club Members > Import & Export) — same column-mapping upload flow as Pro, create-only by design (a row whose member number already exists is skipped rather than updated; Pro's smart update-on-duplicate, CSV export, and Merge Supplemental Data remain real, distinct Pro capabilities). The page now also shows a disabled preview of exactly what Pro adds. Also fixed the "Learn More About Pro" link, which was still pointing at an old placeholder URL from before the real product site existed.

= 3.12.1 =
* Fixed the same multi-line ignore-comment gap (phpcs:ignore only covers one line) in the new Renewal Notice Log page's notice display — the same lesson learned the hard way twice already elsewhere in this plugin, now caught before shipping this time via a deliberate self-check rather than another round-trip. Also added a justified ignore for the CSV export's use of php://output, a PHP stream representing the HTTP response itself rather than a real file — WP_Filesystem has no equivalent for it, so there's no actual alternative to switch to.

= 3.12.0 =
* Added manual renewal notices: a "Send Renewal Notice" button next to each member in the admin list, a "Last Notified" column, a configurable one-off email template (Settings > Renewal Notices), and its own Renewal Notice Log page with CSV export. Fully manual by design — no automated matching or cron — and stays available even alongside the Pro add-on's automated version, since it serves a genuine, distinct troubleshooting purpose (e.g. resending to a member whose notice went to spam). The new Settings tab also shows exactly what Pro's automated four-notice system looks like, for anyone deciding whether to upgrade.

= 3.11.4 =
* Fixed the same multi-line ignore-comment gap in get_members() that 3.11.3 fixed elsewhere — the comment was positioned above the wrong line for a multi-line prepare() call. Did a full sweep of every ignore comment in the codebase afterward to confirm no others have the same issue.

= 3.11.3 =
* All remaining Plugin Check findings are now either mathematically-verified-safe scanner false positives (documented inline) or the expected, non-blocking custom-table caching warnings — nothing left that indicates an actual bug. Fixed a multi-line ignore-comment gap (phpcs:ignore only covers one line; a 3-line ternary needed phpcs:disable/enable instead) and added a fully justified, verified-by-hand ignore for get_members()'s variable-length search-clause query, where the scanner can't trace a placeholder count that depends on another variable's own length.

= 3.11.2 =
* Eliminated the last two spots where a query string was assigned to a variable before being passed to $wpdb->prepare() on a later line — functionally safe either way, but now inlined directly as prepare()'s own argument so there's nothing left for a reviewer (automated or human) to trace by hand. Also covered a second settings-save array-checkbox field that had the same nonce-verification tracing gap as the bulk-delete fix from the previous release.

= 3.11.1 =
* Follow-up Plugin Check pass: rewrote three database queries (sic_exists, get_members, get_members_for_email, get_member_status_counts) that used conditional/incremental SQL-string building — functionally correct, but the automated scanner couldn't trace variable interpolation across lines to verify it, and a human reviewer would have the same difficulty at a glance, so these are now fully explicit per code path. Also fixed several nonce-verification ignore-comments that were only covering an outer `if` line and missing the actual data reads inside, and simplified the member-number sanitization pattern (a `preg_replace()` step was preventing the scanner from recognizing it as sanitized even though the final `absint()` made it safe either way).

= 3.11.0 =
* WordPress.org submission readiness pass: rewrote all database queries to use the modern `%i` identifier placeholder instead of concatenating table names into SQL strings (now requires WordPress 6.2+), fixed several i18n and output-escaping issues flagged by the official Plugin Check tool, corrected an outdated plugin description that still referenced CSV import/export (moved to the Pro add-on back in 3.0.0), and removed the now-discouraged load_plugin_textdomain() call in favor of WordPress's automatic translation loading.

= 3.10.2 =
* The admin menu icon now uses a small custom card mark instead of the generic WordPress "groups" icon, matching the plugin's actual branding.

= 3.10.1 =
* The public roster's member count now reads "X active members, Y in memoriam" instead of a single combined total — expired members are excluded from the "active" count (they're neither active nor in memoriam), and In Memoriam members are no longer counted alongside active members. Searching or filtering the roster still switches to a "Showing X of Y" count as before; clearing the filter restores the active/memoriam breakdown.

= 3.10.0 =
* Added configurable public roster columns: Club Members > Settings > Roster Display now has a checkbox for every available field (member number, name, address, city, state, zip, phone, email, emergency contact name/phone, expiration) — any combination can be shown on the [rkfc_roster] shortcode. Address, phone, email, and emergency contact fields are flagged as sensitive in the UI since they'd be publicly visible if enabled. Defaults to the original fixed column set (member number, name, city, state, expiration), so existing sites look identical until an admin actively customizes this.

= 3.9.0 =
* Added a third membership model: Free. No fee is charged and the PayPal payment step is skipped entirely, but visitors still complete the full join/renewal form and must agree to the waiver/terms. Dated the same way as Rolling (expires exactly one year after signup/renewal).
* Fixed Season model: the "in-season" pricing window is now independently configurable (a start month and end month, can wrap around the year) instead of being locked to a fixed 6 months starting at the Season Start Date. This lets a club price around its actual riding/activity season regardless of when its fiscal year begins.
* Rolling model confirmed single-fee, no seasonal split (this was already the case, just clarified).
* Settings > Membership Year & Pricing reorganized into three boxes (Fixed Season / Rolling / Free) with only the box matching the selected model shown at a time, instead of all fee fields being visible regardless of which model is active.
* Fixed: the fee summary on the join form and renewal-confirm screen no longer mislabels Rolling or Free memberships as "Off-season."

= 3.8.0 =
* Fixed: jumping to a specific page number in the All Members list (typing a number and pressing Enter) could result in a blank screen. The bulk-delete form now submits back to the same page instead of a separate endpoint, and correctly ignores non-bulk-delete submissions (like the page-jump field) instead of misrouting them.
* Fixed: the waiver/terms checkbox on the join and renewal forms was too small to comfortably tap on mobile — increased to a proper touch-friendly size.

= 3.7.0 =
* Added `[rkfc_resend_welcome]`: lets a member resend themselves the configurable welcome email by entering their member number + last name. Lower-risk than the profile-update flow since it only resends an email (never exposes or changes data), so it skips the magic-link step — but still shows a neutral message regardless of match, and enforces a 5-minute per-member cooldown to prevent repeated resends to the same person.

= 3.6.0 =
* Settings page reorganized into tabs (General, Membership Year & Pricing, Roster Display, Signup Details, PayPal Credentials) for easier navigation — no settings changed or moved in meaning, just regrouped and easier to find. All fields still save together in one form regardless of which tab is active.

= 3.5.0 =
* Added a configurable welcome email, sent automatically right after a brand-new member successfully joins online (never for renewals). Subject, body, and enable/disable are all editable under Club Members > Settings, with placeholders for the member's name, member number, expiration date, and club name.

= 3.4.0 =
* Editing, deleting, or bulk-deleting a member now returns you to the exact same spot in the All Members list afterward (same page number, sort order, and search filter) instead of always resetting to the first page.

= 3.3.0 =
* Added `[rkfc_update_profile]`: a self-service "update my info" shortcode. Members identify themselves with their member number + last name, then a one-time secure link is emailed to whatever address is already on file (never shown on-screen, never sent to an address the visitor types in) — this is what actually grants access, preventing one member from looking up or editing another's record even though member number, name, city, and state are already public on the roster. Email address itself stays admin-only; every other contact field is editable.

= 3.2.0 =
* Added two new database helpers (find_members_by_name, fill_blank_fields) used by the Pro add-on's new "Merge Supplemental Data" tool — no change in behavior for the free plugin itself.

= 3.1.0 =
* Added a "Help" admin page: a reference for the three shortcodes, a recommendation for setting up reliable email delivery (WP Mail SMTP), and a plain-language explanation of every Settings field.

= 3.0.0 =
* CSV Import/Export and Email Members moved to a separate paid add-on, "Club Member Manager - CSV & Email Pro" (sold separately, not on WordPress.org). The free plugin still shows these menu items with an upsell screen; installing the add-on replaces the upsell with the real tools in the same spot. This keeps the free plugin's core member-management, roster, and online join/renewal features completely free and unrestricted, per WordPress.org's rules against locking core functionality behind payment.

= 2.1.0 =
* Completed full internationalization (i18n): every user-facing string in the admin, public shortcodes, and join/renewal forms is now wrapped for translation under the `club-member-manager` text domain.

= 2.0.0 =
* Renamed the plugin (and its slug/folder) to Club Member Manager, ahead of public distribution.
* Genericized all club-specific terminology: configurable member number label, club name, and a choice between Fixed Season or Rolling membership-year models.
* Removed hardcoded credentials from the codebase; added a proper license header and this readme.txt in WordPress.org's required format.

= 1.9.x =
* Added configurable "Member Number" and "Member Number Short" labels, and a configurable club name, used throughout the admin, roster, and join/renew forms.

= 1.8.x =
* Added required Emergency Contact Name/Phone fields to the online join and renewal forms and the admin member record.

= 1.7.x =
* Added CSV export and a bulk "Email Members" tool with Active/Expired filtering.
* Added a one-time tool to correct expiration dates for members created before the March 31 convention was adopted.

= 1.6.x =
* Switched the default membership-year expiration convention from April 1 to March 31 (the correct last day of membership).
* Fixed the online renewal flow's redirect handling.
* Differentiated the post-payment success message for renewals vs. new joins.

= 1.5.x =
* Added the online renewal flow (`[rkfc_renew_form]`): SIC/member-number + last-name lookup, a pre-filled confirm/edit step, and PayPal payment, extending the member's expiration date appropriately for on-time vs. lapsed renewals.

= 1.4.x =
* Added a configurable grace period after which expired members are grayed out, then dropped, from the public roster.

= 1.3.x =
* Added a "Total Members" count to the public roster, and a manual/automatic purge tool for the online join-request log.

= 1.2.x =
* Added the online join flow (`[rkfc_join_form]`): a public form, PayPal Checkout payment, and server-verified automatic roster creation with sequential member-number assignment.

= 1.1.0 =
* Added "In Memoriam" membership type, excluding those members from expiration-based filtering while keeping them listed as a tribute.

= 1.0.0 =
* Initial release: member roster, CSV import, admin management, and the public roster shortcode.

== Upgrade Notice ==

= 3.14.0 =
Plugin renamed to "Rosterkeeper for Clubs" (new slug). Existing data, settings, and any [bcm_*] shortcodes already in your page content continue working automatically — nothing to do on your end.

= 3.0.0 =
CSV import/export and bulk email moved to a separate paid add-on, "Rosterkeeper for Clubs - CSV & Email Pro." Your existing member data and settings are unaffected either way.

= 2.0.0 =
This version renames the plugin's folder and main file. If upgrading from the old "bicycle-club-membership" folder, delete that old entry after activating this one — your data is unaffected.
