Files
4WDCSA.co.za/docs/MEMBERSHIP_LINKING.md
twotalesanimation bd20fc0f9b feat: implement membership linking system for couples and family members
- Created membership_links table to associate secondary users with primary memberships
- Created membership_permissions table for granular permission control
- Added linkSecondaryUserToMembership() function to create links with validation
- Added getUserMembershipLink() to check access via secondary links
- Added getLinkedSecondaryUsers() to list all secondary users for a primary member
- Added unlinkSecondaryUser() to remove links
- Updated getUserMemberStatus() to check both direct and linked memberships
- Created link_membership_user processor to handle linking via API
- Created unlink_membership_user processor to handle unlinking via API
- Added .htaccess routes for linking endpoints
- Grants default permissions: access_member_areas, member_pricing, book_campsites, book_courses, book_trips
- Includes transaction safety with rollback on errors
- Includes comprehensive documentation with usage examples
- Validates primary user has active membership before allowing links
- Prevents duplicate links and self-linking
2025-12-05 10:44:52 +02:00

9.0 KiB

Membership Linking Feature

Overview

The Membership Linking feature allows users to link secondary accounts (spouses, family members, etc.) to a primary membership account. This enables multiple users to access member-only areas and receive member pricing under a single membership.

Database Schema

- link_id (INT, PK, AUTO_INCREMENT)
- primary_user_id (INT, FK to users) - Main membership holder
- secondary_user_id (INT, FK to users) - Secondary user sharing the membership
- relationship (VARCHAR 50) - Type of relationship (spouse, family_member, etc)
- linked_at (TIMESTAMP)
- created_at (TIMESTAMP)

Constraints:
- UNIQUE(primary_user_id, secondary_user_id) - Prevent duplicate links
- Foreign keys on both user IDs with CASCADE DELETE
- Indexes on both user IDs for performance

membership_permissions Table

- permission_id (INT, PK, AUTO_INCREMENT)
- link_id (INT, FK to membership_links) - Reference to the link
- permission_name (VARCHAR 100) - Permission type (access_member_areas, member_pricing, etc)
- granted_at (TIMESTAMP)

Constraints:
- UNIQUE(link_id, permission_name) - Prevent duplicate permissions
- Foreign key to membership_links with CASCADE DELETE
- Index on link_id for performance

Default Permissions Granted:
- access_member_areas
- member_pricing
- book_campsites
- book_courses
- book_trips

Functions

linkSecondaryUserToMembership()

Purpose: Link a secondary user to a primary user's active membership

Parameters:

  • int $primary_user_id - The main membership holder
  • int $secondary_user_id - The user to link
  • string $relationship - Relationship type (default: 'spouse')

Returns: array with keys:

  • success (bool) - Whether the link was created
  • message (string) - Status message
  • link_id (int) - ID of created link (on success)

Validation:

  • Primary and secondary user IDs must be different
  • Primary user must have active membership
  • Secondary user must exist
  • Link must not already exist

Side Effects:

  • Creates membership_links record
  • Creates default permission records
  • Uses transaction (rolls back on failure)

Purpose: Check if a user has access through a secondary membership link

Parameters:

  • int $user_id - User to check

Returns: array with keys:

  • has_access (bool) - Whether user has access via link
  • primary_user_id (int|null) - ID of primary account holder
  • relationship (string|null) - Relationship type

Validation:

  • Verifies the link exists
  • Checks primary user has active membership
  • Validates payment status and expiration date
  • Confirms indemnity waiver accepted

getLinkedSecondaryUsers()

Purpose: Get all secondary users linked to a primary user's membership

Parameters:

  • int $primary_user_id - The primary membership holder

Returns: array of linked users with:

  • link_id - Link ID
  • user_id - Secondary user ID
  • first_name - User's first name
  • last_name - User's last name
  • email - User's email
  • relationship - Relationship type
  • linked_at - When the link was created

unlinkSecondaryUser()

Purpose: Remove a secondary user from a primary user's membership

Parameters:

  • int $link_id - The membership link ID to remove
  • int $primary_user_id - The primary user (for verification)

Returns: array with keys:

  • success (bool) - Whether the unlink was successful
  • message (string) - Status message

Validation:

  • Verifies link exists
  • Confirms primary user owns the link
  • Uses transaction (rolls back on failure)

API Endpoints

POST /link_membership_user

Purpose: Link a new secondary user to the requester's membership

Required Parameters:

  • secondary_email (string) - Email of user to link
  • relationship (string, optional) - Relationship type (default: 'spouse')
  • csrf_token (string) - CSRF token

Response:

{
  "success": true,
  "message": "User successfully linked to membership",
  "link_id": 123
}

Error Responses:

  • 403: Forbidden (not authenticated or POST required)
  • 400: Bad Request (invalid CSRF, missing email, user not found, or linking failed)

Access Control:

  • Authenticated users only
  • Can only link to own membership

POST /unlink_membership_user

Purpose: Remove a secondary user from the requester's membership

Required Parameters:

  • link_id (int) - ID of the link to remove
  • csrf_token (string) - CSRF token

Response:

{
  "success": true,
  "message": "User successfully unlinked from membership"
}

Error Responses:

  • 403: Forbidden (not authenticated or POST required)
  • 400: Bad Request (invalid CSRF, link not found, or unauthorized)

Access Control:

  • Authenticated users only
  • Can only remove links from own membership

Integration Points

Updated getUserMemberStatus()

The getUserMemberStatus() function now checks both:

  1. Direct membership (user has membership_application and membership_fees)
  2. Secondary membership (user is linked to another user's active membership)

When user doesn't have direct membership, it automatically checks if they're linked to someone else's active membership.

Member Access Checks

All member-only pages should use getUserMemberStatus() which now automatically handles:

  • Direct members
  • Secondary members via links
  • Expired memberships
  • Indemnity waiver validation

Use Cases

Spouse/Partner Access

  1. User A (primary) has active membership
  2. User B (spouse) links to User A's membership
  3. User B can now:
    • Access member areas
    • Receive member pricing
    • Book campsites
    • Book courses
    • Book trips

Renewal

  • When primary membership renews, secondary users automatically maintain access
  • No need to re-create links on renewal

Membership Termination

  • If primary membership expires, secondary users lose access
  • Primary user can manually unlink secondary users anytime

Usage Examples

Linking a User

$result = linkSecondaryUserToMembership(
    $_SESSION['user_id'],
    'spouse@example.com',
    'spouse'
);

if ($result['success']) {
    $_SESSION['message'] = 'Successfully linked ' . $partner_email . ' to your membership';
} else {
    $_SESSION['error'] = $result['message'];
}

Checking User Access

if (getUserMemberStatus($user_id)) {
    // User has direct or linked membership
    echo "Welcome member!";
} else {
    // Redirect to membership page
    header('Location: membership');
}

Getting Linked Users

$linkedUsers = getLinkedSecondaryUsers($_SESSION['user_id']);
foreach ($linkedUsers as $user) {
    echo "Linked: " . $user['first_name'] . ' (' . $user['relationship'] . ')';
}
$result = unlinkSecondaryUser($link_id, $_SESSION['user_id']);
if ($result['success']) {
    echo "User unlinked successfully";
} else {
    echo "Error: " . $result['message'];
}

Security Considerations

Authorization

  • Users can only link to their own membership
  • Users can only manage their own links
  • Secondary users cannot create or modify links (primary user only)

Data Validation

  • Email validation before linking
  • User existence verification
  • Duplicate link prevention
  • CSRF token validation on all operations

Relationships

  • Foreign keys prevent orphaned links
  • CASCADE DELETE ensures cleanup when users are deleted
  • Transactions ensure consistency

Testing Checklist

  • Link new user to own membership
  • Attempt to link non-existent user (error)
  • Attempt to link same user twice (error)
  • Secondary user can access member areas
  • Secondary user receives member pricing
  • Unlink secondary user
  • Unlinked user cannot access member areas
  • Primary user can see list of linked users
  • Linked user appears in notifications (if applicable)
  • Membership renewal maintains links
  • Expired membership removes secondary access
  • Deleting user removes their links
  • Permission records created on link
  • Cannot link without active primary membership
  • Cannot link if different user attempts
  • CSRF token validation works

Future Enhancements

  1. Admin Management: Allow admins to create/remove links for members
  2. Selective Permissions: Allow customizing which permissions each secondary user has
  3. Invitations: Send email invitations to secondary users to accept
  4. Multiple Links: Allow primary users to link multiple users (families)
  5. UI Dashboard: Create page for managing linked accounts
  6. Notifications: Notify secondary users when linked
  7. Payment Tracking: Track which user made payments for membership
  8. Audit Log: Log all link/unlink operations for compliance

Migration Instructions

  1. Run migration 004 to create tables and permissions table
  2. Update src/config/functions.php with new linking functions
  3. Update getUserMemberStatus() to check links
  4. Add routes to .htaccess for new endpoints
  5. Deploy processors for link/unlink operations
  6. Test with married couple accounts
  7. Document for users in membership information