Перейти к содержимому

Project API — проекты

Слой: libs/apis/providers/project-api · Приложение: apps/project-api

Сервис проектов отвечает за проекты заказчика (Customer) в кабинете: заведение черновика, чтение, правку и удаление. Весь API работает в контексте текущего заказчика — маршруты смонтированы под customers/me/projects, а доступ ограничен токеном заказчика.

Ключевые ответственности:

  • CRUD над проектами заказчика (create / list / read / update / delete);
  • защита инвариантов домена (редактировать и удалять можно только DRAFT);
  • при создании проекта — заведение сопутствующего Walrider-треда (AI-ассистент).

Статус проекта сервис не меняет: переходы вынесены из него (см. «Статус проекта» ниже).

Приложение apps/project-api — тонкая обёртка NestJS, вся бизнес-логика вынесена в библиотеку libs/apis/providers/project-api.

Полное описание модели Project — в database-schema: кроме title/description/category/status она несёт валюту сделки, вилку бюджета в минорных единицах, ставку комиссии, назначенную команду и подстадию контрактинга. Ни одно из этих полей project-api сейчас не читает и не пишет.

Что важно для этого сервиса:

  • Владелец. Проект всегда принадлежит одному заказчику (customerId). Идентификатор берётся из токена (payload.customerId), клиент не передаёт его вручную; все операции чтения и записи проверяют совпадение владельца (ForbiddenException, если проект чужой). Связь Project → Customer объявлена onDelete: Restrict — проект удалить каскадом нельзя.
  • Мягкое удаление. deletedAt проставляется сервисом; репозиторий фильтрует deletedAt: null во всех выборках.
  • Ответ ограничен DTO. ProjectResponseDto собирается MapperService со стратегией excludeAll, поэтому новые колонки таблицы в ответ не просачиваются: наружу отдаётся ровно то, что помечено @Expose().
  • Денежных полей в DTO нет. Суммы хранятся в БД как BigInt в минорных единицах; Decimal в схеме не используется.

Базовый ProjectDto описывает поля и валидацию, остальные DTO наследуются от него:

  • id: string@IsUUID;
  • title: string@IsString, MinLength(3), MaxLength(255), @Trim;
  • description: string@IsString, MinLength(3), @Trim;
  • category?: ProjectCategoryType | null — опционально, @IsEnum(ProjectCategoryType);
  • status: ProjectStatus@IsEnum(ProjectStatus);
  • customerId: string@IsUUID;
  • createdAt, updatedAt: Date@IsDate.

Производные DTO:

  • ProjectResponseDtoextends ProjectDto (полное представление в ответах);
  • ProjectCreateDtoOmitType(ProjectDto, ['id','createdAt','updatedAt','status','customerId']) → на вход только title, description, category?;
  • ProjectUpdateDtoPartialType(OmitType(... те же поля ...)) → все поля необязательны, редактируются только title, description, category;
  • ProjectListQueryDto — фильтр status?, пагинация page? (Min(1), default 1), perPage? (Min(1), default 10).

ProjectCategoryType (enum, 9 значений): WEB_DEVELOPMENT, MOBILE_DEVELOPMENT, DESIGN, MARKETING, COPYWRITING, DATA_SCIENCE, DEV_OPS, QA_TESTING, OTHER.

Enum ProjectStatus — 10 значений: DRAFT, AI_CONSULTATION, SPEC_READY, TEAM_MATCHING, TEAM_PROPOSED, CONTRACTING, ACTIVE, COMPLETED, DISPUTED, CANCELLED. У статуса CONTRACTING есть подстадия Project.contractingStage.

Сменить статус через API нельзя. Маршрут PATCH /api/v1/customers/me/projects/:projectId/status, DTO ProjectUpdateStatusDto и таблица переходов PROJECT_STATUS_TRANSITIONS сняты; новый проект по-прежнему стартует в DRAFT (@default(DRAFT) на уровне БД), и это единственный статус, который сервис проставляет. Статус читается — в фильтре списка и в ответе.

Это ломающее изменение API: клиент, вызывавший PATCH .../status, получает 404.

Причина не в том, что переходы «пока не сделаны», а в том, что прежняя матрица описывала другую модель продукта. В целевой каждый переход — событие с актором, основанием и записью в журнале StateTransition, часть переходов автоматические (по таймеру или по событию денежного контура), и инициирует их не заказчик. Такой переход не помещается в PATCH от владельца проекта. Проектная модель — step-3-state-machines.md.

Контроллер ProjectController@Controller({ path: 'customers/me/projects', version: '1' }). Все маршруты защищены @UseGuards(CustomerGuard) и @ApiBearerAuth(CUSTOMER_ACCESS_TOKEN_STRATEGY_NAME). С учётом глобального префикса api и URI-версионирования полный путь имеет вид /api/v1/customers/me/projects....

МетодПуть (полный)GuardОписаниеDTO вход → выход
POST/api/v1/customers/me/projectsCustomerGuardСоздать проект (статус DRAFT) + Walrider-тредProjectCreateDtoProjectResponseDto
GET/api/v1/customers/me/projectsCustomerGuardСписок проектов заказчика с пагинацией и фильтром по статусуProjectListQueryDto → paginate ProjectResponseDto
GET/api/v1/customers/me/projects/:projectIdCustomerGuardПолучить проект по id (проверка владельца)— → ProjectResponseDto
PATCH/api/v1/customers/me/projects/:projectIdCustomerGuardОбновить проект — только DRAFTProjectUpdateDtoProjectResponseDto
DELETE/api/v1/customers/me/projects/:projectIdCustomerGuardУдалить проект — только DRAFT (soft-delete через deletedAt)— → ProjectResponseDto

Детали поведения (ProjectService):

  • create@Transactional(): создаёт проект (customer.connect), затем в той же транзакции Walrider-тред; статус выставляется БД по умолчанию (DRAFT).
  • findMany — фильтр where.customerId, опционально where.status, сортировка createdAt desc, пагинация (page/perPage).
  • findOneByIdOrFailNotFoundException если нет, ForbiddenException если чужой.
  • updateOneById — если статус ≠ DRAFTBadRequestException('Only DRAFT projects can be updated'); обновляются только переданные поля title/description/category.
  • deleteOneById — если статус ≠ DRAFTBadRequestException('Only DRAFT projects can be deleted'). Репозиторий работает с soft-delete: findUnique/findMany фильтруют deletedAt: null.

Ответы маппятся через MapperService (toResponse / toPaginateResponse) в ProjectResponseDto.

При создании проекта ProjectService.create в рамках одной транзакции вызывает WalriderThreadRepository.create, заводя запись WalriderThread, привязанную к заказчику и проекту (customer.connect + project.connect). Связь Project ↔ WalriderThread — 1:1 (projectId @unique). WalriderThreadRepository в этом сервисе — тонкий адаптер (create, findUnique) поверх транзакционного Prisma-хоста; детальная логика AI-ассистента (треды, сообщения, состояния, внешний externalThreadId) описана отдельно — см. docs/03-services/ai/walrider.md.

  • @nestjs/*, @nestjs/swagger — HTTP-слой, версионирование, документация.
  • @prisma/client — типы Prisma, ProjectStatus, ProjectCategoryType, модель Project.
  • @nestjs-cls/transactional + @nestjs-cls/transactional-adapter-prisma — декларативные транзакции (@Transactional, TransactionHost); настраиваются в AppModule через ClsPluginTransactional.
  • @crewsforge-back/apis/utils/prisma-clientPrismaClientModule / PrismaClientService.
  • @crewsforge-back/apis/sharedCustomerGuard, GetTokenPayload, MapperService, PrismaExceptionFilter, Trim, утилиты пагинации (getPaginate, toPaginateResponse), константы стратегий токенов.
  • @crewsforge-back/apis/configs/shared/appAppConfigService (порт из env APP_PORT).
  • @crewsforge-back/apis/configs/auth-api/jwt-customer + @nestjs/jwt — валидация токена заказчика.
  • @crewsforge-back/apis/providers/auth-api/features/token — тип TokenPayload.

Конфигурация приложения (apps/project-api/src/main.ts):

  • глобальный префикс api, URI-версионирование (v1);
  • ValidationPipe (transform, whitelist, strategy: 'excludeAll');
  • PrismaExceptionFilter, CORS с cookie, cookie-parser;
  • Swagger на /api (title «Project API»), два bearer-auth: customer и employee;
  • порт — AppConfigService.port (env APP_PORT).
  • apps/project-api/src/main.ts — bootstrap, порт, Swagger, глобальные пайпы/фильтры.
  • apps/project-api/src/app/app.module.ts — корневой модуль, Cls/Transactional, ProjectModule.
  • libs/apis/providers/project-api/features/project/src/lib/project.controller.ts — маршруты и guard’ы.
  • .../features/project/src/lib/project.service.ts — бизнес-логика, проверка владельца, транзакции.
  • .../features/project/src/lib/project.repository.ts — доступ к данным, soft-delete, пагинация.
  • .../features/project/src/lib/walrider-thread.repository.ts — адаптер Walrider-треда (кратко).
  • .../features/project/src/lib/project.module.ts — сборка модуля, провайдеры.
  • .../data-access/src/lib/dtos/project.dto.ts — DTO и валидация.
  • apps/core-api/prisma/schema.prisma — модель Project, enum ProjectStatus, ProjectCategoryType.

Добавить поле проекта:

  1. Добавить поле в модель Project в schema.prisma, сгенерировать миграцию и Prisma-клиент (см. migrations). Для денег — BigInt в минорных единицах, не Decimal: так устроены все денежные поля схемы.
  2. Добавить поле в ProjectDto с валидатором и @Expose(); при необходимости учесть его в ProjectCreateDto / ProjectUpdateDto (они строятся через OmitType/PartialType).
  3. Пробросить поле в Prisma.ProjectCreateInput/update внутри ProjectServicecreate и в spread-обновлении updateOneById).
  4. BigInt не сериализуется в JSON — в ProjectResponseDto значение приводится к строке через @Transform.

Добавить операцию над проектом: метод сервиса по схеме <глагол><One|Many>[By<Поле>][OrFail], маршрут в контроллере, проверка владельца через findOneByIdOrFail(id, customerId).

Смена статуса в этот сервис не возвращается точечной правкой: она требует библиотеки машины состояний с журналом переходов. Отправная точка — проектные документы шага 3 и план стройки docs/delivery/ROADMAP.md.