import { CommandBus, CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { ForbiddenError, NotFoundError, assertThat, } from '@task-stream/api/shared/domain'; import { IssuePriority, SYSTEM_ACTOR_ID, } from '@task-stream/api/tracker/domain'; import { FindProjectsRepository } from '../ports/find-projects.repository'; import { CommandResult } from './command-result'; import { CreateIssueCommand } from './create-issue.command'; import { ProposalKind, SubmitProposalCommand } from './submit-proposal.command'; import { WriteAccess } from './write-access'; /** The label every proposal carries, for the team to find and sort them. */ export const PROPOSAL_LABEL = 'propuesta'; /** Preferred ticket type of each kind, when the project has it active. */ const TYPE_OF: Record = { BUG: 'BUG', IDEA: 'FEATURE' }; /** * Only a public project that takes proposals accepts one. It is opened by * TaskStream (nobody signed in), in the first status, as a Bug or a Feature * (or the first active type if the project has neither), and signed with * the name they gave, if any. */ @CommandHandler(SubmitProposalCommand) export class SubmitProposalCommandHandler implements ICommandHandler { constructor( private readonly findProjects: FindProjectsRepository, private readonly access: WriteAccess, private readonly commandBus: CommandBus, ) {} async execute(command: SubmitProposalCommand): Promise { const found = (await this.findProjects.findById(command.project)) ?? (await this.findProjects.findByKey(command.project.toUpperCase())); if (!found?.isPublic) throw new NotFoundError('Project', command.project); if (!found.acceptsProposals) throw new ForbiddenError(`${found.key} does not take proposals`); const title = command.title.trim(); assertThat( title.length >= 3 && title.length <= 150, 'A proposal needs a title of 3 to 150 characters', ); const text = command.description.trim(); assertThat(text.length <= 5000, 'A proposal is 5000 characters at most'); const project = await this.access.project(found.id); const active = project.settings.issueTypes.filter((t) => !t.archived); const type = active.find((t) => t.id === TYPE_OF[command.kind])?.id ?? active[0]?.id; const author = command.author?.trim().slice(0, 80); const signature = `_Propuesta enviada desde el formulario público${author ? ` por ${author}` : ''}._`; return this.commandBus.execute( new CreateIssueCommand( SYSTEM_ACTOR_ID, project.id, type, title, { description: text ? `${text}\n\n${signature}` : signature, labels: [PROPOSAL_LABEL], priority: IssuePriority.MEDIUM, }, null, null, null, null, // What they wrote is the content, not the type's template. false, ), ); } }