add: bill module
This commit is contained in:
@@ -16,6 +16,7 @@ import { rateLimitConfig } from "./configs/rateLimit.config";
|
|||||||
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
||||||
import { AnnouncementsModule } from "./modules/announcements/announcement.module";
|
import { AnnouncementsModule } from "./modules/announcements/announcement.module";
|
||||||
import { AuthModule } from "./modules/auth/auth.module";
|
import { AuthModule } from "./modules/auth/auth.module";
|
||||||
|
import { BillsModule } from './modules/bills/bills.module';
|
||||||
import { BusinessesModule } from "./modules/businesses/businesses.module";
|
import { BusinessesModule } from "./modules/businesses/businesses.module";
|
||||||
import { ChatbotModule } from "./modules/chatbot/chatbot.module";
|
import { ChatbotModule } from "./modules/chatbot/chatbot.module";
|
||||||
import { CompaniesModule } from "./modules/companies/companies.module";
|
import { CompaniesModule } from "./modules/companies/companies.module";
|
||||||
@@ -65,6 +66,7 @@ import { UtilsModule } from "./modules/utils/utils.module";
|
|||||||
SlidersModule,
|
SlidersModule,
|
||||||
ChatbotModule,
|
ChatbotModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
|
BillsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UseInterceptors,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiOperation } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { BillsService } from "./bills.service";
|
||||||
|
import { BillListQueryDto } from "./dto/bill-list-query.dto";
|
||||||
|
import { CreateChargeBillDto } from "./dto/create-charge-bill.dto";
|
||||||
|
import { CreateWaterBillDto } from "./dto/create-water-bill.dto";
|
||||||
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
|
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||||
|
import { ParamDto } from "../../common/DTO/param.dto";
|
||||||
|
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
||||||
|
import { Business } from "../businesses/entities/business.entity";
|
||||||
|
|
||||||
|
@Controller("bills")
|
||||||
|
@UseInterceptors(BusinessInterceptor)
|
||||||
|
@AuthGuards()
|
||||||
|
export class BillsController {
|
||||||
|
constructor(private readonly billsService: BillsService) {}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Create water bill" })
|
||||||
|
@Post("water")
|
||||||
|
createWaterBill(@Body() dto: CreateWaterBillDto, @BusinessDec() business: Business) {
|
||||||
|
return this.billsService.createWaterBill(dto, business);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Create charge bill" })
|
||||||
|
@Post("charge")
|
||||||
|
createChargeBill(@Body() dto: CreateChargeBillDto, @BusinessDec() business: Business) {
|
||||||
|
return this.billsService.createChargeBill(dto, business);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Get all bills with filters and pagination" })
|
||||||
|
@Get()
|
||||||
|
findAll(@Query() queryDto: BillListQueryDto, @BusinessDec("id") businessId: string) {
|
||||||
|
return this.billsService.findAll(queryDto, businessId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Get a bill by id" })
|
||||||
|
@Get(":id")
|
||||||
|
findOne(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||||
|
return this.billsService.findOne(paramDto.id, businessId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Remove a bill" })
|
||||||
|
@Delete(":id")
|
||||||
|
remove(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||||
|
return this.billsService.remove(paramDto.id, businessId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { BillsController } from "./bills.controller";
|
||||||
|
import { BillsService } from "./bills.service";
|
||||||
|
import { Bill } from "./entities/bill.entity";
|
||||||
|
import { BusinessesModule } from "../businesses/businesses.module";
|
||||||
|
import { CompaniesModule } from "../companies/companies.module";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MikroOrmModule.forFeature([Bill]),
|
||||||
|
CompaniesModule,
|
||||||
|
BusinessesModule,
|
||||||
|
],
|
||||||
|
controllers: [BillsController],
|
||||||
|
providers: [BillsService],
|
||||||
|
})
|
||||||
|
export class BillsModule {}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
import { BillListQueryDto } from './dto/bill-list-query.dto';
|
||||||
|
import { CreateChargeBillDto } from './dto/create-charge-bill.dto';
|
||||||
|
import { CreateWaterBillDto } from './dto/create-water-bill.dto';
|
||||||
|
import { Bill } from './entities/bill.entity';
|
||||||
|
import { BillType } from './enums/bill-type.enum';
|
||||||
|
import { BillsRepository } from './repositories/bill.repository';
|
||||||
|
import { CompanyMessage } from '../../common/enums/message.enum';
|
||||||
|
import { Business } from '../businesses/entities/business.entity';
|
||||||
|
import { Company } from '../companies/entities/company.entity';
|
||||||
|
import { CompaniesService } from '../companies/services/companies.service';
|
||||||
|
import { InvoiceItem } from '../invoices/entities/invoice-item.entity';
|
||||||
|
import { Invoice } from '../invoices/entities/invoice.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BillsService {
|
||||||
|
constructor(
|
||||||
|
private readonly em: EntityManager,
|
||||||
|
private readonly companiesService: CompaniesService,
|
||||||
|
private readonly billRepository: BillsRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createWaterBill(dto: CreateWaterBillDto, business: Business) {
|
||||||
|
const { values, waterRate, dueDays } = dto;
|
||||||
|
const em = this.em.fork();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await em.begin();
|
||||||
|
|
||||||
|
const bill = em.create(Bill, { type: BillType.WATER_BILL, business });
|
||||||
|
em.persist(bill);
|
||||||
|
const dueDate = dayjs().add(dueDays, 'day').toDate();
|
||||||
|
|
||||||
|
const uniqueCompanyIds = [...new Set(values.map((v) => v.companyId))];
|
||||||
|
const companies = await this.companiesService.findByIds(uniqueCompanyIds, em);
|
||||||
|
const companyMap = new Map<string, Company>(companies.map((c) => [c.id, c]));
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
const subtotal = new Decimal(waterRate).mul(value.waterUsage);
|
||||||
|
const tax = subtotal.mul(0.1);
|
||||||
|
const totalPrice = subtotal.add(tax);
|
||||||
|
const company = companyMap.get(value.companyId);
|
||||||
|
if (!company) {
|
||||||
|
throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoice = em.create(Invoice, {
|
||||||
|
company,
|
||||||
|
totalPrice,
|
||||||
|
originalPrice: totalPrice,
|
||||||
|
dueDate,
|
||||||
|
tax,
|
||||||
|
business,
|
||||||
|
bill,
|
||||||
|
});
|
||||||
|
|
||||||
|
const invoiceItem = em.create(InvoiceItem, {
|
||||||
|
name: 'قبض آب',
|
||||||
|
count: value.waterUsage,
|
||||||
|
unitPrice: new Decimal(waterRate),
|
||||||
|
discount: new Decimal(0),
|
||||||
|
totalPrice,
|
||||||
|
invoice,
|
||||||
|
});
|
||||||
|
invoice.items.add(invoiceItem);
|
||||||
|
|
||||||
|
em.persist(invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
await em.flush();
|
||||||
|
await em.commit();
|
||||||
|
|
||||||
|
return bill;
|
||||||
|
} catch (error) {
|
||||||
|
await em.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createChargeBill(dto: CreateChargeBillDto, business: Business) {
|
||||||
|
const { chargeRate, dueDays } = dto;
|
||||||
|
const em = this.em.fork();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await em.begin();
|
||||||
|
|
||||||
|
const bill = em.create(Bill, { type: BillType.MONTHLY_CHARGE, business });
|
||||||
|
em.persist(bill);
|
||||||
|
const dueDate = dayjs().add(dueDays, 'day').toDate();
|
||||||
|
|
||||||
|
const companies = await this.companiesService.findAllBybusinessId(business.id, em);
|
||||||
|
|
||||||
|
for (const company of companies) {
|
||||||
|
const subtotal = new Decimal(chargeRate);
|
||||||
|
const tax = subtotal.mul(0.1);
|
||||||
|
const totalPrice = subtotal.add(tax);
|
||||||
|
|
||||||
|
const invoice = em.create(Invoice, {
|
||||||
|
company,
|
||||||
|
totalPrice,
|
||||||
|
originalPrice: totalPrice,
|
||||||
|
dueDate,
|
||||||
|
tax,
|
||||||
|
business,
|
||||||
|
bill,
|
||||||
|
});
|
||||||
|
|
||||||
|
const invoiceItem = em.create(InvoiceItem, {
|
||||||
|
name: 'قبض شارژ',
|
||||||
|
count: 1,
|
||||||
|
unitPrice: subtotal,
|
||||||
|
discount: new Decimal(0),
|
||||||
|
totalPrice,
|
||||||
|
invoice,
|
||||||
|
});
|
||||||
|
invoice.items.add(invoiceItem);
|
||||||
|
|
||||||
|
em.persist(invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
await em.flush();
|
||||||
|
await em.commit();
|
||||||
|
|
||||||
|
return bill;
|
||||||
|
} catch (error) {
|
||||||
|
await em.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(queryDto: BillListQueryDto, businessId: string) {
|
||||||
|
const [bills, count] = await this.billRepository.getBillsList(queryDto, businessId);
|
||||||
|
return {
|
||||||
|
bills,
|
||||||
|
count,
|
||||||
|
paginate: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string, businessId: string) {
|
||||||
|
const bill = await this.billRepository.findOne(
|
||||||
|
{ id, business: { id: businessId } },
|
||||||
|
{ populate: ["invoices"] },
|
||||||
|
);
|
||||||
|
if (!bill) {
|
||||||
|
throw new BadRequestException("Bill not found");
|
||||||
|
}
|
||||||
|
return { bill };
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string, businessId: string) {
|
||||||
|
const bill = await this.billRepository.findOne({
|
||||||
|
id,
|
||||||
|
business: { id: businessId },
|
||||||
|
});
|
||||||
|
if (!bill) {
|
||||||
|
throw new BadRequestException("Bill not found");
|
||||||
|
}
|
||||||
|
await this.em.removeAndFlush(bill);
|
||||||
|
return { deleted: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsDateString, IsEnum, IsOptional } from "class-validator";
|
||||||
|
|
||||||
|
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||||
|
import { BillType } from "../enums/bill-type.enum";
|
||||||
|
|
||||||
|
export class BillListQueryDto extends PaginationDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(BillType)
|
||||||
|
@ApiPropertyOptional({ enum: BillType, description: "Filter by bill type" })
|
||||||
|
type?: BillType;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
@ApiPropertyOptional({ description: "Filter bills created since this date", example: "2025-01-01" })
|
||||||
|
since?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
@ApiPropertyOptional({ description: "Filter bills created until this date", example: "2025-12-31" })
|
||||||
|
to?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsInt } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateChargeBillDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
chargeRate: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
dueDays:number;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { ArrayMinSize, IsArray, IsInt, IsNotEmpty, IsUUID, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
export class ValueDto {
|
||||||
|
@IsNotEmpty({ message: ' BillMessage.COMPANY_ID_REQUIRED' })
|
||||||
|
@ApiProperty({ description: "شناسه شرکت", })
|
||||||
|
@IsUUID("7")
|
||||||
|
companyId: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNotEmpty({ message: 'BillMessage.WATER_USAGE_REQUIRED' })
|
||||||
|
@IsInt()
|
||||||
|
@ApiProperty({ description: "مصرف آب", example: 100 })
|
||||||
|
waterUsage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateWaterBillDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
waterRate:number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
dueDays:number;
|
||||||
|
|
||||||
|
@Type(() => ValueDto)
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
values: ValueDto[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { CreateWaterBillDto } from './create-water-bill.dto';
|
||||||
|
|
||||||
|
export class UpdateBillDto extends PartialType(CreateWaterBillDto) {}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Cascade, Collection, Entity, EntityRepositoryType, Enum, ManyToOne, OneToMany } from "@mikro-orm/core";
|
||||||
|
|
||||||
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { Business } from "../../businesses/entities/business.entity";
|
||||||
|
import { Invoice } from "../../invoices/entities/invoice.entity";
|
||||||
|
import { BillType } from "../enums/bill-type.enum";
|
||||||
|
import { BillsRepository } from "../repositories/bill.repository";
|
||||||
|
|
||||||
|
@Entity({ repository: () => BillsRepository })
|
||||||
|
export class Bill extends BaseEntity {
|
||||||
|
|
||||||
|
@Enum({ items: () => BillType })
|
||||||
|
type: BillType;
|
||||||
|
|
||||||
|
@ManyToOne(() => Business, { nullable: false, deleteRule: "cascade" })
|
||||||
|
business!: Business;
|
||||||
|
|
||||||
|
@OneToMany(() => Invoice, (invoice) => invoice.bill, { cascade: [Cascade.PERSIST, Cascade.REMOVE], orphanRemoval: true })
|
||||||
|
invoices = new Collection<Invoice>(this);
|
||||||
|
|
||||||
|
[EntityRepositoryType]?: BillsRepository;
|
||||||
|
}
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
export enum BillType {
|
||||||
|
MONTHLY_CHARGE = "monthly_charge",
|
||||||
|
WATER_BILL = "water_bill",
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
import { FilterQuery } from "@mikro-orm/core";
|
||||||
|
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||||
|
|
||||||
|
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||||
|
import { BillListQueryDto } from "../dto/bill-list-query.dto";
|
||||||
|
import { Bill } from "../entities/bill.entity";
|
||||||
|
|
||||||
|
export class BillsRepository extends EntityRepository<Bill> {
|
||||||
|
async getBillsList(queryDto: BillListQueryDto, businessId: string) {
|
||||||
|
const { limit, skip } = PaginationUtils(queryDto);
|
||||||
|
|
||||||
|
const where: FilterQuery<Bill> = {
|
||||||
|
business: { id: businessId },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (queryDto.type) {
|
||||||
|
where.type = queryDto.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.since || queryDto.to) {
|
||||||
|
where.createdAt = {};
|
||||||
|
if (queryDto.since) {
|
||||||
|
(where.createdAt as Record<string, unknown>).$gte = new Date(queryDto.since);
|
||||||
|
}
|
||||||
|
if (queryDto.to) {
|
||||||
|
(where.createdAt as Record<string, unknown>).$lte = new Date(queryDto.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.findAndCount(where, {
|
||||||
|
populate: ["invoices"],
|
||||||
|
orderBy: { createdAt: "DESC" },
|
||||||
|
limit,
|
||||||
|
offset: skip,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -458,4 +458,17 @@ export class CompaniesService {
|
|||||||
|
|
||||||
return company;
|
return company;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findByIds(comapnyIds: string[], em: EntityManager) {
|
||||||
|
const companies = await em.find(Company, {
|
||||||
|
id: { $in: comapnyIds }
|
||||||
|
});
|
||||||
|
if (companies.length !== comapnyIds.length) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND);
|
||||||
|
return companies;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllBybusinessId(businessId: string, em: EntityManager) {
|
||||||
|
const companies = await em.find(Company, { business: { id: businessId } });
|
||||||
|
return companies;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Decimal } from "decimal.js";
|
|||||||
|
|
||||||
import { InvoiceItem } from "./invoice-item.entity";
|
import { InvoiceItem } from "./invoice-item.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { Bill } from "../../bills/entities/bill.entity";
|
||||||
import { Business } from "../../businesses/entities/business.entity";
|
import { Business } from "../../businesses/entities/business.entity";
|
||||||
import { Company } from "../../companies/entities/company.entity";
|
import { Company } from "../../companies/entities/company.entity";
|
||||||
import { Payment } from "../../payments/entities/payment.entity";
|
import { Payment } from "../../payments/entities/payment.entity";
|
||||||
@@ -56,6 +57,9 @@ export class Invoice extends BaseEntity {
|
|||||||
@ManyToOne(() => Business, { nullable: false, deleteRule: "cascade" })
|
@ManyToOne(() => Business, { nullable: false, deleteRule: "cascade" })
|
||||||
business!: Business;
|
business!: Business;
|
||||||
|
|
||||||
|
@ManyToOne(() => Bill, { nullable: true, deleteRule: "cascade" })
|
||||||
|
bill?: Bill;
|
||||||
|
|
||||||
//-----------------------------------
|
//-----------------------------------
|
||||||
|
|
||||||
@OneToMany(() => InvoiceItem, (item) => item.invoice, { cascade: [Cascade.PERSIST, Cascade.REMOVE], orphanRemoval: true })
|
@OneToMany(() => InvoiceItem, (item) => item.invoice, { cascade: [Cascade.PERSIST, Cascade.REMOVE], orphanRemoval: true })
|
||||||
|
|||||||
Reference in New Issue
Block a user