From 9fe2321c9bbc99513361696a7c926c6eff51fcfb Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 6 Jan 2026 23:08:58 +0330 Subject: [PATCH] add restuarant to contact entity --- database/migrations/.snapshot-postgres.json | 298 ++++++++++-------- .../migrations/Migration20260106192946.ts | 14 + .../contact/controllers/contact.controller.ts | 17 +- src/modules/contact/dto/create-contact.dto.ts | 8 +- .../contact/entities/contact.entity.ts | 6 +- .../contact/providers/contact.service.ts | 46 ++- .../repositories/contact.repository.ts | 8 +- 7 files changed, 239 insertions(+), 158 deletions(-) create mode 100644 database/migrations/Migration20260106192946.ts diff --git a/database/migrations/.snapshot-postgres.json b/database/migrations/.snapshot-postgres.json index 44a1442..c5c67b1 100644 --- a/database/migrations/.snapshot-postgres.json +++ b/database/migrations/.snapshot-postgres.json @@ -127,143 +127,6 @@ "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" - }, - "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": { "id": { @@ -2003,6 +1866,167 @@ }, "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": { "id": { diff --git a/database/migrations/Migration20260106192946.ts b/database/migrations/Migration20260106192946.ts new file mode 100644 index 0000000..3e324b4 --- /dev/null +++ b/database/migrations/Migration20260106192946.ts @@ -0,0 +1,14 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260106192946 extends Migration { + + override async up(): Promise { + 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 { + this.addSql(`alter table "contacts" drop constraint "contacts_restaurant_id_foreign";`); + } + +} diff --git a/src/modules/contact/controllers/contact.controller.ts b/src/modules/contact/controllers/contact.controller.ts index 22e4d31..d3e3344 100644 --- a/src/modules/contact/controllers/contact.controller.ts +++ b/src/modules/contact/controllers/contact.controller.ts @@ -19,6 +19,7 @@ import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guar import { API_HEADER_SLUG } from 'src/common/constants'; import { Permission } from 'src/common/enums/permission.enum'; import { Permissions } from 'src/common/decorators/permissions.decorator'; +import { RestId } from 'src/common/decorators/rest-id.decorator'; @ApiTags('contact') @Controller() @@ -44,8 +45,8 @@ export class ContactController { @ApiQuery({ name: 'orderBy', required: false, type: String }) @ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] }) @ApiQuery({ name: 'status', required: false, enum: ['new', 'seen'] }) - async findAllPaginated(@Query() dto: FindContactsDto) { - return this.contactService.findAllPaginatedAdmin(dto); + async findAllPaginated(@RestId() restId: string, @Query() dto: FindContactsDto) { + return this.contactService.findAllPaginatedAdmin(dto, restId); } @UseGuards(AdminAuthGuard) @@ -54,8 +55,8 @@ export class ContactController { @Get('admin/contact/:id') @ApiOperation({ summary: 'Get contact details by ID (admin only)' }) @ApiParam({ name: 'id', description: 'Contact ID' }) - async findOne(@Param('id') id: string) { - return this.contactService.findOne(id); + async findOne(@RestId() restId: string, @Param('id') id: string) { + return this.contactService.findOne(id, restId); } @UseGuards(AdminAuthGuard) @@ -64,8 +65,8 @@ export class ContactController { @Patch('admin/contact/:id/status') @ApiOperation({ summary: 'Update contact status (admin only)' }) @ApiParam({ name: 'id', description: 'Contact ID' }) - async updateStatus(@Param('id') id: string, @Body() dto: UpdateContactStatusDto) { - return this.contactService.updateStatus(id, dto); + async updateStatus(@RestId() restId: string, @Param('id') id: string, @Body() dto: UpdateContactStatusDto) { + return this.contactService.updateStatus(id, dto, restId); } @UseGuards(AdminAuthGuard) @@ -74,8 +75,8 @@ export class ContactController { @Delete('admin/contact/:id') @ApiOperation({ summary: 'Hard delete a contact (admin only)' }) @ApiParam({ name: 'id', description: 'Contact ID' }) - async remove(@Param('id') id: string) { - await this.contactService.remove(id); + async remove(@RestId() restId: string, @Param('id') id: string) { + await this.contactService.remove(id, restId); } /*** Super Admin ***/ diff --git a/src/modules/contact/dto/create-contact.dto.ts b/src/modules/contact/dto/create-contact.dto.ts index 134bd54..940c711 100644 --- a/src/modules/contact/dto/create-contact.dto.ts +++ b/src/modules/contact/dto/create-contact.dto.ts @@ -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 { ContactScope } from '../interface/interface'; @@ -18,6 +18,12 @@ export class CreateContactDto { @ApiProperty({ description: 'Contact Scope' }) 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() @IsString() @IsMobilePhone('fa-IR') diff --git a/src/modules/contact/entities/contact.entity.ts b/src/modules/contact/entities/contact.entity.ts index ba0657c..db1e291 100644 --- a/src/modules/contact/entities/contact.entity.ts +++ b/src/modules/contact/entities/contact.entity.ts @@ -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 { ContactScope, ContactStatusEnum } from '../interface/interface'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; @Entity({ tableName: 'contacts' }) export class Contact extends BaseEntity { @@ -16,6 +17,9 @@ export class Contact extends BaseEntity { @Enum(() => ContactScope) scope: ContactScope + @ManyToOne(() => Restaurant, { nullable: true }) + restaurant?: Restaurant | null = null; + @Property({ default: ContactStatusEnum.New }) status: ContactStatusEnum = ContactStatusEnum.New; } diff --git a/src/modules/contact/providers/contact.service.ts b/src/modules/contact/providers/contact.service.ts index 0758a5d..2b04004 100644 --- a/src/modules/contact/providers/contact.service.ts +++ b/src/modules/contact/providers/contact.service.ts @@ -8,6 +8,7 @@ import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { ContactStatusEnum } from '../interface/interface'; import { EntityManager } from '@mikro-orm/postgresql'; import { ContactScope } from '../interface/interface'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; @Injectable() export class ContactService { @@ -17,15 +18,26 @@ export class ContactService { ) { } async create(createContactDto: CreateContactDto): Promise { + 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({ - ...createContactDto, + ...contactData, status: ContactStatusEnum.New, + restaurant, }); await this.em.persistAndFlush(contact); return contact; } - async findAllPaginatedAdmin(dto: FindContactsDto): Promise> { + async findAllPaginatedAdmin(dto: FindContactsDto, restaurantId?: string): Promise> { return this.contactRepository.findAllPaginated({ page: dto.page, limit: dto.limit, @@ -33,7 +45,8 @@ export class ContactService { orderBy: dto.orderBy, order: dto.order, status: dto.status, - scope: ContactScope.RESTAURANT + scope: ContactScope.RESTAURANT, + restaurantId }); } async findAllPaginatedSuper(dto: FindContactsDto): Promise> { @@ -48,16 +61,26 @@ export class ContactService { }); } - async findOne(id: string): Promise { - const contact = await this.contactRepository.findOne({ id }); + async findOne(id: string, restaurantId?: string): Promise { + const where: any = { id }; + if (restaurantId) { + where.restaurant = restaurantId; + } + + const contact = await this.contactRepository.findOne(where); if (!contact) { throw new NotFoundException(`Contact with ID ${id} not found`); } return contact; } - async updateStatus(id: string, dto: UpdateContactStatusDto): Promise { - const contact = await this.contactRepository.findOne({ id }); + async updateStatus(id: string, dto: UpdateContactStatusDto, restaurantId?: string): Promise { + const where: any = { id }; + if (restaurantId) { + where.restaurant = restaurantId; + } + + const contact = await this.contactRepository.findOne(where); if (!contact) { throw new NotFoundException(`Contact with ID ${id} not found`); } @@ -66,8 +89,13 @@ export class ContactService { return contact; } - async remove(id: string): Promise { - const contact = await this.contactRepository.findOne({ id }); + async remove(id: string, restaurantId?: string): Promise { + const where: any = { id }; + if (restaurantId) { + where.restaurant = restaurantId; + } + + const contact = await this.contactRepository.findOne(where); if (!contact) { throw new NotFoundException(`Contact with ID ${id} not found`); } diff --git a/src/modules/contact/repositories/contact.repository.ts b/src/modules/contact/repositories/contact.repository.ts index 084d61c..01f6278 100644 --- a/src/modules/contact/repositories/contact.repository.ts +++ b/src/modules/contact/repositories/contact.repository.ts @@ -12,7 +12,8 @@ type FindContactsOpts = { orderBy?: string; order?: 'asc' | 'desc'; status?: ContactStatusEnum; - scope?: ContactScope + scope?: ContactScope; + restaurantId?: string; }; @Injectable() @@ -26,7 +27,7 @@ export class ContactRepository extends EntityRepository { * Supports: search (subject/content/phone), status, ordering. */ async findAllPaginated(opts: FindContactsOpts = {}): Promise> { - 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; @@ -38,6 +39,9 @@ export class ContactRepository extends EntityRepository { if (scope) { where.scope = scope; } + if (restaurantId) { + where.restaurant = restaurantId; + } if (search) { const pattern = `%${search}%`;