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'; ?>
Project ID: • ELOI: min • Speedster threshold: < min
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.
Remove flagged respondents (speedsters + IP duplicates) before closing. Only valid completes will receive rewards and generate commissions.
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.