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

@@ -132,6 +132,8 @@ RewriteRule ^update_album$ src/processors/update_album.php [L]
RewriteRule ^delete_album$ src/processors/delete_album.php [L]
RewriteRule ^delete_photo$ src/processors/delete_photo.php [L]
RewriteRule ^get_album_photos$ src/processors/get_album_photos.php [L]
RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L]
RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L]
</IfModule>

306
docs/MEMBERSHIP_LINKING.md Normal file
View 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

View File

@@ -0,0 +1,63 @@
-- Migration 004: Create membership linking tables
-- Purpose: Allow multiple users to share a single membership (for couples, families, etc)
-- Create membership_links table to associate secondary users with primary membership accounts
CREATE TABLE IF NOT EXISTS `membership_links` (
`link_id` INT AUTO_INCREMENT PRIMARY KEY,
`primary_user_id` INT NOT NULL,
`secondary_user_id` INT NOT NULL,
`relationship` VARCHAR(50) NOT NULL DEFAULT 'spouse', -- spouse, family member, etc
`linked_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Foreign keys
CONSTRAINT `fk_membership_links_primary` FOREIGN KEY (`primary_user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_membership_links_secondary` FOREIGN KEY (`secondary_user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
-- Indexes for performance
INDEX `idx_primary_user` (`primary_user_id`),
INDEX `idx_secondary_user` (`secondary_user_id`),
-- Prevent duplicate links (user cannot be linked twice)
UNIQUE KEY `unique_link` (`primary_user_id`, `secondary_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create membership_permissions table to define what secondary users can access
CREATE TABLE IF NOT EXISTS `membership_permissions` (
`permission_id` INT AUTO_INCREMENT PRIMARY KEY,
`link_id` INT NOT NULL,
`permission_name` VARCHAR(100) NOT NULL, -- 'access_member_areas', 'member_pricing', 'book_campsites', etc
`granted_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Foreign key
CONSTRAINT `fk_membership_permissions_link` FOREIGN KEY (`link_id`)
REFERENCES `membership_links`(`link_id`) ON DELETE CASCADE ON UPDATE CASCADE,
-- Indexes
INDEX `idx_link` (`link_id`),
-- Prevent duplicate permissions
UNIQUE KEY `unique_permission` (`link_id`, `permission_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Add foreign key to membership_fees to support links (optional - for tracking which membership fee covers the linked users)
-- ALTER TABLE `membership_fees` ADD COLUMN `primary_user_id` INT AFTER `user_id`;
-- This allows you to see if a fee was paid by primary or secondary user while maintaining the relationship
-- Create a view to easily get all users linked to a membership
CREATE OR REPLACE VIEW `linked_membership_users` AS
SELECT
primary_user_id,
secondary_user_id,
relationship,
linked_at
FROM membership_links
UNION ALL
SELECT
primary_user_id,
primary_user_id as secondary_user_id,
'primary' as relationship,
linked_at
FROM membership_links;

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()];
}
}

View File

@@ -0,0 +1,57 @@
<?php
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . '/src/config/env.php');
require_once($rootPath . '/src/config/session.php');
require_once($rootPath . '/src/config/connection.php');
require_once($rootPath . '/src/config/functions.php');
header('Content-Type: application/json');
if (!isset($_SESSION['user_id']) || $_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(403);
exit(json_encode(['success' => false, 'message' => 'Forbidden']));
}
// Validate CSRF token
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
http_response_code(400);
exit(json_encode(['success' => false, 'message' => 'Invalid request']));
}
$primary_user_id = intval($_SESSION['user_id']);
$secondary_email = trim($_POST['secondary_email'] ?? '');
$relationship = trim($_POST['relationship'] ?? 'spouse');
if (empty($secondary_email)) {
http_response_code(400);
exit(json_encode(['success' => false, 'message' => 'Secondary user email is required']));
}
// Get the secondary user by email
$conn = openDatabaseConnection();
$userQuery = $conn->prepare("SELECT user_id FROM users WHERE email = ?");
$userQuery->bind_param("s", $secondary_email);
$userQuery->execute();
$userResult = $userQuery->get_result();
$userQuery->close();
if ($userResult->num_rows === 0) {
$conn->close();
http_response_code(404);
exit(json_encode(['success' => false, 'message' => 'User with that email not found']));
}
$user = $userResult->fetch_assoc();
$secondary_user_id = $user['user_id'];
$conn->close();
// Use the linking function from functions.php
$result = linkSecondaryUserToMembership($primary_user_id, $secondary_user_id, $relationship);
http_response_code($result['success'] ? 200 : 400);
echo json_encode([
'success' => $result['success'],
'message' => $result['message'],
'link_id' => $result['link_id'] ?? null
]);
?>

View File

@@ -0,0 +1,37 @@
<?php
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . '/src/config/env.php');
require_once($rootPath . '/src/config/session.php');
require_once($rootPath . '/src/config/connection.php');
require_once($rootPath . '/src/config/functions.php');
header('Content-Type: application/json');
if (!isset($_SESSION['user_id']) || $_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(403);
exit(json_encode(['success' => false, 'message' => 'Forbidden']));
}
// Validate CSRF token
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
http_response_code(400);
exit(json_encode(['success' => false, 'message' => 'Invalid request']));
}
$primary_user_id = intval($_SESSION['user_id']);
$link_id = intval($_POST['link_id'] ?? 0);
if (!$link_id) {
http_response_code(400);
exit(json_encode(['success' => false, 'message' => 'Link ID is required']));
}
// Use the unlinking function from functions.php
$result = unlinkSecondaryUser($link_id, $primary_user_id);
http_response_code($result['success'] ? 200 : 400);
echo json_encode([
'success' => $result['success'],
'message' => $result['message']
]);
?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
uploads/pop/1 C. PINTO.pdf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB