From 8a97b52eb74e8c43ab7152338c856f852715b90a Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:26:57 -0500 Subject: [PATCH] feat(services): add POST /services/block endpoint for slot blocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BlockSlotDto: validates day (ISO date) and range1_hour1 (HH:MM) - ServicesService.blockSlot(): finds professional by userId, checks for conflicts (any non-cancelled/denied service at that slot), then creates a service with status=self_booked using the professional's own user_id - ServicesController: POST /services/block (JWT-guarded) wired to blockSlot Existing PATCH /services/:id/status → cancelled handles unblocking since VALID_TRANSITIONS already allows self_booked → cancelled. Co-Authored-By: Claude Sonnet 4.6 --- backend/src/services/dto/service.dto.ts | 8 +++++++ backend/src/services/services.controller.ts | 9 ++++++- backend/src/services/services.service.ts | 26 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/backend/src/services/dto/service.dto.ts b/backend/src/services/dto/service.dto.ts index 7e3bc55..bd135bf 100644 --- a/backend/src/services/dto/service.dto.ts +++ b/backend/src/services/dto/service.dto.ts @@ -64,3 +64,11 @@ export class UpdateServiceStatusDto { @IsEnum(ServiceStatus) status: ServiceStatus; } + +export class BlockSlotDto { + @IsDateString() + day: string; + + @Matches(/^\d{2}:\d{2}$/) + range1_hour1: string; +} diff --git a/backend/src/services/services.controller.ts b/backend/src/services/services.controller.ts index e8cbf46..2afdf8b 100644 --- a/backend/src/services/services.controller.ts +++ b/backend/src/services/services.controller.ts @@ -2,7 +2,7 @@ import { Controller, Get, Post, Patch, Param, Body, UseGuards, Req, Query } from import { ApiTags, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; import { ServicesService } from './services.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; -import { CreateServiceDto, UpdateServiceStatusDto } from './dto/service.dto'; +import { CreateServiceDto, UpdateServiceStatusDto, BlockSlotDto } from './dto/service.dto'; @ApiTags('Services') @Controller('services') @@ -16,6 +16,13 @@ export class ServicesController { return this.services.create({ ...dto, user_id: req.user.sub }); } + @Post('block') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + blockSlot(@Req() req, @Body() dto: BlockSlotDto) { + return this.services.blockSlot(req.user.sub, dto); + } + @Get() @UseGuards(JwtAuthGuard) @ApiBearerAuth() diff --git a/backend/src/services/services.service.ts b/backend/src/services/services.service.ts index f96bf11..ed06c67 100644 --- a/backend/src/services/services.service.ts +++ b/backend/src/services/services.service.ts @@ -150,6 +150,32 @@ export class ServicesService { return updated; } + async blockSlot(userId: string, data: { day: string; range1_hour1: string }) { + const prof = await this.prisma.professionals.findUnique({ where: { user_id: userId } }); + if (!prof) throw new NotFoundException('No eres un profesional'); + + const slotTime = new Date(`1970-01-01T${data.range1_hour1}:00`); + const conflict = await this.prisma.services.findFirst({ + where: { + professional_id: prof.id, + day: new Date(data.day), + range1_hour1: slotTime, + status: { notIn: ['denied', 'cancelled'] }, + }, + }); + if (conflict) throw new BadRequestException('Este horario ya está ocupado o bloqueado'); + + return this.prisma.services.create({ + data: { + professional_id: prof.id, + user_id: prof.user_id, + day: new Date(data.day), + range1_hour1: slotTime, + status: 'self_booked' as any, + }, + }); + } + async findByUser(userId: string, page = 1, limit = 20) { const skip = (page - 1) * limit; const where = { user_id: userId, status: { notIn: ['completed' as const, 'cancelled' as const, 'denied' as const] } };