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:
    1. the paid membership valid on date (valid_since <= date <= valid_until);
    2. otherwise, the most recently created paid membership (even if expired);
    3. otherwise nil.
  • A membership with payment_status other than paid (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.
  • nil is 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 from current_membership and returns the earliest paid membership starting on or after the current one’s valid_until, falling back to the current membership when there is no future one, and nil when 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_affiliation in MembershipSerializer therefore 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_onboarding resolves the onboarding through next_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_onboarding returns nil. They cannot submit or resubmit documents (Onboarding::Steps::Process returns Failure :onboarding_not_found), and UserProfile#calculate_documentation_status resolves to :pending instead 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::Moderate fails instead of raising when the member has no onboarding at all: it returns Failure :onboarding_not_found. It runs from an after_save callback on UserProfileModeration, so raising there would break the admin moderation save. The moderation record still saves and the admin sees no error, so UserProfileModeration#check_moderate_onboarding logs a Rails.logger.warn with 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.