Add business registration requests with approval flow, SMS notifications, and owner businesses endpoint.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { BusinessServicesModule } from './modules/business-services/business-ser
|
||||
import { AttributeDefinitionsModule } from './modules/attribute-definitions/attribute-definitions.module';
|
||||
import { BusinessAttributeValuesModule } from './modules/business-attribute-values/business-attribute-values.module';
|
||||
import { BookingsModule } from './modules/bookings/bookings.module';
|
||||
import { BusinessRegistrationRequestsModule } from './modules/business-registration-requests/business-registration-requests.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { UsersModule } from './modules/users/users.module';
|
||||
|
||||
@@ -25,6 +26,7 @@ import { UsersModule } from './modules/users/users.module';
|
||||
AttributeDefinitionsModule,
|
||||
BusinessAttributeValuesModule,
|
||||
BookingsModule,
|
||||
BusinessRegistrationRequestsModule,
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export function slugifyTitle(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9-]/g, '')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
export async function uniqueBusinessSlug(
|
||||
title: string,
|
||||
exists: (slug: string) => Promise<boolean>,
|
||||
): Promise<string> {
|
||||
let base = slugifyTitle(title);
|
||||
if (!base) {
|
||||
base = 'business';
|
||||
}
|
||||
let slug = base;
|
||||
let suffix = 0;
|
||||
while (await exists(slug)) {
|
||||
suffix += 1;
|
||||
slug = `${base}-${suffix}`;
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateBusinessRegistrationRequests1789042000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'CreateBusinessRegistrationRequests1789042000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS "business_registration_requests" (
|
||||
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
"applicantId" uuid NOT NULL,
|
||||
"status" character varying(20) NOT NULL DEFAULT 'pending',
|
||||
"title" character varying(150) NOT NULL,
|
||||
"slug" character varying(120) NOT NULL,
|
||||
"categoryId" uuid NOT NULL,
|
||||
"description" character varying(500),
|
||||
"contactPhone" character varying(20),
|
||||
"address" character varying(500),
|
||||
"latitude" double precision,
|
||||
"longitude" double precision,
|
||||
"adminNote" character varying(500),
|
||||
"reviewedById" uuid,
|
||||
"reviewedAt" TIMESTAMP WITH TIME ZONE,
|
||||
"businessId" uuid,
|
||||
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_business_registration_requests" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "FK_brr_applicant" FOREIGN KEY ("applicantId")
|
||||
REFERENCES "users"("id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_brr_category" FOREIGN KEY ("categoryId")
|
||||
REFERENCES "business_categories"("id") ON DELETE RESTRICT,
|
||||
CONSTRAINT "FK_brr_reviewed_by" FOREIGN KEY ("reviewedById")
|
||||
REFERENCES "users"("id") ON DELETE SET NULL,
|
||||
CONSTRAINT "FK_brr_business" FOREIGN KEY ("businessId")
|
||||
REFERENCES "businesses"("id") ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS "IDX_brr_applicant_status"
|
||||
ON "business_registration_requests" ("applicantId", "status")
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "business_registration_requests"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { BusinessCategoryEntity } from './business-category.entity';
|
||||
import { BusinessEntity } from './business.entity';
|
||||
import { UserEntity } from './user.entity';
|
||||
|
||||
export enum BusinessRegistrationStatus {
|
||||
PENDING = 'pending',
|
||||
APPROVED = 'approved',
|
||||
REJECTED = 'rejected',
|
||||
}
|
||||
|
||||
@Entity('business_registration_requests')
|
||||
export class BusinessRegistrationRequestEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'applicantId' })
|
||||
applicant!: UserEntity;
|
||||
|
||||
@Column('uuid')
|
||||
applicantId!: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: BusinessRegistrationStatus.PENDING,
|
||||
})
|
||||
status!: BusinessRegistrationStatus;
|
||||
|
||||
@Column({ length: 150 })
|
||||
title!: string;
|
||||
|
||||
@Column({ length: 120 })
|
||||
slug!: string;
|
||||
|
||||
@ManyToOne(() => BusinessCategoryEntity, { onDelete: 'RESTRICT' })
|
||||
@JoinColumn({ name: 'categoryId' })
|
||||
category!: BusinessCategoryEntity;
|
||||
|
||||
@Column('uuid')
|
||||
categoryId!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true, length: 500 })
|
||||
description!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true, length: 20 })
|
||||
contactPhone!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true, length: 500 })
|
||||
address!: string | null;
|
||||
|
||||
@Column({ type: 'double precision', nullable: true })
|
||||
latitude!: number | null;
|
||||
|
||||
@Column({ type: 'double precision', nullable: true })
|
||||
longitude!: number | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true, length: 500 })
|
||||
adminNote!: string | null;
|
||||
|
||||
@ManyToOne(() => UserEntity, { nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'reviewedById' })
|
||||
reviewedBy!: UserEntity | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
reviewedById!: string | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
reviewedAt!: Date | null;
|
||||
|
||||
@ManyToOne(() => BusinessEntity, { nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'businessId' })
|
||||
business!: BusinessEntity | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
businessId!: string | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
+1
-1
@@ -35,7 +35,7 @@ async function bootstrap() {
|
||||
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle('Montime API')
|
||||
.setDescription('Local services super app API')
|
||||
.setDescription('Montime (منتایم) — Local services super app API')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth(
|
||||
{
|
||||
|
||||
@@ -27,6 +27,6 @@ import { RolesGuard } from './guards/roles.guard';
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, SmsService, JwtAuthGuard, RolesGuard],
|
||||
exports: [JwtAuthGuard, RolesGuard, JwtModule],
|
||||
exports: [JwtAuthGuard, RolesGuard, JwtModule, SmsService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -37,7 +37,7 @@ export class SmsService {
|
||||
}
|
||||
|
||||
async sendOtpSms(mobile: string, code: string): Promise<boolean> {
|
||||
const text = `کد ورود شما: ${code}`;
|
||||
const text = `کد ورود منتایم: ${code}`;
|
||||
return this.sendSms(mobile, text);
|
||||
}
|
||||
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
BusinessRegistrationRequestEntity,
|
||||
BusinessRegistrationStatus,
|
||||
} from '../../entities/business-registration-request.entity';
|
||||
import { UserRole } from '../../entities/user.entity';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { BusinessRegistrationRequestsService } from './business-registration-requests.service';
|
||||
import { CreateBusinessRegistrationDto } from './dto/create-business-registration.dto';
|
||||
import { RejectBusinessRegistrationDto } from './dto/reject-business-registration.dto';
|
||||
|
||||
@Controller('business-registration-requests')
|
||||
export class BusinessRegistrationRequestsController {
|
||||
constructor(private readonly service: BusinessRegistrationRequestsService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.CUSTOMER)
|
||||
create(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Body() payload: CreateBusinessRegistrationDto,
|
||||
): Promise<BusinessRegistrationRequestEntity> {
|
||||
return this.service.create(user.id, payload);
|
||||
}
|
||||
|
||||
@Get('mine')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
findMine(
|
||||
@CurrentUser() user: AuthUser,
|
||||
): Promise<BusinessRegistrationRequestEntity[]> {
|
||||
return this.service.findMine(user.id);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
findAll(
|
||||
@Query('status') status?: BusinessRegistrationStatus,
|
||||
): Promise<BusinessRegistrationRequestEntity[]> {
|
||||
return this.service.findAll({ status });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
findOne(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
): Promise<BusinessRegistrationRequestEntity> {
|
||||
return this.service.findOne(id).then((entity) => {
|
||||
if (user.role !== UserRole.ADMIN && entity.applicantId !== user.id) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
@Post(':id/approve')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
approve(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
): Promise<BusinessRegistrationRequestEntity> {
|
||||
return this.service.approve(id, user);
|
||||
}
|
||||
|
||||
@Post(':id/reject')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
reject(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Body() payload: RejectBusinessRegistrationDto,
|
||||
): Promise<BusinessRegistrationRequestEntity> {
|
||||
return this.service.reject(id, user, payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BusinessRegistrationRequestEntity } from '../../entities/business-registration-request.entity';
|
||||
import { BusinessCategoryEntity } from '../../entities/business-category.entity';
|
||||
import { BusinessEntity } from '../../entities/business.entity';
|
||||
import { UserEntity } from '../../entities/user.entity';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { BusinessRegistrationRequestsController } from './business-registration-requests.controller';
|
||||
import { BusinessRegistrationRequestsService } from './business-registration-requests.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
BusinessRegistrationRequestEntity,
|
||||
BusinessEntity,
|
||||
BusinessCategoryEntity,
|
||||
UserEntity,
|
||||
]),
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [BusinessRegistrationRequestsController],
|
||||
providers: [BusinessRegistrationRequestsService],
|
||||
})
|
||||
export class BusinessRegistrationRequestsModule {}
|
||||
@@ -0,0 +1,253 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { uniqueBusinessSlug } from '../../common/utils/slug.util';
|
||||
import {
|
||||
BusinessRegistrationRequestEntity,
|
||||
BusinessRegistrationStatus,
|
||||
} from '../../entities/business-registration-request.entity';
|
||||
import { BusinessCategoryEntity } from '../../entities/business-category.entity';
|
||||
import { BusinessEntity } from '../../entities/business.entity';
|
||||
import { UserEntity, UserRole } from '../../entities/user.entity';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { SmsService } from '../auth/sms.service';
|
||||
import { CreateBusinessRegistrationDto } from './dto/create-business-registration.dto';
|
||||
import { RejectBusinessRegistrationDto } from './dto/reject-business-registration.dto';
|
||||
|
||||
type ListFilters = {
|
||||
status?: BusinessRegistrationStatus;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class BusinessRegistrationRequestsService {
|
||||
constructor(
|
||||
@InjectRepository(BusinessRegistrationRequestEntity)
|
||||
private readonly repo: Repository<BusinessRegistrationRequestEntity>,
|
||||
@InjectRepository(BusinessEntity)
|
||||
private readonly businessRepo: Repository<BusinessEntity>,
|
||||
@InjectRepository(BusinessCategoryEntity)
|
||||
private readonly categoryRepo: Repository<BusinessCategoryEntity>,
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userRepo: Repository<UserEntity>,
|
||||
private readonly sms: SmsService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
applicantId: string,
|
||||
dto: CreateBusinessRegistrationDto,
|
||||
): Promise<BusinessRegistrationRequestEntity> {
|
||||
const applicant = await this.userRepo.findOne({ where: { id: applicantId } });
|
||||
if (!applicant) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
if (applicant.role === UserRole.ADMIN) {
|
||||
throw new ForbiddenException('Admin cannot submit business registration');
|
||||
}
|
||||
if (applicant.role === UserRole.BUSINESS_OWNER) {
|
||||
throw new BadRequestException('You already manage a business');
|
||||
}
|
||||
|
||||
const owned = await this.businessRepo.findOne({ where: { ownerId: applicantId } });
|
||||
if (owned) {
|
||||
throw new BadRequestException('You already have a registered business');
|
||||
}
|
||||
|
||||
const pending = await this.repo.findOne({
|
||||
where: {
|
||||
applicantId,
|
||||
status: BusinessRegistrationStatus.PENDING,
|
||||
},
|
||||
});
|
||||
if (pending) {
|
||||
throw new ConflictException('You already have a pending registration request');
|
||||
}
|
||||
|
||||
await this.ensureCategoryExists(dto.categoryId);
|
||||
|
||||
const slug = await uniqueBusinessSlug(dto.title, async (candidate) => {
|
||||
const inBusiness = await this.businessRepo.findOne({ where: { slug: candidate } });
|
||||
if (inBusiness) return true;
|
||||
const inRequest = await this.repo.findOne({ where: { slug: candidate } });
|
||||
return Boolean(inRequest);
|
||||
});
|
||||
|
||||
const entity = this.repo.create({
|
||||
applicantId,
|
||||
status: BusinessRegistrationStatus.PENDING,
|
||||
title: dto.title.trim(),
|
||||
slug,
|
||||
categoryId: dto.categoryId,
|
||||
description: dto.description?.trim() ?? null,
|
||||
contactPhone: dto.contactPhone?.trim() ?? null,
|
||||
address: dto.address?.trim() ?? null,
|
||||
latitude: dto.latitude ?? null,
|
||||
longitude: dto.longitude ?? null,
|
||||
});
|
||||
|
||||
const saved = await this.repo.save(entity);
|
||||
const withRelations = await this.findOne(saved.id);
|
||||
|
||||
await this.notifySubmitted(withRelations, applicant);
|
||||
return withRelations;
|
||||
}
|
||||
|
||||
findMine(applicantId: string): Promise<BusinessRegistrationRequestEntity[]> {
|
||||
return this.repo.find({
|
||||
where: { applicantId },
|
||||
relations: { category: true, business: true },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
findAll(filters: ListFilters): Promise<BusinessRegistrationRequestEntity[]> {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('req')
|
||||
.leftJoinAndSelect('req.applicant', 'applicant')
|
||||
.leftJoinAndSelect('req.category', 'category')
|
||||
.leftJoinAndSelect('req.business', 'business')
|
||||
.orderBy('req.createdAt', 'DESC');
|
||||
|
||||
if (filters.status) {
|
||||
qb.andWhere('req.status = :status', { status: filters.status });
|
||||
}
|
||||
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<BusinessRegistrationRequestEntity> {
|
||||
const entity = await this.repo.findOne({
|
||||
where: { id },
|
||||
relations: { applicant: true, category: true, business: true, reviewedBy: true },
|
||||
});
|
||||
if (!entity) {
|
||||
throw new NotFoundException('Registration request not found');
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
async approve(id: string, admin: AuthUser): Promise<BusinessRegistrationRequestEntity> {
|
||||
const request = await this.findOne(id);
|
||||
if (request.status !== BusinessRegistrationStatus.PENDING) {
|
||||
throw new BadRequestException('Only pending requests can be approved');
|
||||
}
|
||||
|
||||
const slugTaken = await this.businessRepo.findOne({ where: { slug: request.slug } });
|
||||
if (slugTaken) {
|
||||
throw new ConflictException('Business slug already exists');
|
||||
}
|
||||
|
||||
const applicant = await this.userRepo.findOne({ where: { id: request.applicantId } });
|
||||
if (!applicant) {
|
||||
throw new NotFoundException('Applicant not found');
|
||||
}
|
||||
|
||||
const business = this.businessRepo.create({
|
||||
title: request.title,
|
||||
slug: request.slug,
|
||||
categoryId: request.categoryId,
|
||||
description: request.description,
|
||||
contactPhone: request.contactPhone,
|
||||
address: request.address,
|
||||
latitude: request.latitude,
|
||||
longitude: request.longitude,
|
||||
ownerId: applicant.id,
|
||||
isActive: true,
|
||||
});
|
||||
const savedBusiness = await this.businessRepo.save(business);
|
||||
|
||||
if (applicant.role !== UserRole.ADMIN) {
|
||||
applicant.role = UserRole.BUSINESS_OWNER;
|
||||
}
|
||||
applicant.isActive = true;
|
||||
await this.userRepo.save(applicant);
|
||||
|
||||
request.status = BusinessRegistrationStatus.APPROVED;
|
||||
request.businessId = savedBusiness.id;
|
||||
request.reviewedById = admin.id;
|
||||
request.reviewedAt = new Date();
|
||||
request.adminNote = null;
|
||||
const savedRequest = await this.repo.save(request);
|
||||
|
||||
const result = await this.findOne(savedRequest.id);
|
||||
await this.notifyApproved(result, applicant);
|
||||
return result;
|
||||
}
|
||||
|
||||
async reject(
|
||||
id: string,
|
||||
admin: AuthUser,
|
||||
dto: RejectBusinessRegistrationDto,
|
||||
): Promise<BusinessRegistrationRequestEntity> {
|
||||
const request = await this.findOne(id);
|
||||
if (request.status !== BusinessRegistrationStatus.PENDING) {
|
||||
throw new BadRequestException('Only pending requests can be rejected');
|
||||
}
|
||||
|
||||
request.status = BusinessRegistrationStatus.REJECTED;
|
||||
request.adminNote = dto.adminNote?.trim() ?? null;
|
||||
request.reviewedById = admin.id;
|
||||
request.reviewedAt = new Date();
|
||||
const saved = await this.repo.save(request);
|
||||
|
||||
const result = await this.findOne(saved.id);
|
||||
const applicant = result.applicant;
|
||||
if (applicant) {
|
||||
await this.notifyRejected(result, applicant);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async ensureCategoryExists(categoryId: string): Promise<void> {
|
||||
const category = await this.categoryRepo.findOne({ where: { id: categoryId } });
|
||||
if (!category) {
|
||||
throw new NotFoundException('Business category not found');
|
||||
}
|
||||
}
|
||||
|
||||
private async notifySubmitted(
|
||||
request: BusinessRegistrationRequestEntity,
|
||||
applicant: UserEntity,
|
||||
): Promise<void> {
|
||||
await this.sms.sendSms(
|
||||
applicant.mobile,
|
||||
`درخواست ثبت «${request.title}» در منتایم ثبت شد و در انتظار تأیید است.`,
|
||||
);
|
||||
|
||||
const adminMobile = this.config.get<string>('SUPER_ADMIN_MOBILE');
|
||||
if (adminMobile) {
|
||||
await this.sms.sendSms(
|
||||
adminMobile,
|
||||
`درخواست کسبوکار جدید: ${request.title} — ${applicant.mobile}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async notifyApproved(
|
||||
request: BusinessRegistrationRequestEntity,
|
||||
applicant: UserEntity,
|
||||
): Promise<void> {
|
||||
await this.sms.sendSms(
|
||||
applicant.mobile,
|
||||
`کسبوکار «${request.title}» در منتایم تأیید شد. پنل مدیریت در اپ فعال است.`,
|
||||
);
|
||||
}
|
||||
|
||||
private async notifyRejected(
|
||||
request: BusinessRegistrationRequestEntity,
|
||||
applicant: UserEntity,
|
||||
): Promise<void> {
|
||||
const note = request.adminNote ? ` ${request.adminNote}` : '';
|
||||
await this.sms.sendSms(
|
||||
applicant.mobile,
|
||||
`درخواست «${request.title}» در منتایم رد شد.${note}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsLatitude,
|
||||
IsLongitude,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateBusinessRegistrationDto {
|
||||
@ApiProperty({ example: 'کارواش محمد' })
|
||||
@IsString()
|
||||
@MaxLength(150)
|
||||
title!: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@IsUUID()
|
||||
categoryId!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '09121234567' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
contactPhone?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
address?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsLatitude()
|
||||
latitude?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsLongitude()
|
||||
longitude?: number;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
export class RejectBusinessRegistrationDto {
|
||||
@ApiPropertyOptional({ example: 'اطلاعات ناقص است' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
adminNote?: string;
|
||||
}
|
||||
@@ -48,6 +48,13 @@ export class BusinessesController {
|
||||
});
|
||||
}
|
||||
|
||||
@Get('mine')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER)
|
||||
findMine(@CurrentUser() user: AuthUser): Promise<BusinessEntity[]> {
|
||||
return this.service.findMine(user.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string): Promise<BusinessEntity> {
|
||||
return this.service.findOne(id);
|
||||
|
||||
@@ -70,6 +70,14 @@ export class BusinessesService {
|
||||
return business;
|
||||
}
|
||||
|
||||
async findMine(ownerId: string): Promise<BusinessEntity[]> {
|
||||
return this.repo.find({
|
||||
where: { ownerId },
|
||||
relations: { category: true, images: true },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async create(payload: CreateBusinessDto): Promise<BusinessEntity> {
|
||||
const slugTaken = await this.repo.findOne({ where: { slug: payload.slug } });
|
||||
if (slugTaken) {
|
||||
|
||||
Reference in New Issue
Block a user