chore: implenet the external invoice

This commit is contained in:
mahyargdz
2025-07-14 10:24:58 +03:30
parent 5fe82bb661
commit 11b8164c28
13 changed files with 241 additions and 30 deletions
@@ -24,12 +24,13 @@ import { SmsService } from "../../utils/providers/sms.service";
import { Wallet } from "../../wallets/entities/wallet.entity";
import { WalletsService } from "../../wallets/providers/wallets.service";
import { INVOICE } from "../constants";
import { CreateInvoiceDto, InvoiceItemDto } from "../DTO/create-invoice.dto";
import { CreateExternalInvoiceDto, CreateInvoiceDto, InvoiceItemDto } from "../DTO/create-invoice.dto";
import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "../DTO/invoices-search-query.dto";
import { UpdateInvoiceDto } from "../DTO/update-invoice.dto";
import { Invoice } from "../entities/invoice.entity";
import { RecurringPeriodEnum } from "../enums/invoice-recurring-period.enum";
import { InvoiceStatus } from "../enums/invoice-status.enum";
import { IExternalInvoiceJob } from "../interfaces/external-invoice.interface";
import { InvoiceItemsRepository } from "../repositories/invoice-items.repository";
import { InvoicesRepository } from "../repositories/invoices.repository";
@Injectable()
@@ -38,6 +39,7 @@ export class InvoicesService {
constructor(
@InjectQueue(INVOICE.QUEUE_NAME) private readonly invoiceQueue: Queue,
@InjectQueue(INVOICE.EXTERNAL_QUEUE_NAME) private readonly externalInvoiceQueue: Queue,
private readonly notificationQueue: NotificationQueue,
private readonly invoiceRepository: InvoicesRepository,
private readonly invoiceItemsRepository: InvoiceItemsRepository,
@@ -591,6 +593,24 @@ export class InvoicesService {
await this.walletsService.createInvoiceTransaction(invoice.totalPrice, userWallet.id, queryRunner);
await this.addNotifyForWalletDeduction(invoice, user, userWallet, WalletMessage.INVOICE_WALLET_TRANSFER);
}
if (invoice.isExternal && invoice.items?.[0]?.subscriptionPlan) {
const externalInvoiceJobData: IExternalInvoiceJob = {
invoice,
items: invoice.items,
businessId: invoice.externalBusinessId!,
danakSubscriptionId: invoice.items[0].subscriptionPlan.id,
timestamp: dayjs().toISOString(),
};
await this.externalInvoiceQueue.add(INVOICE.EXTERNAL_CALLBACK_JOB_NAME, externalInvoiceJobData, {
delay: dayjs().add(5000, "millisecond").diff(dayjs()),
attempts: INVOICE.EXTERNAL_JOB_ATTEMPTS,
backoff: { type: "exponential", delay: INVOICE.EXTERNAL_JOB_BACKOFF },
priority: INVOICE.EXTERNAL_JOB_PRIORITY,
});
}
invoice.status = InvoiceStatus.PAID;
invoice.paidAt = dayjs().toDate();
@@ -751,6 +771,93 @@ export class InvoicesService {
await queryRunner.release();
}
}
//*********************************** */
async createInvoiceForExternalService(createDto: CreateExternalInvoiceDto) {
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const subscription = await queryRunner.manager.findOne(UserSubscription, {
where: { id: createDto.danakSubscriptionId },
relations: { user: true },
});
if (!subscription) throw new BadRequestException(InvoiceMessage.DANAK_SUBSCRIPTION_NOT_FOUND);
const user = subscription.user;
this.validateInvoiceItems(createDto.items);
this.validateRecurringInvoice(createDto);
const invoiceItems = createDto.items.map((item) => ({
name: item.name,
count: item.count,
unitPrice: item.unitPrice,
discount: item.discount || 0,
totalPrice: item.unitPrice * item.count - (item.unitPrice * item.count * item.discount) / 100,
subscriptionPlan: subscription,
}));
const totalPrice = invoiceItems.reduce((sum, item) => new Decimal(item.totalPrice).add(sum), new Decimal(0));
const tax = totalPrice.mul(0.1);
if (totalPrice.lessThanOrEqualTo(0)) throw new BadRequestException(InvoiceMessage.TOTAL_PRICE_MUST_BE_POSITIVE);
const dueDate = dayjs().add(INVOICE.EXTERNAL_INVOICE_DUEDATE, "day").toDate();
const invoice = queryRunner.manager.create(this.invoiceRepository.target, {
user,
totalPrice: totalPrice.add(tax).toNumber(),
originalPrice: totalPrice.add(tax).toNumber(),
items: invoiceItems,
tax: tax.toNumber(),
dueDate,
status: InvoiceStatus.PENDING,
isRecurring: createDto.isRecurring,
recurringPeriod: createDto.recurringPeriod,
maxRecurringCycles: createDto.maxRecurringCycles,
currentRecurringCycle: 0,
isExternal: true,
externalBusinessId: createDto.businessId,
});
await queryRunner.manager.save(this.invoiceRepository.target, invoice);
await this.notificationQueue.addInvoiceCreationNotification(user.id, {
invoiceId: invoice.numericId.toString(),
dueDate: invoice.dueDate,
createDate: invoice.createdAt,
price: new Decimal(invoice.totalPrice).toNumber(),
userPhone: user.phone,
userEmail: user.email,
items: invoiceItems.map((item) => item.name).join(", "),
paidAt: invoice.paidAt,
});
await this.scheduleInvoiceJobs(invoice, createDto);
await queryRunner.commitTransaction();
return {
message: InvoiceMessage.CREATED,
invoice,
};
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Failed to create invoice: ${error instanceof Error ? error.message : "Unknown error"}`,
error instanceof Error ? error.stack : "",
);
throw error;
} finally {
await queryRunner.release();
}
}
//*********************************** */
private async getInvoiceByIdWithQueryRunner(invoiceId: string, userId: string, queryRunner: QueryRunner) {
const invoice = await queryRunner.manager.findOne(Invoice, {