370 lines
15 KiB
JavaScript

"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 env_1 = require("../../../config/env");
const metrics_1 = require("../../../utils/metrics");
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)();
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