240 lines
6.5 KiB
Markdown
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
|