Files
mono/components/controller/src/extensions/capital/application/use-cases/participation-management.interactor.ts
T
2026-06-28 10:44:02 +05:00

1006 lines
42 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, Logger } from '@nestjs/common';
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 { ProjectGenerationContractGenerateDocumentInputDTO } from '~/application/document/documents-dto/project-generation-agreement-document.dto';
import type { ComponentGenerationContractGenerateDocumentInputDTO } from '~/application/document/documents-dto/component-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 { waitAfterTransactBeforeChainTableRead } from '~/shared/utils/post-transact-chain-read-delay';
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 { generateRandomHash, generateUniqueHash } from '~/utils/generate-hash.util';
import type { MakeClearanceInputDTO } from '../dto/participation_management/make-clearance-input.dto';
import { CANDIDATE_REPOSITORY, CandidateRepository } from '~/domain/account/repository/candidate.repository';
import { UdataDocumentParametersService, UDATA_DOCUMENT_PARAMETERS_SERVICE } from '../../domain/services/udata-document-parameters.service';
import { ProgramKey } from '~/domain/registration/enum';
import type { GenerateCapitalRegistrationDocumentsDomainInput } from '../../domain/actions/generate-capital-registration-documents-domain-input.interface';
import type { GenerateCapitalRegistrationDocumentsDomainOutput } from '../../domain/actions/generate-capital-registration-documents-domain-output.interface';
import type { CompleteCapitalRegistrationDomainInput } from '../../domain/actions/complete-capital-registration-domain-input.interface';
import {
EXTENSION_REPOSITORY,
type ExtensionDomainRepository,
} from '~/domain/extension/repositories/extension-domain.repository';
import type { IConfig } from '../../capital-extension.module';
/**
* Интерактор домена для управления участием в CAPITAL контракте
* Обрабатывает действия связанные с участниками и их регистрацией
*/
@Injectable()
export class ParticipationManagementInteractor {
private readonly logger = new Logger(ParticipationManagementInteractor.name);
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,
@Inject(CANDIDATE_REPOSITORY)
private readonly candidateRepository: CandidateRepository,
@Inject(UDATA_DOCUMENT_PARAMETERS_SERVICE)
private readonly udataDocumentParametersService: UdataDocumentParametersService,
private readonly projectManagementInteractor: ProjectManagementInteractor,
private readonly domainToBlockchainUtils: DomainToBlockchainUtils,
private readonly documentInteractor: DocumentInteractor,
@Inject(EXTENSION_REPOSITORY)
private readonly extensionRepository: ExtensionDomainRepository<IConfig>,
) { }
private async getCapitalProgramDocDataHash(): Promise<string> {
const extension = await this.extensionRepository.findByName('capital');
const hash = (extension?.config as IConfig | undefined)?.capital_program_doc_data_hash?.trim();
if (!hash) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
'Параметры документов ЦПП не заполнены: отсутствует capital_program_doc_data_hash в конфигурации capital'
);
}
return hash;
}
/**
* Получение отображаемого имени из аккаунта через порт расширения
*/
private async getDisplayNameFromAccount(username: string): Promise<string> {
return await this.accountDataPort.getDisplayName(username);
}
/**
* Импорт участника в CAPITAL контракт
*/
async importContributor(
data: ImportContributorDomainInput
): Promise<TransactResult> {
// Проверяем, не существует ли уже участник с указанным username
const existingContributor = await this.contributorRepository.findByUsername(data.username);
if (existingContributor) {
throw new HttpApiError(
httpStatus.CONFLICT,
`Участник с именем пользователя ${data.username} уже зарегистрирован в программе`
);
}
// Генерируем уникальный contributor_hash автоматически
const contributor_hash = generateRandomHash();
// Получаем отображаемое имя из аккаунта
const displayName = await this.getDisplayNameFromAccount(data.username);
// Создаем участника в репозитории (данные базы данных)
const contributor = new ContributorDomainEntity({
_id: '', // будет сгенерирован автоматически
block_num: 0,
present: true,
coopname: config.coopname,
username: data.username,
contributor_hash: contributor_hash,
status: ContributorStatus.PENDING,
_created_at: new Date(),
_updated_at: new Date(),
display_name: displayName,
about: '',
program_key: undefined,
blagorost_offer_hash: undefined,
generator_offer_hash: undefined,
generation_contract_hash: undefined,
storage_agreement_hash: undefined,
blagorost_agreement_hash: undefined,
is_external_blagorost_agreement: true, // При импорте соглашение Благорост всегда предоставляется
});
// Подготавливаем данные для блокчейна с сгенерированным хешем
const blockchainData = {
...data,
contributor_hash: contributor_hash,
memo: data.memo ?? '',
};
// Вызываем блокчейн порт
const result = await this.capitalBlockchainPort.importContributor(blockchainData);
// Создаем участника в репозитории
await this.contributorRepository.create(contributor);
// Сохраняем параметры договора в UData через публичный метод сервиса
await this.udataDocumentParametersService.saveContributorContractParameters(
data.coopname,
data.username,
data.contributor_contract_number,
data.contributor_contract_created_at
);
// Сохраняем параметры соглашения Благорост в UData
await this.udataDocumentParametersService.saveBlagorostAgreementParameters(
data.coopname,
data.username,
data.blagorost_agreement_number,
data.blagorost_agreement_created_at
);
// Синхронизация автоматически обновит данные из блокчейна
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);
// Получаем candidate для извлечения program_key и хешей оферт
const candidate = await this.candidateRepository.findByUsername(data.username);
// Определяем хеш оферты на основе выбранной программы
let blagorostOfferHash: string | undefined;
let generatorOfferHash: string | undefined;
if (candidate) {
if (candidate.documents?.blagorost_offer) {
blagorostOfferHash = candidate.documents.blagorost_offer.doc_hash;
}
if (candidate.documents?.generator_offer) {
generatorOfferHash = candidate.documents.generator_offer.doc_hash;
}
}
// Используем contributor_hash из документа
const databaseData = {
_id: '', // будет сгенерирован автоматически
block_num: 0,
present: false,
coopname: data.coopname,
username: data.username,
contributor_hash: data.contributor_hash,
status: ContributorStatus.PENDING,
about: data.about ?? '',
_created_at: new Date(),
_updated_at: new Date(),
display_name: displayName,
program_key: candidate?.program_key, // Сохраняем выбранную программу
blagorost_offer_hash: blagorostOfferHash, // Хеш оферты Благорост (если была выбрана)
generator_offer_hash: generatorOfferHash, // Хеш оферты Генератор (если была выбрана)
generation_contract_hash: data.contract.doc_hash, // Сохраняем хеш договора УХД
storage_agreement_hash: undefined, // Соглашение о хранении еще не создано
blagorost_agreement_hash: undefined, // Соглашение Благорост еще не создано
};
// Создаем пустой документ для случаев когда соглашения еще не готовы
const createEmptyDocument = () => ({
version: '1.0',
hash: EMPTY_HASH,
doc_hash: EMPTY_HASH,
meta_hash: EMPTY_HASH,
meta: '',
signatures: [],
});
// Преобразовываем доменный документ в формат блокчейна
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
),
// В основной регистрации соглашения еще не готовы, передаем пустые документы
storage_agreement: createEmptyDocument(),
blagorost_agreement: createEmptyDocument(),
};
// Вызываем блокчейн порт для регистрации - получаем транзакцию
const result = await this.capitalBlockchainPort.registerContributor(
blockchainAction
);
await waitAfterTransactBeforeChainTableRead();
// Получаем данные участника из блокчейна после регистрации
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;
//TODO: адаптировать или документ или код ниже к parent_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
),
});
await waitAfterTransactBeforeChainTableRead();
// ШАГ 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;
}
/**
* Генерация документа приложения к договору участия для проекта (1002)
*/
async generateProjectGenerationContract(
data: ProjectGenerationContractGenerateDocumentInputDTO,
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
);
if (isComponent) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Проект ${data.project_hash} является компонентом. Используйте generateComponentGenerationContract`
);
}
// 4. Генерируем уникальный хэш для приложения
const appendix_hash = generateUniqueHash();
// 5. Формируем данные для генерации документа
const documentData = {
coopname: data.coopname,
username: data.username,
lang: data.lang || 'ru',
registry_id: Cooperative.Registry.ProjectGenerationContract.registry_id,
appendix_hash,
contributor_hash: contributor.contributor_hash,
contributor_created_at: contributor.created_at,
project_name: project.title || project.data || '',
project_hash: project.project_hash,
};
// 6. Генерируем документ
const document = await this.documentInteractor.generateDocument({
data: documentData,
options: options || {},
});
return document as GeneratedDocumentDTO;
}
/**
* Генерация документа дополнения к приложению для компонента (1003)
*/
async generateComponentGenerationContract(
data: ComponentGenerationContractGenerateDocumentInputDTO,
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 component = await this.projectManagementInteractor.getProjectByHash(
data.component_hash
);
if (!component) {
throw new HttpApiError(
httpStatus.NOT_FOUND,
`Компонент с хэшем ${data.component_hash} не найден`
);
}
// 3. Проверяем, что это действительно компонент
const isComponent = Boolean(
component.parent_hash && component.parent_hash !== EMPTY_HASH
);
if (!isComponent) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Проект ${data.component_hash} не является компонентом. Используйте generateProjectGenerationContract`
);
}
// 4. Получаем родительский проект
const parentProject = await this.projectManagementInteractor.getProjectByHash(
data.parent_project_hash
);
if (!parentProject) {
throw new HttpApiError(
httpStatus.NOT_FOUND,
`Родительский проект с хэшем ${data.parent_project_hash} не найден`
);
}
// 5. Проверяем, что component действительно дочерний для parent_project
if (component.parent_hash !== parentProject.project_hash) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Компонент ${data.component_hash} не является дочерним для проекта ${data.parent_project_hash}`
);
}
// 6. Находим родительское приложение (appendix) к родительскому проекту
const parentAppendix = await this.appendixRepository.findCreatedByUsernameAndProjectHash(
data.username,
parentProject.project_hash
);
if (!parentAppendix) {
throw new HttpApiError(
httpStatus.NOT_FOUND,
`Не найдено приложение к родительскому проекту ${parentProject.project_hash} для пользователя ${data.username}`
);
}
// 7. Генерируем уникальный хэш для дополнения к приложению
const appendix_hash = generateUniqueHash();
// 8. Формируем данные для генерации документа
const documentData = {
coopname: data.coopname,
username: data.username,
lang: data.lang || 'ru',
registry_id: Cooperative.Registry.ComponentGenerationContract.registry_id,
appendix_hash,
parent_appendix_hash: parentAppendix.appendix_hash,
contributor_hash: contributor.contributor_hash,
contributor_created_at: contributor.created_at,
component_name: component.title || component.data || '',
component_hash: component.project_hash,
project_name: parentProject.title || parentProject.data || '',
project_hash: parentProject.project_hash,
};
// 9. Генерируем документ
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);
}
/**
* Генерация пачки документов для завершения регистрации в Capital
*
* Генерирует документы на основе наличия их хэшей в Contributor:
* 1. GenerationContract (договор УХД) - если НЕ внешний контракт и нет хэша
* 2. StorageAgreement (соглашение о хранении) - если нет хэша
* 3. BlagorostAgreement (соглашение Благорост) - если нет хэша
* 4. GeneratorOffer (соглашение Генератор) - если нет хэша
*
* Такая логика покрывает все случаи:
* - Полная регистрация нового участника (генерирует только недостающий документ)
* - Импортированный участник с внешним контрактом (генерирует оба соглашения)
* - Повторная генерация (пропускает уже подписанные документы)
*/
async generateCapitalRegistrationDocuments(
data: GenerateCapitalRegistrationDocumentsDomainInput
): Promise<GenerateCapitalRegistrationDocumentsDomainOutput> {
// Получаем данные участника
let contributor = await this.contributorRepository.findOne({ username: data.username });
// Если Contributor не найден, создаем его с пустыми хэшами
// Это нужно для обратной совместимости с существующими пользователями
if (!contributor) {
this.logger.log(`Contributor для пользователя ${data.username} не найден, создаем новый`);
// Получаем display_name через порт расширения
const displayName = await this.accountDataPort.getDisplayName(data.username);
const contributorData = {
_id: '',
present: false,
username: data.username,
coopname: data.coopname,
display_name: displayName,
program_key: ProgramKey.GENERATION, // Дефолтная программа для новых участников
status: ContributorStatus.PENDING,
contributor_hash: generateRandomHash(),
blagorost_offer_hash: undefined,
generator_offer_hash: undefined,
generation_contract_hash: undefined,
storage_agreement_hash: undefined,
blagorost_agreement_hash: undefined,
};
contributor = new ContributorDomainEntity(contributorData);
await this.contributorRepository.create(contributor);
this.logger.log(`Создан Contributor для пользователя ${data.username}`);
}
const lang = data.lang || 'ru';
// 1. GenerationContract (договор УХД) - генерируем только если НЕ внешний контракт И нет хэша
let generationContract: GeneratedDocumentDTO | undefined;
const needGenerationContract = !contributor.is_external_contract && !contributor.generation_contract_hash;
if (needGenerationContract) {
this.logger.log(`Генерация договора УХД для ${data.username}`);
// Генерируем параметры для договора УХД
await this.udataDocumentParametersService.generateGenerationContractParametersIfNotExist(data.coopname, data.username);
generationContract = await this.documentInteractor.generateDocument({
data: {
coopname: data.coopname,
username: data.username,
lang,
registry_id: Cooperative.Registry.GenerationContract.registry_id,
},
options: { skip_save: false },
});
} else {
this.logger.log(`Пропуск генерации договора УХД для ${data.username}: ${contributor.is_external_contract ? 'внешний контракт' : 'уже подписан'}`);
}
// 2. StorageAgreement (соглашение о хранении) - генерируем если нет хэша
let storageAgreement: GeneratedDocumentDTO | undefined;
const needStorageAgreement = !contributor.storage_agreement_hash;
if (needStorageAgreement) {
this.logger.log(`Генерация соглашения о хранении для ${data.username}`);
// Генерируем параметры для соглашения о хранении
await this.udataDocumentParametersService.generateStorageAgreementParametersIfNotExist(data.coopname, data.username);
// Для StorageAgreement также нужны параметры GeneratorOffer
await this.udataDocumentParametersService.generateGeneratorOfferParametersIfNotExist(data.coopname, data.username);
storageAgreement = await this.documentInteractor.generateDocument({
data: {
coopname: data.coopname,
username: data.username,
lang,
registry_id: Cooperative.Registry.StorageAgreement.registry_id,
},
options: { skip_save: false },
});
} else {
this.logger.log(`Пропуск генерации соглашения о хранении для ${data.username}: уже подписано`);
}
// 3. BlagorostAgreement (соглашение Благорост) - генерируем если нет ни agreement, ни offer хэша
// и соглашение НЕ было предоставлено при импорте (is_external_blagorost_agreement)
// (оферта Благорост тоже считается соглашением, повторно генерировать не надо)
let blagorostAgreement: GeneratedDocumentDTO | undefined;
const needBlagorostAgreement =
!contributor.blagorost_agreement_hash &&
!contributor.blagorost_offer_hash &&
!contributor.is_external_blagorost_agreement;
if (needBlagorostAgreement) {
this.logger.log(`Генерация соглашения Благорост для ${data.username}`);
// Генерируем параметры для соглашения Благорост
await this.udataDocumentParametersService.generateBlagorostAgreementParametersIfNotExist(data.coopname, data.username);
const capitalProgramDocDataHash = await this.getCapitalProgramDocDataHash();
blagorostAgreement = await this.documentInteractor.generateDocument({
data: {
coopname: data.coopname,
username: data.username,
lang,
registry_id: Cooperative.Registry.BlagorostAgreement.registry_id,
doc_data_hash: capitalProgramDocDataHash,
},
options: { skip_save: false },
});
} else {
this.logger.log(
`Пропуск генерации соглашения Благорост для ${data.username}: ${
contributor.is_external_blagorost_agreement
? 'предоставлено при импорте'
: contributor.blagorost_offer_hash
? 'оферта уже подписана'
: 'уже подписано'
}`
);
}
// 4. GeneratorOffer (оферта Генератор) - генерируем если нет хэша
let generatorOffer: GeneratedDocumentDTO | undefined;
const needGeneratorOffer = !contributor.generator_offer_hash;
if (needGeneratorOffer) {
this.logger.log(`Генерация оферты Генератор для ${data.username}`);
// Генерируем параметры для оферты Генератор
await this.udataDocumentParametersService.generateGeneratorOfferParametersIfNotExist(data.coopname, data.username);
const capitalProgramDocDataHash = await this.getCapitalProgramDocDataHash();
generatorOffer = await this.documentInteractor.generateDocument({
data: {
coopname: data.coopname,
username: data.username,
lang,
registry_id: Cooperative.Registry.GeneratorOffer.registry_id,
doc_data_hash: capitalProgramDocDataHash,
},
options: { skip_save: false },
});
} else {
this.logger.log(`Пропуск генерации оферты Генератор для ${data.username}: уже подписана`);
}
return {
generation_contract: generationContract,
storage_agreement: storageAgreement,
blagorost_agreement: blagorostAgreement,
generator_offer: generatorOffer,
};
}
/**
* Завершение регистрации в Capital через отправку документов в блокчейн
* Отправляет документы через regcontrib с учетом выбранной программы
*/
async completeCapitalRegistration(data: CompleteCapitalRegistrationDomainInput): Promise<TransactResult> {
// Получаем или создаем Contributor для участника
let contributor = await this.contributorRepository.findByUsername(data.username);
if (!contributor) {
// Создаем нового Contributor, если он не существует
const displayName = await this.accountDataPort.getDisplayName(data.username);
const contributorData = {
_id: '',
present: false,
username: data.username,
coopname: config.coopname,
display_name: displayName,
program_key: ProgramKey.UNDEFINED, // Для пользователей, регистрирующихся только в Capital, program_key не указан
status: ContributorStatus.PENDING,
contributor_hash: generateRandomHash(),
blagorost_offer_hash: undefined,
generator_offer_hash: undefined,
generation_contract_hash: undefined,
storage_agreement_hash: undefined,
blagorost_agreement_hash: undefined,
};
contributor = new ContributorDomainEntity(contributorData);
await this.contributorRepository.create(contributor);
this.logger.log(`Создан новый Contributor для участника ${data.username} при регистрации в Capital расширении`);
}
// Валидация документов из базы данных
const documentsToValidate: Array<{ hash: string; name: string }> = [
{ hash: data.storage_agreement.doc_hash, name: 'StorageAgreement' },
];
// Добавляем GenerationContract только если НЕ внешний контракт
if (!contributor.is_external_contract && data.generation_contract) {
documentsToValidate.push({
hash: data.generation_contract.doc_hash,
name: 'GenerationContract'
});
}
// Добавляем BlagorostAgreement только если НЕ предоставлено при импорте
if (data.blagorost_agreement && !contributor.is_external_blagorost_agreement) {
documentsToValidate.push({
hash: data.blagorost_agreement.doc_hash,
name: 'BlagorostAgreement'
});
}
if (data.generator_offer) {
documentsToValidate.push({
hash: data.generator_offer.doc_hash,
name: 'GeneratorOffer'
});
}
for (const doc of documentsToValidate) {
const document = await this.documentInteractor.getDocumentByHash(doc.hash);
if (!document) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Документ ${doc.name} с хэшем ${doc.hash} не найден в базе данных`
);
}
if (document.meta.username !== data.username) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
`Username в документе ${doc.name} не совпадает с переданным`
);
}
}
// Преобразуем документы в формат блокчейна
const blockchainContract = data.generation_contract
? this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.generation_contract)
: undefined;
const blockchainStorageAgreement =
this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.storage_agreement);
const blockchainBlagorostAgreement = data.blagorost_agreement
? this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.blagorost_agreement)
: undefined;
const blockchainGeneratorAgreement = data.generator_offer
? this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.generator_offer)
: undefined;
// Форматируем rate_per_hour в asset строку
let formattedRatePerHour: string;
if (data.rate_per_hour) {
formattedRatePerHour = this.domainToBlockchainUtils.formatNumericStringToAssetString(
data.rate_per_hour,
config.blockchain.root_govern_precision,
config.blockchain.root_govern_symbol
);
} else {
formattedRatePerHour = '0.0000 ' + config.blockchain.root_govern_symbol;
}
// Отправляем в блокчейн через regcontrib
const result = await this.capitalBlockchainPort.registerContributorWithAgreements({
coopname: data.coopname,
username: data.username,
contributor_hash: contributor.contributor_hash,
rate_per_hour: formattedRatePerHour,
hours_per_day: data.hours_per_day || 0,
is_external_contract: contributor.is_external_contract || false,
contract: blockchainContract,
storage_agreement: blockchainStorageAgreement,
blagorost_agreement: blockchainBlagorostAgreement,
generator_agreement: blockchainGeneratorAgreement,
});
// Обновляем Contributor с хешами документов и данными формы
// GenerationContract обновляем только если он был предоставлен
if (data.generation_contract) {
contributor.generation_contract_hash = data.generation_contract.doc_hash;
}
contributor.storage_agreement_hash = data.storage_agreement.doc_hash;
if (data.blagorost_agreement) {
contributor.blagorost_agreement_hash = data.blagorost_agreement.doc_hash;
}
if (data.generator_offer) {
contributor.generator_offer_hash = data.generator_offer.doc_hash;
}
// Обновляем данные из формы регистрации
if (data.about !== undefined) {
contributor.about = data.about;
}
if (data.rate_per_hour !== undefined) {
contributor.rate_per_hour = data.rate_per_hour;
}
if (data.hours_per_day !== undefined) {
contributor.hours_per_day = data.hours_per_day;
}
await this.contributorRepository.update(contributor);
return result;
}
}