From 96e946c1f94788fac99f00ed85bf8d1171e76836 Mon Sep 17 00:00:00 2001 From: Mats Jun Larsen Date: Tue, 2 Dec 2025 13:41:57 +0100 Subject: [PATCH 1/2] Migrate `EventRouter` to Authorization Middlewares --- apps/rpc/src/index.ts | 1 + apps/rpc/src/modules/event/event-router.ts | 550 +++++++++++---------- 2 files changed, 283 insertions(+), 268 deletions(-) diff --git a/apps/rpc/src/index.ts b/apps/rpc/src/index.ts index 4c1d046dad..3f064c27f2 100644 --- a/apps/rpc/src/index.ts +++ b/apps/rpc/src/index.ts @@ -2,3 +2,4 @@ export type { AppRouter } from "./app-router" export type { Pageable } from "./query" export type * from "./modules/article/article-router" +export type * from "./modules/event/event-router" diff --git a/apps/rpc/src/modules/event/event-router.ts b/apps/rpc/src/modules/event/event-router.ts index f67b179820..b735f4606a 100644 --- a/apps/rpc/src/modules/event/event-router.ts +++ b/apps/rpc/src/modules/event/event-router.ts @@ -9,302 +9,316 @@ import { GroupSchema, UserSchema, } from "@dotkomonline/types" +import type { inferProcedureInput, inferProcedureOutput } from "@trpc/server" import { z } from "zod" +import { isEditor } from "../../authorization" +import { withAuditLogEntry, withAuthentication, withAuthorization, withDatabaseTransaction } from "../../middlewares" import { BasePaginateInputSchema, PaginateInputSchema } from "../../query" -import { authenticatedProcedure, procedure, staffProcedure, t } from "../../trpc" +import { procedure, t } from "../../trpc" import { feedbackRouter } from "../feedback-form/feedback-router" import { attendanceRouter } from "./attendance-router" -export const eventRouter = t.router({ - attendance: attendanceRouter, - feedback: feedbackRouter, - get: procedure - .input(EventSchema.shape.id) - .output(EventWithAttendanceSchema) - .query(async ({ input, ctx }) => - ctx.executeTransaction(async (handle) => { - const event = await ctx.eventService.getEventById(handle, input) - const attendance = event.attendanceId - ? await ctx.attendanceService.findAttendanceById(handle, event.attendanceId) - : null - return { event, attendance } - }) - ), - - find: procedure - .input(EventSchema.shape.id) - .output(EventWithAttendanceSchema.nullable()) - .query(async ({ input, ctx }) => - ctx.executeTransaction(async (handle) => { - const event = await ctx.eventService.findEventById(handle, input) - if (!event) { - return null - } - const attendance = event.attendanceId - ? await ctx.attendanceService.findAttendanceById(handle, event.attendanceId) - : null - return { event, attendance } - }) - ), +export type GetEventInput = inferProcedureInput +export type GetEventOutput = inferProcedureOutput +const getEventProcedure = procedure + .input(EventSchema.shape.id) + .output(EventWithAttendanceSchema) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const event = await ctx.eventService.getEventById(ctx.handle, input) + const attendance = event.attendanceId + ? await ctx.attendanceService.findAttendanceById(ctx.handle, event.attendanceId) + : null + return { event, attendance } + }) - create: staffProcedure - .input( - z.object({ - event: EventWriteSchema, - groupIds: z.array(GroupSchema.shape.slug), - companyIds: z.array(CompanySchema.shape.id), - parentId: EventSchema.shape.parentId.optional(), - }) - ) - .output(EventWithAttendanceSchema) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const eventWithoutOrganizers = await ctx.eventService.createEvent(handle, input.event) - const event = await ctx.eventService.updateEventOrganizers( - handle, - eventWithoutOrganizers.id, - new Set(input.groupIds), - new Set(input.companyIds) - ) - await ctx.eventService.updateEventParent(handle, event.id, input.parentId ?? null) - return { event, attendance: null } - }) - }), - - edit: staffProcedure - .input( - z.object({ - id: EventSchema.shape.id, - event: EventWriteSchema, - groupIds: z.array(GroupSchema.shape.slug), - companyIds: z.array(CompanySchema.shape.id), - parentId: EventSchema.shape.parentId.optional(), - }) - ) - .output(EventWithAttendanceSchema) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const updatedEventWithoutOrganizers = await ctx.eventService.updateEvent(handle, input.id, input.event) - const updatedEvent = await ctx.eventService.updateEventOrganizers( - handle, - updatedEventWithoutOrganizers.id, - new Set(input.groupIds), - new Set(input.companyIds) - ) - await ctx.eventService.updateEventParent(handle, updatedEvent.id, input.parentId ?? null) +export type FindEventInput = inferProcedureInput +export type FindEventOutput = inferProcedureOutput +const findEventProcedure = procedure + .input(EventSchema.shape.id) + .output(EventWithAttendanceSchema.nullable()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const event = await ctx.eventService.findEventById(ctx.handle, input) + if (!event) return null + const attendance = event.attendanceId + ? await ctx.attendanceService.findAttendanceById(ctx.handle, event.attendanceId) + : null + return { event, attendance } + }) - const attendance = updatedEventWithoutOrganizers.attendanceId - ? await ctx.attendanceService.findAttendanceById(handle, updatedEventWithoutOrganizers.attendanceId) - : null - return { event: updatedEvent, attendance } - }) - }), - - delete: staffProcedure - .input( - z.object({ - id: EventSchema.shape.id, - }) +export type CreateEventInput = inferProcedureInput +export type CreateEventOutput = inferProcedureOutput +const createEventProcedure = procedure + .input( + z.object({ + event: EventWriteSchema, + groupIds: z.array(GroupSchema.shape.slug), + companyIds: z.array(CompanySchema.shape.id), + parentId: EventSchema.shape.parentId.optional(), + }) + ) + .output(EventWithAttendanceSchema) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const eventWithoutOrganizers = await ctx.eventService.createEvent(ctx.handle, input.event) + const event = await ctx.eventService.updateEventOrganizers( + ctx.handle, + eventWithoutOrganizers.id, + new Set(input.groupIds), + new Set(input.companyIds) ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - return await ctx.eventService.deleteEvent(handle, input.id) - }) - }), + await ctx.eventService.updateEventParent(ctx.handle, event.id, input.parentId ?? null) + return { event, attendance: null } + }) - all: procedure - .input(BasePaginateInputSchema.extend({ filter: EventFilterQuerySchema.optional() }).default({})) - .output( - z.object({ - items: EventWithAttendanceSchema.array(), - nextCursor: EventSchema.shape.id.optional(), - }) +export type EditEventInput = inferProcedureInput +export type EditEventOutput = inferProcedureOutput +const editEventProcedure = procedure + .input( + z.object({ + id: EventSchema.shape.id, + event: EventWriteSchema, + groupIds: z.array(GroupSchema.shape.slug), + companyIds: z.array(CompanySchema.shape.id), + parentId: EventSchema.shape.parentId.optional(), + }) + ) + .output(EventWithAttendanceSchema) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const updatedEventWithoutOrganizers = await ctx.eventService.updateEvent(ctx.handle, input.id, input.event) + const updatedEvent = await ctx.eventService.updateEventOrganizers( + ctx.handle, + updatedEventWithoutOrganizers.id, + new Set(input.groupIds), + new Set(input.companyIds) ) - .query(async ({ input, ctx }) => - ctx.executeTransaction(async (handle) => { - const { filter, ...page } = input - const events = await ctx.eventService.findEvents(handle, { ...filter }, page) - const attendances = await ctx.attendanceService.getAttendancesByIds( - handle, - events.map((item) => item.attendanceId).filter((id) => id !== null) - ) + await ctx.eventService.updateEventParent(ctx.handle, updatedEvent.id, input.parentId ?? null) - const eventsWithAttendance = events.map((event) => ({ - event, - attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, - })) + const attendance = updatedEventWithoutOrganizers.attendanceId + ? await ctx.attendanceService.findAttendanceById(ctx.handle, updatedEventWithoutOrganizers.attendanceId) + : null + return { event: updatedEvent, attendance } + }) - return { - items: eventsWithAttendance, - nextCursor: events.at(-1)?.id, - } - }) - ), +export type DeleteEventInput = inferProcedureInput +export type DeleteEventOutput = inferProcedureOutput +const deleteEventProcedure = procedure + .input(z.object({ id: EventSchema.shape.id })) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + return await ctx.eventService.deleteEvent(ctx.handle, input.id) + }) - allByAttendingUserId: authenticatedProcedure - .input(BasePaginateInputSchema.extend({ filter: EventFilterQuerySchema.optional(), id: UserSchema.shape.id })) - .output( - z.object({ - items: EventWithAttendanceSchema.array(), - nextCursor: EventSchema.shape.id.optional(), - }) +export type AllEventsInput = inferProcedureInput +export type AllEventsOutput = inferProcedureOutput +const allEventsProcedure = procedure + .input(BasePaginateInputSchema.extend({ filter: EventFilterQuerySchema.optional() }).default({})) + .output( + z.object({ + items: EventWithAttendanceSchema.array(), + nextCursor: EventSchema.shape.id.optional(), + }) + ) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const { filter, ...page } = input + const events = await ctx.eventService.findEvents(ctx.handle, { ...filter }, page) + const attendances = await ctx.attendanceService.getAttendancesByIds( + ctx.handle, + events.map((item) => item.attendanceId).filter((id) => id !== null) ) - .query(async ({ input, ctx }) => - ctx.executeTransaction(async (handle) => { - const { id, filter, ...page } = input - const events = await ctx.eventService.findEventsByAttendingUserId(handle, id, { ...filter }, page) - const attendances = await ctx.attendanceService.getAttendancesByIds( - handle, - events.map((item) => item.attendanceId).filter((id) => id !== null) - ) - - const eventsWithAttendance = events.map((event) => ({ - event, - attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, - })) - - return { - items: eventsWithAttendance, - nextCursor: events.at(-1)?.id, - } - }) - ), - addAttendance: staffProcedure - .input( - z.object({ - values: AttendanceWriteSchema, - eventId: EventSchema.shape.id, - }) - ) - .output(EventWithAttendanceSchema) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const attendance = await ctx.attendanceService.createAttendance(handle, input.values) - const event = await ctx.eventService.updateEventAttendance(handle, input.eventId, attendance.id) - return { event, attendance } - }) - }), + const eventsWithAttendance = events.map((event) => ({ + event, + attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, + })) - updateParentEvent: staffProcedure - .input( - z.object({ - eventId: EventSchema.shape.id, - parentEventId: EventSchema.shape.id.nullable(), - }) - ) - .output(EventWithAttendanceSchema) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const updatedEvent = await ctx.eventService.updateEventParent(handle, input.eventId, input.parentEventId) - const attendance = updatedEvent.attendanceId - ? await ctx.attendanceService.findAttendanceById(handle, updatedEvent.attendanceId) - : null - return { event: updatedEvent, attendance } - }) - }), + return { + items: eventsWithAttendance, + nextCursor: events.at(-1)?.id, + } + }) - findParentEvent: procedure - .input( - z.object({ - eventId: EventSchema.shape.id, - }) +export type AllByAttendingUserIdInput = inferProcedureInput +export type AllByAttendingUserIdOutput = inferProcedureOutput +const allByAttendingUserIdProcedure = procedure + .input(BasePaginateInputSchema.extend({ filter: EventFilterQuerySchema.optional(), id: UserSchema.shape.id })) + .output( + z.object({ + items: EventWithAttendanceSchema.array(), + nextCursor: EventSchema.shape.id.optional(), + }) + ) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const { id, filter, ...page } = input + const events = await ctx.eventService.findEventsByAttendingUserId(ctx.handle, id, { ...filter }, page) + const attendances = await ctx.attendanceService.getAttendancesByIds( + ctx.handle, + events.map((item) => item.attendanceId).filter((id) => id !== null) ) - .output(EventWithAttendanceSchema.nullable()) - .query(async ({ input, ctx }) => { - return ctx.executeTransaction(async (handle) => { - const childEvent = await ctx.eventService.findEventById(handle, input.eventId) - if (!childEvent?.parentId) { - return null - } + const eventsWithAttendance = events.map((event) => ({ + event, + attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, + })) - const event = await ctx.eventService.findEventById(handle, childEvent.parentId) + return { + items: eventsWithAttendance, + nextCursor: events.at(-1)?.id, + } + }) - if (!event) { - return null - } +export type AddAttendanceInput = inferProcedureInput +export type AddAttendanceOutput = inferProcedureOutput +const addAttendanceProcedure = procedure + .input(z.object({ values: AttendanceWriteSchema, eventId: EventSchema.shape.id })) + .output(EventWithAttendanceSchema) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const attendance = await ctx.attendanceService.createAttendance(ctx.handle, input.values) + const event = await ctx.eventService.updateEventAttendance(ctx.handle, input.eventId, attendance.id) + return { event, attendance } + }) - const attendance = event?.attendanceId - ? await ctx.attendanceService.findAttendanceById(handle, event.attendanceId) - : null +export type UpdateParentEventInput = inferProcedureInput +export type UpdateParentEventOutput = inferProcedureOutput +const updateParentEventProcedure = procedure + .input(z.object({ eventId: EventSchema.shape.id, parentEventId: EventSchema.shape.id.nullable() })) + .output(EventWithAttendanceSchema) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const updatedEvent = await ctx.eventService.updateEventParent(ctx.handle, input.eventId, input.parentEventId) + const attendance = updatedEvent.attendanceId + ? await ctx.attendanceService.findAttendanceById(ctx.handle, updatedEvent.attendanceId) + : null + return { event: updatedEvent, attendance } + }) - return { event, attendance } - }) - }), +export type FindParentEventInput = inferProcedureInput +export type FindParentEventOutput = inferProcedureOutput +const findParentEventProcedure = procedure + .input(z.object({ eventId: EventSchema.shape.id })) + .output(EventWithAttendanceSchema.nullable()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const childEvent = await ctx.eventService.findEventById(ctx.handle, input.eventId) + if (!childEvent?.parentId) return null + const event = await ctx.eventService.findEventById(ctx.handle, childEvent.parentId) + if (!event) return null + const attendance = event.attendanceId + ? await ctx.attendanceService.findAttendanceById(ctx.handle, event.attendanceId) + : null + return { event, attendance } + }) - findChildEvents: procedure - .input( - z.object({ - eventId: EventSchema.shape.id, - }) +export type FindChildEventsInput = inferProcedureInput +export type FindChildEventsOutput = inferProcedureOutput +const findChildEventsProcedure = procedure + .input(z.object({ eventId: EventSchema.shape.id })) + .output(EventWithAttendanceSchema.array()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const events = await ctx.eventService.findByParentEventId(ctx.handle, input.eventId) + const attendances = await ctx.attendanceService.getAttendancesByIds( + ctx.handle, + events.map((item) => item.attendanceId).filter((id) => id !== null) ) - .output(EventWithAttendanceSchema.array()) - .query(async ({ input, ctx }) => { - return ctx.executeTransaction(async (handle) => { - const events = await ctx.eventService.findByParentEventId(handle, input.eventId) - - const attendances = await ctx.attendanceService.getAttendancesByIds( - handle, - events.map((item) => item.attendanceId).filter((id) => id !== null) - ) + return events.map((event) => ({ + event, + attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, + })) + }) - const eventsWithAttendance = events.map((event) => ({ - event, - attendance: attendances.find((attendance) => attendance.id === event.attendanceId) || null, - })) +export type FindUnansweredByUserInput = inferProcedureInput +export type FindUnansweredByUserOutput = inferProcedureOutput +const findUnansweredByUserProcedure = procedure + .input(UserSchema.shape.id) + .output(EventSchema.array()) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => ctx.eventService.findEventsWithUnansweredFeedbackFormByUserId(ctx.handle, input)) - return eventsWithAttendance - }) - }), - - findUnansweredByUser: authenticatedProcedure - .input(UserSchema.shape.id) - .output(EventSchema.array()) - .query(async ({ input, ctx }) => - ctx.executeTransaction( - async (handle) => await ctx.eventService.findEventsWithUnansweredFeedbackFormByUserId(handle, input) - ) - ), - - isOrganizer: authenticatedProcedure - .input( - z.object({ - eventId: EventSchema.shape.id, - }) - ) - .output(z.boolean()) - .query(async ({ input, ctx }) => { - return ctx.executeTransaction(async (handle) => { - const event = await ctx.eventService.getEventById(handle, input.eventId) - const groups = await ctx.groupService.findManyByMemberUserId(handle, ctx.principal.subject) +export type IsOrganizerInput = inferProcedureInput +export type IsOrganizerOutput = inferProcedureOutput +const isOrganizerProcedure = procedure + .input(z.object({ eventId: EventSchema.shape.id })) + .output(z.boolean()) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const event = await ctx.eventService.getEventById(ctx.handle, input.eventId) + const groups = await ctx.groupService.findManyByMemberUserId(ctx.handle, ctx.principal.subject) + return groups.some((group) => event.hostingGroups.some((organizer) => organizer.slug === group.slug)) + }) - return groups.some((group) => event.hostingGroups.some((organizer) => organizer.slug === group.slug)) - }) - }), +export type FindManyDeregisterReasonsWithEventInput = inferProcedureInput< + typeof findManyDeregisterReasonsWithEventProcedure +> +export type FindManyDeregisterReasonsWithEventOutput = inferProcedureOutput< + typeof findManyDeregisterReasonsWithEventProcedure +> +const findManyDeregisterReasonsWithEventProcedure = procedure + .input(PaginateInputSchema) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .query(async ({ input, ctx }) => { + const rows = await ctx.eventService.findManyDeregisterReasonsWithEvent(ctx.handle, input) + return { + items: rows, + nextCursor: rows.at(-1)?.id, + } + }) - findManyDeregisterReasonsWithEvent: staffProcedure.input(PaginateInputSchema).query(async ({ ctx, input }) => { - return ctx.executeTransaction(async (handle) => { - const rows = await ctx.eventService.findManyDeregisterReasonsWithEvent(handle, input) +export type CreateFileUploadInput = inferProcedureInput +export type CreateFileUploadOutput = inferProcedureOutput +const createFileUploadProcedure = procedure + .input(z.object({ filename: z.string(), contentType: z.string() })) + .output(z.custom()) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ ctx, input }) => { + return ctx.eventService.createFileUpload(ctx.handle, input.filename, input.contentType, ctx.principal.subject) + }) - return { - items: rows, - nextCursor: rows.at(-1)?.id, - } - }) - }), - - createFileUpload: staffProcedure - .input( - z.object({ - filename: z.string(), - contentType: z.string(), - }) - ) - .output(z.custom()) - .mutation(async ({ ctx, input }) => { - return ctx.executeTransaction(async (handle) => { - return ctx.eventService.createFileUpload(handle, input.filename, input.contentType, ctx.principal.subject) - }) - }), +export const eventRouter = t.router({ + attendance: attendanceRouter, + feedback: feedbackRouter, + get: getEventProcedure, + find: findEventProcedure, + create: createEventProcedure, + edit: editEventProcedure, + delete: deleteEventProcedure, + all: allEventsProcedure, + allByAttendingUserId: allByAttendingUserIdProcedure, + addAttendance: addAttendanceProcedure, + updateParentEvent: updateParentEventProcedure, + findParentEvent: findParentEventProcedure, + findChildEvents: findChildEventsProcedure, + findUnansweredByUser: findUnansweredByUserProcedure, + isOrganizer: isOrganizerProcedure, + findManyDeregisterReasonsWithEvent: findManyDeregisterReasonsWithEventProcedure, + createFileUpload: createFileUploadProcedure, }) From d6bf2d7c6f5c09fb8b6e26d15273802c6b4af6b8 Mon Sep 17 00:00:00 2001 From: Mats Jun Larsen Date: Tue, 2 Dec 2025 13:48:50 +0100 Subject: [PATCH 2/2] Migrate `AttendanceRouter` to Authorization Middlewares --- apps/rpc/src/index.ts | 1 + .../src/modules/event/attendance-router.ts | 689 ++++++++++-------- 2 files changed, 376 insertions(+), 314 deletions(-) diff --git a/apps/rpc/src/index.ts b/apps/rpc/src/index.ts index 3f064c27f2..fe9c218a15 100644 --- a/apps/rpc/src/index.ts +++ b/apps/rpc/src/index.ts @@ -3,3 +3,4 @@ export type { Pageable } from "./query" export type * from "./modules/article/article-router" export type * from "./modules/event/event-router" +export type * from "./modules/event/attendance-router" diff --git a/apps/rpc/src/modules/event/attendance-router.ts b/apps/rpc/src/modules/event/attendance-router.ts index 32bfd73403..4a46ca2a70 100644 --- a/apps/rpc/src/modules/event/attendance-router.ts +++ b/apps/rpc/src/modules/event/attendance-router.ts @@ -13,350 +13,411 @@ import { } from "@dotkomonline/types" import { getCurrentUTC } from "@dotkomonline/utils" import { TRPCError } from "@trpc/server" +import type { inferProcedureInput, inferProcedureOutput } from "@trpc/server" import { addHours, max } from "date-fns" import { z } from "zod" +import { isEditor } from "../../authorization" import { FailedPreconditionError } from "../../error" -import { authenticatedProcedure, procedure, staffProcedure, t } from "../../trpc" +import { withAuditLogEntry, withAuthentication, withAuthorization, withDatabaseTransaction } from "../../middlewares" +import { procedure, t } from "../../trpc" -export const attendanceRouter = t.router({ - createPool: staffProcedure - .input( - z.object({ - id: AttendanceSchema.shape.id, - input: AttendancePoolWriteSchema, - }) - ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => - ctx.attendanceService.createAttendancePool(handle, input.id, input.input) - ) - }), +export type CreatePoolInput = inferProcedureInput +export type CreatePoolOutput = inferProcedureOutput +const createPoolProcedure = procedure + .input( + z.object({ + id: AttendanceSchema.shape.id, + input: AttendancePoolWriteSchema, + }) + ) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + return ctx.attendanceService.createAttendancePool(ctx.handle, input.id, input.input) + }) - updatePool: staffProcedure - .input( - z.object({ - id: AttendancePoolSchema.shape.id, - input: AttendancePoolWriteSchema.partial(), - }) - ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const attendance = await ctx.attendanceService.getAttendanceByPoolId(handle, input.id) - const pool = attendance.pools.find((pool) => pool.id === input.id) - if (pool === undefined) { - throw new TRPCError({ code: "NOT_FOUND" }) - } - await ctx.attendanceService.updateAttendancePool(handle, input.id, { ...pool, ...input.input }) - }) - }), +export type UpdatePoolInput = inferProcedureInput +export type UpdatePoolOutput = inferProcedureOutput +const updatePoolProcedure = procedure + .input( + z.object({ + id: AttendancePoolSchema.shape.id, + input: AttendancePoolWriteSchema.partial(), + }) + ) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const attendance = await ctx.attendanceService.getAttendanceByPoolId(ctx.handle, input.id) + const pool = attendance.pools.find((pool) => pool.id === input.id) + if (pool === undefined) { + throw new TRPCError({ code: "NOT_FOUND" }) + } + await ctx.attendanceService.updateAttendancePool(ctx.handle, input.id, { ...pool, ...input.input }) + }) - deletePool: staffProcedure - .input( - z.object({ - id: AttendancePoolSchema.shape.id, - }) - ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => - ctx.attendanceService.deleteAttendancePool(handle, input.id) - ) - }), +export type DeletePoolInput = inferProcedureInput +export type DeletePoolOutput = inferProcedureOutput +const deletePoolProcedure = procedure + .input( + z.object({ + id: AttendancePoolSchema.shape.id, + }) + ) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + return ctx.attendanceService.deleteAttendancePool(ctx.handle, input.id) + }) - adminRegisterForEvent: staffProcedure - .input( - z.object({ - attendanceId: AttendanceSchema.shape.id, - attendancePoolId: AttendancePoolSchema.shape.id, - userId: UserSchema.shape.id, - }) +export type AdminRegisterForEventInput = inferProcedureInput +export type AdminRegisterForEventOutput = inferProcedureOutput +const adminRegisterForEventProcedure = procedure + .input( + z.object({ + attendanceId: AttendanceSchema.shape.id, + attendancePoolId: AttendancePoolSchema.shape.id, + userId: UserSchema.shape.id, + }) + ) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const result = await ctx.attendanceService.getRegistrationAvailability( + ctx.handle, + input.attendanceId, + input.userId, + { + ignoreRegistrationWindow: true, + immediateReservation: true, + immediatePayment: false, + overriddenAttendancePoolId: input.attendancePoolId, + ignoreRegisteredToParent: true, + } ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const result = await ctx.attendanceService.getRegistrationAvailability( - handle, - input.attendanceId, - input.userId, - { - ignoreRegistrationWindow: true, - immediateReservation: true, - immediatePayment: false, - overriddenAttendancePoolId: input.attendancePoolId, - ignoreRegisteredToParent: true, - } - ) - if (!result.success) { - throw new FailedPreconditionError(`Failed to register: ${result.cause}`) - } - return await ctx.attendanceService.registerAttendee(handle, result) - }) - }), + if (!result.success) { + throw new FailedPreconditionError(`Failed to register: ${result.cause}`) + } + return await ctx.attendanceService.registerAttendee(ctx.handle, result) + }) - updateAttendancePayment: staffProcedure - .input( - z.object({ - id: AttendanceSchema.shape.id, - price: z.number().int().nullable(), - }) - ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const attendance = await ctx.attendanceService.getAttendanceById(handle, input.id) - if (attendance === undefined) { - throw new TRPCError({ code: "NOT_FOUND" }) - } - return ctx.attendanceService.updateAttendancePaymentPrice(handle, input.id, input.price) - }) - }), +export type UpdateAttendancePaymentInput = inferProcedureInput +export type UpdateAttendancePaymentOutput = inferProcedureOutput +const updateAttendancePaymentProcedure = procedure + .input( + z.object({ + id: AttendanceSchema.shape.id, + price: z.number().int().nullable(), + }) + ) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const attendance = await ctx.attendanceService.getAttendanceById(ctx.handle, input.id) + if (attendance === undefined) { + throw new TRPCError({ code: "NOT_FOUND" }) + } + return ctx.attendanceService.updateAttendancePaymentPrice(ctx.handle, input.id, input.price) + }) - getSelectionsResults: staffProcedure - .input( - z.object({ - attendanceId: AttendanceSchema.shape.id, - }) - ) - .query(async ({ input, ctx }) => { - return ctx.executeTransaction(async (handle) => { - const attendance = await ctx.attendanceService.getAttendanceById(handle, input.attendanceId) - const allSelectionResponses = attendance.attendees.flatMap((attendee) => attendee.selections) +export type GetSelectionsResultsInput = inferProcedureInput +export type GetSelectionsResultsOutput = inferProcedureOutput +const getSelectionsResultsProcedure = procedure + .input(z.object({ attendanceId: AttendanceSchema.shape.id })) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const attendance = await ctx.attendanceService.getAttendanceById(ctx.handle, input.attendanceId) + const allSelectionResponses = attendance.attendees.flatMap((attendee) => attendee.selections) - return attendance.selections.map((selection) => { - const selectionResponses = allSelectionResponses.filter((response) => response.selectionId === selection.id) + return attendance.selections.map((selection) => { + const selectionResponses = allSelectionResponses.filter((response) => response.selectionId === selection.id) - return { - id: selection.id, - name: selection.name, - totalCount: selectionResponses.length, - options: selection.options.map((option) => ({ - id: option.id, - name: option.name, - count: selectionResponses.filter((response) => response.optionId === option.id).length, - })), - } - }) - }) - }), + return { + id: selection.id, + name: selection.name, + totalCount: selectionResponses.length, + options: selection.options.map((option) => ({ + id: option.id, + name: option.name, + count: selectionResponses.filter((response) => response.optionId === option.id).length, + })), + } + }) + }) - getRegistrationAvailability: authenticatedProcedure - .input( - z.object({ - attendanceId: AttendanceSchema.shape.id, - }) +export type GetRegistrationAvailabilityInput = inferProcedureInput +export type GetRegistrationAvailabilityOutput = inferProcedureOutput +const getRegistrationAvailabilityProcedure = procedure + .input(z.object({ attendanceId: AttendanceSchema.shape.id })) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + return await ctx.attendanceService.getRegistrationAvailability( + ctx.handle, + input.attendanceId, + ctx.principal.subject, + { + ignoreRegistrationWindow: false, + immediateReservation: false, + immediatePayment: true, + overriddenAttendancePoolId: null, + ignoreRegisteredToParent: false, + } ) - .query(async ({ input, ctx }) => - ctx.executeTransaction(async (handle) => { - return await ctx.attendanceService.getRegistrationAvailability( - handle, - input.attendanceId, - ctx.principal.subject, - { - ignoreRegistrationWindow: false, - immediateReservation: false, - immediatePayment: true, - overriddenAttendancePoolId: null, - ignoreRegisteredToParent: false, - } - ) - }) - ), + }) - registerForEvent: authenticatedProcedure - .input( - z.object({ - attendanceId: AttendanceSchema.shape.id, - }) +export type RegisterForEventInput = inferProcedureInput +export type RegisterForEventOutput = inferProcedureOutput +const registerForEventProcedure = procedure + .input(z.object({ attendanceId: AttendanceSchema.shape.id })) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const result = await ctx.attendanceService.getRegistrationAvailability( + ctx.handle, + input.attendanceId, + ctx.principal.subject, + { + ignoreRegistrationWindow: false, + immediateReservation: false, + immediatePayment: true, + overriddenAttendancePoolId: null, + ignoreRegisteredToParent: false, + } ) - .mutation(async ({ input, ctx }) => - ctx.executeAuditedTransaction(async (handle) => { - const result = await ctx.attendanceService.getRegistrationAvailability( - handle, - input.attendanceId, - ctx.principal.subject, - { - ignoreRegistrationWindow: false, - immediateReservation: false, - immediatePayment: true, - overriddenAttendancePoolId: null, - ignoreRegisteredToParent: false, - } - ) - if (!result.success) { - throw new FailedPreconditionError(`Failed to register: ${result.cause}`) - } - return await ctx.attendanceService.registerAttendee(handle, result) - }) - ), + if (!result.success) { + throw new FailedPreconditionError(`Failed to register: ${result.cause}`) + } + return await ctx.attendanceService.registerAttendee(ctx.handle, result) + }) - onRegisterChange: procedure - .input(z.object({ attendanceId: AttendanceSchema.shape.id })) - .subscription(async function* ({ input, ctx, signal }) { - for await (const [data] of on(ctx.eventEmitter, "attendance:register-change", { signal })) { - const attendeeUpdateData = data as { attendee: Attendee; status: "registered" | "deregistered" } +export type OnRegisterChangeInput = inferProcedureInput +export type OnRegisterChangeOutput = inferProcedureOutput +const onRegisterChangeProcedure = procedure + .input(z.object({ attendanceId: AttendanceSchema.shape.id })) + .use(withDatabaseTransaction()) + .subscription(async function* ({ input, ctx, signal }) { + for await (const [data] of on(ctx.eventEmitter, "attendance:register-change", { signal })) { + const attendeeUpdateData = data as { attendee: Attendee; status: "registered" | "deregistered" } - if (attendeeUpdateData.attendee.attendanceId !== input.attendanceId) { - continue - } - - yield attendeeUpdateData + if (attendeeUpdateData.attendee.attendanceId !== input.attendanceId) { + continue } - }), - cancelAttendeePayment: staffProcedure - .input( - z.object({ - attendeeId: AttendeeSchema.shape.id, - }) - ) - .mutation(async ({ input: { attendeeId }, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => - ctx.attendanceService.cancelAttendeePayment(handle, attendeeId, ctx.principal.subject) - ) - }), - startAttendeePayment: staffProcedure - .input( - z.object({ - attendeeId: AttendeeSchema.shape.id, - }) - ) - .mutation(async ({ input: { attendeeId }, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - let deadline = addHours(getCurrentUTC(), 24) + yield attendeeUpdateData + } + }) - // DELETE THIS START - // turn deadline back to const after this is removed - const julebordAttendanceId = "b470eda0-4650-4dbd-bb6e-7bcf9c7757f9" - const arbitraryJulebordDeadline = new TZDate("2025-11-07T19:00:00Z") // this is 20:00 local time in norway - const attendee = await ctx.attendanceService.getAttendeeById(handle, attendeeId) - if (attendee.attendanceId === julebordAttendanceId) { - deadline = max([arbitraryJulebordDeadline, deadline]) - } - // DELETE THIS END +export type CancelAttendeePaymentInput = inferProcedureInput +export type CancelAttendeePaymentOutput = inferProcedureOutput +const cancelAttendeePaymentProcedure = procedure + .input(z.object({ attendeeId: AttendeeSchema.shape.id })) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input: { attendeeId }, ctx }) => { + return ctx.attendanceService.cancelAttendeePayment(ctx.handle, attendeeId, ctx.principal.subject) + }) - return ctx.attendanceService.startAttendeePayment(handle, attendeeId, deadline) - }) - }), - deregisterForEvent: authenticatedProcedure - .input( - z.object({ - attendanceId: AttendancePoolSchema.shape.id, - deregisterReason: z.object({ - type: DeregisterReasonTypeSchema, - details: z.string().nullable(), - }), - }) - ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const attendance = await ctx.attendanceService.getAttendanceById(handle, input.attendanceId) - const attendee = attendance.attendees.find((attendee) => attendee.user.id === ctx.principal.subject) - if (attendee === undefined) { - throw new TRPCError({ code: "NOT_FOUND" }) - } - await ctx.attendanceService.deregisterAttendee(handle, attendee.id, { - ignoreDeregistrationWindow: false, - }) +export type StartAttendeePaymentInput = inferProcedureInput +export type StartAttendeePaymentOutput = inferProcedureOutput +const startAttendeePaymentProcedure = procedure + .input(z.object({ attendeeId: AttendeeSchema.shape.id })) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input: { attendeeId }, ctx }) => { + let deadline = addHours(getCurrentUTC(), 24) - const event = await ctx.eventService.getByAttendanceId(handle, attendance.id) - await ctx.eventService.createDeregisterReason(handle, { - ...input.deregisterReason, - userId: ctx.principal.subject, - eventId: event.id, - registeredAt: attendee.createdAt, - userGrade: attendee.userGrade, - }) - }) - }), + // DELETE THIS START + // turn deadline back to const after this is removed + const julebordAttendanceId = "b470eda0-4650-4dbd-bb6e-7bcf9c7757f9" + const arbitraryJulebordDeadline = new TZDate("2025-11-07T19:00:00Z") // this is 20:00 local time in norway + const attendee = await ctx.attendanceService.getAttendeeById(ctx.handle, attendeeId) + if (attendee.attendanceId === julebordAttendanceId) { + deadline = max([arbitraryJulebordDeadline, deadline]) + } + // DELETE THIS END - adminDeregisterForEvent: staffProcedure - .input( - z.object({ - attendeeId: AttendeeSchema.shape.id, - }) - ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - const attendance = await ctx.attendanceService.getAttendanceByAttendeeId(handle, input.attendeeId) - const attendee = attendance.attendees.find((attendee) => attendee.id === input.attendeeId) - if (attendee === undefined) { - throw new TRPCError({ code: "NOT_FOUND" }) - } - return await ctx.attendanceService.deregisterAttendee(handle, attendee.id, { - ignoreDeregistrationWindow: true, - }) - }) - }), + return ctx.attendanceService.startAttendeePayment(ctx.handle, attendeeId, deadline) + }) - adminUpdateAtteendeeReserved: staffProcedure - .input( - z.object({ - attendeeId: AttendeeSchema.shape.id, - reserved: AttendeeSchema.shape.reserved, - }) - ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - await ctx.attendanceService.updateAttendeeById(handle, input.attendeeId, { reserved: input.reserved }) - }) - }), +export type DeregisterForEventInput = inferProcedureInput +export type DeregisterForEventOutput = inferProcedureOutput +const deregisterForEventProcedure = procedure + .input( + z.object({ + attendanceId: AttendancePoolSchema.shape.id, + deregisterReason: z.object({ + type: DeregisterReasonTypeSchema, + details: z.string().nullable(), + }), + }) + ) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const attendance = await ctx.attendanceService.getAttendanceById(ctx.handle, input.attendanceId) + const attendee = attendance.attendees.find((attendee) => attendee.user.id === ctx.principal.subject) + if (attendee === undefined) { + throw new TRPCError({ code: "NOT_FOUND" }) + } + await ctx.attendanceService.deregisterAttendee(ctx.handle, attendee.id, { + ignoreDeregistrationWindow: false, + }) - registerAttendance: staffProcedure - .input( - z.object({ - id: AttendeeSchema.shape.id, - at: z.coerce.date().nullable(), - }) - ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - await ctx.attendanceService.registerAttendance(handle, input.id, input.at ? new TZDate(input.at) : null) - }) - }), + const event = await ctx.eventService.getByAttendanceId(ctx.handle, attendance.id) + await ctx.eventService.createDeregisterReason(ctx.handle, { + ...input.deregisterReason, + userId: ctx.principal.subject, + eventId: event.id, + registeredAt: attendee.createdAt, + userGrade: attendee.userGrade, + }) + }) - updateSelectionResponses: authenticatedProcedure - .input( - z.object({ - attendeeId: AttendeeSchema.shape.id, - options: AttendeeSelectionResponseSchema.array(), - }) - ) - .mutation(async ({ input, ctx }) => { - return ctx.executeAuditedTransaction(async (handle) => { - await ctx.attendanceService.updateAttendeeById(handle, input.attendeeId, { selections: input.options }) - }) - }), +export type AdminDeregisterForEventInput = inferProcedureInput +export type AdminDeregisterForEventOutput = inferProcedureOutput +const adminDeregisterForEventProcedure = procedure + .input(z.object({ attendeeId: AttendeeSchema.shape.id })) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + const attendance = await ctx.attendanceService.getAttendanceByAttendeeId(ctx.handle, input.attendeeId) + const attendee = attendance.attendees.find((attendee) => attendee.id === input.attendeeId) + if (attendee === undefined) { + throw new TRPCError({ code: "NOT_FOUND" }) + } + return await ctx.attendanceService.deregisterAttendee(ctx.handle, attendee.id, { + ignoreDeregistrationWindow: true, + }) + }) - getAttendance: procedure - .input( - z.object({ - id: AttendanceSchema.shape.id, - }) - ) - .query(async ({ input, ctx }) => - ctx.executeTransaction(async (handle) => ctx.attendanceService.getAttendanceById(handle, input.id)) - ), +export type AdminUpdateAtteendeeReservedInput = inferProcedureInput +export type AdminUpdateAtteendeeReservedOutput = inferProcedureOutput +const adminUpdateAtteendeeReservedProcedure = procedure + .input( + z.object({ + attendeeId: AttendeeSchema.shape.id, + reserved: AttendeeSchema.shape.reserved, + }) + ) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + await ctx.attendanceService.updateAttendeeById(ctx.handle, input.attendeeId, { reserved: input.reserved }) + }) - updateAttendance: staffProcedure - .input( - z.object({ - id: AttendanceSchema.shape.id, - attendance: AttendanceWriteSchema.partial(), - }) - ) - .mutation(async ({ input, ctx }) => - ctx.executeAuditedTransaction(async (handle) => - ctx.attendanceService.updateAttendanceById(handle, input.id, input.attendance) - ) - ), +export type RegisterAttendanceInput = inferProcedureInput +export type RegisterAttendanceOutput = inferProcedureOutput +const registerAttendanceProcedure = procedure + .input( + z.object({ + id: AttendeeSchema.shape.id, + at: z.coerce.date().nullable(), + }) + ) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + await ctx.attendanceService.registerAttendance(ctx.handle, input.id, input.at ? new TZDate(input.at) : null) + }) + +export type UpdateSelectionResponsesInput = inferProcedureInput +export type UpdateSelectionResponsesOutput = inferProcedureOutput +const updateSelectionResponsesProcedure = procedure + .input( + z.object({ + attendeeId: AttendeeSchema.shape.id, + options: AttendeeSelectionResponseSchema.array(), + }) + ) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => { + await ctx.attendanceService.updateAttendeeById(ctx.handle, input.attendeeId, { selections: input.options }) + }) + +export type GetAttendanceInput = inferProcedureInput +export type GetAttendanceOutput = inferProcedureOutput +const getAttendanceProcedure = procedure + .input(z.object({ id: AttendanceSchema.shape.id })) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => ctx.attendanceService.getAttendanceById(ctx.handle, input.id)) - findChargeAttendeeScheduleDate: authenticatedProcedure - .input(z.object({ attendeeId: AttendeeSchema.shape.id })) - .output(z.date().nullable()) - .query(async ({ input, ctx }) => { - return ctx.executeTransaction(async (handle) => { - const attendee = await ctx.attendanceService.getAttendeeById(handle, input.attendeeId) +export type UpdateAttendanceInput = inferProcedureInput +export type UpdateAttendanceOutput = inferProcedureOutput +const updateAttendanceProcedure = procedure + .input( + z.object({ + id: AttendanceSchema.shape.id, + attendance: AttendanceWriteSchema.partial(), + }) + ) + .use(withAuthentication()) + .use(withAuthorization(isEditor())) + .use(withDatabaseTransaction()) + .use(withAuditLogEntry()) + .mutation(async ({ input, ctx }) => + ctx.attendanceService.updateAttendanceById(ctx.handle, input.id, input.attendance) + ) - ctx.authorize.requireMeOrEditorRole(attendee.userId, ["dotkom", "hs"]) +export type FindChargeAttendeeScheduleDateInput = inferProcedureInput +export type FindChargeAttendeeScheduleDateOutput = inferProcedureOutput +const findChargeAttendeeScheduleDateProcedure = procedure + .input(z.object({ attendeeId: AttendeeSchema.shape.id })) + .output(z.date().nullable()) + .use(withAuthentication()) + .use(withDatabaseTransaction()) + .query(async ({ input, ctx }) => { + const attendee = await ctx.attendanceService.getAttendeeById(ctx.handle, input.attendeeId) - return await ctx.attendanceService.findChargeAttendeeScheduleDate(handle, attendee.id) - }) - }), + ctx.authorize.requireMeOrEditorRole(attendee.userId, ["dotkom", "hs"]) + + return await ctx.attendanceService.findChargeAttendeeScheduleDate(ctx.handle, attendee.id) + }) + +export const attendanceRouter = t.router({ + createPool: createPoolProcedure, + updatePool: updatePoolProcedure, + deletePool: deletePoolProcedure, + adminRegisterForEvent: adminRegisterForEventProcedure, + updateAttendancePayment: updateAttendancePaymentProcedure, + getSelectionsResults: getSelectionsResultsProcedure, + getRegistrationAvailability: getRegistrationAvailabilityProcedure, + registerForEvent: registerForEventProcedure, + onRegisterChange: onRegisterChangeProcedure, + cancelAttendeePayment: cancelAttendeePaymentProcedure, + startAttendeePayment: startAttendeePaymentProcedure, + deregisterForEvent: deregisterForEventProcedure, + adminDeregisterForEvent: adminDeregisterForEventProcedure, + adminUpdateAtteendeeReserved: adminUpdateAtteendeeReservedProcedure, + registerAttendance: registerAttendanceProcedure, + updateSelectionResponses: updateSelectionResponsesProcedure, + getAttendance: getAttendanceProcedure, + updateAttendance: updateAttendanceProcedure, + findChargeAttendeeScheduleDate: findChargeAttendeeScheduleDateProcedure, })