mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-08-17 21:25:52 +02:00
Merge pull request #191 from open-reception/187-FIX-Deny-has-no-endpoint
187 fix deny has no endpoint
This commit is contained in:
@@ -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();
|
||||
}
|
||||
};
|
||||
+84
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user