# Influence Models ## Overview The Influence module provides advocacy campaign management with multi-government-level targeting, email/call tracking, response wall with moderation, and representative caching. **Models (10):** - Campaign — Advocacy campaigns with 12 feature flags - Representative — Cached rep data from Represent API - CampaignEmail — Email tracking (SMTP vs MAILTO) - RepresentativeResponse — Response wall submissions - ResponseUpvote — Upvote tracking with deduplication - CustomRecipient — Custom email targets - PostalCodeCache — Postal code geocoding cache - EmailLog — Email audit trail - EmailVerification — Email verification tokens - Call — Phone call tracking **Key Features:** - Multi-government-level targeting (Federal, Provincial, Municipal, School Board) - Dual email methods: SMTP (async BullMQ queue) + mailto: links - Response moderation workflow (PENDING → APPROVED/REJECTED) - Email verification for response wall submissions - Upvote deduplication (user ID + IP address) - Represent API integration for Canadian representatives - Postal code → representative lookup See [Schema Reference](../schema.md#influence) for complete field listings. --- ## Campaign Feature Flags (12 total) | Flag | Default | Description | |------|---------|-------------| | allowSmtpEmail | true | Enable SMTP email sending via BullMQ | | allowMailtoLink | true | Enable mailto: links for client-side email | | collectUserInfo | true | Collect sender name/email/postal code | | showEmailCount | true | Display email sent count on public page | | showCallCount | true | Display call made count on public page | | allowEmailEditing | false | Allow users to edit email subject/body | | allowCustomRecipients | false | Enable custom recipient management | | showResponseWall | false | Enable public response wall | | highlightCampaign | false | Highlight on campaigns list page | --- ## Government Level Targeting ```prisma enum GovernmentLevel { FEDERAL PROVINCIAL MUNICIPAL SCHOOL_BOARD } ``` Campaigns can target multiple levels: ```typescript const campaign = await prisma.campaign.create({ data: { title: 'Support Climate Action', targetGovernmentLevels: [GovernmentLevel.FEDERAL, GovernmentLevel.PROVINCIAL], // ... }, }); ``` Representative lookup filters by targeted levels: ```typescript const reps = await representativeService.lookup(postalCode, campaign.targetGovernmentLevels); ``` --- ## Email Methods ### SMTP (Async Queue) - Queued via BullMQ (Redis backend) - Worker sends via Nodemailer - Supports templates with variable interpolation - Tracks delivery status (QUEUED → SENT/FAILED) - Rate limiting (10 emails/min per IP) ### MAILTO (Client-Side) - Generates mailto: link with pre-filled subject/body - Tracked when link clicked (status: CLICKED) - No server-side email sending - User's default email client used --- ## Response Moderation Workflow ```mermaid stateDiagram-v2 [*] --> PENDING : Submit response PENDING --> APPROVED : Admin approves PENDING --> REJECTED : Admin rejects APPROVED --> [*] REJECTED --> [*] ``` **Status:** `PENDING` (default) → `APPROVED` | `REJECTED` Admin moderation via `/app/influence/responses`: - Filter by status, campaign, date range - Bulk approve/reject - View submitter details - Screenshot attachments --- ## Upvote Deduplication Two unique constraints prevent duplicate upvotes: ```prisma model ResponseUpvote { @@unique([responseId, userId]) // Logged-in users @@unique([responseId, upvotedIp]) // Guest users } ``` **Logic:** - Logged-in user: Check `[responseId, userId]` - Guest user: Check `[responseId, upvotedIp]` - Database-level enforcement (no race conditions) --- ## Represent API Integration **Representative Cache:** - Cached in `representatives` table - TTL: 30 days (check `cachedAt` field) - Re-fetched if cache miss or stale **Lookup Flow:** ```mermaid sequenceDiagram participant Client participant API participant Cache participant Represent Client->>API: GET /api/representatives/lookup?postalCode=K1A0B1 API->>Cache: findMany({ where: { postalCode } }) alt Cache hit (cachedAt < 30 days ago) Cache-->>API: representatives[] API-->>Client: representatives[] else Cache miss or stale API->>Represent: GET /representatives/?point=K1A0B1 Represent-->>API: representatives[] API->>Cache: upsert({ postalCode, ... }) API-->>Client: representatives[] end ``` --- ## Common Queries ### Create Campaign ```typescript const campaign = await prisma.campaign.create({ data: { slug: 'climate-action', title: 'Support Climate Action Bill C-12', emailSubject: 'Support Climate Action', emailBody: 'I urge you to support...', status: CampaignStatus.ACTIVE, targetGovernmentLevels: [GovernmentLevel.FEDERAL], allowSmtpEmail: true, showResponseWall: true, createdByUserId: user.id, }, }); ``` ### Queue Campaign Email (SMTP) ```typescript await emailQueueService.addCampaignEmail({ campaignId: campaign.id, recipientEmail: 'rep@example.com', recipientName: 'Hon. Jane Smith', subject: 'Support Climate Action', message: 'I urge you to...', userEmail: 'voter@example.com', userName: 'John Voter', userPostalCode: 'K1A0B1', }); ``` ### Submit Response ```typescript const response = await prisma.representativeResponse.create({ data: { campaignId: campaign.id, campaignSlug: campaign.slug, representativeName: 'Hon. Jane Smith', representativeLevel: GovernmentLevel.FEDERAL, responseType: ResponseType.EMAIL, responseText: 'Thank you for your letter...', submittedByUserId: user.id, submittedByEmail: 'voter@example.com', status: ResponseStatus.PENDING, }, }); ``` ### Upvote Response ```typescript await prisma.responseUpvote.create({ data: { responseId: response.id, userId: user?.id, // Null for guests userEmail: user?.email, upvotedIp: req.ip, }, }); // Increment upvote count (denormalized) await prisma.representativeResponse.update({ where: { id: response.id }, data: { upvoteCount: { increment: 1 } }, }); ``` --- ## Related Documentation - [Schema Reference](../schema.md#influence) — Complete field listings - [Database Overview](../index.md) — ER diagram - [API Influence Routes](../../api/influence.md) — REST endpoints - [Admin Campaigns Page](../../admin/campaigns.md) — Campaign management UI - [Public Campaign Page](../../admin/public-campaign.md) — Public-facing campaign UI