"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.campaignsService = void 0; const client_1 = require("@prisma/client"); const database_1 = require("../../../config/database"); const error_handler_1 = require("../../../middleware/error-handler"); const roles_1 = require("../../../utils/roles"); function escapeHtml(unsafe) { return unsafe .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } const campaignSelect = { id: true, slug: true, title: true, description: true, emailSubject: true, emailBody: true, callToAction: true, coverPhoto: true, coverVideoId: true, status: true, allowSmtpEmail: true, allowMailtoLink: true, collectUserInfo: true, showEmailCount: true, showCallCount: true, allowEmailEditing: true, allowCustomRecipients: true, showResponseWall: true, highlightCampaign: true, targetGovernmentLevels: true, createdByUserId: true, createdByUserEmail: true, createdByUserName: true, isUserGenerated: true, moderationStatus: true, reviewedByUserId: true, reviewedAt: true, rejectionReason: true, moderationNotes: true, createdAt: true, updatedAt: true, _count: { select: { emails: true, responses: true, }, }, }; /** Public-facing select — strips admin-only fields (emails, internal IDs, moderation notes) */ const publicCampaignSelect = { id: true, slug: true, title: true, description: true, emailSubject: true, emailBody: true, callToAction: true, coverPhoto: true, coverVideoId: true, status: true, allowSmtpEmail: true, allowMailtoLink: true, collectUserInfo: true, showEmailCount: true, showCallCount: true, allowEmailEditing: true, allowCustomRecipients: true, showResponseWall: true, highlightCampaign: true, targetGovernmentLevels: true, createdByUserName: true, isUserGenerated: true, moderationStatus: true, createdAt: true, updatedAt: true, _count: { select: { emails: true, responses: true, }, }, }; function generateSlug(title) { return title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 80); } async function resolveSlugCollision(slug, excludeId) { let candidate = slug; let suffix = 2; while (true) { const existing = await database_1.prisma.campaign.findUnique({ where: { slug: candidate }, select: { id: true }, }); if (!existing || (excludeId && existing.id === excludeId)) { return candidate; } candidate = `${slug}-${suffix}`; suffix++; } } exports.campaignsService = { async findAll(filters, user) { const { page, limit, search, status } = filters; const skip = (page - 1) * limit; const where = {}; if (search) { where.OR = [ { title: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, ]; } if (status) where.status = status; // Non-admin users only see their own campaigns if (user && !(0, roles_1.hasAnyRole)(user, roles_1.ADMIN_ROLES)) { where.createdByUserId = user.id; } const [campaigns, total] = await Promise.all([ database_1.prisma.campaign.findMany({ where, select: campaignSelect, skip, take: limit, orderBy: { createdAt: 'desc' }, }), database_1.prisma.campaign.count({ where }), ]); return { campaigns, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; }, async findById(id) { const campaign = await database_1.prisma.campaign.findUnique({ where: { id }, select: campaignSelect, }); if (!campaign) { throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND'); } return campaign; }, async findBySlug(slug) { const campaign = await database_1.prisma.campaign.findUnique({ where: { slug }, select: campaignSelect, }); if (!campaign) { throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND'); } return campaign; }, async create(data, user) { const baseSlug = generateSlug(data.title); const slug = await resolveSlugCollision(baseSlug); // Look up user name from DB const dbUser = await database_1.prisma.user.findUnique({ where: { id: user.id }, select: { name: true }, }); // If highlighting this campaign, unset any other highlighted campaign if (data.highlightCampaign) { await database_1.prisma.campaign.updateMany({ where: { highlightCampaign: true }, data: { highlightCampaign: false }, }); } const campaign = await database_1.prisma.campaign.create({ data: { ...data, slug, createdByUserId: user.id, createdByUserEmail: user.email, createdByUserName: dbUser?.name ?? null, }, select: campaignSelect, }); return campaign; }, async update(id, data) { const existing = await database_1.prisma.campaign.findUnique({ where: { id } }); if (!existing) { throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND'); } const updateData = { ...data }; // Regenerate slug if title changes if (data.title && data.title !== existing.title) { const baseSlug = generateSlug(data.title); updateData.slug = await resolveSlugCollision(baseSlug, id); } // If highlighting this campaign, unset any other highlighted campaign if (data.highlightCampaign) { await database_1.prisma.campaign.updateMany({ where: { highlightCampaign: true, id: { not: id } }, data: { highlightCampaign: false }, }); } const campaign = await database_1.prisma.campaign.update({ where: { id }, data: updateData, select: campaignSelect, }); return campaign; }, async findActiveCampaigns() { return database_1.prisma.campaign.findMany({ where: { status: 'ACTIVE' }, select: publicCampaignSelect, orderBy: [ { highlightCampaign: 'desc' }, { createdAt: 'desc' }, ], }); }, async findBySlugPublic(slug) { const campaign = await database_1.prisma.campaign.findUnique({ where: { slug }, select: publicCampaignSelect, }); if (!campaign) { throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND'); } if (campaign.status !== 'ACTIVE') { throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND'); } return campaign; }, async delete(id) { const existing = await database_1.prisma.campaign.findUnique({ where: { id } }); if (!existing) { throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND'); } await database_1.prisma.campaign.delete({ where: { id } }); }, // --- User-Generated Campaign Methods --- async createUserCampaign(data, user) { const baseSlug = generateSlug(data.title); const slug = await resolveSlugCollision(baseSlug); const dbUser = await database_1.prisma.user.findUnique({ where: { id: user.id }, select: { name: true }, }); const campaign = await database_1.prisma.campaign.create({ data: { slug, title: escapeHtml(data.title), description: data.description ? escapeHtml(data.description) : null, emailSubject: escapeHtml(data.emailSubject), emailBody: escapeHtml(data.emailBody), callToAction: data.callToAction ? escapeHtml(data.callToAction) : null, targetGovernmentLevels: data.targetGovernmentLevels, status: 'DRAFT', isUserGenerated: true, moderationStatus: client_1.CampaignModerationStatus.PENDING_REVIEW, allowSmtpEmail: false, allowMailtoLink: true, collectUserInfo: true, showEmailCount: true, showCallCount: false, allowEmailEditing: false, allowCustomRecipients: false, showResponseWall: false, highlightCampaign: false, createdByUserId: user.id, createdByUserEmail: user.email, createdByUserName: dbUser?.name ?? null, }, select: campaignSelect, }); return campaign; }, async findUserCampaigns(userId) { return database_1.prisma.campaign.findMany({ where: { createdByUserId: userId, isUserGenerated: true }, select: campaignSelect, orderBy: { createdAt: 'desc' }, }); }, async updateUserCampaign(id, data, user) { const existing = await database_1.prisma.campaign.findUnique({ where: { id } }); if (!existing) { throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND'); } if (existing.createdByUserId !== user.id) { throw new error_handler_1.AppError(403, 'You can only edit your own campaigns', 'FORBIDDEN'); } if (!existing.isUserGenerated) { throw new error_handler_1.AppError(403, 'Cannot edit admin-created campaigns', 'FORBIDDEN'); } if (existing.moderationStatus !== client_1.CampaignModerationStatus.CHANGES_REQUESTED && existing.moderationStatus !== client_1.CampaignModerationStatus.PENDING_REVIEW) { throw new error_handler_1.AppError(400, 'Campaign cannot be edited in its current state', 'INVALID_STATE'); } const updateData = {}; if (data.title) { updateData.title = escapeHtml(data.title); const baseSlug = generateSlug(data.title); updateData.slug = await resolveSlugCollision(baseSlug, id); } if (data.description !== undefined) updateData.description = data.description ? escapeHtml(data.description) : null; if (data.emailSubject) updateData.emailSubject = escapeHtml(data.emailSubject); if (data.emailBody) updateData.emailBody = escapeHtml(data.emailBody); if (data.callToAction !== undefined) updateData.callToAction = data.callToAction ? escapeHtml(data.callToAction) : null; if (data.targetGovernmentLevels) updateData.targetGovernmentLevels = data.targetGovernmentLevels; // Reset to pending review on edit updateData.moderationStatus = client_1.CampaignModerationStatus.PENDING_REVIEW; updateData.rejectionReason = null; return database_1.prisma.campaign.update({ where: { id }, data: updateData, select: campaignSelect, }); }, // --- Moderation Methods --- async findModerationQueue(filters) { const { page, limit, search, moderationStatus } = filters; const skip = (page - 1) * limit; const where = { isUserGenerated: true }; if (moderationStatus) where.moderationStatus = moderationStatus; if (search) { where.OR = [ { title: { contains: search, mode: 'insensitive' } }, { createdByUserName: { contains: search, mode: 'insensitive' } }, { createdByUserEmail: { contains: search, mode: 'insensitive' } }, ]; } const [campaigns, total] = await Promise.all([ database_1.prisma.campaign.findMany({ where, select: campaignSelect, skip, take: limit, orderBy: { createdAt: 'desc' }, }), database_1.prisma.campaign.count({ where }), ]); return { campaigns, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }, }; }, async getModerationStats() { const [total, pending, approved, rejected, changesRequested] = await Promise.all([ database_1.prisma.campaign.count({ where: { isUserGenerated: true } }), database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.PENDING_REVIEW } }), database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.APPROVED } }), database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.REJECTED } }), database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.CHANGES_REQUESTED } }), ]); return { total, pending, approved, rejected, changesRequested }; }, async moderateCampaign(id, input, reviewer) { const existing = await database_1.prisma.campaign.findUnique({ where: { id } }); if (!existing) { throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND'); } if (!existing.isUserGenerated) { throw new error_handler_1.AppError(400, 'Only user-generated campaigns can be moderated', 'INVALID_STATE'); } const updateData = { reviewedByUserId: reviewer.id, reviewedAt: new Date(), moderationNotes: input.notes ?? null, }; switch (input.action) { case 'approve': updateData.moderationStatus = client_1.CampaignModerationStatus.APPROVED; updateData.status = 'ACTIVE'; updateData.rejectionReason = null; break; case 'reject': updateData.moderationStatus = client_1.CampaignModerationStatus.REJECTED; updateData.rejectionReason = input.reason ?? null; break; case 'request_changes': updateData.moderationStatus = client_1.CampaignModerationStatus.CHANGES_REQUESTED; updateData.rejectionReason = input.reason ?? null; break; } return database_1.prisma.campaign.update({ where: { id }, data: updateData, select: campaignSelect, }); }, }; //# sourceMappingURL=campaigns.service.js.map