408 lines
15 KiB
JavaScript
408 lines
15 KiB
JavaScript
"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, '"')
|
|
.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
|