Description
Add a robust moderation workflow for campaigns: automatic suspension triggers (low verification, fraud reports, policy violations), admin suspend/reinstate actions, owner appeals submission/review, suspension reason logging, and user messaging.
Background
We currently handle problematic campaigns ad-hoc. This feature enables consistent containment of harm, an auditable trail, and a clear appeals process.
Scope
- In-scope: DB schema + Prisma migration, backend APIs (owner + admin), background worker(s), audit records, notifications, tests, migration & rollout plan.
- Out-of-scope: Admin frontend UI (API contract provided).
Goals
- Auto-suspend campaigns that meet configured risk rules.
- Allow admins to suspend/reinstate with recorded reasons.
- Allow campaign owners to appeal; allow admins to resolve appeals.
- Persist immutable audit trail for suspensions and appeals.
- Notify owners (and donors where relevant) with clear next steps.
Acceptance Criteria
Functional requirements
- Auto-suspension triggers:
- Low verification score below threshold X for Y days.
- N independent fraud reports within a time window.
- Policy violation flags (automated/manual).
- Admin actions:
- Suspend: immediate suspension with reason and optional evidence.
- Reinstate: reinstate with notes (stored).
- Appeals:
- Owners submit appeal while suspended.
- Admin reviews appeal and approves/denies.
- Appeal history linked to the suspension.
- Suspension metadata:
- Record reason code (enum), reason text, actor (system/admin id), source (
AUTO|ADMIN), evidence links, timestamp.
- Messaging:
- Owner notifications + websocket events; optional donor notifications for fraud suspensions.
Non-functional requirements
- Configurable thresholds via environment/config.
- Idempotent suspend operations.
- Efficient batched background evaluations; near-real-time handling for critical thresholds.
- Admin-only access for review endpoints.
- Secure storage/serving of evidence; access-controlled.
Data model (Prisma + DB)
- Add
status enum on Campaign (e.g., ACTIVE, SUSPENDED, PENDING_REVIEW, DELETED).
- Campaign fields to add:
status, suspendedAt (timestamp), suspensionMetadata (json), suspensionReasonId (FK optional).
- New tables:
suspensions:
- id, campaignId, actorId (nullable for system), source (
AUTO|ADMIN), reasonCode, reasonText, evidence (json), createdAt
appeals:
- id, suspensionId, campaignOwnerId, message, status (
OPEN|UNDER_REVIEW|APPROVED|DENIED), adminNotes, createdAt, updatedAt
fraud_reports:
- id, campaignId, reporterId (nullable), type, details, createdAt
- Files to update: schema.prisma, relevant model seed scripts.
API contract (server-side)
- Public/owner:
GET /campaigns/:id — include status, suspensionSummary, canAppeal
POST /campaigns/:id/appeals — Body: { message, attachments?: string[] }
GET /campaigns/:id/appeals — owner view
POST /campaigns/:id/reports — Body: { type, details }
- Admin:
POST /admin/campaigns/:id/suspend — Body: { reasonCode, reasonText?, evidence?: string[] }
POST /admin/campaigns/:id/reinstate — Body: { adminNotes? }
GET /admin/campaigns/:id/suspensions — list suspensions/appeals
GET /admin/appeals — list+filter appeals
POST /admin/appeals/:id/resolve — Body: { decision: 'APPROVE'|'DENY', adminNotes? }
Suggested implementation files:
- Controllers/routes: campaign.controller.ts or
src/controllers/moderation.controller.ts; update campaign.routes.ts.
- Services: extend
src/services/* (e.g., notification.service.ts, campaign.service.ts).
- Worker:
src/workers/moderation.worker.ts (or extend existing workers).
Background worker / strategy
- Periodic job (daily/cron) to evaluate rules in batch.
- Event-driven: evaluate immediate thresholds on new
fraud_reports.
- Steps:
- Query candidate campaigns
- Run rule checks
- Create
suspensions record and update Campaign.status → SUSPENDED
- Trigger notifications
Logging & audit
- Every status change must write to
suspensions (immutable).
- Log events with logger.ts.
- Do not delete
suspensions or appeals; append-only admin notes only.
Notifications
- Owner email and websocket event via notification.service.ts and
websocket/socket.server.ts.
- Donor notifications (configurable) for fraud-related suspensions.
- Message template includes reason summary, evidence summary, appeal instructions, expected review timeframe.
Testing
- Unit tests: service logic, rule evaluation, controller auth checks (follow existing
*.test.ts patterns).
- Integration tests: end-to-end flow (auto-suspend → appeal → admin resolve).
- Edge cases: concurrent reports, repeated suspend attempts, appeal race conditions.
Migration & rollout
- Add Prisma schema changes and migration.
- Deploy migration to staging; run script to set
Campaign.status = 'ACTIVE' when absent.
- Deploy backend and worker behind feature flag.
- Gradually enable auto-suspension rules; monitor metrics/logs.
Commands:
npx prisma migrate dev --name add_campaign_suspensions
node prisma/seed.js
Security & privacy
- Admin-only endpoints require admin role checks.
- Evidence storage must be access-controlled; treat as sensitive data.
- Rate-limit report submissions.
Implementation notes & suggestions
- Reuse existing services:
notification.service.ts, redis.ts for counters, logger.ts.
- Centralize suspension reason enum in index.ts.
- Feature flag env var for toggling auto-suspension.
- Store thresholds in config (index.ts or environment vars).
Checklist (GitHub task list)
Description
Add a robust moderation workflow for campaigns: automatic suspension triggers (low verification, fraud reports, policy violations), admin suspend/reinstate actions, owner appeals submission/review, suspension reason logging, and user messaging.
Background
We currently handle problematic campaigns ad-hoc. This feature enables consistent containment of harm, an auditable trail, and a clear appeals process.
Scope
Goals
Acceptance Criteria
suspensionsrecord is created.suspensions.GET /campaigns/:idreturnsstatus,suspensionSummary, andcanAppeal.ACTIVEby default.Functional requirements
AUTO|ADMIN), evidence links, timestamp.Non-functional requirements
Data model (Prisma + DB)
statusenum onCampaign(e.g.,ACTIVE,SUSPENDED,PENDING_REVIEW,DELETED).status,suspendedAt(timestamp),suspensionMetadata(json),suspensionReasonId(FK optional).suspensions:AUTO|ADMIN), reasonCode, reasonText, evidence (json), createdAtappeals:OPEN|UNDER_REVIEW|APPROVED|DENIED), adminNotes, createdAt, updatedAtfraud_reports:API contract (server-side)
GET /campaigns/:id— includestatus,suspensionSummary,canAppealPOST /campaigns/:id/appeals— Body:{ message, attachments?: string[] }GET /campaigns/:id/appeals— owner viewPOST /campaigns/:id/reports— Body:{ type, details }POST /admin/campaigns/:id/suspend— Body:{ reasonCode, reasonText?, evidence?: string[] }POST /admin/campaigns/:id/reinstate— Body:{ adminNotes? }GET /admin/campaigns/:id/suspensions— list suspensions/appealsGET /admin/appeals— list+filter appealsPOST /admin/appeals/:id/resolve— Body:{ decision: 'APPROVE'|'DENY', adminNotes? }Suggested implementation files:
src/controllers/moderation.controller.ts; update campaign.routes.ts.src/services/*(e.g.,notification.service.ts,campaign.service.ts).src/workers/moderation.worker.ts(or extend existing workers).Background worker / strategy
fraud_reports.suspensionsrecord and updateCampaign.status→SUSPENDEDLogging & audit
suspensions(immutable).suspensionsorappeals; append-only admin notes only.Notifications
websocket/socket.server.ts.Testing
*.test.tspatterns).Migration & rollout
Campaign.status = 'ACTIVE'when absent.Commands:
Security & privacy
Implementation notes & suggestions
notification.service.ts,redis.tsfor counters,logger.ts.Checklist (GitHub task list)
suspensions,appeals,fraud_reportsmodelsCampaign.statusand populate existing campaignssrc/workers/moderation.worker.ts)POST /campaigns/:id/reports)