add business categories CRUD module

This commit is contained in:
2026-05-07 19:11:14 +03:30
parent 53113d8108
commit 729cc9f4b9
16 changed files with 3263 additions and 1 deletions
+2668
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -1,9 +1,17 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { dataSourceOptions } from './config/typeorm.config';
import { BusinessCategoriesModule } from './modules/business-categories/business-categories.module';
@Module({
imports: [],
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRoot(dataSourceOptions),
BusinessCategoriesModule,
],
controllers: [AppController],
providers: [AppService],
})
@@ -0,0 +1,61 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { BusinessAttributeValueEntity } from './business-attribute-value.entity';
import { BusinessCategoryEntity } from './business-category.entity';
export enum AttributeType {
BOOLEAN = 'boolean',
TEXT = 'text',
NUMBER = 'number',
SELECT = 'select',
}
@Entity('attribute_definitions')
export class AttributeDefinitionEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ length: 120 })
title!: string;
@Column({ length: 120 })
key!: string;
@Column({ type: 'enum', enum: AttributeType })
type!: AttributeType;
@Column({ type: 'simple-array', nullable: true })
options!: string[] | null;
@Column({ default: false })
required!: boolean;
@Column({ default: true })
isFilterable!: boolean;
@ManyToOne(() => BusinessCategoryEntity, (category) => category.attributeDefinitions, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'categoryId' })
category!: BusinessCategoryEntity;
@Column('uuid')
categoryId!: string;
@OneToMany(() => BusinessAttributeValueEntity, (value) => value.attributeDefinition)
values!: BusinessAttributeValueEntity[];
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
+63
View File
@@ -0,0 +1,63 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { BusinessEntity } from './business.entity';
import { BusinessServiceEntity } from './business-service.entity';
import { UserEntity } from './user.entity';
export enum BookingStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
CANCELLED = 'cancelled',
COMPLETED = 'completed',
}
@Entity('bookings')
export class BookingEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@ManyToOne(() => UserEntity, (user) => user.bookings, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'customerId' })
customer!: UserEntity;
@Column('uuid')
customerId!: string;
@ManyToOne(() => BusinessEntity, (business) => business.bookings, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'businessId' })
business!: BusinessEntity;
@Column('uuid')
businessId!: string;
@ManyToOne(() => BusinessServiceEntity, (service) => service.bookings, {
onDelete: 'RESTRICT',
})
@JoinColumn({ name: 'businessServiceId' })
businessService!: BusinessServiceEntity;
@Column('uuid')
businessServiceId!: string;
@Column({ type: 'timestamptz' })
scheduledAt!: Date;
@Column({ type: 'enum', enum: BookingStatus, default: BookingStatus.PENDING })
status!: BookingStatus;
@Column({ type: 'text', nullable: true })
note!: string | null;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,46 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { AttributeDefinitionEntity } from './attribute-definition.entity';
import { BusinessEntity } from './business.entity';
@Entity('business_attribute_values')
@Unique('UQ_business_attribute_value', ['businessId', 'attributeDefinitionId'])
export class BusinessAttributeValueEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@ManyToOne(() => BusinessEntity, (business) => business.attributes, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'businessId' })
business!: BusinessEntity;
@Column('uuid')
businessId!: string;
@ManyToOne(() => AttributeDefinitionEntity, (definition) => definition.values, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'attributeDefinitionId' })
attributeDefinition!: AttributeDefinitionEntity;
@Column('uuid')
attributeDefinitionId!: string;
@Column({ type: 'text', nullable: true })
value!: string | null;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
+41
View File
@@ -0,0 +1,41 @@
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { AttributeDefinitionEntity } from './attribute-definition.entity';
import { BusinessEntity } from './business.entity';
import { ServiceTemplateEntity } from './service-template.entity';
@Entity('business_categories')
export class BusinessCategoryEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true, length: 100 })
slug!: string;
@Column({ length: 150 })
title!: string;
@Column({ default: true })
isActive!: boolean;
@OneToMany(() => BusinessEntity, (business) => business.category)
businesses!: BusinessEntity[];
@OneToMany(() => ServiceTemplateEntity, (service) => service.category)
serviceTemplates!: ServiceTemplateEntity[];
@OneToMany(() => AttributeDefinitionEntity, (attribute) => attribute.category)
attributeDefinitions!: AttributeDefinitionEntity[];
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
+57
View File
@@ -0,0 +1,57 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { BookingEntity } from './booking.entity';
import { BusinessEntity } from './business.entity';
import { ServiceTemplateEntity } from './service-template.entity';
@Entity('business_services')
@Unique('UQ_business_service', ['businessId', 'serviceTemplateId'])
export class BusinessServiceEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@ManyToOne(() => BusinessEntity, (business) => business.services, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'businessId' })
business!: BusinessEntity;
@Column('uuid')
businessId!: string;
@ManyToOne(() => ServiceTemplateEntity, (serviceTemplate) => serviceTemplate.businessServices, {
onDelete: 'RESTRICT',
})
@JoinColumn({ name: 'serviceTemplateId' })
serviceTemplate!: ServiceTemplateEntity;
@Column('uuid')
serviceTemplateId!: string;
@Column({ type: 'numeric', precision: 12, scale: 2 })
price!: string;
@Column({ type: 'int', nullable: true })
durationMinutes!: number | null;
@Column({ default: true })
isActive!: boolean;
@OneToMany(() => BookingEntity, (booking) => booking.businessService)
bookings!: BookingEntity[];
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
+79
View File
@@ -0,0 +1,79 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { BookingEntity } from './booking.entity';
import { BusinessAttributeValueEntity } from './business-attribute-value.entity';
import { BusinessCategoryEntity } from './business-category.entity';
import { BusinessServiceEntity } from './business-service.entity';
import { UserEntity } from './user.entity';
@Entity('businesses')
export class BusinessEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ length: 150 })
title!: string;
@Column({ unique: true, length: 120 })
slug!: string;
@Column({ nullable: true, length: 500 })
description!: string | null;
@Column({ nullable: true, length: 20 })
contactPhone!: string | null;
@Column({ nullable: true, length: 500 })
address!: string | null;
@Column({ type: 'double precision', nullable: true })
latitude!: number | null;
@Column({ type: 'double precision', nullable: true })
longitude!: number | null;
@Column({ default: true })
isActive!: boolean;
@ManyToOne(() => BusinessCategoryEntity, (category) => category.businesses, {
onDelete: 'RESTRICT',
})
@JoinColumn({ name: 'categoryId' })
category!: BusinessCategoryEntity;
@Column('uuid')
categoryId!: string;
@ManyToOne(() => UserEntity, (user) => user.ownedBusinesses, {
nullable: true,
onDelete: 'SET NULL',
})
@JoinColumn({ name: 'ownerId' })
owner!: UserEntity | null;
@Column({ type: 'uuid', nullable: true })
ownerId!: string | null;
@OneToMany(() => BusinessServiceEntity, (service) => service.business)
services!: BusinessServiceEntity[];
@OneToMany(() => BusinessAttributeValueEntity, (attr) => attr.business)
attributes!: BusinessAttributeValueEntity[];
@OneToMany(() => BookingEntity, (booking) => booking.business)
bookings!: BookingEntity[];
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
+51
View File
@@ -0,0 +1,51 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { BusinessCategoryEntity } from './business-category.entity';
import { BusinessServiceEntity } from './business-service.entity';
@Entity('service_templates')
export class ServiceTemplateEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ length: 120 })
title!: string;
@Column({ unique: true, length: 120 })
slug!: string;
@Column({ nullable: true, length: 400 })
description!: string | null;
@Column({ type: 'int', default: 30 })
defaultDurationMinutes!: number;
@Column({ default: true })
isActive!: boolean;
@ManyToOne(() => BusinessCategoryEntity, (category) => category.serviceTemplates, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'categoryId' })
category!: BusinessCategoryEntity;
@Column('uuid')
categoryId!: string;
@OneToMany(() => BusinessServiceEntity, (service) => service.serviceTemplate)
businessServices!: BusinessServiceEntity[];
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
+46
View File
@@ -0,0 +1,46 @@
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { BookingEntity } from './booking.entity';
import { BusinessEntity } from './business.entity';
export enum UserRole {
CUSTOMER = 'customer',
BUSINESS_OWNER = 'business_owner',
ADMIN = 'admin',
}
@Entity('users')
export class UserEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true, length: 20 })
mobile!: string;
@Column({ nullable: true, length: 150 })
fullName!: string | null;
@Column({ type: 'enum', enum: UserRole, default: UserRole.CUSTOMER })
role!: UserRole;
@Column({ default: true })
isActive!: boolean;
@OneToMany(() => BusinessEntity, (business) => business.owner)
ownedBusinesses!: BusinessEntity[];
@OneToMany(() => BookingEntity, (booking) => booking.customer)
bookings!: BookingEntity[];
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
+1
View File
@@ -1,3 +1,4 @@
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
@@ -0,0 +1,49 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
} from '@nestjs/common';
import { BusinessCategoryEntity } from '../../entities/business-category.entity';
import { BusinessCategoriesService } from './business-categories.service';
import { CreateBusinessCategoryDto } from './dto/create-business-category.dto';
import { UpdateBusinessCategoryDto } from './dto/update-business-category.dto';
@Controller('business-categories')
export class BusinessCategoriesController {
constructor(private readonly service: BusinessCategoriesService) {}
@Get()
findAll(): Promise<BusinessCategoryEntity[]> {
return this.service.findAll();
}
@Get(':id')
findOne(@Param('id') id: string): Promise<BusinessCategoryEntity> {
return this.service.findOne(id);
}
@Post()
create(
@Body() payload: CreateBusinessCategoryDto,
): Promise<BusinessCategoryEntity> {
return this.service.create(payload);
}
@Patch(':id')
update(
@Param('id') id: string,
@Body() payload: UpdateBusinessCategoryDto,
): Promise<BusinessCategoryEntity> {
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,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BusinessCategoryEntity } from '../../entities/business-category.entity';
import { BusinessCategoriesController } from './business-categories.controller';
import { BusinessCategoriesService } from './business-categories.service';
@Module({
imports: [TypeOrmModule.forFeature([BusinessCategoryEntity])],
controllers: [BusinessCategoriesController],
providers: [BusinessCategoriesService],
})
export class BusinessCategoriesModule {}
@@ -0,0 +1,70 @@
import {
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BusinessCategoryEntity } from '../../entities/business-category.entity';
import { CreateBusinessCategoryDto } from './dto/create-business-category.dto';
import { UpdateBusinessCategoryDto } from './dto/update-business-category.dto';
@Injectable()
export class BusinessCategoriesService {
constructor(
@InjectRepository(BusinessCategoryEntity)
private readonly repo: Repository<BusinessCategoryEntity>,
) {}
findAll(): Promise<BusinessCategoryEntity[]> {
return this.repo.find({ order: { createdAt: 'DESC' } });
}
async findOne(id: string): Promise<BusinessCategoryEntity> {
const category = await this.repo.findOne({ where: { id } });
if (!category) {
throw new NotFoundException('Business category not found');
}
return category;
}
async create(
payload: CreateBusinessCategoryDto,
): Promise<BusinessCategoryEntity> {
const exists = await this.repo.findOne({ where: { slug: payload.slug } });
if (exists) {
throw new ConflictException('Slug already exists');
}
const entity = this.repo.create({
title: payload.title,
slug: payload.slug,
isActive: payload.isActive ?? true,
});
return this.repo.save(entity);
}
async update(
id: string,
payload: UpdateBusinessCategoryDto,
): Promise<BusinessCategoryEntity> {
const category = await this.findOne(id);
if (payload.slug && payload.slug !== category.slug) {
const slugTaken = await this.repo.findOne({ where: { slug: payload.slug } });
if (slugTaken) {
throw new ConflictException('Slug already exists');
}
}
Object.assign(category, payload);
return this.repo.save(category);
}
async remove(id: string): Promise<void> {
const result = await this.repo.delete(id);
if (!result.affected) {
throw new NotFoundException('Business category not found');
}
}
}
@@ -0,0 +1,5 @@
export class CreateBusinessCategoryDto {
title!: string;
slug!: string;
isActive?: boolean;
}
@@ -0,0 +1,5 @@
export class UpdateBusinessCategoryDto {
title?: string;
slug?: string;
isActive?: boolean;
}