Контроллерный tsc --noEmit (CI) падал: spec тестировал adapter.authExp /
adapter.declineExp, которых нет в ExpensesBlockchainAdapter по дизайну.
authexp/declexp исполняет контракт soviet как callbacks решения совета —
backend-адаптер несёт только 6 прямых actions (createexp/payexp/reportexp/
returnexp/overspendexp/closeexp). Зафиксировано в port, mutations-service и
комментарии адаптера. Прав код — приводим тест к дизайну, удаляя два кейса.
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>
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>
Добавлен фильтр PaymentFiltersInput.proposal_hash — возвращает все платежи,
связанные с расходом (служебной запиской). Связь платёж→расход уже зашита
бэкендом в json-поле blockchain_data.proposal_hash (и у платежа выдачи аванса/
оплаты организации, и у расчётных платёжек возврата/доплаты); фильтр извлекает
его через json-оператор ->> в typeorm-репозитории. Так связанные платежи
достаются одним запросом по расходу, без реконструкции хэшей на фронте.
regen: schema → generate-client → sdk build (zeus-клиент controller+sdk).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В историю состояний расхода добавлены события платежей: выдача аванса/оплата
организации, возврат недорасхода, доплата перерасхода, отклонения. Берутся из тех
же linkedPayments (C28-61), без отдельного запроса. Тип/иконка/текст по статусу
(исполнен / оплачен кассой / создан / отклонён), сумма и причина отказа — в
описании; сортируются в общую ленту по дате updated_at.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Из детали расхода нельзя было вернуться в реестр расходов. Добавлен canon
back-link под шапкой (как в MeetDetails/DocumentDetails): router.push на
expenses-registry. Виден всегда, даже пока расход грузится.
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>
Кассир пишет причину в диалоге «Отклонить», бэкенд сохраняет её в payment.message
и ставит статус CANCELLED — но PaymentDetails показывал message только при FAILED,
поэтому причина нигде не отображалась. Добавлен canon-баннер (.banner--neg) с
причиной для CANCELLED: видно сразу при раскрытии платежа. Компонент общий —
работает и на столе совета, и на столе пайщика.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В блоке «Основание расчёта» (C28-58) добавлена кнопка «Открыть платёж выдачи аванса»:
кассир жмёт → реестр раскрывает исходный платёж-аванс (его hash == item_hash расчётной
платёжки) и прокручивает к нему. Видно, сколько выдавалось и что в чеке, без поиска
строки того же пайщика вручную. id навешены на tr/pay-card для scrollIntoView; если
платёж аванса не на текущей странице — подсказка.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Кассир (и пайщик) при раскрытии расчётной платёжки (возврат недорасхода / доплата
перерасхода) видит основание прямо в строке — без поиска исходного аванса среди
сотен строк реестра:
- ссылка на СЗ (№), что оплачивали, выдано авансом, заявлено по чекам, сумма расчёта
- список подтверждающих документов (чеки REPORT_FILE) со ссылками на открытие
Новый feature ExpenseSettlementBasis (self-contained: грузит позицию СЗ + файлы по
proposal_hash/item_hash из blockchain_data платёжки). Подключён в ListOfPaymentsWidget
для EXPENSE_RETURN/EXPENSE_OVERSPEND на обоих столах (desktop+mobile).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Корень: позиция on-chain остаётся PAID до подтверждения расчётной платёжки кассой
(reportexp отложен), поэтому отчёт можно было подать многократно на разные суммы.
Идемпотентность createSettlementPayment — лишь по виду (возврат/доплата), из-за чего
отчёт 400→200 плодил две платёжки, а 500 молча возвращал старую.
Backend (авторитетный фикс):
- reportExpenseItem: guard по report_state платежа аванса — SETTLEMENT_PENDING/CLOSED
→ BadRequestException «Отчёт по этой позиции уже подан»
- markAdvanceReportState сохраняет reported_amount (заявленный факт) рядом со статусом
- тест на guard + обновлён ассерт SETTLEMENT_PENDING
Frontend:
- ReportExpenseAdvancePanel: новые props report-state/reported-amount; форма
(AmountInput+кнопка) доступна только пока отчёт не подан (canReport); после подачи —
«Отчёт подан на N ₽, ждём расчёт», остаётся лишь загрузка доп.документов
- ListOfPaymentsWidget: прокидывает report_state/reported_amount в панели на обоих столах
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ReportExpenseAdvancePanel: AmountInput в .report-advance__amount (max-width 240px),
чтобы значение не висело во всю ширину панели с разрывом «лейбл … сумма»
- memo возврата: «выданных под аванс под отчёт» → «выданных авансом под отчёт»
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Направление платежа (ListOfPaymentsWidget):
- колонка «Направление» снова показывается на ОБОИХ столах;
- on-chain direction всегда относительно кооператива (INCOMING = в кооператив).
Стол совета (!hideActions) так и видит; на личном столе пайщика (hideActions)
перспектива обратная — direction инвертируется (исходящий из кооператива =
поступление пайщику). Хелперы displayDirection/directionLabel/directionHint,
tooltip с пояснением («В кооператив»/«Из кооператива» vs «Поступление вам»/
«Списание с вас»). colspan/skeleton/min-width поправлены под доп. колонку.
Уведомления о платеже — без привязки к типу:
- workflow payment-refunded (любой исходящий PAID пайщику) больше не «Возврат
взноса выполнен», а универсальное «Платёж выполнен» (тем же каналом идут
аванс под отчёт, доплата по перерасходу и пр. — тип платежа неизвестен).
payment-paid уже был универсальным («Платёж принят» для входящего).
- комментарий в payment-notification.service приведён в соответствие.
ESLint ✓, notifications build ✓ (id platyozh-vypolnen резолвится в каталоге).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Q1 (расхождение 900 vs 50): на странице программных расходов «Доступно»
теперь = остаток кошелька w.cap.pgexp (source_wallet, с которого payexp
реально списывает оплату), а не счётчик state.program_expense_pool. Счётчик
двигался на резерве, кошелёк — на оплате, поэтому кассир видел одну сумму, а
оплата обламывалась на другой. Карточка «Зарезервировано» (счётчик) убрана,
чтобы не смешивать два контура учёта на одной карточке кошелька.
Q2 (нет поля для доп. документов после «Отчёт принят»): FileUploader в
ReportExpenseAdvancePanel вынесен из ветки isAwaitingReport — теперь доступен
и в состоянии REPORTED («Приложите дополнительный документ»), как и обещает
текст «дополнительные документы дополнят его автоматически».
ESLint ✓. Визуальная проверка — по скриншотам.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Диалог «Пополнение пула программных расходов»:
- убрана тёмная плашка под кнопками (footer-bar на --p-canvas) — кнопки на
поверхности диалога через штатный BaseDialog __foot;
- снят двойной паддинг (.form поверх body) и uppercase-eyebrow («прыгающие шрифты»);
- BaseInput → AmountInput с суффиксом валюты в узкой обёртке (не во всю ширину).
Реестр платежей (стол кассира), последовательная подача:
- AttachExpenseProofPanel и ReportExpenseAdvancePanel получили опциональный
проп step (номер+заголовок) — Этап 1 «Подтвердите оплату», Этап 2 «Отчёт
пайщика»; обёрнуты в .expense-flow с хайрлайн-разделителем. Пустых этапов у
DIRECT нет (панель отчёта сама скрывается);
- в отчёте пайщика порядок изменён на «сумма → чек» (сначала AmountInput, видно
недо/перерасход, затем приложить чек, потом кнопка);
- дружелюбные подписи, больше воздуха (gap --p-3/--p-4), снята плотность.
ESLint ✓. Визуальная проверка — по скриншотам пользователя.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Реквизиты получателя, имя и назначение платежа больше не уходят в blockchain
meta документа. Перестроено на паттерн doc_data из marketplace2 (epic5):
приватный payload сохраняется off-chain в DocDataService фабрики, в meta едет
только doc_data_hash; при генерации/регенерации фабрика подмешивает приватную
часть в позиции по number — полнотекстовый документ не страдает.
2010 (СЗ-смета):
- cooptypes: IExpenseItem → публичный (number/description/amount/recipient_type/
mechanics); новые IExpensePrivateItem/PrivateData/IExpenseRenderItem; Action
extends IDocDataRef + doc_data_hash; Model.items = render-позиции.
- factory: Action подгружает PrivateData по doc_data_hash и склеивает по number;
meta строится из публичной data; ExpenseRenderItemSchema для модели рендера.
- controller: DTO расщеплён — вход генерации «богатый» (приватные поля → сервер
кладёт в doc_data), подписываемая meta = публичные позиции + doc_data_hash.
Сервис: saveDocData → Action с публичными items. Десктоп не меняется (подпись
идёт по meta, возвращённой сервером).
2011 (Решение совета): латентный канал закрыт структурно — item-DTO лишён
requisites/payment_purpose/recipient_name; шаблон протокола их и не рендерил.
Публичная ExpenseItemSchema (5 полей) для модели 2011.
cooptypes build ✓, factory build ✓, ESLint ✓, unit 11/11 ✓.
Codegen (schema/client/sdk) — отдельным шагом.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Выборочный перенос переиспользуемого паттерна doc_data из marketplace2
(исходный коммит 10158b5b33, 598-8) — БЕЗ marketplace-документов 1102/1103,
которых нет в dev (их cooptypes-реестры и controller-сервисы тянули конфликты
modify/delete и сломали бы сборку). 1102/1103 придут штатно при мерже marketplace2.
Что такое doc_data: приватные поля документа (реквизиты/ПДн) сохраняются в Mongo
(коллекция doc_private_data) через DocDataService.save(payload, registry_id) →
{hash}; on-chain в meta публикуется только doc_data_hash (sha256). При генерации
фабрика подгружает payload по хэшу и отдаёт шаблону под зарезервированной
переменной {{ doc_data.* }}. Так реквизиты в блокчейн не попадают, но документ
регенерируем и верифицируем по слепку.
Перенесено (идентично marketplace2 — будущий мерж без конфликтов):
- factory: Services/DocData + обвязка Factory/index.ts (saveDocData/getDocData) +
реэкспорт в Services/Databazor + Generator.saveDocData/getDocData в src/index.ts;
- controller: GeneratorPort/GeneratorInfrastructureService/DocumentDomainService —
проброс saveDocData/getDocData;
- cooptypes: IDocDataRef { doc_data_hash } в модели документа.
cooptypes build + factory typecheck/build зелёные. Контур: следующим шагом
перенастроить СЗ-2010 на doc_data для реквизитов (отдельной задачей).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Разводит три независимых акта вокруг аванса под отчёт, которые UI прежде смешивал:
- Платёж (кассир + платёжка PAYMENT_PROOF) — без изменений.
- Отчёт по авансу (факт + чек REPORT_FILE → reportexp) — пайщик ИЛИ кассир «за пайщика».
- Закрывающие документы организации (новый ExpenseFileKind.CLOSING_DOC) — только DIRECT.
Бэкенд:
- ExpenseReportState (AWAITING/SETTLEMENT_PENDING/CLOSED/NOT_REQUIRED) зеркалится в
payment.blockchain_data.report_state: в reportExpenseItem (CLOSED/SETTLEMENT_PENDING)
и в inter-адаптере reportItem в момент on-chain reportexp (CLOSED). AWAITING — дефолт.
- CLOSING_DOC + valuesMap-описания видов файлов; SDK regen.
Desktop:
- Личный стол: убран кассирский значок «Платёжка приложена» (чужая бухгалтерия),
добавлен бейдж статуса отчёта рядом со статусом платежа.
- Стол совета: кассир может отчитаться за пайщика (ReportExpenseAdvancePanel onBehalf,
самоскрытие для DIRECT) + отдельная секция закрывающих документов для оплат организациям.
Авторизация reportExpenseItem уже допускала совет по любой строке — путь «за пайщика»
работает без правок guard'ов. Unit 11/11, ESLint чист, SDK typecheck зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Правки по тест-прогону недо-/перерасхода:
- Суммы в реестре платежей (таблица+карточки) и инпут факта — formatAsset2Digits
(2 знака), а не сырой on-chain precision=4.
- «Направление» (входящий/исходящий — относительно кооператива) скрыто на личном
столе пайщика (hideActions): на его столе семантика обратная и путала. На столе
совета остаётся (там перспектива кооператива корректна).
- PaymentDetails: убран дамп «Данные блокчейна» (JSON); реквизиты показываются при
любом направлении (входящий возврат — реквизиты кооператива, исходящая
доплата — реквизиты пайщика); «Сумма к переводу» — 2 знака.
- Settlement-платёжка несёт payment_details с реквизитами: возврат → банк
кооператива (куда платит пайщик), доплата → снимок реквизитов пайщика (куда
платит кооператив). Назначение: «Возврат неиспользованных средств, выданных под
аванс под отчёт» / «Доплата по перерасходу аванса под отчёт».
- После отчёта реестр перезагружается и сразу раскрывает заведённую платёжку
расчёта (по settlement_payment_hash) — пайщик видит реквизиты, не догадываясь
нажать «развернуть».
Юнит-тесты расходов зелёные (11/11). GraphQL-схема не менялась — SDK regen не нужен.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Замыкаю контур факт→возврат/доплата для аванса под отчёт. Контракт уже имел
returnexp/overspendexp с двусторонними ledger2-проводками, но фронт/бэкенд их не
звали: отчёт слепо закрывал позицию на actual=аванс без сверки факта (дыра
недорасхода — пайщик мог недоотчитаться и оставить разницу).
Бэкенд-слой:
- 2 типа платежей: EXPENSE_RETURN (входящий возврат недорасхода) и
EXPENSE_OVERSPEND (исходящая доплата перерасхода) + labels + direction-списки.
- inter-порт шасси: returnItem/overspendItem/reportItem (зеркало payItem),
адаптер реализует через returnExp/overspendExp/reportExp.
- gateway-ветки: подтверждение кассиром EXPENSE_RETURN (income) → returnexp +
reportexp; EXPENSE_OVERSPEND (outcome) → overspendexp + reportexp. proposal_hash
и item_hash берутся из blockchain_data (hash платёжки уникальный, не item_hash).
- reportExpenseItem(actual_amount?): факт==аванс → reportexp сразу (CLOSED);
недорасход → входящая платёжка на |разницу| (RETURN_PENDING); перерасход →
исходящая (OVERSPEND_PENDING); reportexp отложен до подтверждения кассиром
(контракт принимает settlement только на PAID-позиции). Дельта в минорных
единицах по precision. Идемпотентность платёжки по детерминированному хэшу.
- Новый дискриминированный результат ExpenseReportResultDTO + enum
ExpenseReportOutcome. Юнит-тесты на все три исхода + идемпотентность.
Контракт expense не изменён. Авторитет на сумму факта — пайщик (по чекам).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Откат удаления из C28-44: страницы воркспейса expenses (MyAdvances/Cashier/AdminApprove) — намеренный рабочий КАРКАС будущего стола кассира, а не мёртвый код. Решено их не удалять, а сохранить: соберём стол кассира из них позже, тогда «Мои авансы» выведем туда. Сейчас воркспейс не привязан к столу → в меню их нет и ничего на них не ведёт (то, что и нужно).
— восстановлены MyAdvancesPage.vue + роут expenses-my-advances + экспорт из barrel.
— в install.ts добавлен комментарий-маркер: это каркас, не удалять, ничего на него не ведёт намеренно; рабочие ссылки — на /:coopname/user/payments.
— ссылку напоминателя об авансах НЕ откатываем: остаётся на личные «Платежи» (/user/payments), как просил пользователь — всё ведёт через страницу платежей/реестр платежей, не на каркасные страницы.
— README приведён в соответствие (каркас, а не «удалена»).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MyAdvancesPage была смонтирована только в воркспейсе expenses, который ни к одному столу не привязан (меню столов приходит с бэкенда; setRoutes вешает пункт лишь если воркспейс есть в столе). Страница открывалась только по прямому URL — пользователь её не видел ни разу. Авансы пайщика-получателя и так видны на его личной странице «Платежи» (/:coopname/user/payments, ListOfPaymentsWidget + ReportExpenseAdvancePanel) — отдельная страница дублировала и была мёртвой.
— desktop: удалён MyAdvancesPage.vue, его роут expenses-my-advances и экспорт из barrel; импорт из install.ts.
— controller: напоминатель об авансах при нескольких авансах вёл на /expenses/my/advances → перецелен на /:coopname/user/payments (личные «Платежи»); один аванс по-прежнему ведёт на сам расход.
— notifications: комментарий схемы payload обновлён.
— README/E2E: ссылки и таблица страниц обновлены.
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>
Карточки списка показывали сырой total_planned «1000.0000 RUB». ExpenseProposalListRow.total_planned по контракту типа — уже отформатированная строка; страница подавала сырьё. Оборачиваем в formatAsset2Digits в listRows ProgramExpensesPage (как суммы в сводке/детали).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
— ExpenseDetailPage (стол Совета): добавлена секция «История состояний» (ActivityTimeline, group-by-date) — как на странице расхода программы Благорост. Лента собирается из фактов в данных: создание СЗ, подпись заявления, утверждение/отклонение советом, приложенные документы, подача отчёта, закрытие расхода; актор — ФИО создателя. Отдельной журнальной таблицы у шасси нет.
— capital/install.ts: страница расходов программы переименована «Расходы программы» → «Расходы» (title в meta). Доступ только совету — roles ['chairman','member'] уже стояли. Попутно заменена запрещённая каноном FontAwesome-иконка fa-solid fa-receipt → Material receipt_long на обоих роутах (список + деталь).
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
По скриншот-ревью пользователя.
Реестр расходов (ExpensesRegistryPage):
— убрана шапка PageHead («Шасси расходов» / «Реестр расходов» / подзаголовок) — сразу таблица.
— колонка «Пайщик»: ФИО первой строкой (из сертификата подписанта СЗ — её подписывает создатель, доп. запрос не нужен), ниже имя аккаунта мелким моно с копированием по клику (copyToClipboard).
— добавлена колонка-шеврон справа (chevron_right) — видно, что строка открывается; подсвечивается на hover.
Деталь расхода (ExpenseDetailPage):
— убрана шапка PageHead; статус — чипом вверху справа (канон detail-страниц). Переверстана одноколоночно секциями с заголовками-эйбрау (Сводка / Документы / Строки расходов / Чеки и подтверждения) — как на странице расхода программы.
— Сводка: «Пайщик» = ФИО (из сертификата подписанта), отдельная строка «Аккаунт» (моно, копируемая); «Хеш» (рус. ярлык вместо «Hash») переносится и копируется (DataRow mono, vertical).
— «Строки расходов»: счётчик с правильным русским множественным («1 строка / 2 строки / 5 строк») вместо «1 строк».
— «Чеки и подтверждения»: имя файла — гиперссылка, клик открывает документ в новой вкладке (read_url короткоживущий — запрашиваем свежий по id через новый getExpenseFileReadUrl, как в capital); кто приложил — ФИО (из карты подписей СЗ+протокола), а не имя аккаунта; убраны storage_key/размер.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
— зачем: на столе совета «Расходы» показывал карточки кошельков-пулов и проваливался с редиректом на страницу программы Благорост. Нужен реестр-наблюдение: единая таблица ВСЕХ расходов кооператива по всем пулам без фильтра, с колонкой кошелька-источника; клик по строке → деталь расхода (та же информация, что на странице расхода). Фильтр по конкретному пулу остаётся на странице расходов программы.
— soviet/install.ts: монтирует ExpensesRegistryPage (title «Реестр расходов», маршрут soviet-expenses-registry) вместо ExpenseWalletsPage; редиректа на программу больше нет. Воркспейс expenses активен → generic-роут expenses-detail резолвится глобально.
— ExpensesRegistryPage: + колонки «Назначение» (описание первой позиции) и «Кошелёк (пул)» (резолв кода source_wallet → человеческое имя через listExpenseWallets(), fallback — сам код); суммы план/факт через formatAsset2Digits (2 знака); убрана колонка «Хеш» (шум, есть в детали).
— ExpenseDetailPage (generic, куда ведёт реестр): поднят до канона страницы расхода — документы СЗ/протокол рендерятся каноном ExpenseProposalDocuments (раскрывающийся BaseDocument с подписями) вместо голых хешей; суммы сводки и позиций через formatAsset2Digits; источник средств показан как имя пула; убрано техническое поле «Действие» (operation_code).
— ExpenseWalletsPage остаётся в barrel'е (не смонтирован); listExpenseWallets/registerExpenseWallet теперь служат картой код-кошелька→имя для реестра. README обновлён.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
На карточке расхода в статусе «Создан» председатель видит кнопку «Рассмотреть» —
открывается диалог авторизации СЗ (утвердить с протоколом 2011 / отклонить с
причиной). Фиксы диалога: отклонение теперь идёт через declineExpenseReport →
declexp (раньше ошибочно через authexp, который безусловно ставил AUTHORIZED);
убраны ссылки на удалённый operation_code (назначение = перечень позиций).
Инициатор в карточке — ФИО (creator_name). Тексты карточек пулов укорочены.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
В карточке показывался username (ant) — канон требует ФИО/название организации.
Бэкенд резолвит имя через ACCOUNT_DATA_PORT.getDisplayName (батчем по уникальным
creators, при ошибке остаётся username). Codegen: schema + zeus + sdk selector.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Бегущая строка убрана полностью (раздражала и читалась плохо): заголовок и
подпись переносятся максимум на две строки с многоточием, полный текст — в
title-тултипе. Выпилены measure/ResizeObserver/tabindex и keyframes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
extra_reboot (чистая цепь + совет + dev-shortcut онбординга capital — без него
после reboot снова 5 решений совета руками) → ожидание готовности controller →
seed-capital --up-to=04-contributor (программы, проекты, регистрация ant).
Глубина сида настраивается: SEED_UP_TO=08-investments pnpm run reboot:blago.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Канон-класс .wallet__locked-line — white-space:nowrap + inline-flex, рассчитан
на короткий лейбл; длинный «Зарезервировано под активные расходы» не переносился
и вылезал за карточку. Лейбл → «Зарезервировано» (смысл ясен из контекста
страницы), общий компонент не трогаем.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
global_sequence — глобально-уникальный монотонный id действия в истории цепи.
При re-scan/replay redis-стрима то же действие доставляется повторно → INSERT
падал на unique-индексе (23505) → consumer не ACK'ал сообщение и зацикливал
recoverOwnPending/reclaimStalePending (дикий спам duplicate-key в логах). Теперь
дубль трактуется как «уже сохранено»: возвращаем существующую запись, не бросаем.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Парсер не был подписан на контракт `expense` (нет в subscribedContracts) →
дельты `expense::proposals` не эмитились → postgres-зеркало `expense_proposals`
оставалось пустым → список «Программных расходов пока нет», хотя на цепи СЗ и
резерв создавались. Добавил `expense` в subscribedContracts парсера.
Плюс: `coopname` добавлен явным полем в таблицу `proposals` контракта (struct +
EOSLIB_SERIALIZE + проставление в createexp) и в cooptypes IProposal. Таблица
scoped по coopname, но зеркало требует coopname в строке — дублируем полем по
канону (как capital/soviet), а не выводим из scope дельты.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- expense::createexp принимает контракты-инициаторы по contracts_whitelist
(capital@eosio.code не имеет coopname@active — inline падал по authority);
capital::createpgexp шлёт inline от _capital@active.
- capital::onpgexpdone: require_auth(_expense) вместо _capital — callback
шасси шёл с authority expense@active и всегда падал.
- payexp: cap actual<=plan + DIRECT-item сразу REPORTED с пересчётом статуса
proposal — DIRECT-only СЗ навсегда зависал в PARTIALLY_PAID.
- returnexp/overspendexp: settlement-семантика по PRD — статус item остаётся
PAID, отчёт закрывает item штатным reportexp (раньше RETURNED/OVERSPENT
были терминальными тупиками: перерасход блокировал closeexp навсегда).
- declexp: только CREATED/AUTHORIZED — decline после оплат разъезжался
с учётом пула в capital (возвращал весь резерв при ушедших деньгах).
- onpgexpdone CLOSED: перерасход сверх резерва списывается из
program_expense_pool (spend_program_expense_pool).
- names.hpp: redefinition CREATE_PROGRAM_EXPENSE + несуществующий
Names::Capital::Callbacks — контракты не компилировались вовсе.
- verify_document_or_fail(statement, {creator}) в createpgexp; снос
мёртвых set_program_approved/set_program_authorized.
Оба контракта собраны: expense.wasm + capital.wasm.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
R4: убрал хардкод "RUB" и "1000.0000 RUB" из feature ProgramExpense.
- submitTopup принимает raw число "10000" → форматирует через formatToAsset
с info.symbols.root_govern_symbol/precision.
- submitProgramExpense форматирует total_amount + planned_amount каждого item
через тот же helper (UX-долг "amount без IMask" закрыт по сумме).
- Placeholder'ы Dialog'ов вычисляются computed'ом из system.info.symbols.
R1 (частично): тип `GenerateStatementInput = Mutations.Expense.GenerateExpense
ProposalStatementDocument.IInput['data']` + деструктуризация результата через
computed `[Mutations.X.Y.name]` — убраны 2 из 4 `as any` каста (input + index).
Оставшиеся (signed-as-any и create-input-as-any) — отдельный refactor PR через
signed-doc adapter в shared/lib/document.
Source-wallet вынесен в локальную map SOURCE_WALLET_BY_OPERATION с throw на
неизвестный код — backend-derivation (R3) отдельной задачей.
Вынес общий PaginationInput → PaginationResult helper из capital-service в
~/application/common/dto/pagination.dto. Consumer'ы inter-port'ов (без своего
Repository.findAndCount) получают канон-сборку без дубля логики totalPages/
currentPage.
Также `paginationInputToOffset(options)` — стандартная конверсия для адаптеров,
работающих offset-form (большинство inter-port read API).
Capital ProgramExpensesManagementService переведён на оба helper'а — ужал
listProgramExpenses до 10 строк.
Why: использовал несуществующие токены/классы — UI бы рендерился со сломанными цветами/иконками. Привёл к канону:
- BaseChip: `:variant` (pos/accent/warn/neg/info/neutral), не `:label/:tone` (нет такого API).
- BaseButton: иконка через `<template #icon-left>`, не `icon='X'` (нет такого пропа).
- CSS-токены: `--p-ink`/`--p-ink-2` (не `--p-text`/`--p-muted`), `--p-fs-meta` (не `--p-fs-body-xs`).
- Утилитарные классы: `t-h2`/`t-sm`/`t-eyebrow`/`t-muted` из components.css, не `t-section`/`t-body-sm`.
Why: send_callback_if_any слал (proposal_hash, status, amount, data), а capital::onpgexpdone ждёт (coopname, expense_hash, status, total_actual, data). Скоуп таблицы progexpenses = coopname, без него инициатор не найдёт запись. Применимо ко всем будущим consumer'ам шасси (marketplace, EMP).
Why: ExpenseMechanics и ExpenseRecipientType — string enums; Number(str) даёт NaN, wharfkit не сериализует. Зеркалит маппинг из expenses-mutations.service.ts:57-63.
Тонкое расширение 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 (Капитал на шасси).
Адаптация blagorost-cooptypes под архитектуру «capital = инициатор + callback,
шасси expense обслуживает весь flow». Из blagorost-E2 берётся минимальный набор
интерфейсов под программный расход, без debt/role/duplicate-action типов.
Изменения:
- ICreatepgexp: amount/description упрощены, добавлены operation_code+items[]
(передаются inline в шасси expense::createexp).
- ITopupprogexp — без изменений (пополнение пула).
- IOnpgexpdone — НОВЫЙ callback от шасси, payload (hash, status, total_actual, data).
- IApprvpgexp/IAuthpgexp/IPgexppay/IDeclpgexp — УДАЛЕНЫ (шасси expense замещает).
- IProgramExpense table-interface — сохранён.
- import IItem из expense interface для типизации items[].
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Story 1.5 эпика «Благорост» (blago issue 51F-17).
Полный цикл целевого расхода программы «Благорост» (не привязанного к проекту):
от заявки председателя до подтверждения оплаты gateway. Источник средств —
program_expense_pool (Story 1.6), резерв снимается окончательно в pgexppay.
PRD-B1: «createprogexp/approveprogex/authprogexp/progexppay/declprogexp». Имена
действий сокращены под 12-символьный лимит EOSIO — семантика та же.
Изменения:
- expenses.hpp: struct program_expense + таблица progexpenses (scope coopname)
+ helper'ы create/get/set_program_approved/set_program_authorized/delete
- 5 новых cpp actions в expense_managment/program_expenses/:
- createpgexp — резервирует в State::reserve_program_expense, создаёт запись
- apprvpgexp — председатель → soviet::create_agenda с CAPITAL_RESOLVE_PROGRAM_EXPENSE
- authpgexp — совет → ::Gateway::create_outpay (через Action::send), статус AUTHORIZED
- pgexppay — gateway callback → consume_program_expense + Ledger2::apply PAY_EXPENSE + delete
- declpgexp — отказ от coopname/_soviet/_gateway → release_program_expense + delete
- names.hpp: CREATE_PROGRAM_EXPENSE, AUTHORIZE_PROGRAM_EXPENSE, DECLINE_PROGRAM_EXPENSE,
CONFIRM_PROGRAM_EXPENSE_PAYMENT, SovietActions::CAPITAL_RESOLVE_PROGRAM_EXPENSE
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 1.6 эпика «Благорост» (blago issue 51F-18).
Программные расходы (Story 1.5) списываются из дедикейтнутого пула, а не из
аллокаций проектов. До этого коммита поля для такого пула в global_state не было.
Изменения:
- global_state.program_expense_pool — доступная сумма программы под расходы
- global_state.program_expense_reserved — зарезервированная под approved/authorized
программные расходы (Story 1.5: createprogexp резервирует, declprogexp возвращает,
exppaycnfrm списывает окончательно)
- State::topup_program_expense_pool / reserve_program_expense /
release_program_expense / consume_program_expense — utility-функции для Story 1.5
- topupprogexp(coopname, amount) action — председатель переводит amount из
global_available_invest_pool в program_expense_pool; деньги остаются на
ledger2-кошельке BLAGOROST_FUND, меняется только разбивка по назначению
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ExpensesAdminApprovePage — таблица proposals в status=CREATED (in-memory filter после ExpenseProposalsByCooperative).
ExpensesAdminAuthorizePage — таблица proposals в status=REPORT_SUBMITTED (тот же подход).
Обе по канону: PageHead + TableSkeleton + .table-wrap + .table-foot + EmptyState + переход в /expenses/:hash.
Mutation-действия (одобрить/авторизовать) появятся после расшивки Эпика 2 signature-pipeline на document2.
4/7 страниц #104 расшито.
8-я mutation симметрично authorizeExpenseReport: NotImplementedException
со ссылкой на statement_doc (type=2010) от signature-pipeline UI Эпика 2.
Регистрирует GraphQL-сигнатуру end-to-end, чтобы SDK + UI видели mutation
до подключения document2.
jest 8/8 passed.
7 тестов проверяют что каждое из 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.
Шасси расходов подключено к init-installed-extensions:
- expenses → expensesInstall в extensionsRegistry
- install.ts: убран TODO-комментарий о блокировке C28-31
Workspace 'expenses' (icon: receipt_long) с 6 routes (registry/admin/cashier/my)
становится доступен ролям chairman + user из конфигурации кооператива.
Stub-страницы внутри ждут расшивки через SDK Zeus после generate-client.
Покрывает 5 кейсов: REPORT_SUBMITTED/AUTHORIZED/DECLINED → no-op;
CLOSED без total_actual → warn; CLOSED + total_actual → log с
TODO-ссылкой на Эпик 0. Защищает контракт триггера на будущее, когда
skeleton будет заменён на реальный capital::createrid через
WriteMutationPool.
GraphQL surface: authorizeExpenseReport (председатель утверждает,
триггерит capital-trigger → capitalization Благороста) +
declineExpenseReport (с reason, переход в DECLINED, capitalization
НЕ запускается). Skeleton — NotImplementedException, chain-submit
после Эпика 0 (cooptypes regen для expense::authexp / declineexp).
ExpenseProposalSyncService override handleSyncDelta → emit
`entitysynced::expense::proposals` после save (per-contract pubsub,
canon controller/CLAUDE.md). ExpensesCapitalTriggerService подписан,
проверяет переход в CLOSED + total_actual → log skeleton. Прямой
DI на CapitalExtensionModule ждёт Эпика 0 (cooptypes regen +
WriteMutationPool для capital::createrid-эквивалента).
E2E.md в expenses/:
- Journey A (DIRECT-оплата Yandex Cloud из BLAGOROST_POOL): 10 шагов + matrix ledger2/document2 assertions
- Journey B (ADVANCE master+svetlana 2 items): 9 шагов + matrix
- Helpers-skeleton (loginAs, seedBlagorostPool, mockCouncilAuthorize, fixture*, assertLedgerOp/Document2)
- Performance gates ≤ 3 мин на сценарий
- Прежде чем писать — спросить про Cypress vs Playwright (канон-naming-soglasovyvat)
- Зависимости + дорожная карта расшивки
E2E framework в desktop отсутствует; реальные spec'и пишутся после C28-28..C28-33 и согласования инфраструктуры.
INTEGRATIONS.md в expenses/:
- BLAGOROST_POOL flow через существующий capital::createprogexp (никаких новых contract-actions)
- Каталог 9 универсальных событий шасси для Notification Center
- Backward-compat редиректы /programs/blagorost/expenses → /expenses?source=BLAGOROST_POOL
- Cleanup-чеклист legacy program-expense: на ветке пусто (re-grep ProgramExpense → 0 файлов; PR #59 не залит)
- Шаблоны 1010/1011 — не существуют, удалять нечего; роль закрывают 2010/2011 из C28-30
- Расшивка после закрытия C28-29/C28-31/C28-32
Не трогает рабочие сервисы (только новый .md в extensions/expenses/, не зарегистрировано в extensions-registry).
C28-31 (backend extension шасси расходов) логически блокируется C28-29 (контракт expenses) и C28-28 (P0 коды ledger2). Чтобы зонтичный PR не простаивал и следующий цикл шёл механически — кладём README с планируемой структурой файлов, MinIO-спекой бакета `expenses:files` и черновой GraphQL-схемой.
Когда расшивается C28-28 → C28-29 → этот README раскрывается в код по списку.
Реализация Эпика 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>