"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.responsesService = void 0; const crypto_1 = require("crypto"); const client_1 = require("@prisma/client"); const database_1 = require("../../../config/database"); const error_handler_1 = require("../../../middleware/error-handler"); const email_service_1 = require("../../../services/email.service"); const notification_queue_service_1 = require("../../../services/notification-queue.service"); const notification_helper_1 = require("../../../services/notification.helper"); const env_1 = require("../../../config/env"); const logger_1 = require("../../../utils/logger"); const metrics_1 = require("../../../utils/metrics"); const rocketchat_webhook_service_1 = require("../../../services/rocketchat-webhook.service"); const VERIFICATION_EXPIRY_DAYS = 30; exports.responsesService = { // --- Public --- async submitResponse(slug, data, senderIp) { const campaign = await database_1.prisma.campaign.findUnique({ where: { slug }, select: { id: true, slug: true, title: true, status: true, showResponseWall: true }, }); if (!campaign) { throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND'); } if (campaign.status !== client_1.CampaignStatus.ACTIVE) { throw new error_handler_1.AppError(400, 'Campaign is not active', 'CAMPAIGN_NOT_ACTIVE'); } if (!campaign.showResponseWall) { throw new error_handler_1.AppError(400, 'Response wall is not enabled for this campaign', 'RESPONSE_WALL_DISABLED'); } let verificationToken = null; let verificationSentAt = null; if (data.sendVerification && data.representativeEmail) { verificationToken = (0, crypto_1.randomBytes)(32).toString('hex'); verificationSentAt = new Date(); } const response = await database_1.prisma.representativeResponse.create({ data: { campaignId: campaign.id, campaignSlug: campaign.slug, representativeName: data.representativeName, representativeTitle: data.representativeTitle, representativeLevel: data.representativeLevel, representativeEmail: data.representativeEmail, responseType: data.responseType, responseText: data.responseText, userComment: data.userComment, submittedByName: data.submittedByName, submittedByEmail: data.submittedByEmail, isAnonymous: data.isAnonymous, status: client_1.ResponseStatus.PENDING, verificationToken, verificationSentAt, submittedIp: senderIp, }, }); if (verificationToken && data.representativeEmail) { const baseUrl = env_1.env.API_URL; await email_service_1.emailService.sendResponseVerification({ recipientEmail: data.representativeEmail, campaignTitle: campaign.title, responseType: data.responseType, responseText: data.responseText, submitterName: data.isAnonymous ? 'Anonymous' : (data.submittedByName || 'Anonymous'), verificationUrl: `${baseUrl}/api/responses/${response.id}/verify/${verificationToken}`, reportUrl: `${baseUrl}/api/responses/${response.id}/report/${verificationToken}`, }); } (0, metrics_1.recordResponseSubmission)(); // Notification: admin response submitted alert try { if (await (0, notification_helper_1.isNotificationEnabled)('notifyAdminResponseSubmitted')) { const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN]); if (adminEmails.length > 0) { const adminUrl = `${env_1.env.ADMIN_URL || 'http://localhost:3000'}/app/influence/responses`; await notification_queue_service_1.notificationQueueService.enqueue({ type: 'admin-response-submitted', adminEmails, campaignTitle: campaign.title, representativeName: data.representativeName, responseType: data.responseType, submitterName: data.isAnonymous ? 'Anonymous' : (data.submittedByName || 'Anonymous'), adminUrl, }); } } } catch (err) { logger_1.logger.error('Failed to enqueue response submitted notification:', err); } // Notify Rocket.Chat rocketchat_webhook_service_1.rocketchatWebhookService.onCampaignResponseSubmitted({ campaignTitle: campaign.title, representativeName: data.representativeName, }).catch(() => { }); return { id: response.id, status: response.status, verificationSent: !!verificationToken, }; }, async listApproved(slug, filters) { const { page, limit, sort, level } = filters; const skip = (page - 1) * limit; const where = { campaignSlug: slug, status: client_1.ResponseStatus.APPROVED, }; if (level) where.representativeLevel = level; let orderBy; switch (sort) { case 'upvotes': orderBy = { upvoteCount: 'desc' }; break; case 'verified': orderBy = { isVerified: 'desc' }; break; default: orderBy = { createdAt: 'desc' }; } const [responses, total] = await Promise.all([ database_1.prisma.representativeResponse.findMany({ where, skip, take: limit, orderBy, select: { id: true, representativeName: true, representativeTitle: true, representativeLevel: true, responseType: true, responseText: true, userComment: true, submittedByName: true, isAnonymous: true, isVerified: true, verifiedAt: true, upvoteCount: true, createdAt: true, }, }), database_1.prisma.representativeResponse.count({ where }), ]); return { responses, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; }, async getStats(slug) { const where = { campaignSlug: slug, status: client_1.ResponseStatus.APPROVED }; const [total, verified, upvoteSum, byLevel] = await Promise.all([ database_1.prisma.representativeResponse.count({ where }), database_1.prisma.representativeResponse.count({ where: { ...where, isVerified: true } }), database_1.prisma.representativeResponse.aggregate({ where, _sum: { upvoteCount: true } }), database_1.prisma.representativeResponse.groupBy({ by: ['representativeLevel'], where, _count: true, }), ]); const levelBreakdown = {}; for (const row of byLevel) { levelBreakdown[row.representativeLevel] = row._count; } return { total, verified, totalUpvotes: upvoteSum._sum.upvoteCount || 0, byLevel: levelBreakdown, }; }, async upvote(responseId, userIp, userId) { // Verify response exists and is approved const response = await database_1.prisma.representativeResponse.findUnique({ where: { id: responseId }, select: { id: true, status: true }, }); if (!response) throw new error_handler_1.AppError(404, 'Response not found', 'RESPONSE_NOT_FOUND'); if (response.status !== client_1.ResponseStatus.APPROVED) throw new error_handler_1.AppError(400, 'Response is not approved', 'RESPONSE_NOT_APPROVED'); try { await database_1.prisma.responseUpvote.create({ data: { responseId, userId: userId || null, upvotedIp: !userId ? (userIp || null) : null, }, }); await database_1.prisma.representativeResponse.update({ where: { id: responseId }, data: { upvoteCount: { increment: 1 } }, }); return { success: true }; } catch (err) { if (err.code === 'P2002') { return { success: false, alreadyUpvoted: true }; } throw err; } }, async removeUpvote(responseId, userIp, userId) { const where = { responseId }; if (userId) { where.userId = userId; } else if (userIp) { where.upvotedIp = userIp; } else { return { success: false }; } const deleted = await database_1.prisma.responseUpvote.deleteMany({ where }); if (deleted.count > 0) { await database_1.prisma.representativeResponse.update({ where: { id: responseId }, data: { upvoteCount: { decrement: 1 }, }, }); } return { success: deleted.count > 0 }; }, async verify(responseId, token) { const response = await database_1.prisma.representativeResponse.findUnique({ where: { id: responseId }, select: { id: true, verificationToken: true, verificationSentAt: true, representativeEmail: true, campaign: { select: { title: true } }, }, }); if (!response || response.verificationToken !== token) { return { success: false, reason: 'Invalid verification link' }; } // Check 30-day expiry if (response.verificationSentAt) { const daysSinceSent = (Date.now() - response.verificationSentAt.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceSent > VERIFICATION_EXPIRY_DAYS) { return { success: false, reason: 'Verification link has expired' }; } } await database_1.prisma.representativeResponse.update({ where: { id: responseId }, data: { isVerified: true, verifiedAt: new Date(), verifiedBy: response.representativeEmail || 'Representative', status: client_1.ResponseStatus.APPROVED, }, }); return { success: true, campaignTitle: response.campaign.title }; }, async report(responseId, token) { const response = await database_1.prisma.representativeResponse.findUnique({ where: { id: responseId }, select: { id: true, verificationToken: true, representativeEmail: true, campaign: { select: { title: true } }, }, }); if (!response || response.verificationToken !== token) { return { success: false, reason: 'Invalid verification link' }; } await database_1.prisma.representativeResponse.update({ where: { id: responseId }, data: { status: client_1.ResponseStatus.REJECTED, isVerified: false, verifiedBy: `Disputed by ${response.representativeEmail || 'representative'}`, }, }); return { success: true, campaignTitle: response.campaign.title }; }, // --- Admin --- async findAll(filters) { const { page, limit, status, campaignId, search } = filters; const skip = (page - 1) * limit; const where = {}; if (status) where.status = status; if (campaignId) where.campaignId = campaignId; if (search) { where.OR = [ { representativeName: { contains: search, mode: 'insensitive' } }, { responseText: { contains: search, mode: 'insensitive' } }, { submittedByName: { contains: search, mode: 'insensitive' } }, ]; } const [responses, total] = await Promise.all([ database_1.prisma.representativeResponse.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, select: { id: true, representativeName: true, representativeTitle: true, representativeLevel: true, representativeEmail: true, responseType: true, responseText: true, userComment: true, submittedByName: true, submittedByEmail: true, isAnonymous: true, status: true, isVerified: true, verifiedAt: true, verifiedBy: true, upvoteCount: true, createdAt: true, campaign: { select: { id: true, title: true, slug: true } }, }, }), database_1.prisma.representativeResponse.count({ where }), ]); return { responses, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; }, async updateStatus(id, data) { const existing = await database_1.prisma.representativeResponse.findUnique({ where: { id } }); if (!existing) throw new error_handler_1.AppError(404, 'Response not found', 'RESPONSE_NOT_FOUND'); return database_1.prisma.representativeResponse.update({ where: { id }, data: { status: data.status }, }); }, async deleteResponse(id) { const existing = await database_1.prisma.representativeResponse.findUnique({ where: { id } }); if (!existing) throw new error_handler_1.AppError(404, 'Response not found', 'RESPONSE_NOT_FOUND'); await database_1.prisma.representativeResponse.delete({ where: { id } }); }, async resendVerification(id) { const response = await database_1.prisma.representativeResponse.findUnique({ where: { id }, select: { id: true, representativeEmail: true, representativeName: true, responseType: true, responseText: true, submittedByName: true, isAnonymous: true, verificationToken: true, campaign: { select: { title: true } }, }, }); if (!response) throw new error_handler_1.AppError(404, 'Response not found', 'RESPONSE_NOT_FOUND'); if (!response.representativeEmail) { throw new error_handler_1.AppError(400, 'No representative email on record', 'NO_REPRESENTATIVE_EMAIL'); } // Regenerate token const verificationToken = response.verificationToken || (0, crypto_1.randomBytes)(32).toString('hex'); await database_1.prisma.representativeResponse.update({ where: { id }, data: { verificationToken, verificationSentAt: new Date(), }, }); const baseUrl = env_1.env.API_URL; await email_service_1.emailService.sendResponseVerification({ recipientEmail: response.representativeEmail, campaignTitle: response.campaign.title, responseType: response.responseType, responseText: response.responseText, submitterName: response.isAnonymous ? 'Anonymous' : (response.submittedByName || 'Anonymous'), verificationUrl: `${baseUrl}/api/responses/${id}/verify/${verificationToken}`, reportUrl: `${baseUrl}/api/responses/${id}/report/${verificationToken}`, }); return { success: true }; }, }; //# sourceMappingURL=responses.service.js.map