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:
twotalesanimation
2025-12-05 10:44:52 +02:00
parent 7dad2a4ce2
commit bd20fc0f9b
46 changed files with 718 additions and 2 deletions

View File

@@ -412,7 +412,7 @@ function getUserMemberStatus($user_id)
return false;
}
// Step 1: Check if the user is a member
// Step 1: Check if the user is a direct member
$queryUser = "SELECT member FROM users WHERE user_id = ?";
$stmtUser = $conn->prepare($queryUser);
if (!$stmtUser) {
@@ -445,7 +445,10 @@ function getUserMemberStatus($user_id)
if ($resultApplication->num_rows === 0) {
error_log("No membership application found for user_id: $user_id");
return false;
// Check if user is linked to another user's membership
$linkedStatus = getUserMembershipLink($user_id);
$conn->close();
return $linkedStatus['has_access'];
}
$application = $resultApplication->fetch_assoc();
@@ -484,8 +487,10 @@ function getUserMemberStatus($user_id)
$membership_end_date_obj = DateTime::createFromFormat('Y-m-d', $membership_end_date);
if ($payment_status === "PAID" && $current_date <= $membership_end_date_obj) {
$conn->close();
return true; // Membership is active
} else {
$conn->close();
return false;
}
@@ -2906,3 +2911,249 @@ function optimizeImage($filePath, $maxWidth = 1920, $maxHeight = 1080)
return $success;
}
/**
* Link a secondary user to a primary user's membership
* @param int $primary_user_id The main membership holder
* @param int $secondary_user_id The user to link (spouse, family member, etc)
* @param string $relationship The relationship type (spouse, family_member, etc)
* @return array ['success' => bool, 'message' => string]
*/
function linkSecondaryUserToMembership($primary_user_id, $secondary_user_id, $relationship = 'spouse')
{
$conn = openDatabaseConnection();
if ($conn === null) {
return ['success' => false, 'message' => 'Database connection failed'];
}
// Validation: primary and secondary user IDs must be different
if ($primary_user_id === $secondary_user_id) {
return ['success' => false, 'message' => 'Cannot link user to themselves'];
}
// Validation: primary user must have active membership
if (!getUserMemberStatus($primary_user_id)) {
$conn->close();
return ['success' => false, 'message' => 'Primary user does not have active membership'];
}
// Validation: secondary user must exist
$userCheck = $conn->prepare("SELECT user_id FROM users WHERE user_id = ?");
$userCheck->bind_param("i", $secondary_user_id);
$userCheck->execute();
$userResult = $userCheck->get_result();
$userCheck->close();
if ($userResult->num_rows === 0) {
$conn->close();
return ['success' => false, 'message' => 'Secondary user does not exist'];
}
// Check if link already exists
$existingLink = $conn->prepare("SELECT link_id FROM membership_links WHERE primary_user_id = ? AND secondary_user_id = ?");
$existingLink->bind_param("ii", $primary_user_id, $secondary_user_id);
$existingLink->execute();
$existingResult = $existingLink->get_result();
$existingLink->close();
if ($existingResult->num_rows > 0) {
$conn->close();
return ['success' => false, 'message' => 'Users are already linked'];
}
try {
// Start transaction
$conn->begin_transaction();
// Insert link
$insertLink = $conn->prepare("
INSERT INTO membership_links (primary_user_id, secondary_user_id, relationship, linked_at, created_at)
VALUES (?, ?, ?, NOW(), NOW())
");
$insertLink->bind_param("iis", $primary_user_id, $secondary_user_id, $relationship);
$insertLink->execute();
$linkId = $conn->insert_id;
$insertLink->close();
// Grant default permissions to secondary user
$permissions = [
'access_member_areas',
'member_pricing',
'book_campsites',
'book_courses',
'book_trips'
];
foreach ($permissions as $permission) {
$insertPerm = $conn->prepare("
INSERT INTO membership_permissions (link_id, permission_name, granted_at)
VALUES (?, ?, NOW())
");
$insertPerm->bind_param("is", $linkId, $permission);
$insertPerm->execute();
$insertPerm->close();
}
// Commit transaction
$conn->commit();
$conn->close();
return ['success' => true, 'message' => 'User successfully linked to membership', 'link_id' => $linkId];
} catch (Exception $e) {
$conn->rollback();
$conn->close();
return ['success' => false, 'message' => 'Failed to create link: ' . $e->getMessage()];
}
}
/**
* Check if a user has access through a membership link
* @param int $user_id The user to check
* @return array ['has_access' => bool, 'primary_user_id' => int|null, 'relationship' => string|null]
*/
function getUserMembershipLink($user_id)
{
$conn = openDatabaseConnection();
if ($conn === null) {
return ['has_access' => false, 'primary_user_id' => null, 'relationship' => null];
}
// Check if user is a secondary user with active link
$query = "
SELECT ml.primary_user_id, ml.relationship
FROM membership_links ml
JOIN membership_fees mf ON ml.primary_user_id = mf.user_id
JOIN membership_application ma ON ml.primary_user_id = ma.user_id
WHERE ml.secondary_user_id = ?
AND ma.accept_indemnity = 1
AND mf.payment_status = 'PAID'
AND mf.membership_end_date >= CURDATE()
LIMIT 1
";
$stmt = $conn->prepare($query);
$stmt->bind_param("i", $user_id);
$stmt->execute();
$result = $stmt->get_result();
$stmt->close();
if ($result->num_rows > 0) {
$link = $result->fetch_assoc();
$conn->close();
return [
'has_access' => true,
'primary_user_id' => $link['primary_user_id'],
'relationship' => $link['relationship']
];
}
$conn->close();
return ['has_access' => false, 'primary_user_id' => null, 'relationship' => null];
}
/**
* Get all secondary users linked to a primary user
* @param int $primary_user_id The primary membership holder
* @return array Array of linked users with their info
*/
function getLinkedSecondaryUsers($primary_user_id)
{
$conn = openDatabaseConnection();
if ($conn === null) {
return [];
}
$query = "
SELECT
ml.link_id,
u.user_id,
u.first_name,
u.last_name,
u.email,
ml.relationship,
ml.linked_at
FROM membership_links ml
JOIN users u ON ml.secondary_user_id = u.user_id
WHERE ml.primary_user_id = ?
ORDER BY ml.linked_at DESC
";
$stmt = $conn->prepare($query);
$stmt->bind_param("i", $primary_user_id);
$stmt->execute();
$result = $stmt->get_result();
$stmt->close();
$linkedUsers = [];
while ($row = $result->fetch_assoc()) {
$linkedUsers[] = $row;
}
$conn->close();
return $linkedUsers;
}
/**
* Unlink a secondary user from a primary user's membership
* @param int $link_id The membership link ID to remove
* @param int $primary_user_id The primary user (for verification)
* @return array ['success' => bool, 'message' => string]
*/
function unlinkSecondaryUser($link_id, $primary_user_id)
{
$conn = openDatabaseConnection();
if ($conn === null) {
return ['success' => false, 'message' => 'Database connection failed'];
}
// Verify that this link belongs to the primary user
$linkCheck = $conn->prepare("SELECT primary_user_id FROM membership_links WHERE link_id = ?");
$linkCheck->bind_param("i", $link_id);
$linkCheck->execute();
$linkResult = $linkCheck->get_result();
$linkCheck->close();
if ($linkResult->num_rows === 0) {
$conn->close();
return ['success' => false, 'message' => 'Link not found'];
}
$linkData = $linkResult->fetch_assoc();
if ($linkData['primary_user_id'] !== $primary_user_id) {
$conn->close();
return ['success' => false, 'message' => 'Unauthorized: you do not have permission to remove this link'];
}
try {
// Start transaction
$conn->begin_transaction();
// Delete permissions first (cascade should handle this but being explicit)
$deletePerm = $conn->prepare("DELETE FROM membership_permissions WHERE link_id = ?");
$deletePerm->bind_param("i", $link_id);
$deletePerm->execute();
$deletePerm->close();
// Delete the link
$deleteLink = $conn->prepare("DELETE FROM membership_links WHERE link_id = ?");
$deleteLink->bind_param("i", $link_id);
$deleteLink->execute();
$deleteLink->close();
// Commit transaction
$conn->commit();
$conn->close();
return ['success' => true, 'message' => 'User successfully unlinked from membership'];
} catch (Exception $e) {
$conn->rollback();
$conn->close();
return ['success' => false, 'message' => 'Failed to remove link: ' . $e->getMessage()];
}
}