false, 'message' => 'Invalid request method']); exit; } try { $pdo = getClientDBConnection(); $panelPdo = getPanelDBConnection(); $selection_id = isset($_POST['selection_id']) ? (int)$_POST['selection_id'] : 0; $incentive_override = isset($_POST['incentive_amount']) ? (float)$_POST['incentive_amount'] : 0; if (!$selection_id) { throw new Exception('Invalid selection ID'); } // 1. Get selection + project details $stmt = $pdo->prepare(" SELECT ps.*, p.project_name, p.project_id AS project_code, p.eloi, p.id AS proj_id, p.status AS project_status, p.made_live_at FROM project_selections ps JOIN projects p ON ps.project_id = p.id WHERE ps.id = ? AND ps.client_id = ? "); $stmt->execute([$selection_id, $_SESSION['client_id']]); $selection = $stmt->fetch(PDO::FETCH_ASSOC); if (!$selection) { throw new Exception('Selection not found or unauthorized'); } // 2. Validate selection status — must be 'targeted' (URLs fully assigned) if (!in_array($selection['status'], ['targeted', 'draft'])) { // Allow re-check: if already live, block if ($selection['status'] === 'live') { throw new Exception('Invitations have already been sent for this selection'); } throw new Exception('Selection must be in "targeted" status to send invitations. Current: ' . $selection['status']); } // 3. Get member count $stmt = $pdo->prepare("SELECT COUNT(*) FROM selection_members WHERE selection_id = ?"); $stmt->execute([$selection_id]); $member_count = (int)$stmt->fetchColumn(); if ($member_count === 0) { throw new Exception('No members in this selection'); } // 4. Get assigned URL count for this selection $stmt = $pdo->prepare(" SELECT COUNT(*) FROM survey_urls WHERE assigned_to_selection_id = ? AND status = 'assigned' "); $stmt->execute([$selection_id]); $assigned_count = (int)$stmt->fetchColumn(); if ($assigned_count < $member_count) { throw new Exception("Not all members have URLs assigned. Members: $member_count, URLs assigned: $assigned_count"); } // 5. Calculate & validate incentive $eloi = (int)$selection['eloi']; $default_incentive = calculateDefaultIncentive($eloi); $incentive = $incentive_override > 0 ? $incentive_override : $default_incentive; if ($incentive < $default_incentive) { throw new Exception("Incentive cannot be less than ₹" . number_format($default_incentive, 0) . " (default for {$eloi} min LOI)"); } // 6. Get all assigned URLs for this selection $stmt = $pdo->prepare(" SELECT id, rr_proxy_url, unique_identifier FROM survey_urls WHERE assigned_to_selection_id = ? AND status = 'assigned' ORDER BY id ASC "); $stmt->execute([$selection_id]); $urls = $stmt->fetchAll(PDO::FETCH_ASSOC); // 7. Get all member user_ids for this selection (ordered to pair with URLs) $stmt = $pdo->prepare(" SELECT user_id FROM selection_members WHERE selection_id = ? ORDER BY id ASC "); $stmt->execute([$selection_id]); $members = $stmt->fetchAll(PDO::FETCH_COLUMN); // 8. Pair members with URLs $send_queue = []; for ($i = 0; $i < count($members); $i++) { if (!isset($urls[$i])) break; $send_queue[] = [ 'user_id' => $members[$i], 'url_id' => $urls[$i]['id'], 'proxy_url' => $urls[$i]['rr_proxy_url'], 'unique_id' => $urls[$i]['unique_identifier'] ]; } if (empty($send_queue)) { throw new Exception('No URL-member pairs available to send'); } // 9. Look up member emails from panel DB $user_ids = array_column($send_queue, 'user_id'); $placeholders = implode(',', array_fill(0, count($user_ids), '?')); $stmt = $panelPdo->prepare("SELECT id, email FROM users WHERE id IN ($placeholders) AND status = 'active'"); $stmt->execute($user_ids); $email_map = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); // id => email // 10. Send emails $subject = 'Invitation: Your Online Survey Opportunity from Relevant Reflex India'; $sent_count = 0; $failed_count = 0; $errors = []; $pdo->beginTransaction(); foreach ($send_queue as $item) { $user_id = $item['user_id']; $url_id = $item['url_id']; if (!isset($email_map[$user_id])) { $failed_count++; $errors[] = "User ID $user_id: email not found or inactive"; continue; } $email = $email_map[$user_id]; $html = buildInvitationEmail($item['proxy_url'], $eloi, $incentive); $result = sendEmailViaSendGrid($email, $subject, $html); if ($result['success']) { // Update survey_url: status → sent, link to user $stmt = $pdo->prepare(" UPDATE survey_urls SET status = 'sent', is_sent = 1, sent_to_user_id = ?, sent_at = NOW() WHERE id = ? "); $stmt->execute([$user_id, $url_id]); $sent_count++; } else { $failed_count++; $errors[] = "User ID $user_id ($email): " . $result['message']; error_log("SendGrid error for $email: " . $result['message']); } } // 11. Update selection status $stmt = $pdo->prepare(" UPDATE project_selections SET status = 'live', incentive_amount = ?, invitations_sent_at = NOW(), launched_at = CASE WHEN launched_at IS NULL THEN NOW() ELSE launched_at END WHERE id = ? "); $stmt->execute([$incentive, $selection_id]); // 12. Update project status to 'Live' if not already if ($selection['project_status'] !== 'Live') { $stmt = $pdo->prepare(" UPDATE projects SET status = 'Live', made_live_at = CASE WHEN made_live_at IS NULL THEN NOW() ELSE made_live_at END WHERE id = ? "); $stmt->execute([$selection['proj_id']]); } // 13. Log activity // ✅ FIX: selection_activity_log uses: description (not details), performed_by (not client_id), NO ip_address $details = json_encode([ 'sent' => $sent_count, 'failed' => $failed_count, 'incentive' => $incentive, 'eloi' => $eloi ]); $stmt = $pdo->prepare(" INSERT INTO selection_activity_log (selection_id, action, description, performed_by, created_at) VALUES (?, 'invitations_sent', ?, ?, NOW()) "); $stmt->execute([$selection_id, $details, $_SESSION['client_id']]); // project_activity_log schema is correct as-is (has client_id, details, ip_address) $stmt = $pdo->prepare(" INSERT INTO project_activity_log (project_id, client_id, action, details, ip_address, created_at) VALUES (?, ?, 'invitations_sent', ?, ?, NOW()) "); $stmt->execute([$selection['proj_id'], $_SESSION['client_id'], $details, $_SERVER['REMOTE_ADDR'] ?? '']); $pdo->commit(); echo json_encode([ 'success' => true, 'sent_count' => $sent_count, 'failed_count' => $failed_count, 'errors' => $errors, 'incentive' => $incentive, 'message' => "Successfully sent $sent_count invitation(s)" . ($failed_count > 0 ? ", $failed_count failed" : '') ]); } catch (Exception $e) { if (isset($pdo) && $pdo->inTransaction()) { $pdo->rollBack(); } error_log("Send invitations error: " . $e->getMessage()); echo json_encode([ 'success' => false, 'message' => $e->getMessage() ]); }