diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index e2aca36..a4fa537 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -391,6 +391,7 @@ export const enum InvoiceMessage { DISCOUNT_CANCELLED = "تخفیف با موفقیت لغو شد", INVOICE_IS_REJECTED = "فاکتور قبلا رد شده است", INVOICE_CAN_NOT_PAID = "فاکتور قابل پرداخت نیست", + INVALID_DATE = "تاریخ وارد شده معتبر نیست", } export const enum LearningMessage { diff --git a/src/modules/invoices/DTO/invoices-search-query.dto.ts b/src/modules/invoices/DTO/invoices-search-query.dto.ts index 8b71911..edc125f 100755 --- a/src/modules/invoices/DTO/invoices-search-query.dto.ts +++ b/src/modules/invoices/DTO/invoices-search-query.dto.ts @@ -1,5 +1,5 @@ import { ApiPropertyOptional, PickType } from "@nestjs/swagger"; -import { IsEnum, IsOptional, IsString, IsUUID } from "class-validator"; +import { IsDateString, IsEnum, IsOptional, IsString, IsUUID } from "class-validator"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { InvoiceMessage, ServiceMessage, UserMessage } from "../../../common/enums/message.enum"; @@ -9,22 +9,27 @@ export class InvoicesSearchQueryDto extends PaginationDto { @IsOptional() @IsString({ message: InvoiceMessage.SEARCH_QUERY_MUST_BE_A_STRING }) @ApiPropertyOptional({ description: "search query", example: "search query" }) - q: string; + q?: string; @IsOptional() @IsUUID("4", { message: ServiceMessage.SERVICE_ID_SHOULD_BE_A_UUID }) @ApiPropertyOptional({ description: "service id", example: "123e4567-e89b-12d3-a456-426614174000" }) - serviceId: string; + serviceId?: string; @IsOptional() @IsUUID("4", { message: UserMessage.USER_ID_SHOULD_BE_A_UUID }) @ApiPropertyOptional({ description: "user id", example: "123e4567-e89b-12d3-a456-426614174000" }) - userId: string; + userId?: string; @IsOptional() @IsEnum(InvoiceStatus) @ApiPropertyOptional({ enum: InvoiceStatus, description: "invoice status", example: InvoiceStatus.PAID }) - status: InvoiceStatus; + status?: InvoiceStatus; + + @IsOptional() + @IsDateString({}, { message: InvoiceMessage.INVALID_DATE }) + @ApiPropertyOptional({ description: "since query", example: "2025-01-27" }) + since?: string; } export class UserInvoicesSearchQueryDto extends PickType(InvoicesSearchQueryDto, ["status", "limit", "page"]) {} diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 18c5c2a..b9efcf2 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -161,42 +161,17 @@ export class InvoicesService { ///********************************** */ async getInvoices(queryDto: InvoicesSearchQueryDto) { - const { limit, skip } = PaginationUtils(queryDto); + const [invoices, count] = await this.invoiceRepository.getInvoicesForAdmin(queryDto); - const queryBuilder = this.invoiceRepository.createQueryBuilder("invoice"); - - queryBuilder + const usersWithInvoices = await this.invoiceRepository + .createQueryBuilder("invoice") .leftJoinAndSelect("invoice.user", "user") - .leftJoinAndSelect("invoice.items", "items") - .leftJoinAndSelect("items.subscriptionPlan", "subscriptionPlan", "subscriptionPlan.id IS NOT NULL") - .leftJoinAndSelect("subscriptionPlan.service", "service", "subscriptionPlan.id IS NOT NULL"); - - if (queryDto.q) { - queryBuilder - .orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` }) - .orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` }) - .orWhere("user.email ILIKE :search", { search: `%${queryDto.q}%` }); - } - - if (queryDto.status) { - queryBuilder.andWhere("invoice.status = :status", { status: queryDto.status }); - } - - if (queryDto.userId) { - queryBuilder.andWhere("invoice.user.id = :userId", { userId: queryDto.userId }); - } - - if (queryDto.serviceId) { - queryBuilder.andWhere("subscriptionPlan.serviceId = :serviceId", { serviceId: queryDto.serviceId }); - } - - queryBuilder.orderBy("invoice.createdAt", "DESC"); - queryBuilder.skip(skip); - queryBuilder.take(limit); - - const [invoices, count] = await queryBuilder.getManyAndCount(); + .select(["user.id", "user.firstName", "user.lastName", "user.email"]) + .distinctOn(["user.id"]) + .getMany(); return { + users: usersWithInvoices.map((inv) => inv.user), invoices, count, paginate: true, diff --git a/src/modules/invoices/repositories/invoices.repository.ts b/src/modules/invoices/repositories/invoices.repository.ts index d7090bc..e016589 100755 --- a/src/modules/invoices/repositories/invoices.repository.ts +++ b/src/modules/invoices/repositories/invoices.repository.ts @@ -2,6 +2,8 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; +import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { InvoicesSearchQueryDto } from "../DTO/invoices-search-query.dto"; import { Invoice } from "../entities/invoice.entity"; @Injectable() @@ -9,4 +11,47 @@ export class InvoicesRepository extends Repository { constructor(@InjectRepository(Invoice) invoicesRepository: Repository) { super(invoicesRepository.target, invoicesRepository.manager, invoicesRepository.queryRunner); } + + async getInvoicesForAdmin(queryDto: InvoicesSearchQueryDto) { + const { limit, skip } = PaginationUtils(queryDto); + + const queryBuilder = this.createQueryBuilder("invoice"); + + queryBuilder + .leftJoinAndSelect("invoice.user", "user") + .leftJoinAndSelect("invoice.items", "items") + .leftJoinAndSelect("items.subscriptionPlan", "subscriptionPlan", "subscriptionPlan.id IS NOT NULL") + .leftJoinAndSelect("subscriptionPlan.service", "service", "subscriptionPlan.id IS NOT NULL"); + + if (queryDto.q) { + queryBuilder + .orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` }) + .orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` }) + .orWhere("user.email ILIKE :search", { search: `%${queryDto.q}%` }); + } + + if (queryDto.status) { + queryBuilder.andWhere("invoice.status = :status", { status: queryDto.status }); + } + + if (queryDto.userId) { + queryBuilder.andWhere("invoice.user.id = :userId", { userId: queryDto.userId }); + } + + if (queryDto.serviceId) { + queryBuilder.andWhere("subscriptionPlan.serviceId = :serviceId", { serviceId: queryDto.serviceId }); + } + + if (queryDto.since) { + queryBuilder.andWhere("(invoice.createdAt >= :since OR invoice.dueDate >= :since OR invoice.paidAt >= :since)", { + since: new Date(queryDto.since), + }); + } + + queryBuilder.orderBy("invoice.createdAt", "DESC"); + queryBuilder.skip(skip); + queryBuilder.take(limit); + + return await queryBuilder.getManyAndCount(); + } }