mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-26 19:04:50 +02:00
Deletion endpoint for staff
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { json } from "@sveltejs/kit";
|
||||
import { z } from "zod";
|
||||
import { AppointmentService } from "$lib/server/services/appointment-service";
|
||||
import { BackendError, InternalError, logError, ValidationError } from "$lib/server/utils/errors";
|
||||
import type { RequestHandler } from "@sveltejs/kit";
|
||||
import { registerOpenAPIRoute } from "$lib/server/openapi";
|
||||
import logger from "$lib/logger";
|
||||
import { checkPermission } from "$lib/server/utils/permissions";
|
||||
|
||||
const requestSchema = z.object({
|
||||
clientEmail: z.string().email(),
|
||||
clientLanguage: z.string().optional().default("de"),
|
||||
});
|
||||
|
||||
// Register OpenAPI documentation for DELETE
|
||||
registerOpenAPIRoute("/tenants/{id}/appointments/{appointmentId}", "DELETE", {
|
||||
summary: "Delete appointment",
|
||||
description:
|
||||
"Deletes an appointment, removing it from the database, sending a cancellation email to the client, and creating notifications for channel staff. Accessible to staff and tenant admins.",
|
||||
tags: ["Appointments"],
|
||||
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",
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
clientEmail: {
|
||||
type: "string",
|
||||
format: "email",
|
||||
description: "Email address of the client",
|
||||
example: "client@example.com",
|
||||
},
|
||||
clientLanguage: {
|
||||
type: "string",
|
||||
description: "Client's preferred language for the cancellation email",
|
||||
default: "de",
|
||||
example: "de",
|
||||
},
|
||||
},
|
||||
required: ["clientEmail"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Appointment deleted successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
message: { type: "string", description: "Success message" },
|
||||
},
|
||||
required: ["message"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"400": {
|
||||
description: "Invalid input data",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"401": {
|
||||
description: "Authentication required",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"403": {
|
||||
description: "Insufficient permissions",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"404": {
|
||||
description: "Appointment not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"500": {
|
||||
description: "Internal server error",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params, request, locals }) => {
|
||||
const log = logger.setContext("API");
|
||||
|
||||
try {
|
||||
const tenantId = params.id;
|
||||
const appointmentId = params.appointmentId;
|
||||
|
||||
// Check if user is authenticated
|
||||
if (!tenantId || !appointmentId) {
|
||||
throw new ValidationError("Tenant ID and appointment ID are required");
|
||||
}
|
||||
|
||||
checkPermission(locals, tenantId);
|
||||
|
||||
// Parse request body
|
||||
const body = await request.json();
|
||||
const validationResult = requestSchema.safeParse(body);
|
||||
|
||||
if (!validationResult.success) {
|
||||
throw new ValidationError("Invalid request body");
|
||||
}
|
||||
|
||||
const { clientEmail, clientLanguage } = validationResult.data;
|
||||
|
||||
log.debug("Deleting appointment", {
|
||||
tenantId,
|
||||
appointmentId,
|
||||
clientEmail,
|
||||
requestedBy: locals.user?.id,
|
||||
});
|
||||
|
||||
const appointmentService = await AppointmentService.forTenant(tenantId);
|
||||
await appointmentService.deleteAppointmentByStaff(appointmentId, clientEmail, clientLanguage);
|
||||
|
||||
log.debug("Appointment deleted successfully", {
|
||||
tenantId,
|
||||
appointmentId,
|
||||
requestedBy: locals.user?.id,
|
||||
});
|
||||
|
||||
return json({
|
||||
message: "Appointment deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logError(log)("Error deleting appointment", error, locals.user?.id, params.id);
|
||||
|
||||
if (error instanceof BackendError) {
|
||||
return error.toJson();
|
||||
}
|
||||
|
||||
return new InternalError().toJson();
|
||||
}
|
||||
};
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
||||
import { DELETE } from "../+server";
|
||||
import * as appointmentService from "$lib/server/services/appointment-service";
|
||||
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
|
||||
|
||||
// Mock the appointment service
|
||||
vi.mock("$lib/server/services/appointment-service", () => ({
|
||||
AppointmentService: {
|
||||
forTenant: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock permission check
|
||||
vi.mock("$lib/server/utils/permissions", () => ({
|
||||
checkPermission: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock logger
|
||||
vi.mock("$lib/logger", () => ({
|
||||
default: {
|
||||
setContext: vi.fn(() => ({
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("DELETE /api/tenants/[id]/appointments/[appointmentId]/delete", () => {
|
||||
const mockTenantId = "tenant-123";
|
||||
const mockAppointmentId = "appointment-456";
|
||||
const mockClientEmail = "client@example.com";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should delete appointment successfully with valid data", async () => {
|
||||
const mockDeleteAppointmentByStaff = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.mocked(appointmentService.AppointmentService.forTenant).mockResolvedValue({
|
||||
deleteAppointmentByStaff: mockDeleteAppointmentByStaff,
|
||||
} as any);
|
||||
|
||||
const request = new Request("http://localhost", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({
|
||||
clientEmail: mockClientEmail,
|
||||
clientLanguage: "de",
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await DELETE({
|
||||
params: { id: mockTenantId, appointmentId: mockAppointmentId },
|
||||
request,
|
||||
locals: { user: { id: "user-123" } },
|
||||
} as any);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(result).toEqual({
|
||||
message: "Appointment deleted successfully",
|
||||
});
|
||||
expect(mockDeleteAppointmentByStaff).toHaveBeenCalledWith(
|
||||
mockAppointmentId,
|
||||
mockClientEmail,
|
||||
"de",
|
||||
);
|
||||
});
|
||||
|
||||
it("should use default language when not provided", async () => {
|
||||
const mockDeleteAppointmentByStaff = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.mocked(appointmentService.AppointmentService.forTenant).mockResolvedValue({
|
||||
deleteAppointmentByStaff: mockDeleteAppointmentByStaff,
|
||||
} as any);
|
||||
|
||||
const request = new Request("http://localhost", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({
|
||||
clientEmail: mockClientEmail,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await DELETE({
|
||||
params: { id: mockTenantId, appointmentId: mockAppointmentId },
|
||||
request,
|
||||
locals: { user: { id: "user-123" } },
|
||||
} as any);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockDeleteAppointmentByStaff).toHaveBeenCalledWith(
|
||||
mockAppointmentId,
|
||||
mockClientEmail,
|
||||
"de",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return 422 when clientEmail is missing", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await DELETE({
|
||||
params: { id: mockTenantId, appointmentId: mockAppointmentId },
|
||||
request,
|
||||
locals: { user: { id: "user-123" } },
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
});
|
||||
|
||||
it("should return 422 when clientEmail is invalid", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({
|
||||
clientEmail: "invalid-email",
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await DELETE({
|
||||
params: { id: mockTenantId, appointmentId: mockAppointmentId },
|
||||
request,
|
||||
locals: { user: { id: "user-123" } },
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
});
|
||||
|
||||
it("should return 422 when tenantId is missing", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({
|
||||
clientEmail: mockClientEmail,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await DELETE({
|
||||
params: { appointmentId: mockAppointmentId },
|
||||
request,
|
||||
locals: { user: { id: "user-123" } },
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
});
|
||||
|
||||
it("should return 422 when appointmentId is missing", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({
|
||||
clientEmail: mockClientEmail,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await DELETE({
|
||||
params: { id: mockTenantId },
|
||||
request,
|
||||
locals: { user: { id: "user-123" } },
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
});
|
||||
|
||||
it("should return 404 when appointment is not found", async () => {
|
||||
const mockDeleteAppointmentByStaff = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new NotFoundError("Appointment not found"));
|
||||
|
||||
vi.mocked(appointmentService.AppointmentService.forTenant).mockResolvedValue({
|
||||
deleteAppointmentByStaff: mockDeleteAppointmentByStaff,
|
||||
} as any);
|
||||
|
||||
const request = new Request("http://localhost", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({
|
||||
clientEmail: mockClientEmail,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await DELETE({
|
||||
params: { id: mockTenantId, appointmentId: mockAppointmentId },
|
||||
request,
|
||||
locals: { user: { id: "user-123" } },
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it("should handle service errors gracefully", async () => {
|
||||
const mockDeleteAppointmentByStaff = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("Database connection failed"));
|
||||
|
||||
vi.mocked(appointmentService.AppointmentService.forTenant).mockResolvedValue({
|
||||
deleteAppointmentByStaff: mockDeleteAppointmentByStaff,
|
||||
} as any);
|
||||
|
||||
const request = new Request("http://localhost", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({
|
||||
clientEmail: mockClientEmail,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await DELETE({
|
||||
params: { id: mockTenantId, appointmentId: mockAppointmentId },
|
||||
request,
|
||||
locals: { user: { id: "user-123" } },
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user