add restuarant to contact entity

This commit is contained in:
2026-01-06 23:08:58 +03:30
parent 69dedaa981
commit 9fe2321c9b
7 changed files with 239 additions and 158 deletions
+161 -137
View File
@@ -127,143 +127,6 @@
"foreignKeys": {}, "foreignKeys": {},
"nativeEnums": {} "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"
},
"subject": {
"name": "subject",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 255,
"mappedType": "string"
},
"content": {
"name": "content",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 255,
"mappedType": "string"
},
"phone": {
"name": "phone",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 255,
"mappedType": "string"
},
"scope": {
"name": "scope",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"enumItems": [
"application",
"restaurant"
],
"mappedType": "enum"
},
"status": {
"name": "status",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 255,
"default": "'new'",
"mappedType": "string"
}
},
"name": "contacts",
"schema": "public",
"indexes": [
{
"keyName": "contacts_created_at_index",
"columnNames": [
"created_at"
],
"composite": false,
"constraint": false,
"primary": false,
"unique": false
},
{
"keyName": "contacts_deleted_at_index",
"columnNames": [
"deleted_at"
],
"composite": false,
"constraint": false,
"primary": false,
"unique": false
},
{
"keyName": "contacts_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {},
"nativeEnums": {}
},
{ {
"columns": { "columns": {
"id": { "id": {
@@ -2003,6 +1866,167 @@
}, },
"nativeEnums": {} "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"
},
"subject": {
"name": "subject",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 255,
"mappedType": "string"
},
"content": {
"name": "content",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 255,
"mappedType": "string"
},
"phone": {
"name": "phone",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 255,
"mappedType": "string"
},
"scope": {
"name": "scope",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"enumItems": [
"application",
"restaurant"
],
"mappedType": "enum"
},
"restaurant_id": {
"name": "restaurant_id",
"type": "char(26)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 26,
"mappedType": "character"
},
"status": {
"name": "status",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 255,
"default": "'new'",
"mappedType": "string"
}
},
"name": "contacts",
"schema": "public",
"indexes": [
{
"keyName": "contacts_created_at_index",
"columnNames": [
"created_at"
],
"composite": false,
"constraint": false,
"primary": false,
"unique": false
},
{
"keyName": "contacts_deleted_at_index",
"columnNames": [
"deleted_at"
],
"composite": false,
"constraint": false,
"primary": false,
"unique": false
},
{
"keyName": "contacts_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"contacts_restaurant_id_foreign": {
"constraintName": "contacts_restaurant_id_foreign",
"columnNames": [
"restaurant_id"
],
"localTableName": "public.contacts",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.restaurants",
"deleteRule": "set null",
"updateRule": "cascade"
}
},
"nativeEnums": {}
},
{ {
"columns": { "columns": {
"id": { "id": {
@@ -0,0 +1,14 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260106192946 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table "contacts" add column "restaurant_id" char(26) null;`);
this.addSql(`alter table "contacts" add constraint "contacts_restaurant_id_foreign" foreign key ("restaurant_id") references "restaurants" ("id") on update cascade on delete set null;`);
}
override async down(): Promise<void> {
this.addSql(`alter table "contacts" drop constraint "contacts_restaurant_id_foreign";`);
}
}
@@ -19,6 +19,7 @@ import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guar
import { API_HEADER_SLUG } from 'src/common/constants'; import { API_HEADER_SLUG } from 'src/common/constants';
import { Permission } from 'src/common/enums/permission.enum'; import { Permission } from 'src/common/enums/permission.enum';
import { Permissions } from 'src/common/decorators/permissions.decorator'; import { Permissions } from 'src/common/decorators/permissions.decorator';
import { RestId } from 'src/common/decorators/rest-id.decorator';
@ApiTags('contact') @ApiTags('contact')
@Controller() @Controller()
@@ -44,8 +45,8 @@ export class ContactController {
@ApiQuery({ name: 'orderBy', required: false, type: String }) @ApiQuery({ name: 'orderBy', required: false, type: String })
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] }) @ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
@ApiQuery({ name: 'status', required: false, enum: ['new', 'seen'] }) @ApiQuery({ name: 'status', required: false, enum: ['new', 'seen'] })
async findAllPaginated(@Query() dto: FindContactsDto) { async findAllPaginated(@RestId() restId: string, @Query() dto: FindContactsDto) {
return this.contactService.findAllPaginatedAdmin(dto); return this.contactService.findAllPaginatedAdmin(dto, restId);
} }
@UseGuards(AdminAuthGuard) @UseGuards(AdminAuthGuard)
@@ -54,8 +55,8 @@ export class ContactController {
@Get('admin/contact/:id') @Get('admin/contact/:id')
@ApiOperation({ summary: 'Get contact details by ID (admin only)' }) @ApiOperation({ summary: 'Get contact details by ID (admin only)' })
@ApiParam({ name: 'id', description: 'Contact ID' }) @ApiParam({ name: 'id', description: 'Contact ID' })
async findOne(@Param('id') id: string) { async findOne(@RestId() restId: string, @Param('id') id: string) {
return this.contactService.findOne(id); return this.contactService.findOne(id, restId);
} }
@UseGuards(AdminAuthGuard) @UseGuards(AdminAuthGuard)
@@ -64,8 +65,8 @@ export class ContactController {
@Patch('admin/contact/:id/status') @Patch('admin/contact/:id/status')
@ApiOperation({ summary: 'Update contact status (admin only)' }) @ApiOperation({ summary: 'Update contact status (admin only)' })
@ApiParam({ name: 'id', description: 'Contact ID' }) @ApiParam({ name: 'id', description: 'Contact ID' })
async updateStatus(@Param('id') id: string, @Body() dto: UpdateContactStatusDto) { async updateStatus(@RestId() restId: string, @Param('id') id: string, @Body() dto: UpdateContactStatusDto) {
return this.contactService.updateStatus(id, dto); return this.contactService.updateStatus(id, dto, restId);
} }
@UseGuards(AdminAuthGuard) @UseGuards(AdminAuthGuard)
@@ -74,8 +75,8 @@ export class ContactController {
@Delete('admin/contact/:id') @Delete('admin/contact/:id')
@ApiOperation({ summary: 'Hard delete a contact (admin only)' }) @ApiOperation({ summary: 'Hard delete a contact (admin only)' })
@ApiParam({ name: 'id', description: 'Contact ID' }) @ApiParam({ name: 'id', description: 'Contact ID' })
async remove(@Param('id') id: string) { async remove(@RestId() restId: string, @Param('id') id: string) {
await this.contactService.remove(id); await this.contactService.remove(id, restId);
} }
/*** Super Admin ***/ /*** Super Admin ***/
@@ -1,4 +1,4 @@
import { IsNotEmpty, IsString, IsOptional, IsMobilePhone, IsEnum } from 'class-validator'; import { IsNotEmpty, IsString, IsOptional, IsMobilePhone, IsEnum, IsUUID, ValidateIf } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ContactScope } from '../interface/interface'; import { ContactScope } from '../interface/interface';
@@ -18,6 +18,12 @@ export class CreateContactDto {
@ApiProperty({ description: 'Contact Scope' }) @ApiProperty({ description: 'Contact Scope' })
scope!: ContactScope; scope!: ContactScope;
@ValidateIf((o) => o.scope === ContactScope.RESTAURANT)
@IsNotEmpty()
@IsUUID()
@ApiPropertyOptional({ example: '550e8400-e29b-41d4-a716-446655440000', description: 'Restaurant ID (required when scope is restaurant)' })
restaurantId?: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
@IsMobilePhone('fa-IR') @IsMobilePhone('fa-IR')
@@ -1,6 +1,7 @@
import { Entity, Enum, Property } from '@mikro-orm/core'; import { Entity, Enum, ManyToOne, Property } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity'; import { BaseEntity } from '../../../common/entities/base.entity';
import { ContactScope, ContactStatusEnum } from '../interface/interface'; import { ContactScope, ContactStatusEnum } from '../interface/interface';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
@Entity({ tableName: 'contacts' }) @Entity({ tableName: 'contacts' })
export class Contact extends BaseEntity { export class Contact extends BaseEntity {
@@ -16,6 +17,9 @@ export class Contact extends BaseEntity {
@Enum(() => ContactScope) @Enum(() => ContactScope)
scope: ContactScope scope: ContactScope
@ManyToOne(() => Restaurant, { nullable: true })
restaurant?: Restaurant | null = null;
@Property({ default: ContactStatusEnum.New }) @Property({ default: ContactStatusEnum.New })
status: ContactStatusEnum = ContactStatusEnum.New; status: ContactStatusEnum = ContactStatusEnum.New;
} }
@@ -8,6 +8,7 @@ import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { ContactStatusEnum } from '../interface/interface'; import { ContactStatusEnum } from '../interface/interface';
import { EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
import { ContactScope } from '../interface/interface'; import { ContactScope } from '../interface/interface';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
@Injectable() @Injectable()
export class ContactService { export class ContactService {
@@ -17,15 +18,26 @@ export class ContactService {
) { } ) { }
async create(createContactDto: CreateContactDto): Promise<Contact> { async create(createContactDto: CreateContactDto): Promise<Contact> {
const { restaurantId, ...contactData } = createContactDto;
let restaurant: Restaurant | null = null;
if (restaurantId && createContactDto.scope === ContactScope.RESTAURANT) {
restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
if (!restaurant) {
throw new NotFoundException(`Restaurant with ID ${restaurantId} not found`);
}
}
const contact = this.contactRepository.create({ const contact = this.contactRepository.create({
...createContactDto, ...contactData,
status: ContactStatusEnum.New, status: ContactStatusEnum.New,
restaurant,
}); });
await this.em.persistAndFlush(contact); await this.em.persistAndFlush(contact);
return contact; return contact;
} }
async findAllPaginatedAdmin(dto: FindContactsDto): Promise<PaginatedResult<Contact>> { async findAllPaginatedAdmin(dto: FindContactsDto, restaurantId?: string): Promise<PaginatedResult<Contact>> {
return this.contactRepository.findAllPaginated({ return this.contactRepository.findAllPaginated({
page: dto.page, page: dto.page,
limit: dto.limit, limit: dto.limit,
@@ -33,7 +45,8 @@ export class ContactService {
orderBy: dto.orderBy, orderBy: dto.orderBy,
order: dto.order, order: dto.order,
status: dto.status, status: dto.status,
scope: ContactScope.RESTAURANT scope: ContactScope.RESTAURANT,
restaurantId
}); });
} }
async findAllPaginatedSuper(dto: FindContactsDto): Promise<PaginatedResult<Contact>> { async findAllPaginatedSuper(dto: FindContactsDto): Promise<PaginatedResult<Contact>> {
@@ -48,16 +61,26 @@ export class ContactService {
}); });
} }
async findOne(id: string): Promise<Contact> { async findOne(id: string, restaurantId?: string): Promise<Contact> {
const contact = await this.contactRepository.findOne({ id }); const where: any = { id };
if (restaurantId) {
where.restaurant = restaurantId;
}
const contact = await this.contactRepository.findOne(where);
if (!contact) { if (!contact) {
throw new NotFoundException(`Contact with ID ${id} not found`); throw new NotFoundException(`Contact with ID ${id} not found`);
} }
return contact; return contact;
} }
async updateStatus(id: string, dto: UpdateContactStatusDto): Promise<Contact> { async updateStatus(id: string, dto: UpdateContactStatusDto, restaurantId?: string): Promise<Contact> {
const contact = await this.contactRepository.findOne({ id }); const where: any = { id };
if (restaurantId) {
where.restaurant = restaurantId;
}
const contact = await this.contactRepository.findOne(where);
if (!contact) { if (!contact) {
throw new NotFoundException(`Contact with ID ${id} not found`); throw new NotFoundException(`Contact with ID ${id} not found`);
} }
@@ -66,8 +89,13 @@ export class ContactService {
return contact; return contact;
} }
async remove(id: string): Promise<void> { async remove(id: string, restaurantId?: string): Promise<void> {
const contact = await this.contactRepository.findOne({ id }); const where: any = { id };
if (restaurantId) {
where.restaurant = restaurantId;
}
const contact = await this.contactRepository.findOne(where);
if (!contact) { if (!contact) {
throw new NotFoundException(`Contact with ID ${id} not found`); throw new NotFoundException(`Contact with ID ${id} not found`);
} }
@@ -12,7 +12,8 @@ type FindContactsOpts = {
orderBy?: string; orderBy?: string;
order?: 'asc' | 'desc'; order?: 'asc' | 'desc';
status?: ContactStatusEnum; status?: ContactStatusEnum;
scope?: ContactScope scope?: ContactScope;
restaurantId?: string;
}; };
@Injectable() @Injectable()
@@ -26,7 +27,7 @@ export class ContactRepository extends EntityRepository<Contact> {
* Supports: search (subject/content/phone), status, ordering. * Supports: search (subject/content/phone), status, ordering.
*/ */
async findAllPaginated(opts: FindContactsOpts = {}): Promise<PaginatedResult<Contact>> { async findAllPaginated(opts: FindContactsOpts = {}): Promise<PaginatedResult<Contact>> {
const { page = 1, limit = 10, search, scope, orderBy = 'createdAt', order = 'desc', status } = opts; const { page = 1, limit = 10, search, scope, orderBy = 'createdAt', order = 'desc', status, restaurantId } = opts;
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
@@ -38,6 +39,9 @@ export class ContactRepository extends EntityRepository<Contact> {
if (scope) { if (scope) {
where.scope = scope; where.scope = scope;
} }
if (restaurantId) {
where.restaurant = restaurantId;
}
if (search) { if (search) {
const pattern = `%${search}%`; const pattern = `%${search}%`;