diff --git a/database/seeder.runner.ts b/database/seeder.runner.ts index 77f4535..b2a0b3e 100644 --- a/database/seeder.runner.ts +++ b/database/seeder.runner.ts @@ -11,6 +11,7 @@ const logger = new Logger("Seeder"); export const runSeeder = async () => { await connectionSource.initialize(); logger.log("start seeding database"); + // await seedRole(connectionSource, logger); await seedAdmin(connectionSource, logger); await seedNotifSettings(connectionSource, logger); diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index a1bd16a..59cc2c6 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -313,6 +313,7 @@ export const enum InvoiceMessage { USER_ID_SHOULD_BE_A_UUID = "شناسه کاربر باید یک UUID معتبر باشد", CREATED = "فاکتور با موفقیت ایجاد شد", SEARCH_QUERY_MUST_BE_A_STRING = "رشته جستجو باید یک رشته باشد", + NOT_FOUND_BY_ID = "فاکتوری با این شناسه یافت نشد", } export const enum LearningMessage { diff --git a/src/common/transformers/decimal.transformer.ts b/src/common/transformers/decimal.transformer.ts index 7995d1a..bbfda1b 100644 --- a/src/common/transformers/decimal.transformer.ts +++ b/src/common/transformers/decimal.transformer.ts @@ -1,13 +1,22 @@ // eslint-disable-next-line import/no-named-as-default import Decimal from "decimal.js"; +import { ValueTransformer } from "typeorm"; -export class DecimalTransformer { - to(value: Decimal | string | number): string { - return new Decimal(value).toString(); +export class DecimalTransformer implements ValueTransformer { + // to(value: Decimal | string | number): string { + // return new Decimal(value).toString(); + // } + + // from(value: string): number { + // return new Decimal(value).toNumber(); + // } + + to(value: Decimal | null): string | null { + return value ? value.toString() : "0"; // Ensure it's always a valid number } - from(value: string): number { - return new Decimal(value).toNumber(); + from(value: string | null): number { + return value ? new Decimal(value).toNumber() : new Decimal(0).toNumber(); // Default to 0 if null } } diff --git a/src/modules/danak-services/danak-services.controller.ts b/src/modules/danak-services/danak-services.controller.ts index 6c27e98..380a0fa 100644 --- a/src/modules/danak-services/danak-services.controller.ts +++ b/src/modules/danak-services/danak-services.controller.ts @@ -63,7 +63,7 @@ export class DanakServicesController { @Roles(RoleEnum.ADMIN) @ApiOperation({ summary: "toggle status of categories => admin route" }) @HttpCode(HttpStatus.OK) - @Post("category/toggle-status/:id") + @Post("categories/toggle-status/:id") toggleCategoryStatus(@Param() paramDto: ParamDto) { return this.danakServicesService.toggleCategoryStatus(paramDto); } diff --git a/src/modules/invoices/DTO/invoices-search-query.dto.ts b/src/modules/invoices/DTO/invoices-search-query.dto.ts index b932a6f..8b71911 100644 --- a/src/modules/invoices/DTO/invoices-search-query.dto.ts +++ b/src/modules/invoices/DTO/invoices-search-query.dto.ts @@ -1,4 +1,4 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; +import { ApiPropertyOptional, PickType } from "@nestjs/swagger"; import { IsEnum, IsOptional, IsString, IsUUID } from "class-validator"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; @@ -26,3 +26,5 @@ export class InvoicesSearchQueryDto extends PaginationDto { @ApiPropertyOptional({ enum: InvoiceStatus, description: "invoice status", example: InvoiceStatus.PAID }) status: InvoiceStatus; } + +export class UserInvoicesSearchQueryDto extends PickType(InvoicesSearchQueryDto, ["status", "limit", "page"]) {} diff --git a/src/modules/invoices/entities/invoice-item.entity.ts b/src/modules/invoices/entities/invoice-item.entity.ts index 15fc85d..9dc4774 100644 --- a/src/modules/invoices/entities/invoice-item.entity.ts +++ b/src/modules/invoices/entities/invoice-item.entity.ts @@ -18,12 +18,13 @@ export class InvoiceItem extends BaseEntity { @Column({ type: "int", nullable: false }) count: number; - @Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() }) - unitPrice: Decimal; - @Column({ type: "int", nullable: false }) discount: number; + // + @Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() }) + unitPrice: Decimal; + @Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() }) totalPrice: Decimal; diff --git a/src/modules/invoices/invoices.controller.ts b/src/modules/invoices/invoices.controller.ts index e4697f0..1325c5a 100644 --- a/src/modules/invoices/invoices.controller.ts +++ b/src/modules/invoices/invoices.controller.ts @@ -1,12 +1,14 @@ -import { Body, Controller, Get, Post, Query } from "@nestjs/common"; +import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common"; import { ApiOperation } from "@nestjs/swagger"; import { CreateInvoiceDto } from "./DTO/create-invoice.dto"; -import { InvoicesSearchQueryDto } from "./DTO/invoices-search-query.dto"; +import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "./DTO/invoices-search-query.dto"; import { InvoicesService } from "./providers/invoices.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { Pagination } from "../../common/decorators/pagination.decorator"; import { Roles } from "../../common/decorators/roles.decorator"; +import { UserDec } from "../../common/decorators/user.decorator"; +import { ParamDto } from "../../common/DTO/param.dto"; import { RoleEnum } from "../users/enums/role.enum"; @Controller("invoices") @@ -29,4 +31,21 @@ export class InvoicesController { getInvoices(@Query() queryDto: InvoicesSearchQueryDto) { return this.invoiceService.getInvoices(queryDto); } + + @ApiOperation({ summary: "get all user invoices" }) + @AuthGuards() + @Roles(RoleEnum.USER) + @Pagination() + @Get("user") + getUserInvoices(@Query() queryDto: UserInvoicesSearchQueryDto, @UserDec("id") userId: string) { + return this.invoiceService.getUserInvoices(queryDto, userId); + } + + @ApiOperation({ summary: "get single invoice by Id " }) + @AuthGuards() + @Roles(RoleEnum.ADMIN, RoleEnum.USER) + @Get(":id") + getInvoiceById(@Param() paramDto: ParamDto, @UserDec("role") role: RoleEnum, @UserDec("id") userId: string) { + return this.invoiceService.getInvoiceById(paramDto.id, role, userId); + } } diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 4d6f9ac..dcf9321 100644 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -1,13 +1,14 @@ -import { Injectable } from "@nestjs/common"; +import { BadRequestException, Injectable } from "@nestjs/common"; // eslint-disable-next-line import/no-named-as-default import Decimal from "decimal.js"; import { QueryRunner } from "typeorm"; import { InvoiceMessage } from "../../../common/enums/message.enum"; import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; +import { RoleEnum } from "../../users/enums/role.enum"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { CreateInvoiceDto } from "../DTO/create-invoice.dto"; -import { InvoicesSearchQueryDto } from "../DTO/invoices-search-query.dto"; +import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "../DTO/invoices-search-query.dto"; import { Invoice } from "../entities/invoice.entity"; import { InvoiceStatus } from "../enums/invoice-status.enum"; // import { InvoiceItemsRepository } from "../repositories/invoice-items.repository"; @@ -31,7 +32,7 @@ export class InvoicesService { totalPrice: item.unitPrice * item.count - (item.unitPrice * item.count * item.discount) / 100, }; }); - + // const totalPrice = invoiceItems.reduce((sum, item) => new Decimal(item.totalPrice).add(sum), new Decimal(0)); const tax = totalPrice.mul(0.1); // @@ -40,7 +41,7 @@ export class InvoicesService { // const invoice = this.invoiceRepository.create({ user: { id: createDto.userId }, - totalPrice, + totalPrice: totalPrice.toNumber(), items: invoiceItems, tax: tax.toNumber(), dueDate, @@ -56,8 +57,9 @@ export class InvoicesService { } ///********************************** */ async createInvoiceForSubscription(userId: string, plan: SubscriptionPlan, dueDate: Date, queryRunner: QueryRunner) { - const invoiceItems = [{ name: plan.name, count: 1, unitPrice: plan.price, discount: 0, subscriptionPlan: plan }]; - + const invoiceItems = [ + { name: plan.service.name, count: 1, unitPrice: plan.price, discount: 0, subscriptionPlan: plan, totalPrice: plan.price }, + ]; const invoice = queryRunner.manager.create(Invoice, { totalPrice: plan.price, user: { id: userId }, @@ -66,7 +68,7 @@ export class InvoicesService { dueDate, items: invoiceItems, }); - + // await queryRunner.manager.save(Invoice, invoice); return invoice; } @@ -81,7 +83,8 @@ export class InvoicesService { queryBuilder .leftJoinAndSelect("invoice.user", "user") .leftJoinAndSelect("invoice.items", "items") - .leftJoinAndSelect("items.subscriptionPlan", "subscriptionPlan"); + .leftJoinAndSelect("items.subscriptionPlan", "subscriptionPlan", "subscriptionPlan.id IS NOT NULL") + .leftJoinAndSelect("subscriptionPlan.service", "service", "subscriptionPlan.id IS NOT NULL"); if (queryDto.q) { queryBuilder @@ -102,12 +105,60 @@ export class InvoicesService { queryBuilder.andWhere("subscriptionPlan.serviceId = :serviceId", { serviceId: queryDto.serviceId }); } - queryBuilder.orderBy("createdAt", "DESC"); + queryBuilder.orderBy("invoice.createdAt", "DESC"); queryBuilder.skip(skip); queryBuilder.take(limit); const [invoices, count] = await queryBuilder.getManyAndCount(); + return { + invoices, + count, + paginate: true, + }; + } + //*********************************** */ + async getInvoiceById(invoiceId: string, role: RoleEnum, userId: string) { + let invoice: Invoice | null; + + if (role === RoleEnum.ADMIN) { + invoice = await this.invoiceRepository.findOne({ + where: { id: invoiceId }, + relations: { items: { subscriptionPlan: true }, user: true }, + }); + } else { + invoice = await this.invoiceRepository.findOne({ + where: { id: invoiceId, user: { id: userId } }, + relations: { items: { subscriptionPlan: true } }, + }); + } + + if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); + + return { + invoice, + }; + } + //*********************************** */ + async getUserInvoices(queryDto: UserInvoicesSearchQueryDto, userId: string) { + const { limit, skip } = PaginationUtils(queryDto); + + const queryBuilder = this.invoiceRepository.createQueryBuilder("invoice"); + + queryBuilder + .andWhere("invoice.user.id = :userId", { userId: userId }) + .leftJoinAndSelect("invoice.items", "items") + .leftJoinAndSelect("items.subscriptionPlan", "subscriptionPlan", "subscriptionPlan.id IS NOT NULL") + .leftJoinAndSelect("subscriptionPlan.service", "service", "subscriptionPlan.id IS NOT NULL"); + + if (queryDto.status) { + queryBuilder.andWhere("invoice.status = :status", { status: queryDto.status }); + } + + queryBuilder.orderBy("invoice.createdAt", "DESC").skip(skip).take(limit); + + const [invoices, count] = await queryBuilder.getManyAndCount(); + return { invoices, count, diff --git a/src/modules/subscriptions/providers/subscriptions.service.ts b/src/modules/subscriptions/providers/subscriptions.service.ts index 1f35c65..e14dea7 100644 --- a/src/modules/subscriptions/providers/subscriptions.service.ts +++ b/src/modules/subscriptions/providers/subscriptions.service.ts @@ -131,7 +131,6 @@ export class SubscriptionsService { invoiceDueDate.setDate(userSubscription.endDate.getDate() - 3); const invoice = await this.invoicesService.createInvoiceForSubscription(userId, plan, invoiceDueDate, queryRunner); - userSubscription.status = SubscriptionStatus.ACTIVE; await queryRunner.manager.save(UserSubscription, userSubscription);