add : address,favorite
This commit is contained in:
@@ -18,8 +18,10 @@ This first phase covers:
|
|||||||
- wallet summary
|
- wallet summary
|
||||||
- wallet transaction list
|
- wallet transaction list
|
||||||
- active session list and revoke
|
- 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
|
## Endpoints
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ Response includes:
|
|||||||
- `stats.walletBalance`
|
- `stats.walletBalance`
|
||||||
- `stats.walletTransactionsCount`
|
- `stats.walletTransactionsCount`
|
||||||
- `stats.activeSessionsCount`
|
- `stats.activeSessionsCount`
|
||||||
|
- `stats.addressesCount`
|
||||||
- `stats.ordersCount`
|
- `stats.ordersCount`
|
||||||
- `stats.favoritesCount`
|
- `stats.favoritesCount`
|
||||||
- `stats.ticketsCount`
|
- `stats.ticketsCount`
|
||||||
@@ -74,7 +77,6 @@ Response includes:
|
|||||||
Current placeholder values:
|
Current placeholder values:
|
||||||
|
|
||||||
- `ordersCount = 0`
|
- `ordersCount = 0`
|
||||||
- `favoritesCount = 0`
|
|
||||||
- `ticketsCount = 0`
|
- `ticketsCount = 0`
|
||||||
|
|
||||||
These fields are placeholders until those modules are implemented.
|
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`
|
### `DELETE /api/users/me/sessions/:sessionId`
|
||||||
|
|
||||||
Revokes one active session for the current user.
|
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 { StorageModule } from './modules/storage/storage.module';
|
||||||
import { LoyaltyProfile } from './modules/users/entities/loyalty-profile.entity';
|
import { LoyaltyProfile } from './modules/users/entities/loyalty-profile.entity';
|
||||||
import { User } from './modules/users/entities/user.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 { UserLevelHistory } from './modules/users/entities/user-level-history.entity';
|
||||||
import { WalletTransaction } from './modules/users/entities/wallet-transaction.entity';
|
import { WalletTransaction } from './modules/users/entities/wallet-transaction.entity';
|
||||||
import { Wallet } from './modules/users/entities/wallet.entity';
|
import { Wallet } from './modules/users/entities/wallet.entity';
|
||||||
@@ -46,6 +48,8 @@ import { UsersModule } from './modules/users/users.module';
|
|||||||
WalletTransaction,
|
WalletTransaction,
|
||||||
LoyaltyProfile,
|
LoyaltyProfile,
|
||||||
UserLevelHistory,
|
UserLevelHistory,
|
||||||
|
UserAddress,
|
||||||
|
FavoriteProduct,
|
||||||
AuthOtp,
|
AuthOtp,
|
||||||
UserSession,
|
UserSession,
|
||||||
Product,
|
Product,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { Settings } from '../modules/settings/entities/settings.entity';
|
|||||||
import { MediaAsset } from '../modules/media/entities/media-asset.entity';
|
import { MediaAsset } from '../modules/media/entities/media-asset.entity';
|
||||||
import { LoyaltyProfile } from '../modules/users/entities/loyalty-profile.entity';
|
import { LoyaltyProfile } from '../modules/users/entities/loyalty-profile.entity';
|
||||||
import { User } from '../modules/users/entities/user.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 { UserLevelHistory } from '../modules/users/entities/user-level-history.entity';
|
||||||
import { WalletTransaction } from '../modules/users/entities/wallet-transaction.entity';
|
import { WalletTransaction } from '../modules/users/entities/wallet-transaction.entity';
|
||||||
import { Wallet } from '../modules/users/entities/wallet.entity';
|
import { Wallet } from '../modules/users/entities/wallet.entity';
|
||||||
@@ -33,6 +35,8 @@ export const buildTypeOrmOptions = (
|
|||||||
WalletTransaction,
|
WalletTransaction,
|
||||||
LoyaltyProfile,
|
LoyaltyProfile,
|
||||||
UserLevelHistory,
|
UserLevelHistory,
|
||||||
|
UserAddress,
|
||||||
|
FavoriteProduct,
|
||||||
AuthOtp,
|
AuthOtp,
|
||||||
UserSession,
|
UserSession,
|
||||||
Product,
|
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';
|
} from 'typeorm';
|
||||||
import { UserSession } from '../../auth/entities/user-session.entity';
|
import { UserSession } from '../../auth/entities/user-session.entity';
|
||||||
import { UserRole } from '../enums/user-role.enum';
|
import { UserRole } from '../enums/user-role.enum';
|
||||||
|
import { FavoriteProduct } from './favorite-product.entity';
|
||||||
import { LoyaltyProfile } from './loyalty-profile.entity';
|
import { LoyaltyProfile } from './loyalty-profile.entity';
|
||||||
|
import { UserAddress } from './user-address.entity';
|
||||||
import { Wallet } from './wallet.entity';
|
import { Wallet } from './wallet.entity';
|
||||||
|
|
||||||
@Entity({ name: 'users' })
|
@Entity({ name: 'users' })
|
||||||
@@ -50,6 +52,12 @@ export class User {
|
|||||||
@OneToMany(() => UserSession, (session) => session.user)
|
@OneToMany(() => UserSession, (session) => session.user)
|
||||||
sessions: UserSession[];
|
sessions: UserSession[];
|
||||||
|
|
||||||
|
@OneToMany(() => UserAddress, (address) => address.user)
|
||||||
|
addresses: UserAddress[];
|
||||||
|
|
||||||
|
@OneToMany(() => FavoriteProduct, (favorite) => favorite.user)
|
||||||
|
favoriteProducts: FavoriteProduct[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
Param,
|
Param,
|
||||||
Patch,
|
Patch,
|
||||||
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Req,
|
Req,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -17,7 +18,9 @@ import {
|
|||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||||
import { JwtPayload } from '../auth/interfaces/jwt-payload.interface';
|
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 { ListWalletTransactionsDto } from './dto/list-wallet-transactions.dto';
|
||||||
|
import { UpdateUserAddressDto } from './dto/update-user-address.dto';
|
||||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
@@ -78,4 +81,71 @@ export class UsersController {
|
|||||||
) {
|
) {
|
||||||
return this.usersService.revokeSession(request.user.sub, sessionId);
|
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 { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { UserSession } from '../auth/entities/user-session.entity';
|
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 { LoyaltyProfile } from './entities/loyalty-profile.entity';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
|
import { UserAddress } from './entities/user-address.entity';
|
||||||
import { UserLevelHistory } from './entities/user-level-history.entity';
|
import { UserLevelHistory } from './entities/user-level-history.entity';
|
||||||
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
||||||
import { Wallet } from './entities/wallet.entity';
|
import { Wallet } from './entities/wallet.entity';
|
||||||
@@ -18,6 +21,9 @@ import { UsersService } from './users.service';
|
|||||||
LoyaltyProfile,
|
LoyaltyProfile,
|
||||||
UserLevelHistory,
|
UserLevelHistory,
|
||||||
UserSession,
|
UserSession,
|
||||||
|
UserAddress,
|
||||||
|
FavoriteProduct,
|
||||||
|
Product,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
|
|||||||
@@ -6,10 +6,15 @@ import {
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { IsNull, Repository } from 'typeorm';
|
import { IsNull, Repository } from 'typeorm';
|
||||||
import { UserSession } from '../auth/entities/user-session.entity';
|
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 { ListWalletTransactionsDto } from './dto/list-wallet-transactions.dto';
|
||||||
|
import { UpdateUserAddressDto } from './dto/update-user-address.dto';
|
||||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||||
|
import { FavoriteProduct } from './entities/favorite-product.entity';
|
||||||
import { LoyaltyProfile } from './entities/loyalty-profile.entity';
|
import { LoyaltyProfile } from './entities/loyalty-profile.entity';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
|
import { UserAddress } from './entities/user-address.entity';
|
||||||
import { UserLevelHistory } from './entities/user-level-history.entity';
|
import { UserLevelHistory } from './entities/user-level-history.entity';
|
||||||
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
||||||
import { Wallet } from './entities/wallet.entity';
|
import { Wallet } from './entities/wallet.entity';
|
||||||
@@ -31,6 +36,12 @@ export class UsersService {
|
|||||||
private readonly walletTransactionsRepository: Repository<WalletTransaction>,
|
private readonly walletTransactionsRepository: Repository<WalletTransaction>,
|
||||||
@InjectRepository(UserSession)
|
@InjectRepository(UserSession)
|
||||||
private readonly userSessionsRepository: Repository<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) {
|
findByPhone(phone: string) {
|
||||||
@@ -132,7 +143,13 @@ export class UsersService {
|
|||||||
|
|
||||||
async getDashboard(userId: string) {
|
async getDashboard(userId: string) {
|
||||||
const user = await this.requireUser(userId);
|
const user = await this.requireUser(userId);
|
||||||
const [activeSessionsCount, walletTransactionsCount, loyaltyHistory] =
|
const [
|
||||||
|
activeSessionsCount,
|
||||||
|
walletTransactionsCount,
|
||||||
|
loyaltyHistory,
|
||||||
|
favoritesCount,
|
||||||
|
addressesCount,
|
||||||
|
] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.userSessionsRepository.count({
|
this.userSessionsRepository.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -150,6 +167,16 @@ export class UsersService {
|
|||||||
order: { createdAt: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
take: 5,
|
take: 5,
|
||||||
}),
|
}),
|
||||||
|
this.favoriteProductsRepository.count({
|
||||||
|
where: {
|
||||||
|
user: { id: user.id },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.userAddressesRepository.count({
|
||||||
|
where: {
|
||||||
|
user: { id: user.id },
|
||||||
|
},
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -158,8 +185,9 @@ export class UsersService {
|
|||||||
walletBalance: user.wallet.balance,
|
walletBalance: user.wallet.balance,
|
||||||
walletTransactionsCount,
|
walletTransactionsCount,
|
||||||
activeSessionsCount,
|
activeSessionsCount,
|
||||||
|
addressesCount,
|
||||||
ordersCount: 0,
|
ordersCount: 0,
|
||||||
favoritesCount: 0,
|
favoritesCount,
|
||||||
ticketsCount: 0,
|
ticketsCount: 0,
|
||||||
},
|
},
|
||||||
loyalty: {
|
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) {
|
private async requireUser(userId: string) {
|
||||||
const user = await this.findById(userId);
|
const user = await this.findById(userId);
|
||||||
|
|
||||||
@@ -286,6 +523,31 @@ export class UsersService {
|
|||||||
return user;
|
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) {
|
private mapProfile(user: User) {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -321,4 +583,37 @@ export class UsersService {
|
|||||||
createdAt: transaction.createdAt,
|
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