Files
mono/components/controller/src/extensions/capital/application/use-cases/participation-management.interactor.ts
T
2026-01-20 21:23:02 +05:00

479 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Injectable, Inject } from '@nestjs/common';
import { randomBytes } from 'crypto';
import {
CapitalBlockchainPort,
CAPITAL_BLOCKCHAIN_PORT,
} from '../../domain/interfaces/capital-blockchain.port';
import type { ImportContributorDomainInput } from '../../domain/actions/import-contributor-domain-input.interface';
import type { RegisterContributorDomainInput } from '../../domain/actions/register-contributor-domain-input.interface';
import type { EditContributorDomainInput } from '../../domain/actions/edit-contributor-domain-input.interface';
import type { MakeClearanceDomainInput } from '../../domain/actions/make-clearance-domain-input.interface';
import type { IAppendixDatabaseData } from '../../domain/interfaces/appendix-database.interface';
import type { TransactResult } from '@wharfkit/session';
import {
CONTRIBUTOR_REPOSITORY,
ContributorRepository,
} from '../../domain/repositories/contributor.repository';
import {
APPENDIX_REPOSITORY,
AppendixRepository,
} from '../../domain/repositories/appendix.repository';
import { ContributorDomainEntity } from '../../domain/entities/contributor.entity';
import { AppendixDomainEntity } from '../../domain/entities/appendix.entity';
import { ContributorStatus } from '../../domain/enums/contributor-status.enum';
import { AppendixStatus } from '../../domain/enums/appendix-status.enum';
import type { ContributorFilterInputDTO } from '../dto/participation_management/contributor-filter.input';
import type {
PaginationInputDomainInterface,
PaginationResultDomainInterface,
} from '~/domain/common/interfaces/pagination.interface';
import type { AppendixGenerationAgreementGenerateDocumentInputDTO } from '~/application/document/documents-dto/appendix-generation-agreement-document.dto';
import type { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
import type { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
import { DomainToBlockchainUtils } from '~/shared/utils/domain-to-blockchain.utils';
import {
AccountDataPort,
ACCOUNT_DATA_PORT,
} from '~/domain/account/ports/account-data.port';
import { config } from '~/config';
import { HttpApiError } from '~/utils/httpApiError';
import httpStatus from 'http-status';
import { DocumentInteractor } from '~/application/document/interactors/document.interactor';
import { Cooperative } from 'cooptypes';
import { EMPTY_HASH } from '~/shared/utils/constants';
import { ProjectManagementInteractor } from '../use-cases/project-management.interactor';
import { generateUniqueHash } from '~/utils/generate-hash.util';
import type { MakeClearanceInputDTO } from '../dto/participation_management/make-clearance-input.dto';
/**
* Интерактор домена для управления участием в CAPITAL контракте
* Обрабатывает действия связанные с участниками и их регистрацией
*/
@Injectable()
export class ParticipationManagementInteractor {
constructor(
@Inject(CAPITAL_BLOCKCHAIN_PORT)
private readonly capitalBlockchainPort: CapitalBlockchainPort,
@Inject(CONTRIBUTOR_REPOSITORY)
private readonly contributorRepository: ContributorRepository,
@Inject(APPENDIX_REPOSITORY)
private readonly appendixRepository: AppendixRepository,
@Inject(ACCOUNT_DATA_PORT)
private readonly accountDataPort: AccountDataPort,
private readonly projectManagementInteractor: ProjectManagementInteractor,
private readonly domainToBlockchainUtils: DomainToBlockchainUtils,
private readonly documentInteractor: DocumentInteractor,
) { }
/**
* Получение отображаемого имени из аккаунта через порт расширения
*/
private async getDisplayNameFromAccount(username: string): Promise<string> {
return await this.accountDataPort.getDisplayName(username);
}
/**
* Импорт участника в CAPITAL контракт
*/
async importContributor(
data: ImportContributorDomainInput
): Promise<TransactResult> {
// Получаем отображаемое имя из аккаунта
const displayName = await this.getDisplayNameFromAccount(data.username);
// Создаем участника в репозитории (данные базы данных)
const contributor = new ContributorDomainEntity({
_id: '', // будет сгенерирован автоматически
block_num: 0,
present: true,
contributor_hash: data.contributor_hash,
status: ContributorStatus.PENDING,
_created_at: new Date(),
_updated_at: new Date(),
display_name: displayName,
});
// Вызываем блокчейн порт
const result = await this.capitalBlockchainPort.importContributor(data);
await this.contributorRepository.create(contributor);
// Синхронизация автоматически обновит данные из блокчейна
return result;
}
/**
* Регистрация участника в CAPITAL контракте
*/
async registerContributor(
data: RegisterContributorDomainInput
): Promise<TransactResult> {
// Извлекаем документ из базы данных для верификации
const document = await this.documentInteractor.getDocumentByHash(
data.contract.doc_hash
);
if (!document) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Документ с хэшем ${data.contract.doc_hash} не найден`
);
}
// Проверяем, что username в документе совпадает с переданным
if (document.meta.username !== data.username) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Username в документе (${document.meta.username}) не совпадает с переданным (${data.username})`
);
}
// Проверяем, что contributor_hash в документе совпадает с переданным
if (document.meta.contributor_hash !== data.contributor_hash) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Contributor hash в документе (${document.meta.contributor_hash}) не совпадает с переданным (${data.contributor_hash})`
);
}
// Получаем отображаемое имя из аккаунта
const displayName = await this.getDisplayNameFromAccount(data.username);
// Используем contributor_hash из документа
const databaseData = {
_id: '', // будет сгенерирован автоматически
block_num: 0,
present: false,
contributor_hash: data.contributor_hash,
status: ContributorStatus.PENDING,
about: data.about ?? '',
_created_at: new Date(),
_updated_at: new Date(),
display_name: displayName,
};
// Преобразовываем доменный документ в формат блокчейна
const blockchainAction = {
...data,
contributor_hash: data.contributor_hash,
rate_per_hour:
data.rate_per_hour ?? '0.0000 ' + config.blockchain.root_govern_symbol,
hours_per_day: data.hours_per_day ?? 0,
is_external_contract: false,
contract:
this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(
data.contract
),
};
// Вызываем блокчейн порт для регистрации - получаем транзакцию
const result = await this.capitalBlockchainPort.registerContributor(
blockchainAction
);
// Получаем данные участника из блокчейна после регистрации
const blockchainData = await this.capitalBlockchainPort.getContributor(
data.coopname,
data.username
);
if (!blockchainData) {
throw new HttpApiError(
httpStatus.INTERNAL_SERVER_ERROR,
`Не удалось получить данные участника ${databaseData.contributor_hash} из блокчейна после регистрации`
);
}
// Создаем полный объект участника, объединяя данные базы и блокчейна
const fullContributor = new ContributorDomainEntity(
databaseData,
blockchainData
);
// Сохраняем полный объект в репозиторий
await this.contributorRepository.create(fullContributor);
return result;
}
/**
* Подписание приложения в CAPITAL контракте
* Теперь принимает минимальный набор данных и подписанный документ
*/
async makeClearance(data: MakeClearanceInputDTO): Promise<TransactResult> {
// Извлекаем документ из базы данных для верификации
const document = await this.documentInteractor.getDocumentByHash(
data.document.doc_hash
);
if (!document) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Документ с хэшем ${data.document.doc_hash.toUpperCase()} не найден`
);
}
// Извлекаем appendix_hash из метаданных документа
const appendix_hash = (document.meta as any).appendix_hash;
if (!appendix_hash) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
'В документе отсутствует appendix_hash'
);
}
// Проверяем, есть ли уже запрос на рассмотрении для данного пользователя и проекта
const existingAppendix =
await this.appendixRepository.findCreatedByUsernameAndProjectHash(
data.username,
data.project_hash
);
if (existingAppendix) {
throw new HttpApiError(
httpStatus.CONFLICT,
'Подождите, ваш запрос на рассмотрении'
);
}
// Проверяем, что username в документе совпадает с переданным
if (document.meta.username !== data.username) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Username в документе (${document.meta.username}) не совпадает с переданным (${data.username})`
);
}
// Формируем доменный input с полными данными
const domainInput: MakeClearanceDomainInput = {
coopname: data.coopname,
username: data.username,
project_hash: data.project_hash,
appendix_hash,
document: data.document,
contribution: data.contribution,
};
return await this.makeClearanceDomain(domainInput);
}
/**
* Доменная логика подписания приложения в CAPITAL контракте
*/
private async makeClearanceDomain(
data: MakeClearanceDomainInput
): Promise<TransactResult> {
// Создаем базовые данные appendix для базы данных
const databaseData: IAppendixDatabaseData = {
_id: '',
block_num: undefined,
present: false, // Важно: изначально present = false
appendix_hash: data.appendix_hash, // Используем переданный хэш
status: AppendixStatus.CREATED,
blockchain_status: undefined,
contribution: data.contribution,
_created_at: new Date(),
_updated_at: new Date(),
};
// ШАГ 1: Частичное сохранение в базу данных
const partialAppendix = new AppendixDomainEntity(databaseData);
const savedAppendix = await this.appendixRepository.save(partialAppendix);
// ШАГ 2: Вызываем блокчейн порт для makeClearance
const result = await this.capitalBlockchainPort.makeClearance({
coopname: data.coopname,
username: data.username,
project_hash: data.project_hash,
appendix_hash: partialAppendix.appendix_hash,
document:
this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(
data.document
),
});
// ШАГ 3: Получаем данные appendix из блокчейна после makeClearance
const blockchainData = await this.capitalBlockchainPort.getAppendix(
data.coopname,
partialAppendix.appendix_hash
);
if (!blockchainData) {
throw new HttpApiError(
httpStatus.INTERNAL_SERVER_ERROR,
`Не удалось получить данные appendix ${partialAppendix.appendix_hash} из блокчейна после makeClearance`
);
}
// ШАГ 4: Обновляем существующую запись полными данными
savedAppendix.updateFromBlockchain(
blockchainData,
Number(result.transaction?.ref_block_num) ?? 0,
true
);
await this.appendixRepository.save(savedAppendix);
return result;
}
/**
* Редактирование участника в CAPITAL контракте
*/
async editContributor(
data: EditContributorDomainInput
): Promise<TransactResult> {
// Находим участника в базе данных для обновления поля about
const contributor = await this.getContributorByCriteria({
username: data.username,
});
if (!contributor) {
throw new HttpApiError(
httpStatus.NOT_FOUND,
`Участник ${data.username} не найден в кооперативе ${data.coopname}`
);
}
// Обновляем поле about в базе данных, если оно передано
if (data.about !== undefined) {
contributor.about = data.about;
await this.contributorRepository.update(contributor);
}
// Отправляем в блокчейн только параметры для редактирования (rate_per_hour и hours_per_day)
const blockchainData = {
coopname: data.coopname,
username: data.username,
rate_per_hour:
data.rate_per_hour ?? '0.0000 ' + config.blockchain.root_govern_symbol,
hours_per_day: data.hours_per_day ?? 0,
};
// Вызываем блокчейн порт для редактирования участника
const result = await this.capitalBlockchainPort.editContributor(
blockchainData
);
// Синхронизация автоматически обновит данные из блокчейна
return result;
}
/**
* Генерация документа приложения к договору участия
* Извлекает все необходимые данные на бэкенде по project_hash
*/
async generateAppendixGenerationAgreement(
data: AppendixGenerationAgreementGenerateDocumentInputDTO,
options?: GenerateDocumentOptionsInputDTO
): Promise<GeneratedDocumentDTO> {
// 1. Получаем данные участника
const contributor = await this.getContributorByCriteria({
username: data.username,
});
if (!contributor) {
throw new HttpApiError(
httpStatus.NOT_FOUND,
`Участник ${data.username} не найден`
);
}
// 2. Получаем данные проекта
const project = await this.projectManagementInteractor.getProjectByHash(
data.project_hash
);
if (!project) {
throw new HttpApiError(
httpStatus.NOT_FOUND,
`Проект с хэшем ${data.project_hash} не найден`
);
}
// 3. Определяем, является ли проект компонентом
const isComponent = Boolean(
project.parent_hash && project.parent_hash !== EMPTY_HASH
);
// 4. Если компонент - получаем родительский проект
let parentProject: Awaited<
ReturnType<typeof this.projectManagementInteractor.getProjectByHash>
> = null;
if (isComponent && project.parent_hash) {
parentProject = await this.projectManagementInteractor.getProjectByHash(
project.parent_hash
);
if (!parentProject) {
throw new HttpApiError(
httpStatus.NOT_FOUND,
`Родительский проект с хэшем ${project.parent_hash} не найден`
);
}
}
// 5. Генерируем уникальный хэш для приложения
const appendix_hash = generateUniqueHash();
// 6. Формируем данные для генерации документа
const documentData = {
coopname: data.coopname,
username: data.username,
lang: data.lang || 'ru',
registry_id: Cooperative.Registry.AppendixGenerationAgreement.registry_id,
appendix_hash,
contributor_hash: contributor.contributor_hash,
contributor_created_at: contributor.created_at,
component_name: isComponent ? project.title || project.data || '' : '',
component_id: isComponent ? project.project_hash : '',
project_name: isComponent
? parentProject?.title || parentProject?.data || ''
: project.title || project.data || '',
project_id: isComponent
? project.parent_hash || ''
: project.project_hash,
is_component: isComponent,
};
// 7. Генерируем документ
const document = await this.documentInteractor.generateDocument({
data: documentData,
options: options || {},
});
return document as GeneratedDocumentDTO;
}
// ============ МЕТОДЫ ЧТЕНИЯ ДАННЫХ ============
/**
* Получение всех участников с фильтрацией и пагинацией
*/
async getContributors(
filter?: ContributorFilterInputDTO,
options?: PaginationInputDomainInterface
): Promise<PaginationResultDomainInterface<ContributorDomainEntity>> {
return await this.contributorRepository.findAllPaginated(filter, options);
}
/**
* Получение участника по ID
*/
async getContributorById(
_id: string
): Promise<ContributorDomainEntity | null> {
return await this.contributorRepository.findById(_id);
}
/**
* Получение участника по критериям поиска
* Ищет по _id, username или contributor_hash
*/
async getContributorByCriteria(criteria: {
_id?: string;
username?: string;
contributor_hash?: string;
}): Promise<ContributorDomainEntity | null> {
return await this.contributorRepository.findOne(criteria);
}
}