Compare commits

...

5 Commits

Author SHA1 Message Date
Alex Ant 74dce675b9 fix(install): блокировать переход с формы совета без заполненных данных
Кнопка «Продолжить» проверяла только число слотов совета, а не email и
обязательные поля ФЛ. Добавлена валидация на фронте и в install.interactor,
чтобы частично заполненный состав не уходил на цепь.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 13:04:43 +03:00
Alex Ant 45feef2676 fix(capital): correct CPP template mapping and onboarding widget canon
Typecheck / desktop (pull_request) Failing after 13m22s
Typecheck / controller (pull_request) Failing after 13m26s
Fix swapped doc_data placeholders in generator/blagorost templates, align the install widget with Base* components, and use one shared PrivateData hash in factory tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 21:49:30 +03:00
Alex Ant 02ce9400a2 fix(capital): remove unsafe any from CPP onboarding changes
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 11:05:20 +05:00
Alex Ant bfa2830650 feat(capital): collect CPP document parameters in onboarding
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 10:44:02 +05:00
Alex Ant c7824545cf feat(capital): parameterize CPP templates via private data 2026-06-28 09:50:03 +05:00
42 changed files with 1182 additions and 76 deletions
@@ -0,0 +1,129 @@
import { createHash } from 'node:crypto';
import type { DataSource } from 'typeorm';
import mongoose from 'mongoose';
import config from '~/config/config';
type MigrationLogger = {
info: (message: string) => void;
error: (message: string) => void;
warn: (message: string) => void;
};
const CAPITAL_DOC_REGISTRY_ID = 994;
const TARGET_COOPNAME = 'voskhod';
const COLLECTION_NAME = 'doc_private_data';
const capitalProgramPrivateData = {
approval_protocol_number: '1',
approval_protocol_day: '15',
approval_protocol_month: 'января',
approval_protocol_year: '26',
cooperative_name: 'ВОСХОД',
cooperative_short_name: 'ПК',
cooperative_quoted_name: '«ВОСХОД»',
website: 'https://цифровой-кооператив.рф',
chairman_full_name: 'Муравьев Алексей Николаевич',
generator_program_purpose:
'развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) под названием “Кооперативная Экономика”',
eoap_definition:
'информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”',
generator_task_goal:
'центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений',
idea_unit_cost: '50',
idea_unit_cost_words: 'пятьдесят',
blagorost_goal_expansion:
'вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате',
blagorost_task_expansion:
'расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате',
blagorost_task_development:
'развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений',
return_source_description:
'аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕОАП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий',
return_additional_source:
'взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП',
offer_template_number: '______',
};
function stableStringify(value: unknown): string {
if (value === null || typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
const entries = Object.keys(value as Record<string, unknown>)
.sort()
.map((key) => {
const v = (value as Record<string, unknown>)[key];
return `${JSON.stringify(key)}:${stableStringify(v)}`;
});
return `{${entries.join(',')}}`;
}
function calculateSha256(value: unknown): string {
return createHash('sha256').update(Buffer.from(stableStringify(value), 'utf8')).digest('hex').toUpperCase();
}
export default {
name: 'Backfill capital_program_doc_data_hash for voskhod',
validUntil: new Date('2027-05-01T00:00:00Z'),
async up({ dataSource, logger }: { dataSource: DataSource; logger: MigrationLogger }): Promise<boolean> {
if (config.coopname !== TARGET_COOPNAME) {
logger.info(`COOPNAME=${config.coopname}: миграция предназначена только для ${TARGET_COOPNAME}, пропускаю.`);
return true;
}
const hash = calculateSha256(capitalProgramPrivateData);
let mongo: mongoose.Connection | null = null;
try {
mongo = await mongoose.createConnection(config.mongoose.url).asPromise();
const db = mongo.db;
const collection = db.collection(COLLECTION_NAME);
await collection.createIndex({ hash: 1 }, { unique: true });
await collection.updateOne(
{ hash },
{
$setOnInsert: {
hash,
registry_id: CAPITAL_DOC_REGISTRY_ID,
payload: capitalProgramPrivateData,
_created_at: new Date(),
},
},
{ upsert: true }
);
const result = await dataSource.query(
`UPDATE extensions
SET config = jsonb_set(COALESCE(config, '{}'::jsonb), '{capital_program_doc_data_hash}', to_jsonb($1::text), true),
updated_at = NOW()
WHERE name = 'capital'`,
[hash]
);
logger.info(`capital_program_doc_data_hash=${hash} сохранён для ${TARGET_COOPNAME}; extensions updated=${result?.[1] ?? 'unknown'}.`);
return true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(`Ошибка миграции capital_program_doc_data_hash для ${TARGET_COOPNAME}: ${message}`);
return false;
} finally {
await mongo?.close();
}
},
async down({ dataSource, logger }: { dataSource: DataSource; logger: MigrationLogger }): Promise<boolean> {
if (config.coopname !== TARGET_COOPNAME) {
return true;
}
await dataSource.query(
`UPDATE extensions
SET config = COALESCE(config, '{}'::jsonb) - 'capital_program_doc_data_hash',
updated_at = NOW()
WHERE name = 'capital'`
);
logger.warn('capital_program_doc_data_hash удалён из config capital; запись doc_private_data оставлена как идемпотентный audit trail.');
return true;
},
};
@@ -28,6 +28,39 @@ import { normalizeUserEmail } from '~/utils/normalize-user-email';
/** Минимум членов совета — как MIN_SOVIET_MEMBERS_COUNT в контракте soviet (3 prod / 1 dev). */
const MIN_SOVIET_MEMBERS_COUNT = config.min_soviet_members_count;
function councilRoleLabel(index: number, role?: string): string {
if (role === 'chairman' || index === 0) return 'председателя совета';
return `члена совета №${index + 1}`;
}
function assertSovietMemberComplete(
member: InstallInputDomainInterface['soviet'][number],
index: number,
): void {
const data = member?.individual_data;
const label = councilRoleLabel(index, member?.role);
const email = typeof data?.email === 'string' ? data.email.trim() : '';
if (!email) {
throw new BadRequestException(`Не указан email для ${label}`);
}
const requiredStringFields: Array<[keyof Cooperative.Users.IIndividualData, string]> = [
['last_name', 'фамилия'],
['first_name', 'имя'],
['full_address', 'адрес регистрации'],
['birthdate', 'дата рождения'],
['phone', 'телефон'],
];
for (const [field, humanName] of requiredStringFields) {
const value = data?.[field];
if (typeof value !== 'string' || !value.trim()) {
throw new BadRequestException(`Не заполнено поле «${humanName}» для ${label}`);
}
}
}
@Injectable()
export class InstallInteractor {
constructor(
@@ -140,6 +173,8 @@ export class InstallInteractor {
);
}
data.soviet.forEach((member, index) => assertSovietMemberComplete(member, index));
// Повтор после прерванной установки: снимаем maintenance, дальше — обычный прогон.
if (isIncompleteInstallMaintenance) {
await this.monoStatusRepository.setStatus(SystemStatus.initialized);
@@ -22,6 +22,26 @@ export class DocumentDomainService {
) {}
public async generateDocument(data: GenerateDocumentDomainInterfaceWithOptions): Promise<DocumentDomainEntity> {
const documentData = data.data as Cooperative.Document.IGenerate & {
doc_data?: Record<string, unknown>;
doc_data_hash?: string;
};
if (documentData.doc_data) {
const { doc_data, ...publicDocumentData } = documentData;
const { hash } = documentData.doc_data_hash
? { hash: documentData.doc_data_hash }
: await this.saveDocData(doc_data, documentData.registry_id);
return await this.generatorInfrastructureService.generateDocument({
...data,
data: {
...publicDocumentData,
doc_data_hash: hash,
} as Cooperative.Document.IGenerate,
});
}
return await this.generatorInfrastructureService.generateDocument(data);
}
@@ -68,6 +68,9 @@ export class CapitalOnboardingStateDTO {
@Field(() => String, { nullable: true })
onboarding_blagorost_offer_template_hash?: string | null;
@Field(() => String, { nullable: true })
capital_program_doc_data_hash?: string | null;
@Field(() => String)
onboarding_init_at!: string;
@@ -29,6 +29,10 @@ type OnboardingHashKey =
| 'onboarding_blagorost_provision_hash'
| 'onboarding_blagorost_offer_template_hash';
type CapitalOnboardingConfig = IConfig &
Partial<Record<OnboardingFlagKey, boolean>> &
Partial<Record<OnboardingHashKey | 'onboarding_init_at' | 'onboarding_expire_at' | 'capital_program_doc_data_hash', string>>;
@Injectable()
export class CapitalOnboardingService {
constructor(
@@ -88,10 +92,40 @@ export class CapitalOnboardingService {
}
}
private async loadPlugin(): Promise<ExtensionDomainEntity<IConfig & Record<string, any>>> {
private isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
private getMetaString(meta: unknown, key: string): string | undefined {
if (!this.isRecord(meta)) return undefined;
const value = meta[key];
return typeof value === 'string' ? value : undefined;
}
private isSignatureInfo(value: unknown): value is ISignedDocumentDomainInterface['signatures'][number] {
if (!this.isRecord(value)) return false;
return (
typeof value.id === 'number' &&
typeof value.signed_hash === 'string' &&
typeof value.signer === 'string' &&
typeof value.public_key === 'string' &&
typeof value.signature === 'string' &&
typeof value.signed_at === 'string' &&
typeof value.meta === 'string'
);
}
private getMetaSignatures(meta: unknown): ISignedDocumentDomainInterface['signatures'] {
if (!this.isRecord(meta)) return [];
const value = meta.signatures;
return Array.isArray(value) ? value.filter((item) => this.isSignatureInfo(item)) : [];
}
private async loadPlugin(): Promise<ExtensionDomainEntity<CapitalOnboardingConfig>> {
const plugin = await this.extensionRepository.findByName('capital');
if (!plugin) throw new Error('Конфигурация расширения capital не найдена');
const pluginConfig = { ...plugin.config } as IConfig & Record<string, any>;
const pluginConfig: CapitalOnboardingConfig = { ...plugin.config };
let needUpdate = false;
if (!pluginConfig.onboarding_init_at) {
@@ -113,7 +147,7 @@ export class CapitalOnboardingService {
return { ...plugin, config: pluginConfig };
}
private buildState(pluginConfig: IConfig & Record<string, any>): CapitalOnboardingStateDTO {
private buildState(pluginConfig: CapitalOnboardingConfig): CapitalOnboardingStateDTO {
return {
generator_program_template_done: !!pluginConfig.onboarding_generator_program_template_done,
onboarding_generator_program_template_hash: pluginConfig.onboarding_generator_program_template_hash || null,
@@ -125,6 +159,7 @@ export class CapitalOnboardingService {
onboarding_blagorost_provision_hash: pluginConfig.onboarding_blagorost_provision_hash || null,
blagorost_offer_template_done: !!pluginConfig.onboarding_blagorost_offer_template_done,
onboarding_blagorost_offer_template_hash: pluginConfig.onboarding_blagorost_offer_template_hash || null,
capital_program_doc_data_hash: pluginConfig.capital_program_doc_data_hash || null,
onboarding_init_at: pluginConfig.onboarding_init_at || '',
onboarding_expire_at: pluginConfig.onboarding_expire_at || '',
};
@@ -141,7 +176,7 @@ export class CapitalOnboardingService {
const hashKey = this.mapStepToHash(data.step);
const normalizedTitle = data.title?.trim().substring(0, 200) || undefined;
if ((plugin.config as any)[flagKey]) {
if (plugin.config[flagKey]) {
return this.buildState(plugin.config);
}
const project_id = uuid();
@@ -168,12 +203,12 @@ export class CapitalOnboardingService {
// Публикуем проект решения в блокчейн сразу после генерации
const documentForPublish: ISignedDocumentDomainInterface = {
version: (generatedDoc.meta as any)?.version || '1.0',
version: this.getMetaString(generatedDoc.meta, 'version') || '1.0',
hash: generatedDoc.hash,
doc_hash: (generatedDoc.meta as any)?.doc_hash || generatedDoc.hash,
meta_hash: (generatedDoc.meta as any)?.meta_hash || generatedDoc.hash,
doc_hash: this.getMetaString(generatedDoc.meta, 'doc_hash') || generatedDoc.hash,
meta_hash: this.getMetaString(generatedDoc.meta, 'meta_hash') || generatedDoc.hash,
meta: generatedDoc.meta,
signatures: (generatedDoc.meta as any)?.signatures || [],
signatures: this.getMetaSignatures(generatedDoc.meta),
};
await this.freeDecisionPort.publishProjectOfFreeDecision({
@@ -51,6 +51,11 @@ 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 контракте
@@ -76,8 +81,24 @@ export class ParticipationManagementInteractor {
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;
}
/**
* Получение отображаемого имени из аккаунта через порт расширения
*/
@@ -313,7 +334,8 @@ export class ParticipationManagementInteractor {
}
// Извлекаем appendix_hash из метаданных документа
const appendix_hash = (document.meta as any).appendix_hash;
const documentMeta: Record<string, unknown> = document.meta;
const appendix_hash = typeof documentMeta.appendix_hash === 'string' ? documentMeta.appendix_hash : undefined;
//TODO: адаптировать или документ или код ниже к parent_appendix_hash
if (!appendix_hash) {
@@ -778,6 +800,7 @@ export class ParticipationManagementInteractor {
// Генерируем параметры для соглашения Благорост
await this.udataDocumentParametersService.generateBlagorostAgreementParametersIfNotExist(data.coopname, data.username);
const capitalProgramDocDataHash = await this.getCapitalProgramDocDataHash();
blagorostAgreement = await this.documentInteractor.generateDocument({
data: {
@@ -785,6 +808,7 @@ export class ParticipationManagementInteractor {
username: data.username,
lang,
registry_id: Cooperative.Registry.BlagorostAgreement.registry_id,
doc_data_hash: capitalProgramDocDataHash,
},
options: { skip_save: false },
});
@@ -809,6 +833,7 @@ export class ParticipationManagementInteractor {
// Генерируем параметры для оферты Генератор
await this.udataDocumentParametersService.generateGeneratorOfferParametersIfNotExist(data.coopname, data.username);
const capitalProgramDocDataHash = await this.getCapitalProgramDocDataHash();
generatorOffer = await this.documentInteractor.generateDocument({
data: {
@@ -816,6 +841,7 @@ export class ParticipationManagementInteractor {
username: data.username,
lang,
registry_id: Cooperative.Registry.GeneratorOffer.registry_id,
doc_data_hash: capitalProgramDocDataHash,
},
options: { skip_save: false },
});
@@ -40,6 +40,7 @@ export const defaultConfig = {
onboarding_generator_offer_template_done: false,
onboarding_blagorost_provision_done: false,
onboarding_blagorost_offer_template_done: false,
capital_program_doc_data_hash: '',
/** Ветка для выборки коммитов (FR3 / PRD); URL репозитория — на проекте/компоненте (PRD §6.2.1). */
github_sync_branch: 'dev',
/** Интервал polling GitHub API в минутах (FR2); 0 — периодический опрос отключён. */
@@ -253,6 +254,10 @@ export const Schema = z.object({
.boolean()
.default(defaultConfig.onboarding_blagorost_offer_template_done)
.describe(describeField({ label: 'Шаг предложения Благорост выполнен', visible: false })),
capital_program_doc_data_hash: z
.string()
.default(defaultConfig.capital_program_doc_data_hash)
.describe(describeField({ label: 'PrivateData документов ЦПП', visible: false })),
});
@@ -577,8 +582,10 @@ export class CapitalPlugin extends BaseExtModule {
} else {
this.logger.log('Конфигурация контракта CAPITAL уже актуальна');
}
} catch (error: any) {
this.logger.error(`Не удалось синхронизировать конфигурацию с контрактом CAPITAL: ${error.message}`, error.stack);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
this.logger.error(`Не удалось синхронизировать конфигурацию с контрактом CAPITAL: ${message}`, stack);
// Не бросаем ошибку, чтобы не блокировать запуск модуля
}
@@ -586,10 +593,12 @@ export class CapitalPlugin extends BaseExtModule {
try {
await this.syncInteractor.initializeSync();
this.logger.log('Синхронизация благороста с блокчейном инициализирована');
} catch (error: any) {
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
this.logger.error(
`Не удалось инициализировать синхронизацию благороста с блокчейном: ${error.message}`,
error.stack
`Не удалось инициализировать синхронизацию благороста с блокчейном: ${message}`,
stack
);
// Не бросаем ошибку, чтобы не блокировать запуск модуля
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,64 @@
export interface CapitalProgramPrivateData {
approval_protocol_number: string
approval_protocol_day: string
approval_protocol_month: string
approval_protocol_year: string
cooperative_name: string
cooperative_short_name: string
cooperative_quoted_name: string
website: string
chairman_full_name: string
generator_program_purpose: string
eoap_definition: string
generator_task_goal: string
idea_unit_cost: string
idea_unit_cost_words: string
blagorost_goal_expansion: string
blagorost_task_expansion: string
blagorost_task_development: string
return_source_description: string
return_additional_source: string
offer_template_number: string
}
export const capitalProgramPrivateDataRequiredFields = [
'approval_protocol_number',
'approval_protocol_day',
'approval_protocol_month',
'approval_protocol_year',
'cooperative_name',
'cooperative_short_name',
'cooperative_quoted_name',
'website',
'chairman_full_name',
'generator_program_purpose',
'eoap_definition',
'generator_task_goal',
'idea_unit_cost',
'idea_unit_cost_words',
'blagorost_goal_expansion',
'blagorost_task_expansion',
'blagorost_task_development',
'return_source_description',
'return_additional_source',
'offer_template_number',
] as const
export function requireCapitalProgramPrivateData(
payload: CapitalProgramPrivateData | null | undefined,
registry_id: number,
): CapitalProgramPrivateData {
if (!payload)
throw new Error(`PrivateData для документа #${registry_id} не найдены: передайте doc_data_hash`)
const missing = capitalProgramPrivateDataRequiredFields.filter((field) => {
const value = payload[field]
return typeof value !== 'string' || value.trim() === ''
})
if (missing.length) {
throw new Error(`PrivateData для документа #${registry_id} заполнены не полностью: ${missing.join(', ')}`)
}
return payload
}
@@ -80,3 +80,5 @@ export * as AnnualGeneralMeetingSovietDecision from './301.AnnualGeneralMeetingS
export * as AnnualGeneralMeetingNotification from './302.AnnualGeneralMeetingNotification'
export * as AnnualGeneralMeetingVotingBallot from './303.AnnualGeneralMeetingVotingBallot'
export * as AnnualGeneralMeetingDecision from './304.AnnualGeneralMeetingDecision'
export * from './capitalProgramPrivateData'
@@ -1,2 +1,3 @@
export * from './ui';
export { useCapitalOnboarding } from './model';
export type { CapitalOnboardingState } from './api';
@@ -13,6 +13,8 @@ interface GeneratedDocument {
full_title: string;
}
type CapitalOnboardingStepId = Mutations.Capital.CompleteOnboardingStep.IInput['data']['step'];
export const useCapitalOnboarding = () => {
const systemStore = useSystemStore();
const sessionStore = useSessionStore();
@@ -24,30 +26,40 @@ export const useCapitalOnboarding = () => {
const currentGeneratedDoc = ref<GeneratedDocument | null>(null);
// Маппинг шагов на registry_id
const stepToRegistryId: Record<string, number> = {
const stepToRegistryId: Record<CapitalOnboardingStepId, number> = {
'generator_program_template': 994,
'generation_contract_template': 997,
'generator_offer_template': 995,
'blagorost_program': 998,
'blagorost_offer_template': 999,
};
const capitalProgramDocDataRegistryIds = new Set([994, 995, 998, 999]);
const isCapitalOnboardingStepId = (stepId: string): stepId is CapitalOnboardingStepId => {
return stepId in stepToRegistryId;
};
// Генерация документа для шага
const generateDocument = async (step: ICouncilOnboardingStep): Promise<GeneratedDocument> => {
try {
generatingDocument.value = true;
const registry_id = stepToRegistryId[step.id];
if (!registry_id) {
if (!isCapitalOnboardingStepId(step.id)) {
throw new Error(`Неизвестный шаг онбординга: ${step.id}`);
}
const registry_id = stepToRegistryId[step.id];
const docDataHash = onboardingState.value?.capital_program_doc_data_hash;
if (capitalProgramDocDataRegistryIds.has(registry_id) && !docDataHash) {
throw new Error('Параметры документов ЦПП не заполнены: переустановите или обновите настройки расширения capital');
}
const generateDocInput: Mutations.Documents.GenerateDocument.IInput = {
input: {
data: {
coopname: systemStore.info?.coopname || '',
username: sessionStore.username,
registry_id,
...(docDataHash && capitalProgramDocDataRegistryIds.has(registry_id) ? { doc_data_hash: docDataHash } : {}),
},
},
};
@@ -189,9 +201,13 @@ export const useCapitalOnboarding = () => {
await handleStepClick(step);
}
if (!isCapitalOnboardingStepId(step.id)) {
throw new Error(`Неизвестный шаг онбординга: ${step.id}`);
}
// Подготавливаем данные для отправки
const stepData: Mutations.Capital.CompleteOnboardingStep.IInput['data'] = {
step: step.id as any,
step: step.id,
title: step.title,
question: step.question,
decision: currentGeneratedDoc.value?.html || step.decisionPrefix || step.decision,
@@ -0,0 +1,387 @@
<template lang="pug">
BaseCard(title='Параметры документов ЦПП')
p.capital-doc-params__intro.t-body2
| Заполните поля, которые будут подставлены в положения и оферты программ «ГЕНЕРАТОР» и «БЛАГОРОСТ».
| Перед установкой расширения сформируйте предпросмотр документов: так параметры сохранятся в PrivateData, а в config попадёт только hash.
BaseInput(
:model-value='config?.capital_program_doc_data_hash || ""'
label='Hash PrivateData документов'
hint='Появится после формирования предпросмотра'
readonly
mono
)
BaseBanner(v-if='config?.capital_program_doc_data_hash' variant='pos')
| Параметры сохранены. Hash: {{ config.capital_program_doc_data_hash }}
q-expansion-item(default-opened label='Протокол и кооператив' icon='description')
.row.q-col-gutter-md.q-mt-sm
.col-12.col-md-3(v-for='field in protocolFields' :key='field.key')
BaseInput(
v-model='form[field.key]'
:label='field.label'
:error='fieldErrors[field.key]'
)
.col-12.col-md-6(v-for='field in cooperativeFields' :key='field.key')
BaseInput(
v-model='form[field.key]'
:label='field.label'
:error='fieldErrors[field.key]'
)
q-expansion-item(label='Программа ГЕНЕРАТОР' icon='bolt')
.capital-doc-params__section
BaseInput(
v-for='field in generatorFields'
:key='field.key'
v-model='form[field.key]'
:label='field.label'
:type='field.type || "textarea"'
:autogrow='field.type !== "text"'
:error='fieldErrors[field.key]'
)
q-expansion-item(label='Программа БЛАГОРОСТ' icon='eco')
.capital-doc-params__section
BaseInput(
v-for='field in blagorostFields'
:key='field.key'
v-model='form[field.key]'
:label='field.label'
type='textarea'
autogrow
:error='fieldErrors[field.key]'
)
q-expansion-item(label='Возврат, источники и оферты' icon='article')
.capital-doc-params__section
BaseInput(
v-for='field in returnFields'
:key='field.key'
v-model='form[field.key]'
:label='field.label'
:type='field.type || "textarea"'
:autogrow='field.type !== "text"'
:error='fieldErrors[field.key]'
)
.capital-doc-params__footer
p.capital-doc-params__hint.t-caption
| Предпросмотр генерирует документы без публикации и сохраняет PrivateData по hash.
BaseButton(:loading='isGenerating' @click='generatePreview')
| Сформировать предпросмотр
BaseDialog(v-model='previewOpen' title='Предпросмотр положений ЦПП' size='xl')
.capital-doc-params__preview(v-if='previewDocuments.length')
q-tabs(v-model='activePreviewTab' dense align='left' class='capital-doc-params__tabs')
q-tab(
v-for='doc in previewDocuments'
:key='doc.registry_id'
:name='String(doc.registry_id)'
:label='doc.title'
)
q-separator
q-tab-panels(v-model='activePreviewTab' animated)
q-tab-panel(
v-for='doc in previewDocuments'
:key='doc.registry_id'
:name='String(doc.registry_id)'
)
.doc-preview
DocumentHtmlReader(:html='doc.html' :sanitize='false')
template(#footer)
BaseButton(variant='ghost' @click='previewOpen = false')
| Закрыть
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import { Mutations } from '@coopenomics/sdk';
import { useSystemStore } from 'src/entities/System/model';
import { useSessionStore } from 'src/entities/Session';
import { client } from 'src/shared/api/client';
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { BaseBanner, BaseButton, BaseCard, BaseDialog, BaseInput } from 'src/shared/ui/base';
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
type CapitalProgramPrivateData = {
approval_protocol_number: string;
approval_protocol_day: string;
approval_protocol_month: string;
approval_protocol_year: string;
cooperative_name: string;
cooperative_short_name: string;
cooperative_quoted_name: string;
website: string;
chairman_full_name: string;
generator_program_purpose: string;
eoap_definition: string;
generator_task_goal: string;
idea_unit_cost: string;
idea_unit_cost_words: string;
blagorost_goal_expansion: string;
blagorost_task_expansion: string;
blagorost_task_development: string;
return_source_description: string;
return_additional_source: string;
offer_template_number: string;
};
type FieldDefinition = {
key: keyof CapitalProgramPrivateData;
label: string;
type?: 'text' | 'textarea';
};
type CapitalProgramConfig = Record<string, unknown> & {
capital_program_doc_data_hash?: string | null;
};
type GeneratedPreviewDocument = NonNullable<
Mutations.Documents.GenerateDocument.IOutput[typeof Mutations.Documents.GenerateDocument.name]
>;
interface Props {
config: CapitalProgramConfig;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(event: 'update:config', value: CapitalProgramConfig): void;
}>();
const system = useSystemStore();
const session = useSessionStore();
const isGenerating = ref(false);
const previewOpen = ref(false);
const activePreviewTab = ref('994');
const previewDocuments = ref<Array<{ registry_id: number; title: string; html: string; hash: string }>>([]);
const fieldErrors = reactive<Partial<Record<keyof CapitalProgramPrivateData, string>>>({});
const form = reactive<Omit<CapitalProgramPrivateData, 'cooperative_quoted_name'>>({
approval_protocol_number: '',
approval_protocol_day: '',
approval_protocol_month: '',
approval_protocol_year: '',
cooperative_name: '',
cooperative_short_name: '',
website: '',
chairman_full_name: '',
generator_program_purpose: '',
eoap_definition: '',
generator_task_goal: '',
idea_unit_cost: '',
idea_unit_cost_words: '',
blagorost_goal_expansion: '',
blagorost_task_expansion: '',
blagorost_task_development: '',
return_source_description: '',
return_additional_source: '',
offer_template_number: '______',
});
const protocolFields: FieldDefinition[] = [
{ key: 'approval_protocol_number', label: 'Номер протокола' },
{ key: 'approval_protocol_day', label: 'День протокола' },
{ key: 'approval_protocol_month', label: 'Месяц протокола' },
{ key: 'approval_protocol_year', label: 'Год протокола' },
];
const cooperativeFields: FieldDefinition[] = [
{ key: 'cooperative_name', label: 'Наименование кооператива' },
{ key: 'cooperative_short_name', label: 'Краткое наименование (ПК)' },
{ key: 'website', label: 'Сайт' },
{ key: 'chairman_full_name', label: 'ФИО председателя' },
];
const generatorFields: FieldDefinition[] = [
{ key: 'generator_program_purpose', label: 'Назначение программы ГЕНЕРАТОР' },
{ key: 'eoap_definition', label: 'Определение ЕОАП' },
{ key: 'generator_task_goal', label: 'Цель задач ГЕНЕРАТОР' },
{ key: 'idea_unit_cost', label: 'Стоимость единицы идеи', type: 'text' },
{ key: 'idea_unit_cost_words', label: 'Стоимость единицы идеи прописью', type: 'text' },
];
const blagorostFields: FieldDefinition[] = [
{ key: 'blagorost_goal_expansion', label: 'Расширение цели БЛАГОРОСТ' },
{ key: 'blagorost_task_expansion', label: 'Задача расширения БЛАГОРОСТ' },
{ key: 'blagorost_task_development', label: 'Задача развития БЛАГОРОСТ' },
];
const returnFields: FieldDefinition[] = [
{ key: 'return_source_description', label: 'Источник возврата' },
{ key: 'return_additional_source', label: 'Дополнительный источник возврата' },
{ key: 'offer_template_number', label: 'Номер шаблона оферты', type: 'text' },
];
const allFields = computed(() => [
...protocolFields,
...cooperativeFields,
...generatorFields,
...blagorostFields,
...returnFields,
]);
watch(
() => system.info,
(info) => {
if (!info) return;
form.cooperative_name ||= String(info.coopname || '').toUpperCase();
form.cooperative_short_name ||= 'ПК';
form.website ||= info.website || info.contacts?.website || '';
form.chairman_full_name ||= info.chairman?.full_name || info.chairman_full_name || '';
},
{ immediate: true }
);
function getPayload(): CapitalProgramPrivateData {
const cooperativeName = String(form.cooperative_name || '').trim();
return {
...Object.fromEntries(
allFields.value.map(({ key }) => [key, String(form[key] || '').trim()])
) as Omit<CapitalProgramPrivateData, 'cooperative_quoted_name'>,
cooperative_quoted_name: cooperativeName ? `«${cooperativeName}»` : '',
};
}
function validatePayload(payload: CapitalProgramPrivateData): string[] {
return allFields.value
.filter(({ key }) => !payload[key])
.map(({ label }) => label);
}
function clearFieldErrors(): void {
for (const key of Object.keys(fieldErrors) as Array<keyof CapitalProgramPrivateData>) {
delete fieldErrors[key];
}
}
function setFieldErrors(missingLabels: string[]): void {
clearFieldErrors();
for (const { key, label } of allFields.value) {
if (missingLabels.includes(label)) {
fieldErrors[key] = 'Заполните поле';
}
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function getDocDataHash(doc: GeneratedPreviewDocument): string | null {
if (!isRecord(doc.meta)) return null;
const hash = doc.meta.doc_data_hash;
return typeof hash === 'string' && hash.length > 0 ? hash : null;
}
async function generateDocument(registry_id: number, payload: CapitalProgramPrivateData): Promise<GeneratedPreviewDocument> {
const { [Mutations.Documents.GenerateDocument.name]: result } = await client.Mutation(
Mutations.Documents.GenerateDocument.mutation,
{
variables: {
input: {
data: {
coopname: system.info?.coopname || '',
username: session.username,
lang: 'ru',
registry_id,
doc_data: payload,
},
options: { skip_save: true },
},
},
}
);
if (!result) {
throw new Error('Документ не был сгенерирован');
}
return result;
}
async function generatePreview() {
const payload = getPayload();
const missing = validatePayload(payload);
if (missing.length) {
setFieldErrors(missing);
FailAlert(`Заполните параметры документов: ${missing.join(', ')}`);
return;
}
clearFieldErrors();
try {
isGenerating.value = true;
const docs = await Promise.all([
generateDocument(994, payload),
generateDocument(998, payload),
]);
const docDataHash = docs
.map(getDocDataHash)
.find((hash: unknown): hash is string => typeof hash === 'string' && hash.length > 0);
if (!docDataHash) {
throw new Error('Генератор не вернул doc_data_hash');
}
previewDocuments.value = docs.map((doc, index) => ({
registry_id: index === 0 ? 994 : 998,
title: doc.full_title,
html: doc.html,
hash: doc.hash,
}));
activePreviewTab.value = '994';
previewOpen.value = true;
emit('update:config', {
...props.config,
capital_program_doc_data_hash: docDataHash,
});
SuccessAlert('Параметры документов сохранены в PrivateData');
} catch (error) {
FailAlert(error);
} finally {
isGenerating.value = false;
}
}
</script>
<style scoped lang="scss">
.capital-doc-params__intro {
margin: 0 0 var(--p-4);
color: var(--p-ink-secondary);
}
.capital-doc-params__section {
display: flex;
flex-direction: column;
gap: var(--p-3);
margin-top: var(--p-2);
}
.capital-doc-params__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--p-4);
margin-top: var(--p-6);
}
.capital-doc-params__hint {
margin: 0;
color: var(--p-ink-secondary);
}
.capital-doc-params__tabs {
color: var(--p-primary);
}
.doc-preview :deep([data-doc-param]) {
background: var(--p-warn-soft);
border-radius: var(--p-r-sm);
padding: 0 2px;
}
</style>
@@ -1 +1,2 @@
export { default as CapitalOnboardingCard } from './CapitalOnboardingCard.vue';
export { default as CapitalProgramDocumentParametersWidget } from './CapitalProgramDocumentParametersWidget.vue';
@@ -3,3 +3,9 @@ export {
getSovietMembersProgressHint,
getSovietContinueBlockedTooltip,
} from './minSovietMembers';
export {
isSovietMemberComplete,
countCompleteSovietMembers,
getFirstIncompleteSovietMemberIndex,
type SovietInstallMember,
} from './sovietMemberValidation';
@@ -1,23 +1,57 @@
import { env } from 'src/shared/config';
import {
countCompleteSovietMembers,
getFirstIncompleteSovietMemberIndex,
type SovietInstallMember,
} from './sovietMemberValidation';
/** Как MIN_SOVIET_MEMBERS_COUNT в контракте soviet: 1 на dev/testnet, 3 на production. */
export function getMinSovietMembersCount(): number {
return env.NODE_ENV === 'production' ? 3 : 1;
}
function councilRoleLabel(index: number, role?: SovietInstallMember['role']): string {
if (role === 'chairman' || index === 0) return 'председателя совета';
return `члена совета №${index + 1}`;
}
/** Краткая подсказка под формой: сколько указано и чего не хватает. */
export function getSovietMembersProgressHint(currentCount: number, min = getMinSovietMembersCount()): string | null {
if (min <= 1 || currentCount >= min) return null;
const missing = min - currentCount;
return `Указано ${currentCount} из ${min}. Добавьте ещё ${missing} ${pluralCouncilSlot(missing)}.`;
export function getSovietMembersProgressHint(
members: SovietInstallMember[],
min = getMinSovietMembersCount(),
): string | null {
const completeCount = countCompleteSovietMembers(members);
if (completeCount < min) {
const missing = min - completeCount;
return `Заполнены данные ${completeCount} из ${min}. Дозаполните ещё ${missing} ${pluralCouncilSlot(missing)}.`;
}
const incompleteIndex = getFirstIncompleteSovietMemberIndex(members);
if (incompleteIndex >= 0) {
return `Заполните все поля для ${councilRoleLabel(incompleteIndex, members[incompleteIndex]?.role)}.`;
}
return null;
}
/** Текст для tooltip заблокированной кнопки «Продолжить». */
export function getSovietContinueBlockedTooltip(currentCount: number, min = getMinSovietMembersCount()): string {
if (currentCount >= min) return '';
if (min === 1) return 'Заполните данные председателя';
const missing = min - currentCount;
return `Для продолжения нужно ${min} человека в составе совета (ещё ${missing} ${pluralCouncilSlot(missing)})`;
export function getSovietContinueBlockedTooltip(
members: SovietInstallMember[],
min = getMinSovietMembersCount(),
): string {
const completeCount = countCompleteSovietMembers(members);
if (completeCount < min) {
const missing = min - completeCount;
if (min === 1) return 'Заполните все данные председателя совета';
return `Для продолжения нужно ${min} человека с полными данными (ещё ${missing} ${pluralCouncilSlot(missing)})`;
}
const incompleteIndex = getFirstIncompleteSovietMemberIndex(members);
if (incompleteIndex >= 0) {
return `Заполните все поля для ${councilRoleLabel(incompleteIndex, members[incompleteIndex]?.role)}`;
}
return '';
}
function pluralCouncilSlot(count: number): string {
@@ -0,0 +1,50 @@
import emailRegex from 'email-regex';
import type { IIndividualData } from 'src/shared/lib/types/user/IUserData';
const emailValidator = emailRegex({ exact: true });
/** Член совета в мастере установки (store + API install). */
export interface SovietInstallMember {
role?: 'chairman' | 'member';
individual_data?: IIndividualData & { email?: string };
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
function isPhoneFilled(value: unknown): boolean {
if (typeof value !== 'string') return false;
const trimmed = value.trim();
if (!trimmed) return false;
return trimmed !== '+7 (___) ___-__-__';
}
/** Все обязательные поля IndividualDataForm + email (как на экране «Состав совета»). */
export function isSovietMemberComplete(member: SovietInstallMember | null | undefined): boolean {
const data = member?.individual_data;
if (!data) return false;
if (!isNonEmptyString(data.email) || emailValidator.test(data.email.trim()) !== true) {
return false;
}
if (!isNonEmptyString(data.last_name) || !isNonEmptyString(data.first_name)) {
return false;
}
if (!isNonEmptyString(data.full_address) || !isNonEmptyString(data.birthdate)) {
return false;
}
if (!isPhoneFilled(data.phone)) {
return false;
}
return true;
}
export function countCompleteSovietMembers(members: SovietInstallMember[]): number {
return members.filter(isSovietMemberComplete).length;
}
export function getFirstIncompleteSovietMemberIndex(members: SovietInstallMember[]): number {
return members.findIndex((member) => !isSovietMemberComplete(member));
}
@@ -1,7 +1,7 @@
import type { Mutations, Queries } from '@coopenomics/sdk';
import { useInstallCooperativeStore } from 'src/entities/Installer/model'
import { stripLegacyBankKpp } from 'src/shared/lib/utils/stripLegacyBankKpp';
import { getMinSovietMembersCount } from '../lib';
import { getMinSovietMembersCount, isSovietMemberComplete } from '../lib';
import { api } from '../api'
export type IInstallInput = Mutations.System.InstallSystem.IInput['data']
@@ -61,6 +61,15 @@ export const useInstallCooperative = (): {
);
}
const incompleteIndex = store.soviet.findIndex((member) => !isSovietMemberComplete(member));
if (incompleteIndex >= 0) {
const roleLabel =
store.soviet[incompleteIndex]?.role === 'chairman' || incompleteIndex === 0
? 'председателя совета'
: `члена совета №${incompleteIndex + 1}`;
throw new Error(`Заполните все обязательные поля для ${roleLabel}`);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const soviet = store.soviet.map(({ id, type, ...rest }) => rest);
@@ -64,6 +64,7 @@ import {
getMinSovietMembersCount,
getSovietMembersProgressHint,
getSovietContinueBlockedTooltip,
isSovietMemberComplete,
} from '../../lib';
import { notEmpty } from 'src/shared/lib/utils';
import { validEmail } from 'src/shared/lib/utils/validEmailRule';
@@ -74,11 +75,15 @@ const minSovietMembers = getMinSovietMembersCount();
installStore.is_finish = false;
const sovietCount = computed(() => installStore.soviet.length);
const canContinue = computed(() => sovietCount.value >= minSovietMembers);
const progressHint = computed(() => getSovietMembersProgressHint(sovietCount.value, minSovietMembers));
const sovietMembers = computed(() => installStore.soviet);
const canContinue = computed(
() =>
sovietMembers.value.length >= minSovietMembers &&
sovietMembers.value.every(isSovietMemberComplete),
);
const progressHint = computed(() => getSovietMembersProgressHint(sovietMembers.value, minSovietMembers));
const continueBlockedTooltip = computed(() =>
getSovietContinueBlockedTooltip(sovietCount.value, minSovietMembers),
getSovietContinueBlockedTooltip(sovietMembers.value, minSovietMembers),
);
const add = () => {
@@ -33,6 +33,7 @@
)
ExtensionInstall(
v-if='isInstall',
:extension-name='extension.name',
:schema='extension.schema',
v-model:config='data',
:form-ref='myFormRef'
@@ -4,7 +4,8 @@ export interface BaseInputProps {
hint?: string;
error?: string;
placeholder?: string;
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search' | 'date';
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search' | 'date' | 'textarea';
autogrow?: boolean;
/** Моноширинный шрифт (для аккаунт-имён, hash, и т.п.) */
mono?: boolean;
/** Кнопка очистки значения (например, для фильтров по дате) */
@@ -12,6 +12,7 @@
:error-message="error"
:placeholder="placeholder"
:type="type"
:autogrow="autogrow"
:prefix="prefix"
:suffix="suffix"
:readonly="readonly"
@@ -53,6 +54,7 @@ const props = withDefaults(defineProps<BaseInputProps>(), {
readonly: false,
disabled: false,
required: false,
autogrow: false,
});
const emit = defineEmits<{
@@ -1,6 +1,11 @@
<template lang="pug">
.extension-install
q-form(ref='formRef')
q-form(:ref='setFormRef')
CapitalProgramDocumentParametersWidget(
v-if='extensionName === "capital"'
:config='config'
@update:config='$emit("update:config", $event)'
)
BaseCard(v-if='schema && !isEmpty', title='Настройки')
ZodForm(
:schema='schema',
@@ -14,19 +19,33 @@
<script lang="ts" setup>
import { computed } from 'vue';
import type { ComponentPublicInstance } from 'vue';
import { ZodForm } from 'src/shared/ui/ZodForm';
import { BaseCard } from 'src/shared/ui/base/BaseCard';
import { isExtensionSchemaEmpty } from 'src/shared/lib/utils';
import { CapitalProgramDocumentParametersWidget } from 'app/extensions/capital/features/Onboarding';
type ExtensionInstallConfig = Record<string, unknown>;
type ExtensionInstallFormRef = {
value: Element | ComponentPublicInstance | null | undefined;
};
interface Props {
schema?: any;
config: any;
formRef?: any;
extensionName?: string;
schema?: unknown;
config: ExtensionInstallConfig;
formRef?: ExtensionInstallFormRef;
}
const props = defineProps<Props>();
const isEmpty = computed(() => isExtensionSchemaEmpty(props.schema));
const setFormRef = (element: Element | ComponentPublicInstance | null) => {
if (props.formRef && 'value' in props.formRef) {
props.formRef.value = element;
}
};
</script>
<style scoped lang="scss">
@@ -17,7 +17,7 @@ export class Factory extends DocFactory<BlagorostOffer.Action> {
const udataService = new Udata(this.storage)
// Независимые источники тянем параллельно (см. resolveParallel в DocFactory)
const { template, vars, coop, userData, blagorostAgreementUdata, blagorostAgreementCreatedAtUdata } = await this.resolveParallel({
const { template, vars, coop, userData, blagorostAgreementUdata, blagorostAgreementCreatedAtUdata, docData } = await this.resolveParallel({
template: () => process.env.SOURCE === 'local'
? Promise.resolve(BlagorostOffer.Template as ITemplate<BlagorostOffer.Model>)
: this.getTemplate<BlagorostOffer.Model>(DraftContract.contractName.production, 1000, data.block_num),
@@ -36,12 +36,14 @@ export class Factory extends DocFactory<BlagorostOffer.Action> {
key: Cooperative.Model.UdataKey.BLAGOROST_AGREEMENT_CREATED_AT,
block_num: data.block_num,
}),
docData: () => this.loadDocData<BlagorostOffer.Model['doc_data']>(data),
})
// meta зависит от template.title — считаем после батча
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
const common_user = this.getCommonUser(userData)
const doc_data = Cooperative.Registry.requireCapitalProgramPrivateData(docData, BlagorostOffer.registry_id)
// Проверяем наличие данных протоколов, утвердивших предыдущие документы
if (!vars.blagorost_program?.protocol_number || !vars.blagorost_program?.protocol_day_month_year) {
@@ -68,6 +70,7 @@ export class Factory extends DocFactory<BlagorostOffer.Action> {
common_user,
blagorost_agreement_number: String(blagorostAgreementUdata.value),
blagorost_agreement_created_at: String(blagorostAgreementCreatedAtUdata.value),
doc_data,
}
await this.validate(combinedData, template.model)
@@ -1,4 +1,4 @@
import { DraftContract } from 'cooptypes'
import { Cooperative, DraftContract } from 'cooptypes'
import { GeneratorProgramTemplate } from '../Templates'
import { DocFactory } from '../Factory'
import type { IGeneratedDocument, IGenerationOptions, IMetaDocument, ITemplate } from '../Interfaces'
@@ -12,20 +12,23 @@ export class Factory extends DocFactory<GeneratorProgramTemplate.Action> {
}
async generateDocument(data: GeneratorProgramTemplate.Action, options?: IGenerationOptions): Promise<IGeneratedDocument> {
const { template, vars, coop } = await this.resolveParallel({
const { template, vars, coop, docData } = await this.resolveParallel({
template: () => process.env.SOURCE === 'local'
? Promise.resolve(GeneratorProgramTemplate.Template as ITemplate<GeneratorProgramTemplate.Model>)
: this.getTemplate<GeneratorProgramTemplate.Model>(DraftContract.contractName.production, GeneratorProgramTemplate.registry_id, data.block_num),
vars: () => this.getVars(data.coopname),
coop: () => this.getCooperative(data.coopname),
docData: () => this.loadDocData<GeneratorProgramTemplate.Model['doc_data']>(data),
})
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
const doc_data = Cooperative.Registry.requireCapitalProgramPrivateData(docData, GeneratorProgramTemplate.registry_id)
const combinedData: GeneratorProgramTemplate.Model = {
meta,
coop,
vars,
doc_data,
}
await this.validate(combinedData, template.model)
@@ -1,4 +1,4 @@
import { DraftContract } from 'cooptypes'
import { Cooperative, DraftContract } from 'cooptypes'
import { GeneratorOfferTemplate } from '../Templates'
import { DocFactory } from '../Factory'
import type { IGeneratedDocument, IGenerationOptions, IMetaDocument, ITemplate } from '../Interfaces'
@@ -12,17 +12,19 @@ export class Factory extends DocFactory<GeneratorOfferTemplate.Action> {
}
async generateDocument(data: GeneratorOfferTemplate.Action, options?: IGenerationOptions): Promise<IGeneratedDocument> {
const { template, vars, coop, user } = await this.resolveParallel({
const { template, vars, coop, user, docData } = await this.resolveParallel({
template: () => process.env.SOURCE === 'local'
? Promise.resolve(GeneratorOfferTemplate.Template as ITemplate<GeneratorOfferTemplate.Model>)
: this.getTemplate<GeneratorOfferTemplate.Model>(DraftContract.contractName.production, GeneratorOfferTemplate.registry_id, data.block_num),
vars: () => this.getVars(data.coopname),
coop: () => this.getCooperative(data.coopname),
user: () => this.getUser(data.username, data.block_num),
docData: () => this.loadDocData<GeneratorOfferTemplate.Model['doc_data']>(data),
})
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
const common_user = this.getCommonUser(user)
const doc_data = Cooperative.Registry.requireCapitalProgramPrivateData(docData, GeneratorOfferTemplate.registry_id)
// Проверяем наличие данных протокола, утвердившего генерационную программу
if (!vars.generator_program?.protocol_number || !vars.generator_program?.protocol_day_month_year) {
@@ -34,6 +36,7 @@ export class Factory extends DocFactory<GeneratorOfferTemplate.Action> {
coop,
vars,
common_user,
doc_data,
}
await this.validate(combinedData, template.model)
@@ -17,7 +17,7 @@ export class Factory extends DocFactory<GeneratorOffer.Action> {
const udataService = new Udata(this.storage)
// Независимые источники тянем параллельно (см. resolveParallel в DocFactory)
const { template, vars, coop, user, generatorAgreementUdata, generatorAgreementCreatedAtUdata } = await this.resolveParallel({
const { template, vars, coop, user, generatorAgreementUdata, generatorAgreementCreatedAtUdata, docData } = await this.resolveParallel({
template: () => process.env.SOURCE === 'local'
? Promise.resolve(GeneratorOffer.Template as ITemplate<GeneratorOffer.Model>)
: this.getTemplate<GeneratorOffer.Model>(DraftContract.contractName.production, GeneratorOffer.registry_id, data.block_num),
@@ -36,12 +36,14 @@ export class Factory extends DocFactory<GeneratorOffer.Action> {
key: Cooperative.Model.UdataKey.GENERATOR_AGREEMENT_CREATED_AT,
block_num: data.block_num,
}),
docData: () => this.loadDocData<GeneratorOffer.Model['doc_data']>(data),
})
// meta зависит от template.title — считаем после батча
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
const common_user = this.getCommonUser(user)
const doc_data = Cooperative.Registry.requireCapitalProgramPrivateData(docData, GeneratorOffer.registry_id)
// Проверяем наличие данных протокола, утвердившего генерационную программу
if (!vars.generator_program?.protocol_number || !vars.generator_program?.protocol_day_month_year) {
@@ -68,6 +70,7 @@ export class Factory extends DocFactory<GeneratorOffer.Action> {
common_user,
generator_agreement_number: String(generatorAgreementUdata.value),
generator_agreement_created_at: String(generatorAgreementCreatedAtUdata.value),
doc_data,
}
await this.validate(combinedData, template.model)
@@ -1,4 +1,4 @@
import { DraftContract } from 'cooptypes'
import { Cooperative, DraftContract } from 'cooptypes'
import { BlagorostProgramTemplate } from '../Templates'
import { DocFactory } from '../Factory'
import type { IGeneratedDocument, IGenerationOptions, IMetaDocument, ITemplate } from '../Interfaces'
@@ -12,16 +12,18 @@ export class Factory extends DocFactory<BlagorostProgramTemplate.Action> {
}
async generateDocument(data: BlagorostProgramTemplate.Action, options?: IGenerationOptions): Promise<IGeneratedDocument> {
const { template, vars } = await this.resolveParallel({
const { template, vars, docData } = await this.resolveParallel({
template: () => process.env.SOURCE === 'local'
? Promise.resolve(BlagorostProgramTemplate.Template as ITemplate<BlagorostProgramTemplate.Model>)
: this.getTemplate<BlagorostProgramTemplate.Model>(DraftContract.contractName.production, BlagorostProgramTemplate.registry_id, data.block_num),
vars: () => this.getVars(data.coopname),
docData: () => this.loadDocData<BlagorostProgramTemplate.Model['doc_data']>(data),
})
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
const doc_data = Cooperative.Registry.requireCapitalProgramPrivateData(docData, BlagorostProgramTemplate.registry_id)
const combinedData: BlagorostProgramTemplate.Model = { meta, vars }
const combinedData: BlagorostProgramTemplate.Model = { meta, vars, doc_data }
await this.validate(combinedData, template.model)
@@ -1,4 +1,4 @@
import { DraftContract } from 'cooptypes'
import { Cooperative, DraftContract } from 'cooptypes'
import { BlagorostOfferTemplate } from '../Templates'
import { DocFactory } from '../Factory'
import type { IGeneratedDocument, IGenerationOptions, IMetaDocument, ITemplate } from '../Interfaces'
@@ -12,15 +12,17 @@ export class Factory extends DocFactory<BlagorostOfferTemplate.Action> {
}
async generateDocument(data: BlagorostOfferTemplate.Action, options?: IGenerationOptions): Promise<IGeneratedDocument> {
const { template, vars, coop } = await this.resolveParallel({
const { template, vars, coop, docData } = await this.resolveParallel({
template: () => process.env.SOURCE === 'local'
? Promise.resolve(BlagorostOfferTemplate.Template as ITemplate<BlagorostOfferTemplate.Model>)
: this.getTemplate<BlagorostOfferTemplate.Model>(DraftContract.contractName.production, BlagorostOfferTemplate.registry_id, data.block_num),
vars: () => this.getVars(data.coopname),
coop: () => this.getCooperative(data.coopname),
docData: () => this.loadDocData<BlagorostOfferTemplate.Model['doc_data']>(data),
})
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
const doc_data = Cooperative.Registry.requireCapitalProgramPrivateData(docData, BlagorostOfferTemplate.registry_id)
// Проверяем наличие данных протокола, утвердившего Положение (документ 998)
if (!vars.blagorost_program?.protocol_number || !vars.blagorost_program?.protocol_day_month_year) {
@@ -31,6 +33,7 @@ export class Factory extends DocFactory<BlagorostOfferTemplate.Action> {
meta,
coop,
vars,
doc_data,
}
await this.validate(combinedData, template.model)
@@ -12,6 +12,34 @@ export type Action = Cooperative.Registry.BlagorostOffer.Action
// Модель данных - используем полную модель с дополнительными полями
export type Model = Cooperative.Registry.BlagorostOffer.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки - используем расширенную схему для дополнительных данных
export const Schema: JSONSchemaType<Model> = {
type: 'object',
@@ -19,13 +47,14 @@ export const Schema: JSONSchemaType<Model> = {
meta: IMetaJSONSchema,
coop: CooperativeSchema,
vars: VarsSchema,
doc_data: CapitalProgramPrivateDataSchema,
common_user: CommonUserSchema,
blagorost_agreement_number: { type: 'string' },
blagorost_agreement_created_at: { type: 'string' },
},
required: ['meta', 'coop', 'vars', 'common_user', 'blagorost_agreement_number', 'blagorost_agreement_created_at'],
required: ['meta', 'coop', 'vars', 'common_user', 'blagorost_agreement_number', 'blagorost_agreement_created_at', 'doc_data'],
additionalProperties: true,
} as any
}
export const Template: ITemplate<Model> = {
title: Cooperative.Registry.BlagorostOffer.title,
@@ -12,6 +12,34 @@ export type Action = Cooperative.Registry.GeneratorProgramTemplate.Action
// Модель данных
export type Model = Cooperative.Registry.GeneratorProgramTemplate.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
@@ -19,8 +47,9 @@ export const Schema: JSONSchemaType<Model> = {
meta: IMetaJSONSchema,
coop: CooperativeSchema,
vars: VarsSchema,
doc_data: CapitalProgramPrivateDataSchema,
},
required: ['meta', 'coop', 'vars'],
required: ['meta', 'coop', 'vars', 'doc_data'],
additionalProperties: true,
}
@@ -12,6 +12,34 @@ export type Action = Cooperative.Registry.GeneratorOfferTemplate.Action
// Модель данных
export type Model = Cooperative.Registry.GeneratorOfferTemplate.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
@@ -19,9 +47,10 @@ export const Schema: JSONSchemaType<Model> = {
meta: IMetaJSONSchema,
coop: CooperativeSchema,
vars: VarsSchema,
doc_data: CapitalProgramPrivateDataSchema,
common_user: CommonUserSchema,
},
required: ['meta', 'coop', 'vars', 'common_user'],
required: ['meta', 'coop', 'vars', 'common_user', 'doc_data'],
additionalProperties: true,
}
@@ -12,6 +12,34 @@ export type Action = Cooperative.Registry.GeneratorOffer.Action
// Модель данных
export type Model = Cooperative.Registry.GeneratorOffer.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
@@ -19,11 +47,12 @@ export const Schema: JSONSchemaType<Model> = {
meta: IMetaJSONSchema,
coop: CooperativeSchema,
vars: VarsSchema,
doc_data: CapitalProgramPrivateDataSchema,
common_user: CommonUserSchema,
generator_agreement_number: { type: 'string' },
generator_agreement_created_at: { type: 'string' },
},
required: ['meta', 'coop', 'vars', 'common_user', 'generator_agreement_number', 'generator_agreement_created_at'],
required: ['meta', 'coop', 'vars', 'common_user', 'generator_agreement_number', 'generator_agreement_created_at', 'doc_data'],
additionalProperties: true,
}
@@ -18,14 +18,43 @@ export interface Model {
vars: IVars
}
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
properties: {
meta: IMetaJSONSchema,
vars: VarsSchema,
doc_data: CapitalProgramPrivateDataSchema,
},
required: ['meta', 'vars'],
required: ['meta', 'vars', 'doc_data'],
additionalProperties: true,
}
@@ -12,6 +12,34 @@ export type Action = Cooperative.Registry.BlagorostOfferTemplate.Action
// Модель данных
export type Model = Cooperative.Registry.BlagorostOfferTemplate.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
@@ -19,8 +47,9 @@ export const Schema: JSONSchemaType<Model> = {
meta: IMetaJSONSchema,
coop: CooperativeSchema,
vars: VarsSchema,
doc_data: CapitalProgramPrivateDataSchema,
},
required: ['meta', 'coop', 'vars'],
required: ['meta', 'coop', 'vars', 'doc_data'],
additionalProperties: true,
}
@@ -1,15 +1,18 @@
import { beforeAll, describe, it, vi } from 'vitest'
import { Cooperative } from 'cooptypes'
import { Generator } from '../src'
import type { Numbers } from '../src'
import { testDocumentGeneration } from './utils/testDocument'
import { generator, mongoUri } from './utils'
let capitalProgramDocDataHash = ''
beforeAll(async () => {
await generator.connect(mongoUri)
// Подменяем метод getApprovedDecision для фабрики акта взноса результатов (1042)
// Это позволит тесту найти "принятое решение" без обращения к реальному API
const factory1042 = (generator as any).factories['1042']
const factory1042 = generator.factories[1042 as Numbers]
if (factory1042) {
vi.spyOn(factory1042, 'getApprovedDecision').mockImplementation(async () => {
return {
@@ -44,6 +47,32 @@ beforeAll(async () => {
for (const data of udatas) {
await generator.save('udata', { ...commonUdata, ...data })
}
const capitalProgramPrivateData = {
approval_protocol_number: '1',
approval_protocol_day: '15',
approval_protocol_month: 'января',
approval_protocol_year: '26',
cooperative_name: 'ВОСХОД',
cooperative_short_name: 'ПК',
cooperative_quoted_name: '«ВОСХОД»',
website: 'https://цифровой-кооператив.рф',
chairman_full_name: 'Муравьев Алексей Николаевич',
generator_program_purpose: 'развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) под названием “Кооперативная Экономика”',
eoap_definition: 'информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”',
generator_task_goal: 'центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений',
idea_unit_cost: '50',
idea_unit_cost_words: 'пятьдесят',
blagorost_goal_expansion: 'вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате',
blagorost_task_expansion: 'расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате',
blagorost_task_development: 'развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений',
return_source_description: 'аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕОАП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий',
return_additional_source: 'взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП',
offer_template_number: '______',
}
const { hash } = await generator.saveDocData(capitalProgramPrivateData, 994)
capitalProgramDocDataHash = hash
})
describe('тест генератора документов с registry_id >= 1000', async () => {
@@ -54,6 +83,7 @@ describe('тест генератора документов с registry_id >= 1
coopname: 'voskhod',
username: 'ant',
lang: 'ru',
doc_data_hash: capitalProgramDocDataHash,
})
})
@@ -63,6 +93,7 @@ describe('тест генератора документов с registry_id >= 1
coopname: 'voskhod',
username: 'ant',
lang: 'ru',
doc_data_hash: capitalProgramDocDataHash,
})
})
@@ -72,6 +103,7 @@ describe('тест генератора документов с registry_id >= 1
coopname: 'voskhod',
username: 'ant',
lang: 'ru',
doc_data_hash: capitalProgramDocDataHash,
})
})
@@ -92,6 +124,7 @@ describe('тест генератора документов с registry_id >= 1
coopname: 'voskhod',
username: 'ant',
lang: 'ru',
doc_data_hash: capitalProgramDocDataHash,
})
})
it('генерируем шаблон оферты Благорост', async () => {
@@ -100,6 +133,7 @@ describe('тест генератора документов с registry_id >= 1
coopname: 'voskhod',
username: 'ant',
lang: 'ru',
doc_data_hash: capitalProgramDocDataHash,
})
})
@@ -110,6 +144,7 @@ describe('тест генератора документов с registry_id >= 1
coopname: 'voskhod',
username: 'ant',
lang: 'ru',
doc_data_hash: capitalProgramDocDataHash,
})
})
@@ -12,6 +12,7 @@ const onboardingStateFields = {
onboarding_blagorost_provision_hash: true,
blagorost_offer_template_done: true,
onboarding_blagorost_offer_template_hash: true,
capital_program_doc_data_hash: true,
onboarding_init_at: true,
onboarding_expire_at: true,
} as const
@@ -19,4 +20,4 @@ const onboardingStateFields = {
// Проверка валидности
const _validate: MakeAllFieldsRequired<ValueTypes['CapitalOnboardingState']> = onboardingStateFields
export const capitalOnboardingStateSelector = Selector('CapitalOnboardingState')(onboardingStateFields as any)
export const capitalOnboardingStateSelector = Selector('CapitalOnboardingState')(onboardingStateFields)