modules 050219
This commit is contained in:
@@ -8,6 +8,9 @@ import { BusinessCategoriesModule } from './modules/business-categories/business
|
||||
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';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -17,6 +20,9 @@ import { BusinessServicesModule } from './modules/business-services/business-ser
|
||||
BusinessesModule,
|
||||
ServiceTemplatesModule,
|
||||
BusinessServicesModule,
|
||||
AttributeDefinitionsModule,
|
||||
BusinessAttributeValuesModule,
|
||||
BookingsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { AttributeDefinitionEntity } from '../../entities/attribute-definition.entity';
|
||||
import { AttributeDefinitionsService } from './attribute-definitions.service';
|
||||
import { CreateAttributeDefinitionDto } from './dto/create-attribute-definition.dto';
|
||||
import { UpdateAttributeDefinitionDto } from './dto/update-attribute-definition.dto';
|
||||
|
||||
@Controller('attribute-definitions')
|
||||
export class AttributeDefinitionsController {
|
||||
constructor(private readonly service: AttributeDefinitionsService) {}
|
||||
|
||||
@Get()
|
||||
findAll(
|
||||
@Query('categoryId') categoryId?: string,
|
||||
): Promise<AttributeDefinitionEntity[]> {
|
||||
return this.service.findAll(categoryId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string): Promise<AttributeDefinitionEntity> {
|
||||
return this.service.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(
|
||||
@Body() payload: CreateAttributeDefinitionDto,
|
||||
): Promise<AttributeDefinitionEntity> {
|
||||
return this.service.create(payload);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() payload: UpdateAttributeDefinitionDto,
|
||||
): Promise<AttributeDefinitionEntity> {
|
||||
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,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AttributeDefinitionEntity } from '../../entities/attribute-definition.entity';
|
||||
import { BusinessCategoryEntity } from '../../entities/business-category.entity';
|
||||
import { AttributeDefinitionsController } from './attribute-definitions.controller';
|
||||
import { AttributeDefinitionsService } from './attribute-definitions.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([AttributeDefinitionEntity, BusinessCategoryEntity]),
|
||||
],
|
||||
controllers: [AttributeDefinitionsController],
|
||||
providers: [AttributeDefinitionsService],
|
||||
})
|
||||
export class AttributeDefinitionsModule {}
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
AttributeDefinitionEntity,
|
||||
} from '../../entities/attribute-definition.entity';
|
||||
import { BusinessCategoryEntity } from '../../entities/business-category.entity';
|
||||
import { CreateAttributeDefinitionDto } from './dto/create-attribute-definition.dto';
|
||||
import { UpdateAttributeDefinitionDto } from './dto/update-attribute-definition.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AttributeDefinitionsService {
|
||||
constructor(
|
||||
@InjectRepository(AttributeDefinitionEntity)
|
||||
private readonly repo: Repository<AttributeDefinitionEntity>,
|
||||
@InjectRepository(BusinessCategoryEntity)
|
||||
private readonly categoryRepo: Repository<BusinessCategoryEntity>,
|
||||
) {}
|
||||
|
||||
async findAll(categoryId?: string): Promise<AttributeDefinitionEntity[]> {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('def')
|
||||
.leftJoinAndSelect('def.category', 'category')
|
||||
.orderBy('def.createdAt', 'DESC');
|
||||
|
||||
if (categoryId) {
|
||||
qb.andWhere('def.categoryId = :categoryId', { categoryId });
|
||||
}
|
||||
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<AttributeDefinitionEntity> {
|
||||
const row = await this.repo.findOne({
|
||||
where: { id },
|
||||
relations: { category: true },
|
||||
});
|
||||
if (!row) {
|
||||
throw new NotFoundException('Attribute definition not found');
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
async create(
|
||||
dto: CreateAttributeDefinitionDto,
|
||||
): Promise<AttributeDefinitionEntity> {
|
||||
await this.ensureCategoryExists(dto.categoryId);
|
||||
|
||||
const dup = await this.repo.findOne({
|
||||
where: { categoryId: dto.categoryId, key: dto.key },
|
||||
});
|
||||
if (dup) {
|
||||
throw new ConflictException('key already exists for this category');
|
||||
}
|
||||
|
||||
const entity = this.repo.create({
|
||||
title: dto.title,
|
||||
key: dto.key,
|
||||
type: dto.type,
|
||||
categoryId: dto.categoryId,
|
||||
options: dto.options ?? null,
|
||||
required: dto.required ?? false,
|
||||
isFilterable: dto.isFilterable ?? true,
|
||||
});
|
||||
|
||||
return this.repo.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
dto: UpdateAttributeDefinitionDto,
|
||||
): Promise<AttributeDefinitionEntity> {
|
||||
const row = await this.findOne(id);
|
||||
|
||||
if (dto.categoryId && dto.categoryId !== row.categoryId) {
|
||||
await this.ensureCategoryExists(dto.categoryId);
|
||||
}
|
||||
|
||||
if (dto.key !== undefined || dto.categoryId !== undefined) {
|
||||
const nextCategoryId = dto.categoryId ?? row.categoryId;
|
||||
const nextKey = dto.key ?? row.key;
|
||||
const dup = await this.repo.findOne({
|
||||
where: { categoryId: nextCategoryId, key: nextKey },
|
||||
});
|
||||
if (dup && dup.id !== row.id) {
|
||||
throw new ConflictException('key already exists for this category');
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(row, dto);
|
||||
return this.repo.save(row);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.repo.delete(id);
|
||||
if (!result.affected) {
|
||||
throw new NotFoundException('Attribute definition not found');
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureCategoryExists(categoryId: string): Promise<void> {
|
||||
const cat = await this.categoryRepo.findOne({ where: { id: categoryId } });
|
||||
if (!cat) {
|
||||
throw new NotFoundException('Business category not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AttributeType } from '../../../entities/attribute-definition.entity';
|
||||
|
||||
export class CreateAttributeDefinitionDto {
|
||||
title!: string;
|
||||
/** یکتا در محدوده همان دسته شغلی */
|
||||
key!: string;
|
||||
type!: AttributeType;
|
||||
categoryId!: string;
|
||||
/** برای type=select آرایه گزینهها */
|
||||
options?: string[];
|
||||
required?: boolean;
|
||||
isFilterable?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AttributeType } from '../../../entities/attribute-definition.entity';
|
||||
|
||||
export class UpdateAttributeDefinitionDto {
|
||||
title?: string;
|
||||
key?: string;
|
||||
type?: AttributeType;
|
||||
categoryId?: string;
|
||||
options?: string[] | null;
|
||||
required?: boolean;
|
||||
isFilterable?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { BookingEntity, BookingStatus } from '../../entities/booking.entity';
|
||||
import { BookingsService } from './bookings.service';
|
||||
import { CreateBookingDto } from './dto/create-booking.dto';
|
||||
import { UpdateBookingDto } from './dto/update-booking.dto';
|
||||
|
||||
@Controller('bookings')
|
||||
export class BookingsController {
|
||||
constructor(private readonly service: BookingsService) {}
|
||||
|
||||
@Get()
|
||||
findAll(
|
||||
@Query('customerId') customerId?: string,
|
||||
@Query('businessId') businessId?: string,
|
||||
@Query('status') status?: BookingStatus,
|
||||
): Promise<BookingEntity[]> {
|
||||
return this.service.findAll({ customerId, businessId, status });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string): Promise<BookingEntity> {
|
||||
return this.service.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() payload: CreateBookingDto): Promise<BookingEntity> {
|
||||
return this.service.create(payload);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() payload: UpdateBookingDto,
|
||||
): Promise<BookingEntity> {
|
||||
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,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BookingEntity } from '../../entities/booking.entity';
|
||||
import { BusinessServiceEntity } from '../../entities/business-service.entity';
|
||||
import { BusinessEntity } from '../../entities/business.entity';
|
||||
import { UserEntity } from '../../entities/user.entity';
|
||||
import { BookingsController } from './bookings.controller';
|
||||
import { BookingsService } from './bookings.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
BookingEntity,
|
||||
UserEntity,
|
||||
BusinessEntity,
|
||||
BusinessServiceEntity,
|
||||
]),
|
||||
],
|
||||
controllers: [BookingsController],
|
||||
providers: [BookingsService],
|
||||
})
|
||||
export class BookingsModule {}
|
||||
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BookingEntity, BookingStatus } from '../../entities/booking.entity';
|
||||
import { BusinessServiceEntity } from '../../entities/business-service.entity';
|
||||
import { BusinessEntity } from '../../entities/business.entity';
|
||||
import { UserEntity } from '../../entities/user.entity';
|
||||
import { CreateBookingDto } from './dto/create-booking.dto';
|
||||
import { UpdateBookingDto } from './dto/update-booking.dto';
|
||||
|
||||
type FindAllFilters = {
|
||||
customerId?: string;
|
||||
businessId?: string;
|
||||
status?: BookingStatus;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class BookingsService {
|
||||
private readonly minLeadMinutes: number;
|
||||
|
||||
constructor(
|
||||
private readonly config: ConfigService,
|
||||
@InjectRepository(BookingEntity)
|
||||
private readonly repo: Repository<BookingEntity>,
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userRepo: Repository<UserEntity>,
|
||||
@InjectRepository(BusinessEntity)
|
||||
private readonly businessRepo: Repository<BusinessEntity>,
|
||||
@InjectRepository(BusinessServiceEntity)
|
||||
private readonly businessServiceRepo: Repository<BusinessServiceEntity>,
|
||||
) {
|
||||
const raw = this.config.get<string | undefined>('BOOKING_MIN_LEAD_MINUTES');
|
||||
const parsed = raw !== undefined ? Number.parseInt(String(raw), 10) : NaN;
|
||||
this.minLeadMinutes = Number.isFinite(parsed) && parsed >= 0 ? parsed : 60;
|
||||
}
|
||||
|
||||
async findAll(filters: FindAllFilters): Promise<BookingEntity[]> {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('b')
|
||||
.leftJoinAndSelect('b.customer', 'customer')
|
||||
.leftJoinAndSelect('b.business', 'business')
|
||||
.leftJoinAndSelect('b.businessService', 'businessService')
|
||||
.leftJoinAndSelect('businessService.serviceTemplate', 'template')
|
||||
.orderBy('b.scheduledAt', 'ASC');
|
||||
|
||||
if (filters.customerId) {
|
||||
qb.andWhere('b.customerId = :customerId', {
|
||||
customerId: filters.customerId,
|
||||
});
|
||||
}
|
||||
if (filters.businessId) {
|
||||
qb.andWhere('b.businessId = :businessId', {
|
||||
businessId: filters.businessId,
|
||||
});
|
||||
}
|
||||
if (filters.status) {
|
||||
qb.andWhere('b.status = :status', { status: filters.status });
|
||||
}
|
||||
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<BookingEntity> {
|
||||
const row = await this.repo.findOne({
|
||||
where: { id },
|
||||
relations: {
|
||||
customer: true,
|
||||
business: true,
|
||||
businessService: { serviceTemplate: true },
|
||||
},
|
||||
});
|
||||
if (!row) {
|
||||
throw new NotFoundException('Booking not found');
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
async create(dto: CreateBookingDto): Promise<BookingEntity> {
|
||||
const customer = await this.userRepo.findOne({
|
||||
where: { id: dto.customerId },
|
||||
});
|
||||
if (!customer) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const business = await this.businessRepo.findOne({
|
||||
where: { id: dto.businessId },
|
||||
});
|
||||
if (!business) {
|
||||
throw new NotFoundException('Business not found');
|
||||
}
|
||||
|
||||
const businessService = await this.businessServiceRepo.findOne({
|
||||
where: { id: dto.businessServiceId },
|
||||
relations: { business: true },
|
||||
});
|
||||
if (!businessService) {
|
||||
throw new NotFoundException('Business service not found');
|
||||
}
|
||||
|
||||
if (businessService.businessId !== dto.businessId) {
|
||||
throw new BadRequestException(
|
||||
'businessService does not belong to the given business',
|
||||
);
|
||||
}
|
||||
|
||||
const scheduledAt = this.parseScheduledAt(dto.scheduledAt);
|
||||
this.assertLeadTime(scheduledAt);
|
||||
|
||||
const entity = this.repo.create({
|
||||
customerId: dto.customerId,
|
||||
businessId: dto.businessId,
|
||||
businessServiceId: dto.businessServiceId,
|
||||
scheduledAt,
|
||||
note: dto.note ?? null,
|
||||
status: BookingStatus.PENDING,
|
||||
});
|
||||
|
||||
return this.repo.save(entity);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateBookingDto): Promise<BookingEntity> {
|
||||
const row = await this.findOne(id);
|
||||
|
||||
if (dto.scheduledAt !== undefined) {
|
||||
const scheduledAt = this.parseScheduledAt(dto.scheduledAt);
|
||||
this.assertLeadTime(scheduledAt);
|
||||
row.scheduledAt = scheduledAt;
|
||||
}
|
||||
|
||||
if (dto.status !== undefined) {
|
||||
row.status = dto.status;
|
||||
}
|
||||
|
||||
if (dto.note !== undefined) {
|
||||
row.note = dto.note;
|
||||
}
|
||||
|
||||
return this.repo.save(row);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.repo.delete(id);
|
||||
if (!result.affected) {
|
||||
throw new NotFoundException('Booking not found');
|
||||
}
|
||||
}
|
||||
|
||||
private parseScheduledAt(raw: string): Date {
|
||||
const d = new Date(raw);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
throw new BadRequestException('scheduledAt must be a valid ISO datetime');
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private assertLeadTime(scheduledAt: Date): void {
|
||||
const minMs = this.minLeadMinutes * 60 * 1000;
|
||||
const earliest = Date.now() + minMs;
|
||||
if (scheduledAt.getTime() < earliest) {
|
||||
throw new BadRequestException(
|
||||
`scheduledAt must be at least ${this.minLeadMinutes} minute(s) after the current time`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export class CreateBookingDto {
|
||||
customerId!: string;
|
||||
businessId!: string;
|
||||
businessServiceId!: string;
|
||||
/** ISO 8601 datetime string */
|
||||
scheduledAt!: string;
|
||||
note?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BookingStatus } from '../../../entities/booking.entity';
|
||||
|
||||
export class UpdateBookingDto {
|
||||
scheduledAt?: string;
|
||||
status?: BookingStatus;
|
||||
note?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { BusinessAttributeValueEntity } from '../../entities/business-attribute-value.entity';
|
||||
import { BusinessAttributeValuesService } from './business-attribute-values.service';
|
||||
import { CreateBusinessAttributeValueDto } from './dto/create-business-attribute-value.dto';
|
||||
import { UpdateBusinessAttributeValueDto } from './dto/update-business-attribute-value.dto';
|
||||
|
||||
@Controller('business-attribute-values')
|
||||
export class BusinessAttributeValuesController {
|
||||
constructor(private readonly service: BusinessAttributeValuesService) {}
|
||||
|
||||
@Get()
|
||||
findAll(
|
||||
@Query('businessId') businessId?: string,
|
||||
): Promise<BusinessAttributeValueEntity[]> {
|
||||
return this.service.findAll(businessId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string): Promise<BusinessAttributeValueEntity> {
|
||||
return this.service.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(
|
||||
@Body() payload: CreateBusinessAttributeValueDto,
|
||||
): Promise<BusinessAttributeValueEntity> {
|
||||
return this.service.create(payload);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() payload: UpdateBusinessAttributeValueDto,
|
||||
): Promise<BusinessAttributeValueEntity> {
|
||||
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 { AttributeDefinitionEntity } from '../../entities/attribute-definition.entity';
|
||||
import { BusinessAttributeValueEntity } from '../../entities/business-attribute-value.entity';
|
||||
import { BusinessEntity } from '../../entities/business.entity';
|
||||
import { BusinessAttributeValuesController } from './business-attribute-values.controller';
|
||||
import { BusinessAttributeValuesService } from './business-attribute-values.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
BusinessAttributeValueEntity,
|
||||
BusinessEntity,
|
||||
AttributeDefinitionEntity,
|
||||
]),
|
||||
],
|
||||
controllers: [BusinessAttributeValuesController],
|
||||
providers: [BusinessAttributeValuesService],
|
||||
})
|
||||
export class BusinessAttributeValuesModule {}
|
||||
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
AttributeDefinitionEntity,
|
||||
AttributeType,
|
||||
} from '../../entities/attribute-definition.entity';
|
||||
import { BusinessAttributeValueEntity } from '../../entities/business-attribute-value.entity';
|
||||
import { BusinessEntity } from '../../entities/business.entity';
|
||||
import { CreateBusinessAttributeValueDto } from './dto/create-business-attribute-value.dto';
|
||||
import { UpdateBusinessAttributeValueDto } from './dto/update-business-attribute-value.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BusinessAttributeValuesService {
|
||||
constructor(
|
||||
@InjectRepository(BusinessAttributeValueEntity)
|
||||
private readonly repo: Repository<BusinessAttributeValueEntity>,
|
||||
@InjectRepository(BusinessEntity)
|
||||
private readonly businessRepo: Repository<BusinessEntity>,
|
||||
@InjectRepository(AttributeDefinitionEntity)
|
||||
private readonly defRepo: Repository<AttributeDefinitionEntity>,
|
||||
) {}
|
||||
|
||||
async findAll(businessId?: string): Promise<BusinessAttributeValueEntity[]> {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('v')
|
||||
.leftJoinAndSelect('v.business', 'business')
|
||||
.leftJoinAndSelect('v.attributeDefinition', 'def')
|
||||
.orderBy('v.createdAt', 'DESC');
|
||||
|
||||
if (businessId) {
|
||||
qb.andWhere('v.businessId = :businessId', { businessId });
|
||||
}
|
||||
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<BusinessAttributeValueEntity> {
|
||||
const row = await this.repo.findOne({
|
||||
where: { id },
|
||||
relations: { business: true, attributeDefinition: true },
|
||||
});
|
||||
if (!row) {
|
||||
throw new NotFoundException('Business attribute value not found');
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
async create(
|
||||
dto: CreateBusinessAttributeValueDto,
|
||||
): Promise<BusinessAttributeValueEntity> {
|
||||
const business = await this.businessRepo.findOne({
|
||||
where: { id: dto.businessId },
|
||||
});
|
||||
if (!business) {
|
||||
throw new NotFoundException('Business not found');
|
||||
}
|
||||
|
||||
const def = await this.defRepo.findOne({
|
||||
where: { id: dto.attributeDefinitionId },
|
||||
});
|
||||
if (!def) {
|
||||
throw new NotFoundException('Attribute definition not found');
|
||||
}
|
||||
|
||||
if (def.categoryId !== business.categoryId) {
|
||||
throw new BadRequestException(
|
||||
'Attribute definition must belong to the same category as the business',
|
||||
);
|
||||
}
|
||||
|
||||
const dup = await this.repo.findOne({
|
||||
where: {
|
||||
businessId: dto.businessId,
|
||||
attributeDefinitionId: dto.attributeDefinitionId,
|
||||
},
|
||||
});
|
||||
if (dup) {
|
||||
throw new ConflictException(
|
||||
'This attribute is already set for this business; use PATCH instead',
|
||||
);
|
||||
}
|
||||
|
||||
const normalized = this.normalizeAndValidateValue(def, dto.value);
|
||||
|
||||
const entity = this.repo.create({
|
||||
businessId: dto.businessId,
|
||||
attributeDefinitionId: dto.attributeDefinitionId,
|
||||
value: normalized,
|
||||
});
|
||||
|
||||
return this.repo.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
dto: UpdateBusinessAttributeValueDto,
|
||||
): Promise<BusinessAttributeValueEntity> {
|
||||
const row = await this.findOne(id);
|
||||
|
||||
const def =
|
||||
row.attributeDefinition ??
|
||||
(await this.defRepo.findOne({
|
||||
where: { id: row.attributeDefinitionId },
|
||||
}));
|
||||
|
||||
if (!def) {
|
||||
throw new NotFoundException('Attribute definition not found');
|
||||
}
|
||||
|
||||
if (dto.value !== undefined) {
|
||||
row.value = this.normalizeAndValidateValue(def, dto.value);
|
||||
}
|
||||
|
||||
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 attribute value not found');
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeAndValidateValue(
|
||||
def: AttributeDefinitionEntity,
|
||||
raw: string | null | undefined,
|
||||
): string | null {
|
||||
const empty =
|
||||
raw === undefined || raw === null || String(raw).trim() === '';
|
||||
|
||||
if (empty) {
|
||||
if (def.required) {
|
||||
throw new BadRequestException(`value required for attribute "${def.key}"`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const s = String(raw).trim();
|
||||
|
||||
switch (def.type) {
|
||||
case AttributeType.BOOLEAN: {
|
||||
const low = s.toLowerCase();
|
||||
if (!['true', 'false', '1', '0'].includes(low)) {
|
||||
throw new BadRequestException(
|
||||
`invalid boolean for "${def.key}" (use true/false)`,
|
||||
);
|
||||
}
|
||||
return low === 'true' || low === '1' ? 'true' : 'false';
|
||||
}
|
||||
case AttributeType.NUMBER: {
|
||||
const n = Number(s);
|
||||
if (!Number.isFinite(n)) {
|
||||
throw new BadRequestException(`invalid number for "${def.key}"`);
|
||||
}
|
||||
return String(n);
|
||||
}
|
||||
case AttributeType.SELECT: {
|
||||
const opts = def.options ?? [];
|
||||
if (!opts.includes(s)) {
|
||||
throw new BadRequestException(
|
||||
`value must be one of options for "${def.key}"`,
|
||||
);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
case AttributeType.TEXT:
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class CreateBusinessAttributeValueDto {
|
||||
businessId!: string;
|
||||
attributeDefinitionId!: string;
|
||||
/** مقدار به صورت رشته ذخیره میشود؛ برای boolean مثلا true/false */
|
||||
value?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class UpdateBusinessAttributeValueDto {
|
||||
value?: string | null;
|
||||
}
|
||||
Reference in New Issue
Block a user