This commit is contained in:
@@ -4666,6 +4666,228 @@
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "char(26)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 26,
|
||||
"mappedType": "character"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"is_buyer": {
|
||||
"name": "is_buyer",
|
||||
"type": "boolean",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "false",
|
||||
"mappedType": "boolean"
|
||||
},
|
||||
"food_id": {
|
||||
"name": "food_id",
|
||||
"type": "char(26)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 26,
|
||||
"mappedType": "character"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "char(26)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 26,
|
||||
"mappedType": "character"
|
||||
},
|
||||
"comment": {
|
||||
"name": "comment",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"rating": {
|
||||
"name": "rating",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "0",
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"positive_points": {
|
||||
"name": "positive_points",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"negative_points": {
|
||||
"name": "negative_points",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "'pending'",
|
||||
"enumItems": [
|
||||
"pending",
|
||||
"rejected",
|
||||
"approved"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
}
|
||||
},
|
||||
"name": "reviews",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "reviews_created_at_index",
|
||||
"columnNames": [
|
||||
"created_at"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_deleted_at_index",
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_user_id_index",
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_food_id_status_index",
|
||||
"columnNames": [
|
||||
"food_id",
|
||||
"status"
|
||||
],
|
||||
"composite": true,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_food_id_user_id_unique",
|
||||
"columnNames": [
|
||||
"food_id",
|
||||
"user_id"
|
||||
],
|
||||
"composite": true,
|
||||
"constraint": true,
|
||||
"primary": false,
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"reviews_food_id_foreign": {
|
||||
"constraintName": "reviews_food_id_foreign",
|
||||
"columnNames": [
|
||||
"food_id"
|
||||
],
|
||||
"localTableName": "public.reviews",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.foods",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"reviews_user_id_foreign": {
|
||||
"constraintName": "reviews_user_id_foreign",
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"localTableName": "public.reviews",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.users",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
@@ -5501,251 +5723,6 @@
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "char(26)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 26,
|
||||
"mappedType": "character"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"order_id": {
|
||||
"name": "order_id",
|
||||
"type": "char(26)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 26,
|
||||
"mappedType": "character"
|
||||
},
|
||||
"food_id": {
|
||||
"name": "food_id",
|
||||
"type": "char(26)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 26,
|
||||
"mappedType": "character"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "char(26)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 26,
|
||||
"mappedType": "character"
|
||||
},
|
||||
"comment": {
|
||||
"name": "comment",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"rating": {
|
||||
"name": "rating",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "0",
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"positive_points": {
|
||||
"name": "positive_points",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"negative_points": {
|
||||
"name": "negative_points",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "'pending'",
|
||||
"enumItems": [
|
||||
"pending",
|
||||
"rejected",
|
||||
"approved"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
}
|
||||
},
|
||||
"name": "reviews",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "reviews_created_at_index",
|
||||
"columnNames": [
|
||||
"created_at"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_deleted_at_index",
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_order_id_index",
|
||||
"columnNames": [
|
||||
"order_id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_user_id_index",
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_food_id_status_index",
|
||||
"columnNames": [
|
||||
"food_id",
|
||||
"status"
|
||||
],
|
||||
"composite": true,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_order_id_food_id_user_id_unique",
|
||||
"columnNames": [
|
||||
"order_id",
|
||||
"food_id",
|
||||
"user_id"
|
||||
],
|
||||
"composite": true,
|
||||
"constraint": true,
|
||||
"primary": false,
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"keyName": "reviews_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"reviews_order_id_foreign": {
|
||||
"constraintName": "reviews_order_id_foreign",
|
||||
"columnNames": [
|
||||
"order_id"
|
||||
],
|
||||
"localTableName": "public.reviews",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.orders",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"reviews_food_id_foreign": {
|
||||
"constraintName": "reviews_food_id_foreign",
|
||||
"columnNames": [
|
||||
"food_id"
|
||||
],
|
||||
"localTableName": "public.reviews",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.foods",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"reviews_user_id_foreign": {
|
||||
"constraintName": "reviews_user_id_foreign",
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"localTableName": "public.reviews",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.users",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20260715083002_task extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table "reviews" drop constraint "reviews_order_id_foreign";`);
|
||||
|
||||
this.addSql(`drop index "reviews_order_id_index";`);
|
||||
this.addSql(`alter table "reviews" drop constraint "reviews_order_id_food_id_user_id_unique";`);
|
||||
|
||||
this.addSql(`alter table "reviews" add column "is_buyer" boolean not null default false;`);
|
||||
this.addSql(`alter table "reviews" add constraint "reviews_food_id_user_id_unique" unique ("food_id", "user_id");`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table "reviews" drop constraint "reviews_food_id_user_id_unique";`);
|
||||
|
||||
this.addSql(`alter table "reviews" add column "order_id" char(26) not null;`);
|
||||
this.addSql(`alter table "reviews" add constraint "reviews_order_id_foreign" foreign key ("order_id") references "orders" ("id") on update cascade;`);
|
||||
this.addSql(`create index "reviews_order_id_index" on "reviews" ("order_id");`);
|
||||
this.addSql(`alter table "reviews" add constraint "reviews_order_id_food_id_user_id_unique" unique ("order_id", "food_id", "user_id");`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,11 +9,6 @@ export class CreateReviewDto {
|
||||
@ApiProperty({ description: 'Food ID' })
|
||||
foodId: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ description: 'Order ID' })
|
||||
orderId: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
|
||||
@@ -7,13 +7,12 @@ import { PositivePoint, NegativePoint } from '../enums/review-point.enum';
|
||||
import { ReviewStatus } from '../enums/review-status.enum';
|
||||
|
||||
@Entity({ tableName: 'reviews' })
|
||||
@Unique({ properties: ['order', 'food', 'user'] })
|
||||
@Unique({ properties: ['food', 'user'] })
|
||||
@Index({ properties: ['food', 'status'] })
|
||||
@Index({ properties: ['user'] })
|
||||
@Index({ properties: ['order'] })
|
||||
export class Review extends BaseEntity {
|
||||
@ManyToOne(() => Order)
|
||||
order: Order;
|
||||
@Property({ type: 'boolean' })
|
||||
isBuyer: boolean = false
|
||||
|
||||
@ManyToOne(() => Food)
|
||||
food: Food;
|
||||
|
||||
@@ -5,8 +5,6 @@ export class ReviewCreatedEvent {
|
||||
public readonly userId: string,
|
||||
public readonly foodId: string,
|
||||
public readonly foodName: string,
|
||||
public readonly orderId: string,
|
||||
public readonly orderNumber: string | number,
|
||||
public readonly rating: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -46,13 +46,12 @@ export class ReviewListeners {
|
||||
restaurantId: event.restaurantId,
|
||||
message: {
|
||||
title: NotifTitleEnum.REVIEW_CREATED,
|
||||
content: `نظر جدید برای غذا "${event.foodName}" با امتیاز ${event.rating} از 5 برای سفارش شماره ${event.orderNumber} ثبت شد`,
|
||||
content: `نظر جدید برای غذا "${event.foodName}" با امتیاز ${event.rating} از 5 برای سفارش شماره ثبت شد`,
|
||||
sms: {
|
||||
templateId: this.reviewCreatedSmsTemplateId,
|
||||
parameters: {
|
||||
foodName: event.foodName,
|
||||
rating: event.rating.toString(),
|
||||
orderNumber: String(event.orderNumber),
|
||||
},
|
||||
},
|
||||
pushNotif: {
|
||||
|
||||
@@ -10,12 +10,12 @@ import { RequiredEntityData } from '@mikro-orm/core';
|
||||
import { Review } from '../entities/review.entity';
|
||||
import { FoodMessage, UserMessage, ReviewMessage } from 'src/common/enums/message.enum';
|
||||
import { Order } from '../../orders/entities/order.entity';
|
||||
import { OrderItem } from '../../orders/entities/order-item.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { ReviewStatus } from '../enums/review-status.enum';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { ReviewCreatedEvent } from '../events/review.events';
|
||||
import { sanitizeText } from '../../utils/sanitize.util';
|
||||
import { OrderStatus } from 'src/modules/orders/interface/order.interface';
|
||||
|
||||
@Injectable()
|
||||
export class ReviewService {
|
||||
@@ -28,9 +28,9 @@ export class ReviewService {
|
||||
) { }
|
||||
|
||||
async create(userId: string, createReviewDto: CreateReviewDto): Promise<Review> {
|
||||
const { foodId, orderId, rating, comment, positivePoints, negativePoints } = createReviewDto;
|
||||
const { foodId, rating, comment, positivePoints, negativePoints } = createReviewDto;
|
||||
|
||||
const food = await this.foodRepository.findOne({ id: foodId });
|
||||
const food = await this.foodRepository.findOne({ id: foodId }, { populate: ['restaurant'] });
|
||||
if (!food) {
|
||||
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
}
|
||||
@@ -40,34 +40,14 @@ export class ReviewService {
|
||||
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Find order and verify it belongs to the user, populate restaurant to get restaurantId
|
||||
const order = await this.em.findOne(Order, { id: orderId, user: { id: userId } }, { populate: ['restaurant'] });
|
||||
if (!order) {
|
||||
throw new NotFoundException('Order not found or does not belong to the current user');
|
||||
}
|
||||
|
||||
// Verify the food is in the order
|
||||
const orderItem = await this.em.findOne(OrderItem, {
|
||||
order: { id: orderId },
|
||||
food: { id: foodId },
|
||||
});
|
||||
if (!orderItem) {
|
||||
throw new BadRequestException('Food is not in the specified order');
|
||||
}
|
||||
|
||||
// Check if user already commented on this food for this order
|
||||
const existingReview = await this.reviewRepository.findOne({
|
||||
order: { id: orderId },
|
||||
food: { id: foodId },
|
||||
user: { id: userId },
|
||||
});
|
||||
|
||||
if (existingReview) {
|
||||
throw new BadRequestException(ReviewMessage.ALREADY_COMMENTED);
|
||||
}
|
||||
const isBuyer = await this.em.count(Order, {
|
||||
status: OrderStatus.COMPLETED,
|
||||
items: { food: { id: foodId } },
|
||||
user: { id: userId }
|
||||
}) > 0;
|
||||
|
||||
const data: RequiredEntityData<Review> = {
|
||||
order,
|
||||
isBuyer: true,
|
||||
food,
|
||||
user,
|
||||
rating,
|
||||
@@ -92,12 +72,10 @@ export class ReviewService {
|
||||
ReviewCreatedEvent.name,
|
||||
new ReviewCreatedEvent(
|
||||
review.id,
|
||||
order.restaurant.id,
|
||||
food.restaurant.id,
|
||||
userId,
|
||||
foodId,
|
||||
food.title || 'Unknown Food',
|
||||
orderId,
|
||||
order.orderNumber || '',
|
||||
food.title || '',
|
||||
rating,
|
||||
),
|
||||
);
|
||||
@@ -120,8 +98,8 @@ export class ReviewService {
|
||||
|
||||
async findById(id: string, restaurantId: string): Promise<Review> {
|
||||
const review = await this.reviewRepository.findOne(
|
||||
{ id, order: { restaurant: { id: restaurantId } } },
|
||||
{ populate: ['food', 'user', 'order'] },
|
||||
{ id, food: { restaurant: { id: restaurantId } } },
|
||||
{ populate: ['food', 'user'] },
|
||||
);
|
||||
if (!review) {
|
||||
throw new NotFoundException(ReviewMessage.NOT_FOUND);
|
||||
@@ -130,7 +108,7 @@ export class ReviewService {
|
||||
}
|
||||
|
||||
async update(id: string, userId: string, dto: UpdateReviewDto, isAdmin: boolean = false): Promise<Review> {
|
||||
const review = await this.reviewRepository.findOne({ id }, { populate: ['food', 'user', 'order'] });
|
||||
const review = await this.reviewRepository.findOne({ id }, { populate: ['food', 'user',] });
|
||||
if (!review) {
|
||||
throw new NotFoundException(ReviewMessage.NOT_FOUND);
|
||||
}
|
||||
@@ -165,7 +143,7 @@ export class ReviewService {
|
||||
}
|
||||
|
||||
async changeStatus(reviewId: string, status: ReviewStatus, restaurantId: string): Promise<Review> {
|
||||
const review = await this.reviewRepository.findOne({ id: reviewId, order: { restaurant: { id: restaurantId } } });
|
||||
const review = await this.reviewRepository.findOne({ id: reviewId, food: { restaurant: { id: restaurantId } } });
|
||||
if (!review) {
|
||||
throw new NotFoundException(ReviewMessage.NOT_FOUND);
|
||||
}
|
||||
@@ -175,7 +153,7 @@ export class ReviewService {
|
||||
}
|
||||
|
||||
async remove(id: string, userId: string, isAdmin: boolean = false) {
|
||||
const review = await this.reviewRepository.findOne({ id }, { populate: ['food', 'user', 'order'] });
|
||||
const review = await this.reviewRepository.findOne({ id }, { populate: ['food', 'user'] });
|
||||
if (!review) {
|
||||
throw new NotFoundException(ReviewMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user