module business-services 050219
This commit is contained in:
@@ -7,6 +7,7 @@ import { dataSourceOptions } from './config/typeorm.config';
|
|||||||
import { BusinessCategoriesModule } from './modules/business-categories/business-categories.module';
|
import { BusinessCategoriesModule } from './modules/business-categories/business-categories.module';
|
||||||
import { BusinessesModule } from './modules/businesses/businesses.module';
|
import { BusinessesModule } from './modules/businesses/businesses.module';
|
||||||
import { ServiceTemplatesModule } from './modules/service-templates/service-templates.module';
|
import { ServiceTemplatesModule } from './modules/service-templates/service-templates.module';
|
||||||
|
import { BusinessServicesModule } from './modules/business-services/business-services.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -15,6 +16,7 @@ import { ServiceTemplatesModule } from './modules/service-templates/service-temp
|
|||||||
BusinessCategoriesModule,
|
BusinessCategoriesModule,
|
||||||
BusinessesModule,
|
BusinessesModule,
|
||||||
ServiceTemplatesModule,
|
ServiceTemplatesModule,
|
||||||
|
BusinessServicesModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user