module business-services 050219

This commit is contained in:
2026-05-09 17:07:15 +03:30
parent 87d8912a1f
commit c8a3e4ccb7
6 changed files with 291 additions and 0 deletions
+2
View File
@@ -7,6 +7,7 @@ import { dataSourceOptions } from './config/typeorm.config';
import { BusinessCategoriesModule } from './modules/business-categories/business-categories.module';
import { BusinessesModule } from './modules/businesses/businesses.module';
import { ServiceTemplatesModule } from './modules/service-templates/service-templates.module';
import { BusinessServicesModule } from './modules/business-services/business-services.module';
@Module({
imports: [
@@ -15,6 +16,7 @@ import { ServiceTemplatesModule } from './modules/service-templates/service-temp
BusinessCategoriesModule,
BusinessesModule,
ServiceTemplatesModule,
BusinessServicesModule,
],
controllers: [AppController],
providers: [AppService],
@@ -0,0 +1,61 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
} from '@nestjs/common';
import { BusinessServiceEntity } from '../../entities/business-service.entity';
import { BusinessServicesService } from './business-services.service';
import { CreateBusinessServiceDto } from './dto/create-business-service.dto';
import { UpdateBusinessServiceDto } from './dto/update-business-service.dto';
@Controller('business-services')
export class BusinessServicesController {
constructor(private readonly service: BusinessServicesService) {}
@Get()
findAll(
@Query('businessId') businessId?: string,
@Query('serviceTemplateId') serviceTemplateId?: string,
@Query('isActive') isActive?: string,
): Promise<BusinessServiceEntity[]> {
const parsedIsActive =
typeof isActive === 'string' ? isActive === 'true' : undefined;
return this.service.findAll({
businessId,
serviceTemplateId,
isActive: parsedIsActive,
});
}
@Get(':id')
findOne(@Param('id') id: string): Promise<BusinessServiceEntity> {
return this.service.findOne(id);
}
@Post()
create(
@Body() payload: CreateBusinessServiceDto,
): Promise<BusinessServiceEntity> {
return this.service.create(payload);
}
@Patch(':id')
update(
@Param('id') id: string,
@Body() payload: UpdateBusinessServiceDto,
): Promise<BusinessServiceEntity> {
return this.service.update(id, payload);
}
@Delete(':id')
async remove(@Param('id') id: string): Promise<{ success: true }> {
await this.service.remove(id);
return { success: true };
}
}
@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BusinessEntity } from '../../entities/business.entity';
import { BusinessServiceEntity } from '../../entities/business-service.entity';
import { ServiceTemplateEntity } from '../../entities/service-template.entity';
import { BusinessServicesController } from './business-services.controller';
import { BusinessServicesService } from './business-services.service';
@Module({
imports: [
TypeOrmModule.forFeature([
BusinessServiceEntity,
BusinessEntity,
ServiceTemplateEntity,
]),
],
controllers: [BusinessServicesController],
providers: [BusinessServicesService],
})
export class BusinessServicesModule {}
@@ -0,0 +1,193 @@
import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BusinessEntity } from '../../entities/business.entity';
import { BusinessServiceEntity } from '../../entities/business-service.entity';
import { ServiceTemplateEntity } from '../../entities/service-template.entity';
import { CreateBusinessServiceDto } from './dto/create-business-service.dto';
import { UpdateBusinessServiceDto } from './dto/update-business-service.dto';
type FindAllFilters = {
businessId?: string;
serviceTemplateId?: string;
isActive?: boolean;
};
function normalizePrice(value: number): string {
if (!Number.isFinite(value) || value < 0) {
throw new BadRequestException('price must be a non-negative finite number');
}
return value.toFixed(2);
}
@Injectable()
export class BusinessServicesService {
constructor(
@InjectRepository(BusinessServiceEntity)
private readonly repo: Repository<BusinessServiceEntity>,
@InjectRepository(BusinessEntity)
private readonly businessRepo: Repository<BusinessEntity>,
@InjectRepository(ServiceTemplateEntity)
private readonly templateRepo: Repository<ServiceTemplateEntity>,
) {}
async findAll(filters: FindAllFilters): Promise<BusinessServiceEntity[]> {
const query = this.repo
.createQueryBuilder('bs')
.leftJoinAndSelect('bs.business', 'business')
.leftJoinAndSelect('bs.serviceTemplate', 'template')
.orderBy('bs.createdAt', 'DESC');
if (filters.businessId) {
query.andWhere('bs.businessId = :businessId', {
businessId: filters.businessId,
});
}
if (filters.serviceTemplateId) {
query.andWhere('bs.serviceTemplateId = :serviceTemplateId', {
serviceTemplateId: filters.serviceTemplateId,
});
}
if (typeof filters.isActive === 'boolean') {
query.andWhere('bs.isActive = :isActive', {
isActive: filters.isActive,
});
}
return query.getMany();
}
async findOne(id: string): Promise<BusinessServiceEntity> {
const row = await this.repo.findOne({
where: { id },
relations: { business: true, serviceTemplate: true },
});
if (!row) {
throw new NotFoundException('Business service not found');
}
return row;
}
async create(dto: CreateBusinessServiceDto): Promise<BusinessServiceEntity> {
const business = await this.businessRepo.findOne({
where: { id: dto.businessId },
});
if (!business) {
throw new NotFoundException('Business not found');
}
const template = await this.templateRepo.findOne({
where: { id: dto.serviceTemplateId },
});
if (!template) {
throw new NotFoundException('Service template not found');
}
if (template.categoryId !== business.categoryId) {
throw new BadRequestException(
'Service template category must match business category',
);
}
const dup = await this.repo.findOne({
where: {
businessId: dto.businessId,
serviceTemplateId: dto.serviceTemplateId,
},
});
if (dup) {
throw new ConflictException(
'This service template is already enabled for this business',
);
}
const duration =
dto.durationMinutes ?? template.defaultDurationMinutes ?? null;
const entity = this.repo.create({
businessId: dto.businessId,
serviceTemplateId: dto.serviceTemplateId,
price: normalizePrice(dto.price),
durationMinutes: duration,
isActive: dto.isActive ?? true,
});
return this.repo.save(entity);
}
async update(
id: string,
dto: UpdateBusinessServiceDto,
): Promise<BusinessServiceEntity> {
const row = await this.findOne(id);
let nextTemplateId = row.serviceTemplateId;
if (dto.serviceTemplateId !== undefined) {
nextTemplateId = dto.serviceTemplateId;
}
const business = await this.businessRepo.findOne({
where: { id: row.businessId },
});
if (!business) {
throw new NotFoundException('Business not found');
}
const template = await this.templateRepo.findOne({
where: { id: nextTemplateId },
});
if (!template) {
throw new NotFoundException('Service template not found');
}
if (template.categoryId !== business.categoryId) {
throw new BadRequestException(
'Service template category must match business category',
);
}
if (
dto.serviceTemplateId !== undefined &&
dto.serviceTemplateId !== row.serviceTemplateId
) {
const dup = await this.repo.findOne({
where: {
businessId: row.businessId,
serviceTemplateId: dto.serviceTemplateId,
},
});
if (dup && dup.id !== row.id) {
throw new ConflictException(
'This service template is already enabled for this business',
);
}
}
if (dto.price !== undefined) {
row.price = normalizePrice(dto.price);
}
if (dto.durationMinutes !== undefined) {
row.durationMinutes = dto.durationMinutes;
}
if (dto.isActive !== undefined) {
row.isActive = dto.isActive;
}
if (dto.serviceTemplateId !== undefined) {
row.serviceTemplateId = dto.serviceTemplateId;
}
return this.repo.save(row);
}
async remove(id: string): Promise<void> {
const result = await this.repo.delete(id);
if (!result.affected) {
throw new NotFoundException('Business service not found');
}
}
}
@@ -0,0 +1,9 @@
export class CreateBusinessServiceDto {
businessId!: string;
serviceTemplateId!: string;
/** قیمت به واحد پول (عدد اعشاری مجاز است) */
price!: number;
/** اگر خالی باشد از template.defaultDurationMinutes استفاده می‌شود */
durationMinutes?: number | null;
isActive?: boolean;
}
@@ -0,0 +1,6 @@
export class UpdateBusinessServiceDto {
serviceTemplateId?: string;
price?: number;
durationMinutes?: number | null;
isActive?: boolean;
}