# Canvassing Models ## Overview The Canvassing module provides GPS-tracked volunteer canvassing with session management, visit recording, walking route algorithms, and automatic session abandonment. **Models (4):** - CanvassSession — Session lifecycle (ACTIVE → COMPLETED/ABANDONED) - CanvassVisit — Visit recording with 7 outcome types - TrackingSession — GPS tracking integration - TrackPoint — GPS breadcrumb trail **Key Features:** - Session lifecycle management (ACTIVE → COMPLETED/ABANDONED) - 7 visit outcomes (NOT_HOME, REFUSED, MOVED, ALREADY_VOTED, SPOKE_WITH, LEFT_LITERATURE, COME_BACK_LATER) - Walking route algorithm (nearest-neighbor with haversine distance) - GPS breadcrumb trail with event markers - Support level tracking (1-4) - Sign request tracking - Session abandonment (12h timeout, auto-ABANDONED status) - Distance calculation (meters) See [Schema Reference](../schema.md#canvassing) for complete field listings. --- ## Session Lifecycle ```mermaid stateDiagram-v2 [*] --> ACTIVE : Start session ACTIVE --> COMPLETED : End session (user action) ACTIVE --> ABANDONED : 12h timeout (cron) COMPLETED --> [*] ABANDONED --> [*] ``` **Status:** `CanvassSessionStatus` - `ACTIVE` — Session in progress - `COMPLETED` — Session ended by user - `ABANDONED` — Session inactive > 12h (auto-expired by cron) --- ## Visit Outcomes ```prisma enum VisitOutcome { NOT_HOME // No one home REFUSED // Refused to talk MOVED // Resident moved away ALREADY_VOTED // Already voted (early voting) SPOKE_WITH // Successful conversation LEFT_LITERATURE // Left campaign literature COME_BACK_LATER // Asked to come back later } ``` **Support Level Mapping:** - Outcome: `SPOKE_WITH` → Record support level (1-4) - Outcome: `REFUSED` → Support level defaults to `null` or `1` - Outcome: `NOT_HOME` → No support level --- ## Walking Route Algorithm **Algorithm:** Nearest-neighbor with haversine distance calculation **Steps:** 1. Get all unvisited addresses in cut 2. Start from session start coordinates (or cut centroid) 3. Find nearest unvisited address (haversine distance) 4. Add to route, mark as visited 5. Repeat from new position until all addresses visited **Implementation:** `api/src/modules/map/canvass/walking-route.service.ts` ```typescript function calculateWalkingRoute( addresses: Address[], startLat: number, startLng: number, visitedAddressIds: string[] ): WalkingRoute { const unvisited = addresses.filter(a => !visitedAddressIds.includes(a.id)); const route: Address[] = []; let currentLat = startLat; let currentLng = startLng; while (unvisited.length > 0) { // Find nearest unvisited address const nearest = findNearestAddress(currentLat, currentLng, unvisited); route.push(nearest); currentLat = nearest.location.latitude; currentLng = nearest.location.longitude; unvisited.splice(unvisited.indexOf(nearest), 1); } return { addresses: route, totalDistanceM: calculateTotalDistance(route), }; } ``` --- ## GPS Tracking **TrackingSession** = One-to-one with CanvassSession - Stores total points, distance, last position - `isActive` flag for active tracking **TrackPoint** = GPS breadcrumb - Latitude, longitude, accuracy - Event type markers (LOCATION_ADDED, VISIT_RECORDED, SESSION_STARTED, SESSION_ENDED) **Event Flow:** ```mermaid sequenceDiagram participant Volunteer participant API participant GPS Volunteer->>API: POST /api/canvass/sessions (start session) API-->>Volunteer: sessionId loop Every 30 seconds GPS->>API: POST /api/tracking/:sessionId/points (lat, lng) API-->>GPS: 200 OK end Volunteer->>API: POST /api/canvass/visits (record visit) API->>GPS: POST /api/tracking/:sessionId/points (eventType: VISIT_RECORDED) Volunteer->>API: POST /api/canvass/sessions/:id/end API->>GPS: POST /api/tracking/:sessionId/points (eventType: SESSION_ENDED) API-->>Volunteer: session (status: COMPLETED) ``` --- ## Session Abandonment **Cron Job:** Runs hourly via `api/src/server.ts` startup + interval ```typescript async function abandonStaleSessions() { const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000); await prisma.canvassSession.updateMany({ where: { status: CanvassSessionStatus.ACTIVE, startedAt: { lt: twelveHoursAgo }, }, data: { status: CanvassSessionStatus.ABANDONED, endedAt: new Date(), }, }); } ``` **Trigger Conditions:** - Status = `ACTIVE` - StartedAt < 12 hours ago - No explicit end by user --- ## Common Queries ### Start Canvass Session ```typescript const session = await prisma.canvassSession.create({ data: { userId: user.id, cutId: cut.id, shiftId: shift?.id, status: CanvassSessionStatus.ACTIVE, startLatitude: startLat, startLongitude: startLng, trackingSession: { create: { userId: user.id, isActive: true, }, }, }, }); ``` ### Record Visit ```typescript const visit = await prisma.canvassVisit.create({ data: { addressId: address.id, userId: user.id, sessionId: session.id, shiftId: shift?.id, outcome: VisitOutcome.SPOKE_WITH, supportLevel: SupportLevel.LEVEL_4, signRequested: true, signSize: 'Large', notes: 'Very supportive, wants to volunteer', durationSeconds: 180, }, }); // Update address support level await prisma.address.update({ where: { id: address.id }, data: { supportLevel: SupportLevel.LEVEL_4, sign: true, signSize: 'Large', notes: 'Very supportive, wants to volunteer', updatedByUserId: user.id, }, }); ``` ### End Session ```typescript await prisma.canvassSession.update({ where: { id: sessionId }, data: { status: CanvassSessionStatus.COMPLETED, endedAt: new Date(), trackingSession: { update: { isActive: false, endedAt: new Date(), }, }, }, }); ``` ### Get Walking Route ```typescript const session = await prisma.canvassSession.findUnique({ where: { id: sessionId }, include: { visits: { include: { address: true } }, }, }); const visitedAddressIds = session.visits.map(v => v.addressId); const addresses = await prisma.address.findMany({ where: { location: { // Point-in-polygon check for cut latitude: { gte: cutBounds.south, lte: cutBounds.north }, longitude: { gte: cutBounds.west, lte: cutBounds.east }, }, }, include: { location: true }, }); const route = calculateWalkingRoute( addresses, session.startLatitude, session.startLongitude, visitedAddressIds ); ``` --- ## Related Documentation - [Schema Reference](../schema.md#canvassing) — Complete field listings - [Database Overview](../index.md) — ER diagram - [API Canvass Routes](../../api/canvass.md) — REST endpoints - [Volunteer Canvass Map](../../admin/volunteer-map.md) — Full-screen canvass UI - [Admin Canvass Dashboard](../../admin/canvass-dashboard.md) — Admin oversight UI