diff --git a/src/modules/users/controllers/users.controller.ts b/src/modules/users/controllers/users.controller.ts index c866c04..844453c 100644 --- a/src/modules/users/controllers/users.controller.ts +++ b/src/modules/users/controllers/users.controller.ts @@ -1,9 +1,10 @@ -import { Controller, Get, UseGuards, Patch, Body, ValidationPipe, Post, Query } from '@nestjs/common'; +import { Controller, Get, UseGuards, Patch, Body, ValidationPipe, Post, Query, Delete, Param } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiOkResponse } from '@nestjs/swagger'; import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; import { UserService } from '../user.service'; import { UpdateUserDto } from '../dto/update-user.dto'; import { CreateUserAddressDto } from '../dto/create-user-address.dto'; +import { UpdateUserAddressDto } from '../dto/update-user-address.dto'; import { UserId } from 'src/common/decorators/user-id.decorator'; import { RestId } from 'src/common/decorators'; import { FindUsersDto } from '../dto/find-user.dto'; @@ -59,6 +60,51 @@ export class UsersController { return address; } + @UseGuards(AuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Get a single address by ID for the authenticated user' }) + @ApiOkResponse({ description: 'User address details' }) + @Get('public/user/addresses/:addressId') + async getUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) { + const address = await this.userService.getUserAddress(userId, addressId); + return address; + } + + @UseGuards(AuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Update an address for the authenticated user' }) + @ApiBody({ type: UpdateUserAddressDto }) + @ApiOkResponse({ description: 'Updated user address' }) + @Patch('public/user/addresses/:addressId') + async updateUserAddress( + @UserId() userId: string, + @Param('addressId') addressId: string, + @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserAddressDto, + ) { + const address = await this.userService.updateUserAddress(userId, addressId, dto); + return address; + } + + @UseGuards(AuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Delete an address for the authenticated user' }) + @ApiOkResponse({ description: 'Address deleted successfully' }) + @Delete('public/user/addresses/:addressId') + async deleteUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) { + await this.userService.deleteUserAddress(userId, addressId); + return { message: 'Address deleted successfully' }; + } + + @UseGuards(AuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Set an address as default for the authenticated user' }) + @ApiOkResponse({ description: 'Address set as default' }) + @Patch('public/user/addresses/:addressId/default') + async setDefaultAddress(@UserId() userId: string, @Param('addressId') addressId: string) { + const address = await this.userService.setDefaultAddress(userId, addressId); + return address; + } + @UseGuards(AdminAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get paginated list of restuarant users with filters' }) diff --git a/src/modules/users/dto/update-user-address.dto.ts b/src/modules/users/dto/update-user-address.dto.ts new file mode 100644 index 0000000..a1e98a2 --- /dev/null +++ b/src/modules/users/dto/update-user-address.dto.ts @@ -0,0 +1,57 @@ +import { IsString, IsOptional, IsNumber, IsBoolean, Min, Max } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +/** + * DTO for updating a user address. + */ +export class UpdateUserAddressDto { + @ApiPropertyOptional({ description: 'Address title/label (e.g., "Home", "Work")', example: 'Home' }) + @IsOptional() + @IsString() + title?: string; + + @ApiPropertyOptional({ description: 'Street address', example: '123 Main Street' }) + @IsOptional() + @IsString() + address?: string; + + @ApiPropertyOptional({ description: 'City name', example: 'Tehran' }) + @IsOptional() + @IsString() + city?: string; + + @ApiPropertyOptional({ description: 'Province', example: 'Tehran Province' }) + @IsOptional() + @IsString() + province?: string; + + @ApiPropertyOptional({ description: 'Postal/ZIP code', example: '12345-6789' }) + @IsOptional() + @IsString() + postalCode?: string; + + @ApiPropertyOptional({ description: 'Latitude coordinate', example: 35.6892, minimum: -90, maximum: 90 }) + @IsOptional() + @IsNumber() + @Min(-90) + @Max(90) + latitude?: number; + + @ApiPropertyOptional({ description: 'Longitude coordinate', example: 51.3890, minimum: -180, maximum: 180 }) + @IsOptional() + @IsNumber() + @Min(-180) + @Max(180) + longitude?: number; + + @ApiPropertyOptional({ description: 'Contact phone for this address', example: '+989123456789' }) + @IsOptional() + @IsString() + phone?: string; + + @ApiPropertyOptional({ description: 'Set as default address', example: false }) + @IsOptional() + @IsBoolean() + isDefault?: boolean; +} + diff --git a/src/modules/users/user.service.ts b/src/modules/users/user.service.ts index a583f17..71ffd3b 100644 --- a/src/modules/users/user.service.ts +++ b/src/modules/users/user.service.ts @@ -6,6 +6,7 @@ import { EntityManager } from '@mikro-orm/postgresql'; import { UpdateUserDto } from './dto/update-user.dto'; import { FindUsersDto } from './dto/find-user.dto'; import { CreateUserAddressDto } from './dto/create-user-address.dto'; +import { UpdateUserAddressDto } from './dto/update-user-address.dto'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { UserRepository } from './repositories/user.repository'; @@ -183,4 +184,89 @@ export class UserService { await this.em.persistAndFlush(address); return address; } + + async getUserAddress(userId: string, addressId: string): Promise { + const address = await this.em.findOne(UserAddress, { + id: addressId, + user: { id: userId }, + }); + + if (!address) { + throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); + } + + return address; + } + + async updateUserAddress(userId: string, addressId: string, dto: UpdateUserAddressDto): Promise { + const address = await this.em.findOne(UserAddress, { + id: addressId, + user: { id: userId }, + }); + + if (!address) { + throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); + } + + // If setting as default, unset other default addresses + if (dto.isDefault === true) { + const existingAddresses = await this.em.find(UserAddress, { + user: { id: userId }, + isDefault: true, + id: { $ne: addressId }, + }); + for (const existingAddress of existingAddresses) { + existingAddress.isDefault = false; + } + await this.em.flush(); + } + + // Update address fields + this.em.assign(address, dto); + await this.em.flush(); + + return address; + } + + async deleteUserAddress(userId: string, addressId: string): Promise { + const address = await this.em.findOne(UserAddress, { + id: addressId, + user: { id: userId }, + }); + + if (!address) { + throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); + } + + // Soft delete the address + address.deletedAt = new Date(); + await this.em.persistAndFlush(address); + } + + async setDefaultAddress(userId: string, addressId: string): Promise { + const address = await this.em.findOne(UserAddress, { + id: addressId, + user: { id: userId }, + }); + + if (!address) { + throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); + } + + // Unset all other default addresses + const existingAddresses = await this.em.find(UserAddress, { + user: { id: userId }, + isDefault: true, + id: { $ne: addressId }, + }); + for (const existingAddress of existingAddresses) { + existingAddress.isDefault = false; + } + + // Set this address as default + address.isDefault = true; + await this.em.flush(); + + return address; + } }