fix: bill

This commit is contained in:
2026-02-17 15:44:21 +03:30
parent e905274a94
commit 2555cc12d1
9 changed files with 6910 additions and 33 deletions
+3
View File
@@ -21,12 +21,14 @@ export class BillsController {
@ApiOperation({ summary: "Create water bill" })
@Post("water")
@ApiHeader({ name: "x-business-id" })
createWaterBill(@Body() dto: CreateWaterBillDto, @BusinessDec() business: Business) {
return this.billsService.createWaterBill(dto, business);
}
@ApiOperation({ summary: "Create water bill from excel file" })
@ApiConsumes("multipart/form-data")
@ApiHeader({ name: "x-business-id" })
@UseInterceptors(FileInterceptor("file"))
@ApiBody({ type: CreateWaterBillFromExcelDto })
@Post("water/excel")
@@ -39,6 +41,7 @@ export class BillsController {
}
@ApiOperation({ summary: "Create charge bill" })
@ApiHeader({ name: "x-business-id" })
@Post("charge")
createChargeBill(@Body() dto: CreateChargeBillDto, @BusinessDec() business: Business) {
return this.billsService.createChargeBill(dto, business);
+7 -2
View File
@@ -8,10 +8,15 @@ import { BusinessesModule } from "../businesses/businesses.module";
import { CompaniesModule } from "../companies/companies.module";
import { NotificationModule } from "../notifications/notifications.module";
import { InvoicesModule } from "../invoices/invoices.module";
import { BullModule } from "@nestjs/bullmq";
import { INVOICE } from "../invoices/constants";
@Module({
imports: [MikroOrmModule.forFeature([Bill]), CompaniesModule, BusinessesModule,NotificationModule,InvoicesModule],
imports: [
BullModule.registerQueue({ name: INVOICE.QUEUE_NAME }),
MikroOrmModule.forFeature([Bill]), CompaniesModule, BusinessesModule
, NotificationModule, InvoicesModule],
controllers: [BillsController],
providers: [BillsService],
})
export class BillsModule {}
export class BillsModule { }
+39 -26
View File
@@ -19,7 +19,10 @@ import { CompaniesService } from "../companies/services/companies.service";
import { InvoiceItem } from "../invoices/entities/invoice-item.entity";
import { Invoice } from "../invoices/entities/invoice.entity";
import { NotificationQueue } from "../notifications/queue/notification.queue";
import { InvoicesService } from "../invoices/providers/invoices.service";
import { InjectQueue } from "@nestjs/bullmq";
import { INVOICE } from "../invoices/constants";
import { Queue } from "bullmq";
@Injectable()
export class BillsService {
@@ -27,8 +30,9 @@ export class BillsService {
private readonly em: EntityManager,
private readonly companiesService: CompaniesService,
private readonly billRepository: BillsRepository,
@InjectQueue(INVOICE.QUEUE_NAME)
private readonly invoiceQueue: Queue,
private readonly notificationQueue: NotificationQueue,
private readonly invoicesService: InvoicesService,
) { }
@@ -76,35 +80,14 @@ export class BillsService {
});
invoice.items.add(invoiceItem);
await this.notificationQueue.addInvoiceCreationNotification(company.user.id, {
invoiceId: invoice.numericId.toString(),
dueDate: invoice.dueDate,
createDate: invoice.createdAt,
price: new Decimal(invoice.totalPrice).toNumber(),
userPhone: company.user.phone,
userEmail: company.user.email,
items: invoiceItem.name,
paidAt: invoice.paidAt,
});
await this.invoicesService.scheduleInvoiceJobs(invoice, {
companyId: company.id,
items: [
{
name: invoiceItem.name,
count: invoiceItem.count,
unitPrice: new Decimal(invoiceItem.unitPrice).toNumber(),
discount: new Decimal(invoiceItem.discount).toNumber(),
},
],
});
em.persist(invoice);
}
await em.flush();
await em.commit();
await this.addInvoiceReminderJobs(bill.id);
return bill;
} catch (error) {
await em.rollback();
@@ -112,6 +95,34 @@ export class BillsService {
}
}
private async addInvoiceReminderJobs(billId: string) {
const invoices = await this.em.find(Invoice, { bill: { id: billId } }, { populate: ["items", "company","company.user"] });
for (const invoice of invoices) {
await this.notificationQueue.addInvoiceCreationNotification(invoice.company.user.id, {
invoiceId: invoice.numericId.toString(),
dueDate: invoice.dueDate,
createDate: invoice.createdAt,
price: new Decimal(invoice.totalPrice).toNumber(),
userPhone: invoice.company.user.phone,
userEmail: invoice.company.user.email,
items: invoice.items[0].name,
paidAt: invoice.paidAt,
});
await this.invoiceQueue.add(
INVOICE.REMINDER_JOB_NAME,
{ invoiceId: invoice.id },
{
delay: dayjs(invoice.dueDate).subtract(INVOICE.DAYS_BEFORE_OVERDUE, "day").diff(dayjs()),
attempts: INVOICE.REMINDER_JOB_ATTEMPTS,
backoff: { type: "exponential", delay: INVOICE.REMINDER_JOB_BACKOFF },
priority: INVOICE.REMINDER_JOB_PRIORITY,
},
);
}
}
async createWaterBillByExcel(file: IFile, dto: CreateWaterBillFromExcelDto, business: Business) {
const values = await this.parseWaterBillExcel(file.buffer);
if (values.length === 0) {
@@ -155,7 +166,7 @@ export class BillsService {
const companies = await this.companiesService.findAllBybusinessId(business.id, em);
for (const company of companies) {
const subtotal = new Decimal(chargeRate);
const subtotal = new Decimal(chargeRate).mul(company.metrage);
const tax = subtotal.mul(0.1);
const totalPrice = subtotal.add(tax);
@@ -185,6 +196,8 @@ export class BillsService {
await em.flush();
await em.commit();
await this.addInvoiceReminderJobs(bill.id)
return bill;
} catch (error) {
await em.rollback();
@@ -76,11 +76,10 @@ export class CreateCompanyDto extends PickType(CompleteRegistrationDto, ["phone"
@ApiProperty({ description: "آدرس تصویر کاور", example: "https://www.example.com/cover.jpg" })
coverImageUrl: string;
@IsOptional()
@IsInt({ message: CompanyMessage.METRAGE_MUST_BE_INT })
@Min(1, { message: CompanyMessage.METRAGE_MUST_BE_POSITIVE })
@ApiProperty({ description: "متراژ شرکت (متر مربع)", example: 500 })
metrage?: number;
metrage: number;
@IsOptional()
@IsNotEmpty({ message: CompanyMessage.IS_ACTIVE_REQUIRED })
@@ -126,6 +125,7 @@ export class CreateCompanyRequestDto extends PickType(CreateCompanyDto, [
"industryId",
"products",
"services",
"metrage"
]) {
@IsNotEmpty({ message: CompanyMessage.CHIEF_EXECUTIVE_REQUIRED })
@IsString({ message: CompanyMessage.CHIEF_EXECUTIVE_MUST_BE_STRING })
@@ -48,8 +48,8 @@ export class Company extends BaseEntity {
@Property({ type: "varchar", length: 255 })
coverImageUrl!: string;
@Property({ type: "int", nullable: true })
metrage?: number;
@Property({ type: "int", nullable: false })
metrage!: number;
@Property({ type: "boolean", default: true })
isActive!: boolean & Opt;