controller meets v2

This commit is contained in:
Alex Ant
2026-04-04 00:13:22 +05:00
parent 333553d283
commit 94ab81fa64
19 changed files with 242 additions and 49 deletions
+72
View File
@@ -1,3 +1,75 @@
# v2026.4.2-2
В этом релизе — стабилизация Благороста для вывода в продуктивную работу с результатами интеллектуальной деятельности. Отдельно заложена основа под отчётность в ФНС и ФСС и прототип поиска по документам.
**Благорост и проекты**
- Быстрые действия на странице программы: создать проект, компонент, задачу или требование.
- Требования к компонентам в репозитории: поддержка **Mermaid**, **Draw.io** и **BPMN**.
- Синхронизация проектов, компонентов, требований и задач с Git-репозиторием результатов.
- Встраивание полноразмерного видео (iframe) на страницах проектов.
- Скачивание пакета подписанных документов одной кнопкой.
- Комнаты проектов в кооперативном мессенджере.
**Мессенджер и звонки**
- Автосекретарь: запись синхронных звонков, текстовых и голосовых сообщений в проектных комнатах.
**Отчётность и инфраструктура**
- Прототип фабрики отчётов ФНС/ФСС: выгрузка в XML для дальнейшей отправки.
- Прототип поисковой системы по документам.
- Установщик для развёртывания на своих серверах (разработка или эксплуатация).
- Рефакторинг в сторону чистой архитектуры на бэкенде.
- Ускорение сборки фронтенда за счёт перехода на **Vite 8**.
**Исправления**
- Повторное общее собрание больше не мешало завершить онбординг кооператива.
- Центр уведомлений не блокировал загрузку рабочего стола при отключённом провайдере оповещений.
- Уведомления о собрании совета по свободным вопросам снова доходят до членов совета.
- В интерфейсе восстановлено отображение контактов кооператива.
#releases
---
# v2026.4.2-2
В этом релизе — стабилизация Благороста для вывода в продуктивную работу с результатами интеллектуальной деятельности. Отдельно заложена основа под отчётность в ФНС и ФСС и прототип поиска по документам.
**Благорост и проекты**
- Быстрые действия на странице программы: создать проект, компонент, задачу или требование.
- Требования к компонентам в репозитории: поддержка **Mermaid**, **Draw.io** и **BPMN**.
- Синхронизация проектов, компонентов, требований и задач с Git-репозиторием результатов.
- Встраивание полноразмерного видео (iframe) на страницах проектов.
- Скачивание пакета подписанных документов одной кнопкой.
- Комнаты проектов в кооперативном мессенджере.
**Мессенджер и звонки**
- Автосекретарь: запись синхронных звонков, текстовых и голосовых сообщений в проектных комнатах.
**Отчётность и инфраструктура**
- Прототип фабрики отчётов ФНС/ФСС: выгрузка в XML для дальнейшей отправки.
- Прототип поисковой системы по документам.
- Установщик для развёртывания на своих серверах (разработка или эксплуатация).
- Рефакторинг в сторону чистой архитектуры на бэкенде.
- Ускорение сборки фронтенда за счёт перехода на **Vite 8**.
**Исправления**
- Повторное общее собрание больше не мешало завершить онбординг кооператива.
- Центр уведомлений не блокировал загрузку рабочего стола при отключённом провайдере оповещений.
- Уведомления о собрании совета по свободным вопросам снова доходят до членов совета.
- В интерфейсе восстановлено отображение контактов кооператива.
#releases
---
# v2025.12.28-8 # v2025.12.28-8
В этой версии представлен прототип трекера результатов интеллектуальной деятельности, реализован мост в 1С, обновлен интерфейс и существенно повышена стабильность системы. В этой версии представлен прототип трекера результатов интеллектуальной деятельности, реализован мост в 1С, обновлен интерфейс и существенно повышена стабильность системы.
+1 -1
View File
@@ -16,7 +16,7 @@
"max_transaction_lifetime": 3600, "max_transaction_lifetime": 3600,
"deferred_trx_expiration_window": 600, "deferred_trx_expiration_window": 600,
"max_transaction_delay": 3888000, "max_transaction_delay": 3888000,
"max_inline_action_size": 4096, "max_inline_action_size": 65536,
"max_inline_action_depth": 10, "max_inline_action_depth": 10,
"max_authority_depth": 6 "max_authority_depth": 6
}, },
@@ -1,10 +1,13 @@
import { Field, InputType } from '@nestjs/graphql'; import { Field, InputType } from '@nestjs/graphql';
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; import { IsArray, IsNotEmpty, IsOptional, IsString, MaxLength, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import type { CreateAnnualGeneralMeetInputDomainInterface } from '~/domain/meet/interfaces/create-annual-meet-input-domain.interface'; import type { CreateAnnualGeneralMeetInputDomainInterface } from '~/domain/meet/interfaces/create-annual-meet-input-domain.interface';
import { AgendaGeneralMeetPointInputDTO } from './agenda-meet-point-input.dto'; import { AgendaGeneralMeetPointInputDTO } from './agenda-meet-point-input.dto';
import { AnnualGeneralMeetingAgendaSignedDocumentInputDTO } from '~/application/document/documents-dto/annual-general-meeting-agenda-document.dto'; import { AnnualGeneralMeetingAgendaSignedDocumentInputDTO } from '~/application/document/documents-dto/annual-general-meeting-agenda-document.dto';
/** Согласовано с лимитом на desktop (повестка / форма собрания) */
const MEET_DETAILS_MAX_LEN = 10_000;
@InputType('CreateAnnualGeneralMeetInput') @InputType('CreateAnnualGeneralMeetInput')
export class CreateAnnualGeneralMeetInputDTO implements CreateAnnualGeneralMeetInputDomainInterface { export class CreateAnnualGeneralMeetInputDTO implements CreateAnnualGeneralMeetInputDomainInterface {
@Field(() => String, { description: 'Имя аккаунта кооператива' }) @Field(() => String, { description: 'Имя аккаунта кооператива' })
@@ -43,4 +46,13 @@ export class CreateAnnualGeneralMeetInputDTO implements CreateAnnualGeneralMeetI
@Field(() => AnnualGeneralMeetingAgendaSignedDocumentInputDTO, { description: 'Предложение повестки собрания' }) @Field(() => AnnualGeneralMeetingAgendaSignedDocumentInputDTO, { description: 'Предложение повестки собрания' })
proposal!: AnnualGeneralMeetingAgendaSignedDocumentInputDTO; proposal!: AnnualGeneralMeetingAgendaSignedDocumentInputDTO;
@Field(() => String, {
nullable: true,
description: 'Дополнительная информация о формате собрания (ссылка, как участвовать и т.д.)',
})
@IsOptional()
@IsString()
@MaxLength(MEET_DETAILS_MAX_LEN)
details?: string | null;
} }
@@ -65,6 +65,13 @@ export class MeetPreProcessingDTO implements MeetPreProcessingDomainInterface {
@IsOptional() @IsOptional()
proposal?: DocumentAggregateDTO; proposal?: DocumentAggregateDTO;
@Field(() => String, {
nullable: true,
description: 'Дополнительная информация о формате собрания',
})
@IsOptional()
details?: string | null;
constructor( constructor(
data: MeetPreProcessingDomainInterface, data: MeetPreProcessingDomainInterface,
presiderCertificate?: UserCertificateDomainInterface | null, presiderCertificate?: UserCertificateDomainInterface | null,
@@ -80,6 +87,7 @@ export class MeetPreProcessingDTO implements MeetPreProcessingDomainInterface {
this.open_at = data.open_at; this.open_at = data.open_at;
this.close_at = data.close_at; this.close_at = data.close_at;
this.proposal = data.proposal ? new DocumentAggregateDTO(data.proposal) : undefined; this.proposal = data.proposal ? new DocumentAggregateDTO(data.proposal) : undefined;
this.details = data.details ?? null;
// Обрабатываем сертификат инициатора // Обрабатываем сертификат инициатора
if (initiatorCertificate) { if (initiatorCertificate) {
@@ -1,9 +1,11 @@
import { Field, InputType } from '@nestjs/graphql'; import { Field, InputType } from '@nestjs/graphql';
import { IsString, IsDate, ValidateNested } from 'class-validator'; import { IsString, IsDate, IsOptional, MaxLength, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { AnnualGeneralMeetingAgendaSignedDocumentInputDTO } from '~/application/document/documents-dto/annual-general-meeting-agenda-document.dto'; import { AnnualGeneralMeetingAgendaSignedDocumentInputDTO } from '~/application/document/documents-dto/annual-general-meeting-agenda-document.dto';
import { RestartAnnualGeneralMeetInputDomainInterface } from '~/domain/meet/interfaces/restart-annual-general-meet-input-domain.interface'; import { RestartAnnualGeneralMeetInputDomainInterface } from '~/domain/meet/interfaces/restart-annual-general-meet-input-domain.interface';
const MEET_DETAILS_MAX_LEN = 10_000;
@InputType('RestartAnnualGeneralMeetInput', { description: 'DTO для перезапуска ежегодного общего собрания кооператива' }) @InputType('RestartAnnualGeneralMeetInput', { description: 'DTO для перезапуска ежегодного общего собрания кооператива' })
export class RestartAnnualGeneralMeetInputDTO implements RestartAnnualGeneralMeetInputDomainInterface { export class RestartAnnualGeneralMeetInputDTO implements RestartAnnualGeneralMeetInputDomainInterface {
@Field(() => String, { description: 'Имя аккаунта кооператива' }) @Field(() => String, { description: 'Имя аккаунта кооператива' })
@@ -29,4 +31,14 @@ export class RestartAnnualGeneralMeetInputDTO implements RestartAnnualGeneralMee
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
new_close_at!: Date; new_close_at!: Date;
@Field(() => String, {
nullable: true,
description:
'Доп. информация для нового слота; передайте явно, если нужна в повестке и уведомлениях. Поле не передано — у нового собрания details пустой',
})
@IsOptional()
@IsString()
@MaxLength(MEET_DETAILS_MAX_LEN)
details?: string | null;
} }
@@ -19,6 +19,8 @@ import { ProcessMeetDecisionInputDomainInterface } from '~/domain/meet/interface
import { MeetProcessedDomainEntity } from '~/domain/meet/entities/meet-processed-domain.entity'; import { MeetProcessedDomainEntity } from '~/domain/meet/entities/meet-processed-domain.entity';
import { NotifyOnAnnualGeneralMeetInputDomainInterface } from '~/domain/meet/interfaces/notify-on-annual-general-meet-input-domain.interface'; import { NotifyOnAnnualGeneralMeetInputDomainInterface } from '~/domain/meet/interfaces/notify-on-annual-general-meet-input-domain.interface';
import { generateUniqueHash } from '~/utils/generate-hash.util'; import { generateUniqueHash } from '~/utils/generate-hash.util';
import { HttpApiError } from '~/utils/httpApiError';
import httpStatus from 'http-status';
@Injectable() @Injectable()
export class MeetInteractor { export class MeetInteractor {
@@ -48,10 +50,13 @@ export class MeetInteractor {
// Генерируем уникальный хэш для собрания // Генерируем уникальный хэш для собрания
const hash = generateUniqueHash(); const hash = generateUniqueHash();
const detailsNormalized = MeetInteractor.normalizeMeetDetailsField(data.details);
const preProcessing = new MeetPreProcessingDomainEntity({ const preProcessing = new MeetPreProcessingDomainEntity({
...data, ...data,
hash, hash,
proposal: proposalAggregate, proposal: proposalAggregate,
details: detailsNormalized,
}); });
// Сохраняем данные в репозиторий // Сохраняем данные в репозиторий
@@ -60,6 +65,8 @@ export class MeetInteractor {
// Вызов блокчейн порта для создания собрания // Вызов блокчейн порта для создания собрания
await this.meetBlockchainPort.createMeet({ ...data, hash }); await this.meetBlockchainPort.createMeet({ ...data, hash });
// Задержка синхронизации на 1 секунду, чтобы избежать ошибки "row not found"
await new Promise(resolve => setTimeout(resolve, 1000));
// Получаем обновленные данные из блокчейна // Получаем обновленные данные из блокчейна
const processingData = await this.meetBlockchainPort.getMeet({ const processingData = await this.meetBlockchainPort.getMeet({
coopname: data.coopname, coopname: data.coopname,
@@ -173,22 +180,59 @@ export class MeetInteractor {
} }
async restartMeet(data: RestartAnnualGeneralMeetInputDomainInterface): Promise<MeetAggregate> { async restartMeet(data: RestartAnnualGeneralMeetInputDomainInterface): Promise<MeetAggregate> {
// Вызов блокчейн порта для перезапуска собрания const oldPre = await this.meetPreRepository.findByHash(data.hash);
const new_hash = await this.meetBlockchainPort.restartMeet(data); const new_hash = await this.meetBlockchainPort.restartMeet(data);
// Получаем обновленные данные из обоих источников
const processingData = await this.meetBlockchainPort.getMeet({ const processingData = await this.meetBlockchainPort.getMeet({
coopname: data.coopname, coopname: data.coopname,
hash: new_hash, hash: new_hash,
}); });
if (!processingData) {
throw new HttpApiError(httpStatus.NOT_FOUND, 'Собрание после перезапуска не найдено в блокчейне');
}
const proposalAggregate = await this.documentAggregator.buildDocumentAggregate(data.newproposal);
const meetRow = processingData.meet;
const agendaFromChain = processingData.questions.map((q) => ({
context: q.context,
title: q.title,
decision: q.decision,
}));
const detailsForNewPre = MeetInteractor.normalizeMeetDetailsField(data.details);
const preProcessing = new MeetPreProcessingDomainEntity({
hash: new_hash,
coopname: meetRow.coopname,
initiator: oldPre?.initiator ?? meetRow.initiator,
presider: oldPre?.presider ?? meetRow.presider,
secretary: oldPre?.secretary ?? meetRow.secretary,
agenda: agendaFromChain,
open_at: meetRow.open_at,
close_at: meetRow.close_at,
proposal: proposalAggregate ?? undefined,
details: detailsForNewPre,
});
await this.meetPreRepository.create(preProcessing);
const preMeet = await this.meetPreRepository.findByHash(new_hash); const preMeet = await this.meetPreRepository.findByHash(new_hash);
const processedMeetEntity = await this.meetProcessedRepository.findByHash(new_hash); const processedMeetEntity = await this.meetProcessedRepository.findByHash(new_hash);
// Создаем агрегат из обоих источников данных
return new MeetAggregate(preMeet, processingData, processedMeetEntity); return new MeetAggregate(preMeet, processingData, processedMeetEntity);
} }
/** Пустая строка → отсутствие значения в PG */
private static normalizeMeetDetailsField(value: string | null | undefined): string | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
const t = value.trim();
return t.length > 0 ? t : null;
}
async signBySecretaryOnAnnualGeneralMeet( async signBySecretaryOnAnnualGeneralMeet(
data: SignBySecretaryOnAnnualGeneralMeetInputDomainInterface data: SignBySecretaryOnAnnualGeneralMeetInputDomainInterface
): Promise<MeetAggregate> { ): Promise<MeetAggregate> {
@@ -11,6 +11,7 @@ export class MeetPreProcessingDomainEntity implements MeetPreProcessingDomainInt
public readonly open_at!: Date; public readonly open_at!: Date;
public readonly close_at!: Date; public readonly close_at!: Date;
public readonly proposal?: DocumentAggregateDomainInterface; public readonly proposal?: DocumentAggregateDomainInterface;
public readonly details?: string | null;
constructor(data: MeetPreProcessingDomainInterface) { constructor(data: MeetPreProcessingDomainInterface) {
Object.assign(this, data); Object.assign(this, data);
@@ -9,4 +9,5 @@ export type CreateAnnualGeneralMeetInputDomainInterface = Omit<
open_at: Date; open_at: Date;
close_at: Date; close_at: Date;
hash?: string; hash?: string;
details?: string | null;
}; };
@@ -21,6 +21,8 @@ export interface MeetPreProcessingDomainInterface {
open_at: Date; open_at: Date;
close_at: Date; close_at: Date;
proposal?: DocumentAggregateDomainInterface; proposal?: DocumentAggregateDomainInterface;
/** Текст уровня собрания (ссылка на трансляцию, формат участия и т.п.), только в PG */
details?: string | null;
} }
assertType<AssertKeysMatch<MeetContract.Actions.CreateMeet.IInput, MeetPreProcessingDomainInterface>>(); assertType<AssertKeysMatch<MeetContract.Actions.CreateMeet.IInput, MeetPreProcessingDomainInterface>>();
@@ -9,4 +9,6 @@ export interface RestartAnnualGeneralMeetInputDomainInterface {
newproposal: ISignedDocumentDomainInterface; newproposal: ISignedDocumentDomainInterface;
new_open_at: Date; new_open_at: Date;
new_close_at: Date; new_close_at: Date;
/** Если передано — пишем в meet_pre нового hash; если в мутации не передано — details у нового pre пустой (старый pre не подмешиваем) */
details?: string | null;
} }
@@ -312,7 +312,21 @@ export class GenerationService {
return []; return [];
} }
const rooms = await this.projectCommArtifacts.listCommunicationRoomsForProject(anchorProjectHash); const projectForStory = await this.projectRepository.findByHash(anchorProjectHash);
if (!projectForStory) {
return [];
}
// Компоненты не имеют отдельных записей чатов в реестре: комната Matrix у корневого проекта (parent_hash).
let matrixRoomProjectHash = anchorProjectHash.trim().toLowerCase();
if (projectForStory.isComponent()) {
const parentHash = projectForStory.parent_hash?.trim().toLowerCase() ?? '';
if (parentHash) {
matrixRoomProjectHash = parentHash;
}
}
const rooms = await this.projectCommArtifacts.listCommunicationRoomsForProject(matrixRoomProjectHash);
if (rooms.length === 0) { if (rooms.length === 0) {
return []; return [];
} }
@@ -1,4 +1,5 @@
import { Injectable, Inject, OnModuleInit } from '@nestjs/common'; import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { MEET_REPOSITORY, MeetPreProcessingRepository } from '~/domain/meet/repositories/meet-pre.repository';
import { WinstonLoggerService } from '~/application/logger/logger-app.service'; import { WinstonLoggerService } from '~/application/logger/logger-app.service';
import { NovuWorkflowAdapter } from '~/infrastructure/novu/novu-workflow.adapter'; import { NovuWorkflowAdapter } from '~/infrastructure/novu/novu-workflow.adapter';
import { NOVU_WORKFLOW_PORT } from '~/domain/notification/interfaces/novu-workflow.port'; import { NOVU_WORKFLOW_PORT } from '~/domain/notification/interfaces/novu-workflow.port';
@@ -20,6 +21,8 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
private readonly novuWorkflowAdapter: NovuWorkflowAdapter, private readonly novuWorkflowAdapter: NovuWorkflowAdapter,
@Inject(ACCOUNT_DATA_PORT) @Inject(ACCOUNT_DATA_PORT)
private readonly accountPort: AccountDataPort, private readonly accountPort: AccountDataPort,
@Inject(MEET_REPOSITORY)
private readonly meetPreRepository: MeetPreProcessingRepository,
private readonly logger: WinstonLoggerService private readonly logger: WinstonLoggerService
) { ) {
this.logger.setContext(MeetWorkflowNotificationService.name); this.logger.setContext(MeetWorkflowNotificationService.name);
@@ -56,6 +59,16 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
return config.timezone === 'Europe/Moscow' ? 'МСК' : config.timezone; return config.timezone === 'Europe/Moscow' ? 'МСК' : config.timezone;
} }
/**
* Текст из meet_pre только для workflow «до/у начала»: meet-initial, meet-reminder-start, meet-started.
* В reminder-end, restart, ended поле details в схемах Novu нет — не передаём, иначе ошибка валидации.
*/
private async meetDetailsPayloadPart(hash: string): Promise<{ details?: string }> {
const pre = await this.meetPreRepository.findByHash(hash);
const trimmed = pre?.details?.trim();
return trimmed ? { details: trimmed } : {};
}
// Получение всех email-адресов пользователей // Получение всех email-адресов пользователей
private async getAllUserEmails(): Promise<Array<{ username: string; email: string }>> { private async getAllUserEmails(): Promise<Array<{ username: string; email: string }>> {
try { try {
@@ -113,6 +126,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
const timezone = this.getTimezoneDisplay(); const timezone = this.getTimezoneDisplay();
const meetUrl = this.getNotificationUrl(meet); const meetUrl = this.getNotificationUrl(meet);
const detailsPart = await this.meetDetailsPayloadPart(meet.hash);
const payload: Workflows.MeetInitial.IPayload = { const payload: Workflows.MeetInitial.IPayload = {
coopShortName, coopShortName,
meetId: meet.id, meetId: meet.id,
@@ -122,6 +136,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
meetEndTime, meetEndTime,
timezone, timezone,
meetUrl, meetUrl,
...detailsPart,
}; };
// Отправляем уведомления каждому пользователю в цикле // Отправляем уведомления каждому пользователю в цикле
@@ -166,6 +181,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
const meetUrl = this.getNotificationUrl(meet); const meetUrl = this.getNotificationUrl(meet);
const detailsPart = await this.meetDetailsPayloadPart(meet.hash);
const payload: Workflows.MeetReminderStart.IPayload = { const payload: Workflows.MeetReminderStart.IPayload = {
coopShortName, coopShortName,
meetId: meet.id, meetId: meet.id,
@@ -173,6 +189,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
meetTime, meetTime,
timeDescription, timeDescription,
meetUrl, meetUrl,
...detailsPart,
}; };
// Отправляем уведомления каждому пользователю в цикле // Отправляем уведомления каждому пользователю в цикле
@@ -211,6 +228,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
const timezone = this.getTimezoneDisplay(); const timezone = this.getTimezoneDisplay();
const meetUrl = this.getNotificationUrl(meet); const meetUrl = this.getNotificationUrl(meet);
const detailsPart = await this.meetDetailsPayloadPart(meet.hash);
const payload: Workflows.MeetStarted.IPayload = { const payload: Workflows.MeetStarted.IPayload = {
coopShortName, coopShortName,
meetId: meet.id, meetId: meet.id,
@@ -218,6 +236,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
meetEndTime, meetEndTime,
timezone, timezone,
meetUrl, meetUrl,
...detailsPart,
}; };
// Отправляем уведомления каждому пользователю в цикле // Отправляем уведомления каждому пользователю в цикле
@@ -263,6 +282,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
const timezone = this.getTimezoneDisplay(); const timezone = this.getTimezoneDisplay();
const meetUrl = this.getNotificationUrl(meet); const meetUrl = this.getNotificationUrl(meet);
const detailsPart = await this.meetDetailsPayloadPart(meet.hash);
const payload: Workflows.MeetReminderEnd.IPayload = { const payload: Workflows.MeetReminderEnd.IPayload = {
coopShortName, coopShortName,
meetId: meet.id, meetId: meet.id,
@@ -271,6 +291,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
timeDescription, timeDescription,
timezone, timezone,
meetUrl, meetUrl,
...detailsPart,
}; };
// Отправляем уведомления каждому пользователю в цикле // Отправляем уведомления каждому пользователю в цикле
@@ -181,9 +181,11 @@ export class MeetBlockchainAdapter implements MeetBlockchainPort {
this.blockchainService.initialize(data.coopname, wif); this.blockchainService.initialize(data.coopname, wif);
// Преобразуем доменный объект в инфраструктурный тип const { details: _details, ...meetPayload } = data;
// Преобразуем доменный объект в инфраструктурный тип (поле details только в PG, не в цепи)
const blockchainData: MeetContract.Actions.CreateMeet.IInput = { const blockchainData: MeetContract.Actions.CreateMeet.IInput = {
...data, ...meetPayload,
hash: data.hash || generateUniqueHash(), hash: data.hash || generateUniqueHash(),
proposal: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.proposal), proposal: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.proposal),
open_at: this.domainToBlockchainUtils.convertDateToBlockchainFormat(data.open_at), open_at: this.domainToBlockchainUtils.convertDateToBlockchainFormat(data.open_at),
@@ -34,6 +34,9 @@ export class MeetPreEntity implements MeetPreProcessingDomainInterface {
@Column('jsonb', { nullable: true }) @Column('jsonb', { nullable: true })
proposal?: DocumentAggregateDomainInterface; proposal?: DocumentAggregateDomainInterface;
@Column('text', { nullable: true })
details?: string | null;
@CreateDateColumn() @CreateDateColumn()
created_at!: Date; created_at!: Date;
@@ -48,6 +51,7 @@ export class MeetPreEntity implements MeetPreProcessingDomainInterface {
this.open_at = data.open_at; this.open_at = data.open_at;
this.close_at = data.close_at; this.close_at = data.close_at;
this.proposal = data.proposal; this.proposal = data.proposal;
this.details = data.details ?? null;
} }
} }
@@ -63,6 +67,7 @@ export class MeetPreEntity implements MeetPreProcessingDomainInterface {
open_at: this.open_at, open_at: this.open_at,
close_at: this.close_at, close_at: this.close_at,
proposal: this.proposal, proposal: this.proposal,
details: this.details ?? null,
}); });
} }
@@ -16,6 +16,7 @@ export const meetInitialPayloadSchema = z.object({
meetEndTime: z.string(), meetEndTime: z.string(),
timezone: z.string(), timezone: z.string(),
meetUrl: z.string(), meetUrl: z.string(),
details: z.string().optional(),
}); });
export type IPayload = z.infer<typeof meetInitialPayloadSchema>; export type IPayload = z.infer<typeof meetInitialPayloadSchema>;
@@ -36,12 +37,12 @@ export const workflow: WorkflowDefinition<IWorkflow> = WorkflowBuilder
createEmailStep( createEmailStep(
'meet-initial-email', 'meet-initial-email',
'Уведомление о общем собрании пайщиков №{{payload.meetId}} в {{payload.coopShortName}}', 'Уведомление о общем собрании пайщиков №{{payload.meetId}} в {{payload.coopShortName}}',
'Уважаемый пайщик!<br><br>В кооперативе объявлено новое общее собрание №{{payload.meetId}}.<br><br>Дата и время начала: {{payload.meetDate}} в {{payload.meetTime}} ({{payload.timezone}})<br>Дата и время завершения: {{payload.meetEndDate}} в {{payload.meetEndTime}} ({{payload.timezone}})<br><br>Для ознакомления с повесткой, пожалуйста, перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a><br><br>С уважением, Совет {{payload.coopShortName}}.' 'Уважаемый пайщик!<br><br>В кооперативе объявлено новое общее собрание №{{payload.meetId}}.<br><br>Дата и время начала: {{payload.meetDate}} в {{payload.meetTime}} ({{payload.timezone}})<br>Дата и время завершения: {{payload.meetEndDate}} в {{payload.meetEndTime}} ({{payload.timezone}})<br><br>Для ознакомления с повесткой, пожалуйста, перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a>{{#if payload.details}}<br><br>Дополнительная информация:<div style="white-space:pre-wrap;">{{payload.details}}</div>{{/if}}<br><br>С уважением, Совет {{payload.coopShortName}}.'
), ),
createInAppStep( createInAppStep(
'meet-initial-notification', 'meet-initial-notification',
'Новое общее собрание №{{payload.meetId}}', 'Новое общее собрание №{{payload.meetId}}',
'Назначено собрание на {{payload.meetDate}} в {{payload.meetTime}}' 'Назначено собрание на {{payload.meetDate}} в {{payload.meetTime}}{{#if payload.details}}. {{payload.details}}{{/if}}'
), ),
createPushStep( createPushStep(
'meet-initial-push', 'meet-initial-push',
@@ -14,6 +14,7 @@ export const meetReminderStartPayloadSchema = z.object({
meetTime: z.string(), meetTime: z.string(),
timeDescription: z.string(), timeDescription: z.string(),
meetUrl: z.string(), meetUrl: z.string(),
details: z.string().optional(),
}); });
export type IPayload = z.infer<typeof meetReminderStartPayloadSchema>; export type IPayload = z.infer<typeof meetReminderStartPayloadSchema>;
@@ -34,12 +35,12 @@ export const workflow: WorkflowDefinition<IWorkflow> = WorkflowBuilder
createEmailStep( createEmailStep(
'meet-reminder-start-email', 'meet-reminder-start-email',
'Напоминание о предстоящем общем собрании №{{payload.meetId}} в {{payload.coopShortName}}', 'Напоминание о предстоящем общем собрании №{{payload.meetId}} в {{payload.coopShortName}}',
'Уважаемый пайщик!<br><br>Напоминаем, что {{payload.timeDescription}} состоится общее собрание пайщиков №{{payload.meetId}} ({{payload.meetDate}} в {{payload.meetTime}}).<br><br>Для ознакомления с повесткой и подписи уведомления, пожалуйста, перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a><br><br>С уважением, Совет {{payload.coopShortName}}.' 'Уважаемый пайщик!<br><br>Напоминаем, что {{payload.timeDescription}} состоится общее собрание пайщиков №{{payload.meetId}} ({{payload.meetDate}} в {{payload.meetTime}}).<br><br>Для ознакомления с повесткой и подписи уведомления, пожалуйста, перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a>{{#if payload.details}}<br><br>Дополнительная информация:<div style="white-space:pre-wrap;">{{payload.details}}</div>{{/if}}<br><br>С уважением, Совет {{payload.coopShortName}}.'
), ),
createInAppStep( createInAppStep(
'meet-reminder-start-notification', 'meet-reminder-start-notification',
'Напоминание о собрании №{{payload.meetId}}', 'Напоминание о собрании №{{payload.meetId}}',
'Собрание начнется {{payload.timeDescription}}' 'Собрание начнется {{payload.timeDescription}}{{#if payload.details}}. {{payload.details}}{{/if}}'
), ),
createPushStep( createPushStep(
'meet-reminder-start-push', 'meet-reminder-start-push',
@@ -14,6 +14,7 @@ export const meetStartedPayloadSchema = z.object({
meetEndTime: z.string(), meetEndTime: z.string(),
timezone: z.string(), timezone: z.string(),
meetUrl: z.string(), meetUrl: z.string(),
details: z.string().optional(),
}); });
export type IPayload = z.infer<typeof meetStartedPayloadSchema>; export type IPayload = z.infer<typeof meetStartedPayloadSchema>;
@@ -34,12 +35,12 @@ export const workflow: WorkflowDefinition<IWorkflow> = WorkflowBuilder
createEmailStep( createEmailStep(
'meet-started-email', 'meet-started-email',
'Собрание пайщиков №{{payload.meetId}} в {{payload.coopShortName}} началось', 'Собрание пайщиков №{{payload.meetId}} в {{payload.coopShortName}} началось',
'Уважаемый пайщик!<br><br>Сегодня началось общее собрание пайщиков №{{payload.meetId}}.<br>Собрание будет проходить до {{payload.meetEndDate}} {{payload.meetEndTime}} ({{payload.timezone}}).<br><br>Просим принять участие в голосовании по вопросам повестки дня.<br>Для голосования перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a><br><br>С уважением, Совет {{payload.coopShortName}}.' 'Уважаемый пайщик!<br><br>Сегодня началось общее собрание пайщиков №{{payload.meetId}}.<br>Собрание будет проходить до {{payload.meetEndDate}} {{payload.meetEndTime}} ({{payload.timezone}}).<br><br>Просим принять участие в голосовании по вопросам повестки дня.<br>Для голосования перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a>{{#if payload.details}}<br><br>Дополнительная информация:<div style="white-space:pre-wrap;">{{payload.details}}</div>{{/if}}<br><br>С уважением, Совет {{payload.coopShortName}}.'
), ),
createInAppStep( createInAppStep(
'meet-started-notification', 'meet-started-notification',
'Собрание №{{payload.meetId}} началось', 'Собрание №{{payload.meetId}} началось',
'Примите участие в голосовании' 'Примите участие в голосовании{{#if payload.details}}. {{payload.details}}{{/if}}'
), ),
createPushStep( createPushStep(
'meet-started-push', 'meet-started-push',
+26 -32
View File
@@ -1,39 +1,33 @@
# v2025.12.28-8 # v2026.4.2-2
В этой версии представлен прототип трекера результатов интеллектуальной деятельности, реализован мост в 1С, обновлен интерфейс и существенно повышена стабильность системы. В этом релизе — стабилизация Благороста для вывода в продуктивную работу с результатами интеллектуальной деятельности. Отдельно заложена основа под отчётность в ФНС и ФСС и прототип поиска по документам.
--- **Благорост и проекты**
### ✨ Новые функции - Быстрые действия на странице программы: создать проект, компонент, задачу или требование.
- [#332](https://github.com/coopenomics/mono/issues/332): Прототип конструктора требований дополнительных документов при регистрации - Требования к компонентам в репозитории: поддержка **Mermaid**, **Draw.io** и **BPMN**.
- [#330](https://github.com/coopenomics/mono/issues/330): Прототип моста в 1С:Бухгалтерию для передачи документов и проводок - Синхронизация проектов, компонентов, требований и задач с Git-репозиторием результатов.
- [#329](https://github.com/coopenomics/mono/issues/329): Интеграция и тестирование LMS TUTOR для образовательных задач - Встраивание полноразмерного видео (iframe) на страницах проектов.
- [#328](https://github.com/coopenomics/mono/issues/328): Размещение прототипов мульти-лендингов на цифровой-кооператив.рф и coopenomics.world - Скачивание пакета подписанных документов одной кнопкой.
- [#326](https://github.com/coopenomics/mono/issues/326): Минимальный интерфейс трекера результатов интеллектуальной деятельности - Комнаты проектов в кооперативном мессенджере.
- [#324](https://github.com/coopenomics/mono/issues/324): Смарт-контракт генерации и капитализации результатов интеллектуальной деятельности ("Благорост")
- [#322](https://github.com/coopenomics/mono/issues/322): Поставка обновлений ПО с нулевым даунтаймом по blue-green стратегии
- [#321](https://github.com/coopenomics/mono/issues/321): Внедрение системы проводок по фондам для контрактов
- [#319](https://github.com/coopenomics/mono/issues/319): Палитра команд и быстрый доступ к страницам рабочих столов (cmk+k)
- [#316](https://github.com/coopenomics/mono/issues/316): Переход рабочего стола на GraphQL SDK
- [#314](https://github.com/coopenomics/mono/issues/314): Развёртывание GlitchTip для мониторинга ошибок
- [#306](https://github.com/coopenomics/mono/issues/306): Модуль запросов и мутаций для контракта капитализации
### 🐛 Исправления ошибок **Мессенджер и звонки**
- [#312](https://github.com/coopenomics/mono/issues/312): Исправление подписки на изменение статуса коммитов
- [#309](https://github.com/coopenomics/mono/issues/309): Исправление отображения чужих билетов времени в трекере
### 🔧 Улучшения - Автосекретарь: запись синхронных звонков, текстовых и голосовых сообщений в проектных комнатах.
- [#331](https://github.com/coopenomics/mono/issues/331): Настройка системы мониторинга сбоев и ошибок на базе GlitchTIP, Loki, Prometheus
- [#327](https://github.com/coopenomics/mono/issues/327): Пользовательская документация по интерфейсам цифрового кооператива **Отчётность и инфраструктура**
- [#325](https://github.com/coopenomics/mono/issues/325): Документирование смарт-контракта программы "Благорост"
- [#308](https://github.com/coopenomics/mono/issues/308): Улучшение отображения рабочих столов в магазине приложений - Прототип фабрики отчётов ФНС/ФСС: выгрузка в XML для дальнейшей отправки.
- [#307](https://github.com/coopenomics/mono/issues/307): Объединение настроек контракта с нативными настройками приложения - Прототип поисковой системы по документам.
- [#305](https://github.com/coopenomics/mono/issues/305): Объединение полей title и description в проекте - Установщик для развёртывания на своих серверах (разработка или эксплуатация).
- [#304](https://github.com/coopenomics/mono/issues/304): Доменная модель контракта капитализации на бэкенде - Рефакторинг в сторону чистой архитектуры на бэкенде.
- [#303](https://github.com/coopenomics/mono/issues/303): Пересмотр архитектуры парсера и формирования локальной истории - Ускорение сборки фронтенда за счёт перехода на **Vite 8**.
- [#302](https://github.com/coopenomics/mono/issues/302): Рефакторинг архитектуры, внедрение двухконтурной шины данных и обработки микрофорков
- [#301](https://github.com/coopenomics/mono/issues/301): Доработка и отладка контракта "Капитализация РИД" v0.2 **Исправления**
- [#222](https://github.com/coopenomics/mono/issues/222): Внедрение метода Водянова для распределения пула премий по программе "Благорост"
- [#212](https://github.com/coopenomics/mono/issues/212): Снижение точности валютных значений до двух знаков после запятой в документах - Повторное общее собрание больше не мешало завершить онбординг кооператива.
- Центр уведомлений не блокировал загрузку рабочего стола при отключённом провайдере оповещений.
- Уведомления о собрании совета по свободным вопросам снова доходят до членов совета.
- В интерфейсе восстановлено отображение контактов кооператива.
#releases #releases
+2 -2
View File
@@ -1,4 +1,4 @@
VERSION: v2025.12.28-8 VERSION: v2026.4.2-2
FROM DATE: 2025-07-2T10:00:00+03:00 FROM DATE: 2025-12-28T10:00:00+03:00
COMMENT: COMMENT:
Релиз содержит прототип трекера результатов интеллектуальной деятельности, прототип моста в 1С, обновленный интерфейс и значительно повышенную стабильность. Релиз содержит прототип трекера результатов интеллектуальной деятельности, прототип моста в 1С, обновленный интерфейс и значительно повышенную стабильность.