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
This commit is contained in:
306
docs/MEMBERSHIP_LINKING.md
Normal file
306
docs/MEMBERSHIP_LINKING.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# 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
|
||||
|
||||
### membership_links Table
|
||||
```sql
|
||||
- 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
|
||||
```sql
|
||||
- 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)
|
||||
|
||||
### getUserMembershipLink()
|
||||
**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**:
|
||||
```json
|
||||
{
|
||||
"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**:
|
||||
```json
|
||||
{
|
||||
"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
|
||||
```php
|
||||
$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
|
||||
```php
|
||||
if (getUserMemberStatus($user_id)) {
|
||||
// User has direct or linked membership
|
||||
echo "Welcome member!";
|
||||
} else {
|
||||
// Redirect to membership page
|
||||
header('Location: membership');
|
||||
}
|
||||
```
|
||||
|
||||
### Getting Linked Users
|
||||
```php
|
||||
$linkedUsers = getLinkedSecondaryUsers($_SESSION['user_id']);
|
||||
foreach ($linkedUsers as $user) {
|
||||
echo "Linked: " . $user['first_name'] . ' (' . $user['relationship'] . ')';
|
||||
}
|
||||
```
|
||||
|
||||
### Removing a Link
|
||||
```php
|
||||
$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
|
||||
|
||||
Reference in New Issue
Block a user