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:
@@ -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()];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user