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, @InjectRepository(BusinessEntity) private readonly businessRepo: Repository, @InjectRepository(AttributeDefinitionEntity) private readonly defRepo: Repository, ) {} async findAll(businessId?: string): Promise { 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 { 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 { 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 { 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 { 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; } } }