diff --git a/src/app.module.ts b/src/app.module.ts index 716af22..4776993 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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], diff --git a/src/modules/attribute-definitions/attribute-definitions.controller.ts b/src/modules/attribute-definitions/attribute-definitions.controller.ts new file mode 100644 index 0000000..1707cc6 --- /dev/null +++ b/src/modules/attribute-definitions/attribute-definitions.controller.ts @@ -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 { + return this.service.findAll(categoryId); + } + + @Get(':id') + findOne(@Param('id') id: string): Promise { + return this.service.findOne(id); + } + + @Post() + create( + @Body() payload: CreateAttributeDefinitionDto, + ): Promise { + return this.service.create(payload); + } + + @Patch(':id') + update( + @Param('id') id: string, + @Body() payload: UpdateAttributeDefinitionDto, + ): Promise { + 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 }; + } +} diff --git a/src/modules/attribute-definitions/attribute-definitions.module.ts b/src/modules/attribute-definitions/attribute-definitions.module.ts new file mode 100644 index 0000000..2d58e39 --- /dev/null +++ b/src/modules/attribute-definitions/attribute-definitions.module.ts @@ -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 {} diff --git a/src/modules/attribute-definitions/attribute-definitions.service.ts b/src/modules/attribute-definitions/attribute-definitions.service.ts new file mode 100644 index 0000000..30e12fc --- /dev/null +++ b/src/modules/attribute-definitions/attribute-definitions.service.ts @@ -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, + @InjectRepository(BusinessCategoryEntity) + private readonly categoryRepo: Repository, + ) {} + + async findAll(categoryId?: string): Promise { + 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 { + 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 { + 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 { + 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 { + const result = await this.repo.delete(id); + if (!result.affected) { + throw new NotFoundException('Attribute definition not found'); + } + } + + private async ensureCategoryExists(categoryId: string): Promise { + const cat = await this.categoryRepo.findOne({ where: { id: categoryId } }); + if (!cat) { + throw new NotFoundException('Business category not found'); + } + } +} diff --git a/src/modules/attribute-definitions/dto/create-attribute-definition.dto.ts b/src/modules/attribute-definitions/dto/create-attribute-definition.dto.ts new file mode 100644 index 0000000..6115c62 --- /dev/null +++ b/src/modules/attribute-definitions/dto/create-attribute-definition.dto.ts @@ -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; +} diff --git a/src/modules/attribute-definitions/dto/update-attribute-definition.dto.ts b/src/modules/attribute-definitions/dto/update-attribute-definition.dto.ts new file mode 100644 index 0000000..06d4cc6 --- /dev/null +++ b/src/modules/attribute-definitions/dto/update-attribute-definition.dto.ts @@ -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; +} diff --git a/src/modules/bookings/bookings.controller.ts b/src/modules/bookings/bookings.controller.ts new file mode 100644 index 0000000..729f717 --- /dev/null +++ b/src/modules/bookings/bookings.controller.ts @@ -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 { + return this.service.findAll({ customerId, businessId, status }); + } + + @Get(':id') + findOne(@Param('id') id: string): Promise { + return this.service.findOne(id); + } + + @Post() + create(@Body() payload: CreateBookingDto): Promise { + return this.service.create(payload); + } + + @Patch(':id') + update( + @Param('id') id: string, + @Body() payload: UpdateBookingDto, + ): Promise { + 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 }; + } +} diff --git a/src/modules/bookings/bookings.module.ts b/src/modules/bookings/bookings.module.ts new file mode 100644 index 0000000..37465f6 --- /dev/null +++ b/src/modules/bookings/bookings.module.ts @@ -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 {} diff --git a/src/modules/bookings/bookings.service.ts b/src/modules/bookings/bookings.service.ts new file mode 100644 index 0000000..7364f65 --- /dev/null +++ b/src/modules/bookings/bookings.service.ts @@ -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, + @InjectRepository(UserEntity) + private readonly userRepo: Repository, + @InjectRepository(BusinessEntity) + private readonly businessRepo: Repository, + @InjectRepository(BusinessServiceEntity) + private readonly businessServiceRepo: Repository, + ) { + const raw = this.config.get('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 { + 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 { + 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 { + 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 { + 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 { + 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`, + ); + } + } +} diff --git a/src/modules/bookings/dto/create-booking.dto.ts b/src/modules/bookings/dto/create-booking.dto.ts new file mode 100644 index 0000000..992303c --- /dev/null +++ b/src/modules/bookings/dto/create-booking.dto.ts @@ -0,0 +1,8 @@ +export class CreateBookingDto { + customerId!: string; + businessId!: string; + businessServiceId!: string; + /** ISO 8601 datetime string */ + scheduledAt!: string; + note?: string | null; +} diff --git a/src/modules/bookings/dto/update-booking.dto.ts b/src/modules/bookings/dto/update-booking.dto.ts new file mode 100644 index 0000000..965e62e --- /dev/null +++ b/src/modules/bookings/dto/update-booking.dto.ts @@ -0,0 +1,7 @@ +import { BookingStatus } from '../../../entities/booking.entity'; + +export class UpdateBookingDto { + scheduledAt?: string; + status?: BookingStatus; + note?: string | null; +} diff --git a/src/modules/business-attribute-values/business-attribute-values.controller.ts b/src/modules/business-attribute-values/business-attribute-values.controller.ts new file mode 100644 index 0000000..3107ee1 --- /dev/null +++ b/src/modules/business-attribute-values/business-attribute-values.controller.ts @@ -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 { + return this.service.findAll(businessId); + } + + @Get(':id') + findOne(@Param('id') id: string): Promise { + return this.service.findOne(id); + } + + @Post() + create( + @Body() payload: CreateBusinessAttributeValueDto, + ): Promise { + return this.service.create(payload); + } + + @Patch(':id') + update( + @Param('id') id: string, + @Body() payload: UpdateBusinessAttributeValueDto, + ): Promise { + 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 }; + } +} diff --git a/src/modules/business-attribute-values/business-attribute-values.module.ts b/src/modules/business-attribute-values/business-attribute-values.module.ts new file mode 100644 index 0000000..83fe0bf --- /dev/null +++ b/src/modules/business-attribute-values/business-attribute-values.module.ts @@ -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 {} diff --git a/src/modules/business-attribute-values/business-attribute-values.service.ts b/src/modules/business-attribute-values/business-attribute-values.service.ts new file mode 100644 index 0000000..0564b3d --- /dev/null +++ b/src/modules/business-attribute-values/business-attribute-values.service.ts @@ -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, + @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; + } + } +} diff --git a/src/modules/business-attribute-values/dto/create-business-attribute-value.dto.ts b/src/modules/business-attribute-values/dto/create-business-attribute-value.dto.ts new file mode 100644 index 0000000..589fad4 --- /dev/null +++ b/src/modules/business-attribute-values/dto/create-business-attribute-value.dto.ts @@ -0,0 +1,6 @@ +export class CreateBusinessAttributeValueDto { + businessId!: string; + attributeDefinitionId!: string; + /** مقدار به صورت رشته ذخیره می‌شود؛ برای boolean مثلا true/false */ + value?: string | null; +} diff --git a/src/modules/business-attribute-values/dto/update-business-attribute-value.dto.ts b/src/modules/business-attribute-values/dto/update-business-attribute-value.dto.ts new file mode 100644 index 0000000..8c80d34 --- /dev/null +++ b/src/modules/business-attribute-values/dto/update-business-attribute-value.dto.ts @@ -0,0 +1,3 @@ +export class UpdateBusinessAttributeValueDto { + value?: string | null; +}