fix pagination et filter

This commit is contained in:
Mamadou Khoussa [028918 DSI/DAC/DIF/DS] 2025-12-01 10:13:12 +00:00
parent 0af15e26fc
commit e4c4383ceb
8 changed files with 614 additions and 77 deletions

View File

@ -0,0 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsInt, IsOptional, Min, Max } from 'class-validator';
export class PaginationDto {
@ApiProperty({
description: 'Page number',
minimum: 1,
default: 1,
required: false,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@ApiProperty({
description: 'Number of items per page',
minimum: 1,
maximum: 100,
default: 10,
required: false,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 10;
}

View File

@ -0,0 +1,11 @@
export interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
};
}

View File

@ -7,9 +7,12 @@ import {
IsEnum,
IsDateString,
isNumber,
IsInt,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { PaymentType, TransactionStatus } from 'generated/prisma';
import { PaginationDto } from 'src/common/dto/pagination.dto';
export class ChargeDto {
@ApiProperty({ description: 'User token from authentication' })
@ -77,46 +80,6 @@ export class RefundDto {
metadata?: Record<string, any>;
}
export class PaymentQueryDto {
@ApiProperty({ required: false, enum: ['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED'] })
@IsOptional()
@IsEnum(['PENDING', 'SUCCESS', 'FAILED', 'REFUNDED'])
status?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
userId?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsNumber()
subscriptionId?: number;
@ApiProperty({ required: false })
@IsOptional()
@IsDateString()
startDate?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsDateString()
endDate?: string;
@ApiProperty({ required: false, default: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number = 1;
@ApiProperty({ required: false, default: 20 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit?: number = 20;
}
export class PaymentResponseDto {
@ApiProperty()
@ -168,3 +131,156 @@ export class PaymentListResponseDto {
totalPages: number;
};
}
export class PaymentQueryDto extends PaginationDto {
@ApiProperty({
description: 'Filter by payment type',
enum: PaymentType,
required: false,
})
@IsOptional()
@IsEnum(PaymentType)
type?: PaymentType;
@ApiProperty({
description: 'Filter by transaction status',
enum: TransactionStatus,
required: false,
})
@IsOptional()
@IsEnum(TransactionStatus)
status?: TransactionStatus;
@ApiProperty({
description: 'Filter by merchant partner ID',
required: false,
})
@IsOptional()
@Type(() => Number)
@IsInt()
merchantPartnerId?: number;
@ApiProperty({
description: 'Filter by customer ID',
required: false,
})
@IsOptional()
@Type(() => Number)
@IsInt()
customerId?: number;
@ApiProperty({
description: 'Filter by subscription ID',
required: false,
})
@IsOptional()
@Type(() => Number)
@IsInt()
subscriptionId?: number;
@ApiProperty({
description: 'Search by external reference',
required: false,
})
@IsOptional()
@IsString()
externalReference?: string;
@ApiProperty({
description: 'Search by payment reference',
required: false,
})
@IsOptional()
@IsString()
reference?: string;
@ApiProperty({
description: 'Filter by currency code (e.g., XOF, XAF, EUR)',
example: 'XOF',
required: false,
})
@IsOptional()
@IsString()
currency?: string;
@ApiProperty({
description: 'Filter payments with amount greater than or equal to this value',
required: false,
})
@IsOptional()
@Type(() => Number)
amountMin?: number;
@ApiProperty({
description: 'Filter payments with amount less than or equal to this value',
required: false,
})
@IsOptional()
@Type(() => Number)
amountMax?: number;
@ApiProperty({
description: 'Filter payments created from this date (ISO format)',
example: '2024-01-01T00:00:00Z',
required: false,
})
@IsOptional()
@IsDateString()
createdFrom?: string;
@ApiProperty({
description: 'Filter payments created until this date (ISO format)',
example: '2024-12-31T23:59:59Z',
required: false,
})
@IsOptional()
@IsDateString()
createdTo?: string;
@ApiProperty({
description: 'Filter payments completed from this date (ISO format)',
example: '2024-01-01T00:00:00Z',
required: false,
})
@IsOptional()
@IsDateString()
completedFrom?: string;
@ApiProperty({
description: 'Filter payments completed until this date (ISO format)',
example: '2024-12-31T23:59:59Z',
required: false,
})
@IsOptional()
@IsDateString()
completedTo?: string;
@ApiProperty({
description: 'Filter only failed payments (with failure reason)',
required: false,
})
@IsOptional()
@Type(() => Boolean)
hasFailureReason?: boolean;
@ApiProperty({
description: 'Sort field',
enum: ['createdAt', 'completedAt', 'amount'],
default: 'createdAt',
required: false,
})
@IsOptional()
@IsString()
sortBy?: 'createdAt' | 'completedAt' | 'amount' = 'createdAt';
@ApiProperty({
description: 'Sort order',
enum: ['asc', 'desc'],
default: 'desc',
required: false,
})
@IsOptional()
@IsEnum(['asc', 'desc'])
sortOrder?: 'asc' | 'desc' = 'desc';
}

View File

@ -125,23 +125,32 @@ export class PaymentsController {
@Get('/')
@ApiOperation({
summary: 'Get payment list with pagination and filters',
description: 'Retrieve payments with optional filters on status, type, dates, amounts, etc.'
})
@ApiResponse({
status: 200,
description: 'Paginated list of payments',
})
@ApiOperation({ summary: 'Get payments list' })
async getAll(@Request() req) {
return this.paymentsService.findAll();
async getAll(@Request() req, @Query() paymentQueryDto: PaymentQueryDto) {
return this.paymentsService.findAll(paymentQueryDto);
}
@Get('merchant/:merchantId')
@ApiOperation({ summary: 'Get payments list by merchant' })
async getAllByPaymentByMerchant(@Request() req, @Param('merchantId', ParseIntPipe) merchantId: number) {
return this.paymentsService.findAllByMerchant(merchantId);
async getAllByPaymentByMerchant(@Request() req, @Param('merchantId', ParseIntPipe) merchantId: number, @Query() queryDto: Omit<PaymentQueryDto, 'merchantPartnerId'>,) {
return this.paymentsService.findAll({ ...queryDto, merchantPartnerId: merchantId });
}
@Get('merchant/:merchantId/subscription/:subscriptionId')
@ApiOperation({ summary: 'Get payments list by merchant' })
async getAllBySubscription(@Request() req,
@Param('merchantId', ParseIntPipe) merchantId: number,
@Param('subscriptionId', ParseIntPipe) subscriptionId: number) {
return this.paymentsService.findAllByMerchantSubscription(merchantId,subscriptionId);
@Param('subscriptionId', ParseIntPipe) subscriptionId: number,
@Query() queryDto: Omit<PaymentQueryDto, 'merchantPartnerId' | 'subscriptionId'>,) {
return this.paymentsService.findAll({ ...queryDto, merchantPartnerId: merchantId, subscriptionId: subscriptionId });
}

View File

@ -2,9 +2,10 @@ import { Injectable, BadRequestException, Logger } from '@nestjs/common';
import { OperatorsService } from '../operators/operators.service';
import { PrismaService } from '../../shared/services/prisma.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ChargeDto } from './dto/payment.dto';
import { ChargeDto, PaymentQueryDto } from './dto/payment.dto';
import { RefundDto } from './dto/payment.dto';
import { PaymentType, TransactionStatus } from 'generated/prisma';
import { Payment, PaymentType, Prisma, TransactionStatus } from 'generated/prisma';
import { PaginatedResponse } from 'src/common/interfaces/paginated-response.interface';
@Injectable()
export class PaymentsService {
@ -64,17 +65,149 @@ export class PaymentsService {
});
}
async findAll(): Promise<any[]> {
// Check if merchant exists
return this.prisma.payment.findMany({
// where: { merchantPartnerId: merchantId },
async findAll(
queryDto: PaymentQueryDto,
): Promise<PaginatedResponse<Payment>> {
const {
page = 1,
limit = 10,
sortBy = 'createdAt',
sortOrder = 'desc',
} = queryDto;
orderBy: {
createdAt: 'desc',
const skip = (page - 1) * limit;
const where = this.buildWhereClause(queryDto);
// Construction du orderBy dynamique
const orderBy: Prisma.PaymentOrderByWithRelationInput = {
[sortBy]: sortOrder,
};
// Exécuter les requêtes en parallèle
const [payments, total] = await Promise.all([
this.prisma.payment.findMany({
where,
skip,
take: limit,
orderBy,
// Optionnel: inclure les relations
// include: {
// merchantPartner: true,
// subscription: true,
// reversementRequests: true,
// },
}),
this.prisma.payment.count({ where }),
]);
const totalPages = Math.ceil(total / limit);
return {
data: payments,
meta: {
total,
page,
limit,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
},
});
};
}
private buildWhereClause(
filters: Partial<PaymentQueryDto>
): Prisma.PaymentWhereInput {
const {
type,
status,
merchantPartnerId,
customerId,
subscriptionId,
externalReference,
reference,
currency,
amountMin,
amountMax,
createdFrom,
createdTo,
completedFrom,
completedTo,
hasFailureReason,
} = filters;
const where: Prisma.PaymentWhereInput = {};
// Filtres simples
if (type) where.type = type;
if (status) where.status = status;
if (merchantPartnerId) where.merchantPartnerId = merchantPartnerId;
if (customerId) where.customerId = customerId;
if (subscriptionId) where.subscriptionId = subscriptionId;
if (currency) where.currency = currency;
// Filtres de recherche par référence
if (externalReference) {
where.externalReference = {
contains: externalReference,
mode: 'insensitive',
};
}
if (reference) {
where.reference = {
contains: reference,
mode: 'insensitive',
};
}
// Filtre sur les montants
if (amountMin !== undefined || amountMax !== undefined) {
where.amount = {};
if (amountMin !== undefined) {
where.amount.gte = amountMin;
}
if (amountMax !== undefined) {
where.amount.lte = amountMax;
}
}
// Filtres sur createdAt
if (createdFrom || createdTo) {
where.createdAt = {};
if (createdFrom) {
where.createdAt.gte = new Date(createdFrom);
}
if (createdTo) {
where.createdAt.lte = new Date(createdTo);
}
}
// Filtres sur completedAt
if (completedFrom || completedTo) {
where.completedAt = {};
if (completedFrom) {
where.completedAt.gte = new Date(completedFrom);
}
if (completedTo) {
where.completedAt.lte = new Date(completedTo);
}
}
// Filtre sur les paiements échoués
if (hasFailureReason !== undefined) {
if (hasFailureReason) {
where.failureReason = { not: null };
} else {
where.failureReason = null;
}
}
return where;
}
refundPayment(paymentId: string, partnerId: any, refundDto: RefundDto) {
throw new Error('Method not implemented.');
}

View File

@ -1,5 +1,8 @@
import { IsString, IsOptional, IsNumber, IsEnum, IsBoolean, Min } from 'class-validator';
import { IsString, IsOptional, IsNumber, IsEnum,IsDateString, IsBoolean, Min, IsInt } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { PaginationDto } from 'src/common/dto/pagination.dto';
import { Type } from 'class-transformer';
import { Periodicity, SubscriptionStatus } from 'generated/prisma';
export class CreateSubscriptionDto {
@ApiProperty()
@ -45,3 +48,122 @@ export class UpdateSubscriptionDto {
@IsOptional()
metadata?: Record<string, any>;
}
export class SubscriptionQueryDto extends PaginationDto {
@ApiProperty({
description: 'Filter by merchant partner ID',
required: false,
})
@IsOptional()
@Type(() => Number)
@IsInt()
merchantPartnerId?: number;
@ApiProperty({
description: 'Filter by subscription status',
enum: SubscriptionStatus,
required: false,
})
@IsOptional()
@IsEnum(SubscriptionStatus)
status?: SubscriptionStatus;
@ApiProperty({
description: 'Filter by periodicity',
enum: Periodicity,
required: false,
})
@IsOptional()
@IsEnum(Periodicity)
periodicity?: Periodicity;
@ApiProperty({
description: 'Filter by service ID',
required: false,
})
@IsOptional()
@Type(() => Number)
@IsInt()
serviceId?: number;
@ApiProperty({
description: 'Filter subscriptions starting from this date (ISO format)',
example: '2024-01-01',
required: false,
})
@IsOptional()
@IsDateString()
startDateFrom?: string;
@ApiProperty({
description: 'Filter subscriptions starting until this date (ISO format)',
example: '2024-12-31',
required: false,
})
@IsOptional()
@IsDateString()
startDateTo?: string;
@ApiProperty({
description: 'Filter subscriptions ending from this date (ISO format)',
example: '2024-01-01',
required: false,
})
@IsOptional()
@IsDateString()
endDateFrom?: string;
@ApiProperty({
description: 'Filter subscriptions ending until this date (ISO format)',
example: '2024-12-31',
required: false,
})
@IsOptional()
@IsDateString()
endDateTo?: string;
@ApiProperty({
description: 'Filter subscriptions created from this date (ISO format)',
example: '2024-01-01T00:00:00Z',
required: false,
})
@IsOptional()
@IsDateString()
createdFrom?: string;
@ApiProperty({
description: 'Filter subscriptions created until this date (ISO format)',
example: '2024-12-31T23:59:59Z',
required: false,
})
@IsOptional()
@IsDateString()
createdTo?: string;
@ApiProperty({
description: 'Filter subscriptions with next payment from this date',
required: false,
})
@IsOptional()
@IsDateString()
nextPaymentFrom?: string;
@ApiProperty({
description: 'Filter subscriptions with next payment until this date',
required: false,
})
@IsOptional()
@IsDateString()
nextPaymentTo?: string;
@ApiProperty({
description: 'Filter by customer ID',
required: false,
})
@IsOptional()
@Type(() => Number)
@IsInt()
customerId?: number;
}

View File

@ -13,10 +13,11 @@ import {
Logger,
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { ApiTags, ApiOperation, ApiQuery,ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
import { SubscriptionsService } from './subscriptions.service';
import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription.dto';
import { CreateSubscriptionDto, SubscriptionQueryDto, UpdateSubscriptionDto } from './dto/subscription.dto';
import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
import { PaginationDto } from 'src/common/dto/pagination.dto';
@ApiTags('subscriptions')
@Controller('subscriptions')
@ -41,15 +42,21 @@ export class SubscriptionsController {
}
@Get('/')
@ApiOperation({ summary: 'Get subscription list' })
async getAll(@Request() req) {
return this.subscriptionsService.findAll();
@ApiOperation({ summary: 'Get subscription list with pagination' })
@ApiQuery({ type: PaginationDto })
@ApiResponse({
status: 200,
description: 'Paginated list of subscriptions',
})
async getAll(@Request() req, @Query() paginationDto: SubscriptionQueryDto,) {
return this.subscriptionsService.findAll(paginationDto);
}
@Get('merchant/:merchantId')
@ApiOperation({ summary: 'Get subscription list by merchant' })
async getAllByMErchant(@Request() req, @Param('merchantId', ParseIntPipe) merchantId: number) {
return this.subscriptionsService.findAllByMerchant(merchantId);
async getAllByMErchant(@Request() req, @Param('merchantId', ParseIntPipe,) merchantId: number, paginationDto: Omit<SubscriptionQueryDto, 'merchantPartnerId'> ,) {
const page = {...paginationDto, merchantPartnerId: merchantId};
return this.subscriptionsService.findAll(page);
}

View File

@ -3,9 +3,11 @@ import { InjectQueue } from '@nestjs/bull';
import bull from 'bull';
import { PrismaService } from '../../shared/services/prisma.service';
import { PaymentsService } from '../payments/payments.service';
import { CreateSubscriptionDto, UpdateSubscriptionDto } from './dto/subscription.dto';
import { Subscription } from 'generated/prisma';
import { CreateSubscriptionDto, SubscriptionQueryDto, UpdateSubscriptionDto } from './dto/subscription.dto';
import { Prisma, Subscription } from 'generated/prisma';
import { OperatorsService } from '../operators/operators.service';
import { PaginationDto } from 'src/common/dto/pagination.dto';
import { PaginatedResponse } from 'src/common/interfaces/paginated-response.interface';
//import { SubscriptionStatus } from '@prisma/client';
//import { SubscriptionStatus, Prisma } from '@prisma/client';
@ -46,16 +48,122 @@ export class SubscriptionsService {
});
}
async findAll(): Promise<Subscription[]> {
async findAll( paginationDto: SubscriptionQueryDto,): Promise<PaginatedResponse<Subscription>> {
const { page = 1, limit = 10, status,
periodicity,
merchantPartnerId,
customerId,
serviceId,
startDateFrom,
startDateTo,
endDateFrom,
endDateTo,
createdFrom,
createdTo,
nextPaymentFrom,
nextPaymentTo, } = paginationDto;
const skip = (page - 1) * limit;
// Construction du where clause dynamique
const where: Prisma.SubscriptionWhereInput = {};
// Filtre par status
if (status) {
where.status = status;
}
// Filtre par periodicity
if (periodicity) {
where.periodicity = periodicity;
}
// Filtre par IDs
if (merchantPartnerId) {
where.merchantPartnerId = merchantPartnerId;
}
if (customerId) {
where.customerId = customerId;
}
if (serviceId) {
where.serviceId = serviceId;
}
// Filtres sur startDate
if (startDateFrom || startDateTo) {
where.startDate = {};
if (startDateFrom) {
where.startDate.gte = new Date(startDateFrom);
}
if (startDateTo) {
where.startDate.lte = new Date(startDateTo);
}
}
// Filtres sur endDate
if (endDateFrom || endDateTo) {
where.endDate = {};
if (endDateFrom) {
where.endDate.gte = new Date(endDateFrom);
}
if (endDateTo) {
where.endDate.lte = new Date(endDateTo);
}
}
// Filtres sur createdAt
if (createdFrom || createdTo) {
where.createdAt = {};
if (createdFrom) {
where.createdAt.gte = new Date(createdFrom);
}
if (createdTo) {
where.createdAt.lte = new Date(createdTo);
}
}
// Filtres sur nextPaymentDate
if (nextPaymentFrom || nextPaymentTo) {
where.nextPaymentDate = {};
if (nextPaymentFrom) {
where.nextPaymentDate.gte = new Date(nextPaymentFrom);
}
if (nextPaymentTo) {
where.nextPaymentDate.lte = new Date(nextPaymentTo);
}
}
// Check if merchant exists
return this.prisma.subscription.findMany({
// where: { merchantPartnerId: merchantId },
const [subscriptions, total] = await Promise.all([
this.prisma.subscription.findMany({
where,
skip,
take: limit,
orderBy: {
createdAt: 'desc',
},
});
// Vous pouvez inclure des relations si nécessaire
// include: {
// merchantPartner: true,
// service: true,
// },
}),
this.prisma.subscription.count(),
]);
const totalPages = Math.ceil(total / limit);
return {
data: subscriptions,
meta: {
total,
page,
limit,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
},
};
}
getInvoices(id: string, partnerId: any) {