current_membership returns only paid memberships
User#current_membership is the member’s “official” membership — the one the app shows and the
one other rules derive from. It must never expose a membership that was not paid for.
Rule
User#current_membership(date)returns, in order:- the paid membership valid on
date(valid_since <= date <= valid_until); - otherwise, the most recently created paid membership (even if expired);
- otherwise
nil.
- the paid membership valid on
- A membership with
payment_statusother thanpaid(pending,overdue,refunded,chargeback) is never returned. Before this rule, the fallback returned the most recent membership regardless of payment status, so an unpaid membership could be presented as current. nilis a valid outcome: a member who has never had a paid membership has no current membership.User#next_membership(date)follows the same rule for the member’s next affiliation period: it starts fromcurrent_membershipand returns the earliest paid membership starting on or after the current one’svalid_until, falling back to the current membership when there is no future one, andnilwhen there is no current one. An unpaid future membership — a renewal awaiting payment, for instance — is never returned.
Consequences
GET /api/v2/membership(Api::V2::MembershipsController#show) returns 404 for a member with no paid membership, instead of the unpaid membership’s data.next_affiliationinMembershipSerializertherefore never describes an unpaid membership. A member in good standing with a renewal awaiting payment keeps seeing the current affiliation there, not the pending one.- The onboarding follows the same rule: an unpaid membership has no onboarding.
User#current_onboardingresolves the onboarding throughnext_membership, so it only ever returns the onboarding of a paid membership — the member’s next paid affiliation period, falling back to the current one. Consequences:- A member whose current membership is paid and whose renewal was refunded sees the onboarding of the current membership, not the refunded renewal’s. This is the point of the rule: an onboarding created against a membership that was later refunded must not shadow the onboarding of the affiliation the member actually holds.
- A member with no paid membership at all has no onboarding:
current_onboardingreturnsnil. They cannot submit or resubmit documents (Onboarding::Steps::ProcessreturnsFailure :onboarding_not_found), andUserProfile#calculate_documentation_statusresolves to:pendinginstead of:analysis. A refund therefore does remove access to the onboarding — that is intended, because a membership that was not paid for is not an affiliation. - Because the resolution goes through
next_membership, the membership picked is the earliest paid future period, not the most recently created membership.
Onboarding::Moderatefails instead of raising when the member has no onboarding at all: it returnsFailure :onboarding_not_found. It runs from anafter_savecallback onUserProfileModeration, so raising there would break the admin moderation save. The moderation record still saves and the admin sees no error, soUserProfileModeration#check_moderate_onboardinglogs aRails.logger.warnwith the failure type and the ids involved — otherwise moderating a member with no paid membership would be a completely silent no-op.
Where it applies
app/models/user.rb—current_membership,next_membership,current_onboarding.app/models/onboarding/moderate.rb— nil-onboarding guard.app/models/user_profile_moderation.rb— warn log when the moderation finds no onboarding.- Consumers:
app/controllers/api/v2/memberships_controller.rb,app/serializers/membership_serializer.rb,app/models/user_profile.rb,app/models/onboarding/steps/process.rb,app/controllers/api/v2/onboarding_controller.rb.