240 lines
6.5 KiB
Markdown

# 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