Conversation
Implements classify_membership_standing() and helpers in MembershipStanding.php. Uses completed calendar months since last successful payment, with sticky-lapsed state and new-member exception, matching the standing bands documented in the GMTU lapsing spec. 31 unit tests covering all threshold boundaries, year boundaries, sticky-lapsed override, new-member exception, and multiple payment history scenarios. Reverse step confirmed tests detect threshold errors. Also adds README section documenting the hook lifecycle and lapsing logic.
Stores the sticky-lapsed flag per-member in wp_options, keyed by gmtu_sticky_lapsed_ + SHA-256(lowercased email). Value is JSON with email, timestamp, and trigger for audit purposes. Autoload is disabled since flags are only read during webhook processing. 11 unit tests covering key format, case-insensitivity, read/write/delete operations, and autoload setting.
LapsingOverride.php hooks into ck_join_flow_should_lapse_member and ck_join_flow_should_unlapse_member, applying GMTU's standing rules instead of lapsing/unlapsing immediately on Stripe webhook events. Only lapses at 7+ missed months; suppresses for Good/Early Arrears/Lapsing. Sets sticky-lapsed flag on lapse; clears it on explicit rejoin via success hook. Falls through to parent default on Stripe API error or missing GMTU history. StripePaymentHistory.php fetches GMTU-scoped Stripe charges (metadata.id = join-gmtu, application = Action Network Connect app) and returns deduplicated UTC month keys for standing classification. register_lapsing_override() accepts an optional fetcher callable so tests can inject a fake without depending on patchwork namespaced function interception. 17 integration tests covering all lapse/unlapse/success scenarios.
Add require_once for MembershipStanding, StickyLapsedStore, StripePaymentHistory, and LapsingOverride. Call register_lapsing_override() at startup. Update hook lifecycle comment to document hooks 5-9.
conatus
left a comment
There was a problem hiding this comment.
Needs some improvements.
|
|
||
| // No GMTU payment history at all — not a GMTU member, do not interfere. | ||
| if (empty($history['month_keys']) && $history['first_ever_payment_timestamp'] === null) { | ||
| return $should_lapse; |
There was a problem hiding this comment.
We should also log a warning here. Remember there will be people who have not established a history form, because we never got their first payment, so we need to anticipate this.
src/MembershipStanding.php
Outdated
| function count_missed_completed_months(?string $last_paid_month_key, string $as_of_month_key): int | ||
| { | ||
| if ($last_paid_month_key === null) { | ||
| return 999999; |
There was a problem hiding this comment.
This feels inelegant and there must be a better way.
src/MembershipStanding.php
Outdated
| string $as_of_month_key, | ||
| bool $is_sticky_lapsed | ||
| ): string { | ||
| // Sticky lapsed always wins — a later payment does not reinstate automatically. |
There was a problem hiding this comment.
No a new payment resets the counter.
If I don't pay:
- January
- Feb
Then hit March and pay, I am again at zero and in Good Standing. Any successful payment gets you back into good standing.
src/StripePaymentHistory.php
Outdated
| * @param \Stripe\Charge $charge | ||
| * @return bool | ||
| */ | ||
| function is_gmtu_charge(\Stripe\Charge $charge): bool |
There was a problem hiding this comment.
This was for historic Action Network payments. Not now needed.
README.md
Outdated
| 1. Fetches the member's GMTU payment history from the Stripe Charges API. | ||
| 2. Classifies their standing using the rules above. | ||
| 3. Returns `true` (allow lapse) only if the member is classified as **Lapsed** (7+ missed months). Sets the sticky-lapsed flag. | ||
| 4. Returns `false` (suppress lapse) for Good standing, Early arrears, or Lapsing — Stripe is acting more aggressively than GMTU rules require. |
There was a problem hiding this comment.
It is not Stripe, it is our plugin. Also don't use em-dashes.
… fetcher - Rename StickyLapsedStore -> LapsedStore with is_lapsed/mark_lapsed/ clear_lapsed and gmtu_lapsed_ option key prefix. Lapsed is the only kind of lapse; calling it sticky implied a non-sticky variant exists. - Fix inelegant sentinel: count_missed_completed_months no longer accepts null. Null last_paid is handled explicitly in classify_membership_standing before the call, returning STANDING_LAPSED directly. - Remove Action Network application ID filter from is_gmtu_charge(). That filter was for historic Action Network payments and does not apply to charges created by the join-flow plugin. - Add warning log when no GMTU payment history is found for a member, covering members who joined before this plugin was active. - Fix README: remove em-dashes, replace 'Stripe' with 'parent plugin' in hook descriptions, rename all sticky-lapsed references to lapsed, update structure listing. All 202 tests pass.
Replaces the legacy charge-metadata approach (which modelled the old Action Network world) with the Stripe Subscriptions + Invoices model that the parent CK Join Flow plugin actually uses. Membership payments are now identified by: 1. Reading all configured membership plan product IDs from WordPress options (ck_join_flow_membership_plan_* prefix, stripe_product_id). 2. Finding Stripe subscriptions (all statuses) for each customer whose items belong to a configured product. 3. Collecting paid invoice timestamps (status_transitions.paid_at) from those subscriptions. Removes the GMTU_METADATA_ID constant and is_gmtu_charge() helper. Adds get_membership_product_ids(), is_gmtu_subscription(), and fetch_paid_invoice_timestamps() in their place.
- Add stripe/stripe-php ^16.1 as dev dependency so real Stripe SDK objects can be constructed via constructFrom() in tests - Add tests/stubs/Settings.php to stub the parent plugin Settings class (reads STRIPE_SECRET_KEY from $_ENV, matching the real fallback) - Define ARRAY_A in test bootstrap (WordPress constant used by wpdb) - Restructure fetch_gmtu_payment_months() and fetch_paid_invoice_timestamps() to accept optional injectable callables for every external call (product ID lookup, Customer::all, Subscription::all, Invoice::all) so tests drive all code paths without hitting the Stripe API - 31 new tests covering: get_membership_product_ids, is_gmtu_subscription (including expanded Product object), fetch_paid_invoice_timestamps (including pagination and starting_after cursor), and fetch_gmtu_payment_months (error paths, filtering, deduplication, multi-customer/subscription aggregation, subscription pagination)
Summary
README.mdHow it works
Three new source files, all wired into
join-gmtu.php:src/MembershipStanding.phpsrc/StickyLapsedStore.phpwp_options(SHA-256 keyed, autoload off)src/StripePaymentHistory.phpid=join-gmtu, Action Network Connect app)src/LapsingOverride.phpck_join_flow_should_lapse_member,ck_join_flow_should_unlapse_member, andck_join_flow_successTest plan
composer test)🤖 Generated with Claude Code