extend poll-checker to link polls to legislation, send vote comparison notifications

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 17:04:23 +01:00
parent f1126a4a8b
commit 2a81893683

View File

@@ -1,5 +1,6 @@
import { and, eq, inArray } from "drizzle-orm"
import { and, eq, inArray, isNull } from "drizzle-orm"
import { db } from "../db/client"
import { legislationTexts, userVotes } from "../db/schema/legislation"
import {
deviceFollows,
politicianMandates,
@@ -21,6 +22,12 @@ const VOTE_LABELS: Record<string, string> = {
no_show: "Nicht abgestimmt",
}
const USER_VOTE_LABELS: Record<string, string> = {
ja: "Ja",
nein: "Nein",
enthaltung: "Enthaltung",
}
const MANDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
async function resolveMandateId(politicianId: number): Promise<number | null> {
@@ -56,6 +63,131 @@ async function resolveMandateId(politicianId: number): Promise<number | null> {
return mandateId
}
async function linkPollToLegislation(pollId: number, pollLabel: string) {
const unlinked = await db
.select()
.from(legislationTexts)
.where(isNull(legislationTexts.pollId))
for (const leg of unlinked) {
const legWords = leg.title
.toLowerCase()
.split(/\s+/)
.filter((w) => w.length > 4)
const pollLower = pollLabel.toLowerCase()
const matchCount = legWords.filter((w) => pollLower.includes(w)).length
if (
matchCount >= 2 ||
(legWords.length > 0 && matchCount / legWords.length > 0.5)
) {
await db
.update(legislationTexts)
.set({ pollId })
.where(eq(legislationTexts.id, leg.id))
console.log(
`[poll-checker] linked poll ${pollId} to legislation ${leg.id}: ${leg.title}`,
)
return
}
}
}
async function sendVoteComparisonNotifications(
pollId: number,
pollLabel: string,
pollUrl: string | undefined,
allVotes: VoteDetail[],
) {
const legislations = await db
.select()
.from(legislationTexts)
.where(eq(legislationTexts.pollId, pollId))
if (legislations.length === 0) return
const legislation = legislations[0]
const votes = await db
.select()
.from(userVotes)
.where(eq(userVotes.legislationId, legislation.id))
if (votes.length === 0) return
const votesByMandate = new Map<number, VoteDetail>()
for (const v of allVotes) {
votesByMandate.set(v.mandate.id, v)
}
for (const userVote of votes) {
const deviceId = userVote.deviceId
const subs = await db
.select()
.from(pushSubscriptions)
.where(eq(pushSubscriptions.deviceId, deviceId))
.limit(1)
if (subs.length === 0) continue
const politicianFollows = await db
.select({ entityId: deviceFollows.entityId })
.from(deviceFollows)
.where(
and(
eq(deviceFollows.deviceId, deviceId),
eq(deviceFollows.type, "politician"),
),
)
const comparisons: string[] = []
for (const pf of politicianFollows) {
const mandateId = await resolveMandateId(pf.entityId)
if (mandateId === null) continue
const repVote = votesByMandate.get(mandateId)
if (!repVote) continue
const repLabel = VOTE_LABELS[repVote.vote] ?? repVote.vote
const fraction = repVote.fraction?.label
? ` (${repVote.fraction.label})`
: ""
comparisons.push(`${repVote.mandate.label}${fraction}: ${repLabel}`)
}
const userVoteLabel = USER_VOTE_LABELS[userVote.vote] ?? userVote.vote
const body = `Du: ${userVoteLabel}\n${comparisons.length > 0 ? comparisons.join("\n") : "Keine deiner Abgeordneten haben abgestimmt"}`
const payload: PushPayload = {
title: `Abstimmungsergebnis: ${pollLabel}`,
body,
url: pollUrl,
tag: `vote-result-${pollId}`,
}
const success = await sendPushNotification(
{
endpoint: subs[0].endpoint,
p256dh: subs[0].p256dh,
auth: subs[0].auth,
},
payload,
)
if (!success) {
console.log(
`[poll-checker] removing expired subscription for device ${deviceId}`,
)
await db
.delete(pushSubscriptions)
.where(eq(pushSubscriptions.deviceId, deviceId))
}
}
console.log(
`[poll-checker] poll ${pollId}: sent ${votes.length} vote comparison notification(s)`,
)
}
interface DeviceNotification {
deviceId: string
endpoint: string
@@ -118,6 +250,8 @@ async function processPoll(
fieldTopics: { id: number }[],
pollUrl: string | undefined,
) {
await linkPollToLegislation(pollId, pollLabel)
const topicIds = fieldTopics.map((t) => t.id)
if (topicIds.length === 0) return
@@ -226,4 +360,7 @@ async function processPoll(
console.log(
`[poll-checker] poll ${pollId}: sent ${notifications.length} notification(s)`,
)
// send vote comparison notifications for users who voted on linked legislation
await sendVoteComparisonNotifications(pollId, pollLabel, pollUrl, allVotes)
}