diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index c306ffb..db25c10 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -5,21 +5,32 @@ import { ApiProtectedEndpoint, ApiPublicEndpoint, } from '../swagger/decorators/swagger-auth.decorator'; +import { + ApiStandardController, + ApiStandardEmptyResponse, + ApiStandardMessageResponse, + ApiStandardOkResponse, + ApiStandardTooManyRequestsResponse, +} from '../swagger/decorators/swagger-response.decorator'; import { AuthService } from './auth.service'; import { CurrentUser } from './decorators/current-user.decorator'; import { Public } from './decorators/public.decorator'; +import { AuthTokensResponseDto } from './dto/auth-tokens-response.dto'; import { RefreshTokenDto } from './dto/refresh-token.dto'; import { RequestOtpDto } from './dto/request-otp.dto'; import { VerifyOtpDto } from './dto/verify-otp.dto'; import { AuthUser } from './interfaces/auth-user.interface'; @ApiTags('Auth') +@ApiStandardController() @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Public() @ApiPublicEndpoint('Request OTP code via SMS') + @ApiStandardMessageResponse('OTP sent') + @ApiStandardTooManyRequestsResponse() @Post('otp/request') requestOtp(@Body() dto: RequestOtpDto) { return this.authService.requestOtp(dto.mobile); @@ -27,6 +38,7 @@ export class AuthController { @Public() @ApiPublicEndpoint('Verify OTP and receive access/refresh tokens') + @ApiStandardOkResponse(AuthTokensResponseDto) @Post('otp/verify') verifyOtp(@Body() dto: VerifyOtpDto) { return this.authService.verifyOtp(dto); @@ -34,6 +46,7 @@ export class AuthController { @Public() @ApiPublicEndpoint('Refresh access token') + @ApiStandardOkResponse(AuthTokensResponseDto) @Post('refresh') refresh(@Body() dto: RefreshTokenDto) { return this.authService.refresh(dto.refreshToken); @@ -41,6 +54,7 @@ export class AuthController { @AllowWithoutSubscription() @ApiProtectedEndpoint('Logout and invalidate refresh token') + @ApiStandardEmptyResponse() @Post('logout') logout(@CurrentUser() user: AuthUser, @Body() dto: RefreshTokenDto) { return this.authService.logout(user.id, dto.refreshToken); diff --git a/src/auth/dto/auth-tokens-response.dto.ts b/src/auth/dto/auth-tokens-response.dto.ts new file mode 100644 index 0000000..8efac9d --- /dev/null +++ b/src/auth/dto/auth-tokens-response.dto.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { UserRole } from '../../users/enums/user-role.enum'; + +class AuthUserResponseDto { + @ApiProperty({ format: 'uuid' }) + id: string; + + @ApiProperty() + firstName: string; + + @ApiProperty() + lastName: string; + + @ApiProperty() + mobile: string; + + @ApiProperty({ enum: UserRole }) + role: UserRole; +} + +export class AuthTokensResponseDto { + @ApiProperty() + accessToken: string; + + @ApiProperty() + refreshToken: string; + + @ApiProperty({ type: AuthUserResponseDto }) + user: AuthUserResponseDto; +} diff --git a/src/common/dto/api-response.dto.ts b/src/common/dto/api-response.dto.ts index 95104ff..9cd017b 100644 --- a/src/common/dto/api-response.dto.ts +++ b/src/common/dto/api-response.dto.ts @@ -1,10 +1,4 @@ -import { applyDecorators, Type } from '@nestjs/common'; -import { - ApiExtraModels, - ApiOkResponse, - ApiProperty, - getSchemaPath, -} from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; export class ApiSuccessResponseDto { @ApiProperty({ example: true }) @@ -43,28 +37,3 @@ export class ApiErrorResponseDto { @ApiProperty({ example: '/api/users' }) path: string; } - -export const ApiStandardOkResponse = >( - model: TModel, - options?: { isArray?: boolean }, -) => - applyDecorators( - ApiExtraModels(ApiSuccessResponseDto, model), - ApiOkResponse({ - schema: { - allOf: [ - { $ref: getSchemaPath(ApiSuccessResponseDto) }, - { - properties: { - data: options?.isArray - ? { - type: 'array', - items: { $ref: getSchemaPath(model) }, - } - : { $ref: getSchemaPath(model) }, - }, - }, - ], - }, - }), - ); diff --git a/src/customers/customers.controller.ts b/src/customers/customers.controller.ts index 7b7dee7..27cd523 100644 --- a/src/customers/customers.controller.ts +++ b/src/customers/customers.controller.ts @@ -20,9 +20,16 @@ import { UserRole } from '../users/enums/user-role.enum'; import { CustomersService } from './customers.service'; import { UpdateCustomerDto } from './dto/update-customer.dto'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardEmptyResponse, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { Customer } from './entities/customer.entity'; @ApiTags('Customers') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('customers') @UseGuards(RolesGuard, PermissionsGuard) export class CustomersController { @@ -30,11 +37,13 @@ export class CustomersController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.CUSTOMERS_READ) + @ApiStandardOkResponse(Customer, { isArray: true }) @Get() findAll() { return this.customersService.findAll(); } + @ApiStandardOkResponse(Customer) @Get(':id') findOne( @Param('id', ParseUUIDPipe) id: string, @@ -43,6 +52,7 @@ export class CustomersController { return this.customersService.findOne(id, user); } + @ApiStandardOkResponse(Customer) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -54,6 +64,7 @@ export class CustomersController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.CUSTOMERS_DELETE) + @ApiStandardEmptyResponse() @Delete(':id') remove(@Param('id', ParseUUIDPipe) id: string) { return this.customersService.remove(id); diff --git a/src/reserves/reserves.controller.ts b/src/reserves/reserves.controller.ts index b93465e..23878bf 100644 --- a/src/reserves/reserves.controller.ts +++ b/src/reserves/reserves.controller.ts @@ -23,9 +23,16 @@ import { FindReservesQueryDto } from './dto/find-reserves-query.dto'; import { UpdateReserveDto } from './dto/update-reserve.dto'; import { ReservesService } from './reserves.service'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { ReserveStatusHistory } from './entities/reserve-status-history.entity'; +import { Reserve } from './entities/reserve.entity'; @ApiTags('Reserves') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('reserves') @UseGuards(RolesGuard, PermissionsGuard) export class ReservesController { @@ -33,6 +40,7 @@ export class ReservesController { @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) @Permissions(PermissionCode.RESERVES_WRITE) + @ApiStandardOkResponse(Reserve) @Post() create( @Body() createReserveDto: CreateReserveDto, @@ -43,6 +51,7 @@ export class ReservesController { @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) @Permissions(PermissionCode.RESERVES_READ) + @ApiStandardOkResponse(Reserve, { isArray: true }) @Get() findAll( @Query() query: FindReservesQueryDto, @@ -53,6 +62,7 @@ export class ReservesController { @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) @Permissions(PermissionCode.RESERVES_READ) + @ApiStandardOkResponse(ReserveStatusHistory, { isArray: true }) @Get(':id/status-history') findStatusHistory( @Param('id', ParseUUIDPipe) id: string, @@ -63,6 +73,7 @@ export class ReservesController { @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) @Permissions(PermissionCode.RESERVES_READ) + @ApiStandardOkResponse(Reserve) @Get(':id') findOne( @Param('id', ParseUUIDPipe) id: string, @@ -73,6 +84,7 @@ export class ReservesController { @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) @Permissions(PermissionCode.RESERVES_WRITE) + @ApiStandardOkResponse(Reserve) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/salons/salons.controller.ts b/src/salons/salons.controller.ts index 2f5c713..b00bf56 100644 --- a/src/salons/salons.controller.ts +++ b/src/salons/salons.controller.ts @@ -26,9 +26,16 @@ import { UpdateSalonDto } from './dto/update-salon.dto'; import { SalonsService } from './salons.service'; import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardEmptyResponse, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { Salon } from './entities/salon.entity'; @ApiTags('Salons') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('salons') @UseGuards(RolesGuard, PermissionsGuard) export class SalonsController { @@ -37,6 +44,7 @@ export class SalonsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SALONS_WRITE) @AllowWithoutSubscription() + @ApiStandardOkResponse(Salon) @Post() create( @Body() createSalonDto: CreateSalonDto, @@ -47,6 +55,7 @@ export class SalonsController { @Public() @ApiPublicEndpoint('List all salons') + @ApiStandardOkResponse(Salon, { isArray: true }) @Get() findAll(@OptionalCurrentUser() user?: AuthUser) { return this.salonsService.findAll(user); @@ -54,6 +63,7 @@ export class SalonsController { @Public() @ApiPublicEndpoint('Get salon by ID') + @ApiStandardOkResponse(Salon) @Get(':id') findOne(@Param('id', ParseUUIDPipe) id: string) { return this.salonsService.findOne(id); @@ -61,6 +71,7 @@ export class SalonsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SALONS_WRITE) + @ApiStandardOkResponse(Salon) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -72,6 +83,7 @@ export class SalonsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SALONS_DELETE) + @ApiStandardEmptyResponse() @Delete(':id') remove( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/sent-messages/sent-messages.controller.ts b/src/sent-messages/sent-messages.controller.ts index 26900e3..47a1648 100644 --- a/src/sent-messages/sent-messages.controller.ts +++ b/src/sent-messages/sent-messages.controller.ts @@ -20,15 +20,22 @@ import { FindSentMessagesQueryDto } from './dto/find-sent-messages-query.dto'; import { UpdateSentMessageDto } from './dto/update-sent-message.dto'; import { SentMessagesService } from './sent-messages.service'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { SentMessage } from './entities/sent-message.entity'; @ApiTags('Sent Messages') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('sent-messages') @UseGuards(RolesGuard) export class SentMessagesController { constructor(private readonly sentMessagesService: SentMessagesService) {} @Roles(UserRole.ADMIN, UserRole.SALON) + @ApiStandardOkResponse(SentMessage) @Post() create( @Body() createDto: CreateSentMessageDto, @@ -38,6 +45,7 @@ export class SentMessagesController { } @Roles(UserRole.ADMIN, UserRole.SALON) + @ApiStandardOkResponse(SentMessage, { isArray: true }) @Get() findAll( @Query() query: FindSentMessagesQueryDto, @@ -47,6 +55,7 @@ export class SentMessagesController { } @Roles(UserRole.ADMIN, UserRole.SALON) + @ApiStandardOkResponse(SentMessage) @Get(':id') findOne( @Param('id', ParseUUIDPipe) id: string, @@ -56,6 +65,7 @@ export class SentMessagesController { } @Roles(UserRole.ADMIN, UserRole.SALON) + @ApiStandardOkResponse(SentMessage) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/services/services.controller.ts b/src/services/services.controller.ts index 1e05e5a..c920467 100644 --- a/src/services/services.controller.ts +++ b/src/services/services.controller.ts @@ -26,9 +26,17 @@ import { UpdateServiceDto } from './dto/update-service.dto'; import { ServicesService } from './services.service'; import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardEmptyResponse, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { ServiceSkill } from './entities/service-skill.entity'; +import { Service } from './entities/service.entity'; @ApiTags('Services') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('services') @UseGuards(RolesGuard, PermissionsGuard) export class ServicesController { @@ -36,6 +44,7 @@ export class ServicesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_WRITE) + @ApiStandardOkResponse(Service) @Post() create( @Body() createServiceDto: CreateServiceDto, @@ -46,6 +55,7 @@ export class ServicesController { @Public() @ApiPublicEndpoint('List all services') + @ApiStandardOkResponse(Service, { isArray: true }) @Get() findAll() { return this.servicesService.findAll(); @@ -53,6 +63,7 @@ export class ServicesController { @Public() @ApiPublicEndpoint('Get service by ID') + @ApiStandardOkResponse(Service) @Get(':id') findOne(@Param('id', ParseUUIDPipe) id: string) { return this.servicesService.findOne(id); @@ -60,6 +71,7 @@ export class ServicesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_WRITE) + @ApiStandardOkResponse(Service) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -71,6 +83,7 @@ export class ServicesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_DELETE) + @ApiStandardEmptyResponse() @Delete(':id') remove( @Param('id', ParseUUIDPipe) id: string, @@ -81,6 +94,7 @@ export class ServicesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_WRITE) + @ApiStandardOkResponse(ServiceSkill) @Post(':serviceId/skills') createSkill( @Param('serviceId', ParseUUIDPipe) serviceId: string, @@ -96,6 +110,7 @@ export class ServicesController { @Public() @ApiPublicEndpoint('List skills for a service') + @ApiStandardOkResponse(ServiceSkill, { isArray: true }) @Get(':serviceId/skills') findSkills(@Param('serviceId', ParseUUIDPipe) serviceId: string) { return this.servicesService.findSkills(serviceId); @@ -103,6 +118,7 @@ export class ServicesController { @Public() @ApiPublicEndpoint('Get a service skill by ID') + @ApiStandardOkResponse(ServiceSkill) @Get(':serviceId/skills/:skillId') findSkill( @Param('serviceId', ParseUUIDPipe) serviceId: string, @@ -113,6 +129,7 @@ export class ServicesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_WRITE) + @ApiStandardOkResponse(ServiceSkill) @Patch(':serviceId/skills/:skillId') updateSkill( @Param('serviceId', ParseUUIDPipe) serviceId: string, @@ -130,6 +147,7 @@ export class ServicesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_DELETE) + @ApiStandardEmptyResponse() @Delete(':serviceId/skills/:skillId') removeSkill( @Param('serviceId', ParseUUIDPipe) serviceId: string, diff --git a/src/sms-templates/sms-templates.controller.ts b/src/sms-templates/sms-templates.controller.ts index 99f363c..54f59b7 100644 --- a/src/sms-templates/sms-templates.controller.ts +++ b/src/sms-templates/sms-templates.controller.ts @@ -24,9 +24,16 @@ import { FindSmsTemplatesQueryDto } from './dto/find-sms-templates-query.dto'; import { UpdateSmsTemplateDto } from './dto/update-sms-template.dto'; import { SmsTemplatesService } from './sms-templates.service'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardEmptyResponse, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { SmsTemplate } from './entities/sms-template.entity'; @ApiTags('SMS Templates') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('sms-templates') @UseGuards(RolesGuard, PermissionsGuard) export class SmsTemplatesController { @@ -34,6 +41,7 @@ export class SmsTemplatesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SMS_TEMPLATES_WRITE) + @ApiStandardOkResponse(SmsTemplate) @Post() create( @Body() createDto: CreateSmsTemplateDto, @@ -44,6 +52,7 @@ export class SmsTemplatesController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SMS_TEMPLATES_READ) + @ApiStandardOkResponse(SmsTemplate, { isArray: true }) @Get() findAll(@Query() query: FindSmsTemplatesQueryDto) { return this.smsTemplatesService.findAll(query); @@ -51,6 +60,7 @@ export class SmsTemplatesController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SMS_TEMPLATES_READ) + @ApiStandardOkResponse(SmsTemplate) @Get(':id') findOne(@Param('id', ParseUUIDPipe) id: string) { return this.smsTemplatesService.findOne(id); @@ -58,6 +68,7 @@ export class SmsTemplatesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SMS_TEMPLATES_WRITE) + @ApiStandardOkResponse(SmsTemplate) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -69,6 +80,7 @@ export class SmsTemplatesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SMS_TEMPLATES_WRITE) + @ApiStandardEmptyResponse() @Delete(':id') remove( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/stylists/stylists.controller.ts b/src/stylists/stylists.controller.ts index 6bc2a78..edbdb62 100644 --- a/src/stylists/stylists.controller.ts +++ b/src/stylists/stylists.controller.ts @@ -25,9 +25,16 @@ import { UpdateStylistDto } from './dto/update-stylist.dto'; import { StylistsService } from './stylists.service'; import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardEmptyResponse, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { Stylist } from './entities/stylist.entity'; @ApiTags('Stylists') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('stylists') @UseGuards(RolesGuard, PermissionsGuard) export class StylistsController { @@ -35,6 +42,7 @@ export class StylistsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.STYLISTS_WRITE) + @ApiStandardOkResponse(Stylist) @Post() create( @Body() createStylistDto: CreateStylistDto, @@ -46,6 +54,7 @@ export class StylistsController { @Public() @ApiPublicEndpoint('List stylists, optionally filtered by salon') @ApiQuery({ name: 'salonId', required: false, type: String, format: 'uuid' }) + @ApiStandardOkResponse(Stylist, { isArray: true }) @Get() findAll(@Query('salonId', new ParseUUIDPipe({ optional: true })) salonId?: string) { return this.stylistsService.findAll(salonId); @@ -53,6 +62,7 @@ export class StylistsController { @Public() @ApiPublicEndpoint('Get stylist by ID') + @ApiStandardOkResponse(Stylist) @Get(':id') findOne(@Param('id', ParseUUIDPipe) id: string) { return this.stylistsService.findOne(id); @@ -60,6 +70,7 @@ export class StylistsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.STYLISTS_WRITE) + @ApiStandardOkResponse(Stylist) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -71,6 +82,7 @@ export class StylistsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.STYLISTS_DELETE) + @ApiStandardEmptyResponse() @Delete(':id') remove( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/subscriptions/dto/salon-sms-balance.dto.ts b/src/subscriptions/dto/salon-sms-balance.dto.ts new file mode 100644 index 0000000..bf09343 --- /dev/null +++ b/src/subscriptions/dto/salon-sms-balance.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SalonSmsBalanceDto { + @ApiProperty({ example: 100 }) + freeSmsRemaining: number; + + @ApiProperty({ example: 250 }) + purchasedSmsRemaining: number; + + @ApiProperty({ example: 350 }) + totalRemaining: number; +} diff --git a/src/subscriptions/salon-subscriptions.controller.ts b/src/subscriptions/salon-subscriptions.controller.ts index f336465..a8f93ab 100644 --- a/src/subscriptions/salon-subscriptions.controller.ts +++ b/src/subscriptions/salon-subscriptions.controller.ts @@ -21,9 +21,17 @@ import { PurchaseSmsPackageDto } from './dto/purchase-sms-package.dto'; import { PurchaseSubscriptionDto } from './dto/purchase-subscription.dto'; import { SalonSubscriptionsService } from './salon-subscriptions.service'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { SalonSmsBalanceDto } from './dto/salon-sms-balance.dto'; +import { SalonSmsPurchase } from './entities/salon-sms-purchase.entity'; +import { SalonSubscription } from './entities/salon-subscription.entity'; @ApiTags('Salon Subscriptions') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('salon-subscriptions') @UseGuards(RolesGuard, PermissionsGuard) export class SalonSubscriptionsController { @@ -34,6 +42,7 @@ export class SalonSubscriptionsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SUBSCRIPTIONS_WRITE) @AllowWithoutSubscription() + @ApiStandardOkResponse(SalonSubscription) @Post('purchase') purchaseSubscription( @Body() dto: PurchaseSubscriptionDto, @@ -44,6 +53,7 @@ export class SalonSubscriptionsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SUBSCRIPTIONS_WRITE) + @ApiStandardOkResponse(SalonSmsPurchase) @Post('sms/purchase') purchaseSmsPackage( @Body() dto: PurchaseSmsPackageDto, @@ -55,6 +65,7 @@ export class SalonSubscriptionsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SUBSCRIPTIONS_READ) @AllowWithoutSubscription() + @ApiStandardOkResponse(SalonSubscription, { isArray: true }) @Get('salon/:salonId') findAllForSalon( @Param('salonId', ParseUUIDPipe) salonId: string, @@ -66,6 +77,7 @@ export class SalonSubscriptionsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SUBSCRIPTIONS_READ) @AllowWithoutSubscription() + @ApiStandardOkResponse(SalonSubscription, { nullable: true }) @Get('salon/:salonId/active') findActiveForSalon( @Param('salonId', ParseUUIDPipe) salonId: string, @@ -77,6 +89,7 @@ export class SalonSubscriptionsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SUBSCRIPTIONS_READ) @AllowWithoutSubscription() + @ApiStandardOkResponse(SalonSmsBalanceDto) @Get('salon/:salonId/sms-balance') getSmsBalance( @Param('salonId', ParseUUIDPipe) salonId: string, @@ -87,6 +100,7 @@ export class SalonSubscriptionsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SUBSCRIPTIONS_READ) + @ApiStandardOkResponse(SalonSmsPurchase, { isArray: true }) @Get('salon/:salonId/sms-purchases') findSmsPurchasesForSalon( @Param('salonId', ParseUUIDPipe) salonId: string, @@ -101,6 +115,7 @@ export class SalonSubscriptionsController { @Roles(UserRole.ADMIN, UserRole.SALON) @Permissions(PermissionCode.SUBSCRIPTIONS_READ) @AllowWithoutSubscription() + @ApiStandardOkResponse(SalonSubscription) @Get(':id') findOne( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/subscriptions/sms-packages.controller.ts b/src/subscriptions/sms-packages.controller.ts index ba42973..a95fb99 100644 --- a/src/subscriptions/sms-packages.controller.ts +++ b/src/subscriptions/sms-packages.controller.ts @@ -23,9 +23,16 @@ import { UpdateSmsPackageDto } from './dto/update-sms-package.dto'; import { SmsPackagesService } from './sms-packages.service'; import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardEmptyResponse, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { SmsPackage } from './entities/sms-package.entity'; @ApiTags('SMS Packages') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('sms-packages') @UseGuards(RolesGuard, PermissionsGuard) export class SmsPackagesController { @@ -33,6 +40,7 @@ export class SmsPackagesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SUBSCRIPTIONS_WRITE) + @ApiStandardOkResponse(SmsPackage) @Post() create(@Body() dto: CreateSmsPackageDto) { return this.smsPackagesService.create(dto); @@ -41,6 +49,7 @@ export class SmsPackagesController { @Public() @AllowWithoutSubscription() @ApiPublicEndpoint('List active SMS packages') + @ApiStandardOkResponse(SmsPackage, { isArray: true }) @Get() findAll() { return this.smsPackagesService.findAll(true); @@ -49,6 +58,7 @@ export class SmsPackagesController { @Public() @AllowWithoutSubscription() @ApiPublicEndpoint('Get SMS package by ID') + @ApiStandardOkResponse(SmsPackage) @Get(':id') findOne(@Param('id', ParseUUIDPipe) id: string) { return this.smsPackagesService.findOne(id); @@ -56,6 +66,7 @@ export class SmsPackagesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SUBSCRIPTIONS_WRITE) + @ApiStandardOkResponse(SmsPackage) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -66,6 +77,7 @@ export class SmsPackagesController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SUBSCRIPTIONS_WRITE) + @ApiStandardEmptyResponse() @Delete(':id') remove(@Param('id', ParseUUIDPipe) id: string) { return this.smsPackagesService.remove(id); diff --git a/src/subscriptions/subscription-plans.controller.ts b/src/subscriptions/subscription-plans.controller.ts index 907dc0d..a62a6b5 100644 --- a/src/subscriptions/subscription-plans.controller.ts +++ b/src/subscriptions/subscription-plans.controller.ts @@ -25,9 +25,16 @@ import { UpdateSubscriptionPlanDto } from './dto/update-subscription-plan.dto'; import { SubscriptionPlansService } from './subscription-plans.service'; import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardEmptyResponse, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { SubscriptionPlan } from './entities/subscription-plan.entity'; @ApiTags('Subscription Plans') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('subscription-plans') @UseGuards(RolesGuard, PermissionsGuard) export class SubscriptionPlansController { @@ -37,6 +44,7 @@ export class SubscriptionPlansController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SUBSCRIPTIONS_WRITE) + @ApiStandardOkResponse(SubscriptionPlan) @Post() create(@Body() dto: CreateSubscriptionPlanDto) { return this.subscriptionPlansService.create(dto); @@ -45,6 +53,7 @@ export class SubscriptionPlansController { @Public() @AllowWithoutSubscription() @ApiPublicEndpoint('List active subscription plans') + @ApiStandardOkResponse(SubscriptionPlan, { isArray: true }) @Get() findAll() { return this.subscriptionPlansService.findAll(true); @@ -53,6 +62,7 @@ export class SubscriptionPlansController { @Public() @AllowWithoutSubscription() @ApiPublicEndpoint('Get subscription plan by ID') + @ApiStandardOkResponse(SubscriptionPlan) @Get(':id') findOne(@Param('id', ParseUUIDPipe) id: string) { return this.subscriptionPlansService.findOne(id); @@ -60,6 +70,7 @@ export class SubscriptionPlansController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SUBSCRIPTIONS_WRITE) + @ApiStandardOkResponse(SubscriptionPlan) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -70,6 +81,7 @@ export class SubscriptionPlansController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SUBSCRIPTIONS_WRITE) + @ApiStandardEmptyResponse() @Delete(':id') remove(@Param('id', ParseUUIDPipe) id: string) { return this.subscriptionPlansService.remove(id); diff --git a/src/suggestions/suggestions.controller.ts b/src/suggestions/suggestions.controller.ts index 678a64a..e426ba7 100644 --- a/src/suggestions/suggestions.controller.ts +++ b/src/suggestions/suggestions.controller.ts @@ -26,9 +26,16 @@ import { UpdateServiceSuggestionDto } from './dto/update-service-suggestion.dto' import { SuggestionsService } from './suggestions.service'; import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardEmptyResponse, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { ServiceSuggestion } from './entities/service-suggestion.entity'; @ApiTags('Suggestions') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('suggestions') @UseGuards(RolesGuard, PermissionsGuard) export class SuggestionsController { @@ -36,6 +43,7 @@ export class SuggestionsController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_WRITE) + @ApiStandardOkResponse(ServiceSuggestion) @Post() create( @Body() createDto: CreateServiceSuggestionDto, @@ -46,6 +54,7 @@ export class SuggestionsController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_READ) + @ApiStandardOkResponse(ServiceSuggestion, { isArray: true }) @Get() findAll(@Query() query: FindSuggestionsQueryDto) { return this.suggestionsService.findAll(query); @@ -53,6 +62,7 @@ export class SuggestionsController { @Public() @ApiPublicEndpoint('Get suggestions for a service') + @ApiStandardOkResponse(ServiceSuggestion, { isArray: true }) @Get('for-service/:serviceId') findForService(@Param('serviceId', ParseUUIDPipe) serviceId: string) { return this.suggestionsService.findForService(serviceId); @@ -60,6 +70,7 @@ export class SuggestionsController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_READ) + @ApiStandardOkResponse(ServiceSuggestion) @Get(':id') findOne(@Param('id', ParseUUIDPipe) id: string) { return this.suggestionsService.findOne(id); @@ -67,6 +78,7 @@ export class SuggestionsController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_WRITE) + @ApiStandardOkResponse(ServiceSuggestion) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -78,6 +90,7 @@ export class SuggestionsController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.SERVICES_DELETE) + @ApiStandardEmptyResponse() @Delete(':id') remove( @Param('id', ParseUUIDPipe) id: string, diff --git a/src/swagger/decorators/swagger-response.decorator.ts b/src/swagger/decorators/swagger-response.decorator.ts new file mode 100644 index 0000000..0b37666 --- /dev/null +++ b/src/swagger/decorators/swagger-response.decorator.ts @@ -0,0 +1,125 @@ +import { applyDecorators, Type } from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiExtraModels, + ApiForbiddenResponse, + ApiInternalServerErrorResponse, + ApiNotFoundResponse, + ApiOkResponse, + ApiTooManyRequestsResponse, + ApiUnauthorizedResponse, + getSchemaPath, +} from '@nestjs/swagger'; +import { + ApiErrorResponseDto, + ApiSuccessResponseDto, +} from '../../common/dto/api-response.dto'; + +export function ApiStandardController() { + return applyDecorators( + ApiExtraModels(ApiSuccessResponseDto, ApiErrorResponseDto), + ApiStandardErrorResponses(), + ); +} + +export function ApiStandardErrorResponses() { + return applyDecorators( + ApiBadRequestResponse({ + description: 'Validation failed or bad request', + type: ApiErrorResponseDto, + }), + ApiUnauthorizedResponse({ + description: 'Unauthorized', + type: ApiErrorResponseDto, + }), + ApiForbiddenResponse({ + description: 'Forbidden', + type: ApiErrorResponseDto, + }), + ApiNotFoundResponse({ + description: 'Resource not found', + type: ApiErrorResponseDto, + }), + ApiInternalServerErrorResponse({ + description: 'Internal server error', + type: ApiErrorResponseDto, + }), + ); +} + +export const ApiStandardOkResponse = >( + model: TModel, + options?: { isArray?: boolean; nullable?: boolean }, +) => + applyDecorators( + ApiExtraModels(ApiSuccessResponseDto, model), + ApiOkResponse({ + description: 'Successful response', + schema: { + allOf: [ + { $ref: getSchemaPath(ApiSuccessResponseDto) }, + { + properties: { + data: options?.nullable + ? { type: 'null', nullable: true } + : options?.isArray + ? { + type: 'array', + items: { $ref: getSchemaPath(model) }, + } + : { $ref: getSchemaPath(model) }, + }, + }, + ], + }, + }), + ); + +export function ApiStandardEmptyResponse() { + return applyDecorators( + ApiExtraModels(ApiSuccessResponseDto), + ApiOkResponse({ + description: 'Successful response with no data', + schema: { + allOf: [ + { $ref: getSchemaPath(ApiSuccessResponseDto) }, + { + properties: { + data: { type: 'null', nullable: true }, + }, + }, + ], + }, + }), + ); +} + +export function ApiStandardMessageResponse(example = 'Operation completed') { + return applyDecorators( + ApiExtraModels(ApiSuccessResponseDto), + ApiOkResponse({ + description: 'Successful response with message only', + schema: { + allOf: [ + { $ref: getSchemaPath(ApiSuccessResponseDto) }, + { + properties: { + message: { type: 'string', example }, + data: { type: 'null', nullable: true }, + }, + }, + ], + }, + }), + ); +} + +export function ApiStandardTooManyRequestsResponse() { + return applyDecorators( + ApiExtraModels(ApiErrorResponseDto), + ApiTooManyRequestsResponse({ + description: 'Too many requests', + type: ApiErrorResponseDto, + }), + ); +} diff --git a/src/swagger/swagger.setup.ts b/src/swagger/swagger.setup.ts index 3a4d865..67245a7 100644 --- a/src/swagger/swagger.setup.ts +++ b/src/swagger/swagger.setup.ts @@ -1,12 +1,21 @@ import { INestApplication } from '@nestjs/common'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { + ApiErrorResponseDto, + ApiSuccessResponseDto, +} from '../common/dto/api-response.dto'; export const SWAGGER_JWT_AUTH = 'access-token'; export function setupSwagger(app: INestApplication): void { const config = new DocumentBuilder() .setTitle('Grow API') - .setDescription('BeautyBook Grow API documentation') + .setDescription( + 'BeautyBook Grow API documentation.\n\n' + + 'All API responses follow a standard envelope:\n' + + '- Success: `{ success: true, data, message?, meta? }`\n' + + '- Error: `{ success: false, statusCode, message, errors?, timestamp, path }`', + ) .setVersion('1.0') .addBearerAuth( { @@ -19,7 +28,9 @@ export function setupSwagger(app: INestApplication): void { ) .build(); - const document = SwaggerModule.createDocument(app, config); + const document = SwaggerModule.createDocument(app, config, { + extraModels: [ApiSuccessResponseDto, ApiErrorResponseDto], + }); SwaggerModule.setup('docs', app, document, { jsonDocumentUrl: 'docs-json', diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 10f166a..6a41d32 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -23,9 +23,16 @@ import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { UsersService } from './users.service'; import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup'; +import { + ApiStandardController, + ApiStandardEmptyResponse, + ApiStandardOkResponse, +} from '../swagger/decorators/swagger-response.decorator'; +import { User } from './entities/user.entity'; @ApiTags('Users') @ApiBearerAuth(SWAGGER_JWT_AUTH) +@ApiStandardController() @Controller('users') @UseGuards(RolesGuard, PermissionsGuard) export class UsersController { @@ -33,6 +40,7 @@ export class UsersController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.USERS_WRITE) + @ApiStandardOkResponse(User) @Post() create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); @@ -40,11 +48,13 @@ export class UsersController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.USERS_READ) + @ApiStandardOkResponse(User, { isArray: true }) @Get() findAll() { return this.usersService.findAll(); } + @ApiStandardOkResponse(User) @Get(':id') findOne( @Param('id', ParseUUIDPipe) id: string, @@ -56,6 +66,7 @@ export class UsersController { return this.usersService.findOne(id); } + @ApiStandardOkResponse(User) @Patch(':id') update( @Param('id', ParseUUIDPipe) id: string, @@ -70,6 +81,7 @@ export class UsersController { @Roles(UserRole.ADMIN) @Permissions(PermissionCode.USERS_DELETE) + @ApiStandardEmptyResponse() @Delete(':id') remove(@Param('id', ParseUUIDPipe) id: string) { return this.usersService.remove(id);