invoice
This commit is contained in:
@@ -7,7 +7,13 @@ export enum PermissionEnum {
|
||||
VIEW_REQUESTS = 'view_requests',
|
||||
DELETE_REQUEST = 'delete_request',
|
||||
UPDATE_REQUEST = 'update_request',
|
||||
|
||||
|
||||
// invoice
|
||||
VIEW_INVOICES = 'view_invoices',
|
||||
CREATE_INVOICE = 'create_invoice',
|
||||
UPDATE_INVOICE = 'update_invoice',
|
||||
DELETE_INVOICE = 'delete_invoice',
|
||||
|
||||
VIEW_All_INVOICED_ORDERS = 'view_all_invoiced_orders',
|
||||
VIEW_MY_ASSIGNED_INVOICED_REQUESTS = 'view_my_assigned_invoiced_requests',
|
||||
|
||||
@@ -63,5 +69,9 @@ export const PermissionTitles: Record<PermissionEnum, string> = {
|
||||
[PermissionEnum.VIEW_MY_ASSIGNED_INVOICED_REQUESTS]: "مشاهده همه پیش فاکتور های من",
|
||||
[PermissionEnum.CREATE_ORDER_PRINT]: "ایجاد سفارش چاپ",
|
||||
[PermissionEnum.DELETE_REQUEST]: "",
|
||||
[PermissionEnum.UPDATE_REQUEST]: ""
|
||||
[PermissionEnum.UPDATE_REQUEST]: "",
|
||||
[PermissionEnum.VIEW_INVOICES]: "",
|
||||
[PermissionEnum.CREATE_INVOICE]: "",
|
||||
[PermissionEnum.UPDATE_INVOICE]: "",
|
||||
[PermissionEnum.DELETE_INVOICE]: ""
|
||||
};
|
||||
|
||||
@@ -1 +1,62 @@
|
||||
export class CreateInvoiceDto {}
|
||||
import {
|
||||
IsString,
|
||||
IsInt,
|
||||
IsArray,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
ArrayMinSize,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateInvoiceItemDto {
|
||||
@IsString()
|
||||
@ApiProperty({ description: 'Product ID' })
|
||||
productId!: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@ApiProperty({ example: 1 })
|
||||
quantity!: number;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@ApiProperty({ example: 10000 })
|
||||
unitPrice: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@ApiPropertyOptional({ example: 0 })
|
||||
discount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class CreateInvoiceDto {
|
||||
@IsString()
|
||||
@ApiProperty({ description: 'Request ID to create invoice for' })
|
||||
requestId!: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@Type(() => CreateInvoiceItemDto)
|
||||
@ApiProperty({ type: [CreateInvoiceItemDto] })
|
||||
items!: CreateInvoiceItemDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ example: false })
|
||||
enableTax?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
@ApiPropertyOptional({ description: 'Approval deadline (ISO date string)' })
|
||||
approvalDeadline?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsNumber,
|
||||
Min,
|
||||
Max,
|
||||
IsIn,
|
||||
IsDateString,
|
||||
IsBoolean,
|
||||
} from 'class-validator';
|
||||
import { Type, Transform } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
const sortOrderOptions = ['asc', 'desc'] as const;
|
||||
type SortOrder = (typeof sortOrderOptions)[number];
|
||||
|
||||
export class FindInvoicesDto {
|
||||
@ApiPropertyOptional({ default: 1, minimum: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
page: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ default: 10, minimum: 1, maximum: 50 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
limit: number = 10;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Search by invoice number or user info',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'date-time' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
from?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'date-time' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
to?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Filter by enableTax',
|
||||
})
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
return value;
|
||||
})
|
||||
@IsBoolean()
|
||||
enableTax?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: 'createdAt' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
orderBy: string = 'createdAt';
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: sortOrderOptions,
|
||||
default: 'desc',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(sortOrderOptions)
|
||||
order: SortOrder = 'desc';
|
||||
}
|
||||
@@ -18,8 +18,8 @@ export class InvoiceItem extends BaseEntity {
|
||||
@Property({ type: 'int' })
|
||||
quantity!: number;
|
||||
|
||||
@Property({ type: 'int', nullable: true, })
|
||||
unitPrice?: number;
|
||||
@Property({ type: 'int'})
|
||||
unitPrice: number;
|
||||
|
||||
@Property({
|
||||
type: 'int',
|
||||
|
||||
@@ -1,34 +1,73 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||
import { InvoiceService } from './invoice.service';
|
||||
import { CreateInvoiceDto } from './dto/create-invoice.dto';
|
||||
import { UpdateInvoiceDto } from './dto/update-invoice.dto';
|
||||
import { AdminAuthGuard } from '../auth/guards/adminAuth.guard';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { PermissionEnum } from 'src/common/enums/permission.enum';
|
||||
import { AuthGuard } from '../auth/guards/auth.guard';
|
||||
import { FindInvoicesDto } from './dto/find-invoices.dto';
|
||||
import { UserId } from 'src/common/decorators';
|
||||
|
||||
@Controller('invoice')
|
||||
@Controller()
|
||||
@ApiTags('invoice')
|
||||
@ApiBearerAuth()
|
||||
export class InvoiceController {
|
||||
constructor(private readonly invoiceService: InvoiceService) {}
|
||||
constructor(private readonly invoiceService: InvoiceService) { }
|
||||
|
||||
@Post()
|
||||
create(@Body() createInvoiceDto: CreateInvoiceDto) {
|
||||
return this.invoiceService.create(createInvoiceDto);
|
||||
@Get('public/invoice')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Get all invoices with pagination and filters' })
|
||||
findAll(@Query() dto: FindInvoicesDto, @UserId() userId: string) {
|
||||
return this.invoiceService.findAll(userId, dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.invoiceService.findAll();
|
||||
@Get('public/invoice/:id')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Get one invoice by id' })
|
||||
findOne(@Param('id') id: string,@UserId() userId: string) {
|
||||
return this.invoiceService.findOne(id,userId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.invoiceService.findOne(+id);
|
||||
@Get('public/invoice/item/:id')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Get one invoice item by id' })
|
||||
confirmInvoiceItem(@Param('id') id: string,@UserId() userId: string) {
|
||||
return this.invoiceService.confirmInvoiceItem(id,userId);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
//------------------- Admin Routes-------------
|
||||
@Post('admin/invoice')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(PermissionEnum.CREATE_INVOICE)
|
||||
@ApiOperation({ summary: 'Create a new invoice (admin)' })
|
||||
@ApiBody({ type: CreateInvoiceDto })
|
||||
create(@Body() dto: CreateInvoiceDto) {
|
||||
return this.invoiceService.create(dto);
|
||||
}
|
||||
|
||||
@Get('admin/invoice')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Get all invoices with pagination and filters' })
|
||||
findAllAsAdmin(@Query() dto: FindInvoicesDto) {
|
||||
return this.invoiceService.findAllAsAdmin(dto);
|
||||
}
|
||||
|
||||
@Patch('admin/invoice/:id')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(PermissionEnum.UPDATE_INVOICE)
|
||||
@ApiOperation({ summary: 'Update an invoice (admin)' })
|
||||
@ApiBody({ type: UpdateInvoiceDto })
|
||||
update(@Param('id') id: string, @Body() updateInvoiceDto: UpdateInvoiceDto) {
|
||||
return this.invoiceService.update(+id, updateInvoiceDto);
|
||||
return this.invoiceService.update(id, updateInvoiceDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete('admin/invoice/:id')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(PermissionEnum.DELETE_INVOICE)
|
||||
@ApiOperation({ summary: 'Remove an invoice (admin)' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.invoiceService.remove(+id);
|
||||
return this.invoiceService.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { InvoiceService } from './invoice.service';
|
||||
import { InvoiceController } from './invoice.controller';
|
||||
import { Invoice } from './entities/invoice.entity';
|
||||
import { InvoiceItem } from './entities/invoice-item.entity';
|
||||
import { ProductModule } from '../product/product.module';
|
||||
import { InvoiceRepository } from './repositories/invoice.repository';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Invoice, InvoiceItem]),
|
||||
ProductModule,
|
||||
],
|
||||
controllers: [InvoiceController],
|
||||
providers: [InvoiceService],
|
||||
providers: [InvoiceService, InvoiceRepository],
|
||||
})
|
||||
export class InvoiceModule {}
|
||||
|
||||
@@ -1,26 +1,152 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { CreateInvoiceDto } from './dto/create-invoice.dto';
|
||||
import { UpdateInvoiceDto } from './dto/update-invoice.dto';
|
||||
import { FindInvoicesDto } from './dto/find-invoices.dto';
|
||||
import { Invoice } from './entities/invoice.entity';
|
||||
import { InvoiceItem } from './entities/invoice-item.entity';
|
||||
import { Request } from '../request/entities/request.entity';
|
||||
import { ProductService } from '../product/providers/product.service';
|
||||
import { InvoiceRepository } from './repositories/invoice.repository';
|
||||
|
||||
const TAX_RATE = 0.09;
|
||||
|
||||
@Injectable()
|
||||
export class InvoiceService {
|
||||
create(createInvoiceDto: CreateInvoiceDto) {
|
||||
return 'This action adds a new invoice';
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly productService: ProductService,
|
||||
private readonly invoiceRepository: InvoiceRepository,
|
||||
) { }
|
||||
|
||||
|
||||
async create(dto: CreateInvoiceDto) {
|
||||
const request = await this.em.findOne(
|
||||
Request,
|
||||
{ id: dto.requestId },
|
||||
{ populate: ['user'] },
|
||||
);
|
||||
if (!request) {
|
||||
throw new NotFoundException('Request not found');
|
||||
}
|
||||
|
||||
const enableTax = dto.enableTax ?? false;
|
||||
const approvalDeadline = dto.approvalDeadline
|
||||
? new Date(dto.approvalDeadline)
|
||||
: undefined;
|
||||
|
||||
const discount = dto.items.reduce((acc, item) => acc + (item.discount ?? 0), 0);
|
||||
|
||||
return this.em.transactional(async (em) => {
|
||||
const invoice = em.create(Invoice, {
|
||||
user: request.user,
|
||||
request,
|
||||
enableTax,
|
||||
approvalDeadline,
|
||||
discount,
|
||||
paidAmount: 0,
|
||||
balance: 0,
|
||||
});
|
||||
em.persist(invoice);
|
||||
|
||||
let subTotal = 0;
|
||||
|
||||
for (const item of dto.items) {
|
||||
const product = await this.productService.findOneOrFail(item.productId);
|
||||
const unitPrice = item.unitPrice;
|
||||
const itemDiscount = item.discount ?? 0;
|
||||
const itemTotal = Math.max(0, unitPrice * item.quantity - itemDiscount);
|
||||
subTotal += itemTotal;
|
||||
|
||||
const invoiceItem = em.create(InvoiceItem, {
|
||||
invoice,
|
||||
product,
|
||||
quantity: item.quantity,
|
||||
unitPrice: unitPrice,
|
||||
discount: itemDiscount || undefined,
|
||||
description: item.description,
|
||||
});
|
||||
invoice.items.add(invoiceItem);
|
||||
}
|
||||
|
||||
const taxAmount = enableTax
|
||||
? Math.round((subTotal - discount) * TAX_RATE)
|
||||
: 0;
|
||||
const total = Math.max(0, subTotal - discount + taxAmount);
|
||||
|
||||
invoice.subTotal = subTotal;
|
||||
invoice.taxAmount = taxAmount;
|
||||
invoice.total = total;
|
||||
invoice.balance = total - invoice.paidAmount;
|
||||
|
||||
await em.flush();
|
||||
return invoice;
|
||||
});
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return `This action returns all invoice`;
|
||||
findAll(userId: string, dto: FindInvoicesDto) {
|
||||
return this.invoiceRepository.findAllPaginated({ userId, ...dto });
|
||||
}
|
||||
|
||||
findOne(id: number) {
|
||||
return `This action returns a #${id} invoice`;
|
||||
findAllAsAdmin(dto: FindInvoicesDto) {
|
||||
return this.invoiceRepository.findAllPaginated(dto);
|
||||
}
|
||||
|
||||
update(id: number, updateInvoiceDto: UpdateInvoiceDto) {
|
||||
return `This action updates a #${id} invoice`;
|
||||
async findOne(id: string, userId: string) {
|
||||
const invoice = await this.em.findOne(
|
||||
Invoice,
|
||||
{ id, user: { id: userId } },
|
||||
{ populate: ['user', 'request', 'items', 'items.product'] },
|
||||
);
|
||||
if (!invoice) {
|
||||
throw new NotFoundException('Invoice not found');
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
return `This action removes a #${id} invoice`;
|
||||
async confirmInvoiceItem(id: string, userId: string) {
|
||||
const item = await this.em.findOne(
|
||||
InvoiceItem,
|
||||
{ id, invoice: { user: { id: userId } } },
|
||||
{ populate: ['invoice', 'product'] },
|
||||
);
|
||||
if (!item) {
|
||||
throw new NotFoundException('Invoice item not found');
|
||||
}
|
||||
if(item.confirmedAt){
|
||||
throw new BadRequestException('Invoice item already confirmed');
|
||||
}
|
||||
item.confirmedAt = new Date();
|
||||
await this.em.persistAndFlush(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
async update(id: string, updateInvoiceDto: UpdateInvoiceDto) {
|
||||
const invoice = await this.findOneOrFail(id);
|
||||
if (updateInvoiceDto.enableTax !== undefined) invoice.enableTax = updateInvoiceDto.enableTax;
|
||||
if (updateInvoiceDto.approvalDeadline !== undefined) {
|
||||
invoice.approvalDeadline = new Date(updateInvoiceDto.approvalDeadline);
|
||||
}
|
||||
await this.em.flush();
|
||||
return invoice;
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const invoice = await this.findOneOrFail(id);
|
||||
this.em.remove(invoice);
|
||||
await this.em.flush();
|
||||
return invoice;
|
||||
}
|
||||
|
||||
async findOneOrFail(id: string) {
|
||||
const invoice = await this.em.findOne(
|
||||
Invoice,
|
||||
{ id },
|
||||
{ populate: ['user', 'request', 'items', 'items.product'] },
|
||||
);
|
||||
if (!invoice) {
|
||||
throw new NotFoundException('Invoice not found');
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { FilterQuery } from '@mikro-orm/core';
|
||||
import { Invoice } from '../entities/invoice.entity';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { FindInvoicesDto } from '../dto/find-invoices.dto';
|
||||
|
||||
interface FindInvoicesOpts extends FindInvoicesDto {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class InvoiceRepository extends EntityRepository<Invoice> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, Invoice);
|
||||
}
|
||||
|
||||
async findAllPaginated(opts: FindInvoicesOpts): Promise<PaginatedResult<Invoice>> {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
search,
|
||||
orderBy = 'createdAt',
|
||||
order = 'desc',
|
||||
userId,
|
||||
from,
|
||||
to,
|
||||
enableTax,
|
||||
} = opts;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const where: FilterQuery<Invoice> = {};
|
||||
|
||||
if (userId) {
|
||||
where.user = { id: userId };
|
||||
}
|
||||
|
||||
if (typeof enableTax === 'boolean') {
|
||||
where.enableTax = enableTax;
|
||||
}
|
||||
|
||||
if (from || to) {
|
||||
where.createdAt = {};
|
||||
if (from) {
|
||||
where.createdAt.$gte = new Date(from);
|
||||
}
|
||||
if (to) {
|
||||
where.createdAt.$lte = new Date(to);
|
||||
}
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const searchPattern = `%${search}%`;
|
||||
const searchConditions: FilterQuery<Invoice>[] = [
|
||||
{ user: { firstName: { $ilike: searchPattern } } } as FilterQuery<Invoice>,
|
||||
{ user: { lastName: { $ilike: searchPattern } } } as FilterQuery<Invoice>,
|
||||
{ user: { phone: { $ilike: searchPattern } } } as FilterQuery<Invoice>,
|
||||
];
|
||||
|
||||
const numericSearch = Number(search);
|
||||
if (!isNaN(numericSearch) && isFinite(numericSearch)) {
|
||||
searchConditions.push({ invoiceNumber: numericSearch } as FilterQuery<Invoice>);
|
||||
}
|
||||
|
||||
where.$or = searchConditions;
|
||||
}
|
||||
|
||||
const [data, total] = await this.findAndCount(where, {
|
||||
limit,
|
||||
offset,
|
||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||
populate: ['user', 'request', 'items', 'items.product'],
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Query } from '@nestjs/common';
|
||||
import { ApiTags,ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { RequestService } from './request.service';
|
||||
import { CreateRequestDto } from './dto/create-request.dto';
|
||||
import { UpdateRequestDto } from './dto/update-request.dto';
|
||||
@@ -10,6 +11,8 @@ import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { PermissionEnum } from 'src/common/enums/permission.enum';
|
||||
|
||||
@Controller()
|
||||
@ApiTags('request')
|
||||
@ApiBearerAuth()
|
||||
export class RequestController {
|
||||
constructor(private readonly requestService: RequestService) { }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user