add : address,favorite
This commit is contained in:
@@ -18,8 +18,10 @@ This first phase covers:
|
||||
- wallet summary
|
||||
- wallet transaction list
|
||||
- active session list and revoke
|
||||
- address management
|
||||
- favorite products
|
||||
|
||||
Orders, addresses, favorites, and support tickets are not implemented yet in this phase.
|
||||
Support tickets and orders are not implemented yet in this phase.
|
||||
|
||||
## Endpoints
|
||||
|
||||
@@ -66,6 +68,7 @@ Response includes:
|
||||
- `stats.walletBalance`
|
||||
- `stats.walletTransactionsCount`
|
||||
- `stats.activeSessionsCount`
|
||||
- `stats.addressesCount`
|
||||
- `stats.ordersCount`
|
||||
- `stats.favoritesCount`
|
||||
- `stats.ticketsCount`
|
||||
@@ -74,7 +77,6 @@ Response includes:
|
||||
Current placeholder values:
|
||||
|
||||
- `ordersCount = 0`
|
||||
- `favoritesCount = 0`
|
||||
- `ticketsCount = 0`
|
||||
|
||||
These fields are placeholders until those modules are implemented.
|
||||
@@ -113,3 +115,75 @@ Session metadata is captured during register, login, OTP verify, and refresh.
|
||||
### `DELETE /api/users/me/sessions/:sessionId`
|
||||
|
||||
Revokes one active session for the current user.
|
||||
|
||||
## Address APIs
|
||||
|
||||
These endpoints support both user-panel address management and checkout address selection.
|
||||
|
||||
### `GET /api/users/me/addresses`
|
||||
|
||||
Returns all saved addresses for the current user.
|
||||
|
||||
The default address is returned first.
|
||||
|
||||
### `POST /api/users/me/addresses`
|
||||
|
||||
Creates a new address.
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "خانه",
|
||||
"recipientName": "علی رضایی",
|
||||
"phone": "+989121234567",
|
||||
"province": "فارس",
|
||||
"city": "شیراز",
|
||||
"postalCode": "7183914511",
|
||||
"addressLine": "بلوار معالی آباد، کوچه 12، ساختمان پارس",
|
||||
"plaque": "24",
|
||||
"unit": "3",
|
||||
"isDefault": true
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- each user can have multiple addresses
|
||||
- if this is the first address, it automatically becomes default
|
||||
- if `isDefault=true`, previous default address is cleared
|
||||
|
||||
### `PATCH /api/users/me/addresses/:addressId`
|
||||
|
||||
Updates one saved address.
|
||||
|
||||
Notes:
|
||||
|
||||
- `isDefault=true` makes this address the default
|
||||
- removing the only default address state is blocked
|
||||
|
||||
### `PATCH /api/users/me/addresses/:addressId/default`
|
||||
|
||||
Sets one address as the default address.
|
||||
|
||||
### `DELETE /api/users/me/addresses/:addressId`
|
||||
|
||||
Deletes one address.
|
||||
|
||||
If the deleted address was default, the newest remaining address becomes default automatically.
|
||||
|
||||
## Favorites APIs
|
||||
|
||||
### `GET /api/users/me/favorites`
|
||||
|
||||
Returns favorite products for the current user.
|
||||
|
||||
### `POST /api/users/me/favorites/:productId`
|
||||
|
||||
Adds one product to favorites.
|
||||
|
||||
If the product already exists in favorites, the API returns the existing favorite entry.
|
||||
|
||||
### `DELETE /api/users/me/favorites/:productId`
|
||||
|
||||
Removes one product from favorites.
|
||||
|
||||
@@ -26,6 +26,8 @@ import { SettingsModule } from './modules/settings/settings.module';
|
||||
import { StorageModule } from './modules/storage/storage.module';
|
||||
import { LoyaltyProfile } from './modules/users/entities/loyalty-profile.entity';
|
||||
import { User } from './modules/users/entities/user.entity';
|
||||
import { FavoriteProduct } from './modules/users/entities/favorite-product.entity';
|
||||
import { UserAddress } from './modules/users/entities/user-address.entity';
|
||||
import { UserLevelHistory } from './modules/users/entities/user-level-history.entity';
|
||||
import { WalletTransaction } from './modules/users/entities/wallet-transaction.entity';
|
||||
import { Wallet } from './modules/users/entities/wallet.entity';
|
||||
@@ -46,6 +48,8 @@ import { UsersModule } from './modules/users/users.module';
|
||||
WalletTransaction,
|
||||
LoyaltyProfile,
|
||||
UserLevelHistory,
|
||||
UserAddress,
|
||||
FavoriteProduct,
|
||||
AuthOtp,
|
||||
UserSession,
|
||||
Product,
|
||||
|
||||
@@ -13,6 +13,8 @@ import { Settings } from '../modules/settings/entities/settings.entity';
|
||||
import { MediaAsset } from '../modules/media/entities/media-asset.entity';
|
||||
import { LoyaltyProfile } from '../modules/users/entities/loyalty-profile.entity';
|
||||
import { User } from '../modules/users/entities/user.entity';
|
||||
import { FavoriteProduct } from '../modules/users/entities/favorite-product.entity';
|
||||
import { UserAddress } from '../modules/users/entities/user-address.entity';
|
||||
import { UserLevelHistory } from '../modules/users/entities/user-level-history.entity';
|
||||
import { WalletTransaction } from '../modules/users/entities/wallet-transaction.entity';
|
||||
import { Wallet } from '../modules/users/entities/wallet.entity';
|
||||
@@ -33,6 +35,8 @@ export const buildTypeOrmOptions = (
|
||||
WalletTransaction,
|
||||
LoyaltyProfile,
|
||||
UserLevelHistory,
|
||||
UserAddress,
|
||||
FavoriteProduct,
|
||||
AuthOtp,
|
||||
UserSession,
|
||||
Product,
|
||||
|
||||
63
src/modules/users/dto/create-user-address.dto.ts
Normal file
63
src/modules/users/dto/create-user-address.dto.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Length,
|
||||
Matches,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateUserAddressDto {
|
||||
@ApiProperty({ example: 'خانه' })
|
||||
@IsString()
|
||||
@Length(2, 100)
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: 'علی رضایی' })
|
||||
@IsString()
|
||||
@Length(2, 150)
|
||||
recipientName: string;
|
||||
|
||||
@ApiProperty({ example: '+989121234567' })
|
||||
@IsString()
|
||||
@Length(8, 20)
|
||||
phone: string;
|
||||
|
||||
@ApiProperty({ example: 'فارس' })
|
||||
@IsString()
|
||||
@Length(2, 100)
|
||||
province: string;
|
||||
|
||||
@ApiProperty({ example: 'شیراز' })
|
||||
@IsString()
|
||||
@Length(2, 100)
|
||||
city: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '7183914511' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^[0-9]{5,20}$/)
|
||||
postalCode?: string;
|
||||
|
||||
@ApiProperty({ example: 'بلوار معالی آباد، کوچه 12، ساختمان پارس' })
|
||||
@IsString()
|
||||
@Length(5, 500)
|
||||
addressLine: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '24' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(1, 50)
|
||||
plaque?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '3' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(1, 50)
|
||||
unit?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isDefault?: boolean;
|
||||
}
|
||||
4
src/modules/users/dto/update-user-address.dto.ts
Normal file
4
src/modules/users/dto/update-user-address.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateUserAddressDto } from './create-user-address.dto';
|
||||
|
||||
export class UpdateUserAddressDto extends PartialType(CreateUserAddressDto) {}
|
||||
31
src/modules/users/entities/favorite-product.entity.ts
Normal file
31
src/modules/users/entities/favorite-product.entity.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { Product } from '../../catalog/entities/product.entity';
|
||||
import { User } from './user.entity';
|
||||
|
||||
@Entity({ name: 'favorite_products' })
|
||||
@Unique('UQ_favorite_products_user_product', ['user', 'product'])
|
||||
export class FavoriteProduct {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@ManyToOne(() => User, (user) => user.favoriteProducts, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
|
||||
@Index()
|
||||
@ManyToOne(() => Product, { onDelete: 'CASCADE', eager: true })
|
||||
@JoinColumn({ name: 'product_id' })
|
||||
product: Product;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
}
|
||||
58
src/modules/users/entities/user-address.entity.ts
Normal file
58
src/modules/users/entities/user-address.entity.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
|
||||
@Entity({ name: 'user_addresses' })
|
||||
export class UserAddress {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@ManyToOne(() => User, (user) => user.addresses, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
|
||||
@Column({ length: 100 })
|
||||
title: string;
|
||||
|
||||
@Column({ name: 'recipient_name', length: 150 })
|
||||
recipientName: string;
|
||||
|
||||
@Column({ length: 20 })
|
||||
phone: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
province: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
city: string;
|
||||
|
||||
@Column({ name: 'postal_code', length: 20, nullable: true })
|
||||
postalCode?: string | null;
|
||||
|
||||
@Column({ name: 'address_line', type: 'varchar', length: 500 })
|
||||
addressLine: string;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
plaque?: string | null;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
unit?: string | null;
|
||||
|
||||
@Column({ type: 'boolean', name: 'is_default', default: false })
|
||||
isDefault: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
} from 'typeorm';
|
||||
import { UserSession } from '../../auth/entities/user-session.entity';
|
||||
import { UserRole } from '../enums/user-role.enum';
|
||||
import { FavoriteProduct } from './favorite-product.entity';
|
||||
import { LoyaltyProfile } from './loyalty-profile.entity';
|
||||
import { UserAddress } from './user-address.entity';
|
||||
import { Wallet } from './wallet.entity';
|
||||
|
||||
@Entity({ name: 'users' })
|
||||
@@ -50,6 +52,12 @@ export class User {
|
||||
@OneToMany(() => UserSession, (session) => session.user)
|
||||
sessions: UserSession[];
|
||||
|
||||
@OneToMany(() => UserAddress, (address) => address.user)
|
||||
addresses: UserAddress[];
|
||||
|
||||
@OneToMany(() => FavoriteProduct, (favorite) => favorite.user)
|
||||
favoriteProducts: FavoriteProduct[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
@@ -17,7 +18,9 @@ import {
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { JwtPayload } from '../auth/interfaces/jwt-payload.interface';
|
||||
import { CreateUserAddressDto } from './dto/create-user-address.dto';
|
||||
import { ListWalletTransactionsDto } from './dto/list-wallet-transactions.dto';
|
||||
import { UpdateUserAddressDto } from './dto/update-user-address.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@@ -78,4 +81,71 @@ export class UsersController {
|
||||
) {
|
||||
return this.usersService.revokeSession(request.user.sub, sessionId);
|
||||
}
|
||||
|
||||
@Get('me/addresses')
|
||||
@ApiOperation({ summary: 'List saved addresses for current user' })
|
||||
getAddresses(@Req() request: Request & { user: JwtPayload }) {
|
||||
return this.usersService.getAddresses(request.user.sub);
|
||||
}
|
||||
|
||||
@Post('me/addresses')
|
||||
@ApiOperation({ summary: 'Create a new address for current user' })
|
||||
createAddress(
|
||||
@Req() request: Request & { user: JwtPayload },
|
||||
@Body() dto: CreateUserAddressDto,
|
||||
) {
|
||||
return this.usersService.createAddress(request.user.sub, dto);
|
||||
}
|
||||
|
||||
@Patch('me/addresses/:addressId')
|
||||
@ApiOperation({ summary: 'Update one address for current user' })
|
||||
updateAddress(
|
||||
@Req() request: Request & { user: JwtPayload },
|
||||
@Param('addressId') addressId: string,
|
||||
@Body() dto: UpdateUserAddressDto,
|
||||
) {
|
||||
return this.usersService.updateAddress(request.user.sub, addressId, dto);
|
||||
}
|
||||
|
||||
@Patch('me/addresses/:addressId/default')
|
||||
@ApiOperation({ summary: 'Set one address as default for current user' })
|
||||
setDefaultAddress(
|
||||
@Req() request: Request & { user: JwtPayload },
|
||||
@Param('addressId') addressId: string,
|
||||
) {
|
||||
return this.usersService.setDefaultAddress(request.user.sub, addressId);
|
||||
}
|
||||
|
||||
@Delete('me/addresses/:addressId')
|
||||
@ApiOperation({ summary: 'Delete one address for current user' })
|
||||
deleteAddress(
|
||||
@Req() request: Request & { user: JwtPayload },
|
||||
@Param('addressId') addressId: string,
|
||||
) {
|
||||
return this.usersService.deleteAddress(request.user.sub, addressId);
|
||||
}
|
||||
|
||||
@Get('me/favorites')
|
||||
@ApiOperation({ summary: 'List favorite products for current user' })
|
||||
getFavorites(@Req() request: Request & { user: JwtPayload }) {
|
||||
return this.usersService.getFavorites(request.user.sub);
|
||||
}
|
||||
|
||||
@Post('me/favorites/:productId')
|
||||
@ApiOperation({ summary: 'Add one product to current user favorites' })
|
||||
addFavorite(
|
||||
@Req() request: Request & { user: JwtPayload },
|
||||
@Param('productId') productId: string,
|
||||
) {
|
||||
return this.usersService.addFavorite(request.user.sub, productId);
|
||||
}
|
||||
|
||||
@Delete('me/favorites/:productId')
|
||||
@ApiOperation({ summary: 'Remove one product from current user favorites' })
|
||||
removeFavorite(
|
||||
@Req() request: Request & { user: JwtPayload },
|
||||
@Param('productId') productId: string,
|
||||
) {
|
||||
return this.usersService.removeFavorite(request.user.sub, productId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserSession } from '../auth/entities/user-session.entity';
|
||||
import { Product } from '../catalog/entities/product.entity';
|
||||
import { FavoriteProduct } from './entities/favorite-product.entity';
|
||||
import { LoyaltyProfile } from './entities/loyalty-profile.entity';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UserAddress } from './entities/user-address.entity';
|
||||
import { UserLevelHistory } from './entities/user-level-history.entity';
|
||||
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
||||
import { Wallet } from './entities/wallet.entity';
|
||||
@@ -18,6 +21,9 @@ import { UsersService } from './users.service';
|
||||
LoyaltyProfile,
|
||||
UserLevelHistory,
|
||||
UserSession,
|
||||
UserAddress,
|
||||
FavoriteProduct,
|
||||
Product,
|
||||
]),
|
||||
],
|
||||
controllers: [UsersController],
|
||||
|
||||
@@ -6,10 +6,15 @@ import {
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { UserSession } from '../auth/entities/user-session.entity';
|
||||
import { Product } from '../catalog/entities/product.entity';
|
||||
import { CreateUserAddressDto } from './dto/create-user-address.dto';
|
||||
import { ListWalletTransactionsDto } from './dto/list-wallet-transactions.dto';
|
||||
import { UpdateUserAddressDto } from './dto/update-user-address.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { FavoriteProduct } from './entities/favorite-product.entity';
|
||||
import { LoyaltyProfile } from './entities/loyalty-profile.entity';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UserAddress } from './entities/user-address.entity';
|
||||
import { UserLevelHistory } from './entities/user-level-history.entity';
|
||||
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
||||
import { Wallet } from './entities/wallet.entity';
|
||||
@@ -31,6 +36,12 @@ export class UsersService {
|
||||
private readonly walletTransactionsRepository: Repository<WalletTransaction>,
|
||||
@InjectRepository(UserSession)
|
||||
private readonly userSessionsRepository: Repository<UserSession>,
|
||||
@InjectRepository(UserAddress)
|
||||
private readonly userAddressesRepository: Repository<UserAddress>,
|
||||
@InjectRepository(FavoriteProduct)
|
||||
private readonly favoriteProductsRepository: Repository<FavoriteProduct>,
|
||||
@InjectRepository(Product)
|
||||
private readonly productsRepository: Repository<Product>,
|
||||
) {}
|
||||
|
||||
findByPhone(phone: string) {
|
||||
@@ -132,7 +143,13 @@ export class UsersService {
|
||||
|
||||
async getDashboard(userId: string) {
|
||||
const user = await this.requireUser(userId);
|
||||
const [activeSessionsCount, walletTransactionsCount, loyaltyHistory] =
|
||||
const [
|
||||
activeSessionsCount,
|
||||
walletTransactionsCount,
|
||||
loyaltyHistory,
|
||||
favoritesCount,
|
||||
addressesCount,
|
||||
] =
|
||||
await Promise.all([
|
||||
this.userSessionsRepository.count({
|
||||
where: {
|
||||
@@ -150,6 +167,16 @@ export class UsersService {
|
||||
order: { createdAt: 'DESC' },
|
||||
take: 5,
|
||||
}),
|
||||
this.favoriteProductsRepository.count({
|
||||
where: {
|
||||
user: { id: user.id },
|
||||
},
|
||||
}),
|
||||
this.userAddressesRepository.count({
|
||||
where: {
|
||||
user: { id: user.id },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -158,8 +185,9 @@ export class UsersService {
|
||||
walletBalance: user.wallet.balance,
|
||||
walletTransactionsCount,
|
||||
activeSessionsCount,
|
||||
addressesCount,
|
||||
ordersCount: 0,
|
||||
favoritesCount: 0,
|
||||
favoritesCount,
|
||||
ticketsCount: 0,
|
||||
},
|
||||
loyalty: {
|
||||
@@ -276,6 +304,215 @@ export class UsersService {
|
||||
};
|
||||
}
|
||||
|
||||
async getAddresses(userId: string) {
|
||||
const addresses = await this.userAddressesRepository.find({
|
||||
where: { user: { id: userId } },
|
||||
order: { isDefault: 'DESC', createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return {
|
||||
items: addresses.map((address) => this.mapAddress(address)),
|
||||
};
|
||||
}
|
||||
|
||||
async createAddress(userId: string, dto: CreateUserAddressDto) {
|
||||
const user = await this.requireUser(userId);
|
||||
const currentAddressesCount = await this.userAddressesRepository.count({
|
||||
where: { user: { id: user.id } },
|
||||
});
|
||||
const shouldSetDefault = dto.isDefault === true || currentAddressesCount === 0;
|
||||
|
||||
if (shouldSetDefault) {
|
||||
await this.clearDefaultAddresses(user.id);
|
||||
}
|
||||
|
||||
const address = this.userAddressesRepository.create({
|
||||
user,
|
||||
title: dto.title,
|
||||
recipientName: dto.recipientName,
|
||||
phone: dto.phone,
|
||||
province: dto.province,
|
||||
city: dto.city,
|
||||
postalCode: dto.postalCode,
|
||||
addressLine: dto.addressLine,
|
||||
plaque: dto.plaque,
|
||||
unit: dto.unit,
|
||||
isDefault: shouldSetDefault,
|
||||
});
|
||||
|
||||
const savedAddress = await this.userAddressesRepository.save(address);
|
||||
|
||||
return this.mapAddress(savedAddress);
|
||||
}
|
||||
|
||||
async updateAddress(userId: string, addressId: string, dto: UpdateUserAddressDto) {
|
||||
const address = await this.requireAddress(userId, addressId);
|
||||
|
||||
if (dto.isDefault === true) {
|
||||
await this.clearDefaultAddresses(userId);
|
||||
address.isDefault = true;
|
||||
}
|
||||
|
||||
if (dto.title !== undefined) {
|
||||
address.title = dto.title;
|
||||
}
|
||||
if (dto.recipientName !== undefined) {
|
||||
address.recipientName = dto.recipientName;
|
||||
}
|
||||
if (dto.phone !== undefined) {
|
||||
address.phone = dto.phone;
|
||||
}
|
||||
if (dto.province !== undefined) {
|
||||
address.province = dto.province;
|
||||
}
|
||||
if (dto.city !== undefined) {
|
||||
address.city = dto.city;
|
||||
}
|
||||
if (dto.postalCode !== undefined) {
|
||||
address.postalCode = dto.postalCode;
|
||||
}
|
||||
if (dto.addressLine !== undefined) {
|
||||
address.addressLine = dto.addressLine;
|
||||
}
|
||||
if (dto.plaque !== undefined) {
|
||||
address.plaque = dto.plaque;
|
||||
}
|
||||
if (dto.unit !== undefined) {
|
||||
address.unit = dto.unit;
|
||||
}
|
||||
if (dto.isDefault === false) {
|
||||
const otherDefault = await this.userAddressesRepository.findOne({
|
||||
where: {
|
||||
user: { id: userId },
|
||||
isDefault: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!otherDefault || otherDefault.id === address.id) {
|
||||
throw new BadRequestException('User must have at least one default address');
|
||||
}
|
||||
|
||||
address.isDefault = false;
|
||||
}
|
||||
|
||||
const savedAddress = await this.userAddressesRepository.save(address);
|
||||
|
||||
return this.mapAddress(savedAddress);
|
||||
}
|
||||
|
||||
async setDefaultAddress(userId: string, addressId: string) {
|
||||
const address = await this.requireAddress(userId, addressId);
|
||||
await this.clearDefaultAddresses(userId);
|
||||
address.isDefault = true;
|
||||
const savedAddress = await this.userAddressesRepository.save(address);
|
||||
|
||||
return this.mapAddress(savedAddress);
|
||||
}
|
||||
|
||||
async deleteAddress(userId: string, addressId: string) {
|
||||
const address = await this.requireAddress(userId, addressId);
|
||||
const wasDefault = address.isDefault;
|
||||
|
||||
await this.userAddressesRepository.remove(address);
|
||||
|
||||
if (wasDefault) {
|
||||
const nextAddress = await this.userAddressesRepository.findOne({
|
||||
where: { user: { id: userId } },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
if (nextAddress) {
|
||||
nextAddress.isDefault = true;
|
||||
await this.userAddressesRepository.save(nextAddress);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Address deleted successfully',
|
||||
addressId,
|
||||
};
|
||||
}
|
||||
|
||||
async getFavorites(userId: string) {
|
||||
const favorites = await this.favoriteProductsRepository.find({
|
||||
where: { user: { id: userId } },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return {
|
||||
items: favorites.map((favorite) => ({
|
||||
id: favorite.id,
|
||||
createdAt: favorite.createdAt,
|
||||
product: this.mapFavoriteProduct(favorite.product),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async addFavorite(userId: string, productId: string) {
|
||||
const [user, product, existingFavorite] = await Promise.all([
|
||||
this.requireUser(userId),
|
||||
this.productsRepository.findOne({
|
||||
where: { id: productId },
|
||||
}),
|
||||
this.favoriteProductsRepository.findOne({
|
||||
where: {
|
||||
user: { id: userId },
|
||||
product: { id: productId },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new NotFoundException('Product not found');
|
||||
}
|
||||
|
||||
if (existingFavorite) {
|
||||
return {
|
||||
message: 'Product already exists in favorites',
|
||||
favorite: {
|
||||
id: existingFavorite.id,
|
||||
createdAt: existingFavorite.createdAt,
|
||||
product: this.mapFavoriteProduct(existingFavorite.product),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const favorite = this.favoriteProductsRepository.create({
|
||||
user,
|
||||
product,
|
||||
});
|
||||
const savedFavorite = await this.favoriteProductsRepository.save(favorite);
|
||||
|
||||
return {
|
||||
message: 'Product added to favorites successfully',
|
||||
favorite: {
|
||||
id: savedFavorite.id,
|
||||
createdAt: savedFavorite.createdAt,
|
||||
product: this.mapFavoriteProduct(savedFavorite.product),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async removeFavorite(userId: string, productId: string) {
|
||||
const favorite = await this.favoriteProductsRepository.findOne({
|
||||
where: {
|
||||
user: { id: userId },
|
||||
product: { id: productId },
|
||||
},
|
||||
});
|
||||
|
||||
if (!favorite) {
|
||||
throw new NotFoundException('Favorite product not found');
|
||||
}
|
||||
|
||||
await this.favoriteProductsRepository.remove(favorite);
|
||||
|
||||
return {
|
||||
message: 'Product removed from favorites successfully',
|
||||
productId,
|
||||
};
|
||||
}
|
||||
|
||||
private async requireUser(userId: string) {
|
||||
const user = await this.findById(userId);
|
||||
|
||||
@@ -286,6 +523,31 @@ export class UsersService {
|
||||
return user;
|
||||
}
|
||||
|
||||
private async requireAddress(userId: string, addressId: string) {
|
||||
const address = await this.userAddressesRepository.findOne({
|
||||
where: {
|
||||
id: addressId,
|
||||
user: { id: userId },
|
||||
},
|
||||
relations: { user: true },
|
||||
});
|
||||
|
||||
if (!address) {
|
||||
throw new NotFoundException('Address not found');
|
||||
}
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
private async clearDefaultAddresses(userId: string) {
|
||||
await this.userAddressesRepository
|
||||
.createQueryBuilder()
|
||||
.update(UserAddress)
|
||||
.set({ isDefault: false })
|
||||
.where('user_id = :userId', { userId })
|
||||
.execute();
|
||||
}
|
||||
|
||||
private mapProfile(user: User) {
|
||||
return {
|
||||
id: user.id,
|
||||
@@ -321,4 +583,37 @@ export class UsersService {
|
||||
createdAt: transaction.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
private mapAddress(address: UserAddress) {
|
||||
return {
|
||||
id: address.id,
|
||||
title: address.title,
|
||||
recipientName: address.recipientName,
|
||||
phone: address.phone,
|
||||
province: address.province,
|
||||
city: address.city,
|
||||
postalCode: address.postalCode,
|
||||
addressLine: address.addressLine,
|
||||
plaque: address.plaque,
|
||||
unit: address.unit,
|
||||
isDefault: address.isDefault,
|
||||
createdAt: address.createdAt,
|
||||
updatedAt: address.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private mapFavoriteProduct(product: Product) {
|
||||
return {
|
||||
id: product.id,
|
||||
title: product.title,
|
||||
slug: product.slug,
|
||||
brand: product.brand,
|
||||
mainImageUrl: product.mainImageUrl,
|
||||
stock: product.stock,
|
||||
status: product.status,
|
||||
type: product.type,
|
||||
averageRating: product.averageRating,
|
||||
reviewsCount: product.reviewsCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user