invoice number

This commit is contained in:
2026-04-04 15:24:29 +03:30
parent b29bf380ce
commit b5d555e9ab
15 changed files with 583 additions and 529 deletions
+1
View File
@@ -54,6 +54,7 @@ export class AuthGuard implements CanActivate {
throw new UnauthorizedException('Invalid or expired token');
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
+7 -2
View File
@@ -8,16 +8,21 @@ import { NotifChannelEnum, NotifTitle } from 'src/modules/notification/interface
import { ChatCreatedByAdminEvent } from '../events/chat.events';
import { PermissionEnum } from 'src/common/enums/permission.enum';
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class ChatListeners {
private readonly logger = new Logger(ChatListeners.name);
private newChatSmsTemplateId: string;
constructor(
private readonly em: EntityManager,
private readonly notificationService: NotificationService,
private readonly adminRepository: AdminRepository,
) { }
private readonly configService: ConfigService,
) {
this.newChatSmsTemplateId = this.configService.get<string>('SMS_PATTERN_NEW_CHAT') ?? 'new-chat';
}
@OnEvent(ChatCreatedByAdminEvent.name)
async handleChatCreatedByAdmin(event: ChatCreatedByAdminEvent) {
@@ -43,7 +48,7 @@ export class ChatListeners {
title: NotifTitle.NEW_TICKET,
content: body,
sms: {
templateId: '',
templateId: this.newChatSmsTemplateId,
parameters: {},
},
},
@@ -1,4 +1,4 @@
import { Collection, Entity, ManyToOne, OptionalProps, Property,OneToMany } from '@mikro-orm/core';
import { Collection, Entity, ManyToOne, OptionalProps, Property, OneToMany, Opt } from '@mikro-orm/core';
import { Product } from 'src/modules/product/entities/product.entity';
import { BaseEntity } from 'src/common/entities/base.entity';
import { Invoice } from './invoice.entity';
@@ -7,7 +7,7 @@ import { Order } from 'src/modules/order/entities/order.entity';
@Entity({ tableName: 'invoice_items' })
export class InvoiceItem extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'deletedAt' | 'subTotal' | 'total';
[OptionalProps]?: 'createdAt' | 'deletedAt' | 'discount' | 'description' | 'confirmedAt';
@ManyToOne(() => Invoice)
invoice!: Invoice;
@@ -22,16 +22,15 @@ export class InvoiceItem extends BaseEntity {
@Property({ type: 'int' })
quantity!: number;
@Property({ type: 'int' })
unitPrice: number;
@Property({ type: 'int', fieldName: 'unit_price' })
unitPrice!: number;
@Property({
type: 'int',
// columnType: 'int GENERATED ALWAYS AS (COALESCE(unit_price, 0) * quantity) STORED',
// persist: false,
nullable: true
columnType: 'int GENERATED ALWAYS AS (COALESCE(unit_price, 0) * quantity) STORED',
persist: false,
})
subTotal!: number;
subTotal!: number & Opt;
@Property({
type: 'int',
@@ -41,11 +40,10 @@ export class InvoiceItem extends BaseEntity {
@Property({
type: 'int',
nullable: true,
// columnType: 'int GENERATED ALWAYS AS ((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED',
// persist: false
columnType: 'int GENERATED ALWAYS AS ((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED',
persist: false
})
total!: number;
total!: number & Opt;
@Property({ type: 'text', nullable: true })
description?: string;
+14 -13
View File
@@ -9,6 +9,7 @@ import {
Enum,
PrimaryKey,
OptionalProps,
Opt,
} from '@mikro-orm/core';
import { User } from '../../user/entities/user.entity';
import { Payment } from 'src/modules/payment/entities/payment.entity';
@@ -23,7 +24,7 @@ import { IAttachment } from 'src/modules/order/interface/order.interface';
@Entity({ tableName: 'invoices' })
@Index({ properties: ['user'] })
export class Invoice extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'deletedAt' | 'invoiceNumber' | 'request'
[OptionalProps]?: 'createdAt' | 'deletedAt'
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
id: string = ulid()
@@ -48,34 +49,34 @@ export class Invoice extends BaseEntity {
@Property({
type: 'int',
autoincrement: true
type: 'int',
unique: true
})
invoiceNumber: number;
@Property({ type: 'int', nullable: true })
discount?: number;
@Property({ type: 'int' })
discount!: number & Opt;
@Property({ type: 'int', nullable: true })
subTotal?: number;
@Property({ type: 'int' })
subTotal!: number & Opt;
@Property({ type: 'int', nullable: true })
taxAmount?: number;
@Property({ type: 'int' })
taxAmount!: number & Opt;
@Property({ type: 'int', nullable: true })
total?: number;
@Property({ type: 'int' })
total!: number & Opt;
@Property({ type: 'int', default: 0 })
paidAmount: number = 0;
@Property({ type: 'int', default: 0 })
balance: number = 0;
@Property({ type: 'int' })
balance: number & Opt;
// @Enum(() => InvoiceStatusEnum)
+11 -11
View File
@@ -61,7 +61,7 @@ export class InvoiceService {
approvalDeadline,
discount,
paidAmount: 0,
balance: 0,
// balance: 0,
attachments: [],
paymentMethod: dto.paymentMethod,
invoiceNumber: 20,
@@ -69,14 +69,14 @@ export class InvoiceService {
});
em.persist(invoice);
let subTotal = 0;
// 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;
// subTotal += itemTotal;
const invoiceItem = em.create(InvoiceItem, {
invoice,
@@ -89,16 +89,16 @@ export class InvoiceService {
invoice.items.add(invoiceItem);
}
const taxAmount = enableTax
? Math.round((subTotal - discount) * TAX_RATE)
: 0;
const total = Math.max(0, subTotal - discount + taxAmount);
// const taxAmount = enableTax
// ? Math.round((subTotal - discount) * TAX_RATE)
// : 0;
// const total = Math.max(0, subTotal - discount + taxAmount);
invoice.subTotal = subTotal;
// invoice.subTotal = subTotal;
invoice.attachments = dto.attachments;
invoice.taxAmount = taxAmount;
invoice.total = total;
invoice.balance = total - invoice.paidAmount;
// invoice.taxAmount = taxAmount;
// invoice.total = total;
// invoice.balance = total - invoice.paidAmount;
if (dto.requestId) {
const requestEntity = await em.findOne(
+3 -4
View File
@@ -3,10 +3,9 @@ import type { OrderStatusEnum } from '../interface/order.interface';
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly: string,
public readonly orderNumber: string,
public readonly total: number,
) { }
public readonly userId: string,
public readonly orderNumber: number,
) {}
}
export class OrderStatusChangedEvent {
+37 -192
View File
@@ -1,212 +1,57 @@
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events';
import { PermissionEnum } from 'src/common/enums/permission.enum';
import { OrderCreatedEvent } from '../events/order.events';
import { NotifTitle } from 'src/modules/notification/interfaces/notification.interface';
import { NotificationService } from 'src/modules/notification/services/notification.service';
import { ConfigService } from '@nestjs/config';
import { OrderRepository } from '../repositories/order.repository';
// import { OrderStatusEnum } from '../interface/order.interface';
import { PaymentMethodEnum } from 'src/modules/payment/interface/payment';
import { UserService } from 'src/modules/user/providers/user.service';
import { NotifChannelEnum } from 'src/modules/notification/interfaces/notification.interface';
@Injectable()
export class OrderListeners {
private readonly logger = new Logger(OrderListeners.name);
private orderCreatedSmsTemplateId: string;
private orderStatusChangedSmsTemplateId: string;
// private orderCompletedSmsTemplateId: string;
constructor(
private readonly adminService: AdminRepository,
private readonly OrderRepository: OrderRepository,
private readonly notificationService: NotificationService,
private readonly configService: ConfigService,
private readonly userService: UserService,
) {
this.orderCreatedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_CREATED') ?? '123';
this.orderStatusChangedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_CHANGE') ?? '123';
// this.orderCompletedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_COMPLETED') ?? '123';
this.orderCreatedSmsTemplateId =
this.configService.get<string>('SMS_PATTERN_ORDER_CREATED') ?? '';
}
// private getStatusFarsi(status: OrderStatus): string {
// const statusMap: Record<OrderStatus, string> = {
// [OrderStatus.PENDING_PAYMENT]: 'در انتظار پرداخت',
// [OrderStatus.PAID]: 'پرداخت شده',
// [OrderStatus.PREPARING]: 'در حال آماده‌سازی',
// [OrderStatus.DELIVERED_TO_RECEPTIONIST]: 'تحویل به پذیرش',
// [OrderStatus.DELIVERED_TO_WAITER]: 'تحویل به گارسون',
// [OrderStatus.SHIPPED]: 'ارسال شده',
// [OrderStatus.COMPLETED]: 'تکمیل شده',
// [OrderStatus.CANCELED]: 'لغو شده',
// };
// return statusMap[status] || status;
// }
@OnEvent(OrderCreatedEvent.name)
async handleOrderCreated(event: OrderCreatedEvent) {
try {
this.logger.log(
`Order created event: orderId=${event.orderId}, orderNumber=${event.orderNumber}, userId=${event.userId}`,
);
// @OnEvent(OrderCreatedEvent.name)
// async handleOrderCreated(event: OrderCreatedEvent) {
// try {
// this.logger.log(
// `Order created event received: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`,
// );
const body = `سفارش شماره ${event.orderNumber} با موفقیت ایجاد شد`;
// const order = await this.OrderRepository.findOne(event.orderId);
// if (order?.paymentMethod.method === PaymentMethodEnum.Online) {
// return;
// }
// // get admnin os restuaraant that have order permissuins
// const admins = await this.adminService.findAdminsWithPermission(event., Permission.MANAGE_ORDERS);
// const recipients = admins.map(admin => ({
// adminId: admin.id,
// }));
// await this.notificationService.sendNotification({
// message: {
// title: NotifTitleEnum.ORDER_CREATED,
// content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`,
// sms: {
// templateId: this.orderCreatedSmsTemplateId,
// parameters: {
// orderNumber: event.orderNumber,
// total: event.total.toString(),
// },
// },
// pushNotif: {
// title: `سفارش جدید`,
// content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`,
// icon: `/`,
// action: {
// type: NotifTitleEnum.ORDER_CREATED,
// url: `/`,
// },
// },
// },
// recipients,
// metadata: {
// priority: 1,
// },
// });
// } catch (error) {
// this.logger.error(
// `Failed to send notification for order created event: ${event.}`,
// error instanceof Error ? error.stack : String(error),
// );
// }
// }
// @OnEvent(OrderStatusChangedEvent.name)
// async handleOrderStatusChanged(event: OrderStatusChangedEvent) {
// try {
// this.logger.log(
// `Order status changed event received: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`,
// );
// //TODO : REFACTOR to use queue or other way to handle this
// const recipients = [
// {
// userId: event.userId,
// },
// ];
// if (event.newStatus === OrderStatus.COMPLETED) {
// if (!event?.userId) {
// this.logger.log(
// `User not found for order: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`,
// );
// }
// // const restaurant = await this.RestaurantRepository.findOne(event.);
// // if (!restaurant) {
// // this.logger.log(
// // `Restaurant not found for order: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`,
// // );
// // return;
// // }
// // const score = restaurant.score;
// // if (!score) {
// // this.logger.log(
// // `Score not found for restaurant: ${event.} and order number: ${event.orderNumber}`,
// // );
// // return;
// // }
// // increase score for user
// // const order = await this.OrderRepository.findOne(event.orderId);
// // if (!order) {
// // this.logger.log(
// // `Order not found for order: ${event.orderId} for restaurant: ${event.} and order number: ${event.orderNumber}`,
// // );
// // return;
// // }
// // this.userService.createWalletTransaction(event.userId, event., {
// // amount: order.subTotal,
// // type: WalletTransactionType.CREDIT,
// // reason: WalletTransactionReason.ORDER_COMPLETED_DEPOSIT,
// // });
// await this.notificationService.sendNotification({
// : event.,
// message: {
// title: NotifTitleEnum.ORDER_STATUS_CHANGED,
// content: `لطفابرای ثبت نظر سفارش ${event.orderNumber} به اپ مراجعه کنید`,
// sms: {
// templateId: this.orderCreatedSmsTemplateId,
// parameters: {
// orderNumber: event.orderNumber,
// },
// },
// pushNotif: {
// title: `تغییر وضعیت سفارش`,
// content: `لطفا برای ثبت نظر سفارش ${event.orderNumber} به اپ مراجعه کنید`,
// icon: `/`,
// action: {
// type: NotifTitleEnum.ORDER_STATUS_CHANGED,
// url: ``,
// },
// },
// },
// recipients,
// metadata: {
// priority: 1,
// },
// });
// } else {
// await this.notificationService.sendNotification({
// : event.,
// message: {
// title: NotifTitleEnum.ORDER_STATUS_CHANGED,
// content: `وضعیت سفارش شماره ${event.orderNumber} به ${this.getStatusFarsi(event.newStatus)} تغییر کرد`,
// sms: {
// templateId: this.orderStatusChangedSmsTemplateId,
// parameters: {
// orderNumber: event.orderNumber,
// status: this.getStatusFarsi(event.newStatus),
// },
// },
// pushNotif: {
// title: `تغییر وضعیت سفارش`,
// content: `وضعیت سفارش شماره ${event.orderNumber} به ${this.getStatusFarsi(event.newStatus)} تغییر کرد`,
// icon: `/`,
// action: {
// type: NotifTitleEnum.ORDER_STATUS_CHANGED,
// url: ``,
// },
// },
// },
// recipients,
// metadata: {
// priority: 1,
// },
// });
// }
// } catch (error) {
// this.logger.error(
// `Failed to send notification for order status changed event: ${event.}`,
// error instanceof Error ? error.stack : String(error),
// );
// }
// }
await this.notificationService.sendNotification({
channels: [NotifChannelEnum.SMS],
recipients: [{ userId: event.userId }],
message: {
title: NotifTitle.NEW_ORDER,
content: body,
sms: {
templateId: this.orderCreatedSmsTemplateId,
parameters: {
orderNumber: String(event.orderNumber),
},
},
},
});
this.logger.log(
`Notification sent to user ${event.userId} for order ${event.orderId}`,
);
} catch (error) {
this.logger.error(
`Failed to send notification for order created: ${event.orderId}`,
error instanceof Error ? error.stack : String(error),
);
}
}
}
+9 -10
View File
@@ -8,30 +8,22 @@ import {
} from '../dto/create-order.dto';
import { UserService } from 'src/modules/user/providers/user.service';
import { OrderStatusEnum } from '../interface/order.interface';
import { ProductService } from 'src/modules/product/providers/product.service';
import { ProductRepository } from 'src/modules/product/repositories/product.repository';
import { ChattService } from 'src/modules/chat/providers/chat.service';
import { ChatRepository } from 'src/modules/chat/repositories/chat.repository';
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
import { Order } from '../entities/order.entity';
import { PaymentRepository } from 'src/modules/payment/repositories/payment.repository';
import { AdminService } from 'src/modules/admin/providers/admin.service';
import { UpdateOrderAsAdminDto } from '../dto/update-order.dto';
import { FindOrderItemsDto } from '../dto/find-order-items.dto';
import { PermissionService } from 'src/modules/roles/providers/permissions.service';
import { PermissionEnum } from 'src/common/enums/permission.enum';
import { User } from 'src/modules/user/entities/user.entity';
import { Product } from 'src/modules/product/entities/product.entity';
import { Category } from 'src/modules/product/entities/category.entity';
import { InvoiceItem } from 'src/modules/invoice/entities/invoice-item.entity';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { UpdateStatusAsDesignerDto } from '../dto/update-status-as-designer.dto';
import { UpdateStatusDto } from '../dto/update-status.dto';
import { CreateOrderPrintDto } from '../dto/create-order-print.dto';
import { OrderPrint } from '../entities/print.entity';
import { OrderPrintRepository } from '../repositories/order-print.repository';
import { Request } from 'src/modules/request/entities/request.entity';
import { Invoice } from 'src/modules/invoice/entities/invoice.entity';
import { OrderCreatedEvent } from '../events/order.events';
@Injectable()
export class OrderService {
@@ -43,7 +35,8 @@ export class OrderService {
private readonly userService: UserService,
private readonly adminService: AdminService,
private readonly orderPrintRepository: OrderPrintRepository,
) { }
private readonly eventEmitter: EventEmitter2,
) {}
async createOrderAsAdmin(adminId: string, dto: CreateOrderAsAdminDto): Promise<Order> {
let invoiceItem: null | InvoiceItem = null
@@ -104,6 +97,12 @@ export class OrderService {
});
await this.em.persistAndFlush(order);
this.eventEmitter.emit(
OrderCreatedEvent.name,
new OrderCreatedEvent(order.id, user.id, order.orderNumber),
);
return order;
}
@@ -66,4 +66,6 @@ export class TicketListeners {
);
}
}
// TODO: add notification to user when ticket message is created by admin
}