[mp2-9][@ant] feat(standards-site): уровень 3 кошельков пайщика, UX рабочей области и happy-path навигация
Учётная модель ledger2 теперь выражается в стандарте на трёх уровнях: L1 — бухгалтерские проводки (debit/credit), L2 — кошельки кооператива (wallet_from → wallet_to, агрегаты), L3 — кошелёк пайщика (per-user split available/blocked: user_wallet, user_ref, available_delta, blocked_delta). В types/standard.ts добавлены поля под L3, расширен enum WalletOp до полного набора _blago (BURN/REVOKE/ACCOUNT_ONLY); FocusBar показывает три отдельные карточки «Проводки / Кошельки кооператива / Кошелёк пайщика» с условным показом — пустой уровень не выводится; L3-карточка тянется на всю ширину op-сетки и идёт горизонтальной строкой (wallet · user · Доступно · Заблокировано), лейблы по-русски. На прозрачные --soft-варианты фона добавлен override на непрозрачный --bg, чтобы overlay-панель не светила сквозь себя. Рабочая область процесса доработана: FocusBar переехал из нижней горизонтальной полосы в левую вертикальную панель (правый верх занят зум-контролами VueFlow, куда добавлена кнопка Maximize/Minimize2 для fullscreen-режима через нативный Fullscreen API + автo-фит при смене размера); стартовый зум усилен (boost 2.0 → 2.8 ≈ +3 нажатия «+»). В fullscreen появляются дублирующие prev/next оверлеями у нижнего края (внешние ◀/▶ остаются за экраном). Навигация по ←/→ теперь идёт по happy-path: pickHappyTransition в graph/layout.ts штрафует имена-отказы (cancel/decline/expire/reject/abort/abandon/fail/revoke/discard) и legacy actions[].role === 'reject', штрафует самопетли (from === to), бонус за target-статус с продолжением; tie-break — порядок YAML. Vite dev: cooptypes (workspace ESM-бандл ~2 МБ) добавлен в optimizeDeps.include — без этого vite отдавал его через @fs трансформ-конвейером (раздувался до 10 МБ), по сети валился dynamic import ProcessPage с «Failed to fetch dynamically imported module». Pre-bundled chunk теперь 987 КБ. Пилот L3 на p.mkt.supply.standard.yaml (createorder + полный жизненный цикл): переименовано w.mkt.fund → w.mkt.member по всему файлу (33 случая в supply + return — это per-user членский кошелёк программы Marketplace, fund-наименование вводило в заблуждение); o.mkt.fconv (TRANSFER) получил L3 +order.total_cost на available заказчика, o.mkt.fblock (BLOCK) — only-L3 split −available/+blocked, o.mkt.funblk (UNBLOCK) — зеркало, o.mkt.fefund (TRANSFER) — −available на возврат, o.mkt.consum (REVOKE) — only-L3 −blocked + L1 Дт 86/Кт 10. Также введён промежуточный статус ship_ready («Отгрузка собрана») между accepted и supply_prepared: prepship больше не самопетля accepted→accepted, а accepted→ ship_ready, signsupp идёт от ship_ready → supply_prepared — линейный поток без обратных стрелок dagre. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
# используется (упрощение реализации MVP — реверты исключены из системы).
|
||||
#
|
||||
# **Модель кошельков (упрощённая, refinement 2026-05-04):** один программный
|
||||
# кошелёк w.mkt.fund (per-user) — членские взносы пайщика в программу.
|
||||
# кошелёк w.mkt.member (per-user) — членские взносы пайщика в программу.
|
||||
# Возврат суммы при гарантии происходит на тот же программный кошелёк
|
||||
# (.available восстанавливается); пайщик может потратить на следующий заказ
|
||||
# или вручную вывести на цифровой паевой w.wal.share через o.mkt.fefund.
|
||||
@@ -24,7 +24,7 @@
|
||||
# В процессе участвует 1 ledger2-операция:
|
||||
# • o.mkt.return — compensating forward для отката выдачи имущества
|
||||
# (ISSUE: восстановление .available на пайщикском
|
||||
# w.mkt.fund; проводка Дт 10 / Кт 86 —
|
||||
# w.mkt.member; проводка Дт 10 / Кт 86 —
|
||||
# обратная к o.mkt.consum, имущество назад на склад)
|
||||
#
|
||||
# Канон формата:
|
||||
@@ -35,7 +35,7 @@
|
||||
# • cpp/lib/core/ledger2/operations.hpp — OPERATION_REGISTRY
|
||||
# расширение o.mkt.return
|
||||
# • cpp/lib/core/ledger2/processes.hpp — processes::marketplace::RETURN
|
||||
# • cpp/lib/core/ledger2/wallets.hpp — w.mkt.fund (programmatic)
|
||||
# • cpp/lib/core/ledger2/wallets.hpp — w.mkt.member (programmatic)
|
||||
# • cpp/lib/core/ledger2/accounts.hpp — Целевое финансирование (86),
|
||||
# Материалы (10)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -104,7 +104,7 @@ actions:
|
||||
ledger2-операцию o.mkt.return — compensating forward к выдаче:
|
||||
имущество возвращается на склад КУ (Дт 10 / Кт 86), сумма
|
||||
восстанавливается на программном кошельке заказчика
|
||||
(w.mkt.fund .available +fact_cost).
|
||||
(w.mkt.member .available +fact_cost).
|
||||
|
||||
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
|
||||
# Сущность: marketplace::return_request (таблица return_requests, scope=coopname).
|
||||
@@ -134,7 +134,7 @@ states:
|
||||
Председатель очно принял возврат. Применена o.mkt.return —
|
||||
имущество возвращено на склад КУ (Дт 10 / Кт 86 — обратная проводка
|
||||
к o.mkt.consum), сумма order.fact_cost восстановлена на программном
|
||||
кошельке пайщика как .available на w.mkt.fund. Кошелёк
|
||||
кошельке пайщика как .available на w.mkt.member. Кошелёк
|
||||
пайщика обновлён в реальном времени; пайщик может направить
|
||||
вернувшуюся сумму на следующий заказ в Столе заказов либо вывести
|
||||
на цифровой паевой через отдельную операцию o.mkt.fefund.
|
||||
@@ -261,7 +261,7 @@ scenario:
|
||||
возвращается в учёт по счёту 10 КУ-получателя (проводка
|
||||
Дт 10 / Кт 86 — обратная к o.mkt.consum); (2) сумма order.fact_cost
|
||||
восстанавливается на программном кошельке заказчика
|
||||
(ISSUE на w.mkt.fund пайщика, .available +fact_cost).
|
||||
(ISSUE на w.mkt.member пайщика, .available +fact_cost).
|
||||
Кошелёк пайщика обновляется в реальном времени; пайщик может
|
||||
потратить вернувшуюся сумму на следующий заказ в Столе заказов
|
||||
либо вывести на цифровой паевой через отдельную операцию
|
||||
@@ -272,7 +272,7 @@ scenario:
|
||||
- return_request в approved_for_visit.
|
||||
- Пайщик прибыл с продукцией.
|
||||
post:
|
||||
- При принятии — return_request в return_accepted; имущество в учёте по счёту 10 КУ; сумма на w.mkt.fund пайщика как .available; в журнале опубликована o.mkt.return.
|
||||
- При принятии — return_request в return_accepted; имущество в учёте по счёту 10 КУ; сумма на w.mkt.member пайщика как .available; в журнале опубликована o.mkt.return.
|
||||
- При отказе — return_request в rejected_at_ku; пайщик забрал имущество; в журнале изменений по кошелькам нет.
|
||||
|
||||
alternatives:
|
||||
@@ -339,7 +339,7 @@ operations:
|
||||
human_name: Гарантийный возврат имущества
|
||||
wallet_op: ISSUE
|
||||
wallet_from: '' # вне системы — восстановление средств на пайщикском кошельке
|
||||
wallet_to: w.mkt.fund # ЦПП «Стол Заказов» — программный кошелёк (per-user, .available +fact_cost)
|
||||
wallet_to: w.mkt.member # ЦПП «Стол Заказов» — программный кошелёк (per-user, .available +fact_cost)
|
||||
debit: 10 # Материалы — обратная проводка к o.mkt.consum
|
||||
credit: 86 # Целевое финансирование
|
||||
amount_ref: order.fact_cost
|
||||
@@ -347,7 +347,7 @@ operations:
|
||||
description: >
|
||||
Compensating forward к выдаче имущества (o.mkt.consum). Атомарно
|
||||
выполняет два эффекта в одной операции: (1) ISSUE на пайщикском
|
||||
w.mkt.fund — .available +fact_cost (восстановление ранее
|
||||
w.mkt.member — .available +fact_cost (восстановление ранее
|
||||
списанной суммы); (2) проводка Дт 10 / Кт 86 — имущество возвращается
|
||||
в учёт по счёту 10 КУ-получателя (обратная к проводке o.mkt.consum
|
||||
Дт 86 / Кт 10). Журнал содержит payload original_consume_op_id
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# двойные подписи на АПП приёмки и АПП выдачи.
|
||||
#
|
||||
# **Модель кошельков (упрощённая, refinement 2026-05-04):** один программный
|
||||
# кошелёк w.mkt.fund (per-user) — членские взносы пайщика в программу
|
||||
# кошелёк w.mkt.member (per-user) — членские взносы пайщика в программу
|
||||
# «Стол заказов». Транзитные кошельки не используются. Имущество
|
||||
# отслеживается бухгалтерской аналитикой по счёту 10 (per-КУ субсчета),
|
||||
# без отдельного кошелька. Платформенные кошельки w.wal.share (Цифровой
|
||||
@@ -24,21 +24,21 @@
|
||||
#
|
||||
# В процессе участвуют 6 ledger2-операций (из 9 общих по контракту):
|
||||
# • o.mkt.fconv — конвертация цифрового рубля → членский взнос ЦПП
|
||||
# (TRANSFER, w.wal.share → w.mkt.fund,
|
||||
# (TRANSFER, w.wal.share → w.mkt.member,
|
||||
# Дт 80 / Кт 86)
|
||||
# • o.mkt.fblock — блокировка средств под Order на программном кошельке
|
||||
# (BLOCK)
|
||||
# • o.mkt.funblk — разблокировка при отмене Order'а
|
||||
# (UNBLOCK)
|
||||
# • o.mkt.fefund — возврат разблокированной суммы на цифровой паевой
|
||||
# (TRANSFER, w.mkt.fund → w.wal.share,
|
||||
# (TRANSFER, w.mkt.member → w.wal.share,
|
||||
# Дт 86 / Кт 80)
|
||||
# • o.mkt.purch — приёмка имущества кооперативом по АПП приёмки
|
||||
# (ACCOUNT_ONLY, только проводка, Дт 10 / Кт 86)
|
||||
# • o.mkt.payspl — оплата поставщику с расчётного счёта
|
||||
# (TRANSFER, ∅ → w.mkt.payout, Дт 86 / Кт 51)
|
||||
# • o.mkt.consum — выдача имущества пайщику по АПП выдачи
|
||||
# (REVOKE, w.mkt.fund .blocked → 0,
|
||||
# (REVOKE, w.mkt.member .blocked → 0,
|
||||
# Дт 86 / Кт 10)
|
||||
#
|
||||
# Канон формата:
|
||||
@@ -48,7 +48,7 @@
|
||||
# • cpp/marketplace/src/ — реализация (предстоит)
|
||||
# • cpp/lib/core/ledger2/operations.hpp — OPERATION_REGISTRY (расширение)
|
||||
# • cpp/lib/core/ledger2/processes.hpp — processes::marketplace::SUPPLY
|
||||
# • cpp/lib/core/ledger2/wallets.hpp — w.mkt.fund (programmatic),
|
||||
# • cpp/lib/core/ledger2/wallets.hpp — w.mkt.member (programmatic),
|
||||
# w.wal.share (платформенный),
|
||||
# w.mkt.payout (платформенный)
|
||||
# • cpp/lib/core/ledger2/accounts.hpp — Расчётный (51),
|
||||
@@ -198,7 +198,7 @@ actions:
|
||||
Заказчик ставит вторую (финальную) подпись на АПП выдачи на устройстве
|
||||
оператора КУ. Это закрывающее действие всего процесса: контракт
|
||||
применяет ledger2-операцию o.mkt.consum (Дт 86 / Кт 10, REVOKE —
|
||||
w.mkt.fund пайщика .blocked → 0, имущество выбывает со
|
||||
w.mkt.member пайщика .blocked → 0, имущество выбывает со
|
||||
склада через проводку обратную к o.mkt.purch). При расхождении «факт
|
||||
vs заказ» контракт предварительно выполняет корректировку: факт меньше
|
||||
→ o.mkt.funblk на разницу (остаётся available на программном кошельке
|
||||
@@ -218,7 +218,7 @@ states:
|
||||
human: Заявка размещена
|
||||
description: >
|
||||
Order создан, средства заказчика прошли пару o.mkt.fconv + o.mkt.fblock
|
||||
и заблокированы на программном кошельке w.mkt.fund. Заявка
|
||||
и заблокированы на программном кошельке w.mkt.member. Заявка
|
||||
ждёт окончания цикла отсечки и акцепта поставщика.
|
||||
kind: normal
|
||||
|
||||
@@ -238,10 +238,19 @@ states:
|
||||
description: >
|
||||
Поставщик акцептовал консолидированную заявку, пара Order+Offer
|
||||
опубликована on-chain. Средства заказчика по-прежнему заблокированы
|
||||
на w.mkt.fund; имущество ещё не передано. Поставщик
|
||||
на w.mkt.member; имущество ещё не передано. Поставщик
|
||||
готовит отгрузку.
|
||||
kind: normal
|
||||
|
||||
- name: ship_ready
|
||||
human: Отгрузка собрана
|
||||
description: >
|
||||
Поставщик собрал партию по составу акцептованной заявки и выбрал
|
||||
режим передачи (A — самовывоз / B — экспедитор + ТТН). Имущество
|
||||
готово к подписанию АПП приёмки, но фактически ещё не передано КУ
|
||||
и юридически не оприходовано.
|
||||
kind: normal
|
||||
|
||||
- name: supply_prepared
|
||||
human: Поставщик подписал
|
||||
description: >
|
||||
@@ -271,7 +280,7 @@ states:
|
||||
human: Имущество выдано
|
||||
description: >
|
||||
Заказчик закрыл АПП выдачи. Применена o.mkt.consum (REVOKE на
|
||||
w.mkt.fund пайщика .blocked → 0; проводка Дт 86 / Кт 10 —
|
||||
w.mkt.member пайщика .blocked → 0; проводка Дт 86 / Кт 10 —
|
||||
целевое использование членского взноса с одновременным выбытием
|
||||
имущества со склада). Процесс поставки-приобретения полностью завершён.
|
||||
Имущество на руках у пайщика, средства учтены. Гарантийное окно
|
||||
@@ -289,9 +298,9 @@ transitions:
|
||||
- o.mkt.fblock
|
||||
guards:
|
||||
- Заказчик — активный пайщик кооператива.
|
||||
- Заказчик подписал Соглашение ЦПП «Стол заказов» (программный кошелёк w.mkt.fund открыт).
|
||||
- Заказчик подписал Соглашение ЦПП «Стол заказов» (программный кошелёк w.mkt.member открыт).
|
||||
- Стоимость корзины не превышает 100 000 ₽ (валидируется Backend'ом до вызова контракта).
|
||||
- Достаточно средств на цифровом паевом (w.wal.share) либо available на программном кошельке (w.mkt.fund) — частичная конвертация если на программном уже что-то есть.
|
||||
- Достаточно средств на цифровом паевом (w.wal.share) либо available на программном кошельке (w.mkt.member) — частичная конвертация если на программном уже что-то есть.
|
||||
- Указанный КУ принадлежит этому же кооперативу.
|
||||
|
||||
- from: active
|
||||
@@ -337,14 +346,14 @@ transitions:
|
||||
- Поставщик отказывается от консолидированной партии до её публикации on-chain.
|
||||
|
||||
- from: accepted
|
||||
to: accepted
|
||||
to: ship_ready
|
||||
action: marketplace::prepship
|
||||
actor: supplier
|
||||
guards:
|
||||
- Состав Shipment'а равен составу акцептованной партии (жёсткий акцепт; замены номенклатуры запрещены).
|
||||
- Поставщик выбрал режим: A (самовывоз) или B (экспедитор + ТТН).
|
||||
|
||||
- from: accepted
|
||||
- from: ship_ready
|
||||
to: supply_prepared
|
||||
action: marketplace::signsupp
|
||||
actor: supplier
|
||||
@@ -397,18 +406,18 @@ scenario:
|
||||
налоговый порог ТМЦ). Контракт createorder атомарно выполняет пару
|
||||
операций по программному кошельку: (1) o.mkt.fconv — конвертация
|
||||
цифрового рубля заказчика (w.wal.share) в членский взнос на ЦПП
|
||||
«Стол заказов» (w.mkt.fund), Дт 80 / Кт 86; (2) o.mkt.fblock
|
||||
«Стол заказов» (w.mkt.member), Дт 80 / Кт 86; (2) o.mkt.fblock
|
||||
— блокировка суммы order.total_cost под Order на этом же кошельке,
|
||||
available -= total_cost, blocked += total_cost. Order переходит
|
||||
в статус active.
|
||||
pre:
|
||||
- Пайщик имеет статус active.
|
||||
- Подписано Соглашение ЦПП «Стол заказов» (программный кошелёк w.mkt.fund открыт).
|
||||
- На w.wal.share либо w.mkt.fund.available достаточно средств.
|
||||
- Подписано Соглашение ЦПП «Стол заказов» (программный кошелёк w.mkt.member открыт).
|
||||
- На w.wal.share либо w.mkt.member.available достаточно средств.
|
||||
- Стоимость корзины ≤ 100 000 ₽.
|
||||
post:
|
||||
- Order создан со статусом active.
|
||||
- На w.mkt.fund пайщика заблокирована сумма order.total_cost.
|
||||
- На w.mkt.member пайщика заблокирована сумма order.total_cost.
|
||||
- В журнале опубликованы 2 операции серии (fconv → fblock).
|
||||
|
||||
- step: 2
|
||||
@@ -422,7 +431,7 @@ scenario:
|
||||
(off-chain) и заявки остаются в статусе active. Если порог не
|
||||
достигнут — для каждого Order'а контракт выполняет o.mkt.funblk
|
||||
(разблокировка) + o.mkt.fefund (возврат на цифровой паевой через
|
||||
TRANSFER w.mkt.fund → w.wal.share с проводкой
|
||||
TRANSFER w.mkt.member → w.wal.share с проводкой
|
||||
Дт 86 / Кт 80). Order'ы переходят в статус cancelled, заказчики
|
||||
получают push-уведомление об отмене.
|
||||
pre:
|
||||
@@ -547,7 +556,7 @@ scenario:
|
||||
отрицательный баланс ≤5% либо пайщик пополняет паевой через
|
||||
кассира. Заказчик подписывает АПП выдачи на устройстве оператора —
|
||||
это финальная (вторая) подпись. Контракт применяет o.mkt.consum
|
||||
(REVOKE — w.mkt.fund пайщика .blocked → 0; проводка
|
||||
(REVOKE — w.mkt.member пайщика .blocked → 0; проводка
|
||||
Дт 86 / Кт 10 — расход целевых средств на расход имущества). Order
|
||||
переходит в финальный статус received.
|
||||
pre:
|
||||
@@ -556,7 +565,7 @@ scenario:
|
||||
post:
|
||||
- Имущество выдано пайщику.
|
||||
- Order в финальном статусе received.
|
||||
- Сумма order.total_cost (или fact_cost при коррекции) списана с w.mkt.fund пайщика.
|
||||
- Сумма order.total_cost (или fact_cost при коррекции) списана с w.mkt.member пайщика.
|
||||
- В бухгалтерии — Дт 86 / Кт 10 на сумму fact_cost.
|
||||
- В журнале опубликована o.mkt.consum (с предшествующими корректировками при необходимости).
|
||||
- Гарантийное окно открыто (запуск p.mkt.return по заявлению пайщика возможен до конца окна).
|
||||
@@ -685,7 +694,7 @@ documents:
|
||||
# Семь ledger2-операций для процесса прямой поставки-приобретения. Имена
|
||||
# ≤12 символов eosio::name. Кошельки идентифицируются eosio::name-строками
|
||||
# с префиксом w.<contract>.<waltype> (рефакторинг 2026-04-27, ветка reports):
|
||||
# • w.mkt.fund — ЦПП «Стол Заказов» — программный кошелёк (per-user)
|
||||
# • w.mkt.member — ЦПП «Стол Заказов» — программный кошелёк (per-user)
|
||||
# [members fees программы]
|
||||
# • w.wal.share — ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
|
||||
# [платформенный]
|
||||
@@ -696,59 +705,93 @@ operations:
|
||||
- ledger_code: o.mkt.fconv
|
||||
human_name: Конвертация цифрового рубля в членский взнос
|
||||
wallet_op: TRANSFER
|
||||
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
|
||||
wallet_to: w.mkt.fund # ЦПП «Стол Заказов» — программный кошелёк (членские взносы)
|
||||
# L1 — двойная запись
|
||||
debit: 80 # Паевой фонд
|
||||
credit: 86 # Целевое финансирование
|
||||
# L2 — кошельки кооператива (агрегаты)
|
||||
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
|
||||
wallet_to: w.mkt.member # ЦПП «Стол Заказов» — членские взносы пайщиков по программе
|
||||
# L3 — на стороне заказчика приходит зачисление в available на
|
||||
# w.mkt.member; блокировка под Order — следующая операция o.mkt.fblock
|
||||
user_wallet: w.mkt.member
|
||||
user_ref: order.customer
|
||||
available_delta: +order.total_cost
|
||||
blocked_delta: null
|
||||
amount_ref: order.total_cost
|
||||
triggered_by: marketplace::createorder
|
||||
description: >
|
||||
Атомарная серия — шаг 1 из 2. Конвертация цифрового рубля заказчика
|
||||
из ЦПП «Цифровой Кошелёк» в членский взнос на ЦПП «Стол заказов»
|
||||
(программный кошелёк пайщика в этой ЦПП). Двойная запись Дт 80 / Кт 86 —
|
||||
средства переходят из складочного капитала в целевое финансирование.
|
||||
из ЦПП «Цифровой Кошелёк» в членский взнос на ЦПП «Стол заказов».
|
||||
Двойная запись Дт 80 / Кт 86 — средства переходят из складочного
|
||||
капитала в целевое финансирование. На уровне 3 — пополнение available
|
||||
на w.mkt.member заказчика; блокировка этой суммы под конкретный
|
||||
Order — отдельная операция o.mkt.fblock следом.
|
||||
(TBD-Standardization: проводка фиксируется как baseline; финальный
|
||||
выбор — при формализации стандарта совместно с Ангелиной.)
|
||||
|
||||
- ledger_code: o.mkt.fblock
|
||||
human_name: Блокировка членского взноса под заказ
|
||||
wallet_op: BLOCK
|
||||
wallet_from: w.mkt.fund # ЦПП «Стол Заказов» — программный кошелёк
|
||||
wallet_to: ''
|
||||
# L1 — операция чисто L3, бухгалтерия не двигается
|
||||
debit: null
|
||||
credit: null
|
||||
# L2 — без перевода между кошельками кооператива
|
||||
wallet_from: null
|
||||
wallet_to: null
|
||||
# L3 — split available → blocked на одном кошельке заказчика
|
||||
user_wallet: w.mkt.member # ЦПП «Стол Заказов» — членские взносы заказчика
|
||||
user_ref: order.customer
|
||||
available_delta: -order.total_cost
|
||||
blocked_delta: +order.total_cost
|
||||
amount_ref: order.total_cost
|
||||
triggered_by: marketplace::createorder
|
||||
description: >
|
||||
Атомарная серия — шаг 2 из 2. Блокировка суммы order.total_cost под
|
||||
конкретный Order на программном кошельке заказчика. available -=
|
||||
total_cost, blocked += total_cost. Без бухгалтерских проводок.
|
||||
Блокировка снимается либо при выдаче (o.mkt.consum), либо при отмене
|
||||
(o.mkt.funblk + o.mkt.fefund).
|
||||
конкретный Order на кошельке заказчика w.mkt.member. Это операция
|
||||
уровня 3 — split available → blocked на одном кошельке без перевода
|
||||
между кошельками кооператива и без бухгалтерских проводок.
|
||||
Блокировка снимается либо при выдаче (o.mkt.consum, REVOKE blocked → 0),
|
||||
либо при отмене (o.mkt.funblk + o.mkt.fefund — UNBLOCK + возврат).
|
||||
|
||||
- ledger_code: o.mkt.funblk
|
||||
human_name: Разблокировка членского взноса при отмене
|
||||
wallet_op: UNBLOCK
|
||||
wallet_from: w.mkt.fund # ЦПП «Стол Заказов» — программный кошелёк
|
||||
wallet_to: ''
|
||||
# L1 — без проводок
|
||||
debit: null
|
||||
credit: null
|
||||
# L2 — без перевода
|
||||
wallet_from: null
|
||||
wallet_to: null
|
||||
# L3 — обратное движение blocked → available
|
||||
user_wallet: w.mkt.member
|
||||
user_ref: order.customer
|
||||
available_delta: +order.total_cost
|
||||
blocked_delta: -order.total_cost
|
||||
amount_ref: order.total_cost
|
||||
triggered_by: marketplace::cancelorder | marketplace::expirecycle | marketplace::declinebatch
|
||||
description: >
|
||||
Зеркало o.mkt.fblock. Снятие блокировки суммы на программном кошельке
|
||||
заказчика при отмене Order'а (заказчик отменяет до акцепта; цикл истёк
|
||||
без достижения порога; поставщик отказался от партии). blocked -=
|
||||
total_cost, available += total_cost. Без проводок. Парная операция —
|
||||
o.mkt.fefund (возврат суммы на цифровой паевой через TRANSFER).
|
||||
Зеркало o.mkt.fblock. Снятие блокировки на кошельке заказчика
|
||||
w.mkt.member при отмене Order'а (заказчик отменяет до акцепта;
|
||||
цикл истёк без достижения порога; поставщик отказался от партии).
|
||||
На уровне 3 blocked → available. Без L1, без L2. Парная операция —
|
||||
o.mkt.fefund (возврат разблокированной суммы на цифровой паевой через
|
||||
TRANSFER).
|
||||
|
||||
- ledger_code: o.mkt.fefund
|
||||
human_name: Возврат членского взноса на цифровой паевой
|
||||
wallet_op: TRANSFER
|
||||
wallet_from: w.mkt.fund # ЦПП «Стол Заказов» — программный кошелёк
|
||||
wallet_to: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
|
||||
# L1
|
||||
debit: 86 # Целевое финансирование
|
||||
credit: 80 # Паевой фонд
|
||||
# L2 — кошельки кооператива
|
||||
wallet_from: w.mkt.member # ЦПП «Стол Заказов»
|
||||
wallet_to: w.wal.share # ЦПП «Цифровой Кошелёк»
|
||||
# L3 — у заказчика на w.mkt.member списывается available; зачисление
|
||||
# на w.wal.share — отдельная операция стандарта wallet
|
||||
user_wallet: w.mkt.member
|
||||
user_ref: order.customer
|
||||
available_delta: -order.total_cost
|
||||
blocked_delta: null
|
||||
amount_ref: order.total_cost
|
||||
triggered_by: marketplace::cancelorder | marketplace::expirecycle | marketplace::declinebatch
|
||||
description: >
|
||||
@@ -801,10 +844,18 @@ operations:
|
||||
- ledger_code: o.mkt.consum
|
||||
human_name: Выдача имущества пайщику
|
||||
wallet_op: REVOKE
|
||||
wallet_from: w.mkt.fund # ЦПП «Стол Заказов» — программный кошелёк пайщика (.blocked → 0)
|
||||
wallet_to: ''
|
||||
# L1 — выбытие имущества за счёт целевых средств
|
||||
debit: 86 # Целевое финансирование
|
||||
credit: 10 # Материалы
|
||||
# L2 — без перевода между кошельками кооператива (REVOKE списывает
|
||||
# blocked в пустоту, это L3-семантика)
|
||||
wallet_from: null
|
||||
wallet_to: null
|
||||
# L3 — blocked заказчика обнуляется (целевое расходование членского взноса)
|
||||
user_wallet: w.mkt.member
|
||||
user_ref: order.customer
|
||||
available_delta: null
|
||||
blocked_delta: -order.fact_cost
|
||||
amount_ref: order.fact_cost
|
||||
triggered_by: marketplace::signiss2
|
||||
description: >
|
||||
|
||||
@@ -222,6 +222,25 @@ function walletDisplayId(name: string | null | undefined): string {
|
||||
return name == null || name === '' ? '∅' : name;
|
||||
}
|
||||
|
||||
// ── L3 helpers ──────────────────────────────────────────────────────────
|
||||
// Дельты на user-wallet хранятся как выражения вида '-order.total_cost' /
|
||||
// '+order.total_cost'. Если знак не задан — нормализуем явно для UI.
|
||||
function formatDelta(expr: string | null | undefined): string {
|
||||
if (expr == null || expr === '') return '—';
|
||||
const s = String(expr).trim();
|
||||
if (s.startsWith('+') || s.startsWith('-') || s.startsWith('±')) return s;
|
||||
return `±${s}`;
|
||||
}
|
||||
|
||||
// Показываем L3-карточку только когда операция реально двигает кошелёк
|
||||
// пайщика. Достаточно одного из полей user_wallet / available_delta /
|
||||
// blocked_delta — остальные инферим как «не указано».
|
||||
function hasL3(op: Ledger2Operation): boolean {
|
||||
return Boolean(
|
||||
op.user_wallet || op.user_ref || op.available_delta || op.blocked_delta,
|
||||
);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -336,9 +355,10 @@ function walletDisplayId(name: string | null | undefined): string {
|
||||
<div class="focus-bar__op-name">{{ op.human_name }}</div>
|
||||
|
||||
<div class="focus-bar__op-subs">
|
||||
<!-- L1 — бухгалтерские проводки -->
|
||||
<div
|
||||
v-if="op.debit != null || op.credit != null"
|
||||
class="focus-bar__op-sub"
|
||||
class="focus-bar__op-sub focus-bar__op-sub--l1"
|
||||
>
|
||||
<div class="focus-bar__op-sub-label">Проводки</div>
|
||||
<div class="focus-bar__op-sub-body">
|
||||
@@ -352,11 +372,13 @@ function walletDisplayId(name: string | null | undefined): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- L2 — переводы по кошелькам кооператива -->
|
||||
<div
|
||||
v-if="op.wallet_op !== 'WALLET_ONLY' && (!!op.wallet_from || !!op.wallet_to)"
|
||||
class="focus-bar__op-sub"
|
||||
v-if="['ISSUE', 'TRANSFER', 'BURN', 'WALLET_ONLY'].includes(op.wallet_op)
|
||||
&& (!!op.wallet_from || !!op.wallet_to)"
|
||||
class="focus-bar__op-sub focus-bar__op-sub--l2"
|
||||
>
|
||||
<div class="focus-bar__op-sub-label">Переводы</div>
|
||||
<div class="focus-bar__op-sub-label">Кошельки кооператива</div>
|
||||
<div class="focus-bar__op-sub-body">
|
||||
<span class="tooltip" :data-tip="walletTitle(op.wallet_from, op.wallet_op)">
|
||||
<code>{{ walletDisplayId(op.wallet_from) }}</code>
|
||||
@@ -367,6 +389,32 @@ function walletDisplayId(name: string | null | undefined): string {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- L3 — кошелёк пайщика (per-user available/blocked split) -->
|
||||
<div
|
||||
v-if="hasL3(op)"
|
||||
class="focus-bar__op-sub focus-bar__op-sub--l3"
|
||||
>
|
||||
<div class="focus-bar__op-sub-label">Кошелёк пайщика</div>
|
||||
<div class="focus-bar__op-sub-body focus-bar__op-sub-body--stack">
|
||||
<div class="focus-bar__l3-row">
|
||||
<span class="tooltip" :data-tip="walletTitle(op.user_wallet, op.wallet_op)">
|
||||
<code>{{ walletDisplayId(op.user_wallet) }}</code>
|
||||
</span>
|
||||
<span v-if="op.user_ref" class="focus-bar__l3-user">
|
||||
· {{ op.user_ref }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="op.available_delta" class="focus-bar__l3-delta">
|
||||
<span class="focus-bar__l3-delta-label">Доступно</span>
|
||||
<code>{{ formatDelta(op.available_delta) }}</code>
|
||||
</div>
|
||||
<div v-if="op.blocked_delta" class="focus-bar__l3-delta">
|
||||
<span class="focus-bar__l3-delta-label">Заблокировано</span>
|
||||
<code>{{ formatDelta(op.blocked_delta) }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,9 +451,10 @@ function walletDisplayId(name: string | null | undefined): string {
|
||||
</div>
|
||||
|
||||
<div class="focus-bar__col focus-bar__col--meta">
|
||||
<!-- L1 -->
|
||||
<div
|
||||
v-if="focusedOperation.debit != null || focusedOperation.credit != null"
|
||||
class="focus-bar__op-sub"
|
||||
class="focus-bar__op-sub focus-bar__op-sub--l1"
|
||||
>
|
||||
<div class="focus-bar__op-sub-label">Проводки</div>
|
||||
<div class="focus-bar__op-sub-body">
|
||||
@@ -419,12 +468,13 @@ function walletDisplayId(name: string | null | undefined): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- L2 -->
|
||||
<div
|
||||
v-if="focusedOperation.wallet_op !== 'WALLET_ONLY'
|
||||
v-if="['ISSUE', 'TRANSFER', 'BURN', 'WALLET_ONLY'].includes(focusedOperation.wallet_op)
|
||||
&& (!!focusedOperation.wallet_from || !!focusedOperation.wallet_to)"
|
||||
class="focus-bar__op-sub"
|
||||
class="focus-bar__op-sub focus-bar__op-sub--l2"
|
||||
>
|
||||
<div class="focus-bar__op-sub-label">Переводы</div>
|
||||
<div class="focus-bar__op-sub-label">Переводы (кооператив)</div>
|
||||
<div class="focus-bar__op-sub-body">
|
||||
<span class="tooltip" :data-tip="walletTitle(focusedOperation.wallet_from, focusedOperation.wallet_op)">
|
||||
<code>{{ walletDisplayId(focusedOperation.wallet_from) }}</code>
|
||||
@@ -436,6 +486,32 @@ function walletDisplayId(name: string | null | undefined): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- L3 -->
|
||||
<div
|
||||
v-if="hasL3(focusedOperation)"
|
||||
class="focus-bar__op-sub focus-bar__op-sub--l3"
|
||||
>
|
||||
<div class="focus-bar__op-sub-label">Кошелёк пайщика</div>
|
||||
<div class="focus-bar__op-sub-body focus-bar__op-sub-body--stack">
|
||||
<div class="focus-bar__l3-row">
|
||||
<span class="tooltip" :data-tip="walletTitle(focusedOperation.user_wallet, focusedOperation.wallet_op)">
|
||||
<code>{{ walletDisplayId(focusedOperation.user_wallet) }}</code>
|
||||
</span>
|
||||
<span v-if="focusedOperation.user_ref" class="focus-bar__l3-user">
|
||||
· {{ focusedOperation.user_ref }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="focusedOperation.available_delta" class="focus-bar__l3-delta">
|
||||
<span class="focus-bar__l3-delta-label">Доступно</span>
|
||||
<code>{{ formatDelta(focusedOperation.available_delta) }}</code>
|
||||
</div>
|
||||
<div v-if="focusedOperation.blocked_delta" class="focus-bar__l3-delta">
|
||||
<span class="focus-bar__l3-delta-label">Заблокировано</span>
|
||||
<code>{{ formatDelta(focusedOperation.blocked_delta) }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="focus-bar__op-sub">
|
||||
<div class="focus-bar__op-sub-label">Сумма</div>
|
||||
<div class="focus-bar__op-sub-body"><code>{{ focusedOperation.amount_ref }}</code></div>
|
||||
@@ -650,6 +726,41 @@ function walletDisplayId(name: string | null | undefined): string {
|
||||
.focus-bar__op-sep { color: var(--text-subtle); }
|
||||
.focus-bar__sep { color: var(--text-subtle); }
|
||||
|
||||
/* L1/L2/L3 — лёгкая дифференциация фоном без перекраски рамки. */
|
||||
.focus-bar__op-sub--l1 { background: var(--bg); }
|
||||
.focus-bar__op-sub--l2 { background: var(--edge-focus-soft); }
|
||||
.focus-bar__op-sub--l3 {
|
||||
background: var(--accent-soft);
|
||||
border-color: var(--accent-border);
|
||||
/* L3 — самая «нагруженная» карточка (wallet · user / Доступно / Заблокировано),
|
||||
поэтому растягиваем на всю ширину сетки op-subs, чтобы дельты не зажимались
|
||||
в узкую колонку и читались одной строкой. */
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.focus-bar__op-sub--l3 .focus-bar__op-sub-body code {
|
||||
background: var(--bg); border-color: var(--accent-border); color: var(--accent);
|
||||
}
|
||||
|
||||
/* L3-карточка: горизонтальная (wallet · user · Доступно · Заблокировано). */
|
||||
.focus-bar__op-sub-body--stack {
|
||||
display: flex; flex-direction: row; align-items: baseline;
|
||||
gap: 8px 18px; flex-wrap: wrap;
|
||||
}
|
||||
.focus-bar__l3-row {
|
||||
display: inline-flex; align-items: baseline; gap: 4px; flex-wrap: wrap;
|
||||
}
|
||||
.focus-bar__l3-user {
|
||||
font-size: 11px; color: var(--text-muted); font-style: italic;
|
||||
}
|
||||
.focus-bar__l3-delta {
|
||||
display: inline-flex; align-items: baseline; gap: 6px;
|
||||
font-size: 11.5px; color: var(--text);
|
||||
}
|
||||
.focus-bar__l3-delta-label {
|
||||
font-size: 10px; font-weight: 600; letter-spacing: 0.04em;
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
|
||||
/* ── CSS-tooltip, надёжный (title часто блокируется) ─────────────────────── */
|
||||
.tooltip { position: relative; display: inline-flex; align-items: center; gap: 4px; cursor: help; }
|
||||
.tooltip[data-tip]:hover::after,
|
||||
|
||||
@@ -3,8 +3,8 @@ import { computed, markRaw, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { VueFlow, type EdgeMouseEvent, type NodeMouseEvent, type VueFlowStore } from '@vue-flow/core';
|
||||
import { Background } from '@vue-flow/background';
|
||||
import { Controls } from '@vue-flow/controls';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-vue-next';
|
||||
import { Controls, ControlButton } from '@vue-flow/controls';
|
||||
import { ChevronLeft, ChevronRight, Maximize2, Minimize2 } from 'lucide-vue-next';
|
||||
|
||||
import '@vue-flow/core/dist/style.css';
|
||||
import '@vue-flow/core/dist/theme-default.css';
|
||||
@@ -64,8 +64,8 @@ const layout = computed(() =>
|
||||
const vfInstance = ref<VueFlowStore | null>(null);
|
||||
const minZoom = 0.15;
|
||||
const maxZoom = 2.0;
|
||||
// Во сколько раз сильнее, чем fitView (≈ 4 нажатия «+» в зум-контролах).
|
||||
const START_ZOOM_BOOST = 2.0;
|
||||
// Во сколько раз сильнее, чем fitView (≈ 6–7 нажатий «+» в зум-контролах).
|
||||
const START_ZOOM_BOOST = 2.8;
|
||||
|
||||
function doFit(): void {
|
||||
if (!vfInstance.value) return;
|
||||
@@ -171,6 +171,34 @@ onBeforeUnmount(() => {
|
||||
resizeObserver = null;
|
||||
});
|
||||
|
||||
// ── Fullscreen рабочей области ────────────────────────────────────────
|
||||
const isFullscreen = ref(false);
|
||||
|
||||
function onFsChange(): void {
|
||||
isFullscreen.value = document.fullscreenElement === containerRef.value;
|
||||
// После смены размера — перепосчитать viewport.
|
||||
requestAnimationFrame(() => doFit());
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('fullscreenchange', onFsChange);
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.removeEventListener('fullscreenchange', onFsChange);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleFullscreen(): void {
|
||||
const el = containerRef.value;
|
||||
if (!el) return;
|
||||
if (document.fullscreenElement === el) {
|
||||
void document.exitFullscreen();
|
||||
} else {
|
||||
void el.requestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
function onNodeClick({ node }: NodeMouseEvent) {
|
||||
switch (node.type) {
|
||||
case NODE_TYPES.START:
|
||||
@@ -274,10 +302,42 @@ function labelForNav(n: Nav): string {
|
||||
@edge-click="onEdgeClick"
|
||||
>
|
||||
<Background :pattern-color="gridColor" :gap="16" :size="1" />
|
||||
<Controls position="top-right" :show-interactive="false" />
|
||||
<Controls position="top-right" :show-interactive="false">
|
||||
<ControlButton
|
||||
:title="isFullscreen ? 'Свернуть из полноэкранного режима' : 'Развернуть на весь экран'"
|
||||
:aria-label="isFullscreen ? 'Свернуть' : 'Развернуть на весь экран'"
|
||||
@click="toggleFullscreen"
|
||||
>
|
||||
<Minimize2 v-if="isFullscreen" :size="14" />
|
||||
<Maximize2 v-else :size="14" />
|
||||
</ControlButton>
|
||||
</Controls>
|
||||
</VueFlow>
|
||||
|
||||
<!-- Горизонтальная полоса деталей фокуса, прилипшая к низу рабочей области. -->
|
||||
<!-- Дублирующие prev/next в виде оверлеев — видны только в fullscreen,
|
||||
где внешние кнопки .graph-nav скрыты за пределами экрана. -->
|
||||
<button
|
||||
type="button"
|
||||
class="graph-nav graph-nav--floating graph-nav--floating-prev"
|
||||
:disabled="!prevItem"
|
||||
:aria-label="prevItem ? `Назад: ${labelForNav(prevItem)}` : 'Назад (нет)'"
|
||||
:title="prevItem ? `Назад: ${labelForNav(prevItem)}` : ''"
|
||||
@click="pushNav(prevItem)"
|
||||
>
|
||||
<ChevronLeft :size="20" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="graph-nav graph-nav--floating graph-nav--floating-next"
|
||||
:disabled="!nextItem"
|
||||
:aria-label="nextItem ? `Вперёд: ${labelForNav(nextItem)}` : 'Вперёд (нет)'"
|
||||
:title="nextItem ? `Вперёд: ${labelForNav(nextItem)}` : ''"
|
||||
@click="pushNav(nextItem)"
|
||||
>
|
||||
<ChevronRight :size="20" />
|
||||
</button>
|
||||
|
||||
<!-- Левая панель деталей фокуса, прилипшая к левому краю рабочей области. -->
|
||||
<div class="focus-bar-slot">
|
||||
<FocusBar
|
||||
:standard="standard"
|
||||
@@ -345,21 +405,65 @@ function labelForNav(n: Nav): string {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Полоса деталей фокуса — приклеена к низу рабочей области изнутри */
|
||||
/* Панель деталей фокуса — приклеена к левому краю рабочей области изнутри
|
||||
(правый верх занят зум-контролами VueFlow). */
|
||||
.focus-bar-slot {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
width: min(380px, 42%);
|
||||
z-index: 10;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.focus-bar-slot :deep(.focus-bar) {
|
||||
margin-bottom: 0;
|
||||
background: var(--bg);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
||||
max-height: 42%;
|
||||
box-shadow: 6px 0 20px rgba(0, 0, 0, 0.12);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 14px;
|
||||
}
|
||||
/* Внутри overlay-слота фон панели всегда непрозрачный — варианты
|
||||
--edge / --doc / --op / --process / --rejected используют *-soft цвета
|
||||
(rgba с alpha), сквозь которые просвечивает граф. Цвет окантовки
|
||||
(border-color) остаётся вариантным — это и есть индикатор типа фокуса. */
|
||||
.focus-bar-slot :deep(.focus-bar.focus-bar--edge),
|
||||
.focus-bar-slot :deep(.focus-bar.focus-bar--doc),
|
||||
.focus-bar-slot :deep(.focus-bar.focus-bar--op),
|
||||
.focus-bar-slot :deep(.focus-bar.focus-bar--process),
|
||||
.focus-bar-slot :deep(.focus-bar.focus-bar--rejected) {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* В полноэкранном режиме рабочая зона занимает весь экран. */
|
||||
.process-graph:fullscreen {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* Floating prev/next: пара кнопок у нижнего края в fullscreen — рядом,
|
||||
а не по краям, чтобы не перекрывать вертикальный focus-bar слева. */
|
||||
.graph-nav--floating {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
height: 44px;
|
||||
z-index: 11;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
.graph-nav--floating-prev { right: 68px; }
|
||||
.graph-nav--floating-next { right: 16px; }
|
||||
.process-graph:fullscreen .graph-nav--floating { display: flex; }
|
||||
.focus-bar-slot :deep(.focus-bar__col) {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.focus-bar-slot :deep(.focus-bar--edge) {
|
||||
background: var(--edge-focus-soft);
|
||||
|
||||
@@ -125,6 +125,57 @@ export interface NavItem {
|
||||
to?: string;
|
||||
}
|
||||
|
||||
// Эвристика «happy-path»: action-имена, которые уводят на ветку отмены/отказа.
|
||||
// Сюда же относится legacy-поле `actions[].role === 'reject'` (если задано).
|
||||
// Когда из текущего статуса несколько transitions, при выборе следующего шага
|
||||
// штрафуем reject-кандидатов и предпочитаем тех, чей target имеет продолжение.
|
||||
const REJECT_ACTION_KEYWORDS = [
|
||||
'cancel',
|
||||
'decline',
|
||||
'expire',
|
||||
'reject',
|
||||
'abort',
|
||||
'abandon',
|
||||
'fail',
|
||||
'revoke',
|
||||
'discard',
|
||||
];
|
||||
|
||||
function looksLikeRejectAction(actionName: string): boolean {
|
||||
const short = actionShort(actionName).toLowerCase();
|
||||
return REJECT_ACTION_KEYWORDS.some((kw) => short.includes(kw));
|
||||
}
|
||||
|
||||
function pickHappyTransition(
|
||||
outs: Transition[],
|
||||
allTransitions: Transition[],
|
||||
virtualSet: Set<string>,
|
||||
rejectActionNames: Set<string>,
|
||||
): Transition {
|
||||
if (outs.length === 1) return outs[0];
|
||||
const scored = outs.map((t, idx) => {
|
||||
const isReject = rejectActionNames.has(t.action) || looksLikeRejectAction(t.action);
|
||||
// Самопетля (from === to) — внутренний sub-step, не двигает цепочку:
|
||||
// walk-loop сразу остановится на visited.has(to). Штрафуем.
|
||||
const isSelfLoop = t.from === t.to;
|
||||
// Если из target-статуса есть исходящие переходы — путь продолжается,
|
||||
// это аргумент в пользу happy-ветки (отказы обычно сразу терминальны).
|
||||
const targetHasContinuation = allTransitions.some(
|
||||
(next) => next.from === t.to && !virtualSet.has(next.to),
|
||||
);
|
||||
return { t, idx, isReject, isSelfLoop, targetHasContinuation };
|
||||
});
|
||||
scored.sort((a, b) => {
|
||||
if (a.isReject !== b.isReject) return a.isReject ? 1 : -1;
|
||||
if (a.isSelfLoop !== b.isSelfLoop) return a.isSelfLoop ? 1 : -1;
|
||||
if (a.targetHasContinuation !== b.targetHasContinuation) {
|
||||
return a.targetHasContinuation ? -1 : 1;
|
||||
}
|
||||
return a.idx - b.idx;
|
||||
});
|
||||
return scored[0].t;
|
||||
}
|
||||
|
||||
export function computeProcessFlow(standard: Standard): NavItem[] {
|
||||
const items: NavItem[] = [];
|
||||
const transitions = standard.transitions ?? [];
|
||||
@@ -133,6 +184,12 @@ export function computeProcessFlow(standard: Standard): NavItem[] {
|
||||
const hasTerminal = states.some((s) =>
|
||||
isTerminalSuccess(s, transitions, virtualStateNames),
|
||||
);
|
||||
// legacy: actions[].role === 'reject' — явная пометка отказа в манифесте
|
||||
const rejectActionNames = new Set(
|
||||
(standard.actions ?? [])
|
||||
.filter((a) => a.role === 'reject')
|
||||
.map((a) => a.name),
|
||||
);
|
||||
|
||||
const hasStart = transitions.some((t) => t.from === INITIAL_MARKER);
|
||||
let current: string;
|
||||
@@ -152,7 +209,12 @@ export function computeProcessFlow(standard: Standard): NavItem[] {
|
||||
(t) => t.from === current && !virtualStateNames.has(t.to),
|
||||
);
|
||||
if (outs.length === 0) break;
|
||||
const chosen = outs[0];
|
||||
const chosen = pickHappyTransition(
|
||||
outs,
|
||||
transitions,
|
||||
virtualStateNames,
|
||||
rejectActionNames,
|
||||
);
|
||||
const actionNodeId = actionNodeIdFor(chosen);
|
||||
items.push({
|
||||
kind: 'action',
|
||||
|
||||
@@ -11,7 +11,21 @@
|
||||
*/
|
||||
|
||||
// ── Типы-строки для wallet_op / relation / status / role ────────────────────
|
||||
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'WALLET_ONLY';
|
||||
// Полный набор операций ledger2 (см. _blago L1/L2/L3 spec):
|
||||
// ISSUE / TRANSFER / BURN — двигают L2 + опционально L1 + опционально L3
|
||||
// BLOCK / UNBLOCK — только L3 (available ↔ blocked одного кошелька)
|
||||
// REVOKE — целевое расходование L3.blocked, обычно с L1
|
||||
// ACCOUNT_ONLY — только L1 (бухгалтерская проводка без кошельков)
|
||||
// WALLET_ONLY — legacy: L2 без L1
|
||||
export type WalletOp =
|
||||
| 'ISSUE'
|
||||
| 'TRANSFER'
|
||||
| 'BURN'
|
||||
| 'BLOCK'
|
||||
| 'UNBLOCK'
|
||||
| 'REVOKE'
|
||||
| 'ACCOUNT_ONLY'
|
||||
| 'WALLET_ONLY';
|
||||
|
||||
export type StandardLifecycle = 'proposed' | 'approved' | 'active' | 'deprecated';
|
||||
|
||||
@@ -122,19 +136,43 @@ export interface ProcessDocument {
|
||||
note?: string;
|
||||
}
|
||||
|
||||
// §6 Операции
|
||||
// §6 Операции — три уровня учёта (L1/L2/L3)
|
||||
//
|
||||
// L1 — бухгалтерская проводка: debit / credit (счета 50/76/86/…)
|
||||
// L2 — переводы между кошельками кооператива: wallet_from / wallet_to
|
||||
// (агрегированные балансы кошельков ledger2)
|
||||
// L3 — движения по кошельку пайщика (per-user available/blocked split):
|
||||
// user_wallet, user_ref, available_delta, blocked_delta
|
||||
//
|
||||
// Каждая операция указывает только те уровни, которые реально затрагивает.
|
||||
// Для BLOCK/UNBLOCK имеет смысл только L3. Для TRANSFER user-affecting —
|
||||
// L1 + L2 + L3 (один и тот же `amount_ref` отражается на всех уровнях).
|
||||
//
|
||||
// В YAML указываются только коды/id. Человекочитаемые имена — из registries.
|
||||
export interface Ledger2Operation {
|
||||
ledger_code: string; // cap.lnissue
|
||||
human_name: string;
|
||||
wallet_op: WalletOp;
|
||||
wallet_from: WalletId | null; // id кошелька-источника (null — ISSUE/extern)
|
||||
wallet_to: WalletId | null; // id кошелька-приёмника (null — CONSUME)
|
||||
debit: AccountCode | null; // код счёта (51, 80, …)
|
||||
credit: AccountCode | null;
|
||||
amount_ref: string; // debt.amount
|
||||
triggered_by: string; // capital::debtpaycnfrm
|
||||
description?: string;
|
||||
|
||||
// L1 — двойная запись (null — операция не затрагивает бухгалтерию)
|
||||
debit: AccountCode | null;
|
||||
credit: AccountCode | null;
|
||||
|
||||
// L2 — кошельки кооператива (null/'' — операция не двигает L2)
|
||||
wallet_from: WalletId | null;
|
||||
wallet_to: WalletId | null;
|
||||
|
||||
// L3 — кошелёк пайщика (опционально). Указывается, когда операция
|
||||
// затрагивает per-user split available/blocked. Поля независимы от L2 —
|
||||
// user_wallet может совпадать по имени с одним из L2-кошельков (для
|
||||
// USER_SHARED-кошельков типа w.mkt.member) или быть отдельным.
|
||||
user_wallet?: WalletId | null;
|
||||
user_ref?: string; // ссылка на пайщика — order.customer, debt.holder
|
||||
available_delta?: string | null; // выражение по amount_ref: -order.total_cost
|
||||
blocked_delta?: string | null; // +order.total_cost
|
||||
}
|
||||
|
||||
// §7 Связи
|
||||
|
||||
@@ -15,6 +15,14 @@ export default defineConfig(({ command }) => ({
|
||||
'@': path.resolve(rootDir, 'src'),
|
||||
},
|
||||
},
|
||||
// cooptypes — workspace-пакет (~2 МБ ESM-бандла), vite по умолчанию
|
||||
// workspace-deps не пре-бандлит и отдаёт через @fs трансформ-конвейер,
|
||||
// что в dev по сети растягивает chunk до 10 МБ и валит lazy-import
|
||||
// ProcessPage.vue. Принудительный optimizeDeps.include даёт нормальный
|
||||
// pre-bundled chunk, как для vue/vue-router.
|
||||
optimizeDeps: {
|
||||
include: ['cooptypes'],
|
||||
},
|
||||
server: {
|
||||
host: true, // слушать 0.0.0.0 — доступ по IP из сети
|
||||
port: 5173,
|
||||
|
||||
Reference in New Issue
Block a user