Tonne of debugging - getting ready for the production builds

This commit is contained in:
2026-02-16 10:44:18 -07:00
parent a77306fac2
commit 7895ce683e
1367 changed files with 404191 additions and 2005 deletions

View File

@@ -0,0 +1,4 @@
declare const publicRouter: import("express-serve-static-core").Router;
declare const adminRouter: import("express-serve-static-core").Router;
export { publicRouter as campaignEmailsPublicRouter, adminRouter as campaignEmailsAdminRouter };
//# sourceMappingURL=campaign-emails.routes.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaign-emails.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.routes.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,YAAY,4CAAW,CAAC;AAqC9B,QAAA,MAAM,WAAW,4CAAW,CAAC;AAiC7B,OAAO,EAAE,YAAY,IAAI,0BAA0B,EAAE,WAAW,IAAI,yBAAyB,EAAE,CAAC"}

View File

@@ -0,0 +1,67 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.campaignEmailsAdminRouter = exports.campaignEmailsPublicRouter = void 0;
const express_1 = require("express");
const client_1 = require("@prisma/client");
const campaign_emails_service_1 = require("./campaign-emails.service");
const campaign_emails_schemas_1 = require("./campaign-emails.schemas");
const validate_1 = require("../../../middleware/validate");
const auth_middleware_1 = require("../../../middleware/auth.middleware");
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
const rate_limit_1 = require("../../../middleware/rate-limit");
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
// --- Public Routes (no auth) ---
const publicRouter = (0, express_1.Router)();
exports.campaignEmailsPublicRouter = publicRouter;
// POST /api/campaigns/:slug/send-email
publicRouter.post('/:slug/send-email', rate_limit_1.emailRateLimit, (0, validate_1.validate)(campaign_emails_schemas_1.sendCampaignEmailSchema), async (req, res, next) => {
try {
const slug = req.params.slug;
const senderIp = req.ip || req.socket.remoteAddress;
const result = await campaign_emails_service_1.campaignEmailsService.sendEmail(slug, req.body, senderIp);
res.status(201).json(result);
}
catch (err) {
next(err);
}
});
// POST /api/campaigns/:slug/track-mailto
publicRouter.post('/:slug/track-mailto', rate_limit_1.emailRateLimit, (0, validate_1.validate)(campaign_emails_schemas_1.trackMailtoSchema), async (req, res, next) => {
try {
const slug = req.params.slug;
const senderIp = req.ip || req.socket.remoteAddress;
const result = await campaign_emails_service_1.campaignEmailsService.trackMailto(slug, req.body, senderIp);
res.status(201).json(result);
}
catch (err) {
next(err);
}
});
// --- Admin Routes (auth required) ---
const adminRouter = (0, express_1.Router)();
exports.campaignEmailsAdminRouter = adminRouter;
adminRouter.use(auth_middleware_1.authenticate);
adminRouter.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
// GET /api/campaigns/:id/emails
adminRouter.get('/:id/emails', (0, validate_1.validate)(campaign_emails_schemas_1.listCampaignEmailsSchema, 'query'), async (req, res, next) => {
try {
const id = req.params.id;
const result = await campaign_emails_service_1.campaignEmailsService.listByCampaign(id, req.query);
res.json(result);
}
catch (err) {
next(err);
}
});
// GET /api/campaigns/:id/email-stats
adminRouter.get('/:id/email-stats', async (req, res, next) => {
try {
const id = req.params.id;
const stats = await campaign_emails_service_1.campaignEmailsService.getStats(id);
res.json(stats);
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=campaign-emails.routes.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaign-emails.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.routes.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,116 @@
import { z } from 'zod';
export declare const sendCampaignEmailSchema: z.ZodObject<{
userEmail: z.ZodString;
userName: z.ZodString;
postalCode: z.ZodString;
recipientEmail: z.ZodString;
recipientName: z.ZodOptional<z.ZodString>;
recipientTitle: z.ZodOptional<z.ZodString>;
recipientLevel: z.ZodOptional<z.ZodNativeEnum<{
FEDERAL: "FEDERAL";
PROVINCIAL: "PROVINCIAL";
MUNICIPAL: "MUNICIPAL";
SCHOOL_BOARD: "SCHOOL_BOARD";
}>>;
emailMethod: z.ZodNativeEnum<{
SMTP: "SMTP";
MAILTO: "MAILTO";
}>;
customEmailSubject: z.ZodOptional<z.ZodString>;
customEmailBody: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
userEmail: string;
userName: string;
recipientEmail: string;
emailMethod: "SMTP" | "MAILTO";
postalCode: string;
recipientName?: string | undefined;
recipientTitle?: string | undefined;
recipientLevel?: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD" | undefined;
customEmailSubject?: string | undefined;
customEmailBody?: string | undefined;
}, {
userEmail: string;
userName: string;
recipientEmail: string;
emailMethod: "SMTP" | "MAILTO";
postalCode: string;
recipientName?: string | undefined;
recipientTitle?: string | undefined;
recipientLevel?: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD" | undefined;
customEmailSubject?: string | undefined;
customEmailBody?: string | undefined;
}>;
export declare const trackMailtoSchema: z.ZodObject<{
recipientEmail: z.ZodString;
recipientName: z.ZodOptional<z.ZodString>;
recipientTitle: z.ZodOptional<z.ZodString>;
recipientLevel: z.ZodOptional<z.ZodNativeEnum<{
FEDERAL: "FEDERAL";
PROVINCIAL: "PROVINCIAL";
MUNICIPAL: "MUNICIPAL";
SCHOOL_BOARD: "SCHOOL_BOARD";
}>>;
userEmail: z.ZodOptional<z.ZodString>;
userName: z.ZodOptional<z.ZodString>;
postalCode: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
recipientEmail: string;
userEmail?: string | undefined;
userName?: string | undefined;
recipientName?: string | undefined;
recipientTitle?: string | undefined;
recipientLevel?: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD" | undefined;
postalCode?: string | undefined;
}, {
recipientEmail: string;
userEmail?: string | undefined;
userName?: string | undefined;
recipientName?: string | undefined;
recipientTitle?: string | undefined;
recipientLevel?: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD" | undefined;
postalCode?: string | undefined;
}>;
export declare const listCampaignEmailsSchema: z.ZodObject<{
page: z.ZodDefault<z.ZodNumber>;
limit: z.ZodDefault<z.ZodNumber>;
status: z.ZodOptional<z.ZodNativeEnum<{
QUEUED: "QUEUED";
SENT: "SENT";
FAILED: "FAILED";
CLICKED: "CLICKED";
USER_INFO_CAPTURED: "USER_INFO_CAPTURED";
}>>;
emailMethod: z.ZodOptional<z.ZodNativeEnum<{
SMTP: "SMTP";
MAILTO: "MAILTO";
}>>;
}, "strip", z.ZodTypeAny, {
limit: number;
page: number;
status?: "QUEUED" | "SENT" | "FAILED" | "CLICKED" | "USER_INFO_CAPTURED" | undefined;
emailMethod?: "SMTP" | "MAILTO" | undefined;
}, {
status?: "QUEUED" | "SENT" | "FAILED" | "CLICKED" | "USER_INFO_CAPTURED" | undefined;
limit?: number | undefined;
page?: number | undefined;
emailMethod?: "SMTP" | "MAILTO" | undefined;
}>;
export declare const campaignSlugParamSchema: z.ZodObject<{
slug: z.ZodString;
}, "strip", z.ZodTypeAny, {
slug: string;
}, {
slug: string;
}>;
export declare const campaignIdParamSchema: z.ZodObject<{
id: z.ZodString;
}, "strip", z.ZodTypeAny, {
id: string;
}, {
id: string;
}>;
export type SendCampaignEmailInput = z.infer<typeof sendCampaignEmailSchema>;
export type TrackMailtoInput = z.infer<typeof trackMailtoSchema>;
export type ListCampaignEmailsInput = z.infer<typeof listCampaignEmailsSchema>;
//# sourceMappingURL=campaign-emails.schemas.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaign-emails.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWlC,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQ5B,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;EAKnC,CAAC;AAEH,eAAO,MAAM,uBAAuB;;;;;;EAElC,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;EAEhC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}

View File

@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.campaignIdParamSchema = exports.campaignSlugParamSchema = exports.listCampaignEmailsSchema = exports.trackMailtoSchema = exports.sendCampaignEmailSchema = void 0;
const zod_1 = require("zod");
const client_1 = require("@prisma/client");
exports.sendCampaignEmailSchema = zod_1.z.object({
userEmail: zod_1.z.string().email('Valid email is required'),
userName: zod_1.z.string().min(1, 'Name is required'),
postalCode: zod_1.z.string().min(1, 'Postal code is required'),
recipientEmail: zod_1.z.string().email('Valid recipient email is required'),
recipientName: zod_1.z.string().optional(),
recipientTitle: zod_1.z.string().optional(),
recipientLevel: zod_1.z.nativeEnum(client_1.GovernmentLevel).optional(),
emailMethod: zod_1.z.nativeEnum(client_1.EmailMethod),
customEmailSubject: zod_1.z.string().optional(),
customEmailBody: zod_1.z.string().optional(),
});
exports.trackMailtoSchema = zod_1.z.object({
recipientEmail: zod_1.z.string().email('Valid recipient email is required'),
recipientName: zod_1.z.string().optional(),
recipientTitle: zod_1.z.string().optional(),
recipientLevel: zod_1.z.nativeEnum(client_1.GovernmentLevel).optional(),
userEmail: zod_1.z.string().email().optional(),
userName: zod_1.z.string().optional(),
postalCode: zod_1.z.string().optional(),
});
exports.listCampaignEmailsSchema = zod_1.z.object({
page: zod_1.z.coerce.number().int().positive().default(1),
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
status: zod_1.z.nativeEnum(client_1.CampaignEmailStatus).optional(),
emailMethod: zod_1.z.nativeEnum(client_1.EmailMethod).optional(),
});
exports.campaignSlugParamSchema = zod_1.z.object({
slug: zod_1.z.string().min(1),
});
exports.campaignIdParamSchema = zod_1.z.object({
id: zod_1.z.string().min(1),
});
//# sourceMappingURL=campaign-emails.schemas.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaign-emails.schemas.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,2CAAmF;AAEtE,QAAA,uBAAuB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC9C,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,yBAAyB,CAAC;IACtD,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC;IAC/C,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,yBAAyB,CAAC;IACxD,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,mCAAmC,CAAC;IACrE,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,cAAc,EAAE,OAAC,CAAC,UAAU,CAAC,wBAAe,CAAC,CAAC,QAAQ,EAAE;IACxD,WAAW,EAAE,OAAC,CAAC,UAAU,CAAC,oBAAW,CAAC;IACtC,kBAAkB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACzC,eAAe,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAC;AAEU,QAAA,iBAAiB,GAAG,OAAC,CAAC,MAAM,CAAC;IACxC,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,mCAAmC,CAAC;IACrE,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,cAAc,EAAE,OAAC,CAAC,UAAU,CAAC,wBAAe,CAAC,CAAC,QAAQ,EAAE;IACxD,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IACxC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC;AAEU,QAAA,wBAAwB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC/C,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,MAAM,EAAE,OAAC,CAAC,UAAU,CAAC,4BAAmB,CAAC,CAAC,QAAQ,EAAE;IACpD,WAAW,EAAE,OAAC,CAAC,UAAU,CAAC,oBAAW,CAAC,CAAC,QAAQ,EAAE;CAClD,CAAC,CAAC;AAEU,QAAA,uBAAuB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACxB,CAAC,CAAC;AAEU,QAAA,qBAAqB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC5C,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACtB,CAAC,CAAC"}

View File

@@ -0,0 +1,36 @@
import type { SendCampaignEmailInput, TrackMailtoInput, ListCampaignEmailsInput } from './campaign-emails.schemas';
export declare const campaignEmailsService: {
sendEmail(slug: string, data: SendCampaignEmailInput, senderIp?: string): Promise<{
id: string;
status: import(".prisma/client").$Enums.CampaignEmailStatus;
emailMethod: import(".prisma/client").$Enums.EmailMethod;
}>;
trackMailto(slug: string, data: TrackMailtoInput, senderIp?: string): Promise<{
id: string;
status: import(".prisma/client").$Enums.CampaignEmailStatus;
emailMethod: import(".prisma/client").$Enums.EmailMethod;
}>;
listByCampaign(campaignId: string, filters: ListCampaignEmailsInput): Promise<{
emails: {
status: import(".prisma/client").$Enums.CampaignEmailStatus;
id: string;
userEmail: string | null;
userName: string | null;
userPostalCode: string | null;
recipientEmail: string;
recipientName: string | null;
recipientLevel: import(".prisma/client").$Enums.GovernmentLevel | null;
emailMethod: import(".prisma/client").$Enums.EmailMethod;
subject: string;
sentAt: Date;
}[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}>;
getStats(campaignId: string): Promise<Record<string, number>>;
};
//# sourceMappingURL=campaign-emails.service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaign-emails.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.service.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAEnH,eAAO,MAAM,qBAAqB;oBACV,MAAM,QAAQ,sBAAsB,aAAa,MAAM;;;;;sBAwFrD,MAAM,QAAQ,gBAAgB,aAAa,MAAM;;;;;+BAgDxC,MAAM,WAAW,uBAAuB;;;;;;;;;;;;;;;;;;;;;yBA0C9C,MAAM;CAqClC,CAAC"}

View File

@@ -0,0 +1,206 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.campaignEmailsService = void 0;
const client_1 = require("@prisma/client");
const database_1 = require("../../../config/database");
const error_handler_1 = require("../../../middleware/error-handler");
const email_queue_service_1 = require("../../../services/email-queue.service");
const metrics_1 = require("../../../utils/metrics");
exports.campaignEmailsService = {
async sendEmail(slug, data, senderIp) {
const campaign = await database_1.prisma.campaign.findUnique({
where: { slug },
select: {
id: true,
slug: true,
title: true,
status: true,
emailSubject: true,
emailBody: true,
allowSmtpEmail: true,
allowMailtoLink: true,
allowEmailEditing: 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 (data.emailMethod === client_1.EmailMethod.SMTP && !campaign.allowSmtpEmail) {
throw new error_handler_1.AppError(400, 'SMTP email is not enabled for this campaign', 'SMTP_NOT_ALLOWED');
}
if (data.emailMethod === client_1.EmailMethod.MAILTO && !campaign.allowMailtoLink) {
throw new error_handler_1.AppError(400, 'Mailto link is not enabled for this campaign', 'MAILTO_NOT_ALLOWED');
}
// Determine subject and body (custom only if campaign allows editing)
const subject = (campaign.allowEmailEditing && data.customEmailSubject)
? data.customEmailSubject
: campaign.emailSubject;
const message = (campaign.allowEmailEditing && data.customEmailBody)
? data.customEmailBody
: campaign.emailBody;
const status = data.emailMethod === client_1.EmailMethod.SMTP
? client_1.CampaignEmailStatus.QUEUED
: client_1.CampaignEmailStatus.CLICKED;
const campaignEmail = await database_1.prisma.campaignEmail.create({
data: {
campaignId: campaign.id,
campaignSlug: campaign.slug,
userEmail: data.userEmail,
userName: data.userName,
userPostalCode: data.postalCode,
recipientEmail: data.recipientEmail,
recipientName: data.recipientName,
recipientTitle: data.recipientTitle,
recipientLevel: data.recipientLevel,
emailMethod: data.emailMethod,
subject,
message,
status,
senderIp,
},
});
if (data.emailMethod === client_1.EmailMethod.SMTP) {
await email_queue_service_1.emailQueueService.addCampaignEmail({
campaignEmailId: campaignEmail.id,
recipientEmail: data.recipientEmail,
recipientName: data.recipientName,
recipientLevel: data.recipientLevel,
userEmail: data.userEmail,
userName: data.userName,
postalCode: data.postalCode,
subject,
message,
campaignTitle: campaign.title,
});
}
(0, metrics_1.recordCampaignEmail)(campaign.id);
return {
id: campaignEmail.id,
status: campaignEmail.status,
emailMethod: campaignEmail.emailMethod,
};
},
async trackMailto(slug, data, senderIp) {
const campaign = await database_1.prisma.campaign.findUnique({
where: { slug },
select: {
id: true,
slug: true,
title: true,
status: true,
emailSubject: true,
emailBody: true,
allowMailtoLink: 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');
}
const campaignEmail = await database_1.prisma.campaignEmail.create({
data: {
campaignId: campaign.id,
campaignSlug: campaign.slug,
userEmail: data.userEmail,
userName: data.userName,
userPostalCode: data.postalCode,
recipientEmail: data.recipientEmail,
recipientName: data.recipientName,
recipientTitle: data.recipientTitle,
recipientLevel: data.recipientLevel,
emailMethod: client_1.EmailMethod.MAILTO,
subject: campaign.emailSubject,
message: campaign.emailBody,
status: client_1.CampaignEmailStatus.CLICKED,
senderIp,
},
});
return {
id: campaignEmail.id,
status: campaignEmail.status,
emailMethod: campaignEmail.emailMethod,
};
},
async listByCampaign(campaignId, filters) {
const { page, limit, status, emailMethod } = filters;
const skip = (page - 1) * limit;
const where = { campaignId };
if (status)
where.status = status;
if (emailMethod)
where.emailMethod = emailMethod;
const [emails, total] = await Promise.all([
database_1.prisma.campaignEmail.findMany({
where,
skip,
take: limit,
orderBy: { sentAt: 'desc' },
select: {
id: true,
userEmail: true,
userName: true,
userPostalCode: true,
recipientEmail: true,
recipientName: true,
recipientLevel: true,
emailMethod: true,
subject: true,
status: true,
sentAt: true,
},
}),
database_1.prisma.campaignEmail.count({ where }),
]);
return {
emails,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
},
async getStats(campaignId) {
const [totals, byMethod] = await Promise.all([
database_1.prisma.campaignEmail.groupBy({
by: ['status'],
where: { campaignId },
_count: true,
}),
database_1.prisma.campaignEmail.groupBy({
by: ['emailMethod'],
where: { campaignId },
_count: true,
}),
]);
const stats = {
total: 0,
queued: 0,
sent: 0,
failed: 0,
clicked: 0,
smtpCount: 0,
mailtoCount: 0,
};
for (const row of totals) {
stats.total += row._count;
const key = row.status.toLowerCase();
if (key in stats)
stats[key] = row._count;
}
for (const row of byMethod) {
if (row.emailMethod === client_1.EmailMethod.SMTP)
stats.smtpCount = row._count;
if (row.emailMethod === client_1.EmailMethod.MAILTO)
stats.mailtoCount = row._count;
}
return stats;
},
};
//# sourceMappingURL=campaign-emails.service.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export { router as campaignPublicRouter };
//# sourceMappingURL=campaigns-public.routes.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaigns-public.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns-public.routes.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA6BxB,OAAO,EAAE,MAAM,IAAI,oBAAoB,EAAE,CAAC"}

View File

@@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.campaignPublicRouter = void 0;
const express_1 = require("express");
const campaigns_service_1 = require("./campaigns.service");
const router = (0, express_1.Router)();
exports.campaignPublicRouter = router;
// GET /api/campaigns/public — list all active campaigns (public)
router.get('/public', async (_req, res, next) => {
try {
const campaigns = await campaigns_service_1.campaignsService.findActiveCampaigns();
res.json(campaigns);
}
catch (err) {
next(err);
}
});
// GET /api/campaigns/:slug/details — public campaign data (ACTIVE only)
router.get('/:slug/details', async (req, res, next) => {
try {
const slug = req.params.slug;
const campaign = await campaigns_service_1.campaignsService.findBySlugPublic(slug);
res.json(campaign);
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=campaigns-public.routes.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaigns-public.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns-public.routes.ts"],"names":[],"mappings":";;;AAAA,qCAAkE;AAClE,2DAAuD;AAEvD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AA6BL,sCAAoB;AA3BvC,iEAAiE;AACjE,MAAM,CAAC,GAAG,CACR,SAAS,EACT,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,oCAAgB,CAAC,mBAAmB,EAAE,CAAC;QAC/D,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,wEAAwE;AACxE,MAAM,CAAC,GAAG,CACR,gBAAgB,EAChB,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACxD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAc,CAAC;QACvC,MAAM,QAAQ,GAAG,MAAM,oCAAgB,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC/D,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC"}

View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export { router as campaignsRouter };
//# sourceMappingURL=campaigns.routes.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaigns.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.routes.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA6ExB,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,CAAC"}

View File

@@ -0,0 +1,70 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.campaignsRouter = void 0;
const express_1 = require("express");
const client_1 = require("@prisma/client");
const campaigns_service_1 = require("./campaigns.service");
const campaigns_schemas_1 = require("./campaigns.schemas");
const validate_1 = require("../../../middleware/validate");
const auth_middleware_1 = require("../../../middleware/auth.middleware");
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
const router = (0, express_1.Router)();
exports.campaignsRouter = router;
// All campaign admin routes require authentication + admin role
router.use(auth_middleware_1.authenticate);
router.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
// GET /api/campaigns — list campaigns with pagination/filters
router.get('/', (0, validate_1.validate)(campaigns_schemas_1.listCampaignsSchema, 'query'), async (req, res, next) => {
try {
const result = await campaigns_service_1.campaignsService.findAll(req.query, req.user);
res.json(result);
}
catch (err) {
next(err);
}
});
// GET /api/campaigns/:id — get single campaign
router.get('/:id', async (req, res, next) => {
try {
const id = req.params.id;
const campaign = await campaigns_service_1.campaignsService.findById(id);
res.json(campaign);
}
catch (err) {
next(err);
}
});
// POST /api/campaigns — create campaign
router.post('/', (0, validate_1.validate)(campaigns_schemas_1.createCampaignSchema), async (req, res, next) => {
try {
const campaign = await campaigns_service_1.campaignsService.create(req.body, req.user);
res.status(201).json(campaign);
}
catch (err) {
next(err);
}
});
// PUT /api/campaigns/:id — update campaign
router.put('/:id', (0, validate_1.validate)(campaigns_schemas_1.updateCampaignSchema), async (req, res, next) => {
try {
const id = req.params.id;
const campaign = await campaigns_service_1.campaignsService.update(id, req.body);
res.json(campaign);
}
catch (err) {
next(err);
}
});
// DELETE /api/campaigns/:id — delete campaign
router.delete('/:id', async (req, res, next) => {
try {
const id = req.params.id;
await campaigns_service_1.campaignsService.delete(id);
res.status(204).send();
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=campaigns.routes.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaigns.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.routes.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,163 @@
import { z } from 'zod';
export declare const createCampaignSchema: z.ZodObject<{
title: z.ZodString;
description: z.ZodOptional<z.ZodString>;
emailSubject: z.ZodString;
emailBody: z.ZodString;
callToAction: z.ZodOptional<z.ZodString>;
status: z.ZodDefault<z.ZodOptional<z.ZodNativeEnum<{
DRAFT: "DRAFT";
ACTIVE: "ACTIVE";
PAUSED: "PAUSED";
ARCHIVED: "ARCHIVED";
}>>>;
targetGovernmentLevels: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodNativeEnum<{
FEDERAL: "FEDERAL";
PROVINCIAL: "PROVINCIAL";
MUNICIPAL: "MUNICIPAL";
SCHOOL_BOARD: "SCHOOL_BOARD";
}>, "many">>>;
allowSmtpEmail: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
allowMailtoLink: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
collectUserInfo: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
showEmailCount: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
showCallCount: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
allowEmailEditing: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
allowCustomRecipients: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
showResponseWall: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
highlightCampaign: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
coverPhoto: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
status: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED";
title: string;
emailSubject: string;
emailBody: string;
allowSmtpEmail: boolean;
allowMailtoLink: boolean;
collectUserInfo: boolean;
showEmailCount: boolean;
showCallCount: boolean;
allowEmailEditing: boolean;
allowCustomRecipients: boolean;
showResponseWall: boolean;
highlightCampaign: boolean;
targetGovernmentLevels: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[];
description?: string | undefined;
callToAction?: string | undefined;
coverPhoto?: string | undefined;
}, {
title: string;
emailSubject: string;
emailBody: string;
status?: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED" | undefined;
description?: string | undefined;
callToAction?: string | undefined;
coverPhoto?: string | undefined;
allowSmtpEmail?: boolean | undefined;
allowMailtoLink?: boolean | undefined;
collectUserInfo?: boolean | undefined;
showEmailCount?: boolean | undefined;
showCallCount?: boolean | undefined;
allowEmailEditing?: boolean | undefined;
allowCustomRecipients?: boolean | undefined;
showResponseWall?: boolean | undefined;
highlightCampaign?: boolean | undefined;
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
}>;
export declare const updateCampaignSchema: z.ZodObject<{
title: z.ZodOptional<z.ZodString>;
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
emailSubject: z.ZodOptional<z.ZodString>;
emailBody: z.ZodOptional<z.ZodString>;
callToAction: z.ZodOptional<z.ZodNullable<z.ZodString>>;
status: z.ZodOptional<z.ZodNativeEnum<{
DRAFT: "DRAFT";
ACTIVE: "ACTIVE";
PAUSED: "PAUSED";
ARCHIVED: "ARCHIVED";
}>>;
targetGovernmentLevels: z.ZodOptional<z.ZodArray<z.ZodNativeEnum<{
FEDERAL: "FEDERAL";
PROVINCIAL: "PROVINCIAL";
MUNICIPAL: "MUNICIPAL";
SCHOOL_BOARD: "SCHOOL_BOARD";
}>, "many">>;
allowSmtpEmail: z.ZodOptional<z.ZodBoolean>;
allowMailtoLink: z.ZodOptional<z.ZodBoolean>;
collectUserInfo: z.ZodOptional<z.ZodBoolean>;
showEmailCount: z.ZodOptional<z.ZodBoolean>;
showCallCount: z.ZodOptional<z.ZodBoolean>;
allowEmailEditing: z.ZodOptional<z.ZodBoolean>;
allowCustomRecipients: z.ZodOptional<z.ZodBoolean>;
showResponseWall: z.ZodOptional<z.ZodBoolean>;
highlightCampaign: z.ZodOptional<z.ZodBoolean>;
coverPhoto: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}, "strip", z.ZodTypeAny, {
status?: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED" | undefined;
title?: string | undefined;
description?: string | null | undefined;
emailSubject?: string | undefined;
emailBody?: string | undefined;
callToAction?: string | null | undefined;
coverPhoto?: string | null | undefined;
allowSmtpEmail?: boolean | undefined;
allowMailtoLink?: boolean | undefined;
collectUserInfo?: boolean | undefined;
showEmailCount?: boolean | undefined;
showCallCount?: boolean | undefined;
allowEmailEditing?: boolean | undefined;
allowCustomRecipients?: boolean | undefined;
showResponseWall?: boolean | undefined;
highlightCampaign?: boolean | undefined;
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
}, {
status?: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED" | undefined;
title?: string | undefined;
description?: string | null | undefined;
emailSubject?: string | undefined;
emailBody?: string | undefined;
callToAction?: string | null | undefined;
coverPhoto?: string | null | undefined;
allowSmtpEmail?: boolean | undefined;
allowMailtoLink?: boolean | undefined;
collectUserInfo?: boolean | undefined;
showEmailCount?: boolean | undefined;
showCallCount?: boolean | undefined;
allowEmailEditing?: boolean | undefined;
allowCustomRecipients?: boolean | undefined;
showResponseWall?: boolean | undefined;
highlightCampaign?: boolean | undefined;
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
}>;
export declare const listCampaignsSchema: z.ZodObject<{
page: z.ZodDefault<z.ZodNumber>;
limit: z.ZodDefault<z.ZodNumber>;
search: z.ZodOptional<z.ZodString>;
status: z.ZodOptional<z.ZodNativeEnum<{
DRAFT: "DRAFT";
ACTIVE: "ACTIVE";
PAUSED: "PAUSED";
ARCHIVED: "ARCHIVED";
}>>;
}, "strip", z.ZodTypeAny, {
limit: number;
page: number;
status?: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED" | undefined;
search?: string | undefined;
}, {
status?: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED" | undefined;
search?: string | undefined;
limit?: number | undefined;
page?: number | undefined;
}>;
export declare const campaignIdSchema: z.ZodObject<{
id: z.ZodString;
}, "strip", z.ZodTypeAny, {
id: string;
}, {
id: string;
}>;
export type CreateCampaignInput = z.infer<typeof createCampaignSchema>;
export type UpdateCampaignInput = z.infer<typeof updateCampaignSchema>;
export type ListCampaignsInput = z.infer<typeof listCampaignsSchema>;
//# sourceMappingURL=campaigns.schemas.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaigns.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkB/B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkB/B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;EAK9B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;EAE3B,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}

View File

@@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.campaignIdSchema = exports.listCampaignsSchema = exports.updateCampaignSchema = exports.createCampaignSchema = void 0;
const zod_1 = require("zod");
const client_1 = require("@prisma/client");
exports.createCampaignSchema = zod_1.z.object({
title: zod_1.z.string().min(1, 'Title is required'),
description: zod_1.z.string().optional(),
emailSubject: zod_1.z.string().min(1, 'Email subject is required'),
emailBody: zod_1.z.string().min(1, 'Email body is required'),
callToAction: zod_1.z.string().optional(),
status: zod_1.z.nativeEnum(client_1.CampaignStatus).optional().default(client_1.CampaignStatus.DRAFT),
targetGovernmentLevels: zod_1.z.array(zod_1.z.nativeEnum(client_1.GovernmentLevel)).optional().default([]),
allowSmtpEmail: zod_1.z.boolean().optional().default(true),
allowMailtoLink: zod_1.z.boolean().optional().default(true),
collectUserInfo: zod_1.z.boolean().optional().default(true),
showEmailCount: zod_1.z.boolean().optional().default(true),
showCallCount: zod_1.z.boolean().optional().default(true),
allowEmailEditing: zod_1.z.boolean().optional().default(false),
allowCustomRecipients: zod_1.z.boolean().optional().default(false),
showResponseWall: zod_1.z.boolean().optional().default(false),
highlightCampaign: zod_1.z.boolean().optional().default(false),
coverPhoto: zod_1.z.string().optional(),
});
exports.updateCampaignSchema = zod_1.z.object({
title: zod_1.z.string().min(1).optional(),
description: zod_1.z.string().nullable().optional(),
emailSubject: zod_1.z.string().min(1).optional(),
emailBody: zod_1.z.string().min(1).optional(),
callToAction: zod_1.z.string().nullable().optional(),
status: zod_1.z.nativeEnum(client_1.CampaignStatus).optional(),
targetGovernmentLevels: zod_1.z.array(zod_1.z.nativeEnum(client_1.GovernmentLevel)).optional(),
allowSmtpEmail: zod_1.z.boolean().optional(),
allowMailtoLink: zod_1.z.boolean().optional(),
collectUserInfo: zod_1.z.boolean().optional(),
showEmailCount: zod_1.z.boolean().optional(),
showCallCount: zod_1.z.boolean().optional(),
allowEmailEditing: zod_1.z.boolean().optional(),
allowCustomRecipients: zod_1.z.boolean().optional(),
showResponseWall: zod_1.z.boolean().optional(),
highlightCampaign: zod_1.z.boolean().optional(),
coverPhoto: zod_1.z.string().nullable().optional(),
});
exports.listCampaignsSchema = zod_1.z.object({
page: zod_1.z.coerce.number().int().positive().default(1),
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
search: zod_1.z.string().optional(),
status: zod_1.z.nativeEnum(client_1.CampaignStatus).optional(),
});
exports.campaignIdSchema = zod_1.z.object({
id: zod_1.z.string().min(1),
});
//# sourceMappingURL=campaigns.schemas.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaigns.schemas.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.schemas.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,230 @@
import { UserRole } from '@prisma/client';
import type { CreateCampaignInput, UpdateCampaignInput, ListCampaignsInput } from './campaigns.schemas';
interface AuthUser {
id: string;
email: string;
role: UserRole;
}
export declare const campaignsService: {
findAll(filters: ListCampaignsInput, user?: AuthUser): Promise<{
campaigns: {
status: import(".prisma/client").$Enums.CampaignStatus;
id: string;
createdAt: Date;
updatedAt: Date;
_count: {
responses: number;
emails: number;
};
title: string;
description: string | null;
slug: string;
emailSubject: string;
emailBody: string;
callToAction: string | null;
coverPhoto: string | null;
allowSmtpEmail: boolean;
allowMailtoLink: boolean;
collectUserInfo: boolean;
showEmailCount: boolean;
showCallCount: boolean;
allowEmailEditing: boolean;
allowCustomRecipients: boolean;
showResponseWall: boolean;
highlightCampaign: boolean;
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
createdByUserEmail: string | null;
createdByUserName: string | null;
createdByUserId: string | null;
}[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}>;
findById(id: string): Promise<{
status: import(".prisma/client").$Enums.CampaignStatus;
id: string;
createdAt: Date;
updatedAt: Date;
_count: {
responses: number;
emails: number;
};
title: string;
description: string | null;
slug: string;
emailSubject: string;
emailBody: string;
callToAction: string | null;
coverPhoto: string | null;
allowSmtpEmail: boolean;
allowMailtoLink: boolean;
collectUserInfo: boolean;
showEmailCount: boolean;
showCallCount: boolean;
allowEmailEditing: boolean;
allowCustomRecipients: boolean;
showResponseWall: boolean;
highlightCampaign: boolean;
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
createdByUserEmail: string | null;
createdByUserName: string | null;
createdByUserId: string | null;
}>;
findBySlug(slug: string): Promise<{
status: import(".prisma/client").$Enums.CampaignStatus;
id: string;
createdAt: Date;
updatedAt: Date;
_count: {
responses: number;
emails: number;
};
title: string;
description: string | null;
slug: string;
emailSubject: string;
emailBody: string;
callToAction: string | null;
coverPhoto: string | null;
allowSmtpEmail: boolean;
allowMailtoLink: boolean;
collectUserInfo: boolean;
showEmailCount: boolean;
showCallCount: boolean;
allowEmailEditing: boolean;
allowCustomRecipients: boolean;
showResponseWall: boolean;
highlightCampaign: boolean;
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
createdByUserEmail: string | null;
createdByUserName: string | null;
createdByUserId: string | null;
}>;
create(data: CreateCampaignInput, user: AuthUser): Promise<{
status: import(".prisma/client").$Enums.CampaignStatus;
id: string;
createdAt: Date;
updatedAt: Date;
_count: {
responses: number;
emails: number;
};
title: string;
description: string | null;
slug: string;
emailSubject: string;
emailBody: string;
callToAction: string | null;
coverPhoto: string | null;
allowSmtpEmail: boolean;
allowMailtoLink: boolean;
collectUserInfo: boolean;
showEmailCount: boolean;
showCallCount: boolean;
allowEmailEditing: boolean;
allowCustomRecipients: boolean;
showResponseWall: boolean;
highlightCampaign: boolean;
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
createdByUserEmail: string | null;
createdByUserName: string | null;
createdByUserId: string | null;
}>;
update(id: string, data: UpdateCampaignInput): Promise<{
status: import(".prisma/client").$Enums.CampaignStatus;
id: string;
createdAt: Date;
updatedAt: Date;
_count: {
responses: number;
emails: number;
};
title: string;
description: string | null;
slug: string;
emailSubject: string;
emailBody: string;
callToAction: string | null;
coverPhoto: string | null;
allowSmtpEmail: boolean;
allowMailtoLink: boolean;
collectUserInfo: boolean;
showEmailCount: boolean;
showCallCount: boolean;
allowEmailEditing: boolean;
allowCustomRecipients: boolean;
showResponseWall: boolean;
highlightCampaign: boolean;
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
createdByUserEmail: string | null;
createdByUserName: string | null;
createdByUserId: string | null;
}>;
findActiveCampaigns(): Promise<{
status: import(".prisma/client").$Enums.CampaignStatus;
id: string;
createdAt: Date;
updatedAt: Date;
_count: {
responses: number;
emails: number;
};
title: string;
description: string | null;
slug: string;
emailSubject: string;
emailBody: string;
callToAction: string | null;
coverPhoto: string | null;
allowSmtpEmail: boolean;
allowMailtoLink: boolean;
collectUserInfo: boolean;
showEmailCount: boolean;
showCallCount: boolean;
allowEmailEditing: boolean;
allowCustomRecipients: boolean;
showResponseWall: boolean;
highlightCampaign: boolean;
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
createdByUserEmail: string | null;
createdByUserName: string | null;
createdByUserId: string | null;
}[]>;
findBySlugPublic(slug: string): Promise<{
status: import(".prisma/client").$Enums.CampaignStatus;
id: string;
createdAt: Date;
updatedAt: Date;
_count: {
responses: number;
emails: number;
};
title: string;
description: string | null;
slug: string;
emailSubject: string;
emailBody: string;
callToAction: string | null;
coverPhoto: string | null;
allowSmtpEmail: boolean;
allowMailtoLink: boolean;
collectUserInfo: boolean;
showEmailCount: boolean;
showCallCount: boolean;
allowEmailEditing: boolean;
allowCustomRecipients: boolean;
showResponseWall: boolean;
highlightCampaign: boolean;
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
createdByUserEmail: string | null;
createdByUserName: string | null;
createdByUserId: string | null;
}>;
delete(id: string): Promise<void>;
};
export {};
//# sourceMappingURL=campaigns.service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"campaigns.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AA8DxG,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,eAAO,MAAM,gBAAgB;qBACJ,kBAAkB,SAAS,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2CvC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAaF,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAaV,mBAAmB,QAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAgCrC,MAAM,QAAQ,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BA0CrB,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAiBlB,MAAM;CAQxB,CAAC"}

View File

@@ -0,0 +1,202 @@
"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 campaignSelect = {
id: true,
slug: true,
title: true,
description: true,
emailSubject: true,
emailBody: true,
callToAction: true,
coverPhoto: 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,
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
const adminRoles = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
if (user && !adminRoles.includes(user.role)) {
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: campaignSelect,
orderBy: [
{ highlightCampaign: 'desc' },
{ createdAt: 'desc' },
],
});
},
async findBySlugPublic(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');
}
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 } });
},
};
//# sourceMappingURL=campaigns.service.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export { router as emailQueueRouter };
//# sourceMappingURL=email-queue.routes.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"email-queue.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/email-queue/email-queue.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAwDxB,OAAO,EAAE,MAAM,IAAI,gBAAgB,EAAE,CAAC"}

View File

@@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.emailQueueRouter = void 0;
const express_1 = require("express");
const client_1 = require("@prisma/client");
const auth_middleware_1 = require("../../../middleware/auth.middleware");
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
const email_queue_service_1 = require("../../../services/email-queue.service");
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
const router = (0, express_1.Router)();
exports.emailQueueRouter = router;
router.use(auth_middleware_1.authenticate);
router.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
// GET /api/email-queue/stats
router.get('/stats', async (_req, res, next) => {
try {
const stats = await email_queue_service_1.emailQueueService.getStats();
res.json(stats);
}
catch (err) {
next(err);
}
});
// POST /api/email-queue/pause
router.post('/pause', async (_req, res, next) => {
try {
await email_queue_service_1.emailQueueService.pause();
res.json({ message: 'Queue paused' });
}
catch (err) {
next(err);
}
});
// POST /api/email-queue/resume
router.post('/resume', async (_req, res, next) => {
try {
await email_queue_service_1.emailQueueService.resume();
res.json({ message: 'Queue resumed' });
}
catch (err) {
next(err);
}
});
// POST /api/email-queue/clean
router.post('/clean', async (_req, res, next) => {
try {
const cleaned = await email_queue_service_1.emailQueueService.clean();
res.json({ message: `Cleaned ${cleaned} completed jobs`, cleaned });
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=email-queue.routes.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"email-queue.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/email-queue/email-queue.routes.ts"],"names":[],"mappings":";;;AAAA,qCAAkE;AAClE,2CAA0C;AAC1C,yEAAmE;AACnE,yEAAkE;AAClE,+EAA0E;AAE1E,MAAM,WAAW,GAAe,CAAC,iBAAQ,CAAC,WAAW,EAAE,iBAAQ,CAAC,eAAe,EAAE,iBAAQ,CAAC,SAAS,CAAC,CAAC;AAErG,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAwDL,kCAAgB;AAvDnC,MAAM,CAAC,GAAG,CAAC,8BAAY,CAAC,CAAC;AACzB,MAAM,CAAC,GAAG,CAAC,IAAA,6BAAW,EAAC,GAAG,WAAW,CAAC,CAAC,CAAC;AAExC,6BAA6B;AAC7B,MAAM,CAAC,GAAG,CACR,QAAQ,EACR,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,uCAAiB,CAAC,QAAQ,EAAE,CAAC;QACjD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,8BAA8B;AAC9B,MAAM,CAAC,IAAI,CACT,QAAQ,EACR,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,uCAAiB,CAAC,KAAK,EAAE,CAAC;QAChC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,+BAA+B;AAC/B,MAAM,CAAC,IAAI,CACT,SAAS,EACT,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,uCAAiB,CAAC,MAAM,EAAE,CAAC;QACjC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,8BAA8B;AAC9B,MAAM,CAAC,IAAI,CACT,QAAQ,EACR,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,uCAAiB,CAAC,KAAK,EAAE,CAAC;QAChD,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,OAAO,iBAAiB,EAAE,OAAO,EAAE,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC"}

View File

@@ -0,0 +1,22 @@
import { z } from 'zod';
/** Strip spaces, uppercase */
export declare function normalizePostalCode(raw: string): string;
/** Validate Canadian postal code (all provinces) */
export declare function isValidCanadianPostalCode(code: string): boolean;
export declare const postalCodeParamSchema: z.ZodObject<{
postalCode: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
}, "strip", z.ZodTypeAny, {
postalCode: string;
}, {
postalCode: string;
}>;
export declare const postalCodeQuerySchema: z.ZodObject<{
refresh: z.ZodDefault<z.ZodOptional<z.ZodEnum<["true", "false"]>>>;
}, "strip", z.ZodTypeAny, {
refresh: "true" | "false";
}, {
refresh?: "true" | "false" | undefined;
}>;
export type PostalCodeParam = z.infer<typeof postalCodeParamSchema>;
export type PostalCodeQuery = z.infer<typeof postalCodeQuerySchema>;
//# sourceMappingURL=postal-codes.schemas.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"postal-codes.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/postal-codes/postal-codes.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,8BAA8B;AAC9B,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,oDAAoD;AACpD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED,eAAO,MAAM,qBAAqB;;;;;;EAKhC,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;EAEhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACpE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}

View File

@@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.postalCodeQuerySchema = exports.postalCodeParamSchema = void 0;
exports.normalizePostalCode = normalizePostalCode;
exports.isValidCanadianPostalCode = isValidCanadianPostalCode;
const zod_1 = require("zod");
/** Strip spaces, uppercase */
function normalizePostalCode(raw) {
return raw.replace(/\s/g, '').toUpperCase();
}
/** Validate Canadian postal code (all provinces) */
function isValidCanadianPostalCode(code) {
return /^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ]\d[ABCEGHJKLMNPRSTVWXYZ]\d$/.test(code);
}
exports.postalCodeParamSchema = zod_1.z.object({
postalCode: zod_1.z
.string()
.transform(normalizePostalCode)
.refine(isValidCanadianPostalCode, { message: 'Invalid Canadian postal code' }),
});
exports.postalCodeQuerySchema = zod_1.z.object({
refresh: zod_1.z.enum(['true', 'false']).optional().default('false'),
});
//# sourceMappingURL=postal-codes.schemas.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"postal-codes.schemas.js","sourceRoot":"","sources":["../../../../src/modules/influence/postal-codes/postal-codes.schemas.ts"],"names":[],"mappings":";;;AAGA,kDAEC;AAGD,8DAEC;AAVD,6BAAwB;AAExB,8BAA8B;AAC9B,SAAgB,mBAAmB,CAAC,GAAW;IAC7C,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AAC9C,CAAC;AAED,oDAAoD;AACpD,SAAgB,yBAAyB,CAAC,IAAY;IACpD,OAAO,0EAA0E,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC/F,CAAC;AAEY,QAAA,qBAAqB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC5C,UAAU,EAAE,OAAC;SACV,MAAM,EAAE;SACR,SAAS,CAAC,mBAAmB,CAAC;SAC9B,MAAM,CAAC,yBAAyB,EAAE,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAAC;CAClF,CAAC,CAAC;AAEU,QAAA,qBAAqB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC5C,OAAO,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;CAC/D,CAAC,CAAC"}

View File

@@ -0,0 +1,60 @@
import { Prisma } from '@prisma/client';
interface UpsertData {
postalCode: string;
city?: string | null;
province?: string | null;
centroidLat?: number | null;
centroidLng?: number | null;
}
export declare const postalCodesService: {
upsert(data: UpsertData): Promise<{
id: string;
city: string | null;
postalCode: string;
province: string | null;
centroidLat: Prisma.Decimal | null;
centroidLng: Prisma.Decimal | null;
lastUpdated: Date;
}>;
findByPostalCode(code: string): Promise<{
id: string;
city: string | null;
postalCode: string;
province: string | null;
centroidLat: Prisma.Decimal | null;
centroidLng: Prisma.Decimal | null;
lastUpdated: Date;
} | null>;
findAll(filters: {
page: number;
limit: number;
search?: string;
}): Promise<{
postalCodes: {
id: string;
city: string | null;
postalCode: string;
province: string | null;
centroidLat: Prisma.Decimal | null;
centroidLng: Prisma.Decimal | null;
lastUpdated: Date;
}[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}>;
delete(code: string): Promise<{
id: string;
city: string | null;
postalCode: string;
province: string | null;
centroidLat: Prisma.Decimal | null;
centroidLng: Prisma.Decimal | null;
lastUpdated: Date;
}>;
};
export {};
//# sourceMappingURL=postal-codes.service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"postal-codes.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/postal-codes/postal-codes.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC,UAAU,UAAU;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,eAAO,MAAM,kBAAkB;iBACV,UAAU;;;;;;;;;2BAoBA,MAAM;;;;;;;;;qBAMZ;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;;;;;;;;iBA6BpD,MAAM;;;;;;;;;CAK1B,CAAC"}

View File

@@ -0,0 +1,61 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.postalCodesService = void 0;
const database_1 = require("../../../config/database");
exports.postalCodesService = {
async upsert(data) {
return database_1.prisma.postalCodeCache.upsert({
where: { postalCode: data.postalCode },
update: {
city: data.city ?? undefined,
province: data.province ?? undefined,
centroidLat: data.centroidLat ?? undefined,
centroidLng: data.centroidLng ?? undefined,
lastUpdated: new Date(),
},
create: {
postalCode: data.postalCode,
city: data.city ?? null,
province: data.province ?? null,
centroidLat: data.centroidLat ?? null,
centroidLng: data.centroidLng ?? null,
},
});
},
async findByPostalCode(code) {
return database_1.prisma.postalCodeCache.findUnique({
where: { postalCode: code },
});
},
async findAll(filters) {
const { page, limit, search } = filters;
const skip = (page - 1) * limit;
const where = {};
if (search) {
where.OR = [
{ postalCode: { contains: search, mode: 'insensitive' } },
{ city: { contains: search, mode: 'insensitive' } },
{ province: { contains: search, mode: 'insensitive' } },
];
}
const [items, total] = await Promise.all([
database_1.prisma.postalCodeCache.findMany({
where,
skip,
take: limit,
orderBy: { lastUpdated: 'desc' },
}),
database_1.prisma.postalCodeCache.count({ where }),
]);
return {
postalCodes: items,
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
};
},
async delete(code) {
return database_1.prisma.postalCodeCache.delete({
where: { postalCode: code },
});
},
};
//# sourceMappingURL=postal-codes.service.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"postal-codes.service.js","sourceRoot":"","sources":["../../../../src/modules/influence/postal-codes/postal-codes.service.ts"],"names":[],"mappings":";;;AACA,uDAAkD;AAUrC,QAAA,kBAAkB,GAAG;IAChC,KAAK,CAAC,MAAM,CAAC,IAAgB;QAC3B,OAAO,iBAAM,CAAC,eAAe,CAAC,MAAM,CAAC;YACnC,KAAK,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE;YACtC,MAAM,EAAE;gBACN,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,SAAS;gBAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,SAAS;gBACpC,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,SAAS;gBAC1C,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,SAAS;gBAC1C,WAAW,EAAE,IAAI,IAAI,EAAE;aACxB;YACD,MAAM,EAAE;gBACN,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI;gBACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI;gBAC/B,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;gBACrC,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;aACtC;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,IAAY;QACjC,OAAO,iBAAM,CAAC,eAAe,CAAC,UAAU,CAAC;YACvC,KAAK,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;SAC5B,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAAyD;QACrE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QACxC,MAAM,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;QAEhC,MAAM,KAAK,GAAqC,EAAE,CAAC;QACnD,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,CAAC,EAAE,GAAG;gBACT,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;gBACzD,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;gBACnD,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;aACxD,CAAC;QACJ,CAAC;QAED,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACvC,iBAAM,CAAC,eAAe,CAAC,QAAQ,CAAC;gBAC9B,KAAK;gBACL,IAAI;gBACJ,IAAI,EAAE,KAAK;gBACX,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE;aACjC,CAAC;YACF,iBAAM,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC;SACxC,CAAC,CAAC;QAEH,OAAO;YACL,WAAW,EAAE,KAAK;YAClB,UAAU,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,EAAE;SACzE,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,OAAO,iBAAM,CAAC,eAAe,CAAC,MAAM,CAAC;YACnC,KAAK,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;SAC5B,CAAC,CAAC;IACL,CAAC;CACF,CAAC"}

View File

@@ -0,0 +1,39 @@
export interface RepresentOffice {
type?: string;
tel?: string;
fax?: string;
postal?: string;
}
export interface RepresentRepresentative {
name: string;
email: string | null;
elected_office: string;
district_name: string;
party_name: string | null;
representative_set_name: string;
url: string;
photo_url: string | null;
offices: RepresentOffice[];
}
export interface RepresentPostalCodeResponse {
city: string | null;
province: string | null;
centroid: {
type: string;
coordinates: [number, number];
} | null;
representatives_centroid: RepresentRepresentative[];
representatives_concordance: RepresentRepresentative[];
}
declare class RepresentApiClient {
private baseUrl;
constructor();
getByPostalCode(code: string): Promise<RepresentPostalCodeResponse>;
testConnection(): Promise<{
ok: boolean;
message: string;
}>;
}
export declare const representApiClient: RepresentApiClient;
export {};
//# sourceMappingURL=represent-api.client.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"represent-api.client.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/represent-api.client.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,uBAAuB,EAAE,MAAM,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;IACjE,wBAAwB,EAAE,uBAAuB,EAAE,CAAC;IACpD,2BAA2B,EAAE,uBAAuB,EAAE,CAAC;CACxD;AAuBD,cAAM,kBAAkB;IACtB,OAAO,CAAC,OAAO,CAAS;;IAMlB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,CAAC;IA6BnE,cAAc,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAyBlE;AAED,eAAO,MAAM,kBAAkB,oBAA2B,CAAC"}

View File

@@ -0,0 +1,77 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.representApiClient = void 0;
const env_1 = require("../../../config/env");
const logger_1 = require("../../../utils/logger");
// --- In-memory sliding window rate limiter (55/min, under 60/min API limit) ---
const RATE_LIMIT = 55;
const RATE_WINDOW_MS = 60_000;
const requestTimestamps = [];
function checkRateLimit() {
const now = Date.now();
// Remove timestamps outside the window
while (requestTimestamps.length > 0 && requestTimestamps[0] < now - RATE_WINDOW_MS) {
requestTimestamps.shift();
}
return requestTimestamps.length < RATE_LIMIT;
}
function recordRequest() {
requestTimestamps.push(Date.now());
}
// --- API Client ---
class RepresentApiClient {
baseUrl;
constructor() {
this.baseUrl = env_1.env.REPRESENT_API_URL;
}
async getByPostalCode(code) {
if (!checkRateLimit()) {
throw new Error('Represent API rate limit reached. Please try again in a minute.');
}
const url = `${this.baseUrl}/postcodes/${code}/`;
logger_1.logger.debug(`Represent API request: ${url}`);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
recordRequest();
const response = await fetch(url, {
signal: controller.signal,
headers: { Accept: 'application/json' },
});
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(`Represent API error ${response.status}: ${text}`);
}
return (await response.json());
}
finally {
clearTimeout(timeout);
}
}
async testConnection() {
try {
const url = `${this.baseUrl}/boundary-sets/?limit=1`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
const response = await fetch(url, {
signal: controller.signal,
headers: { Accept: 'application/json' },
});
if (!response.ok) {
return { ok: false, message: `HTTP ${response.status}` };
}
return { ok: true, message: 'Represent API is reachable' };
}
finally {
clearTimeout(timeout);
}
}
catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return { ok: false, message };
}
}
}
exports.representApiClient = new RepresentApiClient();
//# sourceMappingURL=represent-api.client.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"represent-api.client.js","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/represent-api.client.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export { router as representativesRouter };
//# sourceMappingURL=representatives.routes.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"representatives.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.routes.ts"],"names":[],"mappings":"AAWA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAiHxB,OAAO,EAAE,MAAM,IAAI,qBAAqB,EAAE,CAAC"}

View File

@@ -0,0 +1,98 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.representativesRouter = void 0;
const express_1 = require("express");
const client_1 = require("@prisma/client");
const representatives_service_1 = require("./representatives.service");
const representatives_schemas_1 = require("./representatives.schemas");
const postal_codes_schemas_1 = require("../postal-codes/postal-codes.schemas");
const validate_1 = require("../../../middleware/validate");
const auth_middleware_1 = require("../../../middleware/auth.middleware");
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
const router = (0, express_1.Router)();
exports.representativesRouter = router;
// =============================================
// PUBLIC ROUTES (no auth required)
// =============================================
// GET /api/representatives/by-postal/:postalCode — cache-first lookup
router.get('/by-postal/:postalCode', (0, validate_1.validate)(postal_codes_schemas_1.postalCodeParamSchema, 'params'), (0, validate_1.validate)(postal_codes_schemas_1.postalCodeQuerySchema, 'query'), async (req, res, next) => {
try {
const code = req.params.postalCode;
const refresh = req.query.refresh === 'true';
const result = await representatives_service_1.representativesService.lookupByPostalCode(code, refresh);
res.json(result);
}
catch (err) {
next(err);
}
});
// GET /api/representatives/test-connection — Represent API health check
router.get('/test-connection', async (_req, res, next) => {
try {
const result = await representatives_service_1.representativesService.testApiConnection();
res.json(result);
}
catch (err) {
next(err);
}
});
// =============================================
// ADMIN ROUTES (auth + role required)
// =============================================
router.use(auth_middleware_1.authenticate);
router.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
// GET /api/representatives/cache-stats — cache statistics
router.get('/cache-stats', async (_req, res, next) => {
try {
const stats = await representatives_service_1.representativesService.getCacheStats();
res.json(stats);
}
catch (err) {
next(err);
}
});
// GET /api/representatives — list all cached reps (paginated)
router.get('/', (0, validate_1.validate)(representatives_schemas_1.listRepresentativesSchema, 'query'), async (req, res, next) => {
try {
const result = await representatives_service_1.representativesService.findAll(req.query);
res.json(result);
}
catch (err) {
next(err);
}
});
// GET /api/representatives/:id — single cached rep
router.get('/:id', async (req, res, next) => {
try {
const id = req.params.id;
const rep = await representatives_service_1.representativesService.findById(id);
res.json(rep);
}
catch (err) {
next(err);
}
});
// DELETE /api/representatives/by-postal/:postalCode — clear cache for postal code
router.delete('/by-postal/:postalCode', (0, validate_1.validate)(postal_codes_schemas_1.postalCodeParamSchema, 'params'), async (req, res, next) => {
try {
const code = req.params.postalCode;
const result = await representatives_service_1.representativesService.clearByPostalCode(code);
res.json(result);
}
catch (err) {
next(err);
}
});
// DELETE /api/representatives/:id — delete single cached rep
router.delete('/:id', async (req, res, next) => {
try {
const id = req.params.id;
await representatives_service_1.representativesService.deleteById(id);
res.status(204).send();
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=representatives.routes.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"representatives.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.routes.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,19 @@
import { z } from 'zod';
export declare const listRepresentativesSchema: z.ZodObject<{
page: z.ZodDefault<z.ZodNumber>;
limit: z.ZodDefault<z.ZodNumber>;
search: z.ZodOptional<z.ZodString>;
postalCode: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
limit: number;
page: number;
search?: string | undefined;
postalCode?: string | undefined;
}, {
search?: string | undefined;
limit?: number | undefined;
page?: number | undefined;
postalCode?: string | undefined;
}>;
export type ListRepresentativesInput = z.infer<typeof listRepresentativesSchema>;
//# sourceMappingURL=representatives.schemas.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"representatives.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;EAKpC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}

View File

@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.listRepresentativesSchema = void 0;
const zod_1 = require("zod");
exports.listRepresentativesSchema = zod_1.z.object({
page: zod_1.z.coerce.number().int().positive().default(1),
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
search: zod_1.z.string().optional(),
postalCode: zod_1.z.string().optional(),
});
//# sourceMappingURL=representatives.schemas.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"representatives.schemas.js","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAEX,QAAA,yBAAyB,GAAG,OAAC,CAAC,MAAM,CAAC;IAChD,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC"}

View File

@@ -0,0 +1,98 @@
import { Prisma } from '@prisma/client';
import type { ListRepresentativesInput } from './representatives.schemas';
export declare const representativesService: {
lookupByPostalCode(code: string, forceRefresh?: boolean): Promise<{
source: "cache";
postalCode: string;
location: {
city: string | null;
province: string | null;
};
representatives: {
id: string;
email: string | null;
name: string | null;
url: string | null;
postalCode: string;
districtName: string | null;
electedOffice: string | null;
partyName: string | null;
representativeSetName: string | null;
photoUrl: string | null;
offices: Prisma.JsonValue | null;
cachedAt: Date;
}[];
} | {
source: "api";
postalCode: string;
location: {
city: string | null;
province: string | null;
};
representatives: {
id: null;
postalCode: string;
name: string | null;
email: string | null;
districtName: string | null;
electedOffice: string | null;
partyName: string | null;
representativeSetName: string | null;
url: string | null;
photoUrl: string | null;
offices: import("./represent-api.client").RepresentOffice[];
cachedAt: string;
}[];
}>;
findAll(filters: ListRepresentativesInput): Promise<{
representatives: {
id: string;
email: string | null;
name: string | null;
url: string | null;
postalCode: string;
districtName: string | null;
electedOffice: string | null;
partyName: string | null;
representativeSetName: string | null;
photoUrl: string | null;
offices: Prisma.JsonValue | null;
cachedAt: Date;
}[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}>;
findById(id: string): Promise<{
id: string;
email: string | null;
name: string | null;
url: string | null;
postalCode: string;
districtName: string | null;
electedOffice: string | null;
partyName: string | null;
representativeSetName: string | null;
photoUrl: string | null;
offices: Prisma.JsonValue | null;
cachedAt: Date;
}>;
clearByPostalCode(code: string): Promise<{
deleted: number;
postalCode: string;
}>;
deleteById(id: string): Promise<void>;
testApiConnection(): Promise<{
ok: boolean;
message: string;
}>;
getCacheStats(): Promise<{
totalRepresentatives: number;
postalCodesWithRepresentatives: number;
totalPostalCodes: number;
}>;
};
//# sourceMappingURL=representatives.service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"representatives.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAMxC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAY1E,eAAO,MAAM,sBAAsB;6BACF,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAiGd,wBAAwB;;;;;;;;;;;;;;;;;;;;;;iBAmC5B,MAAM;;;;;;;;;;;;;;4BAQK,MAAM;;;;mBAOf,MAAM;;;;;;;;;;CAyB5B,CAAC"}

View File

@@ -0,0 +1,175 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.representativesService = void 0;
const client_1 = require("@prisma/client");
const database_1 = require("../../../config/database");
const logger_1 = require("../../../utils/logger");
const error_handler_1 = require("../../../middleware/error-handler");
const represent_api_client_1 = require("./represent-api.client");
const postal_codes_service_1 = require("../postal-codes/postal-codes.service");
function deduplicateReps(reps) {
const seen = new Set();
return reps.filter((rep) => {
const key = `${rep.name}|${rep.elected_office}`;
if (seen.has(key))
return false;
seen.add(key);
return true;
});
}
exports.representativesService = {
async lookupByPostalCode(code, forceRefresh = false) {
// 1. Check cache unless forcing refresh
if (!forceRefresh) {
const cached = await database_1.prisma.representative.findMany({
where: { postalCode: code },
});
if (cached.length > 0) {
const postalInfo = await postal_codes_service_1.postalCodesService.findByPostalCode(code);
return {
source: 'cache',
postalCode: code,
location: {
city: postalInfo?.city ?? null,
province: postalInfo?.province ?? null,
},
representatives: cached,
};
}
}
// 2. Call Represent API
const apiResponse = await represent_api_client_1.representApiClient.getByPostalCode(code);
// Merge centroid + concordance reps and deduplicate
const allReps = [
...(apiResponse.representatives_centroid || []),
...(apiResponse.representatives_concordance || []),
];
const uniqueReps = deduplicateReps(allReps);
// 3. Fire-and-forget cache write
const cacheWrite = async () => {
try {
// Delete old cached reps for this postal code
await database_1.prisma.representative.deleteMany({ where: { postalCode: code } });
// Cache new reps
if (uniqueReps.length > 0) {
await database_1.prisma.representative.createMany({
data: uniqueReps.map((rep) => ({
postalCode: code,
name: rep.name || null,
email: rep.email || null,
districtName: rep.district_name || null,
electedOffice: rep.elected_office || null,
partyName: rep.party_name || null,
representativeSetName: rep.representative_set_name || null,
url: rep.url || null,
photoUrl: rep.photo_url || null,
offices: rep.offices ? rep.offices : client_1.Prisma.JsonNull,
})),
});
}
// Upsert postal code cache
const coords = apiResponse.centroid?.coordinates;
await postal_codes_service_1.postalCodesService.upsert({
postalCode: code,
city: apiResponse.city,
province: apiResponse.province,
centroidLat: coords ? coords[1] : null,
centroidLng: coords ? coords[0] : null,
});
}
catch (err) {
logger_1.logger.error('Failed to cache representatives', { postalCode: code, error: err });
}
};
// Don't await — fire and forget
cacheWrite();
// 4. Build response from API data
return {
source: 'api',
postalCode: code,
location: {
city: apiResponse.city ?? null,
province: apiResponse.province ?? null,
},
representatives: uniqueReps.map((rep) => ({
id: null,
postalCode: code,
name: rep.name || null,
email: rep.email || null,
districtName: rep.district_name || null,
electedOffice: rep.elected_office || null,
partyName: rep.party_name || null,
representativeSetName: rep.representative_set_name || null,
url: rep.url || null,
photoUrl: rep.photo_url || null,
offices: rep.offices ?? null,
cachedAt: new Date().toISOString(),
})),
};
},
async findAll(filters) {
const { page, limit, search, postalCode } = filters;
const skip = (page - 1) * limit;
const where = {};
if (postalCode) {
where.postalCode = postalCode;
}
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
{ districtName: { contains: search, mode: 'insensitive' } },
{ electedOffice: { contains: search, mode: 'insensitive' } },
];
}
const [representatives, total] = await Promise.all([
database_1.prisma.representative.findMany({
where,
skip,
take: limit,
orderBy: { cachedAt: 'desc' },
}),
database_1.prisma.representative.count({ where }),
]);
return {
representatives,
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
};
},
async findById(id) {
const rep = await database_1.prisma.representative.findUnique({ where: { id } });
if (!rep) {
throw new error_handler_1.AppError(404, 'Representative not found', 'REPRESENTATIVE_NOT_FOUND');
}
return rep;
},
async clearByPostalCode(code) {
const result = await database_1.prisma.representative.deleteMany({
where: { postalCode: code },
});
return { deleted: result.count, postalCode: code };
},
async deleteById(id) {
const existing = await database_1.prisma.representative.findUnique({ where: { id } });
if (!existing) {
throw new error_handler_1.AppError(404, 'Representative not found', 'REPRESENTATIVE_NOT_FOUND');
}
await database_1.prisma.representative.delete({ where: { id } });
},
async testApiConnection() {
return represent_api_client_1.representApiClient.testConnection();
},
async getCacheStats() {
const [totalReps, postalCodesWithReps, totalPostalCodes] = await Promise.all([
database_1.prisma.representative.count(),
database_1.prisma.representative.groupBy({ by: ['postalCode'] }).then((g) => g.length),
database_1.prisma.postalCodeCache.count(),
]);
return {
totalRepresentatives: totalReps,
postalCodesWithRepresentatives: postalCodesWithReps,
totalPostalCodes,
};
},
};
//# sourceMappingURL=representatives.service.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
declare const campaignPublicRouter: import("express-serve-static-core").Router;
declare const responsesPublicRouter: import("express-serve-static-core").Router;
declare const responsesAdminRouter: import("express-serve-static-core").Router;
export { campaignPublicRouter as responseCampaignPublicRouter, responsesPublicRouter, responsesAdminRouter };
//# sourceMappingURL=responses.routes.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"responses.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/responses/responses.routes.ts"],"names":[],"mappings":"AAkBA,QAAA,MAAM,oBAAoB,4CAAW,CAAC;AAiDtC,QAAA,MAAM,qBAAqB,4CAAW,CAAC;AA6EvC,QAAA,MAAM,oBAAoB,4CAAW,CAAC;AA6DtC,OAAO,EAAE,oBAAoB,IAAI,4BAA4B,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC"}

View File

@@ -0,0 +1,199 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.responsesAdminRouter = exports.responsesPublicRouter = exports.responseCampaignPublicRouter = void 0;
const express_1 = require("express");
const client_1 = require("@prisma/client");
const responses_service_1 = require("./responses.service");
const responses_schemas_1 = require("./responses.schemas");
const validate_1 = require("../../../middleware/validate");
const auth_middleware_1 = require("../../../middleware/auth.middleware");
const auth_middleware_2 = require("../../../middleware/auth.middleware");
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
const rate_limit_1 = require("../../../middleware/rate-limit");
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
// --- Campaign-scoped public routes (mount at /api/campaigns) ---
const campaignPublicRouter = (0, express_1.Router)();
exports.responseCampaignPublicRouter = campaignPublicRouter;
// GET /api/campaigns/:slug/responses
campaignPublicRouter.get('/:slug/responses', (0, validate_1.validate)(responses_schemas_1.listPublicResponsesSchema, 'query'), async (req, res, next) => {
try {
const slug = req.params.slug;
const result = await responses_service_1.responsesService.listApproved(slug, req.query);
res.json(result);
}
catch (err) {
next(err);
}
});
// GET /api/campaigns/:slug/response-stats
campaignPublicRouter.get('/:slug/response-stats', async (req, res, next) => {
try {
const slug = req.params.slug;
const stats = await responses_service_1.responsesService.getStats(slug);
res.json(stats);
}
catch (err) {
next(err);
}
});
// POST /api/campaigns/:slug/responses
campaignPublicRouter.post('/:slug/responses', rate_limit_1.responseRateLimit, (0, validate_1.validate)(responses_schemas_1.submitResponseSchema), async (req, res, next) => {
try {
const slug = req.params.slug;
const senderIp = req.ip || req.socket.remoteAddress;
const result = await responses_service_1.responsesService.submitResponse(slug, req.body, senderIp);
res.status(201).json(result);
}
catch (err) {
next(err);
}
});
// --- Response-scoped public routes (mount at /api/responses) ---
const responsesPublicRouter = (0, express_1.Router)();
exports.responsesPublicRouter = responsesPublicRouter;
// POST /api/responses/:id/upvote
responsesPublicRouter.post('/:id/upvote', auth_middleware_2.optionalAuth, async (req, res, next) => {
try {
const id = req.params.id;
const userIp = req.ip || req.socket.remoteAddress;
const userId = req.user?.id;
const result = await responses_service_1.responsesService.upvote(id, userIp, userId);
res.json(result);
}
catch (err) {
next(err);
}
});
// DELETE /api/responses/:id/upvote
responsesPublicRouter.delete('/:id/upvote', auth_middleware_2.optionalAuth, async (req, res, next) => {
try {
const id = req.params.id;
const userIp = req.ip || req.socket.remoteAddress;
const userId = req.user?.id;
const result = await responses_service_1.responsesService.removeUpvote(id, userIp, userId);
res.json(result);
}
catch (err) {
next(err);
}
});
// GET /api/responses/:id/verify/:token — returns HTML page
responsesPublicRouter.get('/:id/verify/:token', async (req, res, next) => {
try {
const id = req.params.id;
const token = req.params.token;
const result = await responses_service_1.responsesService.verify(id, token);
const html = result.success
? buildResultPage('Response Verified', `Thank you for verifying this response for the "${result.campaignTitle}" campaign. The response has been approved and will now appear on the public response wall.`, '#16a34a')
: buildResultPage('Verification Failed', result.reason || 'Unable to verify this response.', '#dc2626');
res.type('html').send(html);
}
catch (err) {
next(err);
}
});
// GET /api/responses/:id/report/:token — returns HTML page
responsesPublicRouter.get('/:id/report/:token', async (req, res, next) => {
try {
const id = req.params.id;
const token = req.params.token;
const result = await responses_service_1.responsesService.report(id, token);
const html = result.success
? buildResultPage('Response Reported', `This response for the "${result.campaignTitle}" campaign has been flagged as invalid and removed from the public response wall. Thank you for letting us know.`, '#dc2626')
: buildResultPage('Report Failed', result.reason || 'Unable to process this report.', '#dc2626');
res.type('html').send(html);
}
catch (err) {
next(err);
}
});
// --- Admin routes (mount at /api/responses) ---
const responsesAdminRouter = (0, express_1.Router)();
exports.responsesAdminRouter = responsesAdminRouter;
responsesAdminRouter.use(auth_middleware_1.authenticate);
responsesAdminRouter.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
// GET /api/responses
responsesAdminRouter.get('/', (0, validate_1.validate)(responses_schemas_1.listAdminResponsesSchema, 'query'), async (req, res, next) => {
try {
const result = await responses_service_1.responsesService.findAll(req.query);
res.json(result);
}
catch (err) {
next(err);
}
});
// PATCH /api/responses/:id/status
responsesAdminRouter.patch('/:id/status', (0, validate_1.validate)(responses_schemas_1.updateResponseStatusSchema), async (req, res, next) => {
try {
const id = req.params.id;
const result = await responses_service_1.responsesService.updateStatus(id, req.body);
res.json(result);
}
catch (err) {
next(err);
}
});
// POST /api/responses/:id/resend-verification
responsesAdminRouter.post('/:id/resend-verification', async (req, res, next) => {
try {
const id = req.params.id;
const result = await responses_service_1.responsesService.resendVerification(id);
res.json(result);
}
catch (err) {
next(err);
}
});
// DELETE /api/responses/:id
responsesAdminRouter.delete('/:id', async (req, res, next) => {
try {
const id = req.params.id;
await responses_service_1.responsesService.deleteResponse(id);
res.status(204).send();
}
catch (err) {
next(err);
}
});
// --- HTML page builder for verify/report endpoints ---
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}
function buildResultPage(title, message, accentColor) {
const escapedTitle = escapeHtml(title);
const escapedMessage = escapeHtml(message);
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapedTitle} - Changemaker Lite</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 40px 20px; background: #f8fafc; color: #334155; }
.container { max-width: 500px; margin: 0 auto; text-align: center; }
.card { background: white; border-radius: 12px; padding: 40px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.icon { font-size: 48px; margin-bottom: 16px; }
h1 { color: ${accentColor}; font-size: 24px; margin: 0 0 16px; }
p { font-size: 16px; line-height: 1.6; color: #64748b; margin: 0; }
.brand { margin-top: 32px; font-size: 14px; color: #94a3b8; }
.brand strong { color: #2563eb; }
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="icon">${accentColor === '#16a34a' ? '&#10003;' : '&#10007;'}</div>
<h1>${escapedTitle}</h1>
<p>${escapedMessage}</p>
</div>
<div class="brand">Powered by <strong>Changemaker Lite</strong></div>
</div>
</body>
</html>`;
}
//# sourceMappingURL=responses.routes.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,110 @@
import { z } from 'zod';
export declare const submitResponseSchema: z.ZodObject<{
representativeName: z.ZodString;
representativeLevel: z.ZodNativeEnum<{
FEDERAL: "FEDERAL";
PROVINCIAL: "PROVINCIAL";
MUNICIPAL: "MUNICIPAL";
SCHOOL_BOARD: "SCHOOL_BOARD";
}>;
responseType: z.ZodNativeEnum<{
EMAIL: "EMAIL";
LETTER: "LETTER";
PHONE_CALL: "PHONE_CALL";
MEETING: "MEETING";
SOCIAL_MEDIA: "SOCIAL_MEDIA";
OTHER: "OTHER";
}>;
responseText: z.ZodString;
representativeTitle: z.ZodOptional<z.ZodString>;
representativeEmail: z.ZodOptional<z.ZodString>;
userComment: z.ZodOptional<z.ZodString>;
submittedByName: z.ZodOptional<z.ZodString>;
submittedByEmail: z.ZodOptional<z.ZodString>;
isAnonymous: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
sendVerification: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
}, "strip", z.ZodTypeAny, {
representativeName: string;
representativeLevel: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD";
responseType: "EMAIL" | "LETTER" | "PHONE_CALL" | "MEETING" | "SOCIAL_MEDIA" | "OTHER";
responseText: string;
isAnonymous: boolean;
sendVerification: boolean;
representativeTitle?: string | undefined;
representativeEmail?: string | undefined;
userComment?: string | undefined;
submittedByName?: string | undefined;
submittedByEmail?: string | undefined;
}, {
representativeName: string;
representativeLevel: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD";
responseType: "EMAIL" | "LETTER" | "PHONE_CALL" | "MEETING" | "SOCIAL_MEDIA" | "OTHER";
responseText: string;
representativeTitle?: string | undefined;
representativeEmail?: string | undefined;
userComment?: string | undefined;
submittedByName?: string | undefined;
submittedByEmail?: string | undefined;
isAnonymous?: boolean | undefined;
sendVerification?: boolean | undefined;
}>;
export declare const listPublicResponsesSchema: z.ZodObject<{
page: z.ZodDefault<z.ZodNumber>;
limit: z.ZodDefault<z.ZodNumber>;
sort: z.ZodDefault<z.ZodOptional<z.ZodEnum<["recent", "upvotes", "verified"]>>>;
level: z.ZodOptional<z.ZodNativeEnum<{
FEDERAL: "FEDERAL";
PROVINCIAL: "PROVINCIAL";
MUNICIPAL: "MUNICIPAL";
SCHOOL_BOARD: "SCHOOL_BOARD";
}>>;
}, "strip", z.ZodTypeAny, {
sort: "upvotes" | "recent" | "verified";
limit: number;
page: number;
level?: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD" | undefined;
}, {
sort?: "upvotes" | "recent" | "verified" | undefined;
level?: "FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD" | undefined;
limit?: number | undefined;
page?: number | undefined;
}>;
export declare const listAdminResponsesSchema: z.ZodObject<{
page: z.ZodDefault<z.ZodNumber>;
limit: z.ZodDefault<z.ZodNumber>;
status: z.ZodOptional<z.ZodNativeEnum<{
PENDING: "PENDING";
APPROVED: "APPROVED";
REJECTED: "REJECTED";
}>>;
campaignId: z.ZodOptional<z.ZodString>;
search: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
limit: number;
page: number;
status?: "PENDING" | "APPROVED" | "REJECTED" | undefined;
search?: string | undefined;
campaignId?: string | undefined;
}, {
status?: "PENDING" | "APPROVED" | "REJECTED" | undefined;
search?: string | undefined;
limit?: number | undefined;
page?: number | undefined;
campaignId?: string | undefined;
}>;
export declare const updateResponseStatusSchema: z.ZodObject<{
status: z.ZodNativeEnum<{
PENDING: "PENDING";
APPROVED: "APPROVED";
REJECTED: "REJECTED";
}>;
}, "strip", z.ZodTypeAny, {
status: "PENDING" | "APPROVED" | "REJECTED";
}, {
status: "PENDING" | "APPROVED" | "REJECTED";
}>;
export type SubmitResponseInput = z.infer<typeof submitResponseSchema>;
export type ListPublicResponsesInput = z.infer<typeof listPublicResponsesSchema>;
export type ListAdminResponsesInput = z.infer<typeof listAdminResponsesSchema>;
export type UpdateResponseStatusInput = z.infer<typeof updateResponseStatusSchema>;
//# sourceMappingURL=responses.schemas.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"responses.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/responses/responses.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAY/B,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;EAKpC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;EAMnC,CAAC;AAEH,eAAO,MAAM,0BAA0B;;;;;;;;;;EAErC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AACjF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC/E,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC"}

View File

@@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateResponseStatusSchema = exports.listAdminResponsesSchema = exports.listPublicResponsesSchema = exports.submitResponseSchema = void 0;
const zod_1 = require("zod");
const client_1 = require("@prisma/client");
exports.submitResponseSchema = zod_1.z.object({
representativeName: zod_1.z.string().min(1, 'Representative name is required'),
representativeLevel: zod_1.z.nativeEnum(client_1.GovernmentLevel),
responseType: zod_1.z.nativeEnum(client_1.ResponseType),
responseText: zod_1.z.string().min(1, 'Response text is required'),
representativeTitle: zod_1.z.string().optional(),
representativeEmail: zod_1.z.string().email().optional(),
userComment: zod_1.z.string().optional(),
submittedByName: zod_1.z.string().optional(),
submittedByEmail: zod_1.z.string().email().optional(),
isAnonymous: zod_1.z.boolean().optional().default(false),
sendVerification: zod_1.z.boolean().optional().default(false),
});
exports.listPublicResponsesSchema = zod_1.z.object({
page: zod_1.z.coerce.number().int().positive().default(1),
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
sort: zod_1.z.enum(['recent', 'upvotes', 'verified']).optional().default('recent'),
level: zod_1.z.nativeEnum(client_1.GovernmentLevel).optional(),
});
exports.listAdminResponsesSchema = zod_1.z.object({
page: zod_1.z.coerce.number().int().positive().default(1),
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
status: zod_1.z.nativeEnum(client_1.ResponseStatus).optional(),
campaignId: zod_1.z.string().optional(),
search: zod_1.z.string().optional(),
});
exports.updateResponseStatusSchema = zod_1.z.object({
status: zod_1.z.nativeEnum(client_1.ResponseStatus),
});
//# sourceMappingURL=responses.schemas.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"responses.schemas.js","sourceRoot":"","sources":["../../../../src/modules/influence/responses/responses.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,2CAA+E;AAElE,QAAA,oBAAoB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3C,kBAAkB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,iCAAiC,CAAC;IACxE,mBAAmB,EAAE,OAAC,CAAC,UAAU,CAAC,wBAAe,CAAC;IAClD,YAAY,EAAE,OAAC,CAAC,UAAU,CAAC,qBAAY,CAAC;IACxC,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,2BAA2B,CAAC;IAC5D,mBAAmB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC1C,mBAAmB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IAClD,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,eAAe,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACtC,gBAAgB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IAC/C,WAAW,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAClD,gBAAgB,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CACxD,CAAC,CAAC;AAEU,QAAA,yBAAyB,GAAG,OAAC,CAAC,MAAM,CAAC;IAChD,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,IAAI,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;IAC5E,KAAK,EAAE,OAAC,CAAC,UAAU,CAAC,wBAAe,CAAC,CAAC,QAAQ,EAAE;CAChD,CAAC,CAAC;AAEU,QAAA,wBAAwB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC/C,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,MAAM,EAAE,OAAC,CAAC,UAAU,CAAC,uBAAc,CAAC,CAAC,QAAQ,EAAE;IAC/C,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAEU,QAAA,0BAA0B,GAAG,OAAC,CAAC,MAAM,CAAC;IACjD,MAAM,EAAE,OAAC,CAAC,UAAU,CAAC,uBAAc,CAAC;CACrC,CAAC,CAAC"}

View File

@@ -0,0 +1,129 @@
import type { SubmitResponseInput, ListPublicResponsesInput, ListAdminResponsesInput, UpdateResponseStatusInput } from './responses.schemas';
export declare const responsesService: {
submitResponse(slug: string, data: SubmitResponseInput, senderIp?: string): Promise<{
id: string;
status: import(".prisma/client").$Enums.ResponseStatus;
verificationSent: boolean;
}>;
listApproved(slug: string, filters: ListPublicResponsesInput): Promise<{
responses: {
id: string;
createdAt: Date;
upvoteCount: number;
representativeName: string;
representativeTitle: string | null;
representativeLevel: import(".prisma/client").$Enums.GovernmentLevel;
responseType: import(".prisma/client").$Enums.ResponseType;
responseText: string;
userComment: string | null;
submittedByName: string | null;
isAnonymous: boolean;
isVerified: boolean;
verifiedAt: Date | null;
}[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}>;
getStats(slug: string): Promise<{
total: number;
verified: number;
totalUpvotes: number;
byLevel: Record<string, number>;
}>;
upvote(responseId: string, userIp?: string, userId?: string): Promise<{
success: boolean;
alreadyUpvoted?: undefined;
} | {
success: boolean;
alreadyUpvoted: boolean;
}>;
removeUpvote(responseId: string, userIp?: string, userId?: string): Promise<{
success: boolean;
}>;
verify(responseId: string, token: string): Promise<{
success: boolean;
reason: string;
campaignTitle?: undefined;
} | {
success: boolean;
campaignTitle: string;
reason?: undefined;
}>;
report(responseId: string, token: string): Promise<{
success: boolean;
reason: string;
campaignTitle?: undefined;
} | {
success: boolean;
campaignTitle: string;
reason?: undefined;
}>;
findAll(filters: ListAdminResponsesInput): Promise<{
responses: {
status: import(".prisma/client").$Enums.ResponseStatus;
id: string;
createdAt: Date;
upvoteCount: number;
campaign: {
id: string;
title: string;
slug: string;
};
representativeName: string;
representativeTitle: string | null;
representativeLevel: import(".prisma/client").$Enums.GovernmentLevel;
representativeEmail: string | null;
responseType: import(".prisma/client").$Enums.ResponseType;
responseText: string;
userComment: string | null;
submittedByName: string | null;
submittedByEmail: string | null;
isAnonymous: boolean;
isVerified: boolean;
verifiedAt: Date | null;
verifiedBy: string | null;
}[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}>;
updateStatus(id: string, data: UpdateResponseStatusInput): Promise<{
status: import(".prisma/client").$Enums.ResponseStatus;
id: string;
createdAt: Date;
updatedAt: Date;
upvoteCount: number;
campaignSlug: string;
campaignId: string;
representativeName: string;
representativeTitle: string | null;
representativeLevel: import(".prisma/client").$Enums.GovernmentLevel;
representativeEmail: string | null;
responseType: import(".prisma/client").$Enums.ResponseType;
responseText: string;
userComment: string | null;
screenshotUrl: string | null;
submittedByName: string | null;
submittedByEmail: string | null;
isAnonymous: boolean;
isVerified: boolean;
verificationToken: string | null;
verificationSentAt: Date | null;
verifiedAt: Date | null;
verifiedBy: string | null;
submittedIp: string | null;
submittedByUserId: string | null;
}>;
deleteResponse(id: string): Promise<void>;
resendVerification(id: string): Promise<{
success: boolean;
}>;
};
//# sourceMappingURL=responses.service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"responses.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/responses/responses.service.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,yBAAyB,EAC1B,MAAM,qBAAqB,CAAC;AAI7B,eAAO,MAAM,gBAAgB;yBAGA,MAAM,QAAQ,mBAAmB,aAAa,MAAM;;;;;uBAmEtD,MAAM,WAAW,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;mBA0D7C,MAAM;;;;;;uBA2BF,MAAM,WAAW,MAAM,WAAW,MAAM;;;;;;;6BAgClC,MAAM,WAAW,MAAM,WAAW,MAAM;;;uBAwB9C,MAAM,SAAS,MAAM;;;;;;;;;uBAqCrB,MAAM,SAAS,MAAM;;;;;;;;;qBA6BvB,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAwDvB,MAAM,QAAQ,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAUrC,MAAM;2BAOF,MAAM;;;CA6CpC,CAAC"}

View File

@@ -0,0 +1,370 @@
"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

File diff suppressed because one or more lines are too long