This commit is contained in:
2026-01-18 18:28:09 +03:30
parent 8db3b8d1ac
commit b1a6af6feb
9 changed files with 222 additions and 192 deletions
@@ -1,25 +1,18 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Query } from '@nestjs/common';
import {
Controller, Get, Post, Body,
Param,
UseGuards,
} from '@nestjs/common';
import { PaymentService } from '../services/payments.service';
import {
ApiTags,
ApiOperation,
ApiNotFoundResponse,
ApiBody,
ApiParam,
ApiBearerAuth,
ApiHeader,
} from '@nestjs/swagger';
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto';
import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto';
import { VerifyPaymentDto } from '../dto/verify-payment.dto';
import { PaymentChartDto } from '../dto/payment-chart.dto';
import { UserId } from 'src/common/decorators';
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { Permission } from 'src/common/enums/permission.enum';
import { PayOrderDto } from '../dto/pay-order.dto';
import { FindPaymentsDto } from '../dto/find-payments.dto';
@ApiTags('payments')
@Controller()
@@ -37,76 +30,38 @@ export class PaymentController {
return this.paymentsService.payOrder(orderId, dto);
}
// @Post('public/payments/verify')
// @ApiOperation({ summary: 'Verify a payment' })
@Post('public/payments/:token/verify/online')
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Verify online payment' })
verifyOnlinePayment(@Param('paymentId') token: string) {
return this.paymentsService.verifyOnlinePayment(token);
}
// @ApiBody({ type: VerifyPaymentDto })
// @ApiNotFoundResponse({ description: 'Payment not found' })
// verifyPayment(@Body() verifyPaymentDto: VerifyPaymentDto) {
// return this.paymentsService.verifyOnlinePayment(verifyPaymentDto.authority, verifyPaymentDto.orderId);
// }
@ApiBearerAuth()
@Get('admin/payments')
@ApiOperation({ summary: 'get all payments as admin' })
findAllByUser(@UserId() userId: string, @Body() dto: FindPaymentsDto) {
return this.paymentsService.findAllByUser(dto, userId);
}
/*===============Admin=============== */
@Post('admin/payments/:paymentId/verify/cash')
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Verify cash payment' })
verifyPayment(@Param('paymentId') paymentId: string) {
return this.paymentsService.verifyCashPayment(paymentId);
}
// @UseGuards(AdminAuthGuard)
// @Permissions(Permission.MANAGE_PAYMENTS)
// @ApiBearerAuth()
// @Post('admin/payments/methods')
// @ApiOperation({ summary: 'Create a new restaurant payment method' })
// @ApiBody({ type: CreatePaymentMethodDto })
// createRestaurantPaymentMethod( @Body() createPaymentMethodDto: CreatePaymentMethodDto) {
// return this.paymentMethodService.create( createPaymentMethodDto);
// }
// @UseGuards(AdminAuthGuard)
// @Permissions(Permission.MANAGE_PAYMENTS)
// @ApiBearerAuth()
// @Get('admin/payments/methods/:paymentMethodId')
// @ApiOperation({ summary: 'Get restaurant payment method by ID' })
// @ApiParam({ name: 'paymentMethodId', type: 'string', description: 'Payment method ID' })
// findOneRestaurantPaymentMethod(@Param('paymentMethodId') paymentMethodId: string) {
// return this.paymentMethodService.findOne(paymentMethodId);
// }
// @UseGuards(AdminAuthGuard)
// @Permissions(Permission.MANAGE_PAYMENTS)
// @ApiBearerAuth()
// @Patch('admin/payments/methods/:paymentMethodId')
// @ApiOperation({ summary: 'Update a restaurant payment method' })
// @ApiParam({ name: 'paymentMethodId', type: 'string', description: 'Payment method ID' })
// @ApiBody({ type: UpdatePaymentMethodDto })
// updateRestaurantPaymentMethod(
// @Param('paymentMethodId') paymentMethodId: string,
// @Body() updatePaymentMethodDto: UpdatePaymentMethodDto,
// ) {
// return this.paymentMethodService.update(paymentMethodId, updatePaymentMethodDto);
// }
// @UseGuards(AdminAuthGuard)
// @Permissions(Permission.MANAGE_PAYMENTS)
// @ApiBearerAuth()
// @Delete('admin/payments/methods/:paymentMethodId')
// @ApiOperation({ summary: 'Delete a restaurant payment method' })
// @ApiParam({ name: 'paymentMethodId', type: 'string', description: 'Payment method ID' })
// removeRestaurantPaymentMethod(@Param('paymentMethodId') paymentMethodId: string) {
// return this.paymentMethodService.remove(paymentMethodId);
// }
// @UseGuards(AdminAuthGuard)
// @Permissions(Permission.MANAGE_PAYMENTS)
// @ApiBearerAuth()
// @Post('admin/payments/verify-cash-method/:paymentId')
// @ApiOperation({ summary: 'Verify cash payment ' })
// @ApiParam({ name: 'paymentId', type: 'string', description: 'Payment ID' })
// verifyCashPayment(@Param('paymentId') paymentId: string) {
// return this.paymentsService.verifyCashPayment(paymentId);
// }
// @UseGuards(AdminAuthGuard)
// @Permissions(Permission.VIEW_REPORTS)
// @ApiBearerAuth()
// @Get('admin/payments/chart')
// @ApiOperation({ summary: 'Get payment chart data with date and period filters' })
@ApiBearerAuth()
@Get('admin/payments')
@ApiOperation({ summary: 'get all payments as admin' })
findAllPayments(@Body() dto: FindPaymentsDto) {
return this.paymentsService.findAll(dto);
}
}
@@ -1,35 +0,0 @@
import { IsString, IsOptional, IsBoolean, IsNumber, IsEnum } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { PaymentMethodEnum, PaymentGatewayEnum } from '../interface/payment';
export class CreatePaymentMethodDto {
@ApiProperty({ description: 'Payment method', enum: PaymentMethodEnum })
@IsEnum(PaymentMethodEnum)
method!: PaymentMethodEnum;
@ApiProperty({ description: 'Payment gateway', enum: PaymentGatewayEnum, required: false })
@IsOptional()
@IsEnum(PaymentGatewayEnum)
gateway?: PaymentGatewayEnum | null;
@ApiProperty({ description: 'Payment method description', required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ description: 'Is payment method enabled', required: false, default: true })
@IsOptional()
@IsBoolean()
enabled?: boolean;
@ApiProperty({ description: 'Display order', required: false, default: 0 })
@IsOptional()
@IsNumber()
order?: number;
@ApiProperty({ description: 'Merchant ID', required: false })
@IsOptional()
@IsString()
merchantId?: string;
}
@@ -0,0 +1,80 @@
import {
IsOptional,
IsString,
IsNumber,
Min,
IsIn,
IsEnum,
IsDateString,
IsArray,
} from 'class-validator';
import { Type, Transform } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { PaymentStatusEnum } from '../interface/payment';
const sortOrderOptions = ['asc', 'desc'] as const;
type SortOrder = (typeof sortOrderOptions)[number];
export class FindPaymentsDto {
@ApiPropertyOptional({ default: 1, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page: number = 1;
@ApiPropertyOptional({ default: 10, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit: number = 10;
@ApiPropertyOptional({
description: 'Filter by order statuses',
enum: PaymentStatusEnum,
isArray: true,
})
@IsOptional()
@Transform(({ value }) =>
Array.isArray(value) ? value : value?.split(',')
)
@IsArray()
@IsEnum(PaymentStatusEnum, { each: true })
statuses?: PaymentStatusEnum[];
// @ApiPropertyOptional({ enum: PaymentStatusEnum })
// @IsOptional()
// @IsEnum(PaymentStatusEnum)
// paymentStatus?: PaymentStatusEnum;
@ApiPropertyOptional()
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ format: 'date-time' })
@IsOptional()
@IsDateString()
from?: string;
@ApiPropertyOptional({ format: 'date-time' })
@IsOptional()
@IsDateString()
to?: string;
@ApiPropertyOptional({ default: 'createdAt' })
@IsOptional()
@IsString()
orderBy: string = 'createdAt';
@ApiPropertyOptional({
enum: sortOrderOptions,
default: 'desc',
})
@IsOptional()
@IsIn(sortOrderOptions)
order: SortOrder = 'desc';
}
@@ -1,41 +0,0 @@
import { IsOptional, IsString, IsEnum, IsDateString } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
export enum ChartPeriodEnum {
Daily = 'daily',
Weekly = 'weekly',
Monthly = 'monthly',
}
export class PaymentChartDto {
@ApiPropertyOptional({
description: 'Start date for filtering (ISO date string)',
type: String,
format: 'date-time',
example: '2024-01-01T00:00:00Z',
})
@IsOptional()
@IsDateString()
startDate?: string;
@ApiPropertyOptional({
description: 'End date for filtering (ISO date string)',
type: String,
format: 'date-time',
example: '2024-12-31T23:59:59Z',
})
@IsOptional()
@IsDateString()
endDate?: string;
@ApiPropertyOptional({
description: 'Period type for grouping data',
enum: ChartPeriodEnum,
default: ChartPeriodEnum.Daily,
example: ChartPeriodEnum.Daily,
})
@IsOptional()
@IsEnum(ChartPeriodEnum)
type?: ChartPeriodEnum = ChartPeriodEnum.Daily;
}
@@ -1,4 +0,0 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreatePaymentMethodDto } from './create-payment-method.dto';
export class UpdatePaymentMethodDto extends PartialType(CreatePaymentMethodDto) {}
@@ -1,8 +0,0 @@
import { IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UpdatePaymentDto {
@ApiProperty({ description: 'Payment ID' })
@IsNumber()
id: number;
}
@@ -1,12 +0,0 @@
import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class VerifyPaymentDto {
@ApiProperty({ description: 'Payment authority token' })
@IsString()
authority: string;
@ApiProperty({ description: 'Payment order id' })
@IsString()
orderId: string;
}
@@ -1,10 +1,105 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { EntityManager, EntityRepository, FilterQuery } from '@mikro-orm/postgresql';
import { Payment } from '../entities/payment.entity';
import { FindPaymentsDto } from '../dto/find-payments.dto';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { PaymentStatusEnum } from '../interface/payment';
class FindPaymentsOpts extends FindPaymentsDto {
userId?: string;
};
@Injectable()
export class PaymentRepository extends EntityRepository<Payment> {
constructor(readonly em: EntityManager) {
super(em, Payment);
}
async findAllPaginated(opts: FindPaymentsOpts): Promise<PaginatedResult<Payment>> {
const {
page = 1,
limit = 10,
statuses,
search,
orderBy = 'createdAt',
order = 'desc',
userId,
from,
to
} = opts;
const offset = (page - 1) * limit;
const where: FilterQuery<Payment> = {};
// Filter by statuses
if (statuses) {
// Ensure statuses is always an array and normalize it
const normalizedStatuses = Array.isArray(statuses) ? statuses : [statuses];
// Filter out any empty values
const validStatuses = normalizedStatuses.filter((s): s is PaymentStatusEnum => !!s);
if (validStatuses.length > 0) {
// Use direct equality for single value, $in for multiple values to avoid SQL generation issues
if (validStatuses.length === 1) {
where.status = validStatuses[0];
} else {
where.status = { $in: validStatuses };
}
}
}
if (userId) {
where.order = { user: { id: userId } };
}
// Filter by date range
if (from || to) {
where.createdAt = {};
if (from) {
where.createdAt.$gte = new Date(from);
}
if (to) {
where.createdAt.$lte = new Date(to);
}
}
// Search by order number or user information
if (search) {
const searchPattern = `%${search}%`;
const searchConditions: any[] = [
{ user: { firstName: { $ilike: searchPattern } } },
{ user: { lastName: { $ilike: searchPattern } } },
{ user: { phone: { $ilike: searchPattern } } },
];
// If search is numeric, also search by orderNumber
const numericSearch = Number(search);
if (!isNaN(numericSearch) && isFinite(numericSearch)) {
searchConditions.push({ orderNumber: numericSearch });
}
where.$or = searchConditions;
}
// First, fetch orders without reviews
const [data, total] = await this.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: [],
});
const totalPages = Math.ceil(total / limit);
return {
data,
meta: {
total,
page,
limit,
totalPages,
},
};
}
}
@@ -15,6 +15,7 @@ import { CreditRepository } from 'src/modules/user/repositories/credit.repositor
import { CreditService } from 'src/modules/user/providers/credit.service';
import { CreditTransactionType } from 'src/modules/user/interface/credit';
import { Payment } from '../entities/payment.entity';
import { FindPaymentsDto } from '../dto/find-payments.dto';
@@ -170,11 +171,10 @@ export class PaymentService {
};
}
async verifyOnlinePayment(transactionId: string): Promise<Payment> {
async verifyOnlinePayment(token: string): Promise<Payment> {
const payment = await this.em.transactional(async em => {
const payment = await em.findOne(
Payment,
{ transactionId },
const payment = await this.paymentRepository.findOne(
{ token },
{ populate: ['order'] },
);
@@ -222,17 +222,17 @@ export class PaymentService {
return payment;
}
// findAllPaymentsBy(: string, userId: string) {
// return this.em.find(
// Payment,
// { order: { restaurant: { id: }, user: { id: userId } } },
// { populate: ['order', 'order.paymentMethod'] },
// );
// }
findAll(dto: FindPaymentsDto) {
return this.paymentRepository.findAllPaginated(dto)
}
async verifyCashPayment(id: string): Promise<Payment> {
findAllByUser(dto: FindPaymentsDto, userId: string) {
return this.paymentRepository.findAllPaginated({ ...dto, userId })
}
async verifyCashPayment(paymentId: string): Promise<Payment> {
const payment = await this.em.transactional(async em => {
const payment = await em.findOne(Payment, id, { populate: ['order'] });
const payment = await em.findOne(Payment, paymentId, { populate: ['order'] });
if (!payment) {
throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
}