remove unused modules
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
import { Controller, Get, UseGuards, Patch, Body, ValidationPipe, Post, Query, Delete, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiOkResponse, ApiHeader } from '@nestjs/swagger';
|
||||
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
|
||||
import { UserService } from '../providers/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';
|
||||
import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants/index';
|
||||
import { UserSuccessMessage } from 'src/common/enums/message.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { WalletService } from '../providers/wallet.service';
|
||||
|
||||
@ApiTags('User')
|
||||
@Controller()
|
||||
export class UsersController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly walletService: WalletService,
|
||||
) {}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get the current authenticated user profile' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@Get('public/user/me')
|
||||
async getUser(@UserId() userId: string) {
|
||||
const user = await this.userService.findById(userId);
|
||||
return user;
|
||||
}
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Update the authenticated user profile' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiBody({ type: UpdateUserDto })
|
||||
@Patch('public/user/update')
|
||||
async updateUser(@UserId() userId: string, @RestId() restId: string, @Body() dto: UpdateUserDto) {
|
||||
const user = await this.userService.updateUser(userId, restId, dto);
|
||||
return user;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get all addresses for the authenticated user' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiOkResponse({ description: 'List of user addresses' })
|
||||
@Get('public/user/addresses')
|
||||
async getUserAddresses(@UserId() userId: string) {
|
||||
const addresses = await this.userService.getUserAddresses(userId);
|
||||
return addresses;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get User Wallet' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@Get('public/user/wallet/balance')
|
||||
async getUserWalletBalance(@UserId() userId: string, @RestId() restId: string) {
|
||||
const wallet = await this.walletService.getUserCurrentWalletBalance(userId, restId);
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get User Wallet' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@Get('public/user/points/balance')
|
||||
async getUserWallet(@UserId() userId: string, @RestId() restId: string) {
|
||||
const wallet = await this.walletService.getUserCurrentPoinrBalance(userId, restId);
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get User Wallet Transactions' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@Get('public/user/wallet/transactions')
|
||||
async getUserWalletTransactions(
|
||||
@UserId() userId: string,
|
||||
@RestId() restId: string,
|
||||
@Query(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
query: FindWalletTransactionsDto,
|
||||
) {
|
||||
const transactions = await this.userService.getUserWalletTransactions(userId, restId, query);
|
||||
return transactions;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Create a new address for the authenticated user' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiBody({ type: CreateUserAddressDto })
|
||||
@ApiOkResponse({ description: 'Created user address' })
|
||||
@Post('public/user/addresses')
|
||||
async createUserAddress(
|
||||
@UserId() userId: string,
|
||||
@Body(new ValidationPipe({ transform: true, whitelist: true })) dto: CreateUserAddressDto,
|
||||
) {
|
||||
const address = await this.userService.createUserAddress(userId, dto);
|
||||
return address;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get a single address by ID for the authenticated user' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@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' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@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' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@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: UserSuccessMessage.ADDRESS_DELETED_SUCCESS };
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Set an address as default for the authenticated user' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@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(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Convert user score (points) to wallet balance' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@Post('public/user/convert-score-to-wallet')
|
||||
async convertScoreToWallet(@UserId() userId: string, @RestId() restId: string) {
|
||||
await this.userService.convertScoreToWallet(userId, restId);
|
||||
return {
|
||||
message: UserSuccessMessage.SCORE_CONVERTED_SUCCESS,
|
||||
};
|
||||
}
|
||||
|
||||
/******************** Admin Routes **********************/
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_USERS)
|
||||
@ApiOperation({ summary: 'Get paginated list of restuarant users with filters' })
|
||||
@Get('admin/users')
|
||||
async findAllRestaurantUsers(
|
||||
@Query(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
query: FindUsersDto,
|
||||
@RestId() restId: string,
|
||||
) {
|
||||
return this.userService.findAll(restId, query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { IsString, IsOptional, IsNumber, IsBoolean, Min, Max } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* DTO for creating a new user address.
|
||||
*/
|
||||
export class CreateUserAddressDto {
|
||||
@ApiProperty({ description: 'Address title/label (e.g., "Home", "Work")', example: 'Home' })
|
||||
@IsString()
|
||||
title!: string;
|
||||
|
||||
@ApiProperty({ description: 'Street address', example: '123 Main Street' })
|
||||
@IsString()
|
||||
address!: string;
|
||||
|
||||
@ApiProperty({ description: 'City name', example: 'Tehran' })
|
||||
@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;
|
||||
|
||||
@ApiProperty({ description: 'Latitude coordinate', example: 35.6892, minimum: -90, maximum: 90 })
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
latitude!: number;
|
||||
|
||||
@ApiProperty({ description: 'Longitude coordinate', example: 51.389, minimum: -180, maximum: 180 })
|
||||
@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, default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isDefault?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { IsNotEmpty, IsNumber, IsIn } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { WalletTransactionType, WalletTransactionReason } from '../interface/wallet';
|
||||
|
||||
export class CreateWalletTransactionDto {
|
||||
@ApiProperty({
|
||||
description: 'Transaction amount',
|
||||
type: Number,
|
||||
example: 1000,
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
amount!: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Transaction type',
|
||||
enum: WalletTransactionType,
|
||||
example: WalletTransactionType.CREDIT,
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsIn(Object.values(WalletTransactionType))
|
||||
type!: WalletTransactionType;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Transaction reason',
|
||||
enum: WalletTransactionReason,
|
||||
example: WalletTransactionReason.ORDER_PAYMENT,
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsIn(Object.values(WalletTransactionReason))
|
||||
reason!: WalletTransactionReason;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { IsOptional, IsString, IsNumber, Min, IsIn } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { User } from '../entities/user.entity';
|
||||
|
||||
// Define the valid sort directions
|
||||
const sortOrderOptions = ['asc', 'desc'] as const;
|
||||
type SortOrder = (typeof sortOrderOptions)[number];
|
||||
|
||||
export class FindUsersDto {
|
||||
/**
|
||||
* The page number to retrieve.
|
||||
* @default 1
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'شماره صفحه',
|
||||
type: Number,
|
||||
default: 1,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
page?: number = 1;
|
||||
|
||||
/**
|
||||
* The number of items per page.
|
||||
* @default 10
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'تعداد مورد در هر صفحه',
|
||||
type: Number,
|
||||
default: 10,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
limit?: number = 10;
|
||||
|
||||
/**
|
||||
* A general search term to filter by.
|
||||
* Searches firstName, lastName, phone, and nationalCode.
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'جستجو بر اساس نام، نامخانوادگی یا شماره تماس',
|
||||
type: String,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
/**
|
||||
* The field to sort the results by.
|
||||
* @default "createdAt"
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'فیلد مرتبسازی',
|
||||
type: String,
|
||||
default: 'createdAt',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
orderBy?: keyof User = 'createdAt';
|
||||
|
||||
/**
|
||||
* The direction to sort the results.
|
||||
* @default "desc"
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'جهت مرتبسازی (asc یا desc)',
|
||||
enum: sortOrderOptions,
|
||||
default: 'desc',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(sortOrderOptions)
|
||||
order?: SortOrder = 'desc';
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { IsOptional, IsString, IsNumber, Min, IsIn, IsDateString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { WalletTransactionType, WalletTransactionReason } from '../interface/wallet';
|
||||
|
||||
// Define the valid sort directions
|
||||
const sortOrderOptions = ['asc', 'desc'] as const;
|
||||
type SortOrder = (typeof sortOrderOptions)[number];
|
||||
|
||||
export class FindWalletTransactionsDto {
|
||||
/**
|
||||
* The page number to retrieve.
|
||||
* @default 1
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Page number',
|
||||
type: Number,
|
||||
default: 1,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
page?: number = 1;
|
||||
|
||||
/**
|
||||
* The number of items per page.
|
||||
* @default 10
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Items per page',
|
||||
type: Number,
|
||||
default: 10,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
limit?: number = 10;
|
||||
|
||||
/**
|
||||
* Filter by transaction type
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Transaction type filter',
|
||||
enum: WalletTransactionType,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(Object.values(WalletTransactionType))
|
||||
type?: WalletTransactionType;
|
||||
|
||||
/**
|
||||
* Filter by transaction reason
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Transaction reason filter',
|
||||
enum: WalletTransactionReason,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(Object.values(WalletTransactionReason))
|
||||
reason?: WalletTransactionReason;
|
||||
|
||||
/**
|
||||
* Filter transactions from this date onwards
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Start date filter (ISO 8601 format)',
|
||||
type: String,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startDate?: string;
|
||||
|
||||
/**
|
||||
* Filter transactions up to this date
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'End date filter (ISO 8601 format)',
|
||||
type: String,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endDate?: string;
|
||||
|
||||
/**
|
||||
* Minimum amount filter
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Minimum amount filter',
|
||||
type: Number,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
minAmount?: number;
|
||||
|
||||
/**
|
||||
* Maximum amount filter
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Maximum amount filter',
|
||||
type: Number,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
maxAmount?: number;
|
||||
|
||||
/**
|
||||
* The field to sort the results by.
|
||||
* @default "createdAt"
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Sort field',
|
||||
type: String,
|
||||
default: 'createdAt',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
orderBy?: string = 'createdAt';
|
||||
|
||||
/**
|
||||
* The direction to sort the results.
|
||||
* @default "desc"
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Sort direction',
|
||||
enum: sortOrderOptions,
|
||||
default: 'desc',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(sortOrderOptions)
|
||||
order?: SortOrder = 'desc';
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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.389, 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;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IsString, IsOptional, IsBoolean } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateUserDto {
|
||||
@ApiPropertyOptional({ description: "User's first name", example: 'John' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "User's last name", example: 'Doe' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastName?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "User's birth date (ISO)", example: '1990-01-01' })
|
||||
@IsOptional()
|
||||
// keep as string here, caller should send ISO date; service will assign to Date
|
||||
@IsString()
|
||||
birthDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "User's marriage date (ISO)", example: '2015-06-01' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
marriageDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Gender flag (boolean)', example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
gender?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Avatar URL' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatarUrl?: string;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Entity, Property, ManyToOne, Index, Enum } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { User } from './user.entity';
|
||||
import { PointTransactionReason } from '../interface/point';
|
||||
import { PointTransactionType } from '../interface/point';
|
||||
|
||||
@Entity({ tableName: 'point_transactions' })
|
||||
@Index({ properties: ['user', 'restaurant'] }) // Composite index for most common query: find wallet by user and restaurant
|
||||
@Index({ properties: ['user'] }) // Index for queries finding all wallets for a user
|
||||
@Index({ properties: ['restaurant'] }) // Index for queries finding all wallets for a restaurant
|
||||
export class PointTransaction extends BaseEntity {
|
||||
@ManyToOne(() => User)
|
||||
user!: User;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
restaurant!: Restaurant;
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 0 })
|
||||
amount!: number;
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 0 })
|
||||
balance!: number;
|
||||
|
||||
@Enum(() => PointTransaction)
|
||||
type!: PointTransactionType;
|
||||
|
||||
@Enum(() => PointTransactionReason)
|
||||
reason!: PointTransactionReason;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Entity, Property, ManyToOne } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { User } from './user.entity';
|
||||
|
||||
@Entity({ tableName: 'user_addresses' })
|
||||
export class UserAddress extends BaseEntity {
|
||||
@ManyToOne(() => User)
|
||||
user!: User;
|
||||
|
||||
@Property()
|
||||
title!: string; // e.g., "Home", "Work", "Office"
|
||||
|
||||
@Property()
|
||||
address!: string;
|
||||
|
||||
@Property()
|
||||
city!: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
province?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
postalCode?: string | null = null;
|
||||
|
||||
@Property({ type: 'float' })
|
||||
latitude!: number;
|
||||
|
||||
@Property({ type: 'float' })
|
||||
longitude!: number;
|
||||
|
||||
@Property({ nullable: true })
|
||||
phone?: string; // Contact phone for this address
|
||||
|
||||
@Property({ default: false })
|
||||
isDefault: boolean = false; // Whether this is the default address
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Entity, Index, Property, OneToMany, Collection, Cascade } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { UserAddress } from './user-address.entity';
|
||||
import { Order } from 'src/modules/order/entities/order.entity';
|
||||
import { normalizePhone } from '../../util/phone.util';
|
||||
|
||||
@Entity({ tableName: 'users' })
|
||||
@Index({ properties: ['isActive'] })
|
||||
export class User extends BaseEntity {
|
||||
@OneToMany(() => Order, order => order.user)
|
||||
orders = new Collection<Order>(this);
|
||||
|
||||
@Property()
|
||||
firstName!: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
private _phone!: string;
|
||||
|
||||
@Property({ unique: true })
|
||||
get phone(): string {
|
||||
return this._phone;
|
||||
}
|
||||
|
||||
set phone(value: string) {
|
||||
this._phone = normalizePhone(value);
|
||||
}
|
||||
|
||||
@Property({ default: null, type: 'date', nullable: true })
|
||||
birthDate?: Date;
|
||||
|
||||
@Property({ default: null, type: 'date', nullable: true })
|
||||
marriageDate?: Date;
|
||||
|
||||
@Property({ nullable: true })
|
||||
referrer?: string;
|
||||
|
||||
@Property({ default: true })
|
||||
isActive?: boolean = true;
|
||||
|
||||
@Property({ default: true, nullable: true })
|
||||
gender?: boolean;
|
||||
|
||||
@Property({ nullable: true })
|
||||
avatarUrl?: string;
|
||||
|
||||
@OneToMany(() => UserAddress, address => address.user, {
|
||||
cascade: [Cascade.ALL],
|
||||
orphanRemoval: true,
|
||||
})
|
||||
addresses = new Collection<UserAddress>(this);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Entity, Index, Property, ManyToOne, Enum } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { User } from './user.entity';
|
||||
import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
|
||||
@Entity({ tableName: 'wallet_transactions' })
|
||||
@Index({ properties: ['user', 'restaurant'] })
|
||||
@Index({ properties: ['user'] })
|
||||
@Index({ properties: ['restaurant'] })
|
||||
export class WalletTransaction extends BaseEntity {
|
||||
@ManyToOne(() => User)
|
||||
user!: User;
|
||||
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant!: Restaurant;
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 0 })
|
||||
amount!: number;
|
||||
|
||||
@Property({ type: 'decimal', precision: 10, scale: 0 })
|
||||
balance!: number;
|
||||
|
||||
@Enum(() => WalletTransactionType)
|
||||
type!: WalletTransactionType;
|
||||
|
||||
@Enum(() => WalletTransactionReason)
|
||||
reason!: WalletTransactionReason;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export enum PointTransactionType {
|
||||
CREDIT = 'credit',
|
||||
DEBIT = 'debit',
|
||||
}
|
||||
|
||||
|
||||
export enum PointTransactionReason {
|
||||
ORDER_COMPLETED_DEPOSIT = 'order_completed_deposit',
|
||||
CONVERT_SCORE_TO_POINT = 'convert_score_to_point',
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export enum WalletTransactionType {
|
||||
CREDIT = 'credit',
|
||||
DEBIT = 'debit',
|
||||
}
|
||||
|
||||
|
||||
export enum WalletTransactionReason {
|
||||
ORDER_PAYMENT = 'order_payment',
|
||||
ORDER_REFUND = 'order_refund',
|
||||
CONVERT_SCORE_TO_WALLET = 'convert_score_to_wallet',
|
||||
WITHDRAW = 'withdraw',
|
||||
DEPOSIT = 'deposit',
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { FilterQuery, RequiredEntityData } from '@mikro-orm/core';
|
||||
import { User } from '../entities/user.entity';
|
||||
import { UserAddress } from '../entities/user-address.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { UpdateUserDto } from '../dto/update-user.dto';
|
||||
import { FindUsersDto } from '../dto/find-user.dto';
|
||||
import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto';
|
||||
import { CreateWalletTransactionDto } from '../dto/create-wallet-transaction.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';
|
||||
import { normalizePhone } from '../../util/phone.util';
|
||||
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
|
||||
import { WalletTransactionRepository } from '../repositories/wallet-transaction.repository';
|
||||
import { WalletService } from './wallet.service';
|
||||
import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet';
|
||||
import { WalletTransaction } from '../entities/wallet-transaction.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly restaurantRepository: RestRepository,
|
||||
private readonly walletTransactionRepository: WalletTransactionRepository,
|
||||
private readonly walletService: WalletService,
|
||||
private readonly em: EntityManager,
|
||||
) { }
|
||||
|
||||
async findOrCreateByPhone(phone: string): Promise<User> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
let user = await this.userRepository.findOne({ phone: normalizedPhone });
|
||||
|
||||
if (!user) {
|
||||
// firstName is required on the entity; use phone as a sensible default
|
||||
const _raw = {
|
||||
phone: normalizedPhone,
|
||||
firstName: '[نام]',
|
||||
// keep dates undefined (no value) — cast through unknown to satisfy TS typing
|
||||
birthDate: undefined,
|
||||
marriageDate: undefined,
|
||||
wallet: 0,
|
||||
points: 0,
|
||||
};
|
||||
|
||||
const createData = _raw as unknown as RequiredEntityData<User>;
|
||||
|
||||
user = this.userRepository.create(createData);
|
||||
await this.em.persistAndFlush(user);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
|
||||
// const user = await this.em.findOneOrFail(User, userId, {}).catch(() => {
|
||||
// throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
// });
|
||||
|
||||
// this.em.assign(user, { ...dto });
|
||||
|
||||
// await this.em.flush();
|
||||
|
||||
// return user;
|
||||
// }
|
||||
|
||||
async findByPhone(phone: string): Promise<User | null> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
return this.userRepository.findOne({ phone: normalizedPhone });
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ id });
|
||||
}
|
||||
// todo : transaction code here
|
||||
async create(phone: string, restId: string): Promise<User> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const _raw = {
|
||||
phone: normalizedPhone,
|
||||
firstName: normalizedPhone,
|
||||
birthDate: undefined,
|
||||
marriageDate: undefined,
|
||||
};
|
||||
// get registerscore from restaurant
|
||||
const restaurant = await this.restaurantRepository.findOne({ id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(`Restaurant not found.`);
|
||||
}
|
||||
const points = Number(restaurant.score?.registerScore || 0);
|
||||
const createData = _raw as unknown as RequiredEntityData<User>;
|
||||
|
||||
const user = this.userRepository.create(createData);
|
||||
const newWalletTransaction = this.walletTransactionRepository.create({
|
||||
user: user,
|
||||
restaurant: restaurant,
|
||||
balance: 0,
|
||||
amount: points,
|
||||
type: WalletTransactionType.CREDIT,
|
||||
reason: WalletTransactionReason.DEPOSIT
|
||||
});
|
||||
await this.em.persistAndFlush([user, newWalletTransaction]);
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateUser(userId: string, restId: string, dto: UpdateUserDto): Promise<User> {
|
||||
const user = await this.userRepository.findOne({ id: userId });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
}
|
||||
|
||||
// Normalize date strings into Date objects if provided
|
||||
const assignData: Partial<User> = { ...dto } as Partial<User>;
|
||||
if (dto.birthDate) {
|
||||
assignData.birthDate = new Date(dto.birthDate);
|
||||
}
|
||||
if (dto.marriageDate) {
|
||||
assignData.marriageDate = new Date(dto.marriageDate);
|
||||
}
|
||||
// get restuarant
|
||||
this.em.assign(user, assignData);
|
||||
await this.em.flush();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async findAll(restId: string, dto: FindUsersDto): Promise<PaginatedResult<User>> {
|
||||
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto;
|
||||
|
||||
// 1. Calculate pagination
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// 2. Build the 'where' filter query
|
||||
const where: FilterQuery<User> = {
|
||||
orders: {
|
||||
restaurant: {
|
||||
id: restId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 4. Add 'search' logic (case-insensitive)
|
||||
if (search) {
|
||||
const searchPattern = `%${search}%`;
|
||||
const normalizedSearch = normalizePhone(search);
|
||||
const normalizedSearchPattern = `%${normalizedSearch}%`;
|
||||
// $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default)
|
||||
// Search with both original and normalized phone patterns to handle various input formats
|
||||
where.$or = [
|
||||
{ firstName: { $ilike: searchPattern } },
|
||||
{ lastName: { $ilike: searchPattern } },
|
||||
{ phone: { $ilike: searchPattern } },
|
||||
...(normalizedSearch !== search ? [{ phone: { $ilike: normalizedSearchPattern } }] : []),
|
||||
];
|
||||
}
|
||||
|
||||
// 5. Execute the query using findAndCount
|
||||
const [users, total] = await this.userRepository.findAndCount(where, {
|
||||
limit,
|
||||
offset,
|
||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||
});
|
||||
|
||||
// 6. Calculate total pages
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
// 7. Return the paginated result
|
||||
return {
|
||||
data: users,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getUserAddresses(userId: string): Promise<UserAddress[]> {
|
||||
const user = await this.userRepository.findOne({ id: userId }, { populate: ['addresses'] });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
}
|
||||
|
||||
// Load addresses if not already loaded
|
||||
await user.addresses.loadItems();
|
||||
return user.addresses.getItems();
|
||||
}
|
||||
|
||||
async createUserAddress(userId: string, dto: CreateUserAddressDto): Promise<UserAddress> {
|
||||
const user = await this.userRepository.findOne({ id: userId });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
}
|
||||
|
||||
// If setting as default, unset other default addresses
|
||||
if (dto.isDefault) {
|
||||
const existingAddresses = await this.em.find(UserAddress, { user: { id: userId }, isDefault: true });
|
||||
for (const address of existingAddresses) {
|
||||
address.isDefault = false;
|
||||
}
|
||||
await this.em.flush();
|
||||
}
|
||||
|
||||
// Create new address
|
||||
const address = this.em.create(UserAddress, {
|
||||
user,
|
||||
title: dto.title,
|
||||
address: dto.address,
|
||||
city: dto.city,
|
||||
province: dto.province,
|
||||
postalCode: dto.postalCode,
|
||||
latitude: dto.latitude,
|
||||
longitude: dto.longitude,
|
||||
phone: dto.phone,
|
||||
isDefault: dto.isDefault ?? false,
|
||||
});
|
||||
|
||||
await this.em.persistAndFlush(address);
|
||||
return address;
|
||||
}
|
||||
|
||||
async getUserAddress(userId: string, addressId: string): Promise<UserAddress> {
|
||||
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<UserAddress> {
|
||||
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<void> {
|
||||
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<UserAddress> {
|
||||
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;
|
||||
}
|
||||
|
||||
async convertScoreToWallet(userId: string, restId: string) {
|
||||
return this.em.transactional(async em => {
|
||||
const user = await em.findOne(User, { id: userId });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
}
|
||||
const restaurant = await em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(`Restaurant with ID ${restId} not found.`);
|
||||
}
|
||||
let walletTransaction = await em.findOne(WalletTransaction, { user: { id: userId }, restaurant: { id: restId } },
|
||||
{ orderBy: { createdAt: 'DESC' } });
|
||||
|
||||
if (!walletTransaction) {
|
||||
walletTransaction = em.create(WalletTransaction,
|
||||
{
|
||||
user: user,
|
||||
restaurant,
|
||||
balance: 0,
|
||||
amount: 0,
|
||||
type: WalletTransactionType.CREDIT,
|
||||
reason: WalletTransactionReason.DEPOSIT
|
||||
});
|
||||
await em.persistAndFlush(walletTransaction);
|
||||
}
|
||||
// Determine how many points to convert
|
||||
const pointsToConvert = walletTransaction.balance;
|
||||
|
||||
// Validate points to convert
|
||||
if (pointsToConvert <= 0) {
|
||||
throw new BadRequestException('Points to convert must be greater than 0.');
|
||||
}
|
||||
|
||||
if (!restaurant.score) {
|
||||
throw new BadRequestException('Restaurant score not found.');
|
||||
}
|
||||
if (!restaurant.score.purchaseScore) {
|
||||
throw new BadRequestException('Restaurant purchase score not found.');
|
||||
}
|
||||
if (!restaurant.score.purchaseAmount) {
|
||||
throw new BadRequestException('Restaurant purchase amount not found.');
|
||||
}
|
||||
const walletIncreaseAmount =
|
||||
(Number(pointsToConvert) * Number(restaurant.score.purchaseAmount)) / Number(restaurant.score.purchaseScore);
|
||||
// Convert points to wallet (1:1 ratio - can be made configurable)
|
||||
|
||||
const newWalletTransaction = em.create(WalletTransaction, {
|
||||
user: user,
|
||||
restaurant: restaurant,
|
||||
amount: walletIncreaseAmount,
|
||||
type: WalletTransactionType.CREDIT,
|
||||
reason: WalletTransactionReason.CONVERT_SCORE_TO_WALLET,
|
||||
balance: walletTransaction.balance + walletIncreaseAmount,
|
||||
});
|
||||
// Update user's wallet and points
|
||||
walletTransaction.balance = walletTransaction.balance + walletIncreaseAmount;
|
||||
em.persist([newWalletTransaction]);
|
||||
|
||||
await em.flush();
|
||||
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
getUserWallet(userId: string, restId: string): Promise<WalletTransaction | null> {
|
||||
return this.walletTransactionRepository.findOne({ user: { id: userId }, restaurant: { id: restId } },
|
||||
{ orderBy: { createdAt: 'DESC' } });
|
||||
}
|
||||
|
||||
getUserWalletTransactions(userId: string, restId: string, dto: FindWalletTransactionsDto) {
|
||||
return this.walletService.getUserWalletTransactions(userId, restId, dto);
|
||||
}
|
||||
|
||||
async createWalletTransaction(userId: string, restId: string, dto: CreateWalletTransactionDto) {
|
||||
return this.em.transactional(async em => {
|
||||
return this.walletService.createTransaction(em, userId, restId, dto);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { FilterQuery } from '@mikro-orm/core';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { WalletTransaction } from '../entities/wallet-transaction.entity';
|
||||
import { WalletTransactionRepository } from '../repositories/wallet-transaction.repository';
|
||||
import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto';
|
||||
import { CreateWalletTransactionDto } from '../dto/create-wallet-transaction.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { WalletTransactionType } from '../interface/wallet';
|
||||
import { PointTransactionRepository } from '../repositories/point-transaction.repository';
|
||||
|
||||
@Injectable()
|
||||
export class WalletService {
|
||||
constructor(
|
||||
private readonly walletTransactionRepository: WalletTransactionRepository,
|
||||
private readonly pointTransactionRepository: PointTransactionRepository,
|
||||
) {}
|
||||
|
||||
async getUserWalletTransactions(
|
||||
userId: string,
|
||||
restId: string,
|
||||
dto: FindWalletTransactionsDto,
|
||||
): Promise<PaginatedResult<WalletTransaction>> {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
type,
|
||||
reason,
|
||||
startDate,
|
||||
endDate,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
orderBy = 'createdAt',
|
||||
order = 'desc',
|
||||
} = dto;
|
||||
|
||||
// Calculate pagination
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Find the user's wallet for this restaurant
|
||||
const walletTransaction = await this.walletTransactionRepository.findOne({
|
||||
user: { id: userId },
|
||||
restaurant: { id: restId },
|
||||
});
|
||||
|
||||
if (!walletTransaction) {
|
||||
throw new NotFoundException(`User wallet not found for user ${userId} and restaurant ${restId}.`);
|
||||
}
|
||||
|
||||
// Build the 'where' filter query
|
||||
const where: FilterQuery<WalletTransaction> = {
|
||||
user: { id: userId },
|
||||
restaurant: { id: restId },
|
||||
};
|
||||
|
||||
// Add type filter
|
||||
if (type) {
|
||||
where.type = type;
|
||||
}
|
||||
|
||||
// Add reason filter
|
||||
if (reason) {
|
||||
where.reason = reason;
|
||||
}
|
||||
|
||||
// Add date range filters
|
||||
if (startDate || endDate) {
|
||||
where.createdAt = {};
|
||||
if (startDate) {
|
||||
where.createdAt.$gte = new Date(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
where.createdAt.$lte = new Date(endDate);
|
||||
}
|
||||
}
|
||||
|
||||
// Add amount range filters
|
||||
if (minAmount !== undefined || maxAmount !== undefined) {
|
||||
where.amount = {};
|
||||
if (minAmount !== undefined) {
|
||||
where.amount.$gte = minAmount;
|
||||
}
|
||||
if (maxAmount !== undefined) {
|
||||
where.amount.$lte = maxAmount;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the query using findAndCount
|
||||
const [transactions, total] = await this.walletTransactionRepository.findAndCount(where, {
|
||||
limit,
|
||||
offset,
|
||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||
});
|
||||
|
||||
// Calculate total pages
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
// Return the paginated result
|
||||
return {
|
||||
data: transactions,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getUserWallet(userId: string, restId: string): Promise<WalletTransaction | null> {
|
||||
return this.walletTransactionRepository.findOne(
|
||||
{
|
||||
user: { id: userId },
|
||||
restaurant: { id: restId },
|
||||
},
|
||||
{ orderBy: { createdAt: 'DESC' } },
|
||||
);
|
||||
}
|
||||
|
||||
async createTransaction(
|
||||
em: EntityManager,
|
||||
userId: string,
|
||||
restId: string,
|
||||
dto: CreateWalletTransactionDto,
|
||||
): Promise<WalletTransaction> {
|
||||
const { amount, type, reason } = dto;
|
||||
|
||||
// Validate amount
|
||||
if (amount <= 0) {
|
||||
throw new BadRequestException('Transaction amount must be greater than 0.');
|
||||
}
|
||||
|
||||
// Find the user's wallet for this restaurant
|
||||
const walletTransaction = await em.findOne(WalletTransaction, {
|
||||
user: { id: userId },
|
||||
restaurant: { id: restId },
|
||||
});
|
||||
|
||||
if (!walletTransaction) {
|
||||
throw new NotFoundException(`User wallet not found for user ${userId} and restaurant ${restId}.`);
|
||||
}
|
||||
|
||||
// Calculate new balance
|
||||
const currentBalance = walletTransaction.balance || 0;
|
||||
let newBalance: number;
|
||||
|
||||
if (type === WalletTransactionType.CREDIT) {
|
||||
newBalance = currentBalance + amount;
|
||||
} else if (type === WalletTransactionType.DEBIT) {
|
||||
if (currentBalance < amount) {
|
||||
throw new BadRequestException(`Insufficient wallet balance. Current: ${currentBalance}, Required: ${amount}.`);
|
||||
}
|
||||
newBalance = currentBalance - amount;
|
||||
} else {
|
||||
throw new BadRequestException(`Invalid transaction type: ${type}`);
|
||||
}
|
||||
|
||||
// Update wallet balance
|
||||
walletTransaction.balance = newBalance;
|
||||
await em.persistAndFlush(walletTransaction);
|
||||
|
||||
// Create transaction record
|
||||
const transaction = em.create(WalletTransaction, {
|
||||
user: walletTransaction.user,
|
||||
restaurant: walletTransaction.restaurant,
|
||||
amount,
|
||||
type,
|
||||
reason,
|
||||
balance: newBalance,
|
||||
});
|
||||
await em.persistAndFlush([walletTransaction, transaction]);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
async getUserCurrentWalletBalance(userId: string, restuarantId: string) {
|
||||
const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, restuarantId);
|
||||
return { balance };
|
||||
}
|
||||
async getUserCurrentPoinrBalance(userId: string, restuarantId: string) {
|
||||
const balance = await this.pointTransactionRepository.getcurrentPointBalance(userId, restuarantId);
|
||||
return { balance };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { PointTransaction } from '../entities/point-transaction.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PointTransactionRepository extends EntityRepository<PointTransaction> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, PointTransaction);
|
||||
}
|
||||
async getcurrentPointBalance(userId: string, restaurantId: string): Promise<number> {
|
||||
const pointTransaction = await this.em.findOne(PointTransaction, {
|
||||
user: { id: userId },
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
return pointTransaction?.balance || 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { User } from '../entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository extends EntityRepository<User> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, User);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { WalletTransaction } from '../entities/wallet-transaction.entity';
|
||||
|
||||
@Injectable()
|
||||
export class WalletTransactionRepository extends EntityRepository<WalletTransaction> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, WalletTransaction);
|
||||
}
|
||||
async getCurrentWalletBalance(userId: string, restaurantId: string): Promise<number> {
|
||||
const walletTransaction = await this.em.findOne(
|
||||
WalletTransaction,
|
||||
{
|
||||
user: { id: userId },
|
||||
restaurant: { id: restaurantId },
|
||||
},
|
||||
{ orderBy: { createdAt: 'desc' } },
|
||||
);
|
||||
return walletTransaction?.balance || 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UserService } from './providers/user.service';
|
||||
import { UsersController } from './controllers/users.controller';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UserAddress } from './entities/user-address.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { WalletTransactionRepository } from './repositories/wallet-transaction.repository';
|
||||
import { WalletService } from './providers/wallet.service';
|
||||
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
||||
import { PointTransaction } from './entities/point-transaction.entity';
|
||||
import { PointTransactionRepository } from './repositories/point-transaction.repository';
|
||||
|
||||
@Module({
|
||||
providers: [UserService, WalletService, UserRepository, WalletTransactionRepository, PointTransactionRepository],
|
||||
controllers: [UsersController],
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction]),
|
||||
JwtModule,
|
||||
RestaurantsModule,
|
||||
],
|
||||
exports: [UserService, WalletService, UserRepository, WalletTransactionRepository, PointTransactionRepository],
|
||||
})
|
||||
export class UserModule {}
|
||||
Reference in New Issue
Block a user