feat(services): add POST /services/block endpoint for slot blocking

- 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 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-12 18:26:57 -05:00
co-authored by Claude Sonnet 4.6
parent c8b2c0c901
commit 8a97b52eb7
3 changed files with 42 additions and 1 deletions
+8
View File
@@ -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;
}
+8 -1
View File
@@ -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()
+26
View File
@@ -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] } };