Merge pull request #191 from open-reception/187-FIX-Deny-has-no-endpoint

187 fix deny has no endpoint
This commit is contained in:
Karl Ludwig Weise
2026-02-03 11:00:26 +01:00
committed by GitHub
2 changed files with 235 additions and 0 deletions
@@ -0,0 +1,151 @@
import { json, type RequestHandler } from "@sveltejs/kit";
import { AppointmentService } from "$lib/server/services/appointment-service";
import { BackendError, InternalError, logError, ValidationError } from "$lib/server/utils/errors";
import { z } from "zod";
import logger from "$lib/logger";
import { registerOpenAPIRoute } from "$lib/server/openapi";
import { checkPermission } from "$lib/server/utils/permissions";
const requestSchema = z.object({
clientEmail: z.string().email().optional(),
clientLanguage: z.string().optional(),
});
// Register OpenAPI documentation for POST
registerOpenAPIRoute("/tenants/{id}/appointments/{appointmentId}/deny", "POST", {
summary: "Deny appointment",
description:
"Denies a pending appointment request. Updates the appointment status to REJECTED and sends a rejection notification email to the client.",
tags: ["Appointments", "Staff"],
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string", format: "uuid" },
description: "Tenant ID",
},
{
name: "appointmentId",
in: "path",
required: true,
schema: { type: "string", format: "uuid" },
description: "Appointment ID to deny",
},
],
requestBody: {
description: "Client information for rejection email",
content: {
"application/json": {
schema: {
type: "object",
properties: {
clientEmail: {
type: "string",
format: "email",
description: "Client email address for rejection notification",
},
clientLanguage: {
type: "string",
description: "Client preferred language (e.g., 'de', 'en')",
},
},
},
},
},
},
responses: {
"200": {
description: "Appointment denied successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
example: true,
},
},
},
},
},
},
"400": {
description: "Missing required parameters",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
"404": {
description: "Appointment not found or in wrong state",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
"500": {
description: "Internal server error",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
},
});
/**
* POST /api/tenants/[id]/appointments/[appointmentId]/deny
*
* Denies a pending appointment request. The appointment must be in NEW status.
* Sends a rejection email to the client and creates staff notifications.
*/
export const POST: RequestHandler = async ({ params, request, locals }) => {
const log = logger.setContext("API.DenyAppointment");
const tenantId = params.id;
const { appointmentId } = params;
try {
if (!tenantId) {
throw new ValidationError("Tenant ID is required");
}
if (!appointmentId) {
throw new ValidationError("appointmentId is required");
}
checkPermission(locals, tenantId);
const appointmentService = await AppointmentService.forTenant(tenantId);
const body = await request.json();
const { clientEmail, clientLanguage } = requestSchema.parse(body);
log.info("Denying appointment", {
tenantId,
appointmentId,
clientEmailPrefix: clientEmail ? clientEmail.slice(0, 3) : undefined,
});
await appointmentService.denyAppointment(appointmentId, clientEmail, clientLanguage);
log.info("Appointment denied successfully", {
tenantId,
appointmentId,
});
return json({ success: true });
} catch (error) {
logError(log)("Error denying appointment", error);
if (error instanceof BackendError) {
return error.toJson();
}
if (error instanceof z.ZodError) {
return new ValidationError("Invalid request data").toJson();
}
return new InternalError().toJson();
}
};
@@ -0,0 +1,84 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach } from "vitest";
import { POST } from "../+server";
import type { RequestEvent } from "@sveltejs/kit";
vi.mock("$lib/server/services/appointment-service", () => ({
AppointmentService: {
forTenant: vi.fn(),
},
}));
vi.mock("$lib/server/utils/permissions", () => ({
checkPermission: vi.fn(),
}));
vi.mock("$lib/logger", () => ({
default: {
setContext: vi.fn(() => ({
info: vi.fn(),
error: vi.fn(),
})),
},
}));
import { AppointmentService } from "$lib/server/services/appointment-service";
import { checkPermission } from "$lib/server/utils/permissions";
describe("POST /api/tenants/[id]/appointments/[appointmentId]/deny", () => {
const mockTenantId = "123e4567-e89b-12d3-a456-426614174000";
const mockAppointmentId = "456e7890-e12b-34d5-a678-901234567890";
const mockAppointmentService = {
denyAppointment: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
(AppointmentService.forTenant as any).mockResolvedValue(mockAppointmentService);
vi.mocked(checkPermission).mockImplementation(() => {});
});
function createMockRequestEvent(overrides: Partial<RequestEvent> = {}): RequestEvent {
return {
params: { id: mockTenantId, appointmentId: mockAppointmentId },
request: {
json: vi.fn().mockResolvedValue({ clientEmail: "test@example.com", clientLanguage: "de" }),
} as any,
locals: {
user: {
userId: "user123",
role: "TENANT_ADMIN",
tenantId: mockTenantId,
},
} as any,
...overrides,
} as RequestEvent;
}
it("should deny appointment", async () => {
mockAppointmentService.denyAppointment.mockResolvedValue(true);
const event = createMockRequestEvent();
const response = await POST(event);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(mockAppointmentService.denyAppointment).toHaveBeenCalledWith(
mockAppointmentId,
"test@example.com",
"de",
);
});
it("should return 400 if appointmentId is missing", async () => {
const event = createMockRequestEvent({ params: { id: mockTenantId } });
const response = await POST(event);
expect(response.status).toBe(422);
});
it("should handle service errors", async () => {
mockAppointmentService.denyAppointment.mockRejectedValue(new Error("fail"));
const event = createMockRequestEvent();
const response = await POST(event);
expect(response.status).toBe(500);
});
});