sent messages
This commit is contained in:
@@ -8,6 +8,7 @@ import { getTypeOrmOptions } from './database/typeorm-options';
|
|||||||
import { ReservesModule } from './reserves/reserves.module';
|
import { ReservesModule } from './reserves/reserves.module';
|
||||||
import { SalonsModule } from './salons/salons.module';
|
import { SalonsModule } from './salons/salons.module';
|
||||||
import { ServicesModule } from './services/services.module';
|
import { ServicesModule } from './services/services.module';
|
||||||
|
import { SentMessagesModule } from './sent-messages/sent-messages.module';
|
||||||
import { SmsTemplatesModule } from './sms-templates/sms-templates.module';
|
import { SmsTemplatesModule } from './sms-templates/sms-templates.module';
|
||||||
import { SuggestionsModule } from './suggestions/suggestions.module';
|
import { SuggestionsModule } from './suggestions/suggestions.module';
|
||||||
import { StylistsModule } from './stylists/stylists.module';
|
import { StylistsModule } from './stylists/stylists.module';
|
||||||
@@ -29,6 +30,7 @@ import { UsersModule } from './users/users.module';
|
|||||||
ReservesModule,
|
ReservesModule,
|
||||||
SuggestionsModule,
|
SuggestionsModule,
|
||||||
SmsTemplatesModule,
|
SmsTemplatesModule,
|
||||||
|
SentMessagesModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
MigrationInterface,
|
||||||
|
QueryRunner,
|
||||||
|
Table,
|
||||||
|
TableForeignKey,
|
||||||
|
TableIndex,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
export class AddSentMessages1720200000000 implements MigrationInterface {
|
||||||
|
name = 'AddSentMessages1720200000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TYPE "sent_messages_status_enum" AS ENUM(
|
||||||
|
'pending',
|
||||||
|
'sent',
|
||||||
|
'failed'
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'sent_messages',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'uuid',
|
||||||
|
isPrimary: true,
|
||||||
|
generationStrategy: 'uuid',
|
||||||
|
default: 'gen_random_uuid()',
|
||||||
|
},
|
||||||
|
{ name: 'salonId', type: 'uuid' },
|
||||||
|
{ name: 'customerId', type: 'uuid', isNullable: true },
|
||||||
|
{ name: 'reserveId', type: 'uuid', isNullable: true },
|
||||||
|
{ name: 'mobile', type: 'varchar', length: '11' },
|
||||||
|
{ name: 'body', type: 'text' },
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
type: 'sent_messages_status_enum',
|
||||||
|
default: `'pending'`,
|
||||||
|
},
|
||||||
|
{ name: 'createdAt', type: 'timestamp', default: 'now()' },
|
||||||
|
{ name: 'updatedAt', type: 'timestamp', default: 'now()' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'sent_messages',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['salonId'],
|
||||||
|
referencedTableName: 'salons',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'sent_messages',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['customerId'],
|
||||||
|
referencedTableName: 'customers',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'sent_messages',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['reserveId'],
|
||||||
|
referencedTableName: 'reserves',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'sent_messages',
|
||||||
|
new TableIndex({
|
||||||
|
name: 'IDX_sent_messages_salonId_createdAt',
|
||||||
|
columnNames: ['salonId', 'createdAt'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'sent_messages',
|
||||||
|
new TableIndex({
|
||||||
|
name: 'IDX_sent_messages_customerId',
|
||||||
|
columnNames: ['customerId'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'sent_messages',
|
||||||
|
new TableIndex({
|
||||||
|
name: 'IDX_sent_messages_reserveId',
|
||||||
|
columnNames: ['reserveId'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'sent_messages',
|
||||||
|
new TableIndex({
|
||||||
|
name: 'IDX_sent_messages_status',
|
||||||
|
columnNames: ['status'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable('sent_messages', true);
|
||||||
|
await queryRunner.query(`DROP TYPE "sent_messages_status_enum"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUUID,
|
||||||
|
Matches,
|
||||||
|
MaxLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { SentMessageStatus } from '../enums/sent-message-status.enum';
|
||||||
|
|
||||||
|
export class CreateSentMessageDto {
|
||||||
|
@IsUUID()
|
||||||
|
salonId: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
customerId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
reserveId?: string;
|
||||||
|
|
||||||
|
@Matches(/^09\d{9}$/)
|
||||||
|
mobile: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(1000)
|
||||||
|
body: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(SentMessageStatus)
|
||||||
|
status?: SentMessageStatus;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { IsEnum, IsOptional, IsUUID } from 'class-validator';
|
||||||
|
import { SentMessageStatus } from '../enums/sent-message-status.enum';
|
||||||
|
|
||||||
|
export class FindSentMessagesQueryDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
salonId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
customerId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
reserveId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(SentMessageStatus)
|
||||||
|
status?: SentMessageStatus;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { IsEnum } from 'class-validator';
|
||||||
|
import { SentMessageStatus } from '../enums/sent-message-status.enum';
|
||||||
|
|
||||||
|
export class UpdateSentMessageDto {
|
||||||
|
@IsEnum(SentMessageStatus)
|
||||||
|
status: SentMessageStatus;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Customer } from '../../customers/entities/customer.entity';
|
||||||
|
import { Reserve } from '../../reserves/entities/reserve.entity';
|
||||||
|
import { Salon } from '../../salons/entities/salon.entity';
|
||||||
|
import { SentMessageStatus } from '../enums/sent-message-status.enum';
|
||||||
|
|
||||||
|
@Entity('sent_messages')
|
||||||
|
@Index(['salonId', 'createdAt'])
|
||||||
|
@Index(['customerId'])
|
||||||
|
@Index(['reserveId'])
|
||||||
|
@Index(['status'])
|
||||||
|
export class SentMessage {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
salonId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Salon, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'salonId' })
|
||||||
|
salon: Salon;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', nullable: true })
|
||||||
|
customerId: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Customer, { onDelete: 'SET NULL', nullable: true })
|
||||||
|
@JoinColumn({ name: 'customerId' })
|
||||||
|
customer: Customer | null;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', nullable: true })
|
||||||
|
reserveId: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Reserve, { onDelete: 'SET NULL', nullable: true })
|
||||||
|
@JoinColumn({ name: 'reserveId' })
|
||||||
|
reserve: Reserve | null;
|
||||||
|
|
||||||
|
@Column({ length: 11 })
|
||||||
|
mobile: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
body: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'enum',
|
||||||
|
enum: SentMessageStatus,
|
||||||
|
enumName: 'sent_messages_status_enum',
|
||||||
|
default: SentMessageStatus.PENDING,
|
||||||
|
})
|
||||||
|
status: SentMessageStatus;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum SentMessageStatus {
|
||||||
|
PENDING = 'pending',
|
||||||
|
SENT = 'sent',
|
||||||
|
FAILED = 'failed',
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
|
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||||
|
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||||
|
import { UserRole } from '../users/enums/user-role.enum';
|
||||||
|
import { CreateSentMessageDto } from './dto/create-sent-message.dto';
|
||||||
|
import { FindSentMessagesQueryDto } from './dto/find-sent-messages-query.dto';
|
||||||
|
import { UpdateSentMessageDto } from './dto/update-sent-message.dto';
|
||||||
|
import { SentMessagesService } from './sent-messages.service';
|
||||||
|
|
||||||
|
@Controller('sent-messages')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
export class SentMessagesController {
|
||||||
|
constructor(private readonly sentMessagesService: SentMessagesService) {}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN, UserRole.SALON)
|
||||||
|
@Post()
|
||||||
|
create(
|
||||||
|
@Body() createDto: CreateSentMessageDto,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.sentMessagesService.create(createDto, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN, UserRole.SALON)
|
||||||
|
@Get()
|
||||||
|
findAll(
|
||||||
|
@Query() query: FindSentMessagesQueryDto,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.sentMessagesService.findAll(user, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN, UserRole.SALON)
|
||||||
|
@Get(':id')
|
||||||
|
findOne(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.sentMessagesService.findOne(id, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN, UserRole.SALON)
|
||||||
|
@Patch(':id')
|
||||||
|
update(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Body() updateDto: UpdateSentMessageDto,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.sentMessagesService.update(id, updateDto, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AuthGuardsModule } from '../auth/auth-guards.module';
|
||||||
|
import { AuthorizationModule } from '../authorization/authorization.module';
|
||||||
|
import { CustomersModule } from '../customers/customers.module';
|
||||||
|
import { ReservesModule } from '../reserves/reserves.module';
|
||||||
|
import { SalonsModule } from '../salons/salons.module';
|
||||||
|
import { SentMessage } from './entities/sent-message.entity';
|
||||||
|
import { SentMessagesController } from './sent-messages.controller';
|
||||||
|
import { SentMessagesService } from './sent-messages.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([SentMessage]),
|
||||||
|
AuthorizationModule,
|
||||||
|
AuthGuardsModule,
|
||||||
|
SalonsModule,
|
||||||
|
CustomersModule,
|
||||||
|
ReservesModule,
|
||||||
|
],
|
||||||
|
controllers: [SentMessagesController],
|
||||||
|
providers: [SentMessagesService],
|
||||||
|
exports: [SentMessagesService],
|
||||||
|
})
|
||||||
|
export class SentMessagesModule {}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { AuthorizationService } from '../authorization/authorization.service';
|
||||||
|
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||||
|
import { CustomersService } from '../customers/customers.service';
|
||||||
|
import { ReservesService } from '../reserves/reserves.service';
|
||||||
|
import { SalonsService } from '../salons/salons.service';
|
||||||
|
import { UserRole } from '../users/enums/user-role.enum';
|
||||||
|
import { CreateSentMessageDto } from './dto/create-sent-message.dto';
|
||||||
|
import { FindSentMessagesQueryDto } from './dto/find-sent-messages-query.dto';
|
||||||
|
import { UpdateSentMessageDto } from './dto/update-sent-message.dto';
|
||||||
|
import { SentMessage } from './entities/sent-message.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SentMessagesService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SentMessage)
|
||||||
|
private readonly sentMessagesRepository: Repository<SentMessage>,
|
||||||
|
private readonly salonsService: SalonsService,
|
||||||
|
private readonly customersService: CustomersService,
|
||||||
|
private readonly reservesService: ReservesService,
|
||||||
|
private readonly authorizationService: AuthorizationService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
createDto: CreateSentMessageDto,
|
||||||
|
user: AuthUser,
|
||||||
|
): Promise<SentMessage> {
|
||||||
|
const salon = await this.salonsService.findOne(createDto.salonId);
|
||||||
|
this.ensureCanAccessSalon(user, salon.ownerId);
|
||||||
|
|
||||||
|
if (createDto.customerId) {
|
||||||
|
await this.customersService.findOne(createDto.customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createDto.reserveId) {
|
||||||
|
const reserve = await this.reservesService.findOne(
|
||||||
|
createDto.reserveId,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
if (reserve.salonId !== createDto.salonId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Reserve does not belong to the selected salon',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = this.sentMessagesRepository.create({
|
||||||
|
salonId: createDto.salonId,
|
||||||
|
customerId: createDto.customerId ?? null,
|
||||||
|
reserveId: createDto.reserveId ?? null,
|
||||||
|
mobile: createDto.mobile,
|
||||||
|
body: createDto.body,
|
||||||
|
status: createDto.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
const saved = await this.sentMessagesRepository.save(message);
|
||||||
|
return this.findOne(saved.id, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(
|
||||||
|
user: AuthUser,
|
||||||
|
query: FindSentMessagesQueryDto,
|
||||||
|
): Promise<SentMessage[]> {
|
||||||
|
const qb = this.sentMessagesRepository
|
||||||
|
.createQueryBuilder('message')
|
||||||
|
.leftJoinAndSelect('message.salon', 'salon')
|
||||||
|
.leftJoinAndSelect('message.customer', 'customer')
|
||||||
|
.leftJoinAndSelect('customer.user', 'customerUser')
|
||||||
|
.leftJoinAndSelect('message.reserve', 'reserve')
|
||||||
|
.orderBy('message.createdAt', 'DESC');
|
||||||
|
|
||||||
|
if (query.salonId) {
|
||||||
|
qb.andWhere('message.salonId = :salonId', { salonId: query.salonId });
|
||||||
|
}
|
||||||
|
if (query.customerId) {
|
||||||
|
qb.andWhere('message.customerId = :customerId', {
|
||||||
|
customerId: query.customerId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (query.reserveId) {
|
||||||
|
qb.andWhere('message.reserveId = :reserveId', {
|
||||||
|
reserveId: query.reserveId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (query.status) {
|
||||||
|
qb.andWhere('message.status = :status', { status: query.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.authorizationService.isAdmin(user)) {
|
||||||
|
if (user.role === UserRole.SALON) {
|
||||||
|
qb.andWhere('salon.ownerId = :ownerId', { ownerId: user.id });
|
||||||
|
} else {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string, user: AuthUser): Promise<SentMessage> {
|
||||||
|
const message = await this.sentMessagesRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: {
|
||||||
|
salon: true,
|
||||||
|
customer: { user: true },
|
||||||
|
reserve: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!message) {
|
||||||
|
throw new NotFoundException(`Sent message #${id} not found`);
|
||||||
|
}
|
||||||
|
this.ensureCanAccessSalon(user, message.salon.ownerId);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: string,
|
||||||
|
updateDto: UpdateSentMessageDto,
|
||||||
|
user: AuthUser,
|
||||||
|
): Promise<SentMessage> {
|
||||||
|
const message = await this.findOne(id, user);
|
||||||
|
message.status = updateDto.status;
|
||||||
|
await this.sentMessagesRepository.save(message);
|
||||||
|
return this.findOne(id, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureCanAccessSalon(user: AuthUser, ownerId: string): void {
|
||||||
|
if (this.authorizationService.isAdmin(user)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.role === UserRole.SALON && ownerId === user.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user