Files
serverMontime/src/modules/business-attribute-values/business-attribute-values.service.ts
T
2026-05-09 17:29:22 +03:30

178 lines
5.0 KiB
TypeScript

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;
}
}
}