farsi messages

This commit is contained in:
2025-12-22 19:12:03 +03:30
parent ed251611c4
commit 1802b05e74
14 changed files with 365 additions and 230 deletions
+108
View File
@@ -66,6 +66,10 @@ export const enum AuthMessage {
TOKEN_MISSED_OR_EXPIRED = 'توکن مفقود یا منقضی شده است', TOKEN_MISSED_OR_EXPIRED = 'توکن مفقود یا منقضی شده است',
OLD_PASSWORD_REQUIRED = 'رمز عبور قبلی الزامی است', OLD_PASSWORD_REQUIRED = 'رمز عبور قبلی الزامی است',
PASSWORD_CHANGE_FAILED = 'تغییر رمز عبور با مشکل روبرد شد', PASSWORD_CHANGE_FAILED = 'تغییر رمز عبور با مشکل روبرد شد',
SLUG_REQUIRED = 'اسلاگ الزامی است',
INVALID_TOKEN_PAYLOAD = 'محتویات توکن نامعتبر است',
INVALID_SLUG = 'اسلاگ نامعتبر است',
INVALID_OR_EXPIRED_TOKEN = 'توکن نامعتبر یا منقضی شده است',
} }
export const enum FoodMessage { export const enum FoodMessage {
NOT_FOUND = 'غذایی با این مشخصات یافت نشد', NOT_FOUND = 'غذایی با این مشخصات یافت نشد',
@@ -579,6 +583,12 @@ export const enum NotificationMessage {
NEW_EMAIL_MESSAGE = 'یک ایمیل جدید با موضوع [subject] دریافت شد', NEW_EMAIL_MESSAGE = 'یک ایمیل جدید با موضوع [subject] دریافت شد',
CHANGE_PASSWORD_MESSAGE = 'رمز عبور شما با موفقیت تغییر کرد', CHANGE_PASSWORD_MESSAGE = 'رمز عبور شما با موفقیت تغییر کرد',
CHANGE_PASSWORD = 'تغییر رمز عبور', CHANGE_PASSWORD = 'تغییر رمز عبور',
NOT_FOUND = 'اعلان یافت نشد',
READ_SUCCESS = 'اعلان با موفقیت خوانده شد',
ORDER_CREATED = 'سفارش جدید ایجاد شد',
ORDER_STATUS_CHANGED = 'وضعیت سفارش تغییر کرد',
PAYMENT_SUCCESS = 'پرداخت با موفقیت انجام شد',
PAGER_CREATED = 'پیج جدید ایجاد شد',
} }
export const enum SettingMessageEnum { export const enum SettingMessageEnum {
@@ -664,3 +674,101 @@ export const enum GroupMessage {
NOT_UPDATED = 'به‌روزرسانی گروه آیکون با خطا مواجه شد', NOT_UPDATED = 'به‌روزرسانی گروه آیکون با خطا مواجه شد',
NOT_DELETED = 'حذف گروه آیکون با خطا مواجه شد', NOT_DELETED = 'حذف گروه آیکون با خطا مواجه شد',
} }
export const enum OrderMessage {
NOT_FOUND = 'سفارش یافت نشد',
PAYMENT_METHOD_MISSING = 'روش پرداخت سفارش مشخص نشده است',
INVALID_STATUS_TRANSITION = 'انتقال وضعیت نامعتبر است',
CART_EMPTY = 'سبد خرید خالی است. لطفا قبل از ایجاد سفارش، آیتم‌هایی به سبد اضافه کنید',
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
PAYMENT_METHOD_REQUIRED = 'روش پرداخت الزامی است. لطفا قبل از ایجاد سفارش، روش پرداخت را تنظیم کنید',
USER_NOT_FOUND = 'کاربر یافت نشد',
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
DELIVERY_NOT_FOUND = 'روش ارسال یافت نشد',
MIN_ORDER_AMOUNT_NOT_MET = 'مبلغ سفارش کمتر از حداقل مجاز برای این روش ارسال است',
TABLE_NUMBER_REQUIRED = 'شماره میز برای سفارش در محل الزامی است',
ADDRESS_REQUIRED = 'آدرس الزامی است. لطفا قبل از ایجاد سفارش، آدرس تحویل را تنظیم کنید',
CAR_ADDRESS_REQUIRED = 'آدرس خودرو الزامی است. لطفا قبل از ایجاد سفارش، آدرس خودرو را تنظیم کنید',
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست',
FOOD_NOT_FOUND = 'غذا یافت نشد',
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
}
export const enum CartMessage {
TABLE_NUMBER_REQUIRED = 'شماره میز برای سفارش در محل الزامی است',
CAR_ADDRESS_REQUIRED = 'آدرس خودرو برای تحویل به خودرو الزامی است',
ADDRESS_REQUIRED = 'آدرس برای تحویل پیک الزامی است',
NOT_FOUND = 'سبد خرید یافت نشد',
COUPON_NOT_FOUND = 'کوپن یافت نشد',
FOOD_NOT_FOUND = 'غذا یافت نشد',
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
FOOD_NO_INVENTORY = 'غذا موجودی ندارد',
INSUFFICIENT_STOCK = 'موجودی کافی نیست',
USER_NOT_FOUND = 'کاربر یافت نشد',
ADDRESS_NOT_FOUND = 'آدرس یافت نشد',
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
ADDRESS_NOT_BELONGS_TO_USER = 'آدرس به کاربر فعلی تعلق ندارد',
ADDRESS_NO_COORDINATES = 'آدرس مختصات جغرافیایی ندارد',
ADDRESS_OUTSIDE_SERVICE_AREA = 'آدرس خارج از محدوده سرویس رستوران است',
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
DELIVERY_METHOD_NOT_ENABLED = 'روش ارسال برای این رستوران فعال نیست',
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست',
WALLET_NOT_FOUND = 'کیف پول کاربر یافت نشد',
WALLET_INSUFFICIENT = 'موجودی کیف پول کافی نیست',
COUPON_MAX_USES_REACHED = 'حداکثر استفاده از کوپن به پایان رسیده است',
ITEM_NOT_FOUND = 'آیتم در سبد خرید یافت نشد',
}
export const enum PaymentMessage {
UNSUPPORTED_PAYMENT_METHOD = 'روش پرداخت پشتیبانی نمی‌شود',
ORDER_NOT_FOUND = 'سفارش یافت نشد',
AMOUNT_MUST_BE_GREATER_THAN_ZERO = 'مبلغ باید بیشتر از صفر باشد',
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
GATEWAY_REQUIRED = 'درگاه پرداخت برای پرداخت آنلاین الزامی است',
MERCHANT_ID_REQUIRED = 'شناسه مرچنت برای پرداخت آنلاین الزامی است',
RESTAURANT_DOMAIN_REQUIRED = 'دامنه رستوران برای پرداخت آنلاین الزامی است',
WALLET_NOT_FOUND = 'کیف پول کاربر یافت نشد',
INSUFFICIENT_WALLET_BALANCE = 'موجودی کیف پول کافی نیست',
PAYMENT_NOT_FOUND = 'پرداخت یافت نشد',
INVALID_PAYMENT_CONFIGURATION = 'پیکربندی پرداخت نامعتبر است',
PAYMENT_METHOD_NOT_CASH = 'روش پرداخت نقدی نیست',
PAYMENT_ALREADY_PAID = 'پرداخت قبلا انجام شده است',
EXISTING_PENDING_PAYMENT_MISMATCH = 'پرداخت در انتظار موجود با روش پرداخت سفارش مطابقت ندارد',
ZARINPAL_INVALID_RESPONSE = 'پاسخ نامعتبر از API زرین‌پال',
ZARINPAL_PAYMENT_REQUEST_ERROR = 'خطا در درخواست پرداخت زرین‌پال',
ZARINPAL_VERIFY_ERROR = 'خطا در تایید پرداخت زرین‌پال',
GATEWAY_ERROR = 'خطا در اتصال به درگاه پرداخت',
}
export const enum DeliveryMessage {
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
}
export const enum InventoryMessage {
AVAILABLE_STOCK_EXCEEDS_TOTAL = 'موجودی موجود نمی‌تواند از موجودی کل بیشتر باشد',
FOOD_NOT_FOUND = 'غذا یافت نشد',
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
FOODS_NOT_FOUND = 'غذاها یافت نشدند',
}
export const enum PagerMessage {
CREATED_SUCCESS = 'درخواست پیج با موفقیت ایجاد شد',
}
export const enum UserSuccessMessage {
ADDRESS_DELETED_SUCCESS = 'آدرس با موفقیت حذف شد',
SCORE_CONVERTED_SUCCESS = 'امتیاز با موفقیت به کیف پول تبدیل شد',
}
export const enum ContactMessage {
NOT_FOUND = 'تماس یافت نشد',
CREATED = 'تماس با موفقیت ثبت شد',
UPDATED = 'تماس با موفقیت به‌روزرسانی شد',
DELETED = 'تماس با موفقیت حذف شد',
}
@@ -3,6 +3,7 @@ import { Food } from '../../foods/entities/food.entity';
import { Cart, CartItem } from '../interfaces/cart.interface'; import { Cart, CartItem } from '../interfaces/cart.interface';
import { CartValidationService } from './cart-validation.service'; import { CartValidationService } from './cart-validation.service';
import { CartCalculationService } from './cart-calculation.service'; import { CartCalculationService } from './cart-calculation.service';
import { CartMessage } from 'src/common/enums/message.enum';
@Injectable() @Injectable()
export class CartItemService { export class CartItemService {
@@ -83,7 +84,7 @@ export class CartItemService {
removeItemOrFail(cart: Cart, foodId: string): void { removeItemOrFail(cart: Cart, foodId: string): void {
const itemIndex = this.getItemIndex(cart, foodId); const itemIndex = this.getItemIndex(cart, foodId);
if (itemIndex < 0) { if (itemIndex < 0) {
throw new NotFoundException(`Item with food ID ${foodId} not found in cart`); throw new NotFoundException(CartMessage.ITEM_NOT_FOUND);
} }
cart.items.splice(itemIndex, 1); cart.items.splice(itemIndex, 1);
} }
@@ -94,7 +95,7 @@ export class CartItemService {
async decrementOrRemoveItem(cart: Cart, foodId: string): Promise<void> { async decrementOrRemoveItem(cart: Cart, foodId: string): Promise<void> {
const itemIndex = this.getItemIndex(cart, foodId); const itemIndex = this.getItemIndex(cart, foodId);
if (itemIndex < 0) { if (itemIndex < 0) {
throw new NotFoundException(`Item with food ID ${foodId} not found in cart`); throw new NotFoundException(CartMessage.ITEM_NOT_FOUND);
} }
const existingItem = cart.items[itemIndex]; const existingItem = cart.items[itemIndex];
@@ -13,6 +13,7 @@ import { OrderStatus } from '../../orders/interface/order.interface';
import { Cart } from '../interfaces/cart.interface'; import { Cart } from '../interfaces/cart.interface';
import { GeographicUtils } from '../utils/geographic.utils'; import { GeographicUtils } from '../utils/geographic.utils';
import { MealType } from '../../foods/interface/food.interface'; import { MealType } from '../../foods/interface/food.interface';
import { CartMessage } from 'src/common/enums/message.enum';
@Injectable() @Injectable()
export class CartValidationService { export class CartValidationService {
@@ -24,15 +25,15 @@ export class CartValidationService {
async validateAndGetFood(foodId: string, restaurantId: string, quantity: number): Promise<Food> { async validateAndGetFood(foodId: string, restaurantId: string, quantity: number): Promise<Food> {
const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] }); const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] });
if (!food) { if (!food) {
throw new NotFoundException(`Food with ID ${foodId} not found`); throw new NotFoundException(CartMessage.FOOD_NOT_FOUND);
} }
if (food.restaurant.id !== restaurantId) { if (food.restaurant.id !== restaurantId) {
throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`); throw new BadRequestException(CartMessage.FOOD_NOT_BELONGS_TO_RESTAURANT);
} }
if (!food.inventory) { if (!food.inventory) {
throw new BadRequestException(`The food ${food.title || food.id} does not have inventory`); throw new BadRequestException(CartMessage.FOOD_NO_INVENTORY);
} }
this.validateStock(food, quantity); this.validateStock(food, quantity);
@@ -45,7 +46,7 @@ export class CartValidationService {
validateStock(food: Food, quantity: number): void { validateStock(food: Food, quantity: number): void {
const availableStock = food.inventory!.availableStock; const availableStock = food.inventory!.availableStock;
if (availableStock < quantity) { if (availableStock < quantity) {
throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`); throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK);
} }
} }
@@ -55,7 +56,7 @@ export class CartValidationService {
async getUserOrFail(userId: string): Promise<User> { async getUserOrFail(userId: string): Promise<User> {
const user = await this.em.findOne(User, { id: userId }); const user = await this.em.findOne(User, { id: userId });
if (!user) { if (!user) {
throw new NotFoundException(`User with ID ${userId} not found`); throw new NotFoundException(CartMessage.USER_NOT_FOUND);
} }
return user; return user;
} }
@@ -66,7 +67,7 @@ export class CartValidationService {
async getUserAddressOrFail(addressId: string): Promise<UserAddress> { async getUserAddressOrFail(addressId: string): Promise<UserAddress> {
const address = await this.em.findOne(UserAddress, { id: addressId }, { populate: ['user'] }); const address = await this.em.findOne(UserAddress, { id: addressId }, { populate: ['user'] });
if (!address) { if (!address) {
throw new NotFoundException(`Address with ID ${addressId} not found`); throw new NotFoundException(CartMessage.ADDRESS_NOT_FOUND);
} }
return address; return address;
} }
@@ -77,7 +78,7 @@ export class CartValidationService {
async getFoodOrFail(foodId: string): Promise<Food> { async getFoodOrFail(foodId: string): Promise<Food> {
const food = await this.em.findOne(Food, { id: foodId }); const food = await this.em.findOne(Food, { id: foodId });
if (!food) { if (!food) {
throw new NotFoundException(`Food with ID ${foodId} not found`); throw new NotFoundException(CartMessage.FOOD_NOT_FOUND);
} }
return food; return food;
} }
@@ -88,7 +89,7 @@ export class CartValidationService {
async getRestaurantOrFail(restaurantId: string): Promise<Restaurant> { async getRestaurantOrFail(restaurantId: string): Promise<Restaurant> {
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
if (!restaurant) { if (!restaurant) {
throw new NotFoundException('Restaurant not found'); throw new NotFoundException(CartMessage.RESTAURANT_NOT_FOUND);
} }
return restaurant; return restaurant;
} }
@@ -98,7 +99,7 @@ export class CartValidationService {
*/ */
validateAddressOwnership(address: UserAddress, userId: string): void { validateAddressOwnership(address: UserAddress, userId: string): void {
if (address.user.id !== userId) { if (address.user.id !== userId) {
throw new BadRequestException('Address does not belong to the current user'); throw new BadRequestException(CartMessage.ADDRESS_NOT_BELONGS_TO_USER);
} }
} }
@@ -133,7 +134,7 @@ export class CartValidationService {
); );
if (!GeographicUtils.isPointInPolygon(point, polygon)) { if (!GeographicUtils.isPointInPolygon(point, polygon)) {
throw new BadRequestException('Address is outside the restaurant service area'); throw new BadRequestException(CartMessage.ADDRESS_OUTSIDE_SERVICE_AREA);
} }
} }
@@ -168,12 +169,10 @@ export class CartValidationService {
{ populate: ['restaurant'] }, { populate: ['restaurant'] },
); );
if (!deliveryMethod) { if (!deliveryMethod) {
throw new NotFoundException( throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND);
`Delivery method with ID ${deliveryMethodId} not found for restaurant ${restaurantId}`,
);
} }
if (!deliveryMethod.enabled) { if (!deliveryMethod.enabled) {
throw new BadRequestException('Delivery method is not enabled for this restaurant'); throw new BadRequestException(CartMessage.DELIVERY_METHOD_NOT_ENABLED);
} }
return deliveryMethod; return deliveryMethod;
} }
@@ -207,10 +206,10 @@ export class CartValidationService {
{ populate: ['restaurant'] }, { populate: ['restaurant'] },
); );
if (!paymentMethod) { if (!paymentMethod) {
throw new NotFoundException(`Payment method with ID ${paymentMethodId} not found for restaurant ${restaurantId}`); throw new NotFoundException(CartMessage.PAYMENT_METHOD_NOT_FOUND);
} }
if (!paymentMethod.enabled) { if (!paymentMethod.enabled) {
throw new BadRequestException('Payment method is not enabled for this restaurant'); throw new BadRequestException(CartMessage.PAYMENT_METHOD_NOT_ENABLED);
} }
return paymentMethod; return paymentMethod;
} }
@@ -225,11 +224,11 @@ export class CartValidationService {
}); });
if (!wallet) { if (!wallet) {
throw new BadRequestException('User wallet not found'); throw new BadRequestException(CartMessage.WALLET_NOT_FOUND);
} }
if (wallet.wallet < amount) { if (wallet.wallet < amount) {
throw new BadRequestException('User wallet is not enough'); throw new BadRequestException(CartMessage.WALLET_INSUFFICIENT);
} }
} }
@@ -247,7 +246,7 @@ export class CartValidationService {
}); });
if (ordersThatUsedTheCouponCount >= maxUsesPerUser) { if (ordersThatUsedTheCouponCount >= maxUsesPerUser) {
throw new BadRequestException(`You have reached the maximum number of uses (${maxUsesPerUser}) for this coupon`); throw new BadRequestException(CartMessage.COUPON_MAX_USES_REACHED);
} }
} }
+7 -6
View File
@@ -12,6 +12,7 @@ import { CartValidationService } from './cart-validation.service';
import { CartCalculationService } from './cart-calculation.service'; import { CartCalculationService } from './cart-calculation.service';
import { CartItemService } from './cart-item.service'; import { CartItemService } from './cart-item.service';
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto'; import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
import { CartMessage } from 'src/common/enums/message.enum';
@Injectable() @Injectable()
export class CartService { export class CartService {
@@ -44,7 +45,7 @@ export class CartService {
case DeliveryMethodEnum.DineIn: case DeliveryMethodEnum.DineIn:
// DineIn requires table number and clears incompatible fields // DineIn requires table number and clears incompatible fields
if (!tableNumber) { if (!tableNumber) {
throw new BadRequestException('Table number is required for dine-in orders'); throw new BadRequestException(CartMessage.TABLE_NUMBER_REQUIRED);
} }
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn); this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
cart.tableNumber = tableNumber; cart.tableNumber = tableNumber;
@@ -58,7 +59,7 @@ export class CartService {
case DeliveryMethodEnum.DeliveryCar: case DeliveryMethodEnum.DeliveryCar:
// DeliveryCar requires carAddress and clears incompatible fields // DeliveryCar requires carAddress and clears incompatible fields
if (!carAddress) { if (!carAddress) {
throw new BadRequestException('Car address is required for car delivery'); throw new BadRequestException(CartMessage.CAR_ADDRESS_REQUIRED);
} }
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar); this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar);
const user = await this.validationService.getUserOrFail(userId); const user = await this.validationService.getUserOrFail(userId);
@@ -73,7 +74,7 @@ export class CartService {
case DeliveryMethodEnum.DeliveryCourier: case DeliveryMethodEnum.DeliveryCourier:
// DeliveryCourier requires addressId and clears incompatible fields // DeliveryCourier requires addressId and clears incompatible fields
if (!addressId) { if (!addressId) {
throw new BadRequestException('Address is required for courier delivery'); throw new BadRequestException(CartMessage.ADDRESS_REQUIRED);
} }
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier); this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
const address = await this.validationService.getUserAddressOrFail(addressId); const address = await this.validationService.getUserAddressOrFail(addressId);
@@ -167,7 +168,7 @@ export class CartService {
async findOneOrFail(userId: string, restaurantId: string): Promise<Cart> { async findOneOrFail(userId: string, restaurantId: string): Promise<Cart> {
const cart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId); const cart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId);
if (!cart) { if (!cart) {
throw new NotFoundException('Cart not found'); throw new NotFoundException(CartMessage.NOT_FOUND);
} }
return cart; return cart;
} }
@@ -229,11 +230,11 @@ export class CartService {
); );
if (!valid) { if (!valid) {
throw new BadRequestException(message || 'Invalid coupon code'); throw new BadRequestException(message || CartMessage.COUPON_NOT_FOUND);
} }
if (!coupon) { if (!coupon) {
throw new BadRequestException('Coupon not found'); throw new BadRequestException(CartMessage.COUPON_NOT_FOUND);
} }
// Check maxUsesPerUser limit by counting how many orders this user has with this coupon // Check maxUsesPerUser limit by counting how many orders this user has with this coupon
@@ -6,6 +6,7 @@ import { CreateDeliveryDto } from '../dto/create-delivery.dto';
import { UpdateDeliveryDto } from '../dto/update-delivery.dto'; import { UpdateDeliveryDto } from '../dto/update-delivery.dto';
import { RestRepository } from '../../restaurants/repositories/rest.repository'; import { RestRepository } from '../../restaurants/repositories/rest.repository';
import { DeliveryMethodEnum } from '../interface/delivery'; import { DeliveryMethodEnum } from '../interface/delivery';
import { DeliveryMessage } from 'src/common/enums/message.enum';
@Injectable() @Injectable()
export class DeliveryService { export class DeliveryService {
@@ -18,7 +19,7 @@ export class DeliveryService {
async create(restId: string, createDto: CreateDeliveryDto): Promise<Delivery> { async create(restId: string, createDto: CreateDeliveryDto): Promise<Delivery> {
const restaurant = await this.restRepository.findOne({ id: restId }); const restaurant = await this.restRepository.findOne({ id: restId });
if (!restaurant) { if (!restaurant) {
throw new NotFoundException(`رستوران با شناسه ${restId} یافت نشد.`); throw new NotFoundException(DeliveryMessage.RESTAURANT_NOT_FOUND);
} }
// Check if the delivery method with the same name already exists for this restaurant // Check if the delivery method with the same name already exists for this restaurant
@@ -60,7 +61,7 @@ export class DeliveryService {
}); });
if (!delivery) { if (!delivery) {
throw new NotFoundException(`روش ارسال با شناسه ${deliveryId} برای رستوران با شناسه ${restId} یافت نشد.`); throw new NotFoundException(DeliveryMessage.DELIVERY_METHOD_NOT_FOUND);
} }
return delivery; return delivery;
@@ -73,7 +74,7 @@ export class DeliveryService {
}); });
if (!delivery) { if (!delivery) {
throw new NotFoundException(`روش ارسال با شناسه ${deliveryId} برای رستوران با شناسه ${restId} یافت نشد.`); throw new NotFoundException(DeliveryMessage.DELIVERY_METHOD_NOT_FOUND);
} }
// If method is being updated, check for conflicts // If method is being updated, check for conflicts
@@ -102,7 +103,7 @@ export class DeliveryService {
}); });
if (!delivery) { if (!delivery) {
throw new NotFoundException(`روش ارسال با شناسه ${deliveryId} برای رستوران با شناسه ${restId} یافت نشد.`); throw new NotFoundException(DeliveryMessage.DELIVERY_METHOD_NOT_FOUND);
} }
await this.em.removeAndFlush(delivery); await this.em.removeAndFlush(delivery);
@@ -9,6 +9,7 @@ import { RestId } from '../../../common/decorators/rest-id.decorator';
import { UpdatePreferenceDto } from '../dto/update-preference.dto'; import { UpdatePreferenceDto } from '../dto/update-preference.dto';
import { AdminId } from 'src/common/decorators/admin-id.decorator'; import { AdminId } from 'src/common/decorators/admin-id.decorator';
import { API_HEADER_SLUG } from 'src/common/constants/index'; import { API_HEADER_SLUG } from 'src/common/constants/index';
import { NotificationMessage } from 'src/common/enums/message.enum';
@ApiTags('notifications') @ApiTags('notifications')
@Controller() @Controller()
@@ -64,7 +65,7 @@ export class NotificationsController {
@ApiParam({ name: 'id', description: 'Notification ID' }) @ApiParam({ name: 'id', description: 'Notification ID' })
async readNotificationUser(@RestId() restaurantId: string, @Param('id') id: string, @UserId() userId: string) { async readNotificationUser(@RestId() restaurantId: string, @Param('id') id: string, @UserId() userId: string) {
await this.notificationService.readNotificationAsUser(id, userId, restaurantId); await this.notificationService.readNotificationAsUser(id, userId, restaurantId);
return { message: 'Notification read successfully' }; return { message: NotificationMessage.READ_SUCCESS };
} }
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@@ -73,7 +74,7 @@ export class NotificationsController {
@ApiOperation({ summary: 'Read all notification ' }) @ApiOperation({ summary: 'Read all notification ' })
async readAllNotificationUser(@RestId() restaurantId: string, @UserId() userId: string) { async readAllNotificationUser(@RestId() restaurantId: string, @UserId() userId: string) {
await this.notificationService.readAllNotifsAsUser(userId, restaurantId); await this.notificationService.readAllNotifsAsUser(userId, restaurantId);
return { message: 'Notification read successfully' }; return { message: NotificationMessage.READ_SUCCESS };
} }
/* ***************** Admin Endpoints ***************** */ /* ***************** Admin Endpoints ***************** */
@@ -116,7 +117,7 @@ export class NotificationsController {
@ApiParam({ name: 'id', description: 'Notification ID' }) @ApiParam({ name: 'id', description: 'Notification ID' })
async readNotificationAdmin(@RestId() restaurantId: string, @AdminId() adminId: string, @Param('id') id: string) { async readNotificationAdmin(@RestId() restaurantId: string, @AdminId() adminId: string, @Param('id') id: string) {
await this.notificationService.readNotificationAdmin(id, adminId, restaurantId); await this.notificationService.readNotificationAdmin(id, adminId, restaurantId);
return { message: 'Notification read successfully' }; return { message: NotificationMessage.READ_SUCCESS };
} }
@UseGuards(AdminAuthGuard) @UseGuards(AdminAuthGuard)
@@ -125,7 +126,7 @@ export class NotificationsController {
@ApiOperation({ summary: 'Read all notification ' }) @ApiOperation({ summary: 'Read all notification ' })
async readAllNotificationAdmin(@RestId() restaurantId: string, @AdminId() adminId: string, @Param('id') id: string) { async readAllNotificationAdmin(@RestId() restaurantId: string, @AdminId() adminId: string, @Param('id') id: string) {
await this.notificationService.readAllNotifsAsAdmin(adminId, restaurantId); await this.notificationService.readAllNotifsAsAdmin(adminId, restaurantId);
return { message: 'Notification read successfully' }; return { message: NotificationMessage.READ_SUCCESS };
} }
@UseGuards(AdminAuthGuard) @UseGuards(AdminAuthGuard)
+157 -142
View File
@@ -24,6 +24,21 @@ export class OrderListeners {
this.orderStatusChangedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_CHANGE') ?? '123'; this.orderStatusChangedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_CHANGE') ?? '123';
} }
private getStatusFarsi(status: OrderStatus): string {
const statusMap: Record<OrderStatus, string> = {
[OrderStatus.PENDING_PAYMENT]: 'در انتظار پرداخت',
[OrderStatus.PAID]: 'پرداخت شده',
[OrderStatus.CONFIRMED]: 'تایید شده',
[OrderStatus.PREPARING]: 'در حال آماده‌سازی',
[OrderStatus.DELIVERED_TO_RECEPTIONIST]: 'تحویل به پذیرش',
[OrderStatus.DELIVERED_TO_WAITER]: 'تحویل به گارسون',
[OrderStatus.SHIPPED]: 'ارسال شده',
[OrderStatus.COMPLETED]: 'تکمیل شده',
[OrderStatus.CANCELED]: 'لغو شده',
};
return statusMap[status] || status;
}
@OnEvent(OrderCreatedEvent.name) @OnEvent(OrderCreatedEvent.name)
async handleOrderCreated(event: OrderCreatedEvent) { async handleOrderCreated(event: OrderCreatedEvent) {
try { try {
@@ -39,7 +54,7 @@ export class OrderListeners {
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
message: { message: {
title: NotifTitleEnum.ORDER_CREATED, title: NotifTitleEnum.ORDER_CREATED,
content: `Order ${event.orderNumber} with ${event.total} has been created successfully`, content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`,
sms: { sms: {
templateId: this.orderCreatedSmsTemplateId, templateId: this.orderCreatedSmsTemplateId,
parameters: { parameters: {
@@ -48,8 +63,8 @@ export class OrderListeners {
}, },
}, },
pushNotif: { pushNotif: {
title: `Order ${event.orderNumber} with ${event.total} has been created successfully`, title: `سفارش جدید`,
content: `Order ${event.orderNumber} with ${event.total} has been created successfully`, content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`,
icon: `/`, icon: `/`,
action: { action: {
type: NotifTitleEnum.ORDER_CREATED, type: NotifTitleEnum.ORDER_CREATED,
@@ -95,7 +110,7 @@ export class OrderListeners {
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
message: { message: {
title: NotifTitleEnum.ORDER_STATUS_CHANGED, title: NotifTitleEnum.ORDER_STATUS_CHANGED,
content: `Order #${event.orderNumber} status is changed to ${event.newStatus} .`, content: `وضعیت سفارش شماره ${event.orderNumber} به ${this.getStatusFarsi(event.newStatus)} تغییر کرد`,
sms: { sms: {
templateId: this.orderStatusChangedSmsTemplateId, templateId: this.orderStatusChangedSmsTemplateId,
parameters: { parameters: {
@@ -104,8 +119,8 @@ export class OrderListeners {
}, },
}, },
pushNotif: { pushNotif: {
title: `Order #${event.orderNumber} status is changed to ${event.newStatus} .`, title: `تغییر وضعیت سفارش`,
content: `Order #${event.orderNumber} status is changed to ${event.newStatus} .`, content: `وضعیت سفارش شماره ${event.orderNumber} به ${this.getStatusFarsi(event.newStatus)} تغییر کرد`,
icon: `/`, icon: `/`,
action: { action: {
type: NotifTitleEnum.ORDER_STATUS_CHANGED, type: NotifTitleEnum.ORDER_STATUS_CHANGED,
@@ -126,140 +141,140 @@ export class OrderListeners {
} }
} }
private factory(orderStatus: OrderStatus, event: OrderStatusChangedEvent) { // private factory(orderStatus: OrderStatus, event: OrderStatusChangedEvent) {
switch (orderStatus) { // switch (orderStatus) {
case OrderStatus.CONFIRMED: // case OrderStatus.CONFIRMED:
return { // return {
message: { // message: {
title: NotifTitleEnum.ORDER_STATUS_CHANGED, // title: NotifTitleEnum.ORDER_STATUS_CHANGED,
content: `Order ${event.orderNumber} confirmed .`, // content: `Order ${event.orderNumber} confirmed .`,
sms: { // sms: {
templateId: this.orderCreatedSmsTemplateId, // templateId: this.orderCreatedSmsTemplateId,
parameters: { // parameters: {
orderNumber: event.orderNumber, // orderNumber: event.orderNumber,
}, // },
}, // },
pushNotif: { // pushNotif: {
title: `Order ${event.orderNumber} has been confirmed successfully`, // title: `تایید سفارش`,
content: `Order ${event.orderNumber} has been confirmed successfully`, // content: `سفارش شماره ${event.orderNumber} با موفقیت تایید شد`,
icon: `Order ${event.orderNumber} has been confirmed successfully`, // icon: `Order ${event.orderNumber} has been confirmed successfully`,
action: { // action: {
type: NotifTitleEnum.ORDER_STATUS_CHANGED, // type: NotifTitleEnum.ORDER_STATUS_CHANGED,
url: ``, // url: ``,
}, // },
}, // },
}, // },
}; // };
case OrderStatus.PREPARING: // case OrderStatus.PREPARING:
return { // return {
message: { // message: {
title: NotifTitleEnum.ORDER_STATUS_CHANGED, // title: NotifTitleEnum.ORDER_STATUS_CHANGED,
content: `Order ${event.orderNumber} is preparing.`, // content: `سفارش شماره ${event.orderNumber} در حال آماده‌سازی است`,
sms: { // sms: {
templateId: this.orderCreatedSmsTemplateId, // templateId: this.orderCreatedSmsTemplateId,
parameters: { // parameters: {
orderNumber: event.orderNumber, // orderNumber: event.orderNumber,
}, // },
}, // },
pushNotif: { // pushNotif: {
title: `Order ${event.orderNumber} is preparing`, // title: `آماده‌سازی سفارش`,
content: `Order ${event.orderNumber} is preparing`, // content: `سفارش شماره ${event.orderNumber} در حال آماده‌سازی است`,
icon: `Order ${event.orderNumber} is preparing`, // icon: `Order ${event.orderNumber} is preparing`,
action: { // action: {
type: NotifTitleEnum.ORDER_STATUS_CHANGED, // type: NotifTitleEnum.ORDER_STATUS_CHANGED,
url: ``, // url: ``,
}, // },
}, // },
}, // },
}; // };
case OrderStatus.DELIVERED_TO_RECEPTIONIST: // case OrderStatus.DELIVERED_TO_RECEPTIONIST:
return { // return {
message: { // message: {
title: NotifTitleEnum.ORDER_STATUS_CHANGED, // title: NotifTitleEnum.ORDER_STATUS_CHANGED,
content: `Order ${event.orderNumber} is ready.`, // content: `سفارش شماره ${event.orderNumber} آماده است`,
sms: { // sms: {
templateId: this.orderCreatedSmsTemplateId, // templateId: this.orderCreatedSmsTemplateId,
parameters: { // parameters: {
orderNumber: event.orderNumber, // orderNumber: event.orderNumber,
}, // },
}, // },
pushNotif: { // pushNotif: {
title: `Order ${event.orderNumber} is ready`, // title: `سفارش آماده است`,
content: `Order ${event.orderNumber} is ready`, // content: `سفارش شماره ${event.orderNumber} آماده است`,
icon: `Order ${event.orderNumber} is ready`, // icon: `Order ${event.orderNumber} is ready`,
action: { // action: {
type: NotifTitleEnum.ORDER_STATUS_CHANGED, // type: NotifTitleEnum.ORDER_STATUS_CHANGED,
url: ``, // url: ``,
}, // },
}, // },
}, // },
}; // };
case OrderStatus.SHIPPED: // case OrderStatus.SHIPPED:
return { // return {
message: { // message: {
title: NotifTitleEnum.ORDER_STATUS_CHANGED, // title: NotifTitleEnum.ORDER_STATUS_CHANGED,
content: `Order ${event.orderNumber} is shipped.`, // content: `سفارش شماره ${event.orderNumber} ارسال شد`,
sms: { // sms: {
templateId: this.orderCreatedSmsTemplateId, // templateId: this.orderCreatedSmsTemplateId,
parameters: { // parameters: {
orderNumber: event.orderNumber, // orderNumber: event.orderNumber,
}, // },
}, // },
pushNotif: { // pushNotif: {
title: `Order ${event.orderNumber} is shipped`, // title: `ارسال سفارش`,
content: `Order ${event.orderNumber} is shipped`, // content: `سفارش شماره ${event.orderNumber} ارسال شد`,
icon: `Order ${event.orderNumber} is shipped`, // icon: `Order ${event.orderNumber} is shipped`,
action: { // action: {
type: NotifTitleEnum.ORDER_STATUS_CHANGED, // type: NotifTitleEnum.ORDER_STATUS_CHANGED,
url: ``, // url: ``,
}, // },
}, // },
}, // },
}; // };
case OrderStatus.CANCELED: // case OrderStatus.CANCELED:
return { // return {
message: { // message: {
title: NotifTitleEnum.ORDER_STATUS_CHANGED, // title: NotifTitleEnum.ORDER_STATUS_CHANGED,
content: `Order ${event.orderNumber} is canceled.`, // content: `سفارش شماره ${event.orderNumber} لغو شد`,
sms: { // sms: {
templateId: this.orderCreatedSmsTemplateId, // templateId: this.orderCreatedSmsTemplateId,
parameters: { // parameters: {
orderNumber: event.orderNumber, // orderNumber: event.orderNumber,
}, // },
}, // },
pushNotif: { // pushNotif: {
title: `Order ${event.orderNumber} is canceled`, // title: `لغو سفارش`,
content: `Order ${event.orderNumber} is canceled`, // content: `سفارش شماره ${event.orderNumber} لغو شد`,
icon: `Order ${event.orderNumber} is canceled`, // icon: `Order ${event.orderNumber} is canceled`,
action: { // action: {
type: NotifTitleEnum.ORDER_STATUS_CHANGED, // type: NotifTitleEnum.ORDER_STATUS_CHANGED,
url: ``, // url: ``,
}, // },
}, // },
}, // },
}; // };
default: // default:
return { // return {
message: { // message: {
title: NotifTitleEnum.ORDER_STATUS_CHANGED, // title: NotifTitleEnum.ORDER_STATUS_CHANGED,
content: `Order ${event.orderNumber} is ....`, // content: `Order ${event.orderNumber} is ....`,
sms: { // sms: {
templateId: this.orderCreatedSmsTemplateId, // templateId: this.orderCreatedSmsTemplateId,
parameters: { // parameters: {
orderNumber: event.orderNumber, // orderNumber: event.orderNumber,
}, // },
}, // },
pushNotif: { // pushNotif: {
title: `Order ${event.orderNumber} is ...`, // title: `Order ${event.orderNumber} is ...`,
content: `Order ${event.orderNumber} is ...`, // content: `Order ${event.orderNumber} is ...`,
icon: `Order ${event.orderNumber} is ...`, // icon: `Order ${event.orderNumber} is ...`,
action: { // action: {
type: NotifTitleEnum.ORDER_STATUS_CHANGED, // type: NotifTitleEnum.ORDER_STATUS_CHANGED,
url: ``, // url: ``,
}, // },
}, // },
}, // },
}; // };
} // }
} // }
} }
+19 -24
View File
@@ -22,6 +22,7 @@ import { BulkReserveFoodDto } from 'src/modules/inventory/dto/bulk-reserve-food.
import { StatusTransitionRef } from '../interface/order.interface'; import { StatusTransitionRef } from '../interface/order.interface';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events'; import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events';
import { OrderMessage } from 'src/common/enums/message.enum';
type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number }; type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number };
type ValidatedCartForOrder = { type ValidatedCartForOrder = {
@@ -224,7 +225,7 @@ export class OrdersService {
}, },
); );
if (!order) { if (!order) {
throw new NotFoundException('Order not found'); throw new NotFoundException(OrderMessage.NOT_FOUND);
} }
return order; return order;
} }
@@ -291,11 +292,11 @@ export class OrdersService {
private assertStatusTransitionAllowed(order: Order, to: OrderStatus, ref: StatusTransitionRef) { private assertStatusTransitionAllowed(order: Order, to: OrderStatus, ref: StatusTransitionRef) {
const paymentMethod = order.paymentMethod?.method; const paymentMethod = order.paymentMethod?.method;
if (!paymentMethod) { if (!paymentMethod) {
throw new BadRequestException('Order payment method is missing'); throw new BadRequestException(OrderMessage.PAYMENT_METHOD_MISSING);
} }
if (!this.canTransition(order.status, to, paymentMethod, ref)) { if (!this.canTransition(order.status, to, paymentMethod, ref)) {
throw new BadRequestException(`Invalid status transition: ${order.status} -> ${to}`); throw new BadRequestException(OrderMessage.INVALID_STATUS_TRANSITION);
} }
} }
@@ -336,70 +337,66 @@ export class OrdersService {
{ id: orderId, restaurant: { id: restId } }, { id: orderId, restaurant: { id: restId } },
{ populate: ['paymentMethod'] }, { populate: ['paymentMethod'] },
); );
if (!order) throw new NotFoundException('Order not found'); if (!order) throw new NotFoundException(OrderMessage.NOT_FOUND);
return order; return order;
} }
private assertCartHasItems(cart: Cart) { private assertCartHasItems(cart: Cart) {
if (!cart.items || cart.items.length === 0) { if (!cart.items || cart.items.length === 0) {
throw new BadRequestException('Cart is empty. Add items to cart before creating an order.'); throw new BadRequestException(OrderMessage.CART_EMPTY);
} }
} }
private assertCartHasDeliveryMethod(cart: Cart) { private assertCartHasDeliveryMethod(cart: Cart) {
if (!cart.deliveryMethodId) { if (!cart.deliveryMethodId) {
throw new NotFoundException('Delivery method not found'); throw new NotFoundException(OrderMessage.DELIVERY_METHOD_NOT_FOUND);
} }
} }
private assertCartHasPaymentMethod(cart: Cart) { private assertCartHasPaymentMethod(cart: Cart) {
if (!cart.paymentMethodId) { if (!cart.paymentMethodId) {
throw new BadRequestException( throw new BadRequestException(OrderMessage.PAYMENT_METHOD_REQUIRED);
'Payment method is required. Please set a payment method before creating an order.',
);
} }
} }
private async getUserOrFail(userId: string): Promise<User> { private async getUserOrFail(userId: string): Promise<User> {
const user = await this.em.findOne(User, { id: userId }); const user = await this.em.findOne(User, { id: userId });
if (!user) throw new NotFoundException('User not found'); if (!user) throw new NotFoundException(OrderMessage.USER_NOT_FOUND);
return user; return user;
} }
private async getRestaurantOrFail(restaurantId: string): Promise<Restaurant> { private async getRestaurantOrFail(restaurantId: string): Promise<Restaurant> {
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
if (!restaurant) throw new NotFoundException('Restaurant not found'); if (!restaurant) throw new NotFoundException(OrderMessage.RESTAURANT_NOT_FOUND);
return restaurant; return restaurant;
} }
private async getDeliveryOrFail(deliveryId: string): Promise<Delivery> { private async getDeliveryOrFail(deliveryId: string): Promise<Delivery> {
const delivery = await this.em.findOne(Delivery, { id: deliveryId }); const delivery = await this.em.findOne(Delivery, { id: deliveryId });
if (!delivery) throw new NotFoundException('Delivery not found'); if (!delivery) throw new NotFoundException(OrderMessage.DELIVERY_NOT_FOUND);
return delivery; return delivery;
} }
private assertMeetsMinOrderForDelivery(cart: Cart, delivery: Delivery) { private assertMeetsMinOrderForDelivery(cart: Cart, delivery: Delivery) {
const minOrderPrice = Number(delivery.minOrderPrice) || 0; const minOrderPrice = Number(delivery.minOrderPrice) || 0;
if (minOrderPrice > 0 && cart.total < minOrderPrice) { if (minOrderPrice > 0 && cart.total < minOrderPrice) {
throw new BadRequestException( throw new BadRequestException(OrderMessage.MIN_ORDER_AMOUNT_NOT_MET);
`Minimum order amount for this delivery method is ${minOrderPrice}. Current total is ${cart.total}.`,
);
} }
} }
private assertDeliveryMethodRequirements(cart: Cart, delivery: Delivery) { private assertDeliveryMethodRequirements(cart: Cart, delivery: Delivery) {
if (delivery.method === DeliveryMethodEnum.DineIn) { if (delivery.method === DeliveryMethodEnum.DineIn) {
if (!cart.tableNumber || cart.tableNumber.trim() === '') { if (!cart.tableNumber || cart.tableNumber.trim() === '') {
throw new BadRequestException('Table number is required when delivery method is DineIn'); throw new BadRequestException(OrderMessage.TABLE_NUMBER_REQUIRED);
} }
} }
if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !cart.userAddress) { if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !cart.userAddress) {
throw new BadRequestException('Address is required. Please set a delivery address before creating an order.'); throw new BadRequestException(OrderMessage.ADDRESS_REQUIRED);
} }
if (delivery.method === DeliveryMethodEnum.DeliveryCar && !cart.carAddress) { if (delivery.method === DeliveryMethodEnum.DeliveryCar && !cart.carAddress) {
throw new BadRequestException('Car address is required. Please set a car address before creating an order.'); throw new BadRequestException(OrderMessage.CAR_ADDRESS_REQUIRED);
} }
} }
@@ -422,7 +419,7 @@ export class OrdersService {
private assertPaymentMethodEnabled(paymentMethod: PaymentMethod) { private assertPaymentMethodEnabled(paymentMethod: PaymentMethod) {
if (!paymentMethod.enabled) { if (!paymentMethod.enabled) {
throw new BadRequestException('Payment method is not enabled for this restaurant'); throw new BadRequestException(OrderMessage.PAYMENT_METHOD_NOT_ENABLED);
} }
} }
@@ -431,10 +428,10 @@ export class OrdersService {
for (const cartItem of cart.items) { for (const cartItem of cart.items) {
const food = await this.em.findOne(Food, { id: cartItem.foodId }, { populate: ['restaurant', 'inventory'] }); const food = await this.em.findOne(Food, { id: cartItem.foodId }, { populate: ['restaurant', 'inventory'] });
if (!food) throw new NotFoundException(`Food with ID ${cartItem.foodId} not found`); if (!food) throw new NotFoundException(OrderMessage.FOOD_NOT_FOUND);
if (food.restaurant.id !== restaurantId) { if (food.restaurant.id !== restaurantId) {
throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`); throw new BadRequestException(OrderMessage.FOOD_NOT_BELONGS_TO_RESTAURANT);
} }
this.assertFoodHasSufficientStock(food, cartItem.quantity); this.assertFoodHasSufficientStock(food, cartItem.quantity);
@@ -453,9 +450,7 @@ export class OrdersService {
private assertFoodHasSufficientStock(food: Food, quantity: number) { private assertFoodHasSufficientStock(food: Food, quantity: number) {
const availableStock = food.inventory?.availableStock ?? 0; const availableStock = food.inventory?.availableStock ?? 0;
if (availableStock < quantity) { if (availableStock < quantity) {
throw new BadRequestException( throw new BadRequestException(OrderMessage.FOOD_NOT_FOUND);
`Insufficient stock for food ${food.title || food.id}. Available: ${availableStock}, Requested: ${quantity}`,
);
} }
} }
@@ -10,6 +10,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard'; import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard';
import { AdminId } from 'src/common/decorators/admin-id.decorator'; import { AdminId } from 'src/common/decorators/admin-id.decorator';
import { UpdatePagerStatusDto } from '../dto/update-pager.dto'; import { UpdatePagerStatusDto } from '../dto/update-pager.dto';
import { PagerMessage } from 'src/common/enums/message.enum';
@ApiTags('pager') @ApiTags('pager')
@Controller() @Controller()
@@ -49,7 +50,7 @@ export class PagerController {
}); });
return reply.code(200).send({ return reply.code(200).send({
success: true, success: true,
message: 'Pager request created successfully', message: PagerMessage.CREATED_SUCCESS,
data: null, data: null,
}); });
} }
@@ -12,6 +12,7 @@ import {
IZarinpalVerifyResponse, IZarinpalVerifyResponse,
} from '../interface/gateway'; } from '../interface/gateway';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { PaymentMessage } from 'src/common/enums/message.enum';
@Injectable() @Injectable()
export class ZarinpalGateway implements IPaymentGateway { export class ZarinpalGateway implements IPaymentGateway {
@@ -64,7 +65,7 @@ export class ZarinpalGateway implements IPaymentGateway {
if (!data) { if (!data) {
this.logger.error('Invalid response from Zarinpal API (missing data)', JSON.stringify(res.data)); this.logger.error('Invalid response from Zarinpal API (missing data)', JSON.stringify(res.data));
throw new BadRequestException('Invalid response from Zarinpal API'); throw new BadRequestException(PaymentMessage.ZARINPAL_INVALID_RESPONSE);
} }
if ((Array.isArray(errors) && errors.length > 0) || code !== 100 || !authority) { if ((Array.isArray(errors) && errors.length > 0) || code !== 100 || !authority) {
@@ -79,7 +80,7 @@ export class ZarinpalGateway implements IPaymentGateway {
orderId, orderId,
}), }),
); );
throw new BadRequestException(message ?? 'Zarinpal payment request error'); throw new BadRequestException(message ?? PaymentMessage.ZARINPAL_PAYMENT_REQUEST_ERROR);
} }
return { transactionId: authority }; return { transactionId: authority };
@@ -98,12 +99,12 @@ export class ZarinpalGateway implements IPaymentGateway {
orderId, orderId,
}), }),
); );
throw new BadRequestException('Zarinpal payment request error'); throw new BadRequestException(PaymentMessage.ZARINPAL_PAYMENT_REQUEST_ERROR);
} }
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error('Zarinpal payment request error', errorMessage); this.logger.error('Zarinpal payment request error', errorMessage);
throw new BadRequestException(`Failed to request payment from gateway: ${errorMessage}`); throw new BadRequestException(PaymentMessage.GATEWAY_ERROR);
} }
} }
@@ -124,14 +125,14 @@ export class ZarinpalGateway implements IPaymentGateway {
// Check if response data exists // Check if response data exists
if (!res.data || !res.data.data) { if (!res.data || !res.data.data) {
throw new BadRequestException('Invalid response from Zarinpal API'); throw new BadRequestException(PaymentMessage.ZARINPAL_INVALID_RESPONSE);
} }
const { code, message, ref_id, card_pan } = res.data.data; const { code, message, ref_id, card_pan } = res.data.data;
// Check if there are errors in the response // Check if there are errors in the response
if (code !== 100 && code !== 0) { if (code !== 100 && code !== 0) {
throw new BadRequestException(message ?? 'Zarinpal error'); throw new BadRequestException(message ?? PaymentMessage.ZARINPAL_VERIFY_ERROR);
} }
return { return {
@@ -146,7 +147,7 @@ export class ZarinpalGateway implements IPaymentGateway {
throw new BadRequestException(error.response.data); throw new BadRequestException(error.response.data);
} }
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new BadRequestException(`Failed to verify payment with gateway: ${errorMessage}`); throw new BadRequestException(PaymentMessage.GATEWAY_ERROR);
} }
} }
@@ -20,6 +20,15 @@ export class PaymentListeners {
this.paymentSuccessSmsTemplateId = this.configService.get<string>('SMS_PATTERN_PAYMENT_SUCCESS') ?? '123'; this.paymentSuccessSmsTemplateId = this.configService.get<string>('SMS_PATTERN_PAYMENT_SUCCESS') ?? '123';
} }
private getPaymentMethodFarsi(method: string): string {
const methodMap: Record<string, string> = {
Cash: 'نقدی',
Wallet: 'کیف پول',
Online: 'آنلاین',
};
return methodMap[method] || method;
}
@OnEvent(paymentSucceedEvent.name) @OnEvent(paymentSucceedEvent.name)
async handlePaymentSucceed(event: paymentSucceedEvent) { async handlePaymentSucceed(event: paymentSucceedEvent) {
try { try {
@@ -36,7 +45,7 @@ export class PaymentListeners {
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
message: { message: {
title: NotifTitleEnum.PAYMENT_SUCCESS, title: NotifTitleEnum.PAYMENT_SUCCESS,
content: `payment order #${event.orderNumber} has been paid successfully with ${event.paymentMethod} method and total amount of ${event.total}`, content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${event.total} تومان با موفقیت انجام شد`,
sms: { sms: {
templateId: this.paymentSuccessSmsTemplateId, templateId: this.paymentSuccessSmsTemplateId,
parameters: { parameters: {
@@ -45,8 +54,8 @@ export class PaymentListeners {
}, },
}, },
pushNotif: { pushNotif: {
title: `Payment Order ${event.orderNumber} has been paid successfully with ${event.paymentMethod} method and total amount of ${event.total} `, title: `پرداخت موفق`,
content: `Payment Order ${event.orderNumber} has been paid successfully with ${event.paymentMethod} method and total amount of ${event.total} `, content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${event.total} تومان با موفقیت انجام شد`,
icon: ``, icon: ``,
action: { action: {
type: NotifTitleEnum.PAYMENT_SUCCESS, type: NotifTitleEnum.PAYMENT_SUCCESS,
@@ -6,6 +6,7 @@ import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto';
import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto'; import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto';
import { RequiredEntityData } from '@mikro-orm/core'; import { RequiredEntityData } from '@mikro-orm/core';
import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { RestMessage, PaymentMessage } from 'src/common/enums/message.enum';
@Injectable() @Injectable()
export class PaymentMethodService { export class PaymentMethodService {
@@ -18,7 +19,7 @@ export class PaymentMethodService {
// Check if restaurant exists // Check if restaurant exists
const restaurant = await this.em.findOne(Restaurant, { id: restId }); const restaurant = await this.em.findOne(Restaurant, { id: restId });
if (!restaurant) { if (!restaurant) {
throw new NotFoundException(`Restaurant with ID ${restId} not found`); throw new NotFoundException(RestMessage.NOT_FOUND);
} }
const paymentMethod = this.paymentMethodRepository.create({ const paymentMethod = this.paymentMethodRepository.create({
@@ -36,7 +37,7 @@ export class PaymentMethodService {
async findOne(id: string): Promise<PaymentMethod> { async findOne(id: string): Promise<PaymentMethod> {
const paymentMethod = await this.paymentMethodRepository.findOne({ id }, { populate: ['restaurant'] }); const paymentMethod = await this.paymentMethodRepository.findOne({ id }, { populate: ['restaurant'] });
if (!paymentMethod) { if (!paymentMethod) {
throw new NotFoundException(`PaymentMethod with ID ${id} not found`); throw new NotFoundException(PaymentMessage.PAYMENT_METHOD_NOT_FOUND);
} }
return paymentMethod; return paymentMethod;
} }
@@ -9,6 +9,7 @@ import { UserWallet } from 'src/modules/users/entities/user-wallet.entity';
import { OrderPaymentContext } from '../interface/payment'; import { OrderPaymentContext } from '../interface/payment';
import { OrderStatus } from 'src/modules/orders/interface/order.interface'; import { OrderStatus } from 'src/modules/orders/interface/order.interface';
import { ChartPeriodEnum, PaymentChartDto } from '../dto/payment-chart.dto'; import { ChartPeriodEnum, PaymentChartDto } from '../dto/payment-chart.dto';
import { PaymentMessage, OrderMessage } from 'src/common/enums/message.enum';
@Injectable() @Injectable()
export class PaymentsService { export class PaymentsService {
@@ -40,7 +41,7 @@ export class PaymentsService {
return this.handleOnlinePayment(ctx); return this.handleOnlinePayment(ctx);
default: default:
throw new BadRequestException('Unsupported payment method'); throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD);
} }
} }
@@ -52,22 +53,22 @@ export class PaymentsService {
); );
if (!order) { if (!order) {
throw new NotFoundException('Order not found'); throw new NotFoundException(OrderMessage.NOT_FOUND);
} }
if (order.total <= 0) { if (order.total <= 0) {
throw new BadRequestException('Amount must be greater than zero'); throw new BadRequestException(PaymentMessage.AMOUNT_MUST_BE_GREATER_THAN_ZERO);
} }
const pm = order.paymentMethod; const pm = order.paymentMethod;
if (!pm) { if (!pm) {
throw new NotFoundException('Payment method not found'); throw new NotFoundException(PaymentMessage.PAYMENT_METHOD_NOT_FOUND);
} }
if (pm.method === PaymentMethodEnum.Online) { if (pm.method === PaymentMethodEnum.Online) {
if (!pm.gateway) throw new BadRequestException('Gateway is required for online payment'); if (!pm.gateway) throw new BadRequestException(PaymentMessage.GATEWAY_REQUIRED);
if (!pm.merchantId) throw new BadRequestException('Merchant ID is required for online payment'); if (!pm.merchantId) throw new BadRequestException(PaymentMessage.MERCHANT_ID_REQUIRED);
if (!pm.restaurant?.domain) throw new BadRequestException('Restaurant domain is required for online payment'); if (!pm.restaurant?.domain) throw new BadRequestException(PaymentMessage.RESTAURANT_DOMAIN_REQUIRED);
} }
return { return {
@@ -91,7 +92,7 @@ export class PaymentsService {
private async handleWalletPayment(ctx: OrderPaymentContext): Promise<void> { private async handleWalletPayment(ctx: OrderPaymentContext): Promise<void> {
await this.em.transactional(async em => { await this.em.transactional(async em => {
const order = await em.findOne(Order, { id: ctx.order.id }, { populate: ['user', 'restaurant'] }); const order = await em.findOne(Order, { id: ctx.order.id }, { populate: ['user', 'restaurant'] });
if (!order) throw new NotFoundException('Order not found'); if (!order) throw new NotFoundException(OrderMessage.NOT_FOUND);
if (order.status === OrderStatus.PAID) { if (order.status === OrderStatus.PAID) {
return; return;
@@ -169,7 +170,7 @@ export class PaymentsService {
); );
if (!payment) { if (!payment) {
throw new NotFoundException('Payment not found'); throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
} }
if (payment.status === PaymentStatusEnum.Paid) { if (payment.status === PaymentStatusEnum.Paid) {
@@ -178,7 +179,7 @@ export class PaymentsService {
const pm = payment.order.paymentMethod; const pm = payment.order.paymentMethod;
if (!pm?.merchantId || !payment.gateway) { if (!pm?.merchantId || !payment.gateway) {
throw new BadRequestException('Invalid payment configuration'); throw new BadRequestException(PaymentMessage.INVALID_PAYMENT_CONFIGURATION);
} }
const gateway = this.gatewayManager.get(payment.gateway); const gateway = this.gatewayManager.get(payment.gateway);
@@ -215,13 +216,13 @@ export class PaymentsService {
const payment = await this.em.transactional(async em => { const payment = await this.em.transactional(async em => {
const payment = await em.findOne(Payment, id, { populate: ['order'] }); const payment = await em.findOne(Payment, id, { populate: ['order'] });
if (!payment) { if (!payment) {
throw new NotFoundException('Payment not found'); throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
} }
if (payment.method !== PaymentMethodEnum.Cash) { if (payment.method !== PaymentMethodEnum.Cash) {
throw new BadRequestException('Payment method is not cash'); throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH);
} }
if (payment.status === PaymentStatusEnum.Paid) { if (payment.status === PaymentStatusEnum.Paid) {
throw new BadRequestException('Payment already paid'); throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID);
} }
payment.order.status = OrderStatus.PAID; payment.order.status = OrderStatus.PAID;
payment.status = PaymentStatusEnum.Paid; payment.status = PaymentStatusEnum.Paid;
@@ -269,7 +270,7 @@ export class PaymentsService {
if (existing) { if (existing) {
if (existing.method !== params.method) { if (existing.method !== params.method) {
throw new BadRequestException('Existing pending payment method does not match order payment method'); throw new BadRequestException(PaymentMessage.EXISTING_PENDING_PAYMENT_MISMATCH);
} }
return existing; return existing;
@@ -10,6 +10,7 @@ import { RestId } from 'src/common/decorators';
import { FindUsersDto } from '../dto/find-user.dto'; import { FindUsersDto } from '../dto/find-user.dto';
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
import { API_HEADER_SLUG } from 'src/common/constants/index'; import { API_HEADER_SLUG } from 'src/common/constants/index';
import { UserSuccessMessage } from 'src/common/enums/message.enum';
@ApiTags('User') @ApiTags('User')
@Controller() @Controller()
@@ -107,7 +108,7 @@ export class UsersController {
@Delete('public/user/addresses/:addressId') @Delete('public/user/addresses/:addressId')
async deleteUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) { async deleteUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) {
await this.userService.deleteUserAddress(userId, addressId); await this.userService.deleteUserAddress(userId, addressId);
return { message: 'Address deleted successfully' }; return { message: UserSuccessMessage.ADDRESS_DELETED_SUCCESS };
} }
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@@ -129,7 +130,7 @@ export class UsersController {
async convertScoreToWallet(@UserId() userId: string, @RestId() restId: string) { async convertScoreToWallet(@UserId() userId: string, @RestId() restId: string) {
await this.userService.convertScoreToWallet(userId, restId); await this.userService.convertScoreToWallet(userId, restId);
return { return {
message: 'Score converted to wallet successfully', message: UserSuccessMessage.SCORE_CONVERTED_SUCCESS,
}; };
} }