update suggestion
This commit is contained in:
@@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
MigrationInterface,
|
||||||
|
QueryRunner,
|
||||||
|
TableColumn,
|
||||||
|
TableForeignKey,
|
||||||
|
TableIndex,
|
||||||
|
TableUnique,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
export class MoveSuggestionsToServiceSkill1720900000000
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'MoveSuggestionsToServiceSkill1720900000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const table = await queryRunner.getTable('service_suggestions');
|
||||||
|
|
||||||
|
const serviceFk = table?.foreignKeys.find((fk) =>
|
||||||
|
fk.columnNames.includes('serviceId'),
|
||||||
|
);
|
||||||
|
if (serviceFk) {
|
||||||
|
await queryRunner.dropForeignKey('service_suggestions', serviceFk);
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestedServiceFk = table?.foreignKeys.find((fk) =>
|
||||||
|
fk.columnNames.includes('suggestedServiceId'),
|
||||||
|
);
|
||||||
|
if (suggestedServiceFk) {
|
||||||
|
await queryRunner.dropForeignKey('service_suggestions', suggestedServiceFk);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueConstraints = table?.uniques ?? [];
|
||||||
|
for (const unique of uniqueConstraints) {
|
||||||
|
await queryRunner.dropUniqueConstraint('service_suggestions', unique);
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexes = table?.indices ?? [];
|
||||||
|
for (const index of indexes) {
|
||||||
|
if (index.columnNames.includes('serviceId') || index.columnNames.includes('serviceSkillId')) {
|
||||||
|
await queryRunner.dropIndex('service_suggestions', index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table?.findColumnByName('serviceId')) {
|
||||||
|
await queryRunner.dropColumn('service_suggestions', 'serviceId');
|
||||||
|
}
|
||||||
|
if (table?.findColumnByName('suggestedServiceId')) {
|
||||||
|
await queryRunner.dropColumn('service_suggestions', 'suggestedServiceId');
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshedTable = await queryRunner.getTable('service_suggestions');
|
||||||
|
|
||||||
|
if (!refreshedTable?.findColumnByName('serviceSkillId')) {
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableColumn({
|
||||||
|
name: 'serviceSkillId',
|
||||||
|
type: 'uuid',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!refreshedTable?.findColumnByName('suggestedServiceSkillId')) {
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableColumn({
|
||||||
|
name: 'suggestedServiceSkillId',
|
||||||
|
type: 'uuid',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['serviceSkillId'],
|
||||||
|
referencedTableName: 'service_skills',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['suggestedServiceSkillId'],
|
||||||
|
referencedTableName: 'service_skills',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createUniqueConstraint(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableUnique({
|
||||||
|
name: 'UQ_service_suggestions_serviceSkillId_suggestedServiceSkillId',
|
||||||
|
columnNames: ['serviceSkillId', 'suggestedServiceSkillId'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableIndex({
|
||||||
|
name: 'IDX_service_suggestions_serviceSkillId_isActive_displayPriority',
|
||||||
|
columnNames: ['serviceSkillId', 'isActive', 'displayPriority'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const table = await queryRunner.getTable('service_suggestions');
|
||||||
|
|
||||||
|
for (const fk of table?.foreignKeys ?? []) {
|
||||||
|
if (
|
||||||
|
fk.columnNames.includes('serviceSkillId') ||
|
||||||
|
fk.columnNames.includes('suggestedServiceSkillId')
|
||||||
|
) {
|
||||||
|
await queryRunner.dropForeignKey('service_suggestions', fk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.dropUniqueConstraint(
|
||||||
|
'service_suggestions',
|
||||||
|
'UQ_service_suggestions_serviceSkillId_suggestedServiceSkillId',
|
||||||
|
);
|
||||||
|
await queryRunner.dropIndex(
|
||||||
|
'service_suggestions',
|
||||||
|
'IDX_service_suggestions_serviceSkillId_isActive_displayPriority',
|
||||||
|
);
|
||||||
|
await queryRunner.dropColumn('service_suggestions', 'serviceSkillId');
|
||||||
|
await queryRunner.dropColumn('service_suggestions', 'suggestedServiceSkillId');
|
||||||
|
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableColumn({ name: 'serviceId', type: 'uuid' }),
|
||||||
|
);
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableColumn({ name: 'suggestedServiceId', type: 'uuid' }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['serviceId'],
|
||||||
|
referencedTableName: 'services',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['suggestedServiceId'],
|
||||||
|
referencedTableName: 'services',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createUniqueConstraint(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableUnique({
|
||||||
|
name: 'UQ_service_suggestions_serviceId_suggestedServiceId',
|
||||||
|
columnNames: ['serviceId', 'suggestedServiceId'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'service_suggestions',
|
||||||
|
new TableIndex({
|
||||||
|
name: 'IDX_service_suggestions_serviceId_isActive_displayPriority',
|
||||||
|
columnNames: ['serviceId', 'isActive', 'displayPriority'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUUID,
|
||||||
|
MaxLength,
|
||||||
|
Min,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { SuggestionType } from '../enums/suggestion-type.enum';
|
||||||
|
|
||||||
|
export class CreateBulkServiceSuggestionItemDto {
|
||||||
|
@IsUUID()
|
||||||
|
suggestedServiceSkillId: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(100)
|
||||||
|
badgeTitle: string;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
displayPriority: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(500)
|
||||||
|
shortDescription?: string;
|
||||||
|
|
||||||
|
@IsEnum(SuggestionType)
|
||||||
|
type: SuggestionType;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
ArrayMinSize,
|
||||||
|
IsArray,
|
||||||
|
IsUUID,
|
||||||
|
ValidateNested,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { CreateBulkServiceSuggestionItemDto } from './create-bulk-service-suggestion-item.dto';
|
||||||
|
|
||||||
|
export class CreateBulkServiceSuggestionsDto {
|
||||||
|
@IsUUID()
|
||||||
|
serviceSkillId: string;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => CreateBulkServiceSuggestionItemDto)
|
||||||
|
suggestions: CreateBulkServiceSuggestionItemDto[];
|
||||||
|
}
|
||||||
@@ -13,10 +13,10 @@ import { SuggestionType } from '../enums/suggestion-type.enum';
|
|||||||
|
|
||||||
export class CreateServiceSuggestionDto {
|
export class CreateServiceSuggestionDto {
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
serviceId: string;
|
serviceSkillId: string;
|
||||||
|
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
suggestedServiceId: string;
|
suggestedServiceSkillId: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { IsBoolean, IsOptional, IsUUID } from 'class-validator';
|
|||||||
export class FindSuggestionsQueryDto {
|
export class FindSuggestionsQueryDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
serviceId?: string;
|
serviceSkillId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => value === 'true' || value === true)
|
@Transform(({ value }) => value === 'true' || value === true)
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import { SuggestionType } from '../enums/suggestion-type.enum';
|
|||||||
export class UpdateServiceSuggestionDto {
|
export class UpdateServiceSuggestionDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
serviceId?: string;
|
serviceSkillId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
suggestedServiceId?: string;
|
suggestedServiceSkillId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
@@ -9,29 +9,29 @@ import {
|
|||||||
Unique,
|
Unique,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Service } from '../../services/entities/service.entity';
|
import { ServiceSkill } from '../../services/entities/service-skill.entity';
|
||||||
import { SuggestionType } from '../enums/suggestion-type.enum';
|
import { SuggestionType } from '../enums/suggestion-type.enum';
|
||||||
|
|
||||||
@Entity('service_suggestions')
|
@Entity('service_suggestions')
|
||||||
@Unique(['serviceId', 'suggestedServiceId'])
|
@Unique(['serviceSkillId', 'suggestedServiceSkillId'])
|
||||||
@Index(['serviceId', 'isActive', 'displayPriority'])
|
@Index(['serviceSkillId', 'isActive', 'displayPriority'])
|
||||||
export class ServiceSuggestion {
|
export class ServiceSuggestion {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
serviceId: string;
|
serviceSkillId: string;
|
||||||
|
|
||||||
@ManyToOne(() => Service, { onDelete: 'CASCADE' })
|
@ManyToOne(() => ServiceSkill, { onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'serviceId' })
|
@JoinColumn({ name: 'serviceSkillId' })
|
||||||
service: Service;
|
serviceSkill: ServiceSkill;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
suggestedServiceId: string;
|
suggestedServiceSkillId: string;
|
||||||
|
|
||||||
@ManyToOne(() => Service, { onDelete: 'CASCADE' })
|
@ManyToOne(() => ServiceSkill, { onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'suggestedServiceId' })
|
@JoinColumn({ name: 'suggestedServiceSkillId' })
|
||||||
suggestedService: Service;
|
suggestedServiceSkill: ServiceSkill;
|
||||||
|
|
||||||
@Column({ default: true })
|
@Column({ default: true })
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { RolesGuard } from '../auth/guards/roles.guard';
|
|||||||
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||||
import { PermissionCode } from '../authorization/enums/permission-code.enum';
|
import { PermissionCode } from '../authorization/enums/permission-code.enum';
|
||||||
import { UserRole } from '../users/enums/user-role.enum';
|
import { UserRole } from '../users/enums/user-role.enum';
|
||||||
|
import { CreateBulkServiceSuggestionsDto } from './dto/create-bulk-service-suggestions.dto';
|
||||||
import { CreateServiceSuggestionDto } from './dto/create-service-suggestion.dto';
|
import { CreateServiceSuggestionDto } from './dto/create-service-suggestion.dto';
|
||||||
import { FindSuggestionsQueryDto } from './dto/find-suggestions-query.dto';
|
import { FindSuggestionsQueryDto } from './dto/find-suggestions-query.dto';
|
||||||
import { UpdateServiceSuggestionDto } from './dto/update-service-suggestion.dto';
|
import { UpdateServiceSuggestionDto } from './dto/update-service-suggestion.dto';
|
||||||
@@ -52,6 +53,17 @@ export class SuggestionsController {
|
|||||||
return this.suggestionsService.create(createDto, user);
|
return this.suggestionsService.create(createDto, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@Permissions(PermissionCode.SERVICES_WRITE)
|
||||||
|
@ApiStandardOkResponse(ServiceSuggestion, { isArray: true })
|
||||||
|
@Post('bulk')
|
||||||
|
createBulk(
|
||||||
|
@Body() createDto: CreateBulkServiceSuggestionsDto,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.suggestionsService.createBulk(createDto, user);
|
||||||
|
}
|
||||||
|
|
||||||
@Roles(UserRole.ADMIN)
|
@Roles(UserRole.ADMIN)
|
||||||
@Permissions(PermissionCode.SERVICES_READ)
|
@Permissions(PermissionCode.SERVICES_READ)
|
||||||
@ApiStandardOkResponse(ServiceSuggestion, { isArray: true })
|
@ApiStandardOkResponse(ServiceSuggestion, { isArray: true })
|
||||||
@@ -61,11 +73,13 @@ export class SuggestionsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@ApiPublicEndpoint('Get suggestions for a service')
|
@ApiPublicEndpoint('Get suggestions for a service skill')
|
||||||
@ApiStandardOkResponse(ServiceSuggestion, { isArray: true })
|
@ApiStandardOkResponse(ServiceSuggestion, { isArray: true })
|
||||||
@Get('for-service/:serviceId')
|
@Get('for-service-skill/:serviceSkillId')
|
||||||
findForService(@Param('serviceId', ParseUUIDPipe) serviceId: string) {
|
findForServiceSkill(
|
||||||
return this.suggestionsService.findForService(serviceId);
|
@Param('serviceSkillId', ParseUUIDPipe) serviceSkillId: string,
|
||||||
|
) {
|
||||||
|
return this.suggestionsService.findForServiceSkill(serviceSkillId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(UserRole.ADMIN)
|
@Roles(UserRole.ADMIN)
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthGuardsModule } from '../auth/auth-guards.module';
|
import { AuthGuardsModule } from '../auth/auth-guards.module';
|
||||||
import { AuthorizationModule } from '../authorization/authorization.module';
|
import { AuthorizationModule } from '../authorization/authorization.module';
|
||||||
import { Service } from '../services/entities/service.entity';
|
import { ServiceSkill } from '../services/entities/service-skill.entity';
|
||||||
import { ServiceSuggestion } from './entities/service-suggestion.entity';
|
import { ServiceSuggestion } from './entities/service-suggestion.entity';
|
||||||
import { SuggestionsController } from './suggestions.controller';
|
import { SuggestionsController } from './suggestions.controller';
|
||||||
import { SuggestionsService } from './suggestions.service';
|
import { SuggestionsService } from './suggestions.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([ServiceSuggestion, Service]),
|
TypeOrmModule.forFeature([ServiceSuggestion, ServiceSkill]),
|
||||||
AuthorizationModule,
|
AuthorizationModule,
|
||||||
AuthGuardsModule,
|
AuthGuardsModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,22 +5,25 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
import { FindOptionsWhere, In, Repository } from 'typeorm';
|
||||||
import { ServicePolicy } from '../authorization/policies/service.policy';
|
import { ServicePolicy } from '../authorization/policies/service.policy';
|
||||||
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||||
import { Service } from '../services/entities/service.entity';
|
import { ServiceSkill } from '../services/entities/service-skill.entity';
|
||||||
|
import { CreateBulkServiceSuggestionsDto } from './dto/create-bulk-service-suggestions.dto';
|
||||||
import { CreateServiceSuggestionDto } from './dto/create-service-suggestion.dto';
|
import { CreateServiceSuggestionDto } from './dto/create-service-suggestion.dto';
|
||||||
import { FindSuggestionsQueryDto } from './dto/find-suggestions-query.dto';
|
import { FindSuggestionsQueryDto } from './dto/find-suggestions-query.dto';
|
||||||
import { UpdateServiceSuggestionDto } from './dto/update-service-suggestion.dto';
|
import { UpdateServiceSuggestionDto } from './dto/update-service-suggestion.dto';
|
||||||
import { ServiceSuggestion } from './entities/service-suggestion.entity';
|
import { ServiceSuggestion } from './entities/service-suggestion.entity';
|
||||||
|
|
||||||
|
const skillRelations = { service: true } as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SuggestionsService {
|
export class SuggestionsService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(ServiceSuggestion)
|
@InjectRepository(ServiceSuggestion)
|
||||||
private readonly suggestionsRepository: Repository<ServiceSuggestion>,
|
private readonly suggestionsRepository: Repository<ServiceSuggestion>,
|
||||||
@InjectRepository(Service)
|
@InjectRepository(ServiceSkill)
|
||||||
private readonly servicesRepository: Repository<Service>,
|
private readonly serviceSkillsRepository: Repository<ServiceSkill>,
|
||||||
private readonly servicePolicy: ServicePolicy,
|
private readonly servicePolicy: ServicePolicy,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -29,27 +32,73 @@ export class SuggestionsService {
|
|||||||
user: AuthUser,
|
user: AuthUser,
|
||||||
): Promise<ServiceSuggestion> {
|
): Promise<ServiceSuggestion> {
|
||||||
this.ensureCanManage(user);
|
this.ensureCanManage(user);
|
||||||
await this.validateServices(
|
await this.validateSuggestionTargets(
|
||||||
createDto.serviceId,
|
createDto.serviceSkillId,
|
||||||
createDto.suggestedServiceId,
|
createDto.suggestedServiceSkillId,
|
||||||
);
|
);
|
||||||
const suggestion = this.suggestionsRepository.create(createDto);
|
const suggestion = this.suggestionsRepository.create(createDto);
|
||||||
const saved = await this.suggestionsRepository.save(suggestion);
|
const saved = await this.suggestionsRepository.save(suggestion);
|
||||||
return this.findOne(saved.id);
|
return this.findOne(saved.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll(query: FindSuggestionsQueryDto): Promise<ServiceSuggestion[]> {
|
async createBulk(
|
||||||
|
createDto: CreateBulkServiceSuggestionsDto,
|
||||||
|
user: AuthUser,
|
||||||
|
): Promise<ServiceSuggestion[]> {
|
||||||
|
this.ensureCanManage(user);
|
||||||
|
|
||||||
|
const { serviceSkillId, suggestions } = createDto;
|
||||||
|
const suggestedServiceSkillIds = suggestions.map(
|
||||||
|
(item) => item.suggestedServiceSkillId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.validateBaseServiceSkill(serviceSkillId);
|
||||||
|
this.ensureUniqueSuggestedSkills(suggestedServiceSkillIds);
|
||||||
|
await this.validateSuggestedServiceSkills(
|
||||||
|
serviceSkillId,
|
||||||
|
suggestedServiceSkillIds,
|
||||||
|
);
|
||||||
|
await this.ensureNoExistingSuggestions(
|
||||||
|
serviceSkillId,
|
||||||
|
suggestedServiceSkillIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
const savedIds = await this.suggestionsRepository.manager.transaction(
|
||||||
|
async (manager) => {
|
||||||
|
const repository = manager.getRepository(ServiceSuggestion);
|
||||||
|
const entities = suggestions.map((item) =>
|
||||||
|
repository.create({ ...item, serviceSkillId }),
|
||||||
|
);
|
||||||
|
const saved = await repository.save(entities);
|
||||||
|
return saved.map((suggestion) => suggestion.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return this.suggestionsRepository.find({
|
return this.suggestionsRepository.find({
|
||||||
where: this.buildWhere(query),
|
where: { id: In(savedIds) },
|
||||||
relations: { service: true, suggestedService: true },
|
relations: {
|
||||||
|
serviceSkill: skillRelations,
|
||||||
|
suggestedServiceSkill: skillRelations,
|
||||||
|
},
|
||||||
order: { displayPriority: 'ASC', createdAt: 'ASC' },
|
order: { displayPriority: 'ASC', createdAt: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findForService(serviceId: string): Promise<ServiceSuggestion[]> {
|
findAll(query: FindSuggestionsQueryDto): Promise<ServiceSuggestion[]> {
|
||||||
return this.suggestionsRepository.find({
|
return this.suggestionsRepository.find({
|
||||||
where: { serviceId, isActive: true },
|
where: this.buildWhere(query),
|
||||||
relations: { suggestedService: true },
|
relations: {
|
||||||
|
serviceSkill: skillRelations,
|
||||||
|
suggestedServiceSkill: skillRelations,
|
||||||
|
},
|
||||||
|
order: { displayPriority: 'ASC', createdAt: 'ASC' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findForServiceSkill(serviceSkillId: string): Promise<ServiceSuggestion[]> {
|
||||||
|
return this.suggestionsRepository.find({
|
||||||
|
where: { serviceSkillId, isActive: true },
|
||||||
|
relations: { suggestedServiceSkill: skillRelations },
|
||||||
order: { displayPriority: 'ASC', createdAt: 'ASC' },
|
order: { displayPriority: 'ASC', createdAt: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -57,7 +106,10 @@ export class SuggestionsService {
|
|||||||
async findOne(id: string): Promise<ServiceSuggestion> {
|
async findOne(id: string): Promise<ServiceSuggestion> {
|
||||||
const suggestion = await this.suggestionsRepository.findOne({
|
const suggestion = await this.suggestionsRepository.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations: { service: true, suggestedService: true },
|
relations: {
|
||||||
|
serviceSkill: skillRelations,
|
||||||
|
suggestedServiceSkill: skillRelations,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!suggestion) {
|
if (!suggestion) {
|
||||||
throw new NotFoundException(`Suggestion #${id} not found`);
|
throw new NotFoundException(`Suggestion #${id} not found`);
|
||||||
@@ -72,11 +124,11 @@ export class SuggestionsService {
|
|||||||
): Promise<ServiceSuggestion> {
|
): Promise<ServiceSuggestion> {
|
||||||
this.ensureCanManage(user);
|
this.ensureCanManage(user);
|
||||||
const suggestion = await this.findOne(id);
|
const suggestion = await this.findOne(id);
|
||||||
const serviceId = updateDto.serviceId ?? suggestion.serviceId;
|
const serviceSkillId = updateDto.serviceSkillId ?? suggestion.serviceSkillId;
|
||||||
const suggestedServiceId =
|
const suggestedServiceSkillId =
|
||||||
updateDto.suggestedServiceId ?? suggestion.suggestedServiceId;
|
updateDto.suggestedServiceSkillId ?? suggestion.suggestedServiceSkillId;
|
||||||
|
|
||||||
await this.validateServices(serviceId, suggestedServiceId);
|
await this.validateSuggestionTargets(serviceSkillId, suggestedServiceSkillId);
|
||||||
Object.assign(suggestion, updateDto);
|
Object.assign(suggestion, updateDto);
|
||||||
await this.suggestionsRepository.save(suggestion);
|
await this.suggestionsRepository.save(suggestion);
|
||||||
return this.findOne(id);
|
return this.findOne(id);
|
||||||
@@ -93,8 +145,8 @@ export class SuggestionsService {
|
|||||||
): FindOptionsWhere<ServiceSuggestion> {
|
): FindOptionsWhere<ServiceSuggestion> {
|
||||||
const where: FindOptionsWhere<ServiceSuggestion> = {};
|
const where: FindOptionsWhere<ServiceSuggestion> = {};
|
||||||
|
|
||||||
if (query.serviceId) {
|
if (query.serviceSkillId) {
|
||||||
where.serviceId = query.serviceId;
|
where.serviceSkillId = query.serviceSkillId;
|
||||||
}
|
}
|
||||||
if (query.isActive !== undefined) {
|
if (query.isActive !== undefined) {
|
||||||
where.isActive = query.isActive;
|
where.isActive = query.isActive;
|
||||||
@@ -109,27 +161,71 @@ export class SuggestionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateServices(
|
private async validateSuggestionTargets(
|
||||||
serviceId: string,
|
serviceSkillId: string,
|
||||||
suggestedServiceId: string,
|
suggestedServiceSkillId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (serviceId === suggestedServiceId) {
|
if (serviceSkillId === suggestedServiceSkillId) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException('A skill cannot be suggested for itself');
|
||||||
'A service cannot be suggested for itself',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [serviceExists, suggestedExists] = await Promise.all([
|
await this.validateBaseServiceSkill(serviceSkillId);
|
||||||
this.servicesRepository.existsBy({ id: serviceId }),
|
await this.validateSuggestedServiceSkills(serviceSkillId, [
|
||||||
this.servicesRepository.existsBy({ id: suggestedServiceId }),
|
suggestedServiceSkillId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!serviceExists) {
|
|
||||||
throw new NotFoundException(`Service #${serviceId} not found`);
|
|
||||||
}
|
}
|
||||||
if (!suggestedExists) {
|
|
||||||
|
private async validateBaseServiceSkill(serviceSkillId: string): Promise<void> {
|
||||||
|
const exists = await this.serviceSkillsRepository.existsBy({ id: serviceSkillId });
|
||||||
|
if (!exists) {
|
||||||
|
throw new NotFoundException(`Service skill #${serviceSkillId} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureUniqueSuggestedSkills(suggestedServiceSkillIds: string[]): void {
|
||||||
|
const uniqueIds = new Set(suggestedServiceSkillIds);
|
||||||
|
if (uniqueIds.size !== suggestedServiceSkillIds.length) {
|
||||||
|
throw new BadRequestException('Duplicate suggested skills in batch');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validateSuggestedServiceSkills(
|
||||||
|
serviceSkillId: string,
|
||||||
|
suggestedServiceSkillIds: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
if (suggestedServiceSkillIds.includes(serviceSkillId)) {
|
||||||
|
throw new BadRequestException('A skill cannot be suggested for itself');
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestedSkills = await this.serviceSkillsRepository.find({
|
||||||
|
where: { id: In(suggestedServiceSkillIds) },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (suggestedSkills.length !== suggestedServiceSkillIds.length) {
|
||||||
|
const foundIds = new Set(suggestedSkills.map((skill) => skill.id));
|
||||||
|
const missingId = suggestedServiceSkillIds.find((id) => !foundIds.has(id));
|
||||||
throw new NotFoundException(
|
throw new NotFoundException(
|
||||||
`Suggested service #${suggestedServiceId} not found`,
|
`Suggested service skill #${missingId} not found`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureNoExistingSuggestions(
|
||||||
|
serviceSkillId: string,
|
||||||
|
suggestedServiceSkillIds: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
const existing = await this.suggestionsRepository.find({
|
||||||
|
where: {
|
||||||
|
serviceSkillId,
|
||||||
|
suggestedServiceSkillId: In(suggestedServiceSkillIds),
|
||||||
|
},
|
||||||
|
select: { suggestedServiceSkillId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Some suggestions already exist for this service skill',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user