Merge branch 'dev' into feat/membership-exit

Втягиваем инфраструктуру исходящих платежей (шасси расходов C28) для
создания возврата паевого взноса в gateway при выходе пайщика.

Конфликты (объединены обе стороны):
- gateway payment-type.enum / payment-domain.entity: MEMBERSHIP_EXIT + EXPENSE*
- zeus-клиенты (controller, sdk): взята версия dev, регенерация следом

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
coopops
2026-06-15 16:30:43 +00:00
285 changed files with 21609 additions and 1404 deletions
@@ -82,3 +82,8 @@ export const _apps = {
production: 'apps',
testnet: 'apps',
} as const
export const _expense = {
production: 'expense',
testnet: 'expense',
} as const
@@ -0,0 +1,17 @@
import * as Permissions from '../../../common/permissions'
import type * as Capital from '../../../interfaces/capital'
import { Actors } from '../../../common'
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const
/**
* Имя действия. Инициатор программного расхода: резервирует
* `program_expense_pool` и шлёт inline action в шасси `expense::createexp`
* с callback handler `{capital, onpgexpdone}`.
*/
export const actionName = 'createpgexp'
/**
* @interface
*/
export type ICreateProgramExpense = Capital.ICreatepgexp
@@ -48,6 +48,12 @@ export * as CommitDecline from './commitDecline'
// Долги
export * as CreateDebt from './createDebt'
// Расходы программы через шасси expense (Эпик 5 шасси)
// capital — инициатор+callback, весь flow обслуживает контракт expense.
export * as TopupProgramExpense from './topupProgramExpense'
export * as CreateProgramExpense from './createProgramExpense'
export * as OnProgramExpenseDone from './onProgramExpenseDone'
// Регистрация участников
export * as RegisterContributor from './registerContributor'
@@ -0,0 +1,25 @@
import * as Permissions from '../../../common/permissions'
import type * as Capital from '../../../interfaces/capital'
import { Actors } from '../../../common'
/**
* Self-callback контракта `capital`. Шасси `expense` шлёт inline action
* на capital после терминального перехода (CLOSED либо DECLINED).
* Authority — `capital@active`, не пользователь.
*/
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._contract }] as const
/**
* Имя действия. Callback от шасси expense на финализацию program-расхода.
* status == ExpenseProposalStatus.CLOSED (4) → списать `total_actual` из
* `program_expense_reserved`, остаток вернуть в `program_expense_pool`,
* удалить запись `progexpenses`.
* status == ExpenseProposalStatus.DECLINED (5) → вернуть весь резерв в пул,
* удалить запись.
*/
export const actionName = 'onpgexpdone'
/**
* @interface
*/
export type IOnProgramExpenseDone = Capital.IOnpgexpdone
@@ -0,0 +1,17 @@
import * as Permissions from '../../../common/permissions'
import type * as Capital from '../../../interfaces/capital'
import { Actors } from '../../../common'
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const
/**
* Имя действия. Пополнение пула программных расходов из доступного остатка
* `global_available_invest_pool`. Председатель решает сколько денег из
* инвестиционного пула программы перевести в пул, доступный для расходов.
*/
export const actionName = 'topupprogexp'
/**
* @interface
*/
export type ITopupProgramExpense = Capital.ITopupprogexp
@@ -6,6 +6,7 @@ export * as Commits from './commits'
export * as Contributors from './contributors'
export * as Debts from './debts'
export * as Expenses from './expenses'
export * as ProgramExpenses from './programExpenses'
export * as Invests from './invests'
export * as ProgramInvests from './programInvests'
export * as ProgramProperties from './programProperties'
@@ -0,0 +1,18 @@
import type * as Capital from '../../../interfaces/capital'
import { Actors } from '../../../common'
/**
* Имя таблицы
*/
export const tableName = 'progexpenses'
/**
* Таблица хранится в {@link Actors._coopname | области памяти кооператива}.
*/
export const scope = Actors._coopname
/**
* @interface
* Таблица содержит информацию о расходах программы (Благорост Эпик B).
*/
export type IProgramExpense = Capital.IProgramExpense
@@ -0,0 +1,14 @@
import * as Permissions from '../../../common/permissions'
import type * as Expense from '../../../interfaces/expense'
import { Actors } from '../../../common'
/**
* authexp — авторизация СЗ советом (signact2 decision_doc, type=2011).
* CREATED → AUTHORIZED. См. expense.hpp::authexp.
*/
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const
export const actionName = 'authexp'
/** @interface */
export type IAuthExp = Expense.IAuthexp
@@ -0,0 +1,15 @@
import * as Permissions from '../../../common/permissions'
import type * as Expense from '../../../interfaces/expense'
import { Actors } from '../../../common'
/**
* closeexp — финализация СЗ-отчёта советом. REPORT_SUBMITTED → CLOSED.
* При заполненном callback — inline action (capital::createrid для Благороста).
* См. expense.hpp::closeexp.
*/
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const
export const actionName = 'closeexp'
/** @interface */
export type ICloseExp = Expense.ICloseexp
@@ -0,0 +1,14 @@
import * as Permissions from '../../../common/permissions'
import type * as Expense from '../../../interfaces/expense'
import { Actors } from '../../../common'
/**
* createexp — создаёт и подаёт СЗ-расход. Подписывает создатель
* (signact1 statement_doc, type=2010). См. expense.hpp::createexp.
*/
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._username }] as const
export const actionName = 'createexp'
/** @interface */
export type ICreateExp = Expense.ICreateexp
@@ -0,0 +1,14 @@
import * as Permissions from '../../../common/permissions'
import type * as Expense from '../../../interfaces/expense'
import { Actors } from '../../../common'
/**
* declexp — отклонение СЗ-предложения. CREATED/REPORT_SUBMITTED → DECLINED.
* Capitalization Благороста НЕ запускается. См. expense.hpp::declexp.
*/
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const
export const actionName = 'declexp'
/** @interface */
export type IDeclineExp = Expense.IDeclexp
@@ -0,0 +1,11 @@
// Actions контракта expense (см. components/contracts/cpp/expense/expense.hpp).
// MVP: 8 actions для шасси расходов цифрового кооператива.
export * as CreateExp from './createExp'
export * as AuthExp from './authExp'
export * as DeclineExp from './declineExp'
export * as PayExp from './payExp'
export * as ReportExp from './reportExp'
export * as CloseExp from './closeExp'
export * as ReturnExp from './returnExp'
export * as OverspendExp from './overspendExp'
@@ -0,0 +1,15 @@
import * as Permissions from '../../../common/permissions'
import type * as Expense from '../../../interfaces/expense'
import { Actors } from '../../../common'
/**
* overspendexp — доплата при перерасходе ADVANCE.
* Контракт зовёт Ledger2::apply(OVERSPEND) + сразу Ledger2::apply(ADVANCE_REPORT) одной транзакцией.
* См. expense.hpp::overspendexp.
*/
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const
export const actionName = 'overspendexp'
/** @interface */
export type IOverspendExp = Expense.IOverspendexp
@@ -0,0 +1,15 @@
import * as Permissions from '../../../common/permissions'
import type * as Expense from '../../../interfaces/expense'
import { Actors } from '../../../common'
/**
* payexp — оплата item: ADVANCE (выдача аванса) или DIRECT (прямая оплата).
* Контракт зовёт Ledger2::apply(operation_code, actual_amount).
* Для DIRECT сразу за payexp идёт фоновый reportexp. См. expense.hpp::payexp.
*/
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const
export const actionName = 'payexp'
/** @interface */
export type IPayExp = Expense.IPayexp
@@ -0,0 +1,14 @@
import * as Permissions from '../../../common/permissions'
import type * as Expense from '../../../interfaces/expense'
import { Actors } from '../../../common'
/**
* reportexp — закрытие ADVANCE-item чеком. Контракт зовёт Ledger2::apply(ADVANCE_REPORT).
* Все items REPORTED → proposal становится REPORT_SUBMITTED. См. expense.hpp::reportexp.
*/
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._username }] as const
export const actionName = 'reportexp'
/** @interface */
export type IReportExp = Expense.IReportexp
@@ -0,0 +1,14 @@
import * as Permissions from '../../../common/permissions'
import type * as Expense from '../../../interfaces/expense'
import { Actors } from '../../../common'
/**
* returnexp — возврат неиспользованного ADVANCE-остатка.
* Контракт зовёт Ledger2::apply(ADVANCE_RETURN). См. expense.hpp::returnexp.
*/
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._username }] as const
export const actionName = 'returnexp'
/** @interface */
export type IReturnExp = Expense.IReturnexp
@@ -0,0 +1,9 @@
import * as contractNames from '../../common/names'
export * as Actions from './actions'
export * as Tables from './tables'
/** @private */
export * as Interfaces from '../../interfaces/expense'
export const contractName = contractNames._expense
@@ -0,0 +1 @@
export * as Proposals from './proposals'
@@ -0,0 +1,13 @@
import type * as Expense from '../../../interfaces/expense'
import { Actors } from '../../../common'
/**
* Таблица СЗ-расходов. Scope = coopname.
* On-chain индексы: byhash (proposal_hash), byusername (username), bystatus (status).
*/
export const tableName = 'proposals'
export const scope = Actors._coopname
/** @interface */
export type IProposal = Expense.IProposal
@@ -94,3 +94,13 @@ export * as MeetContract from './meet'
* Деплоится на корневой KE-цепи и обслуживает несколько подсетей одновременно.
*/
export * as AppsContract from './apps'
/**
* Смарт-контракт шасси расходов цифрового кооператива (MVP — Благорост).
*
* Универсальный движок СЗ-расходов: principal storage = proposal с массивом items,
* operation_code из ledger2 OPERATION_REGISTRY передаётся в payexp,
* callback на финализацию задаётся при createexp (контракт+action+data).
* См. components/contracts/cpp/expense/expense.hpp (C28-29 v3).
*/
export * as ExpenseContract from './expense'
@@ -112,6 +112,18 @@ export interface IGenerate extends Omit<Partial<IMetaDocument>, 'title'> {
[key: string]: any
}
/**
* Зарезервированная ссылка на приватный payload документа.
* Action конкретного документа расширяет этот интерфейс, если хочет
* хранить персональные/приватные данные off-chain — payload лежит в
* коллекции `doc_private_data` фабрики, on-chain публикуется только
* `doc_data_hash`. См. раздел «Document Generation Pattern: doc_data»
* в архитектуре.
*/
export interface IDocDataRef {
doc_data_hash?: string
}
/**
* Интерфейс генерации заявления на вступление в кооператив
*/
@@ -0,0 +1,157 @@
import type { IDocDataRef, IGenerate, IMetaDocument } from '../../document'
import type { ICommonUser, ICooperativeData, IVars } from '../../model'
export const registry_id = 2010
export type ExpenseItemRecipientType = 'SELF' | 'MEMBER' | 'ORG'
export type ExpenseItemPaymentMechanics = 'ADVANCE' | 'DIRECT'
/**
* Публичная часть позиции расхода — единственное, что публикуется в meta
* документа on-chain. Реквизиты получателя, его имя и назначение платежа
* сюда НЕ входят — они приватны (см. {@link IExpensePrivateItem}).
*/
export interface IExpenseItem {
number: string
description: string
amount: string
recipient_type: ExpenseItemRecipientType
mechanics: ExpenseItemPaymentMechanics
}
/**
* Приватная часть позиции — НЕ публикуется в блокчейн. Имя получателя,
* его банковские реквизиты и назначение платежа сохраняются off-chain в
* `doc_private_data` фабрики; в meta документа едет только `doc_data_hash`.
* Корреляция с публичной позицией — по `number`.
*/
export interface IExpensePrivateItem {
number: string
recipient_name?: string
requisites?: string
/** Назначение платежа — отдельной строкой после реквизитов (для оплаты по счёту) */
payment_purpose?: string
}
/**
* Приватный payload СЗ-сметы целиком (off-chain). Сохраняется в
* `DocDataService` фабрики, адресуется публичным `doc_data_hash`.
* См. раздел «Document Generation Pattern: doc_data» в архитектуре.
*/
export interface PrivateData {
items: IExpensePrivateItem[]
}
/**
* Позиция в модели рендера — публичная + приватная части, склеенные фабрикой
* по `number` при генерации документа. Шаблон обращается к полям как `item.*`.
*/
export interface IExpenseRenderItem extends IExpenseItem {
recipient_name?: string
requisites?: string
payment_purpose?: string
}
export interface IExpenseProposalHeader {
description: string
total_amount: string
items_count: number
source_wallet: string
/** Срок исполнения («в срок до»), формат DD.MM.YYYY — передаётся при создании расхода */
deadline?: string
/** Фонд списания в дательном падеже («по …») — подставляет сервер из параметров шасси расходов */
fund_name?: string
}
export interface Action extends IGenerate, IDocDataRef {
proposal_hash: string
proposal: IExpenseProposalHeader
/** Публичные позиции расхода — без реквизитов/имени/назначения (они в doc_data). */
items: IExpenseItem[]
/** sha256 приватного payload (PrivateData) — реквизиты/имя/назначение off-chain. */
doc_data_hash: string
}
export type Meta = IMetaDocument & Action
export interface Model {
meta: IMetaDocument
coop: ICooperativeData
user: ICommonUser
vars: IVars
proposal_hash: string
/** Короткий идентификатор СЗ (первые 16 символов хэша, uppercase) — для шапки документа */
proposal_short_hash: string
proposal: IExpenseProposalHeader
/** Позиции с подмешанной приватной частью (фабрика склеивает по number из doc_data). */
items: IExpenseRenderItem[]
}
export const title = 'Служебная записка-смета о расходах'
export const description = 'Заявка-смета председателю кооператива на финансирование расходов по программе. Содержит описание цели и массив позиций расхода.'
// Вёрстка по канону документов реестра (см. 1040): без <style>-блока — превью
// его не применяет; только inline text-align на div-обёртках. Шапка-адресат
// справа, центрированный заголовок с идентификатором (= proposal_hash, по нему
// записку можно найти), позиции — нумерованным списком: описание и сумма,
// затем построчно получатель, реквизиты и назначение платежа (для кассира).
// Кошелёк-источник в документе не указывается.
export const context = `<div class="digital-document"><div style="text-align: right"><p>{% trans 'TO_COUNCIL' %} {{vars.full_abbr_genitive}} «{{vars.name}}»</p><p>{% trans 'FROM_MEMBER' %} {{ user.full_name_or_short_name }}</p></div><div style="text-align: center"><h2>{% trans 'PROPOSAL_TITLE' %} № {{ proposal_short_hash }}</h2></div><p style="text-align: right">{{ coop.city }}, {{ meta.created_at }}</p><p>{% trans 'BODY_INTRO' %}{% if proposal.fund_name %} {% trans 'BY_FUND' %} {{ proposal.fund_name }}{% endif %} {% trans 'IN_TOTAL' %} {{ proposal.total_amount }}{% if proposal.deadline %} {% trans 'DUE_BY' %} {{ proposal.deadline }}{% endif %}, {% trans 'BODY_NAMELY' %}:</p>{% for item in items %}<div style="padding-top: 10px"><p><strong>{{ item.number }}. {{ item.description }} — {{ item.amount }} ({% if item.mechanics == 'ADVANCE' %}{% trans 'MECH_ADVANCE' %}{% else %}{% trans 'MECH_DIRECT' %}{% endif %})</strong></p><p>{% trans 'ITEM_RECIPIENT' %}: {% if item.recipient_type == 'SELF' %}{{ user.full_name_or_short_name }}{% else %}{{ item.recipient_name }}{% endif %}</p>{% if item.requisites %}<p>{% trans 'ITEM_REQUISITES' %}: {{ item.requisites }}</p>{% endif %}{% if item.payment_purpose %}<p>{% trans 'ITEM_PAYMENT_PURPOSE' %}: {{ item.payment_purpose }}</p>{% endif %}</div>{% endfor %}<p>{% trans 'PURPOSE' %}: {{ proposal.description }}</p><div style="padding-top: 30px"><p>{{ user.full_name_or_short_name }}</p><p>{% trans 'SIGNED_DIGITALLY' %}</p></div></div>`
export const translations = {
ru: {
TO_COUNCIL: 'В Совет',
FROM_MEMBER: 'от пайщика',
PROPOSAL_TITLE: 'СЛУЖЕБНАЯ ЗАПИСКА',
BODY_INTRO: 'Прошу согласовать списание затрат',
BY_FUND: 'по',
IN_TOTAL: 'в общей сумме',
DUE_BY: 'в срок до',
BODY_NAMELY: 'а именно',
MECH_ADVANCE: 'аванс под отчёт',
MECH_DIRECT: 'оплата по счёту',
ITEM_RECIPIENT: 'Получатель',
ITEM_REQUISITES: 'Реквизиты',
ITEM_PAYMENT_PURPOSE: 'Назначение платежа',
PURPOSE: 'Цель расходов',
SIGNED_DIGITALLY: 'подписано электронной подписью',
},
}
export const exampleData = {
coop: { city: 'Москва' },
meta: { created_at: '02.06.2026 14:00' },
vars: { full_abbr_genitive: 'ПК', name: 'Восход' },
user: { full_name_or_short_name: 'Иванов И.И.' },
proposal_hash: '55c470039a8c53ce1b4b6e842fe8063ab3d5b85ba2ba8ab0ae6e30be3ad328b7',
proposal_short_hash: '55C470039A8C53CE',
proposal: {
description: 'Закупка хостинга и бухгалтерских услуг на июнь 2026',
total_amount: '15000.00 RUB',
items_count: 2,
source_wallet: 'w.cap.bla',
deadline: '30.06.2026',
fund_name: 'Фонду хозяйственной деятельности',
},
items: [
{
number: '1',
description: 'Аренда серверов Yandex Cloud (тариф S, июнь)',
amount: '12000.00 RUB',
recipient_type: 'ORG',
mechanics: 'DIRECT',
recipient_name: 'ООО «Яндекс.Облако»',
requisites: 'ИНН 7704414297, р/с 40702810000000000000, БИК 044525000',
payment_purpose: 'Оплата по счёту № 814 от 01.06.2026 за аренду серверов',
},
{
number: '2',
description: 'Канцелярия для офиса',
amount: '3000.00 RUB',
recipient_type: 'SELF',
mechanics: 'ADVANCE',
recipient_name: '',
requisites: 'Банковский перевод: счёт 40817810000000000000, Банк ВТБ (ПАО), БИК 044525187',
},
],
}
@@ -0,0 +1,138 @@
import type { IDecisionData, IGenerate, IMetaDocument } from '../../document'
import type { ICooperativeData, IVars } from '../../model'
import type { IExpenseItem, IExpenseProposalHeader } from '../2010.ExpenseProposalStatement'
export const registry_id = 2011
export type ExpenseProposalDecisionKind = 'approve' | 'decline'
/** Резолюция совета по СЗ: утвердить / отказать (+ причина отказа). */
export interface IExpenseProposalDecisionBody {
kind: ExpenseProposalDecisionKind
reason?: string
}
export interface Action extends IGenerate {
proposal_hash: string
/** Идентификатор решения совета (повестка) — источник данных голосования */
decision_id: number
proposal: IExpenseProposalHeader
items: IExpenseItem[]
resolution: IExpenseProposalDecisionBody
}
export type Meta = IMetaDocument & Action
export interface Model {
meta: IMetaDocument
coop: ICooperativeData
vars: IVars
/** Данные собрания совета (кворум, голоса) — канон протоколов решений */
decision: IDecisionData
proposal_hash: string
/** Короткий идентификатор СЗ (первые 16 символов хэша, uppercase) */
proposal_short_hash: string
proposal: IExpenseProposalHeader
items: IExpenseItem[]
resolution: IExpenseProposalDecisionBody
}
export const title = 'Протокол решения совета о расходах'
export const description = 'Протокол собрания совета по служебной записке-смете о расходах. Структура — канон протоколов решений совета (кворум, голоса, повестка); меняется только содержание вопроса и принятого решения.'
// Вёрстка — точная копия канона протоколов решений совета (600.FreeDecision):
// форма/место/дата/время, члены совета, правомочность, повестка, голосование,
// РЕШИЛИ, подпись председателя. Отличается только содержание вопроса повестки
// и принятого решения (смета расходов по СЗ).
export const context = '<style> \nh1 {\nmargin: 0px; \ntext-align:center;\n}\nh3{\nmargin: 0px;\npadding-top: 15px;\n}\n.about {\npadding: 20px;\n}\n.about p{\nmargin: 0px;\n}\n.signature {\npadding-top: 20px;\n}\n.digital-document {\npadding: 20px;\nwhite-space: pre-wrap;\n}\n.subheader {\npadding-bottom: 20px; \n}\ntable {\n width: 100%;\n border-collapse: collapse;\n}\nth, td {\n border: 1px solid #ccc;\n padding: 8px;\n text-align: left;\n word-wrap: break-word; \n overflow-wrap: break-word; \n}\nth {\n background-color: #f4f4f4;\n width: 30%;\n}\n</style>\n\n<div class="digital-document"><h1 class="header">{% trans \'protocol_number\', decision.id %}</h1>\n<p style="text-align:center" class="subheader">{% trans \'council_meeting_name\' %} {{vars.full_abbr_genitive}} "{{vars.name}}"</p>\n<p style="text-align: right"> {{ meta.created_at }}, {{ coop.city }}</p>\n<table class="about">\n<tbody>\n<tr>\n<th>{% trans \'meeting_format\' %}</th>\n<td>{% trans \'meeting_format_value\' %}</td>\n</tr>\n<tr>\n<th>{% trans \'meeting_place\' %}</th>\n<td>{{ coop.full_address }}</td>\n</tr>\n<tr>\n<th>{% trans \'meeting_date\' %}</th>\n<td>{{ decision.date }}</td>\n</tr>\n<tr>\n<th>{% trans \'opening_time\' %}</th>\n<td>{{ decision.time }}</td>\n</tr>\n</tbody>\n</table>\n<h3>{% trans \'council_members\' %}</h3>\n<table>\n<tbody>\n{% for member in coop.members %}\n<tr>\n<th>{% if member.is_chairman %}{% trans \'chairman_of_the_council\' %}{% else %}{% trans \'member_of_the_council\' %}{% endif %}</th>\n<td>{{ member.last_name }} {{ member.first_name }} {{ member.middle_name }}</td>\n</tr>\n{% endfor %}\n</tbody>\n</table>\n<h3>{% trans \'meeting_legality\' %} </h3>\n<p>{% trans \'voting_results\', decision.voters_percent %} {% trans \'quorum\' %} {% trans \'chairman_of_the_meeting\', coop.chairman.last_name, coop.chairman.first_name, coop.chairman.middle_name %}.</p>\n<h3>{% trans \'agenda\' %}</h3>\n<table>\n<tbody>\n<tr>\n<th>№</th>\n<td>{% trans \'question\' %}</td>\n</tr>\n<tr>\n<th>1</th>\n<td>{% trans \'question_body\', proposal_short_hash, proposal.total_amount %} {% trans \'question_purpose\' %}: {{ proposal.description }}</td>\n</tr>\n</tbody>\n</table>\n<h3>{% trans \'voting\' %}</h3>\n<p>{% trans \'vote_results\' %} </p><table>\n<tbody>\n<tr>\n<th>{% trans \'votes_for\' %}</th>\n<td>{{ decision.votes_for }}</td>\n</tr>\n<tr>\n<th>{% trans \'votes_against\' %}</th>\n<td>{{ decision.votes_against }}</td>\n</tr>\n<tr>\n<th>{% trans \'votes_abstained\' %}</th>\n<td>{{ decision.votes_abstained }}</td>\n</tr>\n</tbody>\n</table>\n<h3>{% trans \'decision_made\' %}</h3>\n<table>\n<tbody>\n<tr>\n<th>№</th>\n<td>{% trans \'decision\' %}</td>\n</tr>\n<tr>\n<th>1</th>\n<td>{% if resolution.kind == \'approve\' %}<p>{% trans \'decision_approve\', proposal_short_hash, proposal.total_amount %}{% if proposal.deadline %} {% trans \'decision_deadline\', proposal.deadline %}{% endif %}{% trans \'decision_namely\' %}:</p>{% for item in items %}<p>{{ item.number }}. {{ item.description }} — {{ item.amount }} ({% if item.mechanics == \'ADVANCE\' %}{% trans \'mech_advance\' %}{% else %}{% trans \'mech_direct\' %}{% endif %})</p>{% endfor %}{% else %}<p>{% trans \'decision_decline\', proposal_short_hash %}{% if resolution.reason %} {% trans \'decline_reason\' %}: {{ resolution.reason }}{% endif %}</p>{% endif %}</td>\n</tr>\n</tbody>\n</table>\n<hr>\n<p>{% trans \'closing_time\', decision.time %}</p>\n<div class="signature"><p>{% trans \'signature\' %}</p><p>{% trans \'chairman\' %} {{ coop.chairman.last_name }} {{ coop.chairman.first_name }} {{ coop.chairman.middle_name }}</p></div></div>\n'
export const translations = {
ru: {
meeting_format: 'Форма',
meeting_date: 'Дата',
meeting_place: 'Место',
opening_time: 'Время открытия',
council_members: 'ЧЛЕНЫ СОВЕТА',
voting_results: 'Количество голосов составляет {0}% от общего числа членов Совета.',
meeting_legality: 'СОБРАНИЕ ПРАВОМОЧНО',
chairman_of_the_meeting: 'Председатель собрания совета: {0} {1} {2}',
agenda: 'ПОВЕСТКА ДНЯ',
vote_results: 'По первому вопросу повестки дня проголосовали:',
decision_made: 'РЕШИЛИ',
closing_time: 'Время закрытия собрания совета: {0}.',
protocol_number: 'ПРОТОКОЛ № {0}',
council_meeting_name: 'Собрания Совета',
chairman_of_the_council: 'Председатель совета',
signature: 'Документ подписан электронной подписью.',
chairman: 'Председатель',
quorum: 'Кворум для решения поставленных на повестку дня вопросов имеется.',
voting: 'ГОЛОСОВАНИЕ',
meeting_format_value: 'Заочная',
member_of_the_council: 'Член совета',
question: 'Вопрос',
votes_for: 'ЗА',
votes_against: 'ПРОТИВ',
votes_abstained: 'ВОЗДЕРЖАЛСЯ',
decision: 'Решение',
question_body: 'Рассмотрение служебной записки-сметы о расходах № {0} на общую сумму {1}.',
question_purpose: 'Цель расходов',
decision_approve: 'Утвердить расходы по служебной записке № {0} в общей сумме {1}',
decision_deadline: 'со сроком исполнения до {0}',
decision_namely: ', а именно',
mech_advance: 'аванс под отчёт',
mech_direct: 'оплата по счёту',
decision_decline: 'Отказать в утверждении расходов по служебной записке № {0}.',
decline_reason: 'Основание отказа',
},
}
export const exampleData = {
meta: {
created_at: '03.06.2026 11:00',
},
coop: {
city: 'Москва',
full_address: 'г. Москва, ул. Смольная, д. 3, оф. 84',
chairman: {
last_name: 'Муравьев',
first_name: 'Алексей',
middle_name: 'Николаевич',
},
},
decision: {
time: '11:00',
date: '03.06.2026',
votes_for: '3',
votes_against: '0',
votes_abstained: '0',
voters_percent: '100',
id: '15',
},
member: {
is_chairman: true,
last_name: 'Муравьев',
first_name: 'Алексей',
middle_name: 'Николаевич',
},
vars: {
full_abbr_genitive: 'потребительского кооператива',
name: 'ВОСХОД',
},
proposal_hash: '55c470039a8c53ce1b4b6e842fe8063ab3d5b85ba2ba8ab0ae6e30be3ad328b7',
proposal_short_hash: '55C470039A8C53CE',
proposal: {
description: 'Закупка хостинга и бухгалтерских услуг на июнь 2026',
total_amount: '15000.00 RUB',
items_count: 2,
source_wallet: 'w.cap.bla',
deadline: '30.06.2026',
fund_name: 'Фонду хозяйственной деятельности',
},
items: [
{ number: '1', description: 'Аренда серверов Yandex Cloud', amount: '12000.00 RUB', recipient_type: 'ORG', mechanics: 'DIRECT' },
{ number: '2', description: 'Канцелярия для офиса', amount: '3000.00 RUB', recipient_type: 'SELF', mechanics: 'ADVANCE' },
],
resolution: {
kind: 'approve',
},
}
@@ -44,6 +44,10 @@ export * as InitProjectDecision from './1006.InitProjectDecision'
export * as ExpenseStatement from './1010.ExpenseStatement'
export * as ExpenseDecision from './1011.ExpenseDecision'
// Шасси расходов (волна 6, MVP-SINGLE) — C28-30
export * as ExpenseProposalStatement from './2010.ExpenseProposalStatement'
export * as ExpenseProposalDecision from './2011.ExpenseProposalDecision'
export * as GenerationMoneyInvestStatement from './1020.GenerationMoneyInvestStatement'
export * as GenerationMoneyReturnUnusedStatement from './1025.GenerationMoneyReturnUnusedStatement'
+71 -1
View File
@@ -1,11 +1,14 @@
// Generated by eosio-abi2ts 1.2.2 - eosio::abi/1.2
import type { IItem as IExpenseProposalItem } from './expense'
export type IAsset = string
export type IName = string
export type IChecksum256 = string
export type IPublicKey = string
export type ISignature = string
export type ITimePointSec = string
export type IUint8 = number
export type IUint32 = number
export type IUint64 = number | string
export type IFloat64 = number
@@ -359,6 +362,8 @@ export interface IDebt {
approved_statement: IDocument2
authorization: IDocument2
memo: string
last_pay_error?: string
due_at?: ITimePointSec
}
export interface IDebtauthcnfr {
@@ -556,6 +561,8 @@ export interface IGlobalState {
program_membership_distributed: IAsset
program_membership_cumulative_reward_per_share: IFloat64
config: IConfig
program_expense_pool?: IAsset
program_expense_reserved?: IAsset
}
export interface IImportcontrib {
@@ -803,6 +810,9 @@ export interface ISegment {
available_for_program: IAsset
available_for_wallet: IAsset
share_percent: IFloat64
approved_rate_per_hour?: IAsset
approved_hours_per_day?: IUint64
active_debts_count?: IUint32
}
export interface ISetconfig {
@@ -827,7 +837,7 @@ export interface ISetplan {
export interface ISettledebt {
coopname: IName
username: IName
debt_hash: IChecksum256
amount: IAsset
statement: IDocument2
}
@@ -912,3 +922,63 @@ export interface IVotingData {
amounts: IVotingAmounts
voting_deadline: ITimePointSec
}
// ─── Эпик 5 (шасси расходов): програмные расходы через шасси expense ──────
/**
* Инициация программного расхода через шасси expense.
* capital резервирует пул, шлёт inline action в expense::createexp с
* callback handler `{capital, onpgexpdone}`. Шасси обслуживает весь flow
* расхода (auth/pay/report/close|decline) и шлёт callback назад.
*/
export interface ICreatepgexp {
coopname: IName
expense_hash: IChecksum256
creator: IName
/** Позиции СЗ-расхода (Expense.IItem из шасси); механика оплаты — per-item (mechanics: ADVANCE | DIRECT). */
items: IExpenseProposalItem[]
description: string
statement: IDocument2
}
/**
* Self-callback `capital`. Шасси `expense` дёргает после CLOSED/DECLINED.
* status == ExpenseProposalStatus.CLOSED (4) → consume + release остаток + delete.
* status == ExpenseProposalStatus.DECLINED (5) → release полностью + delete.
*/
export interface IOnpgexpdone {
coopname: IName
expense_hash: IChecksum256
status: IUint8
total_actual: IAsset
/** Произвольный payload от инициатора (на MVP не используется). */
data: string
}
export interface ITopupprogexp {
coopname: IName
amount: IAsset
}
// Таблицы
/**
* Таблица `progexpenses` — целевые расходы программы «Благорост», не привязанные
* к проекту. Создаётся в `createpgexp`, удаляется в `onpgexpdone` после
* терминального перехода шасси. На MVP approved_statement / authorization
* остаются пустыми (флоу подписания делает шасси expense).
*/
export interface IProgramExpense {
id: IUint64
coopname: IName
username: IName
status: IName
expense_hash: IChecksum256
fund_id: IUint64
amount: IAsset
description: string
expense_statement: IDocument2
approved_statement: IDocument2
authorization: IDocument2
spended_at: ITimePointSec
}
@@ -0,0 +1,177 @@
// Hand-written from components/contracts/cpp/expense/expense.hpp (C28-29 v3, MVP — Благорост).
// До появления codegen из ABI этот файл = SoT для cooptypes/expense.
// При regen ABI заменить ровно тем же набором имён интерфейсов, не ломая публичный API.
export type IAsset = string
export type IName = string
export type IChecksum256 = string
export type ITimePointSec = string
export type IUint8 = number
export type IUint32 = number
export type IUint64 = number | string
/**
* Документ document2 (signact1/signact2 — двухподписные акты).
* Точная копия C++ struct document2 из components/contracts/cpp/lib/domain/document_core.hpp.
*/
export interface IDocument2 {
version: string
hash: IChecksum256
doc_hash: IChecksum256
meta_hash: IChecksum256
meta: string
signatures: ISignature2[]
}
export interface ISignature2 {
id: IUint32
signed_hash: IChecksum256
signer: IName
public_key: string
signature: string
signed_at: ITimePointSec
meta: string
}
/**
* Перечисления expense::* — в TS экспонируются как `number` (uint8), сами литералы
* (`enum`) задаются на стороне backend extension (`expense-proposal-status.enum.ts`
* + `expense-item-status.enum.ts` + `expense-mechanics.enum.ts` + `expense-recipient-type.enum.ts`).
*/
export interface IItem {
item_hash: IChecksum256
/** Mechanics (ADVANCE=0, DIRECT=1). */
mechanics: IUint8
/** RecipientType (SELF=0, MEMBER=1, ORG=2). */
recipient_type: IUint8
recipient: IName
description: string
planned_amount: IAsset
actual_amount: IAsset
/** ItemStatus (APPROVED=0, PAID=1, REPORTED=2, RETURNED=3, OVERSPENT=4). */
status: IUint8
}
/**
* Callback на финализацию closeexp. Пустой `contract` (`""`) = нет callback.
* `data` — base16/hex (eosio bytes) либо base64 в зависимости от транспорта.
*/
export interface ICallbackHandler {
contract: IName
action: IName
data: string
}
// ─────────────────────────── Actions ───────────────────────────
/**
* createexp — создание и подача СЗ-расхода (signact1 statement_doc, type=2010).
*/
export interface ICreateexp {
coopname: IName
username: IName
proposal_hash: IChecksum256
source_wallet: IName
items: IItem[]
callback: ICallbackHandler
statement: IDocument2
}
/**
* authexp — авторизация СЗ советом (signact2 decision_doc, type=2011).
* После — CREATED → AUTHORIZED.
*/
export interface IAuthexp {
coopname: IName
proposal_hash: IChecksum256
decision: IDocument2
}
/**
* declexp — отклонение СЗ-предложения (CREATED/REPORT_SUBMITTED → DECLINED).
* Capitalization Благороста НЕ запускается.
*/
export interface IDeclexp {
coopname: IName
proposal_hash: IChecksum256
reason: string
}
/**
* payexp — оплата item (ADVANCE = выдача аванса / DIRECT = прямая оплата).
* Контракт: Ledger2::apply(operation_code, actual_amount); для DIRECT сразу за payexp — фоновый reportexp.
*/
export interface IPayexp {
coopname: IName
proposal_hash: IChecksum256
item_hash: IChecksum256
actual_amount: IAsset
}
/**
* reportexp — закрытие item чеком (только ADVANCE).
* Контракт: Ledger2::apply(ADVANCE_REPORT). Все items reported → PARTIALLY_PAID/CLOSED-эскалация
* выносится в closeexp.
*/
export interface IReportexp {
coopname: IName
proposal_hash: IChecksum256
item_hash: IChecksum256
}
/**
* closeexp — финализация СЗ-отчёта советом. REPORT_SUBMITTED → CLOSED.
* При наличии callback — inline action на (callback.contract, callback.action, callback.data).
* **Триггерит capitalization Благороста** (см. capital::createrid через callback).
*/
export interface ICloseexp {
coopname: IName
proposal_hash: IChecksum256
}
/**
* returnexp — возврат неиспользованного аванса (ADVANCE-остаток).
* Контракт: Ledger2::apply(ADVANCE_RETURN).
*/
export interface IReturnexp {
coopname: IName
proposal_hash: IChecksum256
item_hash: IChecksum256
return_amount: IAsset
}
/**
* overspendexp — доплата при перерасходе (только ADVANCE).
* Контракт: Ledger2::apply(OVERSPEND) → сразу Ledger2::apply(ADVANCE_REPORT) одной транзакцией.
*/
export interface IOverspendexp {
coopname: IName
proposal_hash: IChecksum256
item_hash: IChecksum256
overspend_amount: IAsset
}
// ─────────────────────────── Tables ───────────────────────────
/**
* Таблица `proposals` (scope = coopname).
* Индексы on-chain: byhash (proposal_hash), byusername (username), bystatus (status).
*/
export interface IProposal {
id: IUint64
coopname: IName
proposal_hash: IChecksum256
username: IName
source_wallet: IName
/** ProposalStatus (CREATED=0, AUTHORIZED=1, PARTIALLY_PAID=2, REPORT_SUBMITTED=3, CLOSED=4, DECLINED=5). */
status: IUint8
items: IItem[]
total_planned: IAsset
total_actual: IAsset
callback: ICallbackHandler
statement_doc: IDocument2
decision_doc: IDocument2
created_at: ITimePointSec
updated_at: ITimePointSec
}
@@ -1,6 +1,7 @@
export * as Branch from './branch'
export * as Capital from './capital'
export * as Draft from './draft'
export * as Expense from './expense'
export * as Fund from './fund'
export * as Gateway from './gateway'
export * as Ledger from './ledger'
@@ -108,6 +108,37 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
{ code: 'o.cap.cnvbl', process_type: 'p.cap.rid', contract: 'capital', name: 'CONVERT_TO_BLAGO', wallet_op: 'TRANSFER', wallet_from: 'w.cap.gen', wallet_to: 'w.cap.blago', debit: null, credit: null, human_name: 'Конвертация сегмента: РИД → ЦПП «Благорост»' },
{ code: 'o.cap.pgtop', process_type: 'p.cap.pgexp', contract: 'capital',
name: 'PROGRAM_EXPENSE_TOPUP', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.pgexp',
debit: null, credit: null,
human_name: 'Пополнение пула программных расходов ЦПП «Благорост»' },
// expense — шасси расходов (оплата СЗ из кооперативного пула расходов)
{ code: 'o.exp.blgadv', process_type: 'p.exp.expns', contract: 'expense',
name: 'BLAGO_ADVANCE', wallet_op: 'TRANSFER', wallet_from: 'w.cap.pgexp', wallet_to: 'w.exp.adv',
debit: 8, credit: 51,
human_name: 'Выдача подотчётных из пула расходов ЦПП «Благорост»' },
{ code: 'o.exp.blgdir', process_type: 'p.exp.expns', contract: 'expense',
name: 'BLAGO_DIRECT', wallet_op: 'BURN', wallet_from: 'w.cap.pgexp', wallet_to: null,
debit: 8, credit: 51,
human_name: 'Прямая оплата из пула расходов ЦПП «Благорост»' },
{ code: 'o.exp.advrpt', process_type: 'p.exp.expns', contract: 'expense',
name: 'ADVANCE_REPORT', wallet_op: 'BURN', wallet_from: 'w.exp.adv', wallet_to: null,
debit: null, credit: null,
human_name: 'Закрытие подотчёта пайщика по отчёту' },
{ code: 'o.exp.advret', process_type: 'p.exp.expns', contract: 'expense',
name: 'ADVANCE_RETURN', wallet_op: 'TRANSFER', wallet_from: 'w.exp.adv', wallet_to: 'w.cap.pgexp',
debit: 51, credit: 8,
human_name: 'Возврат неиспользованного подотчёта в пул расходов' },
{ code: 'o.exp.over', process_type: 'p.exp.expns', contract: 'expense',
name: 'OVERSPEND', wallet_op: 'TRANSFER', wallet_from: 'w.cap.pgexp', wallet_to: 'w.exp.adv',
debit: 8, credit: 51,
human_name: 'Доплата сверх подотчёта (перерасход)' },
// marketplace
{ code: 'o.mkt.supply', process_type: 'p.mkt.reqst', contract: 'marketplace', name: 'CONFIRM_SUPPLY', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.wal.share', debit: 51, credit: 80, human_name: 'Подтверждение поставки' },
@@ -36,6 +36,10 @@ export const LEDGER2_PROCESS_REGISTRY: readonly ProcessMeta[] = [
{ type: 'p.cap.prop', contract: 'capital', name: 'PROPERTY', human_name: 'Приём имущественного паевого взноса' },
{ type: 'p.cap.preimp', contract: 'capital', name: 'PREIMP', human_name: 'Первичный учёт РИД-взноса до перехода на электронный учёт' },
{ type: 'p.cap.wthcap', contract: 'capital', name: 'WTHCAP', human_name: 'Возврат паевого из ЦПП «Благорост» в кошелёк' },
{ type: 'p.cap.pgexp', contract: 'capital', name: 'PGEXP', human_name: 'Пополнение пула программных расходов' },
// expense
{ type: 'p.exp.expns', contract: 'expense', name: 'PROPOSAL', human_name: 'Расход по служебной записке' },
// marketplace
{ type: 'p.mkt.reqst', contract: 'marketplace', name: 'REQUEST', human_name: 'Запрос маркетплейса' },
@@ -24,6 +24,7 @@ export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
{ name: "w.wal.member", human_name: "ЦК — членская часть пайщика", kind: "USER_SHARED" },
{ name: "w.cap.blago", human_name: "ЦПП «Благорост» — единый кошелёк программы у пайщика", kind: "USER_SHARED" },
{ name: "w.cap.preimp", human_name: "Первичный учёт РИД-взносов до перехода на электронный учёт", kind: "USER_SHARED" },
{ name: "w.exp.adv", human_name: "Подотчётные средства пайщика", kind: "USER_SHARED" },
{ name: "w.reg.pend", human_name: "Регистрационный взнос в ожидании решения совета", kind: "USER_SHARED" },
{ name: "w.cap.gen", human_name: "ЦПП «Генератор» — единый кошелёк программы", kind: "COOPERATIVE" },
{ name: "w.reg.entry", human_name: "Вступительные взносы", kind: "COOPERATIVE" },
@@ -35,6 +36,7 @@ export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
{ name: "w.sov.mnused", human_name: "Использованные минимальные паевые взносы", kind: "COOPERATIVE" },
{ name: "w.cap.loan", human_name: "Выданные пайщикам беспроцентные займы", kind: "COOPERATIVE" },
{ name: "w.mkt.payout", human_name: "Выплаты поставщикам", kind: "COOPERATIVE" },
{ name: "w.cap.pgexp", human_name: "Пул программных расходов ЦПП «Благорост»", kind: "COOPERATIVE" },
] as const
export interface ProgramWalletMapping {
@@ -57,5 +59,6 @@ export const LEDGER2_USER_SHARED_PROGRAM_MAPPING: readonly ProgramWalletMapping[
{ wallet_name: "w.cap.blago", required_program_id: 4, program_label: "Благорост" },
{ wallet_name: "w.cap.gen", required_program_id: 3, program_label: "Генератор" },
{ wallet_name: "w.cap.preimp", required_program_id: 0, program_label: null },
{ wallet_name: "w.exp.adv", required_program_id: 0, program_label: null },
{ wallet_name: "w.reg.pend", required_program_id: 0, program_label: null },
] as const