fix(capital): remove unsafe any from CPP onboarding changes

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alex Ant
2026-06-28 11:05:20 +05:00
parent bfa2830650
commit 02ce9400a2
14 changed files with 273 additions and 53 deletions
@@ -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,
@@ -142,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();
@@ -169,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({
@@ -334,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) {
@@ -582,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);
// Не бросаем ошибку, чтобы не блокировать запуск модуля
}
@@ -591,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
);
// Не бросаем ошибку, чтобы не блокировать запуск модуля
}
@@ -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,7 +26,7 @@ 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,
@@ -33,17 +35,20 @@ export const useCapitalOnboarding = () => {
};
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 docDataHash = (onboardingState.value as any)?.capital_program_doc_data_hash;
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');
}
@@ -196,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,
@@ -149,13 +149,21 @@ type FieldDefinition = {
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: Record<string, any>;
config: CapitalProgramConfig;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(event: 'update:config', value: Record<string, any>): void;
(event: 'update:config', value: CapitalProgramConfig): void;
}>();
const system = useSystemStore();
@@ -236,7 +244,7 @@ const allFields = computed(() => [
watch(
() => system.info,
(info: any) => {
(info) => {
if (!info) return;
form.cooperative_name ||= String(info.coopname || '').toUpperCase();
form.cooperative_short_name ||= 'ПК';
@@ -259,7 +267,17 @@ function validatePayload(payload: CapitalProgramPrivateData): string[] {
.map(({ label }) => label);
}
async function generateDocument(registry_id: number, payload: CapitalProgramPrivateData) {
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,
{
@@ -278,6 +296,10 @@ async function generateDocument(registry_id: number, payload: CapitalProgramPriv
}
);
if (!result) {
throw new Error('Документ не был сгенерирован');
}
return result;
}
@@ -297,14 +319,14 @@ async function generatePreview() {
]);
const docDataHash = docs
.map((doc: any) => doc?.meta?.doc_data_hash)
.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: any, index) => ({
previewDocuments.value = docs.map((doc, index) => ({
registry_id: index === 0 ? 994 : 998,
title: doc.full_title,
html: doc.html,
@@ -19,23 +19,29 @@
<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 {
extensionName?: string;
schema?: any;
config: any;
formRef?: any;
schema?: unknown;
config: ExtensionInstallConfig;
formRef?: ExtensionInstallFormRef;
}
const props = defineProps<Props>();
const isEmpty = computed(() => isExtensionSchemaEmpty(props.schema));
const setFormRef = (element: any) => {
const setFormRef = (element: Element | ComponentPublicInstance | null) => {
if (props.formRef && 'value' in props.formRef) {
props.formRef.value = element;
}
@@ -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',
@@ -26,7 +54,7 @@ export const Schema: JSONSchemaType<Model> = {
},
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,10 +12,33 @@ export type Action = Cooperative.Registry.GeneratorProgramTemplate.Action
// Модель данных
export type Model = Cooperative.Registry.GeneratorProgramTemplate.Model
const CapitalProgramPrivateDataSchema = {
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
additionalProperties: { type: 'string' },
} as const
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> = {
@@ -28,7 +51,7 @@ export const Schema: JSONSchemaType<Model> = {
},
required: ['meta', 'coop', 'vars', 'doc_data'],
additionalProperties: true,
} as any
}
export const Template: ITemplate<Model> = {
title: Cooperative.Registry.GeneratorProgramTemplate.title,
@@ -12,10 +12,33 @@ export type Action = Cooperative.Registry.GeneratorOfferTemplate.Action
// Модель данных
export type Model = Cooperative.Registry.GeneratorOfferTemplate.Model
const CapitalProgramPrivateDataSchema = {
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
additionalProperties: { type: 'string' },
} as const
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> = {
@@ -29,7 +52,7 @@ export const Schema: JSONSchemaType<Model> = {
},
required: ['meta', 'coop', 'vars', 'common_user', 'doc_data'],
additionalProperties: true,
} as any
}
export const Template: ITemplate<Model> = {
title: Cooperative.Registry.GeneratorOfferTemplate.title,
@@ -12,10 +12,33 @@ export type Action = Cooperative.Registry.GeneratorOffer.Action
// Модель данных
export type Model = Cooperative.Registry.GeneratorOffer.Model
const CapitalProgramPrivateDataSchema = {
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
additionalProperties: { type: 'string' },
} as const
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> = {
@@ -31,7 +54,7 @@ export const Schema: JSONSchemaType<Model> = {
},
required: ['meta', 'coop', 'vars', 'common_user', 'generator_agreement_number', 'generator_agreement_created_at', 'doc_data'],
additionalProperties: true,
} as any
}
export const Template: ITemplate<Model> = {
title: Cooperative.Registry.GeneratorOffer.title,
@@ -18,10 +18,33 @@ export interface Model {
vars: IVars
}
const CapitalProgramPrivateDataSchema = {
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
additionalProperties: { type: 'string' },
} as const
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> = {
@@ -33,7 +56,7 @@ export const Schema: JSONSchemaType<Model> = {
},
required: ['meta', 'vars', 'doc_data'],
additionalProperties: true,
} as any
}
export const Template: ITemplate<Model> = {
title: Cooperative.Registry.BlagorostProgramTemplate.title,
@@ -12,10 +12,33 @@ export type Action = Cooperative.Registry.BlagorostOfferTemplate.Action
// Модель данных
export type Model = Cooperative.Registry.BlagorostOfferTemplate.Model
const CapitalProgramPrivateDataSchema = {
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
additionalProperties: { type: 'string' },
} as const
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> = {
@@ -28,7 +51,7 @@ export const Schema: JSONSchemaType<Model> = {
},
required: ['meta', 'coop', 'vars', 'doc_data'],
additionalProperties: true,
} as any
}
export const Template: ITemplate<Model> = {
title: Cooperative.Registry.BlagorostOfferTemplate.title,
@@ -1,6 +1,7 @@
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'
@@ -11,7 +12,7 @@ beforeAll(async () => {
// Подменяем метод 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 {
@@ -20,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)