add legislation service with CRUD for legislation + user votes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
97
src/server/features/legislation/legislation.test.ts
Normal file
97
src/server/features/legislation/legislation.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
vi.mock("../../shared/db/client", () => {
|
||||
const mockDb = {
|
||||
select: vi.fn(),
|
||||
insert: vi.fn(),
|
||||
update: vi.fn(),
|
||||
}
|
||||
// chain builder pattern
|
||||
const chain = (result: unknown) => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(result),
|
||||
innerJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue(result),
|
||||
}),
|
||||
}),
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(result),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
mockDb.select.mockReturnValue(chain([]))
|
||||
mockDb.insert.mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
onConflictDoUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
returning: vi.fn().mockResolvedValue([{ id: 1 }]),
|
||||
}),
|
||||
})
|
||||
return { db: mockDb }
|
||||
})
|
||||
|
||||
const { getLegislation, castVote, getUserVote } = await import("./service")
|
||||
const { db } = await import("../../shared/db/client")
|
||||
|
||||
describe("legislation service", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("getLegislation", () => {
|
||||
it("returns null when legislation not found", async () => {
|
||||
const result = await getLegislation(999)
|
||||
expect(result).toBeNull()
|
||||
expect(db.select).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("castVote", () => {
|
||||
it("upserts a user vote", async () => {
|
||||
await castVote(1, {
|
||||
deviceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
vote: "ja",
|
||||
})
|
||||
expect(db.insert).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getUserVote", () => {
|
||||
it("returns null when no vote exists", async () => {
|
||||
const result = await getUserVote(
|
||||
1,
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("returns vote when it exists", async () => {
|
||||
const mockVote = {
|
||||
id: 1,
|
||||
deviceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
legislationId: 1,
|
||||
vote: "ja",
|
||||
votedAt: new Date("2026-03-10"),
|
||||
}
|
||||
const mockChain = {
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([mockVote]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
vi.mocked(db.select).mockReturnValueOnce(
|
||||
mockChain as ReturnType<typeof db.select>,
|
||||
)
|
||||
|
||||
const result = await getUserVote(
|
||||
1,
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
)
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.vote).toBe("ja")
|
||||
})
|
||||
})
|
||||
})
|
||||
8
src/server/features/legislation/schema.ts
Normal file
8
src/server/features/legislation/schema.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const castVoteSchema = z.object({
|
||||
deviceId: z.string().uuid(),
|
||||
vote: z.enum(["ja", "nein", "enthaltung"]),
|
||||
})
|
||||
|
||||
export type CastVoteRequest = z.infer<typeof castVoteSchema>
|
||||
107
src/server/features/legislation/service.ts
Normal file
107
src/server/features/legislation/service.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { and, desc, eq } from "drizzle-orm"
|
||||
import { db } from "../../shared/db/client"
|
||||
import {
|
||||
legislationSummaries,
|
||||
legislationTexts,
|
||||
userVotes,
|
||||
} from "../../shared/db/schema/legislation"
|
||||
import type { CastVoteRequest } from "./schema"
|
||||
|
||||
export async function getUpcomingLegislation() {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: legislationTexts.id,
|
||||
title: legislationTexts.title,
|
||||
beratungsstand: legislationTexts.beratungsstand,
|
||||
summary: legislationSummaries.summary,
|
||||
})
|
||||
.from(legislationTexts)
|
||||
.leftJoin(
|
||||
legislationSummaries,
|
||||
eq(legislationSummaries.legislationId, legislationTexts.id),
|
||||
)
|
||||
.orderBy(desc(legislationTexts.fetchedAt))
|
||||
.limit(20)
|
||||
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
beratungsstand: r.beratungsstand,
|
||||
summary: r.summary ?? null,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getLegislation(id: number) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(legislationTexts)
|
||||
.where(eq(legislationTexts.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (rows.length === 0) return null
|
||||
|
||||
const leg = rows[0]
|
||||
|
||||
// fetch summary if available
|
||||
const summaries = await db
|
||||
.select()
|
||||
.from(legislationSummaries)
|
||||
.where(eq(legislationSummaries.legislationId, id))
|
||||
.limit(1)
|
||||
|
||||
return {
|
||||
id: leg.id,
|
||||
dipVorgangsId: leg.dipVorgangsId,
|
||||
title: leg.title,
|
||||
abstract: leg.abstract,
|
||||
fullText: leg.fullText,
|
||||
drucksacheUrl: leg.drucksacheUrl,
|
||||
beratungsstand: leg.beratungsstand,
|
||||
summary: summaries[0]?.summary ?? null,
|
||||
fetchedAt: leg.fetchedAt.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLegislationText(id: number) {
|
||||
const rows = await db
|
||||
.select({ fullText: legislationTexts.fullText })
|
||||
.from(legislationTexts)
|
||||
.where(eq(legislationTexts.id, id))
|
||||
.limit(1)
|
||||
|
||||
return rows[0]?.fullText ?? null
|
||||
}
|
||||
|
||||
export async function castVote(legislationId: number, data: CastVoteRequest) {
|
||||
await db
|
||||
.insert(userVotes)
|
||||
.values({
|
||||
deviceId: data.deviceId,
|
||||
legislationId,
|
||||
vote: data.vote,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [userVotes.deviceId, userVotes.legislationId],
|
||||
set: { vote: data.vote, votedAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
export async function getUserVote(legislationId: number, deviceId: string) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(userVotes)
|
||||
.where(
|
||||
and(
|
||||
eq(userVotes.legislationId, legislationId),
|
||||
eq(userVotes.deviceId, deviceId),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (rows.length === 0) return null
|
||||
return {
|
||||
legislationId: rows[0].legislationId,
|
||||
vote: rows[0].vote,
|
||||
votedAt: rows[0].votedAt.toISOString(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user