sees summary of completes, speedsters, IP dups * 2. Client chooses: * a) Accept All - mark all pending as complete, close, credit rewards + partner commission * b) Upload Fraudsters - upload CSV with reason + remarks, pending admin approval */ require_once 'client-session.php'; require_once 'panel-db-config.php'; requireClientLogin(); // Include the project close rewards processor if (file_exists(__DIR__ . '/../close-project-rewards.php')) { require_once __DIR__ . '/../close-project-rewards.php'; } $project_id = intval($_GET['id'] ?? $_POST['project_id'] ?? 0); if (!$project_id) { header('Location: projects-list.php'); exit; } $pdo = getClientDBConnection(); // Get project $stmt = $pdo->prepare("SELECT * FROM projects WHERE id = ? AND client_id = ?"); $stmt->execute([$project_id, $_SESSION['client_id']]); $project = $stmt->fetch(PDO::FETCH_ASSOC); if (!$project) { header('Location: projects-list.php'); exit; } // Don't allow closing already closed projects if ($project['status'] === 'Closed') { $_SESSION['info'] = 'This project is already closed.'; header('Location: view-project.php?id=' . $project_id); exit; } // Don't allow closing projects pending review if (isset($project['closure_status']) && $project['closure_status'] === 'pending_review') { $_SESSION['info'] = 'This project is pending admin review for closure.'; header('Location: view-project.php?id=' . $project_id); exit; } $error = ''; $success = ''; // Defensive column checks for survey_urls $cols = array_column($pdo->query("SHOW COLUMNS FROM survey_urls")->fetchAll(), 'Field'); $hasQF = in_array('quality_flag', $cols); $hasLoi = in_array('actual_loi_seconds', $cols); // Check projects table for closure columns $projCols = array_column($pdo->query("SHOW COLUMNS FROM projects")->fetchAll(), 'Field'); $hasClosureCols = in_array('closure_status', $projCols); $hasRewardCol = in_array('reward_per_complete', $projCols); // Get project stats if ($hasQF) { $stmt = $pdo->prepare(" SELECT COUNT(*) as total_urls, SUM(CASE WHEN status = 'complete' THEN 1 ELSE 0 END) as total_completes, SUM(CASE WHEN status = 'complete' AND quality_flag = 'valid' THEN 1 ELSE 0 END) as valid_completes, SUM(CASE WHEN status = 'complete' AND quality_flag = 'speedster' THEN 1 ELSE 0 END) as speedsters, SUM(CASE WHEN status = 'complete' AND quality_flag = 'ip_duplicate' THEN 1 ELSE 0 END) as ip_duplicates, SUM(CASE WHEN status = 'complete' AND quality_flag = 'client_flagged' THEN 1 ELSE 0 END) as client_flagged, " . ($hasLoi ? "AVG(CASE WHEN status = 'complete' AND actual_loi_seconds IS NOT NULL THEN actual_loi_seconds END)" : "NULL") . " as avg_loi, SUM(CASE WHEN status IN ('sent','clicked') THEN 1 ELSE 0 END) as still_pending, SUM(CASE WHEN status IN ('partial','earlyscreenout','latescreenout','timeout','quotafull') THEN 1 ELSE 0 END) as non_completes FROM survey_urls WHERE project_id = ? "); } else { $stmt = $pdo->prepare(" SELECT COUNT(*) as total_urls, SUM(CASE WHEN status = 'complete' THEN 1 ELSE 0 END) as total_completes, SUM(CASE WHEN status = 'complete' THEN 1 ELSE 0 END) as valid_completes, 0 as speedsters, 0 as ip_duplicates, 0 as client_flagged, NULL as avg_loi, SUM(CASE WHEN status IN ('sent','clicked') THEN 1 ELSE 0 END) as still_pending, SUM(CASE WHEN status IN ('partial','earlyscreenout','latescreenout','timeout','quotafull') THEN 1 ELSE 0 END) as non_completes FROM survey_urls WHERE project_id = ? "); } $stmt->execute([$project['project_id']]); $stats = $stmt->fetch(PDO::FETCH_ASSOC); // Handle download sample CSV if (isset($_GET['action']) && $_GET['action'] === 'download_sample_csv') { header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="fraud_report_sample_' . $project['project_id'] . '.csv"'); $out = fopen('php://output', 'w'); fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF)); fputcsv($out, ['client_url', 'reason', 'remarks']); $validReasons = ['Straightlining', 'Speeding', 'Gibberish / Nonsense Responses', 'Duplicate Response', 'Failed Attention Check', 'Inconsistent Answers', 'Bot / Automated Response', 'Off-Target Demographics', 'Other']; $exampleRemarks = ['Same answer for all grid questions', 'Completed in under 2 minutes', 'Random text in open-ends']; // Get a few actual complete URLs for the sample $stmt = $pdo->prepare("SELECT client_url FROM survey_urls WHERE project_id = ? AND status = 'complete' LIMIT 3"); $stmt->execute([$project['project_id']]); $sampleUrls = $stmt->fetchAll(PDO::FETCH_COLUMN); foreach ($sampleUrls as $i => $url) { fputcsv($out, [$url, $validReasons[$i] ?? 'Straightlining', $exampleRemarks[$i] ?? 'Details here']); } if (empty($sampleUrls)) { fputcsv($out, ['https://example.com/survey?id=ABC123', 'Straightlining', 'Same answer for all grid questions']); fputcsv($out, ['https://example.com/survey?id=DEF456', 'Speeding', 'Completed in under 2 minutes']); } fclose($out); exit; } // === HANDLE POST ACTIONS === if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; try { if ($action === 'accept_all') { $pdo->beginTransaction(); // Update project status to Closed if ($hasClosureCols) { $stmt = $pdo->prepare(" UPDATE projects SET status = 'Closed', closure_status = 'approved', closure_method = 'accept_all', closure_requested_at = NOW(), closure_approved_at = NOW(), closed_at = NOW() WHERE id = ? "); } else { $stmt = $pdo->prepare("UPDATE projects SET status = 'Closed' WHERE id = ?"); } $stmt->execute([$project_id]); // Close all selections $stmt = $pdo->prepare(" UPDATE project_selections SET status = 'closed', updated_at = NOW() WHERE project_id = ? AND status != 'closed' "); $stmt->execute([$project_id]); // ============================================================ // KEY FIX: Mark ALL pending selection_members as 'complete' // This is what was missing - without this, rewards never process // ============================================================ $markStmt = $pdo->prepare(" UPDATE selection_members sm JOIN project_selections ps ON sm.selection_id = ps.id SET sm.sample_status = 'complete', sm.completed_at = NOW() WHERE ps.project_id = ? AND sm.sample_status = 'pending' "); $markStmt->execute([$project_id]); $membersMarked = $markStmt->rowCount(); // Log activity — uses: details, client_id, ip_address $stmt = $pdo->prepare(" INSERT INTO project_activity_log (project_id, client_id, action, details, ip_address) VALUES (?, ?, 'project_closed', ?, ?) "); $stmt->execute([ $project_id, $_SESSION['client_id'], "Project closed - all completes accepted as valid. $membersMarked pending member(s) marked complete.", $_SERVER['REMOTE_ADDR'] ?? '' ]); $pdo->commit(); // ============================================================ // PROCESS REWARDS: Credit member survey points + affiliate revenue share // Must be AFTER commit so member statuses are updated // ============================================================ $rewardMsg = ''; if (function_exists('processProjectCloseRewards')) { try { $panelPdo = getPanelDBConnection(); $rewardSummary = processProjectCloseRewards($project_id, $pdo, $panelPdo); if ($membersMarked > 0) { $rewardMsg .= $membersMarked . ' member(s) accepted as complete. '; } if ($rewardSummary['members_rewarded'] > 0) { $rewardMsg .= $rewardSummary['members_rewarded'] . ' member(s) credited survey rewards (' . number_format($rewardSummary['total_member_points'], 0) . ' pts). '; } if ($rewardSummary['affiliates_rewarded'] > 0) { $rewardMsg .= $rewardSummary['affiliates_rewarded'] . ' affiliate(s) credited revenue share.'; } if (!empty($rewardSummary['errors'])) { error_log('Close project reward notes: ' . implode('; ', $rewardSummary['errors'])); // Don't show errors to client unless critical } } catch (Exception $e) { error_log('Close project rewards failed: ' . $e->getMessage()); $rewardMsg .= ' Reward processing note: ' . $e->getMessage(); } } else { $rewardMsg = 'close-project-rewards.php not found - rewards not processed.'; } $_SESSION['success'] = 'Project closed successfully. All completes accepted as valid. ' . $rewardMsg; header('Location: view-project.php?id=' . $project_id); exit; } elseif ($action === 'remove_flagged') { $pdo->beginTransaction(); $removedCount = 0; if ($hasQF) { // Mark flagged survey_urls $stmt = $pdo->prepare(" UPDATE survey_urls SET quality_notes = CONCAT(COALESCE(quality_notes, ''), ' | Removed on closure') WHERE project_id = ? AND status = 'complete' AND quality_flag IN ('speedster', 'ip_duplicate') "); $stmt->execute([$project['project_id']]); $removedCount = $stmt->rowCount(); // Update selection_members for removed users to screen_out $stmt = $pdo->prepare(" UPDATE selection_members sm INNER JOIN survey_urls su ON su.sent_to_user_id = sm.user_id INNER JOIN project_selections ps ON sm.selection_id = ps.id AND ps.project_id = ? SET sm.sample_status = 'screen_out' WHERE su.project_id = ? AND su.status = 'complete' AND su.quality_flag IN ('speedster', 'ip_duplicate') "); $stmt->execute([$project_id, $project['project_id']]); } // Mark remaining pending members as complete (valid ones) $markStmt = $pdo->prepare(" UPDATE selection_members sm JOIN project_selections ps ON sm.selection_id = ps.id SET sm.sample_status = 'complete', sm.completed_at = NOW() WHERE ps.project_id = ? AND sm.sample_status = 'pending' "); $markStmt->execute([$project_id]); $membersMarked = $markStmt->rowCount(); // Close project if ($hasClosureCols) { $stmt = $pdo->prepare(" UPDATE projects SET status = 'Closed', closure_status = 'approved', closure_method = 'remove_flagged', closure_requested_at = NOW(), closure_approved_at = NOW(), closed_at = NOW() WHERE id = ? "); } else { $stmt = $pdo->prepare("UPDATE projects SET status = 'Closed' WHERE id = ?"); } $stmt->execute([$project_id]); // Close all selections $stmt = $pdo->prepare(" UPDATE project_selections SET status = 'closed', updated_at = NOW() WHERE project_id = ? AND status != 'closed' "); $stmt->execute([$project_id]); // Log $stmt = $pdo->prepare(" INSERT INTO project_activity_log (project_id, client_id, action, details, ip_address) VALUES (?, ?, 'project_closed', ?, ?) "); $stmt->execute([ $project_id, $_SESSION['client_id'], "Project closed - $removedCount flagged respondents removed. $membersMarked valid member(s) marked complete.", $_SERVER['REMOTE_ADDR'] ?? '' ]); $pdo->commit(); // Process rewards for valid completes $rewardMsg = ''; if (function_exists('processProjectCloseRewards')) { try { $panelPdo = getPanelDBConnection(); $rewardSummary = processProjectCloseRewards($project_id, $pdo, $panelPdo); if ($rewardSummary['members_rewarded'] > 0) { $rewardMsg .= $rewardSummary['members_rewarded'] . ' member(s) credited survey rewards. '; } if ($rewardSummary['affiliates_rewarded'] > 0) { $rewardMsg .= $rewardSummary['affiliates_rewarded'] . ' affiliate(s) credited revenue share.'; } } catch (Exception $e) { error_log('Remove flagged rewards: ' . $e->getMessage()); } } $_SESSION['success'] = "Project closed. $removedCount flagged respondents removed. $rewardMsg"; header('Location: view-project.php?id=' . $project_id); exit; } elseif ($action === 'upload_fraudsters') { // Handle CSV upload of fraudulent client URLs with reason and remarks if (!isset($_FILES['fraud_csv']) || $_FILES['fraud_csv']['error'] !== UPLOAD_ERR_OK) { throw new Exception('Please upload a valid CSV file.'); } $file = $_FILES['fraud_csv']['tmp_name']; $ext = strtolower(pathinfo($_FILES['fraud_csv']['name'], PATHINFO_EXTENSION)); if (!in_array($ext, ['csv', 'txt'])) { throw new Exception('Only CSV or TXT files are accepted.'); } $validReasons = ['Straightlining', 'Speeding', 'Gibberish / Nonsense Responses', 'Duplicate Response', 'Failed Attention Check', 'Inconsistent Answers', 'Bot / Automated Response', 'Off-Target Demographics', 'Other']; $handle = fopen($file, 'r'); if (!$handle) throw new Exception('Failed to read CSV file.'); $header = fgetcsv($handle); // Detect if first row is header $isHeader = false; if ($header) { $firstVal = strtolower(trim($header[0] ?? '')); if (in_array($firstVal, ['client_url', 'url', 'survey_url', 'link'])) { $isHeader = true; } } $entries = []; $parseErrors = []; $rowNum = $isHeader ? 2 : 1; if (!$isHeader && $header) { // First row is data, process it $url = trim($header[0] ?? ''); $reason = trim($header[1] ?? 'Other'); $remarks = trim($header[2] ?? ''); if (!empty($url)) { if (!in_array($reason, $validReasons)) { $remarks = $reason . ($remarks ? ' - ' . $remarks : ''); $reason = 'Other'; } $entries[] = ['url' => $url, 'reason' => $reason, 'remarks' => $remarks]; } } while (($row = fgetcsv($handle)) !== false) { $url = trim($row[0] ?? ''); $reason = trim($row[1] ?? 'Other'); $remarks = trim($row[2] ?? ''); if (empty($url)) { $parseErrors[] = "Row $rowNum: empty URL, skipped"; $rowNum++; continue; } if (!in_array($reason, $validReasons)) { $remarks = $reason . ($remarks ? ' - ' . $remarks : ''); $reason = 'Other'; } $entries[] = ['url' => $url, 'reason' => $reason, 'remarks' => $remarks]; $rowNum++; } fclose($handle); if (empty($entries)) { throw new Exception('No valid entries found in the CSV file.'); } // Match URLs to survey_urls in database $matchedCount = 0; $unmatchedUrls = []; $pdo->beginTransaction(); foreach ($entries as &$entry) { $stmt = $pdo->prepare(" SELECT id, unique_identifier, sent_to_user_id FROM survey_urls WHERE project_id = ? AND client_url = ? AND status = 'complete' LIMIT 1 "); $stmt->execute([$project['project_id'], $entry['url']]); $match = $stmt->fetch(PDO::FETCH_ASSOC); if ($match) { $entry['matched'] = true; $entry['survey_url_id'] = $match['id']; $entry['unique_id'] = $match['unique_identifier']; $matchedCount++; // Flag in DB if quality_flag column exists if ($hasQF) { $stmt = $pdo->prepare(" UPDATE survey_urls SET quality_flag = 'client_flagged', quality_notes = CONCAT(COALESCE(quality_notes, ''), ?) WHERE id = ? "); $stmt->execute([' | Client flagged: ' . $entry['reason'] . ' - ' . $entry['remarks'], $match['id']]); } } else { $entry['matched'] = false; $unmatchedUrls[] = $entry['url']; } } // Store closure data as JSON in project $closureData = json_encode([ 'entries' => $entries, 'total_uploaded' => count($entries), 'matched' => $matchedCount, 'unmatched' => count($unmatchedUrls), 'unmatched_urls' => $unmatchedUrls, 'uploaded_by' => $_SESSION['client_id'], 'uploaded_at' => date('Y-m-d H:i:s') ]); if ($hasClosureCols) { $stmt = $pdo->prepare(" UPDATE projects SET closure_status = 'pending_review', closure_method = 'upload_flagged', closure_requested_at = NOW(), closure_flagged_urls = ? WHERE id = ? "); $stmt->execute([$closureData, $project_id]); } // Log $stmt = $pdo->prepare(" INSERT INTO project_activity_log (project_id, client_id, action, details, ip_address) VALUES (?, ?, 'closure_requested', ?, ?) "); $stmt->execute([ $project_id, $_SESSION['client_id'], "Fraud report uploaded: " . count($entries) . " entries, $matchedCount matched. Pending admin review.", $_SERVER['REMOTE_ADDR'] ?? '' ]); $pdo->commit(); $msg = "Fraud report submitted: " . count($entries) . " entries uploaded, $matchedCount matched to survey URLs. Pending admin review."; if (!empty($parseErrors)) { $msg .= " Note: " . count($parseErrors) . " row(s) had errors and were skipped."; } $_SESSION['success'] = $msg; header('Location: view-project.php?id=' . $project_id); exit; } } catch (Exception $e) { if ($pdo->inTransaction()) $pdo->rollBack(); $error = $e->getMessage(); } } // Format LOI helper function formatLoi($seconds) { if ($seconds === null) return '-'; $min = floor($seconds / 60); $sec = $seconds % 60; return $min > 0 ? "{$min}m {$sec}s" : "{$sec}s"; } $speedsterThreshold = round($project['eloi'] / 3, 1); $page_title = 'Close Project'; include 'client-portal-header.php'; ?>

Close Project:

Project ID: • ELOI: min • Speedster threshold: < min

Total Completes
Valid Completes
Possible Speedsters
Possible IP Duplicates
Avg LOI
Non-Completes

✅ Option 1: Accept All Completes

Close the project immediately. All completes (including speedsters and IP duplicates) will be accepted as valid. Panel members will receive reward points and partners will receive commissions.

0 || intval($stats['ip_duplicates']) > 0)): ?>

⚠️ Option 2: Remove Flagged & Close

Remove flagged respondents (speedsters + IP duplicates) before closing. Only valid completes will receive rewards and generate commissions.

📋 Option 3: Upload Fraudulent Respondents for Review

Upload a CSV file identifying specific completed survey URLs that you consider fraudulent. Each entry must include a reason (selected from predefined options) and an optional remarks column for additional detail. This will be submitted to the admin team for review before the project is closed.

↓ Download Sample CSV
CSV format: client_url, reason, remarks (3 columns).
Valid reasons: Straightlining, Speeding, Gibberish / Nonsense Responses, Duplicate Response, Failed Attention Check, Inconsistent Answers, Bot / Automated Response, Off-Target Demographics, Other.
The project will remain open until an admin approves or rejects the flagged URLs.
Cancel - Don't Close Yet