[C28-28..C28-34] feat: шасси системы расходов (MVP-SINGLE) + интеграция Капитала через inter-порт #61
Reference in New Issue
Block a user
Delete Branch "feat/expense-chassis-mvp"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Объединённый PR волны 6 проекта 14 (платформа ЦК v3): шасси системы расходов плюс его первый consumer — программные расходы Капитала (бывший stacked-PR #67, влит сюда целиком; конфликты с dev разрешены).
Содержание (по эпикам)
o.exp.blgadv/blgdir/advrpt/advret/over, кошелёкw.exp.advADVANCE_HOLD)expense(шасси) + интеграция с capitalАрхитектура интеграции (бывший #67)
capital — только инициатор + получатель callback'а. Весь lifecycle (authorize / pay / report / close | decline) обслуживает шасси
expense:Backend capital-extension читает proposals через порт
@coopenomics/inter::InterExpenseChassisPort(write — на capital). Тот же порт переиспользует marketplace, EMP и любой следующий consumer.Ключевые свойства (после ревью и фиксов 2026-06-10)
{_self, active}, приёмник проверяетrequire_auth(<контракт-отправитель>)либо whitelist (contracts_whitelist), т.к.*@eosio.codeне имеетcoopname@active.payexpограничен планом;returnexp/overspendexp— settlement-семантика (статус item остаётся PAID, отчёт закрывает штатнымreportexp);declexpтолько до оплат.program_expense_pool; при нехватке — recoverable-ошибка с подсказкойtopupprogexp.expenseдобавлен в boot-деплой (аккаунт + code permissions).w.exp.adv+w.reg.pend), собраны ledger2/expense/capital/registrator.Тест-план
Открытый вопрос (не блокер)
Проводки
o.exp.blgadvидутДт 08 / Кт 51сразу при выдаче (счёт 71 не используется) — расходится с PRD-Chassis (выдача 71/51, отчёт 08/71). Требует подтверждения схемы с Ангелиной либо отдельного фикса реестра операций.🤖 Generated with Claude Code
Реализация Эпика 2 шасси системы расходов (волна 6 проекта 14). cooptypes/cooperative/registry/: - 2010.ExpenseProposalStatement — СЗ-смета с массивом items (description, amount, recipient_type SELF/MEMBER/ORG, mechanics ADVANCE/DIRECT, recipient_name, requisites), заголовком proposal (description/total_amount/items_count/source_wallet). LiquidJS context с таблицей позиций, переводы ru. - 2011.ExpenseProposalDecision — протокол-1 утверждения/отказа председателем. Поля decision {kind: approve|decline, reason?, protocol_number?, protocol_date?}, переиспользование IExpenseItem/IExpenseProposalHeader из 2010 через cross-import. factory/src/: - Schema/ExpenseItemSchema — JSONSchemaType для ExpenseItem, ExpenseProposalHeader, ExpenseProposalDecisionBody. - Templates/2010+2011 — обёртки по образцу 1010 ExpenseStatement / 1011 ExpenseDecision, но с массивом items в Schema. - Actions/2010+2011 — генераторы pdf через DocFactory с маппингом meta+coop+user+vars +proposal+items[+decision]. factory/test/documents-1000-plus.test.ts: - 3 golden-pdf теста (2010 СЗ, 2011 approve, 2011 decline + reason). Legacy 1010/1011 НЕ удалены — будут выпилены в C28-33 после миграции backend/UI на новые ID (чтобы не порвать PR #55 на марафоне рефакторинга). Решение 2026-06-02: только 2 шаблона document2 в MVP (2010, 2011); остальные слоты (payment_proof, report_files, return_proof, протокол-2) — файлы в MinIO/expense_files без factory-цепочки. Тяжёлая document2-цепочка нужна только для юридически-весомых актов СЗ-смета + протокол совета; первичные документы внешнего происхождения и events приёмки идут отдельным механизмом. Refs: C28-30, PRD f8 раздел 8.16 + override 2026-06-02, образцы 1010.ExpenseStatement + 1011.ExpenseDecision. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>components/contracts/cpp/expense/: - expense.hpp: 1 таблица proposals (scope=coopname; secondary by_hash/by_username/by_status), типы item, callback_handler, enum Mechanics/RecipientType/ProposalStatus/ItemStatus. - expense.cpp: 8 actions (createexp/authexp/declexp/payexp/reportexp/closeexp/returnexp/overspendexp). - CMakeLists.txt: стандартный add_contract(expense expense expense.cpp). components/contracts/cpp/lib/consts.hpp: - _expense = expense_n + добавлен в contracts_whitelist. components/contracts/CMakeLists.txt: - add_contract_build(expense). Принцип v3: - Один процесс p.exp.expns, 5 ledger2 operation_codes (o.exp.{blgadv,blgdir,advrpt,advret,over}). - createexp принимает operation_code+source_wallet+callback в payload — контракт агностичен к источнику. - payexp на DIRECT-механике сразу помечает item REPORTED (BURN blago = один полный шаг). - payexp на ADVANCE — отдельный reportexp пайщиком после чека. - closeexp шлёт inline action на сохранённый callback (contract,action,data) если он непустой. - overspendexp — две последовательные Ledger2::apply (OVERSPEND + ADVANCE_REPORT) в одной транзакции.- ExpenseFilesService с @UseBucket(EXPENSES_BUCKET): upload (base64 → SHA-256 verify → MinIO put → БД-запись), getReadUrl, listByProposal/Item, deleteFile; детерминированный ключ MinIO {coopname}/{proposal}/{item|_proposal}/{kind}/{checksum}.{ext}; 409 на повтор по checksum (per-coopname idempotence). - UploadExpenseFileInputDTO: валидация mime allowlist + size_bytes + SHA-256 regex. - ExpenseFileOutputDTO с короткоживущим read_url (HMAC-signed, TTL бакета). - ExpenseFilesResolver: uploadExpenseFile + expenseFile/expenseFilesByProposal/ByItem. - FileStorageInfrastructureModule.forFeature([ExpenseFilesService]) wire. tsc --noEmit: 0 ошибок.- `SubmitExpenseReportInputDTO` + `ExpenseMutationsResolver.submitExpenseReport` (chairman/member/user) — финализация СЗ-отчёта пайщиком когда все items закрыты; сервис — skeleton `NotImplementedException`, реальный chain-submit (`expense::closeexp`) подключим после Эпика 0 (cooptypes regen). - `findByCoopnamePaginated` / `findByUsernamePaginated` в `ExpenseProposalRepository` + TypeORM-имплементация на `findAndCount` (sort по `_created_at DESC`, limit ≤200). - `listProposalsByCooperativePaginated` / `ByMemberPaginated` в service'е. - `ExpenseProposalResolver` — paginated по канону controller'а (`PaginationInputDTO` + `createPaginationResult('PaginatedExpenseProposals')`). controller-tsc — 0 ошибок. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>- interfaces/expense.ts: 8 action input + IItem/IProposal/ICallbackHandler (hand-written из expense.hpp до ABI regen) - contracts/expense/{actions,tables,index}: тонкие обёртки в стиле capital — authorizations + actionName + типовой re-export - common/names: _expense ({production:'expense',testnet:'expense'}) - interfaces/index + contracts/index: re-export ExpenseContract - ledger2/wallets.generated: +w.exp.adv (prebuild gen-from-cpp подцепил свежий wallet) build EXIT=0, dist exports ExpenseContract; smoke node: contractName=expense, 8 actions, 1 table7 тестов проверяют что каждое из 6 действий expense-контракта уходит в BlockchainService.transact с правильным форматом: account = ExpenseContract.contractName.production name = ExpenseContract.Actions.<X>.actionName authorization = [{actor: coopname, permission: 'active'}] Защита от дрейфа cooptypes actionName → adapter имя. Также verify HttpApiError 502 при отсутствии WIF в vault'е (initialize/transact не зовутся). jest 7/7 passed.- api/queries/getExpenseProposalsByCooperative — thin wrapper над SDK, возвращает PaginatedExpenseProposals (типизировано через IInput/IOutput). - api/index.ts — barrel-экспорт. - model/expenseProposalStatus — label/variant maps для 7 значений Zeus.ExpenseProposalStatus (CREATED/AUTHORIZED/PARTIALLY_PAID/REPORT_SUBMITTED/ CLOSED/DECLINED/UNDEFINED) + helpers с дефолтом neutral. - ExpensesRegistryPage.vue: pug + .table-wrap canon, TableSkeleton(6 cols), EmptyState (receipt_long 48px), BaseBadge с variant из map, BaseButton ghost "Загрузить ещё", монохешчик в .t-mono-sm. coopname из route.params, sortBy=created_at DESC, page-by-page до hasMore. Open: tooltip с FQH хеша, sort по колонкам, filter-bar по статусу — будут вместе со страницами /expenses/admin/{approve,authorize} и /my/advances (своя сигнатура запроса by-member).BaseDialog (size=lg) с BaseInput / q-select (recipient_type, mechanics) + динамическим массивом item-card'ов и валидацией canSubmit. При submit: - canonical JSON.stringify полей payload → SHA-256 через crypto.subtle (детерминированный proposal_hash); - Mutations.Expense.CreateExpenseProposal({ coopname, proposal_hash }) — бэкенд резервирует placeholder, далее signature-pipeline document2. Info-баннер информирует пайщика что подача откроется после Эпика 2. 7/7 страниц #104 расшито. Mutation-цепочка одобрить/авторизовать/оплатить — следующий шаг после расшивки document2 signature-pipeline.8f64a4652bto1bca8cf34dШасси расходов получает inter-порт для consumer-extension'ов (capital, marketplace, EMP). Порт — read-only: list-view и карточка proposal'а по hash. Mutations approve/authorize/pay/report/close/decline остаются прямыми GraphQL-endpoint'ами шасси, consumer их НЕ оборачивает. inter: - tokens.ts: INTER_EXPENSE_CHASSIS = Symbol.for(...) - expense-chassis.port.ts: InterExpenseChassisPort + DTO (Proposal/Item/ Callback/Status). Enum-статусов = зеркало доменного ExpenseProposalStatus. - index.ts: реэкспорт. extensions/expenses (шасси-backend): - infrastructure/inter/expenses-inter-expense-chassis.adapter.ts — тонкий read через ExpenseProposalTypeormEntity. Проекция доменных полей в нейтральный inter-DTO, без зависимости inter → controller. - Регистрация adapter'а в expenses-extension.module + экспорт. inter-communication-bridge.module: - Импорт ExpensesExtensionModule + provider INTER_EXPENSE_CHASSIS = useExisting ExpensesInterExpenseChassisAdapter. PR1 (шасси-собственное), будет использовано в PR2 (Капитал на шасси).capital теперь только инициатор+получатель результата для программного расхода — весь flow обслуживает шасси expense. Раньше blagorost-ветка дублировала approve/auth/pay/decline на стороне capital. Изменения capital: - удалены apprvpgexp/authpgexp/pgexppay/declpgexp (дубль шасси). - createpgexp переписан: вместо самообслуживания резервирует пул и шлёт inline expense::createexp с callback={capital, onpgexpdone}. - сигнатура createpgexp принимает operation_code+items[] (передаётся в шасси). - добавлен onpgexpdone(coopname, hash, status, total_actual, data) — callback от шасси на CLOSED/DECLINED, обновляет program_expense_pool и удаляет progexpense. Изменения expense (шасси): - callback payload расширен полем status (ExpenseDomain::ProposalStatus) — инициатор отличает CLOSED от DECLINED. - declexp теперь шлёт callback тоже (раньше только closeexp). Изменения names.hpp: - удалён CAPITAL_RESOLVE_PROGRAM_EXPENSE / AUTHORIZE_PROGRAM_EXPENSE / DECLINE_PROGRAM_EXPENSE / CONFIRM_PROGRAM_EXPENSE_PAYMENT. - добавлен ON_PROGRAM_EXPENSE_DONE. - добавлен External::CREATE_EXPENSE_PROPOSAL для inline в шасси. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Тонкое расширение Capital для программных расходов: чисто consumer над шасси `expense` (только Капитал-специфика, остальное обслуживает шасси). Чейн: - CapitalBlockchainPort.createProgramExpense → capital::createpgexp. - CapitalBlockchainPort.topupProgramExpense → capital::topupprogexp. Backend: - ProgramExpensesManagementService — write через capital, read через INTER_EXPENSE_CHASSIS (list по owner='capital'+action='onpgexpdone' + карточка по hash). Никакого собственного TypeORM-mirror. - ProgramExpensesResolver — chairman/member guards, mutation+query. - DTO: CreateProgramExpenseInputDTO (items+statement registry 2010), TopupProgramExpenseInputDTO, ProgramExpenseOutputDTO. - Регистрация в capital-extension.module. actual_amount/status в createProgramExpense инициализируются 0/0 серверно (их C++ дефолтит, но cooptypes требует все поля IItem). PR2 (Капитал на шасси).Тонкая consumer-страница Капитала над шасси расходов. Доступ chairman+member. entities/ProgramExpense — store + api (Queries.Capital.GetProgramExpense{s}). features/ProgramExpense/CreateProgramExpense — useCreateProgramExpense: generateUniqueHash → reuse `generateExpenseProposalStatementDocument` шасси → подпись через DigitalDocument (registry 2010, Эпик 4 pipeline) → Mutations.Capital.CreateProgramExpense. features/ProgramExpense/TopupPool — useTopupProgramExpensePool: Mutations.Capital.TopupProgramExpense. pages/ProgramExpensesPage — list по канону mono-platform (BaseCard/Chip/ EmptyState/--p-* токены), два диалога в шапке. install.ts — route `/:coopname/capital/program-expenses` (name `capital-program-expenses`, иконка receipt, agreementsBase, roles= chairman+member).[C28-28..C28-34] feat: шасси системы расходов (MVP-SINGLE)to [C28-28..C28-34] feat: шасси системы расходов (MVP-SINGLE) + интеграция Капитала через inter-портOn-chain callback.data — vector<char>; парсер десериализует в Uint8Array, который в jsonb-зеркале становится {} (пустой) или {"0":..}. GraphQL-поле data — String, сериализация ответа capitalProgramExpenses падала «String cannot represent value: {}» и UI показывал пустой список при живой строке в зеркале. Нормализуем в hex ещё в дельта-маппере (на записи), существующая строка поправлена UPDATE'ом. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>— симптом: колонка «Пайщик» (ФИО) в реестре расходов рвалась по буквам в столбик. — причина: глобальный канон components.css форсит `.table{min-width:0!important}`; мой `.table{min-width:…}` без !important проигрывал → колонки схлопывались уже контента и слова ломались посимвольно. Плюс `overflow-wrap: anywhere` на ячейках добивал. — фикс (эталон — ListOfPaymentsWidget): `.table{table-layout:fixed!important; min-width:…!important}` + `overflow-wrap: break-word` (не anywhere) на текстовых ячейках. При нехватке ширины таблица скроллится в .table-scroll, а не ломает слова. Применено и к таблице позиций в детали (тот же латентный баг). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Симптом: пайщик создаёт расход с получателем-организацией (не он сам) — и этот исходящий платёж появляется у него в личном кабинете на странице платежей, хотя он лишь инициатор, а деньги идут не к нему. Корень: expense-payments.listener создавал gateway-платёж по позиции расхода с username = entity.username (создатель СЗ) для org-получателя. Личный реестр платежей фильтрует по username (loadPayments({username: me})), поэтому платёж кооператива организации оказывался на столе инициатора. Фикс: владелец org-платежа = сам КООПЕРАТИВ (entity.coopname), а не инициатор. В личных реестрах пайщиков (фильтр по username) он больше не виден; кассир видит его в общем реестре платежей кооператива (loadPayments без username). Аванс под отчёт по-прежнему принадлежит пайщику-получателю (item.recipient) — он у него и отображается. На on-chain payexp правка не влияет: проводка берёт coopname/proposal_hash/item_hash, username не участвует (см. gateway.interactor.processOutgoingPayment). Касается только НОВЫХ платежей; уже созданные org-платежи с username=создатель не переписываются. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>В детали расхода (стол совета → реестр расходов → расход) добавлена секция «Платежи по расходу»: выдача аванса/оплата организации и, если был перерасчёт по чекам, расчётная платёжка (возврат недорасхода / доплата перерасхода). Реквизиты «куда уходил платёж», назначение и причина отклонения — внутри PaymentDetails. Платежи собираются по ДЕТЕРМИНИРОВАННЫМ хэшам, а не по username: у позиции- организации платёж принадлежит кооперативу, у аванса — пайщику-получателю, и у каждой позиции свой получатель — единого владельца нет. Хэш выдачи = item_hash, расчётные = sha256('expense-settlement:coop:item:kind') (новый settlementPaymentHash в shared/lib/expenses, точная копия серверного generateHashFromString). Точечный hash-фильтр gateway не требует листать общий реестр и нового бэкенд-поля. Клик «Открыть в реестре платежей» → реестр кассира, отфильтрованный по владельцу (:username? в PaymentsPage), с фокусом на платёж (виджет раскрывает его по ?focus=hash). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>ExpenseDetailPage переведён со сбора платежей по реконструированным хэшам на один запрос getPayments({ coopname, proposal_hash }) — фильтр из C28-64. Убрана скопированная серверная формула settlementPaymentHash/sha256Hex из shared/lib/expenses: фронт больше не дублирует деривацию хэшей расчётных платёжек. Запрос type-agnostic — ловит все связанные платежи любого типа, устойчив к добавлению новых видов. Сортировка: выдача/оплата раньше расчётных. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>CI впервые дошёл до vue-tsc по ветке и вскрыл накопленный долг типов (локально vue-tsc не гоняем). Чиним точечно, без смены логики: - uploaded_at — Zeus-скаляр даты типизирован как {}: new Date(String(...)) и date:String(...) в ProgramExpensePage, ExpenseDetailPage и трёх Payment-panel (Attach/Settlement/Report). - ExpenseDetailPage: loadPayments options требует sortOrder → 'DESC'; :key платежа допускает null → ключ pay.hash ?? idx. - PaymentsPage: routeUsername null → undefined под prop :username. - expenses/model: снят реэкспорт несуществующего IAuthorizeProposalDraft. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>