This commit is contained in:
2025-12-13 11:14:18 +03:30
parent 3eb6b889a4
commit c25a5afe01
80 changed files with 440 additions and 190 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ export function cacheConfig(): CacheModuleAsyncOptions {
keyvRedisStore.client.on('connect', () => { keyvRedisStore.client.on('connect', () => {
logger.log('✅ Redis connected successfully.'); logger.log('✅ Redis connected successfully.');
}); });
keyvRedisStore.client.on('error', (error) => { keyvRedisStore.client.on('error', error => {
logger.error('❌ Redis connection error:', error); logger.error('❌ Redis connection error:', error);
}); });
@@ -40,4 +40,4 @@ export function cacheConfig(): CacheModuleAsyncOptions {
}; };
}, },
}; };
} }
+2 -2
View File
@@ -29,7 +29,7 @@ const dataBaseConfig: MikroOrmModuleAsyncOptions = {
const encodedPassword = encodeURIComponent(DB_PASS); const encodedPassword = encodeURIComponent(DB_PASS);
const isProduction = configService.getOrThrow<string>('NODE_ENV') === 'production'; const isProduction = configService.getOrThrow<string>('NODE_ENV') === 'production';
return ({ return {
// entities: ['dist/**/*.entity.js'], // entities: ['dist/**/*.entity.js'],
entitiesTs: ['src/**/*.entity.ts'], entitiesTs: ['src/**/*.entity.ts'],
driver: PostgreSqlDriver, driver: PostgreSqlDriver,
@@ -102,7 +102,7 @@ const dataBaseConfig: MikroOrmModuleAsyncOptions = {
return false; return false;
}, },
}, },
}); };
}, },
}; };
@@ -174,5 +174,4 @@ export class AdminService {
} }
return this.em.removeAndFlush(adminRole); return this.em.removeAndFlush(adminRole);
} }
} }
@@ -1,6 +1,5 @@
import { SetMetadata } from '@nestjs/common'; import { SetMetadata } from '@nestjs/common';
export const PERMISSIONS_KEY = 'permissions'; export const PERMISSIONS_KEY = 'permissions';
export const Permissions = (...permissions: string[]) => export const Permissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);
SetMetadata(PERMISSIONS_KEY, permissions);
@@ -1,5 +1,5 @@
import { User } from '../../users/entities/user.entity'; import type { User } from '../../users/entities/user.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import type { Restaurant } from '../../restaurants/entities/restaurant.entity';
export interface UserLoginResponse { export interface UserLoginResponse {
id: string; id: string;
@@ -34,4 +34,3 @@ export class UserLoginTransformer {
}; };
} }
} }
+112 -14
View File
@@ -22,14 +22,28 @@ export class CartController {
@Get() @Get()
@ApiOperation({ summary: 'Get cart for current user and restaurant' }) @ApiOperation({ summary: 'Get cart for current user and restaurant' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
async findOne(@UserId() userId: string, @RestId() restaurantId: string) { async findOne(@UserId() userId: string, @RestId() restaurantId: string) {
return this.cartService.getOrCreateCart(userId, restaurantId); return this.cartService.getOrCreateCart(userId, restaurantId);
} }
@Post('items/:foodId/increment') @Post('items/:foodId/increment')
@ApiOperation({ summary: 'Increment item quantity in cart by 1' }) @ApiOperation({ summary: 'Increment item quantity in cart by 1' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'foodId', description: 'Food ID to increment in cart' }) @ApiParam({ name: 'foodId', description: 'Food ID to increment in cart' })
async incrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) { async incrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.incrementItem(userId, restaurantId, foodId); return this.cartService.incrementItem(userId, restaurantId, foodId);
@@ -37,7 +51,14 @@ export class CartController {
@Post('items/:foodId/decrement') @Post('items/:foodId/decrement')
@ApiOperation({ summary: 'Decrement item quantity in cart by 1 (removes item if quantity reaches 0)' }) @ApiOperation({ summary: 'Decrement item quantity in cart by 1 (removes item if quantity reaches 0)' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'foodId', description: 'Food ID to decrement in cart' }) @ApiParam({ name: 'foodId', description: 'Food ID to decrement in cart' })
async decrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) { async decrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.decrementItem(userId, restaurantId, foodId); return this.cartService.decrementItem(userId, restaurantId, foodId);
@@ -45,7 +66,14 @@ export class CartController {
@Post('items/bulk') @Post('items/bulk')
@ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' }) @ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: BulkAddItemsToCartDto }) @ApiBody({ type: BulkAddItemsToCartDto })
bulkAddItems( bulkAddItems(
@UserId() userId: string, @UserId() userId: string,
@@ -57,7 +85,14 @@ export class CartController {
@Patch('items/:foodId') @Patch('items/:foodId')
@ApiOperation({ summary: 'Update item quantity in cart' }) @ApiOperation({ summary: 'Update item quantity in cart' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'foodId', description: 'Food ID in the cart' }) @ApiParam({ name: 'foodId', description: 'Food ID in the cart' })
@ApiBody({ type: UpdateItemQuantityDto }) @ApiBody({ type: UpdateItemQuantityDto })
async updateItemQuantity( async updateItemQuantity(
@@ -71,7 +106,14 @@ export class CartController {
@Delete('items/:foodId') @Delete('items/:foodId')
@ApiOperation({ summary: 'Remove item from cart' }) @ApiOperation({ summary: 'Remove item from cart' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'foodId', description: 'Food ID in the cart' }) @ApiParam({ name: 'foodId', description: 'Food ID in the cart' })
async removeItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) { async removeItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.removeItem(userId, restaurantId, foodId); return this.cartService.removeItem(userId, restaurantId, foodId);
@@ -79,14 +121,28 @@ export class CartController {
@Delete() @Delete()
@ApiOperation({ summary: 'Clear entire cart' }) @ApiOperation({ summary: 'Clear entire cart' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
async clearCart(@UserId() userId: string, @RestId() restaurantId: string) { async clearCart(@UserId() userId: string, @RestId() restaurantId: string) {
return this.cartService.clearCart(userId, restaurantId); return this.cartService.clearCart(userId, restaurantId);
} }
@Post('apply-coupon') @Post('apply-coupon')
@ApiOperation({ summary: 'Apply coupon to cart' }) @ApiOperation({ summary: 'Apply coupon to cart' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: ApplyCouponDto }) @ApiBody({ type: ApplyCouponDto })
async applyCoupon(@UserId() userId: string, @RestId() restaurantId: string, @Body() applyCouponDto: ApplyCouponDto) { async applyCoupon(@UserId() userId: string, @RestId() restaurantId: string, @Body() applyCouponDto: ApplyCouponDto) {
return this.cartService.applyCoupon(userId, restaurantId, applyCouponDto); return this.cartService.applyCoupon(userId, restaurantId, applyCouponDto);
@@ -94,14 +150,28 @@ export class CartController {
@Delete('coupon') @Delete('coupon')
@ApiOperation({ summary: 'Remove coupon from cart' }) @ApiOperation({ summary: 'Remove coupon from cart' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
async removeCoupon(@UserId() userId: string, @RestId() restaurantId: string) { async removeCoupon(@UserId() userId: string, @RestId() restaurantId: string) {
return this.cartService.removeCoupon(userId, restaurantId); return this.cartService.removeCoupon(userId, restaurantId);
} }
@Patch('address') @Patch('address')
@ApiOperation({ summary: 'Set address for cart' }) @ApiOperation({ summary: 'Set address for cart' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: SetAddressDto }) @ApiBody({ type: SetAddressDto })
async setAddress(@UserId() userId: string, @RestId() restaurantId: string, @Body() setAddressDto: SetAddressDto) { async setAddress(@UserId() userId: string, @RestId() restaurantId: string, @Body() setAddressDto: SetAddressDto) {
return this.cartService.setAddress(userId, restaurantId, setAddressDto); return this.cartService.setAddress(userId, restaurantId, setAddressDto);
@@ -109,7 +179,14 @@ export class CartController {
@Patch('payment-method') @Patch('payment-method')
@ApiOperation({ summary: 'Set payment method for cart' }) @ApiOperation({ summary: 'Set payment method for cart' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: SetPaymentMethodDto }) @ApiBody({ type: SetPaymentMethodDto })
async setPaymentMethod( async setPaymentMethod(
@UserId() userId: string, @UserId() userId: string,
@@ -121,7 +198,14 @@ export class CartController {
@Patch('delivery-method') @Patch('delivery-method')
@ApiOperation({ summary: 'Set delivery method for cart' }) @ApiOperation({ summary: 'Set delivery method for cart' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: SetDeliveryMethodDto }) @ApiBody({ type: SetDeliveryMethodDto })
setDeliveryMethod( setDeliveryMethod(
@UserId() userId: string, @UserId() userId: string,
@@ -133,7 +217,14 @@ export class CartController {
@Patch('description') @Patch('description')
@ApiOperation({ summary: 'Set description for cart' }) @ApiOperation({ summary: 'Set description for cart' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: SetDescriptionDto }) @ApiBody({ type: SetDescriptionDto })
setDescription( setDescription(
@UserId() userId: string, @UserId() userId: string,
@@ -145,7 +236,14 @@ export class CartController {
@Patch('table-number') @Patch('table-number')
@ApiOperation({ summary: 'Set table number for cart (required when delivery method is DineIn)' }) @ApiOperation({ summary: 'Set table number for cart (required when delivery method is DineIn)' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: SetTableNumberDto }) @ApiBody({ type: SetTableNumberDto })
setTableNumber( setTableNumber(
@UserId() userId: string, @UserId() userId: string,
-1
View File
@@ -7,4 +7,3 @@ export class ApplyCouponDto {
@IsString() @IsString()
code!: string; code!: string;
} }
-1
View File
@@ -7,4 +7,3 @@ export class SetAddressDto {
@IsString() @IsString()
addressId!: string; addressId!: string;
} }
@@ -7,4 +7,3 @@ export class SetDeliveryMethodDto {
@IsString() @IsString()
deliveryMethodId!: string; deliveryMethodId!: string;
} }
+5 -2
View File
@@ -2,9 +2,12 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator'; import { IsOptional, IsString } from 'class-validator';
export class SetDescriptionDto { export class SetDescriptionDto {
@ApiProperty({ description: 'Cart description or notes', required: false, example: 'Please deliver to the back door' }) @ApiProperty({
description: 'Cart description or notes',
required: false,
example: 'Please deliver to the back door',
})
@IsOptional() @IsOptional()
@IsString() @IsString()
description?: string; description?: string;
} }
@@ -7,4 +7,3 @@ export class SetPaymentMethodDto {
@IsString() @IsString()
paymentMethodId!: string; paymentMethodId!: string;
} }
@@ -7,4 +7,3 @@ export class SetTableNumberDto {
@IsString() @IsString()
tableNumber?: string; tableNumber?: string;
} }
-1
View File
@@ -8,4 +8,3 @@ export class UpdateItemQuantityDto {
@Min(1) @Min(1)
quantity!: number; quantity!: number;
} }
+1 -5
View File
@@ -613,11 +613,7 @@ export class CartService {
/** /**
* Set table number for cart * Set table number for cart
*/ */
async setTableNumber( async setTableNumber(userId: string, restaurantId: string, setTableNumberDto: SetTableNumberDto): Promise<Cart> {
userId: string,
restaurantId: string,
setTableNumberDto: SetTableNumberDto,
): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId); const cart = await this.findOneOrFail(userId, restaurantId);
// If delivery method is set, validate that table number is required for DineIn // If delivery method is set, validate that table number is required for DineIn
@@ -25,7 +25,14 @@ export class ContactController {
@Post('public/contact') @Post('public/contact')
@ApiOperation({ summary: 'Create a new contact (public endpoint)' }) @ApiOperation({ summary: 'Create a new contact (public endpoint)' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
async create(@Body() createContactDto: CreateContactDto) { async create(@Body() createContactDto: CreateContactDto) {
return this.contactService.create(createContactDto); return this.contactService.create(createContactDto);
} }
@@ -39,4 +39,3 @@ export class FindContactsDto {
@ApiPropertyOptional({ enum: ContactStatusEnum, description: 'Filter by status' }) @ApiPropertyOptional({ enum: ContactStatusEnum, description: 'Filter by status' })
status?: ContactStatusEnum; status?: ContactStatusEnum;
} }
@@ -8,4 +8,3 @@ export class UpdateContactStatusDto {
@ApiProperty({ enum: ContactStatusEnum, description: 'New status for the contact' }) @ApiProperty({ enum: ContactStatusEnum, description: 'New status for the contact' })
status!: ContactStatusEnum; status!: ContactStatusEnum;
} }
@@ -37,11 +37,7 @@ export class ContactRepository extends EntityRepository<Contact> {
if (search) { if (search) {
const pattern = `%${search}%`; const pattern = `%${search}%`;
where.$or = [ where.$or = [{ subject: { $ilike: pattern } }, { content: { $ilike: pattern } }, { phone: { $ilike: pattern } }];
{ subject: { $ilike: pattern } },
{ content: { $ilike: pattern } },
{ phone: { $ilike: pattern } },
];
} }
const [data, total] = await this.findAndCount(where, { const [data, total] = await this.findAndCount(where, {
@@ -63,4 +59,3 @@ export class ContactRepository extends EntityRepository<Contact> {
}; };
} }
} }
@@ -28,7 +28,14 @@ export class CouponController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('public/coupons/me') @Get('public/coupons/me')
@ApiOperation({ summary: 'Get paginated list of restaurant coupons' }) @ApiOperation({ summary: 'Get paginated list of restaurant coupons' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiOkResponse({ description: 'Paginated list of restaurant coupons' }) @ApiOkResponse({ description: 'Paginated list of restaurant coupons' })
@ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number }) @ApiQuery({ name: 'limit', required: false, type: Number })
-1
View File
@@ -15,4 +15,3 @@ import { JwtModule } from '@nestjs/jwt';
exports: [CouponRepository, CouponService], exports: [CouponRepository, CouponService],
}) })
export class CouponModule {} export class CouponModule {}
@@ -42,4 +42,3 @@ export class FindCouponsDto {
@ApiPropertyOptional({ enum: ['asc', 'desc'], example: 'desc', description: 'Sort order' }) @ApiPropertyOptional({ enum: ['asc', 'desc'], example: 'desc', description: 'Sort order' })
order?: 'asc' | 'desc'; order?: 'asc' | 'desc';
} }
@@ -2,4 +2,3 @@ import { PartialType } from '@nestjs/swagger';
import { CreateCouponDto } from './create-coupon.dto'; import { CreateCouponDto } from './create-coupon.dto';
export class UpdateCouponDto extends PartialType(CreateCouponDto) {} export class UpdateCouponDto extends PartialType(CreateCouponDto) {}
@@ -19,4 +19,3 @@ export class ValidateCouponDto {
@ApiPropertyOptional({ example: 'user-id', description: 'User ID for per-user usage limits' }) @ApiPropertyOptional({ example: 'user-id', description: 'User ID for per-user usage limits' })
userId?: string; userId?: string;
} }
@@ -27,7 +27,14 @@ export class DeliveryController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('public/delivery-methods/restaurant') @Get('public/delivery-methods/restaurant')
@ApiOperation({ summary: 'Get restaurant delivery methods' }) @ApiOperation({ summary: 'Get restaurant delivery methods' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiOkResponse({ @ApiOkResponse({
description: 'List of restaurant delivery methods', description: 'List of restaurant delivery methods',
type: [Delivery], type: [Delivery],
@@ -8,4 +8,3 @@ export class DeliveryRepository extends EntityRepository<Delivery> {
super(em, Delivery); super(em, Delivery);
} }
} }
@@ -25,7 +25,14 @@ export class CategoryController {
@Get('public/categories/restaurant/:slug') @Get('public/categories/restaurant/:slug')
@ApiOperation({ summary: 'Get all categories by restaurant slug' }) @ApiOperation({ summary: 'Get all categories by restaurant slug' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' }) @ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' })
@ApiOkResponse({ description: 'List of all categories for the restaurant' }) @ApiOkResponse({ description: 'List of all categories for the restaurant' })
findAllByRestaurant(@Param('slug') slug: string) { findAllByRestaurant(@Param('slug') slug: string) {
@@ -25,7 +25,14 @@ export class FoodController {
@Get('public/foods/restaurant/:slug') @Get('public/foods/restaurant/:slug')
@ApiOperation({ summary: 'Get all foods by restaurant slug' }) @ApiOperation({ summary: 'Get all foods by restaurant slug' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' }) @ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' })
@ApiOkResponse({ description: 'List of all foods for the restaurant' }) @ApiOkResponse({ description: 'List of all foods for the restaurant' })
findAllByRestaurant(@Param('slug') slug: string) { findAllByRestaurant(@Param('slug') slug: string) {
@@ -21,7 +21,14 @@ export class NotificationsController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('public/notifications') @Get('public/notifications')
@ApiOperation({ summary: 'Get user restaurant notifications' }) @ApiOperation({ summary: 'Get user restaurant notifications' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiQuery({ name: 'limit', required: false, type: Number }) @ApiQuery({ name: 'limit', required: false, type: Number })
async getUserNotifications(@UserId() userId: string, @RestId() restaurantId: string, @Query('limit') limit?: number) { async getUserNotifications(@UserId() userId: string, @RestId() restaurantId: string, @Query('limit') limit?: number) {
return await this.notificationService.findByUserAndRestaurant( return await this.notificationService.findByUserAndRestaurant(
@@ -1,5 +1,6 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import type { ExecutionContext } from '@nestjs/common';
import { AuthenticatedSocket } from '../guards/ws-admin-auth.guard'; import { createParamDecorator } from '@nestjs/common';
import type { AuthenticatedSocket } from '../guards/ws-admin-auth.guard';
/** /**
* Extract restaurant ID from authenticated WebSocket client * Extract restaurant ID from authenticated WebSocket client
@@ -38,4 +38,3 @@ export class CreateNotificationDto {
@IsOptional() @IsOptional()
data?: Record<string, any>; data?: Record<string, any>;
} }
@@ -28,7 +28,7 @@ export class PushNotificationService {
private initializeFirebase() { private initializeFirebase() {
try { try {
const firebaseConfig = this.configService.get<string>('FIREBASE_SERVICE_ACCOUNT'); const firebaseConfig = this.configService.get<string>('FIREBASE_SERVICE_ACCOUNT');
if (!firebaseConfig) { if (!firebaseConfig) {
this.logger.warn('Firebase service account not configured. Push notifications will be disabled.'); this.logger.warn('Firebase service account not configured. Push notifications will be disabled.');
return; return;
@@ -51,7 +51,10 @@ export class PushNotificationService {
async sendToToken(payload: PushNotificationPayload): Promise<PushNotificationResponse> { async sendToToken(payload: PushNotificationPayload): Promise<PushNotificationResponse> {
if (!this.firebaseApp || !payload.token) { if (!this.firebaseApp || !payload.token) {
throw new HttpException('Push notification service not configured or token missing', HttpStatus.SERVICE_UNAVAILABLE); throw new HttpException(
'Push notification service not configured or token missing',
HttpStatus.SERVICE_UNAVAILABLE,
);
} }
try { try {
@@ -115,9 +118,7 @@ export class PushNotificationService {
}; };
const response = await admin.messaging().sendEachForMulticast(message); const response = await admin.messaging().sendEachForMulticast(message);
this.logger.log( this.logger.log(`Push notifications sent: ${response.successCount} successful, ${response.failureCount} failed`);
`Push notifications sent: ${response.successCount} successful, ${response.failureCount} failed`,
);
return { return {
success: response.successCount > 0, success: response.successCount > 0,
@@ -145,4 +146,3 @@ export class PushNotificationService {
return this.firebaseApp !== null; return this.firebaseApp !== null;
} }
} }
@@ -16,7 +16,14 @@ export class OrdersController {
@ApiBearerAuth() @ApiBearerAuth()
@Post('public/checkout') @Post('public/checkout')
@ApiOperation({ summary: 'Checkout : create order and payment record' }) @ApiOperation({ summary: 'Checkout : create order and payment record' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
checkout(@UserId() userId: string, @RestId() restaurantId: string) { checkout(@UserId() userId: string, @RestId() restaurantId: string) {
return this.ordersService.checkout(userId, restaurantId); return this.ordersService.checkout(userId, restaurantId);
} }
@@ -25,7 +32,14 @@ export class OrdersController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('public/orders') @Get('public/orders')
@ApiOperation({ summary: 'Get all orders with pagination and filters' }) @ApiOperation({ summary: 'Get all orders with pagination and filters' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
findAll(@RestId() restId: string, @Query() dto: FindOrdersDto, @UserId() userId: string) { findAll(@RestId() restId: string, @Query() dto: FindOrdersDto, @UserId() userId: string) {
return this.ordersService.findAllForUser(restId, dto, userId); return this.ordersService.findAllForUser(restId, dto, userId);
} }
@@ -33,7 +47,14 @@ export class OrdersController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Get an order By id for User' }) @ApiOperation({ summary: 'Get an order By id for User' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'orderId', description: 'Order ID' }) @ApiParam({ name: 'orderId', description: 'Order ID' })
@Get('public/orders/:orderId') @Get('public/orders/:orderId')
findOne(@Param('orderId') orderId: string, @RestId() restId: string) { findOne(@Param('orderId') orderId: string, @RestId() restId: string) {
@@ -44,7 +65,14 @@ export class OrdersController {
@ApiBearerAuth() @ApiBearerAuth()
@Patch('public/orders/:id/cancel') @Patch('public/orders/:id/cancel')
@ApiOperation({ summary: 'Cancel an order By User' }) @ApiOperation({ summary: 'Cancel an order By User' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'id', description: 'Order ID' }) @ApiParam({ name: 'id', description: 'Order ID' })
cancelOrder(@Param('id') id: string, @RestId() restId: string) { cancelOrder(@Param('id') id: string, @RestId() restId: string) {
return this.ordersService.cancelOrderAsUser(id, restId); return this.ordersService.cancelOrderAsUser(id, restId);
@@ -124,4 +124,3 @@ export class FindOrdersDto {
@IsIn(sortOrderOptions) @IsIn(sortOrderOptions)
order?: SortOrder = 'desc'; order?: SortOrder = 'desc';
} }
@@ -57,7 +57,14 @@ export class PagerController {
@UseGuards(OptionalAuthGuard) @UseGuards(OptionalAuthGuard)
@Get('public/pager') @Get('public/pager')
@ApiOperation({ summary: 'Get all pager requests for the current session' }) @ApiOperation({ summary: 'Get all pager requests for the current session' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiHeader({ name: 'Authorization', required: false, description: 'Bearer token (optional authentication)' }) @ApiHeader({ name: 'Authorization', required: false, description: 'Bearer token (optional authentication)' })
async findAll(@Req() request: FastifyRequest) { async findAll(@Req() request: FastifyRequest) {
const cookieId = request.cookies['pagerId'] || undefined; const cookieId = request.cookies['pagerId'] || undefined;
@@ -1,5 +1,6 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import type { ExecutionContext } from '@nestjs/common';
import { AuthenticatedSocket } from '../guards/ws-admin-auth.guard'; import { createParamDecorator } from '@nestjs/common';
import type { AuthenticatedSocket } from '../guards/ws-admin-auth.guard';
/** /**
* Extract admin ID from authenticated WebSocket client * Extract admin ID from authenticated WebSocket client
@@ -23,4 +24,3 @@ export const WsAdminId = createParamDecorator((data: unknown, ctx: ExecutionCont
return adminId; return adminId;
}); });
@@ -1,5 +1,6 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import type { ExecutionContext } from '@nestjs/common';
import { AuthenticatedSocket } from '../guards/ws-admin-auth.guard'; import { createParamDecorator } from '@nestjs/common';
import type { AuthenticatedSocket } from '../guards/ws-admin-auth.guard';
/** /**
* Extract restaurant ID from authenticated WebSocket client * Extract restaurant ID from authenticated WebSocket client
@@ -23,4 +24,3 @@ export const WsRestId = createParamDecorator((data: unknown, ctx: ExecutionConte
return restId; return restId;
}); });
+1 -1
View File
@@ -69,7 +69,7 @@ export class PagerService {
// Populate relations before emitting // Populate relations before emitting
await this.em.populate(pager, ['restaurant']); await this.em.populate(pager, ['restaurant']);
// Emit socket event for new pager // Emit socket event for new pager
this.eventEmitter.emit(PagerCreatedEvent.name, new PagerCreatedEvent(pager.restaurant.id)); this.eventEmitter.emit(PagerCreatedEvent.name, new PagerCreatedEvent(pager.restaurant.id));
return pager; return pager;
@@ -29,7 +29,14 @@ export class PaymentsController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('public/payments/methods/restaurant') @Get('public/payments/methods/restaurant')
@ApiOperation({ summary: 'Get the restaurant payment methods' }) @ApiOperation({ summary: 'Get the restaurant payment methods' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiNotFoundResponse({ description: 'Restaurant payment methods not found' }) @ApiNotFoundResponse({ description: 'Restaurant payment methods not found' })
getTheRestaurantPaymentMethods(@RestId() restId: string) { getTheRestaurantPaymentMethods(@RestId() restId: string) {
return this.paymentMethodService.findByRestaurant(restId); return this.paymentMethodService.findByRestaurant(restId);
@@ -39,7 +46,14 @@ export class PaymentsController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('public/payments') @Get('public/payments')
@ApiOperation({ summary: 'Get all the restaurant payments for user' }) @ApiOperation({ summary: 'Get all the restaurant payments for user' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
findAllByRestaurant(@UserId() userId: string, @RestId() restId: string) { findAllByRestaurant(@UserId() userId: string, @RestId() restId: string) {
return this.paymentsService.findAllByRestaurantId(restId, userId); return this.paymentsService.findAllByRestaurantId(restId, userId);
} }
@@ -127,7 +141,14 @@ export class PaymentsController {
@Post('public/payments/verify') @Post('public/payments/verify')
@ApiOperation({ summary: 'Verify a payment' }) @ApiOperation({ summary: 'Verify a payment' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: VerifyPaymentDto }) @ApiBody({ type: VerifyPaymentDto })
@ApiNotFoundResponse({ description: 'Payment not found' }) @ApiNotFoundResponse({ description: 'Payment not found' })
verifyPayment(@Body() verifyPaymentDto: VerifyPaymentDto) { verifyPayment(@Body() verifyPaymentDto: VerifyPaymentDto) {
@@ -2,4 +2,3 @@ import { PartialType } from '@nestjs/mapped-types';
import { CreatePaymentMethodDto } from './create-payment-method.dto'; import { CreatePaymentMethodDto } from './create-payment-method.dto';
export class UpdatePaymentMethodDto extends PartialType(CreatePaymentMethodDto) {} export class UpdatePaymentMethodDto extends PartialType(CreatePaymentMethodDto) {}
@@ -42,10 +42,7 @@ export class PaymentMethodService {
} }
async findByRestaurant(restaurantId: string): Promise<PaymentMethod[]> { async findByRestaurant(restaurantId: string): Promise<PaymentMethod[]> {
return this.paymentMethodRepository.find( return this.paymentMethodRepository.find({ restaurant: { id: restaurantId } }, { populate: ['restaurant'] });
{ restaurant: { id: restaurantId } },
{ populate: ['restaurant'] },
);
} }
async update(id: string, updatePaymentMethodDto: UpdatePaymentMethodDto): Promise<PaymentMethod> { async update(id: string, updatePaymentMethodDto: UpdatePaymentMethodDto): Promise<PaymentMethod> {
@@ -21,7 +21,14 @@ export class RestaurantsController {
@Get('public/restaurants/:slug') @Get('public/restaurants/:slug')
@ApiOperation({ summary: 'Get restaurant specification by slug' }) @ApiOperation({ summary: 'Get restaurant specification by slug' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'slug', required: true, description: 'Restaurant slug' }) @ApiParam({ name: 'slug', required: true, description: 'Restaurant slug' })
@ApiNotFoundResponse({ description: 'Restaurant not found' }) @ApiNotFoundResponse({ description: 'Restaurant not found' })
findBySlug(@Param('slug') slug: string) { findBySlug(@Param('slug') slug: string) {
@@ -26,7 +26,14 @@ export class ScheduleController {
@Get('public/schedules/restaurant/:slug') @Get('public/schedules/restaurant/:slug')
@ApiOperation({ summary: 'Get schedule of a restaurant by slug' }) @ApiOperation({ summary: 'Get schedule of a restaurant by slug' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'slug', description: 'Restaurant Slug' }) @ApiParam({ name: 'slug', description: 'Restaurant Slug' })
@ApiOkResponse({ description: 'Today schedule of the restaurant', type: Schedule }) @ApiOkResponse({ description: 'Today schedule of the restaurant', type: Schedule })
async getTodaySchedules(@Param('slug') slug: string): Promise<Schedule[]> { async getTodaySchedules(@Param('slug') slug: string): Promise<Schedule[]> {
@@ -29,5 +29,4 @@ export class CreateScheduleDto {
@IsBoolean() @IsBoolean()
@Type(() => Boolean) @Type(() => Boolean)
isActive?: boolean; isActive?: boolean;
} }
@@ -26,7 +26,18 @@ export class RestaurantSpecificationDto {
longitude?: number; longitude?: number;
@ApiPropertyOptional({ @ApiPropertyOptional({
example: { type: 'Polygon', coordinates: [[[51.389, 35.6892], [51.390, 35.6892], [51.390, 35.6902], [51.389, 35.6902], [51.389, 35.6892]]] }, example: {
type: 'Polygon',
coordinates: [
[
[51.389, 35.6892],
[51.39, 35.6892],
[51.39, 35.6902],
[51.389, 35.6902],
[51.389, 35.6892],
],
],
},
description: 'Service area as GeoJSON Polygon', description: 'Service area as GeoJSON Polygon',
}) })
serviceArea?: { serviceArea?: {
@@ -30,7 +30,14 @@ export class ReviewController {
@ApiBearerAuth() @ApiBearerAuth()
@Post('public/reviews') @Post('public/reviews')
@ApiOperation({ summary: 'Create a new review and rating' }) @ApiOperation({ summary: 'Create a new review and rating' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiCreatedResponse({ description: 'The review has been successfully created.' }) @ApiCreatedResponse({ description: 'The review has been successfully created.' })
@ApiBody({ type: CreateReviewDto }) @ApiBody({ type: CreateReviewDto })
create(@Body() createReviewDto: CreateReviewDto, @UserId() userId: string) { create(@Body() createReviewDto: CreateReviewDto, @UserId() userId: string) {
@@ -39,7 +46,14 @@ export class ReviewController {
@Get('public/reviews/:foodId') @Get('public/reviews/:foodId')
@ApiOperation({ summary: 'Get all reviews of food by id(public - only approved)' }) @ApiOperation({ summary: 'Get all reviews of food by id(public - only approved)' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiOkResponse({ description: 'List of approved reviews' }) @ApiOkResponse({ description: 'List of approved reviews' })
@ApiParam({ name: 'foodId', required: true, type: String }) @ApiParam({ name: 'foodId', required: true, type: String })
@ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'page', required: false, type: Number })
@@ -53,7 +67,14 @@ export class ReviewController {
@Get('public/reviews/restuarant/:restuarantSlug') @Get('public/reviews/restuarant/:restuarantSlug')
@ApiOperation({ summary: 'Get all reviews of restuarant by slug(public - only approved)' }) @ApiOperation({ summary: 'Get all reviews of restuarant by slug(public - only approved)' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiOkResponse({ description: 'List of approved reviews' }) @ApiOkResponse({ description: 'List of approved reviews' })
@ApiParam({ name: 'restuarantSlug', required: true, type: String }) @ApiParam({ name: 'restuarantSlug', required: true, type: String })
@ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'page', required: false, type: Number })
@@ -78,7 +99,14 @@ export class ReviewController {
@ApiBearerAuth() @ApiBearerAuth()
@Patch('public/reviews/:reviewId') @Patch('public/reviews/:reviewId')
@ApiOperation({ summary: 'Update a review (own reviews only)' }) @ApiOperation({ summary: 'Update a review (own reviews only)' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'reviewId', required: true }) @ApiParam({ name: 'reviewId', required: true })
@ApiBody({ type: UpdateReviewDto }) @ApiBody({ type: UpdateReviewDto })
@ApiOkResponse({ description: 'The updated review' }) @ApiOkResponse({ description: 'The updated review' })
@@ -90,7 +118,14 @@ export class ReviewController {
@ApiBearerAuth() @ApiBearerAuth()
@Delete('public/reviews/:id') @Delete('public/reviews/:id')
@ApiOperation({ summary: 'Delete a review (own reviews only)' }) @ApiOperation({ summary: 'Delete a review (own reviews only)' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiParam({ name: 'id', required: true }) @ApiParam({ name: 'id', required: true })
remove(@Param('id') id: string, @UserId() userId: string) { remove(@Param('id') id: string, @UserId() userId: string) {
return this.reviewService.remove(id, userId, false); return this.reviewService.remove(id, userId, false);
@@ -11,4 +11,3 @@ export class ChangeStatusDto {
}) })
status: ReviewStatus; status: ReviewStatus;
} }
+6 -7
View File
@@ -21,20 +21,20 @@ export class UpdateReviewDto {
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@IsEnum(PositivePoint, { each: true }) @IsEnum(PositivePoint, { each: true })
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Positive points about the food', description: 'Positive points about the food',
enum: PositivePoint, enum: PositivePoint,
isArray: true isArray: true,
}) })
positivePoints?: PositivePoint[]; positivePoints?: PositivePoint[];
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@IsEnum(NegativePoint, { each: true }) @IsEnum(NegativePoint, { each: true })
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Negative points about the food', description: 'Negative points about the food',
enum: NegativePoint, enum: NegativePoint,
isArray: true isArray: true,
}) })
negativePoints?: NegativePoint[]; negativePoints?: NegativePoint[];
@@ -43,4 +43,3 @@ export class UpdateReviewDto {
@ApiPropertyOptional({ description: 'Review status (admin only)', enum: ReviewStatus }) @ApiPropertyOptional({ description: 'Review status (admin only)', enum: ReviewStatus })
status?: ReviewStatus; status?: ReviewStatus;
} }
@@ -3,4 +3,3 @@ export enum ReviewStatus {
REJECTED = 'rejected', REJECTED = 'rejected',
APPROVED = 'approved', APPROVED = 'approved',
} }
@@ -69,12 +69,9 @@ export class FoodRatingCronService {
// Flush all updates at once for better performance // Flush all updates at once for better performance
await this.em.flush(); await this.em.flush();
this.logger.log( this.logger.log(`Food rating calculation completed. Updated: ${updatedCount}, Errors: ${errorCount}`);
`Food rating calculation completed. Updated: ${updatedCount}, Errors: ${errorCount}`,
);
} catch (error) { } catch (error) {
this.logger.error(`Error in food rating cron job: ${error.message}`, error.stack); this.logger.error(`Error in food rating cron job: ${error.message}`, error.stack);
} }
} }
} }
@@ -53,7 +53,7 @@ export class ReviewRepository extends EntityRepository<Review> {
limit, limit,
offset, offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: ['food','user'], populate: ['food', 'user'],
}); });
const totalPages = Math.ceil(total / limit); const totalPages = Math.ceil(total / limit);
+1 -8
View File
@@ -11,16 +11,9 @@ import { AuthModule } from '../auth/auth.module';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
@Module({ @Module({
imports: [ imports: [MikroOrmModule.forFeature([Review]), FoodModule, UserModule, AuthModule, JwtModule],
MikroOrmModule.forFeature([Review]),
FoodModule,
UserModule,
AuthModule,
JwtModule,
],
controllers: [ReviewController], controllers: [ReviewController],
providers: [ReviewService, ReviewRepository, FoodRatingCronService], providers: [ReviewService, ReviewRepository, FoodRatingCronService],
exports: [ReviewRepository], exports: [ReviewRepository],
}) })
export class ReviewModule {} export class ReviewModule {}
-1
View File
@@ -7,7 +7,6 @@ export class CreateRoleDto {
@IsString() @IsString()
name!: string; name!: string;
@ApiProperty({ description: 'List of permission IDs', isArray: true, required: false }) @ApiProperty({ description: 'List of permission IDs', isArray: true, required: false })
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@@ -10,7 +10,7 @@ export class Permission extends BaseEntity {
@Property() @Property()
title!: string; title!: string;
// اضافه شد // اضافه شد
@Property({ default: 'general' }) @Property({ default: 'general' })
group!: string; group!: string;
@@ -1,4 +1,4 @@
import { File } from "@nest-lab/fastify-multer"; import type { File } from '@nest-lab/fastify-multer';
export interface FileUploadResult { export interface FileUploadResult {
file: File; file: File;
@@ -19,7 +19,7 @@ export interface FileMetadata {
language?: string; language?: string;
} }
export type FileType = "video" | "audio" | "image" | "document" | "text" | "unknown"; export type FileType = 'video' | 'audio' | 'image' | 'document' | 'text' | 'unknown';
export interface FileValidationOptions { export interface FileValidationOptions {
maxFileSize: number; maxFileSize: number;
@@ -32,7 +32,7 @@ export interface FileUploadOptions {
} }
// File processing status // File processing status
export type FileProcessingStatus = "pending" | "processing" | "completed" | "failed"; export type FileProcessingStatus = 'pending' | 'processing' | 'completed' | 'failed';
export interface FileProcessingResult { export interface FileProcessingResult {
status: FileProcessingStatus; status: FileProcessingStatus;
@@ -7,7 +7,7 @@ export interface S3UploadResult {
export interface S3UploadOptions { export interface S3UploadOptions {
contentType: string; contentType: string;
metadata?: Record<string, string>; metadata?: Record<string, string>;
acl?: "private" | "public-read" | "public-read-write"; acl?: 'private' | 'public-read' | 'public-read-write';
} }
export interface S3DownloadOptions { export interface S3DownloadOptions {
@@ -19,7 +19,14 @@ export class UsersController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Get the current authenticated user profile' }) @ApiOperation({ summary: 'Get the current authenticated user profile' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@Get('public/user/me') @Get('public/user/me')
async getUser(@UserId() userId: string) { async getUser(@UserId() userId: string) {
const user = await this.userService.findById(userId); const user = await this.userService.findById(userId);
@@ -28,7 +35,14 @@ export class UsersController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Update the authenticated user profile' }) @ApiOperation({ summary: 'Update the authenticated user profile' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: UpdateUserDto }) @ApiBody({ type: UpdateUserDto })
@Patch('public/user/update') @Patch('public/user/update')
async updateUser( async updateUser(
@@ -42,7 +56,14 @@ export class UsersController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Get all addresses for the authenticated user' }) @ApiOperation({ summary: 'Get all addresses for the authenticated user' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiOkResponse({ description: 'List of user addresses' }) @ApiOkResponse({ description: 'List of user addresses' })
@Get('public/user/addresses') @Get('public/user/addresses')
async getUserAddresses(@UserId() userId: string) { async getUserAddresses(@UserId() userId: string) {
@@ -53,7 +74,14 @@ export class UsersController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Create a new address for the authenticated user' }) @ApiOperation({ summary: 'Create a new address for the authenticated user' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: CreateUserAddressDto }) @ApiBody({ type: CreateUserAddressDto })
@ApiOkResponse({ description: 'Created user address' }) @ApiOkResponse({ description: 'Created user address' })
@Post('public/user/addresses') @Post('public/user/addresses')
@@ -68,7 +96,14 @@ export class UsersController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Get a single address by ID for the authenticated user' }) @ApiOperation({ summary: 'Get a single address by ID for the authenticated user' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiOkResponse({ description: 'User address details' }) @ApiOkResponse({ description: 'User address details' })
@Get('public/user/addresses/:addressId') @Get('public/user/addresses/:addressId')
async getUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) { async getUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) {
@@ -79,7 +114,14 @@ export class UsersController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Update an address for the authenticated user' }) @ApiOperation({ summary: 'Update an address for the authenticated user' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: UpdateUserAddressDto }) @ApiBody({ type: UpdateUserAddressDto })
@ApiOkResponse({ description: 'Updated user address' }) @ApiOkResponse({ description: 'Updated user address' })
@Patch('public/user/addresses/:addressId') @Patch('public/user/addresses/:addressId')
@@ -95,7 +137,14 @@ export class UsersController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Delete an address for the authenticated user' }) @ApiOperation({ summary: 'Delete an address for the authenticated user' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiOkResponse({ description: 'Address deleted successfully' }) @ApiOkResponse({ description: 'Address deleted successfully' })
@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) {
@@ -106,7 +155,14 @@ export class UsersController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Set an address as default for the authenticated user' }) @ApiOperation({ summary: 'Set an address as default for the authenticated user' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiOkResponse({ description: 'Address set as default' }) @ApiOkResponse({ description: 'Address set as default' })
@Patch('public/user/addresses/:addressId/default') @Patch('public/user/addresses/:addressId/default')
async setDefaultAddress(@UserId() userId: string, @Param('addressId') addressId: string) { async setDefaultAddress(@UserId() userId: string, @Param('addressId') addressId: string) {
@@ -130,7 +186,14 @@ export class UsersController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Convert user score (points) to wallet balance' }) @ApiOperation({ summary: 'Convert user score (points) to wallet balance' })
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier', example: 'zhivan' }) @ApiHeader({
name: 'X-Slug',
required: true,
schema: {
type: 'string',
default: 'zhivan',
},
})
@ApiBody({ type: ConvertScoreToWalletDto }) @ApiBody({ type: ConvertScoreToWalletDto })
@Post('public/user/convert-score-to-wallet') @Post('public/user/convert-score-to-wallet')
async convertScoreToWallet(@UserId() userId: string, @Body() dto: ConvertScoreToWalletDto, @RestId() restId: string) { async convertScoreToWallet(@UserId() userId: string, @Body() dto: ConvertScoreToWalletDto, @RestId() restId: string) {
@@ -33,7 +33,7 @@ export class CreateUserAddressDto {
@Max(90) @Max(90)
latitude!: number; latitude!: number;
@ApiProperty({ description: 'Longitude coordinate', example: 51.3890, minimum: -180, maximum: 180 }) @ApiProperty({ description: 'Longitude coordinate', example: 51.389, minimum: -180, maximum: 180 })
@IsNumber() @IsNumber()
@Min(-180) @Min(-180)
@Max(180) @Max(180)
@@ -49,4 +49,3 @@ export class CreateUserAddressDto {
@IsBoolean() @IsBoolean()
isDefault?: boolean; isDefault?: boolean;
} }
@@ -37,7 +37,7 @@ export class UpdateUserAddressDto {
@Max(90) @Max(90)
latitude?: number; latitude?: number;
@ApiPropertyOptional({ description: 'Longitude coordinate', example: 51.3890, minimum: -180, maximum: 180 }) @ApiPropertyOptional({ description: 'Longitude coordinate', example: 51.389, minimum: -180, maximum: 180 })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(-180) @Min(-180)
@@ -54,4 +54,3 @@ export class UpdateUserAddressDto {
@IsBoolean() @IsBoolean()
isDefault?: boolean; isDefault?: boolean;
} }
+1 -4
View File
@@ -43,9 +43,6 @@ export class CacheService implements OnModuleInit {
} }
} }
// // src/modules/cache/cache.service.ts // // src/modules/cache/cache.service.ts
// import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager'; // import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
// import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; // import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
@@ -147,4 +144,4 @@ export class CacheService implements OnModuleInit {
// return undefined; // return undefined;
// } // }
// } // }
// } // }
-1
View File
@@ -28,4 +28,3 @@ export function normalizePhone(phone: string): string {
return normalized; return normalized;
} }
+2 -2
View File
@@ -1,8 +1,8 @@
import type { EntityManager } from '@mikro-orm/core'; import type { EntityManager } from '@mikro-orm/core';
import { Admin } from '../modules/admin/entities/admin.entity'; import { Admin } from '../modules/admin/entities/admin.entity';
import { AdminRole } from '../modules/admin/entities/adminRole.entity'; import { AdminRole } from '../modules/admin/entities/adminRole.entity';
import { Role } from '../modules/roles/entities/role.entity'; import type { Role } from '../modules/roles/entities/role.entity';
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
import { adminsData } from './data/admins.data'; import { adminsData } from './data/admins.data';
import { normalizePhone } from '../modules/utils/phone.util'; import { normalizePhone } from '../modules/utils/phone.util';
+2 -6
View File
@@ -1,13 +1,10 @@
import type { EntityManager } from '@mikro-orm/core'; import type { EntityManager } from '@mikro-orm/core';
import { Category } from '../modules/foods/entities/category.entity'; import { Category } from '../modules/foods/entities/category.entity';
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
import { categoriesData } from './data/categories.data'; import { categoriesData } from './data/categories.data';
export class CategoriesSeeder { export class CategoriesSeeder {
async run( async run(em: EntityManager, restaurantsMap: Map<string, Restaurant>): Promise<Map<string, Category>> {
em: EntityManager,
restaurantsMap: Map<string, Restaurant>,
): Promise<Map<string, Category>> {
const categoriesMap = new Map<string, Category>(); const categoriesMap = new Map<string, Category>();
for (const catData of categoriesData) { for (const catData of categoriesData) {
@@ -36,4 +33,3 @@ export class CategoriesSeeder {
return categoriesMap; return categoriesMap;
} }
} }
+1 -2
View File
@@ -1,6 +1,6 @@
import type { EntityManager } from '@mikro-orm/core'; import type { EntityManager } from '@mikro-orm/core';
import { Coupon } from '../modules/coupons/entities/coupon.entity'; import { Coupon } from '../modules/coupons/entities/coupon.entity';
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
import { couponsData } from './data/coupons.data'; import { couponsData } from './data/coupons.data';
export class CouponsSeeder { export class CouponsSeeder {
@@ -40,4 +40,3 @@ export class CouponsSeeder {
await em.flush(); await em.flush();
} }
} }
-1
View File
@@ -29,4 +29,3 @@ export const adminsData: AdminData[] = [
restaurantSlug: null, restaurantSlug: null,
}, },
]; ];
-1
View File
@@ -13,4 +13,3 @@ export const categoriesData: CategoryData[] = [
{ title: 'دسر', restaurantSlug: 'boote' }, { title: 'دسر', restaurantSlug: 'boote' },
{ title: 'نوشیدنی', restaurantSlug: 'boote' }, { title: 'نوشیدنی', restaurantSlug: 'boote' },
]; ];
-1
View File
@@ -92,4 +92,3 @@ export const couponsData: CouponData[] = [
isActive: true, isActive: true,
}, },
]; ];
@@ -48,4 +48,3 @@ export const deliveryMethodsData: DeliveryMethodData[] = [
order: 4, order: 4,
}, },
]; ];
@@ -1,5 +1,5 @@
import { NotifChannelEnum, NotifTitleEnum } from '../../modules/notifications/interfaces/notification.interface'; import { NotifChannelEnum, NotifTitleEnum } from '../../modules/notifications/interfaces/notification.interface';
export interface NotificationPreferenceData { export interface NotificationPreferenceData {
title: NotifTitleEnum; title: NotifTitleEnum;
channels: NotifChannelEnum[]; channels: NotifChannelEnum[];
-1
View File
@@ -13,4 +13,3 @@ export const getPermissionsData = () => {
group: specialPermissions.includes(name) ? 'special' : 'general', group: specialPermissions.includes(name) ? 'special' : 'general',
})); }));
}; };
+1 -4
View File
@@ -16,9 +16,7 @@ export const rolesData: RoleConfig[] = [
name: 'accountant', name: 'accountant',
isSystem: false, isSystem: false,
permissionFilter: (name: Permission) => permissionFilter: (name: Permission) =>
name === Permission.VIEW_REPORTS || name === Permission.VIEW_REPORTS || name === Permission.MANAGE_FINANCES || name === Permission.MANAGE_ORDERS,
name === Permission.MANAGE_FINANCES ||
name === Permission.MANAGE_ORDERS,
}, },
{ {
name: 'superAdmin', name: 'superAdmin',
@@ -29,4 +27,3 @@ export const rolesData: RoleConfig[] = [
name === Permission.UPDATE_RESTAURANT, name === Permission.UPDATE_RESTAURANT,
}, },
]; ];
-1
View File
@@ -8,4 +8,3 @@ export const timeSlots: TimeSlot[] = [
{ openTime: '12:00', closeTime: '15:00' }, // Lunch { openTime: '12:00', closeTime: '15:00' }, // Lunch
{ openTime: '18:00', closeTime: '23:00' }, // Dinner { openTime: '18:00', closeTime: '23:00' }, // Dinner
]; ];
+7 -8
View File
@@ -20,7 +20,7 @@ export const userAddressesData: UserAddressData[] = [
province: 'تهران', province: 'تهران',
postalCode: '1234567890', postalCode: '1234567890',
latitude: 35.6892, latitude: 35.6892,
longitude: 51.3890, longitude: 51.389,
phone: '09362532122', phone: '09362532122',
isDefault: true, isDefault: true,
}, },
@@ -31,8 +31,8 @@ export const userAddressesData: UserAddressData[] = [
city: 'تهران', city: 'تهران',
province: 'تهران', province: 'تهران',
postalCode: '1234567891', postalCode: '1234567891',
latitude: 35.6900, latitude: 35.69,
longitude: 51.3900, longitude: 51.39,
phone: '09185290775', phone: '09185290775',
isDefault: true, isDefault: true,
}, },
@@ -43,8 +43,8 @@ export const userAddressesData: UserAddressData[] = [
city: 'تهران', city: 'تهران',
province: 'تهران', province: 'تهران',
postalCode: '1234567892', postalCode: '1234567892',
latitude: 35.6910, latitude: 35.691,
longitude: 51.3910, longitude: 51.391,
phone: '09129283395', phone: '09129283395',
isDefault: true, isDefault: true,
}, },
@@ -55,10 +55,9 @@ export const userAddressesData: UserAddressData[] = [
city: 'تهران', city: 'تهران',
province: 'تهران', province: 'تهران',
postalCode: '1234567893', postalCode: '1234567893',
latitude: 35.6920, latitude: 35.692,
longitude: 51.3920, longitude: 51.392,
phone: '09184317567', phone: '09184317567',
isDefault: true, isDefault: true,
}, },
]; ];
+1 -1
View File
@@ -1,6 +1,6 @@
import type { EntityManager } from '@mikro-orm/core'; import type { EntityManager } from '@mikro-orm/core';
import { Delivery } from '../modules/delivery/entities/delivery.entity'; import { Delivery } from '../modules/delivery/entities/delivery.entity';
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
import { deliveryMethodsData } from './data/delivery-methods.data'; import { deliveryMethodsData } from './data/delivery-methods.data';
import { DeliveryFeeTypeEnum } from 'src/modules/delivery/interface/delivery'; import { DeliveryFeeTypeEnum } from 'src/modules/delivery/interface/delivery';
+2 -2
View File
@@ -1,7 +1,7 @@
import type { EntityManager } from '@mikro-orm/core'; import type { EntityManager } from '@mikro-orm/core';
import { Food } from '../modules/foods/entities/food.entity'; import { Food } from '../modules/foods/entities/food.entity';
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
import { Category } from '../modules/foods/entities/category.entity'; import type { Category } from '../modules/foods/entities/category.entity';
import { foodsData } from './data/foods.data'; import { foodsData } from './data/foods.data';
export class FoodsSeeder { export class FoodsSeeder {
+1 -2
View File
@@ -1,6 +1,6 @@
import type { EntityManager } from '@mikro-orm/core'; import type { EntityManager } from '@mikro-orm/core';
import { PaymentMethod } from '../modules/payments/entities/payment-method.entity'; import { PaymentMethod } from '../modules/payments/entities/payment-method.entity';
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
import { paymentMethodsData } from './data/payment-methods.data'; import { paymentMethodsData } from './data/payment-methods.data';
export class PaymentMethodsSeeder { export class PaymentMethodsSeeder {
@@ -53,4 +53,3 @@ export class PaymentMethodsSeeder {
await em.flush(); await em.flush();
} }
} }
-1
View File
@@ -26,4 +26,3 @@ export class PermissionsSeeder {
return createdPermissions; return createdPermissions;
} }
} }
-1
View File
@@ -19,4 +19,3 @@ export class RestaurantsSeeder {
return restaurantsMap; return restaurantsMap;
} }
} }
+3 -6
View File
@@ -1,7 +1,7 @@
import type { EntityManager } from '@mikro-orm/core'; import type { EntityManager } from '@mikro-orm/core';
import { Role } from '../modules/roles/entities/role.entity'; import { Role } from '../modules/roles/entities/role.entity';
import { Permission } from '../common/enums/permission.enum'; import type { Permission } from '../common/enums/permission.enum';
import { Permission as PermissionEntity } from '../modules/roles/entities/permission.entity'; import type { Permission as PermissionEntity } from '../modules/roles/entities/permission.entity';
import { rolesData } from './data/roles.data'; import { rolesData } from './data/roles.data';
export class RolesSeeder { export class RolesSeeder {
@@ -25,9 +25,7 @@ export class RolesSeeder {
generalPermissions.forEach(p => newRole.permissions.add(p)); generalPermissions.forEach(p => newRole.permissions.add(p));
} else { } else {
// For other roles, use the permission filter // For other roles, use the permission filter
const filteredPermissions = permissions.filter(p => const filteredPermissions = permissions.filter(p => roleConfig.permissionFilter(p.name as Permission));
roleConfig.permissionFilter(p.name as Permission),
);
filteredPermissions.forEach(p => newRole.permissions.add(p)); filteredPermissions.forEach(p => newRole.permissions.add(p));
} }
@@ -41,4 +39,3 @@ export class RolesSeeder {
return rolesMap; return rolesMap;
} }
} }
+1 -2
View File
@@ -1,6 +1,6 @@
import type { EntityManager } from '@mikro-orm/core'; import type { EntityManager } from '@mikro-orm/core';
import { Schedule } from '../modules/restaurants/entities/schedule.entity'; import { Schedule } from '../modules/restaurants/entities/schedule.entity';
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; import type { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
import { timeSlots } from './data/schedules.data'; import { timeSlots } from './data/schedules.data';
export class SchedulesSeeder { export class SchedulesSeeder {
@@ -36,4 +36,3 @@ export class SchedulesSeeder {
await em.flush(); await em.flush();
} }
} }
+4 -3
View File
@@ -1,7 +1,8 @@
import { Test, TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing';
import type { INestApplication } from '@nestjs/common';
import request from 'supertest'; import request from 'supertest';
import { App } from 'supertest/types'; import type { App } from 'supertest/types';
import { AppModule } from './../src/app.module'; import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => { describe('AppController (e2e)', () => {