fix(registration): resolve doc_data_hash for extension offers via registry spec
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled

Шаблоны оферт ЦПП (#996/#1000) с c7824545cf требуют PrivateData, но общий
путь регистрации (RegistrationDocumentsService) не передавал doc_data_hash —
регистрация пайщика по GENERATION/CAPITALIZATION падала на генерации превью.

Ядро остаётся слепым к расширениям: AgreementRegistrationSpec получает
опциональный resolve_doc_data_hash, который расширение-владелец прикрепляет
к своим офертам при регистрации в реестре. Capital резолвит hash свежим
чтением своего конфига на каждую генерацию (совет может пересохранить
параметры без перезапуска). Гранулярность — per-оферта: при разделении
параметров по программам ядро менять не потребуется.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
coopops
2026-07-19 08:41:57 +00:00
parent a6cc71d6a3
commit a2b132b457
6 changed files with 58 additions and 2 deletions
@@ -60,6 +60,14 @@ export interface IAgreementConfigItem {
* Порядок отображения на фронтенде (сортировка)
*/
order: number;
/**
* Резолвер hash'а приватных данных документа (doc_data_hash), если шаблон
* оферты на фабрике требует PrivateData. Заполняется только для
* extension-оферт из AgreementRegistrationSpec; платформенные соглашения
* его не имеют. В GraphQL DTO не попадает (поля мапятся декораторами).
*/
resolve_doc_data_hash?: () => Promise<string | undefined>;
}
/**
@@ -55,4 +55,18 @@ export interface AgreementRegistrationSpec {
* реестр удаляет все записи с этим extension_name.
*/
extension_name: string;
/**
* Опциональный резолвер hash'а приватных данных документа (doc_data_hash).
* Если шаблон оферты на фабрике требует PrivateData (например, параметры
* ЦПП, утверждённые советом), расширение предоставляет резолвер — ядро
* вызывает его перед каждой генерацией документа этой оферты и передаёт
* результат в фабрику как doc_data_hash.
*
* Резолвер вызывается на каждую генерацию (не кэшируется): источник
* значения — конфигурация расширения, которая может обновляться без
* перезапуска (совет пересохранил параметры программ). Ядро не знает,
* откуда берётся hash — это контракт расширения с его фабричным шаблоном.
*/
resolve_doc_data_hash?: () => Promise<string | undefined>;
}
@@ -50,6 +50,7 @@ export class AgreementConfigurationService {
link_to_statement: true,
applicable_account_types: spec.applicable_account_types,
order: spec.order,
resolve_doc_data_hash: spec.resolve_doc_data_hash,
};
}
@@ -132,11 +132,17 @@ export class RegistrationDocumentsService {
): Promise<IGeneratedRegistrationDocument> {
this.logger.debug(`Генерация документа: ${config.id} (registry_id=${config.registry_id})`);
// Если шаблон оферты требует PrivateData, расширение-владелец предоставило
// резолвер hash'а в спеке регистрации — ядро не знает ни источника значения,
// ни того, какие registry_id этого требуют.
const doc_data_hash = config.resolve_doc_data_hash ? await config.resolve_doc_data_hash() : undefined;
const document = await this.documentInteractor.generateDocument({
data: {
coopname,
username,
registry_id: config.registry_id,
...(doc_data_hash ? { doc_data_hash } : {}),
},
options: {
skip_save: false, // Сохраняем документ в базу для последующей сверки
@@ -26,10 +26,17 @@ import type { IConfig } from '../../capital-extension.module';
* • при завершённом L1 — две оферты (generator/blagorost) и две
* программы (generation/capitalization);
* • идемпотентность гарантируется AgreementRegistryService.
*
* `resolveDocDataHash` — резолвер hash'а PrivateData параметров ЦПП,
* прикрепляется к обеим офертам: их фабричные шаблоны (#996/#1000) требуют
* doc_data. Сейчас все документы ЦПП читают единый набор параметров
* (capital_program_doc_data_hash); если параметры разделятся по программам,
* каждая спека получит собственный резолвер — ядро менять не потребуется.
*/
export function registerCapitalInAgreementRegistry(
port: AgreementRegistrationPort,
extensionConfig: IConfig
extensionConfig: IConfig,
resolveDocDataHash?: () => Promise<string | undefined>
): boolean {
const onboardingDone =
extensionConfig.onboarding_generator_program_template_done &&
@@ -52,6 +59,7 @@ export function registerCapitalInAgreementRegistry(
applicable_account_types: [],
order: 6,
extension_name: CAPITAL_EXTENSION_NAME,
resolve_doc_data_hash: resolveDocDataHash,
});
port.registerAgreement({
@@ -70,6 +78,7 @@ export function registerCapitalInAgreementRegistry(
applicable_account_types: [],
order: 5,
extension_name: CAPITAL_EXTENSION_NAME,
resolve_doc_data_hash: resolveDocDataHash,
});
port.registerProgram({
@@ -654,7 +654,25 @@ export class CapitalPlugin extends BaseExtModule {
// Если L1 ещё не завершён — реестр остаётся пустым, SignUp не предлагает
// программы capital (раздел 4.1 req 44 проекта 13).
try {
const registered = registerCapitalInAgreementRegistry(this.agreementRegistrationPort, extensionConfig as IConfig);
// Резолвер читает конфиг заново на каждую генерацию: совет может
// пересохранить параметры ЦПП (новый hash) без перезапуска расширения,
// а спека в реестре регистрируется один раз на initialize.
const resolveCapitalProgramDocDataHash = async (): Promise<string | undefined> => {
const ext = await this.extensionRepository.findByName(this.name);
const hash = ext?.config?.capital_program_doc_data_hash?.trim();
if (!hash) {
throw new Error(
'Параметры документов ЦПП не заполнены: отсутствует capital_program_doc_data_hash в конфигурации capital'
);
}
return hash;
};
const registered = registerCapitalInAgreementRegistry(
this.agreementRegistrationPort,
extensionConfig as IConfig,
resolveCapitalProgramDocDataHash
);
if (registered) {
this.logger.log('[CAPITAL.REGISTRY] зарегистрировано 2 оферты + 2 программы capital');
} else {