Compare commits

...

70 Commits

Author SHA1 Message Date
ant 0f20dc0eb7 Эпик 7 ревью: ownership, fail-fast tx_hash, limits, cleanup
CRITICAL:
- resolver: добавлена ownership-проверка `:own`/`:own-KU` для
  marketplaceReturnClaim и marketplaceListReturnClaimsByBraname —
  matrix capability guard'ом не закрывает data-уровень (matrix явно
  делегирует ownership resolver'у).
- service: extractTxHash → fail-fast ConflictException вместо записи
  фейкового tx_hash `<action>-<claim.id>` в БД при пустом ответе цепи
  (5 мест: submretrn/aprretrem/rejretrem/accretrn/rejretrn). Аналог
  фикса PR #7 для signiss1/signiss2.
- service: reason_text лимит 500 → 2000 (соответствие AC Story 7.1).
- service: warranty_until === null → fail-closed (ConflictException
  «гарантия не предусмотрена»). Раньше null читался как «без проверки»,
  возврат принимался даже на заказы без гарантии.
- frontend SubmitReturnClaimDialog: payload-документ генерируется один
  раз вместе с preview (с reason_text + defect_category), подписывается
  именно показанный snapshot — устраняет hash-mismatch preview ≠ signed.

HIGH:
- service: assertBranameMatchesClaim — backend проверяет совпадение
  input.braname с claim.delivery_braname перед on-chain action.
  Раньше председатель КУ-X мог одобрить заявление с delivery=КУ-Y
  (контракт проверял только is_user_authorized signer для braname,
  не cross-link с return_request).
- service: удалён костыль barcode-валидации
  `.includes(order_id.slice(0,8))` — UUID Order'а никогда не пересекается
  с EAN-13 inventory; проверка ложно отбивала валидные сканы.
  Полноценная сверка с marketplace_inventory — Эпик 5/9.
- service: validatePhotoPayloads — размер фото (10 МБ) валидируется
  в service до bucket.put, не полагаясь только на @UseBucket maxBytes.
- service: cleanupBucketPhotos — orphan-фото удаляются из bucket'а
  при провале on-chain submit'а (submretrn/accretrn/rejretrn).
- adapter listByDeliveryBraname: default без status возвращает ВСЕ
  заявления (включая архив). Раньше default был ACTIVE_STATUSES — это
  делало архивную секцию в operator-столе всегда пустой.

Frontend:
- maxlength 500 → 2000 для reason_text в q-input.

Known limitations (Phase 2):
- chairman_account lookup через `coop_ku.chairman_account` (сейчас
  fallback `chairmanAccount = delivery_braname` в notifications +
  resolver — ownership `:own-KU` приближение).
- Загрузка фото base64-payload одной mutation (AC требует per-file
  upload в bucket → image_registry_id). Реальный лимит body-parser
  упрётся в ~10MB.
- C++ submretrn не сохраняет delivery_braname в return_request →
  cross-branch decision разрешён by design контракта (backend закрывает
  ту же дыру service-level проверкой).
- BarcodeScanner в OnSiteDecisionDialog — mock-режим (UX-DR26 Phase 2).
- BarcodeScanner backend-сверка с marketplace_inventory — Эпик 5/9.
- Уведомление председателю шлётся на delivery_braname account
  (TODO в notification.service.ts) — Phase 2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:40:43 +00:00
ant 9435942b3d Merge pull request 'Эпик 1 ревью: guards membership + MEDIUM/LOW' (#1) from chore/review-E1-onboarding-fixes into marketplace2
Reviewed-on: #1
2026-05-20 12:01:36 +00:00
ant cdbce2b967 Merge pull request 'Эпик 2 ревью: race-guard в геокодинге ПВЗ + фикс flaky теста' (#2) from chore/review-E2-pvz-fixes into marketplace2
Reviewed-on: #2
2026-05-20 11:59:54 +00:00
ant 1720f8667e [598-5][@ant] fix(marketplace): ревью Эпика 2 — race-guard в геокодинге + flaky тест 2026-05-20 11:33:16 +00:00
ant a61c5ce076 [598-4][@ant] fix(marketplace): ревью Эпика 1 — security/membership + MEDIUM/LOW 2026-05-20 11:10:22 +00:00
Alex Ant 4877c29274 chore(marketplace2): sync с origin/dev + DI-фиксы controller для запуска dev-стека (#411)
* chore(release): publish

* chore(release): publish

* ci: атомарный release.yaml, убрать workflow_run-связку (#366)

build-contracts + build-containers через workflow_run упёрлись в:
(а) default-branch caveat (новая логика не активна, пока не в main),
(б) `${{ github.event.workflow_run.head_sha }}` иногда пуст в YAML-
выражениях — описание см. в шаге Resolve tag, инцидент v2026.5.14
не дёрнул PRODUCTION_WEBHOOK_URL.

Замена — один `release.yaml` на push тэга `v*`: резолвит ветку через
`git branch --contains`, собирает контракты → пушит
`dicoop/contracts:<branch>`, собирает базу + сервисные образы →
пушит `dicoop/<svc>:<tag>`, шлёт webhook. Гонок нет by construction.

`build-contracts.yaml` остаётся только на push веток для CI-
обновления `dicoop/contracts:dev|testnet|main` без релизного тэга.
Триггер `tags: ['*']` и логика резолва ветки через --contains
оттуда удалены — это теперь забота release.yaml.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* chore(release): publish

* chore(release): publish

* ci: tag-only триггеры для build-contracts и docs (#367)

build-contracts.yaml — только workflow_dispatch (ручная пересборка
`dicoop/contracts:<branch>` для отладки на dev-ноде). Тэги обрабатывает
release.yaml атомарно (контракты + контейнеры + webhook), отдельная
сборка по push'у в ветку только давала вторую параллельную сборку.

publish-docs.yaml и build-contracts-docs.yaml — на push:tags v* с
gate-job'ом, пропускающим только продакшн-тэги (без -alpha/-beta/-rc/
-test) на main. Раньше docs пересобирались на каждый push в
main/testnet/dev/reports/marketplace2 — впустую.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix(capital/commit): nullable description/meta в CommitOutputDTO + не дефолтить satisfaction в 5

Если syncCommit не дождался delta из блокчейна, interactor возвращает DB-only
entity, где description/meta остаются undefined — non-nullable GraphQL field
ломал ответ мутации capitalCreateCommit. Сделал оба поля nullable.

UI CreateCommitButton.vue: satisfaction_stars=0 по умолчанию, label "не указано"
пока пользователь не выбрал; блок contribution_feedback в payload только если
stars >= 1 или review_text непустой.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix commit process

* chore(release): publish

* fix(file-storage): MinIO стартует только при заданном MINIO_ENDPOINT

Раньше MINIO_ENDPOINT имел default http://minio:9000, и на проде без
minio-контейнера контроллер падал на bootstrap в HeadBucket с
getaddrinfo ENOTFOUND minio — Nest application не поднимался вообще.

Теперь MINIO_ENDPOINT optional без default; адаптер хранит enabled-флаг
по наличию endpoint и при отсутствии — onApplicationBootstrap логирует
warning и возвращается без сетевых вызовов. Любая попытка getBucket /
fetchObjectForReadProxy кидает InterFileStorageBackendUnavailableError
с понятным сообщением.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): publish

* chore(release): publish

* fix(capital/time-tracking): личные доли estimate + revert при decline (#387)

* chore(release): publish

* chore(release): publish

* ci: атомарный release.yaml, убрать workflow_run-связку (#366)

build-contracts + build-containers через workflow_run упёрлись в:
(а) default-branch caveat (новая логика не активна, пока не в main),
(б) `${{ github.event.workflow_run.head_sha }}` иногда пуст в YAML-
выражениях — описание см. в шаге Resolve tag, инцидент v2026.5.14
не дёрнул PRODUCTION_WEBHOOK_URL.

Замена — один `release.yaml` на push тэга `v*`: резолвит ветку через
`git branch --contains`, собирает контракты → пушит
`dicoop/contracts:<branch>`, собирает базу + сервисные образы →
пушит `dicoop/<svc>:<tag>`, шлёт webhook. Гонок нет by construction.

`build-contracts.yaml` остаётся только на push веток для CI-
обновления `dicoop/contracts:dev|testnet|main` без релизного тэга.
Триггер `tags: ['*']` и логика резолва ветки через --contains
оттуда удалены — это теперь забота release.yaml.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* chore(release): publish

* chore(release): publish

* ci: tag-only триггеры для build-contracts и docs (#367)

build-contracts.yaml — только workflow_dispatch (ручная пересборка
`dicoop/contracts:<branch>` для отладки на dev-ноде). Тэги обрабатывает
release.yaml атомарно (контракты + контейнеры + webhook), отдельная
сборка по push'у в ветку только давала вторую параллельную сборку.

publish-docs.yaml и build-contracts-docs.yaml — на push:tags v* с
gate-job'ом, пропускающим только продакшн-тэги (без -alpha/-beta/-rc/
-test) на main. Раньше docs пересобирались на каждый push в
main/testnet/dev/reports/marketplace2 — впустую.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix(capital/time-tracking): личные доли estimate, partial-split, revert при decline

Три бага в распределении билетов времени, вскрытые на прод-инциденте voskhod
(проект CC7-1 «Концепция», estimate=15 ч, 3 creators):

БАГ #1 — recalcDoneEstimatesForContributorProject раздавал «общий остаток пула»
(estimate − committed_total) / N всем creators поровну. Закоммитивший свою долю
получал её ещё раз, остальные — урезанную (15/3=5 → после committed 5 у одного
становилось 10/3=3.33 у каждого, включая того кто уже закоммитил).

Фикс: личная доля = estimate/N − собственный committed estimate. Введён общий
helper redistributeIssueEstimateEntries, который используют и applyExplicit-
EstimateToTimeEntries (force=true), и recalcDoneEstimates (force=false с
no-op оптимизацией если раскладка уже совпадает с планом).

БАГ #2 — commitTime при partial split (entry.hours > requested) создавал новую
committed-запись без entry_type и estimate_snapshot. По default'у БД сохраняла
её как entry_type='hourly', что ломало последующий recalc (он фильтрует только
entry_type='estimate'). Фикс: явно копировать entry_type и estimate_snapshot
из оригинального entry.

БАГ #3 — declineCommit / handleDeclineCommit меняли только commit.status в БД,
но не возвращали time-entries в is_committed=false. После отказа мастера часы
оставались в total_committed_hours и не возвращались в available_hours. Фикс:
новый метод revertEntriesForDeclinedCommit в TimeTrackingInteractor + методы
findCommittedByCommitHash / revertCommittedEntriesByCommitHash в TimeEntry-
Repository. После revert делается force=true redistribute для затронутых DONE
задач, чтобы доли вернулись в норму.

Покрытие тестами: 17 unit-тестов в time-tracking.interactor.spec.ts с
регрессионными сценариями под каждый из трёх багов плюс integration-сценарий
полного lifecycle CC7-1 (estimate → коммит → decline → revert).

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix commit process

* chore(release): publish

* chore(release): publish

* feat(controller): is_server_init флаг в initSystem для provider-overwrite (#391)

* feat(docs-harness): onboarding 01..06 — визуальная цепочка регистрация → активация

Шесть сценариев visual docs-harness, покрывающих полный путь подключения
нового кооператива через провайдера Восход:

  01-register-coop                 — регистрация кооператива-клиента
  02-sign-and-submit               — подпись заявления о вступлении + PayInitial
  03-operator-approve              — chairman принимает заявку в реестре одобрений
  04-sign-connection-agreement     — Партнёр-1 видит ConnectionAgreementStepper
                                     (на текущем стенде получаем заглушку
                                     /signup, пока пайщик не принят)
  05-activate-from-registry        — оператор открывает карточку инстанса
                                     в provider-frontend, выбирает preset
  06-wait-instance-active          — pending → ACTIVE (overrideInstance
                                     для имитации финального статуса в шоте)

Обвязка:
  • lib/registrator-signup.mjs — переиспользуемый helper подписания.
  • lib/harness.mjs — расширенный dismissOnboardingDialogs (Положение ЦПП
    и связанные модалки chairman'а).
  • state/cooperatives/{,.gitkeep} — папка для фикстур; partner1.json
    (с приватным wif) игнорируется (.gitignore обновлён).

ОГРАНИЧЕНИЕ: в 06 финальный ACTIVE — это playwright-override JSON, а не
реальный POST /instances/activate с боевым Ansible до testnet300.coopenomics.world.
Реальный E2E (аренда VM на Hostkey + поднятие кооператива на домене)
будет следующим шагом Эпика 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(controller): is_server_init flag в initSystem для разблокировки provider-overwrite

Поле data.is_server_init: boolean (optional) в InitDTO + домейн-интерфейсе.
Если true (вызов от провайдера через server-secret) — coopback ставит
init_by_server=true безусловно, даже если до этого пользователь успел
заполнить визард первым (user-init). Это разблокирует ситуацию, когда
провайдер не успел вызвать initSystem до того как chairman открыл
/install — следующий callInitSystemMutation от провайдера перезапишет
organization_data и пометит её readonly для визарда.

Без флага сохраняется прежняя логика: первая инициализация — серверная,
повторная наследует флаг.

Инцидент 2026-05-18 на partner1: при первой установке provider вообще
не успел/не сходил в callInitSystemMutation, визард пользователя
проинициализировал систему как user-init (init_by_server=false), визард
2-го захода не предзаполнил форму. С этим фиксом следующий вызов
provider'а поднимет флаг и фикстура встанет на место.

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(capital/convert): унифицировать шаблон 1080, убрать 1081/1082 (#392)

* refactor(capital/convert): унифицировать 1080 как универсальное заявление о конвертации, убрать 1081/1082

Шаблон 1080 (GenerationConvertStatement) уже технически универсален — содержит
обе суммы (main_wallet_amount/blagorost_wallet_amount) и условные блоки в
context. Шаблоны 1081 (GenerationToProjectConvertStatement) и 1082
(GenerationToCapitalizationConvertStatement) были недоделанными заглушками
без полей и нигде не подключены в UI.

Изменения:
- cooptypes: 1080 переименован GenerationToMainWalletConvertStatement →
  GenerationConvertStatement; title/description нейтральные. 1081/1082 удалены.
- factory: Template + Action 1080 переименованы; в Action добавлено
  super.formatAsset(...) для обеих сумм (как в Action 1020). 1081/1082 удалены.
- controller: DTO переименован; appendix_hash убран из generate-input и
  перенесён в signed-meta-input; добавлен enrich appendix_hash через
  AppendixRepository.findConfirmedByUsernameAndProjectHash в
  DistributionManagementInteractor.prepareGenerationConvertStatementData
  (по образцу InvestsManagementInteractor для 1020). Резолвер мутации
  переименован в capitalGenerateGenerationConvertStatement; убраны два
  резолвера 1081/1082. mutation-log-mapper обновлён.
- sdk: мутация переименована, две удалены, zeus regenerated.
- desktop: Distribution-фичи 1081/1082 удалены, 1080-фича переименована.
  ConvertSegment теперь шлёт project_hash + обе суммы (formatToEosioAsset) +
  to_wallet/to_blagorost; appendix_hash подтягивается на бекенде.
- controller schema.gql regenerated.

registry_id=1080 не меняется — on-chain контракт convertsegm не сверяет
registry_id, миграций БД/контракта не требуется.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(capital/convert): применить ревью — title «трансляция паевого взноса из программы Генерация»

По комментарию ревью в PR #392 (строка 37): принять доменный термин
«трансляция паевого взноса» (перенос между программами) вместо
«конвертация»; description согласован в том же стиле.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(capital): подтянуть 3 ссылки на GenerationConvertStatement → MoneyInvestStatement (#394)

PR #392 переименовал Cooperative.Registry.GenerationConvertStatement в
GenerationMoneyInvestStatement в @coopenomics/document, но в controller
осталось 3 несинхронизированные ссылки:

- distribution-management.service.ts:56 — registry_id метода generation
- distribution-management.interactor.ts:48,66 — тип Action в return и as-cast
- generation-convert-statement-document.dto.ts:12 — type action в DTO

Coopback падал на ts-node compile (TS2551/2724), что блокировало старт
всего dev-stack после reboot dev-chain 2026-05-18.

Co-authored-by: coopops <coopos@coopenomics.world>

* fix(controller): IS_UNIONED zod-парсер принимает string из .env (#395)

z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).

Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.

Co-authored-by: coopops <coopos@coopenomics.world>

* fix(capital): revert ошибочной замены GenerationConvertStatement → MoneyInvestStatement (#394) (#396)

PR #394 «починил» TS-ошибки coopback заменой типа `Cooperative.Registry.GenerationConvertStatement`
на `GenerationMoneyInvestStatement` в 3 файлах controller'а. Это семантически неверно:

- 1080 GenerationConvertStatement — заявление о трансляции паевого взноса
  (поля: project_hash, main_wallet_amount, blagorost_wallet_amount, to_wallet, to_blagorost, appendix_hash)
- 1020 GenerationMoneyInvestStatement — заявление о денежном паевом взносе по программе Генерация
  (совсем другой набор полей)

DTO BaseGenerationConvertStatementMetaDocumentInputDTO декларирует поля 1080, но `implements ExcludeCommonProps<action>`
где action = тип 1020 → TS2352 на as-cast в interactor, потому что Action'ы не пересекаются по полям.

Реальная причина исходных ошибок coopback после #392 — несвежий dist `@coopenomics/cooptypes`
на dev-узле (старое имя символа). Лечится пересборкой пакета, не переименованием ссылок.

Изменено (откат #394):
- generation-convert-statement-document.dto.ts:12 — `action = ...GenerationConvertStatement.Action`
- distribution-management.service.ts:56 — `registry_id: ...GenerationConvertStatement.registry_id`
- distribution-management.interactor.ts:48,66 — `Promise<...GenerationConvertStatement.Action>`

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix(capital/convertsegm): регистрировать convert_statement в реестре документов как completed (#403)

Финальная фаза процесса p.cap.rid (convertsegm) принимала document2 convert_statement (шаблон 1080)
параметром, но:

1. **Не проверяла подпись** — `verify_document_or_fail` отсутствовала, поэтому on-chain
   принял бы любую сконструированную document2 без валидной user-подписи.
   У signact1/signact2 (соседние фазы того же процесса) verify есть — здесь забыли.

2. **Не регистрировала документ в реестре** — `newlink`/`newsubmitted`/`newresolved`
   не вызывался, заявление о трансляции 1080 «терялось»: off-chain controller (process_instance)
   не видел финальный документ привязанным к result_hash, процесс p.cap.rid не помечался completed.
   У pushrslt (create_approval) и signact2 (newlink с SIGN_ACT2_RESULT) линковка есть.

Канон есть в soviet/src/system/converttoaxn.cpp:54 и soviet/src/agreement/sndagreement.cpp:104:
паттерн `Soviet::make_complete_document(calling_contract, coopname, username, action, package_hash, document)`
шлёт newsubmitted + newresolved одной парой, package = анкер процесса (здесь result_hash).

Изменено:
- names.hpp: новая константа Names::Capital::CONVERT_SEGMENT = "convertsegm"_n (12 символов)
- convertsegm.cpp:
  - verify_document_or_fail(convert_statement, {username}) сразу после require_auth
  - Soviet::make_complete_document(...) ДО delete_result (чтобы линковка прошла, пока result_hash ещё анкер)

Контракт capital собирается без ошибок (cdt-cpp testnet mode), warnings — старые ricardian.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* feat(epic-0): onboarding harness 01-08 + install bundle adduser+createBoard в одну tx (#398)

* fix(controller): IS_UNIONED zod-парсер принимает string из .env

z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).

Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.

* feat(epic-0): partner onboarding harness — signin → sign agreements → connect

Что добавлено:
- desktop/quasar.config.cjs: vite server.allowedHosts для voskhod-dev/
  partner-dev/api-dev (Vite 5.4+ блокирует cross-origin Host без явного
  списка — иначе SSR/HMR-сервер возвращает 403 «Blocked request»).
- docs-harness/lib/harness.mjs: helper signOnboardingAgreements —
  реальная подпись каскада SignAgreementDialog (wallet/signature/
  privacy/user), каждый клик отправляет sendAgreement → wallet::signagree
  on-chain. В отличие от dismissOnboardingDialogs делает on-chain эффект
  (см. inc 2026-05-18: stale vault SERVER_SECRET → bad decrypt).
- scenarios/onboarding/01..07: новый partner-flow Эпика 0 — partner1
  заходит в Восход, подписывает 4 типовых соглашения, идёт на «Подключение»,
  ant одобряет, наблюдаем установку partner-dev.
- scenarios/registration/01..04: архив старого registration-doc как
  отдельная история (см. mem feedback_provider_mono_pr_flow).
- scripts/debug-*: вспомогательные скрипты для портал-структуры и
  chairman onboarding'а.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(docs-harness/08): chairman install wizard на partner-dev

Сценарий 08 проходит установочный wizard /:coopname/install целиком:
шаг 1 (RequestKeyForm: WIF partner1 из state/cooperatives/partner1.json),
шаг 2 (SetInitForm: readonly orgdata из is_server_init=true, «Далее»),
шаг 3 (SetSovietForm: один председатель — Иванов И.И.),
шаг 4 (SetVariablesForm: ОПФ+ во всех падежах, устав, конф.email).

Финальный submit «Завершить установку» сейчас падает on-chain в
soviet::create — `assertion: Один из аккаунтов не найден в реестре
пайщиков`. Причина: в install.interactor.ts adduser и createBoard
шлются двумя отдельными tx; partner1 nodeos не producer, между tx есть
лаг p2p-репликации, createBoard приходит до того как participants[N]
обновился. Шот 06-error-state снимается при таймауте; шот
06-completed появится после фикса race в install.interactor (отдельный
коммит).

Сценарий запускается:

  BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
  node run.mjs onboarding/08-chairman-install-on-partner-dev

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(controller/install): bundle adduser+createBoard в одну tx

Cause. install.interactor.ts шлёт adduser×N и createBoard двумя отдельными
tx через BlockchainService. На production-нодах с producer-схемой это
работает (lag реплицирования минимальный), но на dev-loop'е partner-
coopback соединён со своим nodeos, который p2p-репликой подтягивает
блоки от producer'а — после accept'а adduser в local state ещё нет
soviet::participants[username] к моменту push'а createBoard. Контракт
soviet::createboard падает «Один из аккаунтов не найден в реестре
пайщиков».

Fix. Объединил все adduser-action'ы и createBoard-action в одну tx
через новый метод BlockchainPort.installSoviet(). Обе action'ы теперь
атомарны в одном блоке — soviet::addpartcpnt (inline action от
adduser) обновляет participants и createBoard видит запись сразу же
в том же блоке.

Поток в install.interactor.ts перестроен в два шага:
  1. Цикл по soviet: createUser в БД + setupNotificationSubscriber +
     сбор addUserActions[] и members[] (без on-chain активности).
  2. installSoviet(addUserActions, createBoardData) — одна tx.

Catch при ошибке on-chain (как и раньше) откатывает users из БД.

Зачем. Закрывает блокер сценария 08-chairman-install-on-partner-dev:
без этого финальный экран wizard'а «Установка завершена» недостижим
на dev-loop'е (Эпик 0 не закрывается).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(docs-harness/09): invite-token → WIF → signin chairman

Закрывает Эпик 0: после сценария 08 (chairman install wizard) на
partner1 в Postgres появляется invite-токен председателя, но Novu не
доставит его на @example.com адрес. Сценарий 09 идёт за токеном
напрямую в БД через `ssh partner1 → docker exec postgres → psql` и
прогоняет до финального signin под новым ключом.

Шаги:
1. fetchLatestInviteToken — SELECT token FROM tokens WHERE
   type='invite' AND blacklisted=false AND expires > NOW() LIMIT 1.
2. Открываем `${BASE_URL}/${COOPNAME}/auth/invite?token=<token>` —
   widget Invite.vue клиентски генерирует новый WIF (generateAccount).
3. Извлекаем WIF из q-input, сохраняем в
   state/cooperatives/partner1-chairman.json для последующих сценариев.
4. Чекбокс «Я сохранил ключ» → «Установить ключ» → resetKey шлёт
   on-chain ChangeKey, фронт редиректит на /auth/signin.
5. Финальный signin под chairman.partner1@example.com + новый WIF;
   ждём перехода на /chairman или /participant.

Запускается:

  BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
    node run.mjs onboarding/09-chairman-key-and-login

Требует SSH-доступа к partner1 (PARTNER_SSH=user1@91.218.246.46 по
умолчанию) и предварительно прогнанный сценарий 08 (после merge
fix(controller/install) — иначе токен не появится).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(controller/install): откатить installSoviet bundle на sleep 2s

Bundle adduser×N + createBoard в одну tx работает, но требует расширения
BlockchainPort и больше read'а; для не-producer-нод (dev-loop) достаточно
короткой паузы между adduser и createBoard, чтобы p2p-реплика подтянула
блок с participants. 2с гарантированно перекрывают и prod (~50ms), и
dev-loop (1-3с).

Отменён 3f2ecc42cf (installSoviet в blockchain.port + blockchain.service +
install.interactor), добавлен `await new Promise(setTimeout, 2000)` между
циклом adduser и createBoard.

Это однострочный фикс race-condition; bundle вернётся когда понадобится
многошаговая атомарность.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: coopops <coopops@coopenomics.world>

* fix(asset-formatting): truncate (toward-zero) до 2 знаков вместо math-round (#404)

UI показывал баланс 99999.9999 RUB как 100 000,00 — пользователь видел
больше, чем реально на кошельке, и попытка инвестировать «весь баланс»
падала в walletop TRANSFER с insufficient funds (precision=4 на цепи
vs precision=2 в UI).

Принцип: «никогда не показывать сумму больше реальной». Truncate
выполняется на уровне строки, без преобразования в Number, чтобы
избежать FP-погрешности (0.29 * 100 = 28.999... в IEEE 754).

Изменения:
- desktop: новая утилита floorDecimalString — единый источник истины.
- desktop: formatAsset2Digits / formatToAsset / addAssets используют floor.
- desktop: убран double-formatting amount.toFixed(4) перед formatAsset2Digits
  в ParticipantWalletsPage.
- factory: Factory.formatAsset / Factory.formatShare через приватный
  truncateDecimal — тот же принцип в документах (заявления, акты).

Не затронуто (отдельный фолоу-ап): 10 файлов с inline .toFixed(4)
перед отправкой в контракт — рекомендую заменить на formatToAsset().

Co-authored-by: coopops <coopos@coopenomics.world>

* feat(capital): требовать допуск для просмотра артефактов проекта/компонента (#407)

Любой авторизованный пользователь раньше мог запросить capitalStories /
capitalStory и получить требования всех проектов кооператива. Закрываем эту
лазейку на бекенде и показываем аккуратную заглушку на фронте.

Backend (controller):
- GenerationService.getStories: фильтрует список разрешённых project_hash
  через PermissionsService. Допуск к корневому проекту каскадно открывает
  все его компоненты; допуск к одному компоненту — только этот компонент.
  Запрос без project_hash/issue_hash доступен только chairman/member.
- GenerationService.getStoryByHash: проверяет допуск через project_hash
  (или issue.project_hash для story с issue_hash) и возвращает null без
  доступа.
- getCapitalStories / getCapitalStory резолверы теперь получают currentUser.
- ProjectPermissionsOutputDTO: добавлены has_parent_clearance и
  can_view_artifacts — для отображения заглушек на фронте.

Frontend (desktop):
- ProjectRequirementsPage / ComponentRequirementsPage: показывают
  ArtifactsAccessPlaceholder когда can_view_artifacts=false, с кнопкой
  получения допуска (или PendingClearanceButton при поданном запросе).
- Новый shared компонент ArtifactsAccessPlaceholder в стиле остальных
  заглушек расширения capital.

SDK:
- projectSelector обновлён под новые поля; schema.gql / zeus
  регенерированы.

Co-authored-by: coopops <coopos@coopenomics.world>

* fix(capital/convertsegm): линковать 1080 к пакету через newlink, не make_complete_document (#409)

PR #403 использовал Soviet::make_complete_document — это вытащило заявление о трансляции (1080)
на верхний уровень реестра как самостоятельный документ. В разделе «Документы» 1080 оказался
отдельной строкой, под ним болтались протокол+акт из соседнего пакета (агрегатор подтягивал их
по тому же result_hash без главного документа).

Правильно — линковать 1080 к существующему пакету процесса p.cap.rid, где ведущим документом
является заявление о внесении РИД (1040, statement из pushrslt). Канон уже есть в signact1/signact2:

  Action::send<newlink_interface>(_soviet, "newlink"_n, _capital, coopname, username,
                                  Names::Capital::SIGN_ACT*_RESULT, result_hash, act);

Здесь по тому же паттерну, action = Names::Capital::CONVERT_SEGMENT (константа уже в dev из #403,
её не трогаем), package = result_hash. Off-chain controller добавит 1080 в группу пакета 1040,
а не на верхний уровень.

verify_document_or_fail оставлен — это безопасность, не связана с проблемой реестра.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* migration complete

* [598-14][@ant] fix(desktop): vue-tsc на marketplace2 после merge'ов

— OperatorInventoryLabelingPage: BarcodeStrategy/Format через Zeus.MarketplaceBarcode*
— OperatorOwnWarehousePage: pug-tернарник в одну строку (vue-tsc не парсит мультиline | text-block)
— AdminWriteoffsPage: statuses[] через Zeus.MarketplaceWriteoffProposalStatus, trigger === CRON
— SubmitToCouncilDialog: useSessionStore вместо удалённого useStore
— ProcessesPage: ProcessDocument → { hash, source.{code,table,...} } + sortOrder в PaginationInput

* [598-14][@ant] fix(controller): DI marketplace модулей чтобы controller стартовал на marketplace2

— application/marketplace-application.module: импортирует MarketplaceInfrastructureModule (резолверы инжектят MARKETPLACE_*_REPOSITORY) и AccountInfrastructureModule (MarketplaceNotificationService требует ACCOUNT_DATA_PORT). Импорт ExtensionDomainModule удалён — порождал циркуляр AppModule → ExtensionDomainModule → ExtensionsModule → MarketplacePluginModule → MarketplaceExtensionApplicationModule, из-за чего Nest падал с "module at index [6] is undefined" сразу после merge Эпика 8.
— marketplace-writeoff-cron.service: ExtensionDomainService инжектится через @Optional() с null-fallback (cron сам читает env при отсутствии extension config). Это убирает необходимость в проблемном импорте ExtensionDomainModule.
— infrastructure/marketplace-infrastructure.module: импорт VaultDomainModule, потому что MarketplaceCanonicalBlockchainAdapter инжектит VAULT_DOMAIN_SERVICE.

Также добавлен MINIO_ENDPOINT в controller/.env (Эпик 7 / Story 7.1 stol-zakazov:images bucket теперь корректно поднимается на dev).

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: coopops <coopops@coopenomics.world>
Co-authored-by: ant <ant@noreply.local>
2026-05-19 21:32:16 +05:00
Alex Ant 58652e5fae Эпик 11 (тех.долги): FR45 AC5 jest-тесты + 598-21 frontend batch-flow (#408)
Publish Docs / build-and-publish-docs (push) Failing after 11m42s
* [598-14][@ant] feat: дозакрытие техдолгов Эпика 11 — FR45 AC5 jest-тесты + 598-21 frontend batch-flow

— FR45 AC5 (598-15): jest unit-тесты compensating-rollback signSupp/signChair
  с mock chainPort failure → status не сдвинут + idempotent retry.
  6 тестов в marketplace-apl-reception.service.spec.ts (76 сек локально).
— 598-21 frontend batch-flow в OperatorInventoryLabelingPage:
  тогл «Маркировка партии целиком / Поштучно по заказу», выбор default
  стратегии и формата, селектор партии из marketplaceListShipments,
  идемпотентность через skipped_order_ids в toast, A4 print grid в
  @media print, page-break-inside: avoid на этикетку.
— SDK добавлена mutation marketplaceLabelShipmentInventory + селектор
  результата (labeled_order_ids / skipped_order_ids); zeus regen из
  controller schema.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-14][@ant] fix: добавить _validate typeguard на marketplaceLabelShipmentInventoryResultSelector

Селектор должен проходить compile-time проверку MakeAllFieldsRequired<ValueTypes[...]>
как все остальные селекторы в проекте; добивает review-комментарий PR #408.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 13:15:05 +05:00
Alex Ant a1513127d1 Эпик 11 «Контрактный аудит и регуляторика релиза» MVP Стола заказов (598-14) (#405)
* Эпик 11: «Реестр процессов» на столе бухгалтера (598-14)

UI-надстройка над уже работающим Phase A/B ProcessRegistry в core controller:
страница /reports/processes показывает листинг процессов ledger2 с фильтрами
по типу процесса (LEDGER2_PROCESS_REGISTRY) и пайщику, при раскрытии строки
рисует events + documents процесса (через getProcess), отдаёт слот
processInfoFactory для бизнес-описания (marketplace handlers уже регистрируют
SUPPLY/RETURN/WRITEOFF в extensions/market/app/extensions.ts) и кнопку
deep-link «Открыть в реестре операций» (filter process_hash).

Используется existing SDK query `Queries.Processes.ListProcesses` —
расширены entities/Process api+store+types методом `listProcesses` и
type alias'ами IProcessListInput/IProcessListResult/IProcessSummary.

Стол бухгалтера приобретает четвёртый базовый реестр (операции, проводки,
**процессы**, кошельки, счета) — закрывает аудит-видимость marketplace
flow «Стола заказов» в рамках Эпика 11.

* Эпик 11 техдолг 598-21: batch-маркировка партии за один вызов

Mutation `marketplaceLabelShipmentInventory(shipment_id, default_strategy?,
per_order_overrides?)` массово маркирует все Order'ы партии поставки —
оператор перестаёт проходить N карточек по одной.

Service `MarketplaceInventoryLabelService.labelShipment`:
  1. Shipment.findById → берёт coopname/cycle_id/braname.
  2. orderRepo.findByCycleId(coopname, cycle_id) → filter по
     delivery_braname партии = все Order'ы партии.
  3. Для каждого Order'а зовёт уже работающий execute() — единый
     источник правил маркировки (стратегия из Offer.barcode_strategy
     по 598-22, per-Order override для смешанных Offer'ов в партии).
  4. Идемпотентность: ConflictException от execute() при повторной
     маркировке → skipped_order_ids в результате (повторный заход на
     страницу не валится fatal'ом).

Возврат включает labeled_order_ids + skipped_order_ids + сами наклейки
(`MarketplaceInventoryItem[]`) — UI оператора может одним вызовом
получить grid этикеток для печати и понимать что было сделано.

`@media print` grid layout и расширение `OperatorInventoryLabelingPage`
под выбор default-стратегии на всю партию — следующий шаг, mutation
теперь доступна.

* Эпик 11 Stories 11.2 + 11.3: off-chain инварианты ledger2 marketplace + coverage трассировки 13 op-кодов

Backend-side scaffold контрактных проверок Stories 11.2 (Journal/wjournal
трассировка) + 11.3 (CI-инварианты ledger2). Реализация — pure-функциями
без NestJS/PG: работают на синтетических массивах Ledger2OperationDTO,
гонятся unit-тестом без реального chain'а (отвечает требованию off-chain
агрегации: никаких сумм по всем кошелькам пайщиков в смарт-контракте).

Состав:

- `marketplace-ledger2-invariants.ts` — 6 pure-агрегаторов (I1 payout
  balance, I2 Δ86, I3 acc.10, I4 acc.91 transit = 0, I5 blocked в
  w.mkt.member, I6 нет orphan o.mkt.block). Та же логика что UI стола
  бухгалтера (`/reports/operations`, `/reports/postings`).
- `marketplace-ledger2-invariants.spec.ts` — 86 тестов: happy path +
  violation path для каждого I1..I6 + property-based на 50 случайных
  последовательностях (mix happy/cancel/return/writeoff).
- `marketplace-process-trace-coverage.spec.ts` — 10 групп: coverage по
  всем 13 marketplace operation_code (12 o.mkt.* + o.wal.conv) в
  cooptypes + OPERATION_CODE_TO_PROCESS_TYPE + PROCESS_HASH_LOCATOR;
  Phase-A якорь связности apply+walletop+debit+credit под одним
  process_hash; full-flow scenarios для supply/return/wroff.

Контрактные тесты в Antelope simulator (mono-ai-5) остаются за другим
агентом; здесь — backend-side mapping контроль и off-chain агрегация
инвариантов, доступные для CI и admin-вью.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 10:23:10 +05:00
Alex Ant 8326ddeb4e Эпик 9 «Склад и отчётность кооператива» MVP Стола заказов (598-12) (#400)
Publish Docs / build-and-publish-docs (push) Failing after 11m38s
* [598-12][@ant] chore: process-hash-locator Phase B готов к активации marketplace

Эпик 9 / Story 9.3: обновлён комментарий к ключам p.mkt.supply / p.mkt.return /
p.mkt.wroff. Phase A через OPERATION_CODE_TO_PROCESS_TYPE уже связывает все
o.mkt.* с тремя process_type из cooptypes; Phase B ждёт появления entity-
таблицы marketplace::requests с полем request_hash (Locked Decision L10).
Заглушка [] остаётся корректной — Phase B наполняется одной правкой при
готовности C++.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: operator-стол «Склад моего КУ» — Story 9.1

Лента marketplace_inventory отфильтрована по braname текущего КУ;
per-row статусы (LABELED / ISSUED / RETURNED / WRITTEN_OFF) через
mp-status-chip, фильтры по статусу/orderer, summary count by status
(activeOnly + writtenOff). Сортировка по labeled_at DESC по умолчанию.

Backend-доступ ограничивается guard'ом Warehouse: ['read:own-KU'] —
оператор видит только свой участок. Real-time подписки AR37 SDK
платформы подключаются отдельным шагом эпика (на MVP — refresh кнопкой).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: admin-стол «Сводный склад» через WarehouseSummaryGrid — Story 9.2

Две вкладки admin-стола (UX-DR16 + DR31, mp-role-admin):
- «Сводный склад» — агрегация marketplace_inventory по ku × sku через
  canon-виджет WarehouseSummaryGrid (incoming = LABELED + RETURNED, outgoing
  = ISSUED, balance — транзитный остаток без ISSUED);
- «Поток заказов и поставок» — sums маркировок / выдач / возвратов /
  списаний + топ-10 SKU по обороту. Графики динамики (orders_per_day,
  supplies_per_day, writeoffs_per_month) подключаются по AR37 SDK-
  подпискам платформы отдельным шагом.

Backend-доступ Warehouse: ['read:all'] (chairman + member).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: SDK обёртки processes + страница «История операций» — Story 9.5

SDK:
- selectors/processes/processViewSelector + processSummarySelector
  (полное покрытие ProcessView / ProcessSummary / Pagination)
- queries/processes/getProcess + listProcesses
- queries/index.ts: export * as Processes

Desktop pages/Marketplace/OperationsHistory:
- лента всех процессов с фильтрами по coopname / processType / username;
- разворот через стандартный core processRegistry.getProcess(process_hash)
  показывает actions, delta_history и связанные documents;
- кнопка «Скачать JSON» для аудиторской выгрузки конкретного процесса
  (AR36, L13 pull-модель).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: UI-stub «Экосистема кооперативов» — Story 9.4

Раздел admin-стола под межкооперативную торговлю Phase 2 (NFR-Sc2 / AR18).
На MVP — заглушка с информацией о текущем кооперативе и явным сообщением
что список других controller'ов с marketplace появится после подключения
платформенного ecosystem_registry. Backend-регистрация controller через
core-API в отдельном Эпике 9 + техдолге platform (см. issue эпика).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: маршруты market workspace под Эпик 9

Регистрирует 4 новые страницы Эпика 9 в desktop/extensions/market/install:
- market-pvz/warehouse — Story 9.1 «Склад моего КУ» (operator-стол КУ);
- market/warehouse-summary — Story 9.2 «Сводный склад» (admin-стол);
- market/history — Story 9.5 «История операций» (admin-стол);
- market/ecosystem — Story 9.4 «Экосистема» (admin-стол, MVP-stub).

Доступы соответствуют access-matrix marketplace: warehouse-стол КУ —
только chairman; сводный склад / история / экосистема — chairman +
member (board_readonly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] chore: регенерация schema.gql / zeus / SDK Zeus + fix DTO writeoff

Source-of-truth GraphQL-схема была устаревшая после Эпика 8 — DTO
`MarketplaceWriteoffProposalItemDTO` использовал `?: string | null` без
explicit-`@Field(() => String)`, и Nest GraphQL падал при
`generate-schema` ⇒ Zeus в SDK не получил writeoff queries и Processes
из Story 9.5.

- DTO: пять nullable string-полей writeoff'а получили `@Field(() => String, { nullable: true })`.
- schema.gql регенерирован (`pnpm run generate-schema`).
- zeus обновлён в controller/zeus и sdk/src/zeus (`pnpm run generate-client`).
- В Zeus теперь видны: ProcessView / ProcessSummary / ProcessesFilter,
  MarketplaceWriteoffProposal / PaginatedMarketplaceWriteoffProposals и т.п.

После этого `cd components/sdk && pnpm run build` собирается без ошибок;
desktop потребители (AdminWriteoffs API + новая страница OperationsHistory)
получают типизированный доступ к Zeus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] fix: enum-значения статусов + убрать as const в bucket аккумуляторе

После регенерации Zeus `MarketplaceInventoryStatus` стал nominal enum
вместо string-literal union — статусы передаются через
`Zeus.MarketplaceInventoryStatus.LABELED|ISSUED|RETURNED|WRITTEN_OFF`,
а не голыми строками.

В `AdminWarehouseSummaryPage.warehouseRows` убран лишний `as const` на
дефолтном bucket'е — он делал поля `in/out/balance` readonly и ломал
накопление через `b.in += qty`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: Phase B locator p.mkt.* — реальные ссылки на marketplace::orders/retrequests/wroffprops

Story 9.3. Контракт marketplace опубликован в Эпике 11; все три marketplace-процесса
(supply / return / writeoff) уже имеют свои entity-таблицы с одноимённым
checksum256-полем `hash`. Pre-existing заглушка `[]` с упоминанием Locked Decision L10
снята: теперь Phase B end-to-end отдаёт текущий снэпшот процесса из blockchain_deltas.

  - p.mkt.supply -> marketplace::orders.hash
  - p.mkt.return -> marketplace::retrequests.hash
  - p.mkt.wroff  -> marketplace::wroffprops.hash

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] refactor: убрать дубль «Истории операций marketplace» — стол бухгалтера единая точка

Старая страница `pages/Marketplace/OperationsHistory` повторяла стол бухгалтера
(`extensions/reports`) — actions / delta_history / documents видны там в общем
реестре операций. По ревью к PR #400 удаляем полностью; вместо неё marketplace
будет инжектировать бизнес-описание процесса в стол бухгалтера через фабрику
(см. следующие коммиты).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: фабрика processInfoFactory и три marketplace info-widget'а — слот в стол бухгалтера

Аналог DecisionFactory, но для бизнес-описания процесса в реестре операций
стола бухгалтера. Каждое расширение регистрирует свой компонент по `process_type`;
стол бухгалтера рендерит его в развёрнутой строке после проводок и движений
по кошелькам. Дубля «истории» больше нет.

  - shared/lib/process-info-factory + types: реестр + getInfoComponent
  - widgets/Marketplace/ProcessSupplyInfoWidget — заказчик, поставщик, КУ,
    состояние, единицы; deep-link «открыть заказ на столе ПВЗ»
  - widgets/Marketplace/ProcessReturnInfoWidget — заказчик, состояние,
    исходный заказ, причина; deep-link «открыть заявление на столе ПВЗ»
  - widgets/Marketplace/ProcessWriteoffInfoWidget — источник проекта (cron/manual),
    состояние, цикл, сумма; deep-link «открыть проект на столе списания»
  - extensions/market/app/extensions.ts — регистрация по `LEDGER2_PROCESS_REGISTRY.name`
    (SUPPLY/RETURN/WRITEOFF), без string-литералов в коде
  - extensions/reports/pages/OperationsPage — слот «Содержание процесса» в развороте

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] style: страницы Эпика 9 на <template lang="pug"> + русские лейблы — снять техжаргон из UI

Привожу страницы к канону стола заказов: PUG-шаблоны, бизнес-словарь
в подписях/фильтрах (Состояние / Кооперативный участок / Заказчик),
никаких `p.mkt.*` или raw-имён enum-значений в видимой части. Внутри —
строгая типизация через `Zeus.MarketplaceInventoryStatus`, switch на
status-полях больше не сравнивается со строками.

  - OperatorOwnWarehouse (Story 9.1) — PUG, enum-fix, человеческие лейблы
  - AdminWarehouseSummary (Story 9.2) — PUG, enum-fix, человеческие лейблы
  - EcosystemRegistry (Story 9.4) — PUG

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] refactor: вынести запрос процесса в entities/Process + чистая типизация в info-widget'ах

По канону Ledger2: api/store/types. Widget'ы больше не вызывают client.Query
и не делают cast'ов вида `as Record<string, unknown>` — снэпшот приходит
строго типизированным `IProcessSnapshot` из SDK через `useProcessStore`.

  - entities/Process/types — IProcessView / IProcessSnapshot / IProcessDelta
    выведены из Queries.Processes.GetProcess.IOutput
  - entities/Process/api — getProcess + pickLatestSnapshot (единственная
    точка приведения JSON-скаляра)
  - entities/Process/model — Pinia store с loadProcess / loadLatestSnapshot
  - widgets/Marketplace/Process{Supply,Return,Writeoff}InfoWidget — используют
    processStore, типы из entities/Process

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] style: убрать пользовательский техжаргон со сводного склада и экосистемы

  - AdminWarehouseSummary: удалить сноску про SDK-подписки и формулу остатка —
    пользователю кооператива это не нужно
  - EcosystemRegistry: оставить короткую фразу «включится позже», без режима
    просмотра и упоминаний платформенного реестра

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] docs: зафиксировать паттерн фабрик инжекции в стол совета и стол бухгалтера

Новый документ `components/desktop/docs/extension-factories.md`:
- общий принцип «общие столы единственная точка просмотра, расширения
  инжектируют бизнес-описание»
- DecisionFactory: контракт, пример регистрации (capital → стол совета)
- ProcessInfoFactory: контракт, пример регистрации (market → стол
  бухгалтера) с использованием LEDGER2_PROCESS_REGISTRY вместо
  строковых литералов
- чек-лист для нового расширения (виджет/PUG → app/extensions.ts →
  install.ts → реестр в cooptypes)
- что делать НЕЛЬЗЯ: повторять у себя реестры операций/проводок/повестки
  и пихать техжаргон в infoComponent

В README desktop — ссылка из раздела «Система расширений».

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:09:52 +05:00
Alex Ant 80ebdb4a73 Эпик 8 «Списание скоропорта по решению совета» MVP Стола заказов (598-11) (#399)
* [598-11][@ant] feat: Эпик 8 «Списание скоропорта по решению совета» — перевод процесса на канонический паттерн soviet::createagenda с двумя документами

ОСНОВНОЕ ИЗМЕНЕНИЕ ОТ ПРЕДЫДУЩЕЙ ИТЕРАЦИИ:
- Предыдущая реализация Эпика 8 (PR #399 — reverted) шла «мимо совета»: backend
  напрямую дергал propwroff/execwroff/declwroff, в UI председатель «исполнял»
  и «отклонял» сам. Это не совпадало с каноническим паттерном принятия решений
  совета (приём в пайщики 100→501, паевой взнос имуществом 700→701, свободное
  решение 600, ResultContribution 1040→1041 — и т.д.).
- Текущая ветка переделана с нуля под канон: документ-Предложение (1106)
  подписывается председателем → soviet::createagenda(type=mktwroff) ставит
  повестку → совет голосует и авторизует Протокол (1105) → callback от soviet
  (onmktwoauth / onmktwodecl) фиксирует решение на цепи → backend per-item
  проводит execwroff.

C++ КОНТРАКТЫ:
- contracts/cpp/marketplace: добавлены actions onmktwoauth(coopname, hash,
  authorization) и onmktwodecl(coopname, hash, reason) с require_auth(_soviet).
  Сигнатуры совпадают с AUTHORIZE_CALLBACK_SIGNATURE и DECLINE_CALLBACK_SIGNATURE
  в soviet.hpp.
- contracts/cpp/marketplace: добавлен статус AUTHORIZED в WroffStatus; execwroff
  упрощён — больше не принимает протокол (он лежит в proposal.protocol после
  callback'а onmktwoauth), проверяет статус == AUTHORIZED.
- contracts/cpp/marketplace: удалён declwroff (его роль исполняет
  soviet::cancelexprd → callback onmktwodecl).
- contracts/cpp/lib: добавлен type "mktwroff"_n в consts; dispatch в
  soviet::exec.cpp идёт по default-branch на authorize_action_effect.
- p.mkt.wroff.standard.yaml: states/transitions/documents переведены под
  новый граф.

COOPTYPES + FACTORY:
- registry 1106.MarketplaceWriteoffStatement (Заявление председателя о списании)
  — подписывается перед propwroff + createagenda.
- registry 1105.MarketplaceWriteoffProtocol (Протокол совета о списании) —
  подписывается chairman'ом в стандартном sov.decision flow, ложится в
  wroffprops.protocol через callback onmktwoauth.
- factory/Actions + Templates для обоих registry; registry.ts обновлён.
- contracts/marketplace/actions: declWroff удалён, добавлены onMktWoAuth и
  onMktWoDecl; IExecWroff в interfaces/marketplace.ts больше не несёт protocol.

CONTROLLER:
- domain/infrastructure MarketplaceWriteoffProposal: статусы DRAFT → ON_AGENDA
  → AUTHORIZED → EXECUTING → EXECUTED | REJECTED; trigger cron/manual;
  decision_id для связки с soviet.decisions; items с inventory_id для
  крон-пометки записей WRITTEN_OFF после executed.
- partial-unique индексы запрещают параллельные DRAFT/ON_AGENDA per coopname.
- application/services/marketplace-writeoff.service: createDraft / updateDraft /
  cancelDraft / submitToCouncil (propwroff + createagenda) / onCouncilAuthorized
  (callback → AUTHORIZED → executeAuthorizedProposal) / executeAuthorizedProposal
  (цикл execwroff per-item) / onCouncilDeclined.
- detected proposal_hash = sha256('writeoff|coopname|cycle_started_at|draft_id|items').
- application/services/marketplace-writeoff-cron: ежемесячный сканер
  inventory.expiry_date; если config.writeoff.auto_proposal_enabled — формирует
  DRAFT-предложение по позициям LABELED с приближающимся сроком (грейс
  заданный конфигом), иначе только reminder председателю.
- infrastructure/inventory: новая колонка expiry_date (TypeORM synchronize),
  partial-индекс под крон-сканер; проставление при labeling
  (`labeled_at + Offer.warranty_days * 86400`).
- types.ts: IConfig.writeoff{auto_proposal_enabled, expiry_grace_days}.
- migrations/marketplace-bootstrap-v6: bump schema_version + merge writeoff
  defaults.
- domain/ports/marketplace-canonical-blockchain: propWroff / execWroff /
  createWriteoffAgenda. Adapter использует BlockchainService + VaultDomainService.
- application/dto/marketplace-writeoff.dto: enum'ы (StatusEnum / TriggerEnum)
  через registerEnumType; PaginatedMarketplaceWriteoffProposals на
  createPaginationResult; PaginationInputDTO в resolver.
- application/resolvers/marketplace-writeoff.resolver: marketplaceOpenWriteoffDraft,
  marketplaceListWriteoffProposals, marketplaceWriteoffProposal,
  marketplaceCreateWriteoffDraft (admin/manage_draft),
  marketplaceUpdateWriteoffDraft, marketplaceCancelWriteoffDraft,
  marketplaceWriteoffStatementSignablePayload (admin/propose, генерирует 1106),
  marketplaceSubmitWriteoffDraft (admin/propose, подписанное 1106 → propwroff +
  createagenda). marketplaceExecuteWriteoffProposal / marketplaceDeclineWriteoffProposal
  больше нет — это теперь делает совет.
- access-matrix: admin.Writeoff = ['manage_draft','propose','read:all'],
  board.Writeoff = ['decide','read:all'], board_readonly.Writeoff = ['read:all'].

SDK (@coopenomics/sdk):
- selectors/marketplace/writeoffSelector (item, decision_log, proposal,
  paginated) с MakeAllFieldsRequired-валидаторами.
- queries.marketplace: openWriteoffDraft, listWriteoffProposals,
  getWriteoffProposal, writeoffStatementSignablePayload.
- mutations.marketplace: createWriteoffDraft, updateWriteoffDraft,
  cancelWriteoffDraft, submitWriteoffDraft.

NOTIFICATIONS:
- 5 Novu workflows: marketplace-writeoff-draft-built (адресат:
  председатель/админ), -proposed (адресат: совет), -authorized, -executed,
  -rejected (адресат: админ/председатель). Liquid-синтаксис.
- Эмиты событий marketplace.writeoff.* добавлены в
  marketplace-notification.events.ts; listener-bridge wiring оставлен Phase 2
  (отдельный коммит, паттерн 598-20).

DESKTOP:
- pages/Marketplace/AdminWriteoffs/api: типизированные обёртки над
  @coopenomics/sdk.
- AdminWriteoffsPage.vue: open-draft + лента «в работе совета» +
  архив (EXECUTED/REJECTED).
- DraftEditorDialog: добавление/удаление/правка позиций с live-total.
- SubmitToCouncilDialog: превью Заявления 1106 → подпись DigitalDocument →
  submitWriteoffDraft (запускает propwroff + createagenda).
- WriteoffProposalDetailsDialog: детали, items.executed, decision_log.
- extensions/market/install.ts: маршрут /<coopname>/market/writeoffs для
  ролей chairman+member; mp-role-admin / --mp-font-body токены по канону.

ИЗВЕСТНЫЕ ОГРАНИЧЕНИЯ / PHASE 2:
- Listener-bridge `MarketplaceNotificationService` для каналов
  marketplace.writeoff.* остаётся на отдельный коммит (паттерн 598-20).
- Sync writeoff_proposals из chain delta (двусторонняя сверка decision_id ↔
  soviet.decisions) — Phase 2, если будет нужен reconciler.
- Крон в auto-proposal-mode проставляет amount=0 placeholder (нет
  attached unit-cost при labeling) — председатель руками поправит при ревью.
- Переуступка по сниженной цене — out of MVP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-11][@ant] fix: ревью PR #399 — asset из config, стандарт на бизнес-языке, spec и docs-фабрика

- ASSET_DECIMALS/SYMBOL в writeoff.service / writeoff-cron.service берутся из
  MARKETPLACE_ASSET_CONFIG (DI), не хардкод (как в order-create.service)
- p.mkt.wroff.standard.yaml переписан без техжаргона (callback/soviet::exec/
  AUTHORIZE_CALLBACK_SIGNATURE убраны, эталон p.mkt.return)
- Заявление 1106 и Протокол 1105 включены в documents-1000-plus тест фабрики
  с моком getDecision для 1105
- spec для MarketplaceWriteoffService: createDraft/updateDraft/cancelDraft,
  computeProposalHash детерминистичность, submitToCouncil happy + 4 reject-кейса,
  onCouncilAuthorized/Declined, executeAuthorizedProposal per-item
- coopname в writeoff.resolver — из config.coopname (IMarketplaceCurrentMember
  не несёт coopname); статичный сигнатурный долг toDocument убран
- CreateAgenda путь — SovietContract.Actions.Decisions.CreateAgenda

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:10:32 +05:00
Alex Ant f0311afaa7 Эпик 7 «Гарантийный возврат имущества» MVP Стола заказов (598-10) (#397)
* [598-10][@ant] feat: backend Эпика 7 «Гарантийный возврат» (Stories 7.1-7.4) — завести MarketplaceReturnClaimService с 5 mutations (submretrn/aprretrem/rejretrem/accretrn/rejretrn) и хранилище фото в bucket stol-zakazov:images

— Domain: ReturnClaim entity + types (5 статусов pendrev/approvvisit/accepted/rejremote/rejatku с маппингом на ReturnStatus::* контракта), decision_log append-only журнал решений председателя, on_site_inspection-снапшот очного осмотра, ledger_snapshot снапшот compensating-forward пары после accretrn. Photo type хранит bucket_key + sha256 content_hash + mime_type — хеш используется как анкер on-chain (параметр photos[] submretrn).
— Repository: create/findById/findByRequestHash/findActiveByOrderId (idempotency check) /listByOrderer/listByDeliveryBraname/applyDecision (append к decision_log + опциональная фиксация on_site_inspection / ledger_snapshot). Partial-unique индекс (coopname, order_id) ограниченный активными статусами — пайщик не может одновременно открыть две заявки на один заказ.
— Canonical blockchain port + adapter: 5 новых методов submRetrn/aprRetRem/rejRetRem/accRetrn/rejRetrn — обёртки над marketplace::submretrn/aprretrem/rejretrem/accretrn/rejretrn (контракты уже реализованы в cpp/marketplace/src/p.mkt.return/). accretrn — композитная транзакция через транзит 91: o.mkt.return + o.mkt.return2 (compensating forward к o.mkt.consum, AR9/AR14 L3).
— MarketplaceReturnClaimService: submitReturnClaim (Story 7.1: валидация warranty_until>now, reason_text 1-500, photos 1-10, request_hash детерминирован от order_hash+orderer+actual_quantity, фото загружаются в bucket до on-chain submit, on-chain photos[] = sha256-хеши), approveReturnVisit/rejectReturnRemote (Story 7.2 FR31: PENDING_CHAIRMAN_REVIEW → APPROVED_FOR_VISIT/REJECTED_REMOTELY, comment обязателен), acceptReturnAtVisit/rejectReturnAtVisit (Story 7.3-7.4 FR32-FR33: APPROVED_FOR_VISIT → ACCEPTED_AT_VISIT/REJECTED_AT_VISIT, при accept атомарно compensating forward o.mkt.return+o.mkt.return2 восстанавливает w.mkt.member.available заказчика и возвращает имущество на склад КУ, barcode-сверка с Order). getReturnClaimSignablePayload — preview через factory registry_id=800 (ReturnByAssetStatement, переиспользован для членской модели по стандарту p.mkt.return.standard.yaml).
— MarketplaceReturnClaimImagesService: @UseBucket('stol-zakazov:images') — конвенция «один сервис на один bucket» из README file-storage. maxBytes 10MB, allowedMime jpeg/png/webp, metadataSchema требует ownerAccount+orderId+claimId. Ключ объекта детерминирован: returns/<claim_id>/<role>/<index>.<ext>; backend хеширует bytes (sha256-hex) — этот же хеш идёт on-chain в submretrn.photos[].
— Resolver + DTO: 5 mutations + 3 queries (marketplaceListMyReturnClaims, marketplaceListReturnClaimsByBraname, marketplaceReturnClaimSignablePayload, marketplaceReturnClaim) + Photo/Decision/Inspection/Ledger ObjectType. GraphQL enum через registerEnumType (MarketplaceReturnClaimStatus / MarketplaceReturnClaimDefectCategory / MarketplaceReturnClaimExpectedResolution). Подписанное заявление приходит как SignedDigitalDocumentInputDTO meta=MarketplaceReturnClaimSignedStatementInputDTO; toDocument() конвертит в Cooperative.Registry.ReturnByAssetStatement.Action для submretrn.statement.
— Signed-document DTO: переиспользуем шаблон 800 «Заявление на возврат паевого взноса имуществом» (по стандарту p.mkt.return — структурно совместим с членской моделью; поля request.hash/title/units/unit_cost/total_cost заполняются из Order). request.hash = backend-computed request_hash → двусторонняя сверка backend ↔ on-chain.
— Access-matrix: orderer ['create:own', 'read:own'] на ReturnClaim, operator ['read:own-KU', 'decide:remote', 'decide:on-site']. resolver-уровень проверка ownership (orderer_account == member.username, braname == председатель чьего КУ — отложено до core coop_ku.chairman_account, в MVP передаётся параметром).
— Регистрация в marketplace-infrastructure.module.ts (TypeORM entity + repository) и marketplace-application.module.ts (FileStorageInfrastructureModule.forFeature + service + resolver).

Why: реализация Эпика 7 «Гарантийный возврат имущества» MVP «Стол Заказов» — пайщик подаёт заявление с фото-доказательством, председатель КУ удалённо рассматривает и при необходимости приглашает на очный осмотр, при принятии возврата атомарно через транзит 91 восстанавливаются средства на программном кошельке заказчика и имущество возвращается на склад участка (compensating forward — отдельное событие в журнале, не revert исходного o.mkt.consum). Возврат поставщику и работа с поставщиком по претензиям — Phase 2 (out of MVP).

* [598-10][@ant] feat: notifications Эпика 7 — 3 Novu workflow и слушатели per-contract event-bus (submitted/decided/finalized) для гарантийного возврата

— @coopenomics/notifications: 3 workflow: marketplace-return-claim-submitted (председателю КУ на новое заявление; email + in-app + push с deep-link на operator-стол), marketplace-return-claim-decided (заказчику на промежуточные решения председателя — invite на очный визит), marketplace-return-claim-finalized (заказчику на финальный исход с восстановленной суммой если accept_at_visit).
— controller: 3 события в marketplace-notification.events.ts — MARKETPLACE_RETURN_CLAIM_SUBMITTED_EVENT/DECIDED/FINALIZED + типы payload. Сервис эмитит их после save в PG (INV-12).
— marketplace-notification.service.ts: 3 listener (handleReturnClaimSubmitted/Decided/Finalized) тянут subscriber_id/email из AccountDataPort, формируют payload с display-name и deep-link через config.frontend_url, шлют через NovuWorkflowPort. Ошибки доставки logged как warn — основной flow возврата не блокируется.

Why: уведомления (push/email/in-app) — обязательный канал коммуникации по UX-DR38; заявление на гарантийный возврат должно дойти до председателя КУ без поляризации UI (председатель не всегда сидит в operator-столе), решение председателя — до заказчика без необходимости вручную проверять статус заявления.

* [598-10][@ant] feat: SDK Эпика 7 — Zeus selectors, 5 mutations и 3 queries для гарантийного возврата + перегенерированная schema.gql

— Selectors: marketplaceReturnClaimSelector (полный набор полей заявления вместе с photos / decision_log / on_site_inspection / ledger_snapshot), marketplaceReturnClaimResultSelector (claim + tx_hash) и поднаборы (photo / decision-entry / inspection / ledger). Валидаторы MakeAllFieldsRequired гарантируют полное покрытие type.
— Mutations: CreateReturnClaim / ApproveReturnVisit / RejectReturnRemote / AcceptReturnAtVisit / RejectReturnAtVisit. Каждая принимает соответствующий Input DTO и возвращает marketplaceReturnClaimResultSelector.
— Queries: ListMyReturnClaims (без args), ListReturnClaimsByBraname (data.delivery_braname), ReturnClaimSignablePayload (data.order_id + actual_quantity? — preview registry_id=800 для подписи пайщиком).
— schema.gql пересчитана; zeus/const.ts + zeus/index.ts перегенерированы. Все ID-поля заявления через @Field(() => String) (Zeus резолвит scalar ID как unknown без глобального scalar-resolver — поэтому Order и ReturnClaim единообразно используют String).

Why: типобезопасный SDK-слой между backend GraphQL и desktop UI; правило «никаких raw GraphQL-строк в desktop» (feedback_graphql_no_raw_strings_desktop) требует прохода через Zeus client.

* [598-10][@ant] feat: desktop UI Эпика 7 — orderer и operator страницы возврата на канон-widgets (TakeoverDialog + BarcodeScanner)

— OrdererReturnClaims (страница /:coopname/market/returns): список заявлений пайщика разделён на «активные» (PENDING_CHAIRMAN_REVIEW + APPROVED_FOR_VISIT) и «архив» (финальные статусы). Подача нового заявления через SubmitReturnClaimDialog — full-screen TakeoverDialog с q-stepper 3 шага: описание (reason_text + defect_category + actual_quantity?) → фото (q-file 1-10 шт jpeg/png/webp до 10МБ → base64) → подпись заявления (preview registry_id=800 + Classes.Document.signDocument приватным ключом из useGlobalStore). Просмотр через ReturnClaimDetailsDialog — фото-thumbnails со ссылками на signed-URL bucket'а, q-timeline decision_log председателя, снапшот compensating-forward при ACCEPTED_AT_VISIT.
— OperatorReturnClaims (страница /:coopname/market-pvz/returns): три ленты — pending (PENDING_CHAIRMAN_REVIEW) с inline-thumbnails фото и кнопкой «Принять решение» → RemoteDecisionDialog, approved (APPROVED_FOR_VISIT) с кнопкой «Очный осмотр» → OnSiteDecisionDialog, archive. RemoteDecisionDialog — q-option-group approve/reject + обязательный комментарий 1-500 симв. OnSiteDecisionDialog — q-stepper: 1) BarcodeScanner сканирует штрих-код имущества для сверки с Order; 2) inspection_result 1-2000 симв; 3) опциональные inspection_photos до 10 шт; 4) accept (compensating forward предупреждение) либо reject (имущество остаётся у заказчика).
— API: типобезопасные обёртки в pages/<page>/api/index.ts через destructuring результата `{ [Queries.X.name]: result } = await client.Query(...)`. Типы выведены из Queries/Mutations.X.IOutput без cast'ов. Фото-загрузка через FileReader→base64 в browser, backend кладёт в bucket stol-zakazov:images.
— DateTime utility: `formatDateTime(value: unknown)` для GraphQL DateTime scalar — Zeus резолвит как unknown без глобального scalar-resolver, простой helper парсит и форматирует через toLocaleString('ru-RU').
— install.ts: 2 новых маршрута зарегистрированы — workspace 'market' (orderer-стол) → '/:coopname/market/returns' (roles []); workspace 'market-pvz' (operator-стол) → '/:coopname/market-pvz/returns' (roles ['chairman']). Иконки fa-rotate-left / fa-clipboard-check; agreements: agreementsBase.

Why: продолжение поведенческой части Эпика 7 — пайщик и председатель кооперативного участка получают функциональные UI для подачи и рассмотрения заявлений на гарантийный возврат, со всеми атрибутами (фото, подпись заявления, барскод-сверка, compensating forward) согласно FR29-FR33. Соблюдён канон дизайн-системы Эпика 10: используются только widgets/Marketplace/* (TakeoverDialog + BarcodeScanner), inline-вёрстка кастомных диалогов запрещена; токены через marketplace-tokens.scss (var(--mp-space-md), mp-role-* классы на корнях страниц).

* [598-10][@ant] feat: добавить registry_id=1104 MarketplaceReturnStatement в cooptypes — отделить документ Эпика 7 «Стол заказов» от системы клиринга

Marketplace по системе членских взносов ведёт свой контур документов рядом
с 1102 «Акт приёма-передачи» и 1103 «ТТН». До этого Эпик 7 ошибочно
переиспользовал registry_id=800 ReturnByAssetStatement из старой системы
клиринга «по структурному совпадению» — это смешало два процесса в одном
signed-document пайплайне. Завожу полноценный собственный документ:
заявление пайщика о гарантийном возврате имущества с marketplace-полями
(order_id / order_hash / fact_cost / actual_quantity / reason_text /
defect_category / braname). Шаблон 800 остаётся как есть на случай
будущего клиринга.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-10][@ant] feat: проложить factory-цепочку для registry_id=1104 — Action + Template + регистрация в Registry/index

Action 1104.MarketplaceReturnStatement.Factory повторяет паттерн 1102
(getCooperative/getVars/getUser/getRequest/getProgram/getOrganization),
но кладёт в Model marketplace-поля (fact_cost / actual_quantity /
reason_text / defect_category) и не требует transmitter/decision —
заявление подписывает только пайщик.

Также: убираю tsconfig.tsbuildinfo из индекса (build-артефакт, не должен
жить в git).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-10][@ant] feat: переключить controller + SDK на registry_id=1104 для Эпика 7 — заявление о гарантийном возврате имущества

- marketplace-return-statement-document.dto.ts — новый Signed-DTO под
  Cooperative.Registry.MarketplaceReturnStatement.Action; старый
  marketplace-return-claim-document.dto.ts (ссылавшийся на 800
  ReturnByAssetStatement) удалён.
- MarketplaceReturnClaimService.generateStatementDocument теперь строит
  Action.MarketplaceReturnStatement с marketplace-полями (order_id /
  order_hash / fact_cost / reason_text / defect_category / braname),
  а не reuse'ит request{hash,units,unit_cost,total_cost} из старого 800.
- getReturnClaimSignablePayload принимает reason_text — превью теперь
  показывает реальный текст обращения пайщика.
- Регенерированы schema.gql + zeus client + SDK zeus (вход
  MarketplaceReturnStatementSignedInput с marketplace-меткой meta).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-10][@ant] feat: desktop SubmitReturnClaimDialog — превью под registry_id=1104 с пробросом reason_text

- API getReturnClaimSignablePayload теперь принимает reason_text и
  defect_category — превью показывает реальный текст обращения пайщика.
- Убрал `result as MarketplaceGeneratedDocumentView` cast (Zeus уже
  выдаёт нужный тип после регенерации schema/SDK).
- Комментарии и подсказки UI обновлены: 1104 MarketplaceReturnStatement
  вместо устаревшего 800 ReturnByAssetStatement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-10][@ant] refactor: десктоп-API Эпика 7 на строгие типы из SDK — variables передаём целиком data

Канон @coopenomics/sdk: каждый api-метод принимает `data: Mutations.<X>.<Y>.IInput['data']`
(или `Queries.<X>.<Y>.IInput['data']`) и передаёт его в variables объектом
целиком — `{ variables: { data } }`. Без раскладки полей вручную и без
самодельных интерфейсов аргументов.

Затронуто:
- OrdererReturnClaims/api: getReturnClaimSignablePayload + createReturnClaim
- OperatorReturnClaims/api: listReturnClaimsByBraname, approveReturnVisit,
  rejectReturnRemote, acceptReturnAtVisit, rejectReturnAtVisit
- SubmitReturnClaimDialog: defectCategory типизирован как enum
  Zeus.MarketplaceReturnClaimDefectCategory (раньше падал на string→enum,
  но был замаскирован ручной раскладкой полей в variables)
- OnSiteDecisionDialog: ReturnClaimPhotoUploadInput выведен из IAcceptReturnAtVisitInput
- OperatorReturnClaimsPage: listReturnClaimsByBraname({ delivery_braname })

Зачем — ревью PR #397: при ручной раскладке полей в variables.data
изменение схемы в SDK не подсвечивает call-site как ошибку TS;
регрессии маскируются. Канон даёт строгую сверку при изменении бэкенда.

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:32:47 +05:00
Alex Ant 6250abd56b Эпик 6 «Выдача пайщику с двойной подписью и корректировкой» MVP Стола заказов (598-9) (#390)
Publish Docs / build-and-publish-docs (push) Failing after 12m39s
* [598-9][@ant] feat: backend Эпика 6 «Выдача пайщику с двойной подписью» (Stories 6.1-6.4) — расширить Order полями выдачи, завести MarketplaceIssuanceService с двумя mutations (signiss1 + signiss2) и push «заказ готов к получению»

— Order entity + typeorm + mapper: добавлены поля выдачи (current_warehouse_braname, issuance_fact, chairman_signed_at/chairman_account/signiss1_tx_hash, orderer_signed_at/delivery_signer_account/signiss2_tx_hash), derived-геттеры awaits_chairman_issue_open/awaits_orderer_issue_final/is_received. На Order живут metadata подписей; жирные Document2 хранятся в on-chain row marketplace::orders.
— Repository: applyIssuanceOpened (статус ACCEPTED_TO_COOP → READY_TO_RECEIVE, проставление chairman_signed_at + signiss1_tx_hash + current_warehouse_braname = delivery_braname), applyIssuanceFinalized (READY_TO_RECEIVE → RECEIVED + issuance_fact + warranty_until), listForIssuanceByBraname (operator), listReadyToReceiveByOrderer (orderer).
— Chain port + adapter: signIss1, signIss2 — обёртки над marketplace::signiss1/signiss2 (контракт уже реализован в cpp/marketplace/src/p.mkt.supply/).
— MarketplaceIssuanceService: openIssuance (председатель КУ открывает выдачу), finalizeIssuance (заказчик закрывает финальной подписью; actual_quantity влияет на корректирующие операции в C++ signiss2 — три ветки FR23), getOpenIssuanceSignablePayload / getFinalizeIssuanceSignablePayload (preview документов через factory registry_id=1102).
— GraphQL DTO + resolver: MarketplaceIssueActSignedDocumentInputDTO, MarketplaceOpenIssuanceInputDTO, MarketplaceFinalizeIssuanceInputDTO, MarketplaceIssuanceResultDTO; mutations marketplaceOpenIssuance / marketplaceFinalizeIssuance; queries marketplaceListIssuancesByBraname / marketplaceListMyReadyToReceive + preview payloads.
— Access matrix: operator получает Issuance:{create,sign:first,read:own-KU}; orderer получает Issuance:{sign:final,read:own}.
— Push notification (Story 6.4 / FR22): новый workflow @coopenomics/notifications/marketplace-order-ready (email + in-app + push) + listener в MarketplaceNotificationService, эмит после applyIssuanceOpened (INV-12 — после save в PG).
— MarketplaceOrderDTO расширен публичными полями выдачи (issuance_fact с enum diff_state, chairman/orderer signatures metadata) — UI карточки заказа в RECEIVED видит фактическое количество и diff_state.

Issue: 598-9 / Эпик 6 MVP Стола Заказов

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-9][@ant] feat: SDK Эпика 6 — обёртки openIssuance/finalizeIssuance + queries listing/preview, обновлены схема и zeus-клиент (Story 6.5)

— schema.gql + zeus regen после расширения marketplace-process-controller: MarketplaceOpenIssuance/FinalizeIssuance/IssuanceResult/IssueActPayload + 4 query/mutation для столa выдачи.
— Selectors: orderSelector расширен полями выдачи (current_warehouse_braname, issuance_fact с diff_state, chairman/orderer signatures metadata) + новый marketplaceOrderIssuanceFactSnapshotSelector + marketplaceIssuanceResultSelector — UI карточки заказа в RECEIVED увидит фактическое количество.
— Mutations: openIssuance.ts, finalizeIssuance.ts — обе через единый marketplaceIssuanceResultSelector.
— Queries: listIssuancesByBraname.ts (operator-стол), listMyReadyToReceive.ts (orderer-стол), issueActChairmanSignablePayload.ts / issueActOrdererSignablePayload.ts (preview документа для подписи).
— Side-fix техдолгов рассинхронизации cooptypes dist ↔ controller, мешавших generate-schema:
  * marketplace-apl-reception.service.ts — добавлено обязательное reception_id в Action 1102.
  * marketplace-shipment-create.service.ts — Action 1103.MarketplaceTransportNote/PrivateData приведены к any (поля экспедитора уже off-chain в privatePayload через doc_data_hash).
  * generator.service.ts — saveDocData/getDocData проброшены через cast (методы из @coopenomics/factory есть в runtime, в типах ещё не публиковались).

Issue: 598-9 / Эпик 6 MVP Стола Заказов

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-9][@ant] feat: каркас столов выдачи в desktop — OperatorIssuance + OrdererReadyToReceive (Stories 6.6-6.7)

Минимальные каркасные страницы по образцу OperatorReception для Эпика 6 — лента заказов на ПВЗ + точки входа в подписи. Полный flow подписи (BarcodeScanner со штрих-кодом заказа, CorrectionTable для сверки факт vs заказ, TakeoverDialog для двух подписей) — следующий UI PR; backend уже принимает signed_document и исполняет три ветки сверки в C++ signiss2 атомарно.

— OperatorIssuance: список заказов на КУ выдачи (ACCEPTED_TO_COOP + READY_TO_RECEIVE) + точки входа «Открыть выдачу» (signiss1) / «Завершить выдачу» (signiss2). API через @coopenomics/sdk — listIssuancesByBraname / openIssuance / finalizeIssuance + getChairman/OrdererSignablePayload.
— OrdererReadyToReceive: лента заказов пайщика в READY_TO_RECEIVE — визуальное продолжение push-уведомления marketplace-order-ready (FR22). API — listMyReadyToReceive через @coopenomics/sdk.
— Никаких raw GraphQL-строк — только типизированные обёртки @coopenomics/sdk, как требует components/desktop/extensions/market/CLAUDE.md.

Issue: 598-9 / Эпик 6 MVP Стола Заказов

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-9][@ant] feat: полный UI flow подписи Эпика 6 через канон-widgets + регистрация в роутинге

Закрываем техдолг Эпика 6, оставленный из предыдущего коммита. Оба диалога — это реальная подпись приватным ключом через @coopenomics/sdk Classes.Document, не моки.

— IssueActOpenDialog (Story 6.1 / FR21): full-screen TakeoverDialog с предварительным актом выдачи (registry_id=1102). Председатель кооперативного участка подписывает первой подписью приватным ключом из useGlobalStore (signatureId=1), backend верифицирует и отправляет on-chain signiss1; статус заказа READY_TO_RECEIVE + push заказчику.
— IssueActFinalizeDialog (Story 6.3 / FR23-FR25): full-screen takeover с тремя шагами через q-stepper:
  * **Шаг 1 — штрих-код.** BarcodeScanner (DR11) сверяет штрих-код заказа.
  * **Шаг 2 — сверка факта.** CorrectionTable (DR13) показывает «план vs факт», оператор корректирует actual_quantity. UI вычисляет fact_cost и diff_state (equal/less/more) и подсказывает, какие корректирующие операции исполнит контракт (o.mkt.unblk при less, o.wal.conv + o.mkt.assign + o.mkt.block при more).
  * **Шаг 3 — двойная подпись.** Заказчик вводит свой WIF (signatureId=1, signer = orderer_account), оператор подписывает вторым ключом из сессии (signatureId=2, delivery_signer = current member). Backend верифицирует обе подписи и отправляет on-chain signiss2 — C++ исполняет три ветки сверки в одной композитной транзакции; при недостатке средств на доплату (more) транзакция падает с человеческим сообщением (L6 guard / FR25), backend пробрасывает его клиенту.
— OperatorIssuancePage переключён на эти диалоги — теперь кнопки «Открыть выдачу» и «Завершить выдачу» делают реальный сценарий, а не NotifyAlert-заглушку.

Регистрация в extensions/market/install.ts:
— `/<coopname>/market-pvz/issuance` — operator-стол выдачи (roles: chairman, иконка handshake).
— `/<coopname>/market/ready-to-receive` — orderer лента «Готово к получению» (roles: все, иконка box-check).

Issue: 598-9 / Эпик 6 MVP Стола Заказов

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-9][@ant] refactor: убрать any/unknown по ревью — пересобрать cooptypes/factory + типизировать desktop api через Queries.X.IOutput и Types.Document

Реакция на ревью PR #390: ревьюер требует строгую типизацию, any/unknown запрещены вне очень редких случаев.

Корневая причина прошлых хаков — рассинхронизация cooptypes/factory dist ↔ source в monorepo. Source 1102 уже без reception_id и 1103 уже с PrivateData, но dist в node_modules был собран из старой версии. Решение: пересборка cooptypes и factory из текущего source — dist приходит в соответствие с типами, controller компилируется без cast'ов.

— marketplace-shipment-create.service.ts: `const action: any` → `Cooperative.Registry.MarketplaceTransportNote.Action`, `privatePayload: any` → `PrivateData`. Все expeditor-поля живут off-chain в privatePayload, как и задумано в архитектуре 1103.
— marketplace-apl-reception.service.ts: убран лишний `reception_id` (его нет в свежей dist Action 1102).
— marketplace-issue-act-document.dto.ts: убрано поле `reception_id` из подписной формы АПП-выдачи — тоже не требуется.
— generator.service.ts: убран `(this.generator as any).saveDocData/getDocData` — методы есть в новой factory dist.
— catch (err: any) → catch (err) с `err instanceof Error ? err.message : String(err)` в marketplace-issuance.service.ts.

Desktop:
— OperatorIssuance/api/index.ts полностью переписан: вместо локальных View-интерфейсов и `as unknown as` — производные типы от SDK: `Queries.Marketplace.ListMyReadyToReceive.IOutput['marketplaceListMyReadyToReceive'][number]` для Order'а в контексте выдачи, `Types.Document.IGeneratedDocument` для preview-документа, `Types.Document.ISignedDocumentInput` для signed_document, `Mutations.Marketplace.OpenIssuance.IOutput['marketplaceOpenIssuance']` для результата. Все client.Query / client.Mutation возвращают строго типизированный `result` через destructuring `const { [Mutations.X.name]: result }` — паттерн из features/User/LoginUser/api/index.ts.
— OrdererReadyToReceive/api/index.ts: то же — без `as unknown as`.
— IssueActOpenDialog/FinalizeDialog: убраны все `generated as unknown as Parameters<...>` и `signed as unknown as SignedDocumentInput` — `docSigner.signDocument(generated, ...)` принимает результат `getXxxSignablePayload` напрямую через `Types.Document.IGeneratedDocument`. catch (err: any) → catch (err) с проверкой `instanceof Error`.

Issue: 598-9 / Эпик 6 MVP Стола Заказов

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 11:57:46 +05:00
Alex Ant 9ac3d8ea33 Эпик 5 «Поставка и приёмка имущества на КУ» MVP Стола заказов (598-8) (#388)
* [598-8][@ant] feat: добавить формирование партий поставки из акцептованной заявки с жёстким акцептом состава — закрыть Stories 5.1 и 5.2 Эпика 5

Backend (controller/extensions/marketplace) — старт Эпика 5 «Поставка и
приёмка имущества на КУ». Story 5.1 — pre-shipment группировка Order'ов
консолидированной заявки по КУ-получателю с выбором варианта доставки.
Story 5.2 — жёсткий акцепт состава поставки backend'ом перед формированием
партий.

Domain (новое):
- MarketplaceShipmentDomainEntity + types (delivery_variant A/B, status
  DRAFT/SUPPLY_PREPARED/RECEPTION_IN_PROGRESS/ACCEPTED_TO_COOP/CANCELLED,
  TTNData с полями экспедитора, ТС, погрузки, доставки). Backend-only PG
  без on-chain зеркала: фиксация поставки идёт через АПП-приёмки и
  signsupp/signchair (Stories 5.3/5.4/5.6).
- MarketplaceSupplyValidationLogDomainEntity + типы Outcome/Reason —
  append-only audit-журнал валидаций состава.

Infrastructure (новое):
- MarketplaceShipmentEntity TypeORM с unique-индексом (coopname,
  cycle_id, ku_id) + partial unique-индексом по ttn_number + hot-path
  индексами по offerer/ku/status.
- MarketplaceSupplyValidationLogEntity TypeORM.
- Адаптеры репозиториев + мапперы Row→Domain под обе сущности.

Application (новое):
- MarketplaceShipmentCreateService — guard ACCEPTED + ownership заявки,
  жёсткая валидация (Story 5.2): покрытие всех КУ заявки группами,
  непересечение групп, статус Order'ов ACCEPTED. Атомарное создание
  Shipment'ов per группа + перевод Order'ов в SUPPLY_PREPARED. Для
  Варианта Б — генерация уникального ttn_number и stub-URL ТТН
  (реальная подпись через document factory подключается Story 5.4
  follow-up).
- MarketplaceShipmentResolver — мутация marketplaceCreateShipment под
  Shipment:create:own access-matrix, query marketplaceListShipments
  + marketplaceGetShipment для стола подготовки поставки.

Wiring:
- marketplace-infrastructure.module.ts регистрирует обе TypeORM-сущности,
  адаптеры и мапперы, экспортирует репозиторные токены.
- marketplace-application.module.ts регистрирует create-service и
  resolver.

Не входит в Stories 5.1/5.2 (отложено по scope в follow-up Stories
эпика):
- Apollo-codegen интеграция нового стола «Подготовка поставки» в desktop
  offerer-flow — обвязка через @coopenomics/sdk после generate-schema +
  generate-client (паттерн PR #386 Эпика 4).
- Реальная подпись ТТН через AR33 document factory — закрывается
  Story 5.4 (асинхронная подпись поставщика).
- GraphQL Subscription marketplaceShipmentUpdated для live-tracking
  партий — follow-up по аналогии с Order subscriptions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] feat: добавить маркировку имущества штрих-кодом Code128/EAN-13 на приёмке КУ — закрыть Story 5.5 Эпика 5

Backend (controller/extensions/marketplace) — оператор КУ при приёмке
маркирует имущество внутренним штрих-кодом (стандарт маркетплейсов;
QR-коды в MVP запрещены).

Domain (новое):
- MarketplaceInventoryDomainEntity с конструктор-валидацией формата
  EAN-13 (13 цифр + checksum) и обязательного barcode_value.
- MarketplaceBarcodeFormat (CODE128/EAN13), MarketplaceBarcodeStrategy
  (PER_ORDER/PER_UNIT/PER_PACKAGE), MarketplaceInventoryStatus
  (LABELED/ISSUED/RETURNED/WRITTEN_OFF — последние три задел для Эпиков
  6/7/8).

Infrastructure (новое):
- MarketplaceInventoryEntity TypeORM с unique-индексом по
  (coopname, barcode_value) — поиск сканером при выдаче — плюс hot-path
  индексами по order/ku/shipment + status.
- Адаптер репозитория + маппер Row→Domain.

Application (новое):
- MarketplaceInventoryLabelService — guard перехода статуса Order
  (SUPPLY_PREPARED/READY_TO_RECEIVE/ACCEPTED_TO_COOP), guard повторной
  маркировки (countByOrder>0 → 409), привязка к Shipment через
  cycle_id+ku_id, planLabels по стратегии (PER_ORDER → 1 этикетка;
  PER_UNIT → N=quantity; PER_PACKAGE → ceil(quantity/pack_size)),
  генерация уникального barcode_value с retry-loop против коллизий,
  корректный checksum-digit EAN-13.
- MarketplaceInventoryResolver — мутация marketplaceLabelInventory под
  новый access-matrix permission Inventory:label (роль operator), query
  marketplaceListInventory под Warehouse:read:own-KU для admin-стола.
- Access matrix дополнен `operator → Inventory: ['label']`.

Wiring:
- MarketplaceInventoryEntity подключён в connection «marketplace»,
  адаптер и маппер зарегистрированы в infrastructure-module, сервис
  и резолвер — в application-module.

Не входит в Story 5.5 (отложено по scope):
- Apollo-codegen интеграция BarcodeScanner/BarcodeDisplay в operator-стол
  — обвязка через @coopenomics/sdk после generate-schema +
  generate-client.
- Batch-маркировка одной страницей для всей партии (UX-уровень,
  обвязка вокруг marketplaceLabelInventory).
- barcode_strategy как поле Offer — в MVP стратегия передаётся в
  mutation. Поле на Offer'е — техдолг витрины (Stories 3.x follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] feat: добавить акт приёмки партии с двухподписной state machine для Вариантов А и Б — закрыть Stories 5.3 и 5.4 Эпика 5

Backend (controller/extensions/marketplace) — АПП приёмки на КУ:
оператор формирует акт, поставщик подписывает первой подписью
(лично — Вариант А или асинхронно через push — Вариант Б), председатель
закрывающей. Backend state machine (PG) + расширение canonical-port
методами signSupp / signChair.

Domain (новое):
- MarketplaceAplReceptionDomainEntity + types: вариант A/B, статусы
  PENDING_SUPPLIER_SIGN → PENDING_CHAIRMAN_RECEPTION_SIGN →
  ACCEPTED_TO_COOP (плюс CANCELLED резерв), fact_quantity_per_order
  json для корректировки Варианта Б, expeditor_data снапшот из
  Shipment.ttn_data.
- canonical-port расширен signSupp / signChair (Marketplace.ISignSupp /
  ISignChair из cooptypes).

Infrastructure (новое):
- MarketplaceAplReceptionEntity TypeORM с unique-индексом по
  (coopname, shipment_id) — одна активная АПП на партию + partial
  unique по ttn_number + hot-path индексами по ku/offerer.
- Адаптер репозитория + маппер Row→Domain.
- MarketplaceCanonicalBlockchainAdapter дополнен submit'ами signsupp
  и signchair (подпись кооператива через VaultDomainService).

Application (новое):
- MarketplaceAplReceptionService — state machine:
  * create(shipment_id, fact_quantity?) — guard статуса партии
    SUPPLY_PREPARED + ownership + одна-АПП-на-партию через unique-
    constraint; для Варианта Б копирует TTN-данные снапшотом;
    fact_quantity по умолчанию = order.quantity; total_amount =
    Σ fact_quantity * price_per_unit. Shipment →
    RECEPTION_IN_PROGRESS.
  * signAsSupplier(reception_id) — переход
    PENDING_SUPPLIER_SIGN → PENDING_CHAIRMAN_RECEPTION_SIGN;
    placeholder tx_hash до подключения on-chain signsupp с реальной
    подписью документа Document2.
  * signAsChairman(reception_id, chairman_account) — переход
    PENDING_CHAIRMAN_RECEPTION_SIGN → ACCEPTED_TO_COOP; Order'ы
    группы переходят в ACCEPTED_TO_COOP (FR19a — выдача
    разблокируется только здесь); Shipment → ACCEPTED_TO_COOP.
- MarketplaceAplReceptionResolver:
  * marketplaceCreateAplReception под Receiving:create.
  * marketplaceSignAplReceptionAsSupplier под Receiving:sign:first
    (теперь принадлежит роли offerer — корректировка матрицы).
  * marketplaceSignAplReceptionAsChairman под Receiving:sign:closing
    (новый permission на роли operator).
  * marketplaceListAplReceptionsByKu — список текущих АПП КУ для
    operator-стола.
  * marketplaceListAplReceptionsAsSupplier — список ждущих подписи
    поставщика для offerer-стола.
- Access-matrix:
  * offerer + Receiving: ['sign:first'] — первая подпись поставщика
    в обоих вариантах.
  * operator: Receiving: ['create', 'sign:closing'] — формирование
    АПП и закрывающая подпись председателя КУ.

Wiring:
- Сущность подключена в connection «marketplace», адаптер/маппер в
  infrastructure-module, сервис/резолвер в application-module.

Не входит в Stories 5.3/5.4 (отложено по scope):
- FR45 signing infrastructure: реальная подпись Document2 (IPFS +
  AR33 document factory) + on-chain submit signsupp/signchair. Сейчас
  поля supplier_signsupp_tx_hash / chairman_signchair_tx_hash
  содержат placeholder-значения; chain-port методы реализованы и
  готовы к вызову, но сервис их пока не дёргает (TODO Story 5.6
  follow-up).
- Push-уведомление поставщику в Варианте Б при создании АПП — обвязка
  через notification-module.
- outgoing_payment и o.mkt.purch / o.mkt.payout — закрываются Story
  5.6 / 5.7 отдельным коммитом эпика.
- GraphQL Subscription marketplaceAplReceptionUpdated — follow-up
  по аналогии с Order subscriptions.
- Apollo-codegen интеграция TakeoverDialog в operator-стол.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] feat: добавить marketplace-реестр исходящих платежей с подтверждением кассиром — закрыть Stories 5.6 и 5.7 Эпика 5

Backend (controller/extensions/marketplace) — на закрывающей подписи
АПП-приёмки (Story 5.3/5.4) backend автоматически создаёт запрос
исходящего платежа поставщику; кассир видит задачу в своём столе,
подтверждает факт банковского перевода, статус переходит в
LEDGER_RECORDED.

Domain (новое):
- MarketplaceOutgoingPaymentRequestDomainEntity + types: статусы
  PENDING_CASHIER_ACTION → CONFIRMED_BY_CASHIER → LEDGER_RECORDED
  (плюс BLOCKED при отказе банка). Поля payment_reference (банковский
  ПП), bank_statement_ref (URL/hash выписки), payout_tx_hash под
  lazy-вариант L12, blocked_reason для аудита отказов.

Infrastructure (новое):
- MarketplaceOutgoingPaymentRequestEntity TypeORM с unique-индексом по
  (coopname, apl_reception_id) — один платёж на одну АПП + hot-path
  индексами по payee и статусу.
- Адаптер репозитория + маппер Row→Domain.

Application (новое):
- MarketplaceAplReceptionService.signAsChairman расширен: после
  закрывающей подписи председателя создаётся запрос платежа со статусом
  PENDING_CASHIER_ACTION. Промах не блокирует основной flow АПП
  (логируется warn, наблюдаемость по статусам реестра).
- MarketplaceOutgoingPaymentService:
  * confirm(payment_request_id, payment_reference, bank_statement_ref?)
    — кассир подтверждает физический перевод; статус → LEDGER_RECORDED
    (MVP-поведение pre-L12, см. ограничения ниже).
  * markBlocked(payment_request_id, reason) — отказ банка; платёж
    помечается, обязательство по 86 остаётся открытым, кооператив
    разбирает вручную.
- MarketplaceOutgoingPaymentResolver:
  * marketplaceConfirmOutgoingPayment — мутация подтверждения кассиром.
  * marketplaceBlockOutgoingPayment — мутация блокировки.
  * marketplaceListOutgoingPaymentsForCashier — стол кассира.
  * marketplaceListOutgoingPaymentsAsSupplier — история выплат для
    offerer-стола.

Wiring:
- Сущность подключена в connection «marketplace», адаптер/маппер в
  infrastructure-module, сервис/резолвер в application-module.

MVP-ограничения и техдолг:
- C++ marketplace::signchair выполняет композитную пару
  o.mkt.purch + o.mkt.payout атомарно (pre-L12 поведение). Поэтому
  в текущей реализации подтверждение кассира трактуется как
  audit-trail банковского перевода (LEDGER_RECORDED сразу), а не как
  триггер lazy on-chain payout. Полная L12 (отдельная транзакция
  o.mkt.payout на confirm) — требует разделения signchair и payout в
  C++ контракте; техдолг PRD/FR57.
- Реестр marketplace-scoped (PG-only). Синхронизация с core
  outgoing_payment (AR35) — отдельным follow-up'ом со связкой
  core/gateway-API.
- On-chain submit signsupp/signchair с реальной подписью Document2
  через AR33 document factory — FR45 техдолг, отдельный коммит после
  настройки инфраструктуры подписания.
- Push-уведомления кассиру при создании запроса и поставщику при
  подтверждении — обвязка через notification-module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] feat: закрыть техдолги Эпика 5 — barcode_strategy на Offer, AR35 core sync, FR45 signing, UI SDK + каркасы 6 столов — чтобы Эпик 5 был cohesive и не висел недоделками

Закрывает блокеры завершения Эпика 5 «Поставка и приёмка имущества на КУ»
(см. план в `_blago/.../598-8-epik-5-...md`, секция «Sprint-план доделки»).

598-22 — barcode_strategy + pack_size на marketplace_offer:
- ALTER через TypeORM synchronize + bootstrap-v5 миграция для bump версии;
- Domain Offer entity + types + mapper + repo create/update;
- Валидация PER_PACKAGE → pack_size (диапазон 1..1000);
- @InputType расширен enum MarketplaceBarcodeStrategy + pack_size;
- MarketplaceInventoryLabelService теперь читает стратегию из Offer
  (per-call параметр оставлен как admin-override);
- +5 unit-тестов для новых валидаций; toMarketplaceOfferDTO вынесен
  в shared helper, убраны 3 дублирующих локальных toDTO.

598-17 / AR35 — синхронизация marketplace выплат с core outgoing_payment:
- Payment entity дополнен опциональными related_extension /
  related_entity_id, PaymentDomainInterface — теми же полями;
- GatewayInteractor.createSystemOutgoingPayment без statement/method_id
  (идемпотентность через payment_hash, source = extension);
- GatewayInteractorPort + оба адаптера расширены новым методом;
- marketplace_outgoing_payment_request получил core_payment_id колонку
  и applyCorePaymentId / deleteById repository-методы;
- AplReceptionService.createOutgoingPaymentRequest вызывает core
  с compensating delete на core-fail; OutgoingPaymentService на
  confirm/block отзеркаливает статус в core (COMPLETED / CANCELLED).

598-15 — FR45 signing infrastructure для signsupp/signchair:
- MarketplaceAplReceptionDocumentFactory строит unsigned Document2
  per-Order с meta (schema, reception_id, fact_quantity, accept_braname);
- Queries marketplaceAplReceptionSupplierSignablePayloads /
  marketplaceAplReceptionChairmanSignablePayloads возвращают payload'ы
  клиенту для подписания приватным ключом;
- Mutations marketplaceSignAplReception* расширены опциональным
  signed_documents (массив signed Document2 per-Order);
- При наличии signed_documents AplReceptionService.submitOnChainSign*
  отправляет реальные chainPort.signSupp/signChair и сохраняет
  реальный tx_hash; compensating-rollback при on-chain fail (статус
  АПП не меняется, ошибка наверх — клиент решает retry);
- Backwards-compat: без signed_documents сохраняется placeholder tx.

598-18 — SDK regenerated с операциями Эпика 5 + 6 UI-pages каркас:
- pnpm generate-schema + generate-client + sdk build (пятишаговая
  процедура feedback_graphql_no_raw_strings_desktop);
- Selectors: shipment, inventory, aplReception (+signable payload),
  outgoingPayment;
- Mutations.Marketplace: CreateShipment, LabelInventory,
  CreateAplReception, SignAplReceptionAsSupplier,
  SignAplReceptionAsChairman, ConfirmOutgoingPayment,
  BlockOutgoingPayment;
- Queries.Marketplace: ListShipments, GetShipment, ListInventory,
  ListAplReceptionsByKu, ListAplReceptionsAsSupplier,
  ListOutgoingPaymentsForCashier, ListOutgoingPaymentsAsSupplier,
  AplReceptionSupplierSignablePayloads,
  AplReceptionChairmanSignablePayloads;
- 6 pages в pages/Marketplace/{OffererSupplyPreparation,
  OffererPendingAplReceptions, OffererPaymentHistory,
  OperatorReception, OperatorInventoryLabeling,
  CashierOutgoingPayments} — каркасы с импортом @coopenomics/sdk и
  per-role классами; полная интеграция с canonical widgets
  (ExpeditorGroupingBoard, TTNPrintPreview, BarcodeScanner) —
  вторым UI-PR'ом после визуальной приёмки в браузере.

598-21 — batch-маркировка штрих-кодом:
- OperatorInventoryLabeling-стол поддерживает режим «печать партии»:
  все этикетки текущего КУ выводятся в одну CSS Grid с break-inside:
  avoid + @media print 2-колончатая разметка для печати на термопринтере.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-20][@ant] feat: push-уведомления marketplace flow — поставщик/кассир/поставщик в 3 точках Эпика 5 — чтобы вариант Б АПП не висел polling'ом и кассир сразу видел новые задачи

— 3 Novu workflow в @coopenomics/notifications:
  marketplace-apl-supplier-sign-request, marketplace-cashier-new-payment,
  marketplace-supplier-payment-confirmed. Email + InApp + Push шаги,
  payload-схемы (z.object) + slugify(name) → workflowId.
— MarketplaceNotificationService с 3 @OnEvent listener'ами:
  слушает event-bus EventEmitter2 (marketplace.aplReception.b.supplier.signRequested,
  marketplace.outgoingPayment.cashier.newTask,
  marketplace.outgoingPayment.supplier.confirmed),
  получает аккаунт через ACCOUNT_DATA_PORT, дёргает NOVU_WORKFLOW_PORT.
  Ошибки Novu/missing subscriber_id — warn без блокировки flow (INV-12:
  emit ПОСЛЕ save в PG, не блокирующий).
— Emit-точки в marketplace-services после save:
  • MarketplaceAplReceptionService.create — push поставщику для
    Варианта Б (EXPEDITOR), для Варианта А поставщик подписывает на
    стойке оператора — push не нужен.
  • MarketplaceAplReceptionService.signAsChairman — push кассиру
    после createOutgoingPaymentRequest успеха (compensating-rollback
    при core-fail убирает запись — push в этом случае не идёт).
  • MarketplaceOutgoingPaymentService.confirm — push поставщику
    после фиксации в LEDGER_RECORDED.
— MVP-ограничение: кассиром выступает chairman кооператива (role:
  'chairman'); когда появится extension-роль cashier — поменять
  фильтр в MarketplaceNotificationService.handleCashierNewPayment.

Закрывает 598-20. Из 7 техдолгов Эпика 5 в этом PR'е закрыто 6
(598-22, 598-17, 598-15, 598-18, 598-21, 598-20). 598-16 (L12 split
signchair/payout в C++ marketplace::supply) остаётся cross-repo
(mono-ai-5 contracts) — выходит за scope PR #388.

* [598-20][@ant] refactor: централизовать кассира в gateway по L12 — снять marketplace-only ручки confirm/markBlocked и подцепиться к payconfirm/paydecline через delta — кассир работает на весь кооператив, marketplace только зеркалит результат

Поток L12 (PR #389):
- backend marketplace::payout → inline gateway::createoutpay (outcome pending)
- кассир в общем столе gateway подтверждает банковский перевод → callback marketplace::payconfirm
- marketplace::paydecline при отказе кассира с reason
Listener (MarketplacePayoutSyncService) переводит per-Order projection PENDING → COMPLETED/DECLINED + setPaymentStatus в core + emit push поставщику.

Backend:
- entity/repository/mapper marketplace_outgoing_payment_request: 1 запись = 1 Order (order_hash unique), статусы PENDING/COMPLETED/DECLINED
- удалён MarketplaceOutgoingPaymentService.confirm/markBlocked + resolver мутации и query listForCashier — кассирского стола в marketplace нет
- MarketplaceAplReceptionService.signAsChairman: per-Order payOut() через chainPort + createSystemOutgoingPayment в core + projection PENDING (вместо агрегированной заявки)
- Novu workflow marketplace-supplier-payment-declined + событие MARKETPLACE_SUPPLIER_PAYMENT_DECLINED_EVENT

Frontend / SDK:
- удалён desktop стол CashierOutgoingPayments (кассир в gateway-расширении)
- удалены SDK mutations confirmOutgoingPayment/blockOutgoingPayment и query listOutgoingPaymentsForCashier
- selector marketplaceOutgoingPaymentRequest обновлён под per-Order поля
- regenerate schema + zeus client + sdk build зелёный

Resolves: ревью PR #388 (кассир централизован), часть E11 техдолга 598-16

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-20][@ant] refactor: документы АПП приёмки переведены на канон factory+SignedDocument — снять локальную самодельную обвязку Document2 (signature.info, builder hash) и подключиться к GENERATOR_PORT + ядерный SignedDigitalDocumentInputDTO как у participant/capital, плюс обязательная подпись на mutation подписания

Что было не по канону (ревью PR #388):
- локальная фабрика marketplace-apl-reception-document-factory.ts с самописным sha256(meta||doc_hash);
- MarketplaceSignatureInfoInputDTO / MarketplaceSignedDocumentInputDTO дублировали SignedDigitalDocumentInputDTO ядра;
- signed_documents nullable + placeholder-tx fallback на backend (документ мог не передаваться вообще);
- output query — кастомный SignablePayload, не GeneratedDocument из ядра.

Канон (как в participant/capital):
- cooptypes: новый registry 1102.MarketplaceAplReception (Action extends IGenerate, Model + html placeholder + переводы);
- controller/application/document/documents-dto/marketplace-apl-reception-document.dto.ts — Base + Generate + SignedMeta + SignedDocumentInput через IntersectionType с GenerateMetaDocumentInputDTO и MetaDocumentInputDTO;
- preview формируется через DocumentDomainService.generateDocument с registry_id=1102 per-Order;
- сервис принимает MarketplaceAplReceptionSignedDocumentInputDTO[], верифицирует подпись через @wharfkit/antelope (PublicKey.from + Signature.verifyDigest), индексирует документы по meta.order_id;
- signed_documents — обязательное (ArrayMinSize(1)), placeholder-режим удалён;
- resolver возвращает [GeneratedDocument!]!;
- module marketplace-application подключает DocumentDomainModule.

Заодно убран any в extract tx hash (G задача из ревью) — типизирован через узкий object literal.

Frontend OffererPendingAplReceptionsPage временно показывает уведомление «UI подписи в разработке» — backend ждёт реальную подпись пайщика через wif (диалог подписания — следующий этап).

Resolves: ревью PR #388 (документы по канону, A + G), удаляет техдолг FR45 на backend.

* [598-20][@ant] refactor: ku_id → braname в marketplace Эпика 5 — выровнять имена с контрактом, где приёмный КУ описывается полем accept_braname, а сам идентификатор КУ — это просто braname; «ku_id» путал агентов и десинхронизировал backend ↔ on-chain поля

Затронуты все слои Эпика 5: shipment / inventory / apl-reception в controller + sdk + desktop.

- domain entity/types: поле и геттеры braname вместо ku_id;
- TypeORM column + индексы: `IDX_marketplace_shipment_cycle_braname_unique` (был cycle_ku) + per-table covered index по (coopname, braname, status);
- repository: findByBraname / listByBraname (вместо ByKu);
- application services + DTO + resolvers: `marketplaceListAplReceptionsByBraname` (query name), `@Args('braname')`;
- SDK: queries `ListAplReceptionsByBraname` (файл переименован), selectors с полем `braname`;
- Desktop: `fetchInventoryByBraname`, view-интерфейсы (operator-inventory + apl-reception);
- schema.gql + zeus client регенерированы.

В dev-стеке `synchronize: true` подхватит column-rename автоматически; для prod-окружения отдельная двухэтапная миграция (ADD braname → backfill ← copy from ku_id → DROP ku_id) — вне scope этого PR, пока БД stateless для разработки.

Resolves: ревью PR #388 (C — ku_id → braname).

* [598-20][@ant] refactor: канон data/input + pug в шаблонах Эпика 5 — все мутации/queries marketplace принимают единый @Args('data', InputDTO) как в core/meet, голые скаляры обёрнуты в Input; шаблоны 5 столов переведены на pug — выровнять стиль с остальной частью desktop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] refactor: новые столы Эпика 5 переведены на SuccessAlert/FailAlert/NotifyAlert — убрать сырой Notify.create в пользу канона src/shared/api; ошибки идут через FailAlert(e, 'кастомный текст'), успех через SuccessAlert, заглушки UI-этапа — через NotifyAlert; кейс ревью PR #388 (OffererSupplyPreparationPage:22)

* [598-8][@ant] feat: акт АПП registry 1102 — содержание 1-к-1 из 802.ReturnByAssetAct, переменные адаптированы под marketplace — текст и переводы юр.отдела выверены; никакой самодеятельности с placeholder-шаблоном; в Action добавлены act_id и transmitter, сервис проставляет их при генерации (act_id формируется из reception+order short-id, transmitter — account поставщика для строки «Передал заказ»); кейс ревью PR #388 (1102.MarketplaceAplReception:1)

* [598-8][@ant] feat: Story 5.4 ТТН — реальная генерация через document-factory (registry_id=1103) + локальный реестр marketplace_ttn_document, Shipment.ttn_document_id вместо MVP-stub pdf_url — закрыть замечание ревью «MVP-stub в shipment-create.service.ts:321»: при формировании партии Варианта Б backend факторит документ ТТН тем же путём, что и АПП (DocumentDomainService.generateDocument с Cooperative.Registry.MarketplaceTransportNote.Action), и сохраняет в отдельной marketplace-таблице — общий реестр документов кооператива не трогаем, экспедиторы пока не пайщики и подписывают перевозку вне платформы; из Shipment удалены ttn_pdf_url и ttn_document_registry_id, заменены на ttn_document_id (uuid → marketplace_ttn_document.id); кейс ревью PR #388 (marketplace-shipment-create.service:321)

* [598-8][@ant] chore: regenerate schema.gql + SDK zeus + shipment selector после правок ревью PR #388 — pnpm run generate-schema + generate-client; в SDK zeus теперь нет ttn_pdf_url/ttn_document_registry_id (удалены из MarketplaceShipment DTO), добавлен ttn_document_id; shipmentSelector.ts руками поправлен под новый набор полей; SDK typecheck и build зелёные

* [598-8][@ant] fix: desktop typecheck — починить рассинхрон tsc/vue-tsc + убрать pug-мусор — без vue-tsc и с trailing \ в шаблонах PR не катит тайпчек

— components/desktop/package.json: `typecheck` возвращён на `vue-tsc --noEmit --skipLibCheck` (был «hard update» 2025-10-14 на чистый tsc; tsc не парсит .vue и не видит named re-export типов из них — отсюда 21 ошибка TS2614 на widget index.ts из Эпика 10).
— 4 страницы Marketplace (OffererPaymentHistory/OffererPendingAplReceptions/OffererSupplyPreparation/OperatorReception): убраны trailing `\` внутри `:columns="[...]"` — pug не требует line-continuation внутри `()`, vue-tsc ругался TS1127 Invalid character.
— components/sdk/src/queries/marketplace/listOutgoingPaymentsAsSupplier.ts: `query` приведён к канону остальных queries (объект через `$()`, не функция (input)=>Selector(...)); desktop вызывал как объект — отсюда TS2560 «Did you mean to call it?».
— components/desktop/tsconfig.vue-tsc.tsbuildinfo: удалён tracked билд-кэш vue-tsc.

* [598-8][@ant] feat: паттерн doc_data в @coopenomics/factory + ввести 1102/1103 как канонные документы — приватные данные документа off-chain, on-chain только sha256 hash, шаблон обращается через зарезервированную {{ doc_data.* }}

Закрывает крайнее ревью PR #388 (2026-05-17):

— components/factory/src/Services/DocData/index.ts: новый DocDataService — sha256 + Mongo upsert по hash (идемпотентный save), get по hash (null если запись удалена — graceful degradation).
— components/factory/src/Factory/index.ts: в DocFactory методы saveDocData/getDocData/loadDocData; factory автоматически даёт фабрике помощник для подгрузки приватного payload по data.doc_data_hash.
— components/factory/src/index.ts: Generator-фасад выставляет saveDocData/getDocData в IGenerator.
— components/cooptypes/src/cooperative/document/index.ts: IDocDataRef { doc_data_hash?: string } — зарезервированная ссылка, которую Action любого нового документа расширяет.
— components/factory/README.md + AGENTS.md: раздел «Приватные данные документа: doc_data» с потоком, зарезервированными именами и graceful degradation.
— components/factory/test/doc-data.test.ts: unit — sha256, идемпотентность, стабильный порядок ключей, null при удалении, делегирование через Generator-фасад.

1102.MarketplaceAplReception:
— cooptypes registry/1102: Action упрощён до канона 802.ReturnByAssetAct (registry_id, order_id, order_hash, act_id, transmitter, braname?, doc_data_hash?); семантика — `user` = пайщик-получатель (USERNAME), `transmitter` = USERNAME председателя КУ или доверенного им лица, передающего имущество (ФИО подставляется factory через getUser → getFirstLastMiddleName). Текст и переводы — 1-в-1 с 802 (юр.выверенный текст). Никаких «филиалов».
— factory/Actions/1102, factory/Templates/1102: каноничная фабрика по образцу 802.ReturnByAssetAct (factory.getUser(username) → пайщик, factory.getUser(transmitter) → председатель); decision для marketplace — стаб (нет протокола совета на каждый order).
— controller/extensions/marketplace/.../marketplace-apl-reception.service.ts: при сборке Action в Action идут только канонические поля; ФИО пайщика берётся по order.orderer_account, transmitter = chairman_account ?? created_by_operator_account.

1103.MarketplaceTransportNote:
— cooptypes registry/1103: Action очищен — { ttn_number, cycle_id, shipment_id, accept_braname, supplier_account, total_amount, currency, doc_data_hash }. Все персональные данные экспедитора (ФИО, паспорт, телефон, госномер, адрес погрузки, время доставки) вынесены в interface PrivateData — payload off-chain.
— Шаблон обращается через {{ doc_data.expeditor_full_name }}, {{ doc_data.vehicle_number }} и т.п.
— factory/Actions/1103, factory/Templates/1103: фабрика загружает приватный payload через loadDocData; ошибка если doc_data_hash не задан или payload удалён.
— controller/extensions/marketplace/.../marketplace-shipment-create.service.ts: убран MVP-stub — приватные поля экспедитора сохраняются через documentDomainService.saveDocData(...) → doc_data_hash → in Action; on-chain Action не содержит персональных данных.

Прочие документы (700-1099, 300-304, 50-51, …) остаются как были — паттерн прогрессивный, для новых документов с PII и при явной потребности.

Архитектурный артефакт (_bmad-output/planning-artifacts/architecture.md в blago проекте 1-prilozhenie-stol-zakazov): дополнен разделом «Document Generation Pattern: doc_data (Private Document Data Store)». Story 5.8 заведена в blago через `blago create req`.

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:31:50 +05:00
Alex Ant 83e6b654a9 fix(marketplace): Эпик 11 техдолг 598-16 — выплата поставщику через gateway (L12) (#389)
Publish Docs / build-and-publish-docs (push) Failing after 11m31s
* feat(marketplace): Эпик 11 техдолг 598-16 — split signchair и lazy payout (L12)

Locked Decision L12 — выплата поставщику должна выполняться только после
фактического банковского перевода кассиром, не атомарно с приёмкой имущества.
Текущая реализация signchair выполняла composite-пару o.mkt.purch + o.mkt.payout
в одной транзакции — ledger закрывал обязательство Кт 51 ДО реального
cashflow, формальное расхождение между bookkeeping и расчётным счётом.

Изменения:

C++ контракт marketplace:
- signchair оставляет только o.mkt.purch (Дт 10 / Кт 86); status
  supply_prepared → accepted_to_coop, current_warehouse_braname = accept_braname.
- Новый action `payout(coopname, order_hash)` с require_auth(coopname) — единственная
  операция o.mkt.payout (Дт 86 / Кт 51); статус Order'а не меняется.
- Order struct +bool payout_done — guard от двойного списания со счёта 51.

YAML стандарт p.mkt.supply:
- В actions добавлен marketplace::payout (actor: backend).
- В transitions: signchair → operations [o.mkt.purch]; новый transition
  accepted_to_coop → accepted_to_coop через payout с operations [o.mkt.payout].
- В operations: o.mkt.payout.triggered_by = marketplace::payout (раньше signchair).
- Сценарий: step 6 — приёмка без оплаты; новый step 6a — подтверждение выплаты.
- Описание состояния accepted_to_coop отражает открытое обязательство выплаты.

cooptypes:
- IPayout interface (coopname, order_hash); actions/payOut.ts wrapper.
- IOrder + payout_done: IBool.
- Index/jsdoc signChair приведён в соответствие.

Backend controller:
- MarketplaceCanonicalBlockchainPort.payOut + adapter с require_auth(coopname).

Что НЕ входит:
- MarketplaceOutgoingPaymentService.confirm — сервиса ещё нет (ожидается
  в Story 5.6 Эпика 5); реализуется отдельно поверх этого port'а.

Сборка:
- cdt build marketplace зелёный, ABI = 19 actions (был 18 + payout),
  order.payout_done присутствует.
- pnpm --filter cooptypes build green.
- pnpm exec tsc --noEmit (controller) green.

Refs: 598-14 / 598-16 (E11 техдолг), Locked Decision L12.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(marketplace): payout проходит через gateway с callback'ами payconfirm/paydecline

Уточнение архитектуры техдолга 598-16: исходящая выплата поставщику не должна
быть единичным action'ом контракта marketplace — она проходит через
существующий contract gateway, который умеет принимать запрос и дёргать
callback'и обратно по факту действия кассира. Перепроектировано:

C++ контракт marketplace:
- `payout(coopname, order_hash)` — backend инициирует исходящий платёж;
  inline-вызовом `Gateway::create_outcome` регистрирует запись в
  `gateway::outcomes` со статусом pending и callback'ами на marketplace.
  Ledger2 не двигается; статус Order'а не меняется.
- `payconfirm(coopname, outcome_hash)` — callback от `gateway::outcomplete`
  (require_auth(_gateway)) после действия кассира. ТОЛЬКО здесь применяется
  `Ledger2::apply(o.mkt.payout, ...)` — Дт 86 / Кт 51.
- `paydecline(coopname, outcome_hash, reason)` — callback от
  `gateway::outdecline` (require_auth(_gateway)). Без ledger-движения;
  обязательство Кт 86 остаётся открытым; reason сохраняется на Order.
- Order struct: убран `bool payout_done`, заменён на
  `eosio::name payout_status` (none/pending/completed/declined) + `string
  payout_decline_reason`. Введён namespace OrderPayoutStatus.

YAML стандарт p.mkt.supply: добавлены три новых action, transitions
accepted_to_coop → accepted_to_coop через payout/payconfirm/paydecline,
scenario 6a/6b + alternative «Кассир отклонил банковский перевод», роль
`gateway` в списке ролей; `o.mkt.payout.triggered_by` = `payconfirm`.

cooptypes:
- IPayout (без изменений по сигнатуре), новые IPayConfirm/IPayDecline.
- IOrder: payout_status + payout_decline_reason (вместо payout_done).
- payOut/payConfirm/payDecline action wrappers; index.ts экспорт.

Controller:
- MarketplaceCanonicalBlockchainPort.payOut — обновлён docstring; backend
  callback'и НЕ отправляет (parser2 подхватывает delta).

Документация:
- `components/contracts/cpp/marketplace/payout-via-gateway.md` — шпаргалка
  для агентов: триплет действий, под-граф payout_status, контрактные
  детали, anti-patterns, источники правды.

Сборка:
- cdt build marketplace зелёный; ABI = 21 action (было 19 + payconfirm +
  paydecline); order.payout_status:name, payout_decline_reason:string.
- pnpm --filter cooptypes build green.
- pnpm exec tsc --noEmit (controller) green.

Refs: 598-14 / 598-16 (E11 техдолг), Locked Decision L12.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 20:02:54 +05:00
Alex Ant 75a22fe613 Эпик 4 «Заказ и блокировка средств» MVP Стола заказов (#386)
* [598-7][@ant] feat: добавить создание Order с атомарной серией блока средств — закрыть Story 4.1 Эпика 4

Backend (controller/extensions/marketplace):
- MarketplaceOrderDomainEntity (composite db + bc; IBlockchainSynchronizable) +
  types (cycle_type / status / create_tx snapshot) + repository interface +
  TypeORM entity + mapper + delta-mapper для marketplace::orders +
  adapter (persistAfterBlock / findBySyncKey / sync-fork API).
- MarketplaceCanonicalBlockchainPort + adapter — submit canonical
  Actions.CreateOrder (cooptypes из PR #385 / Story 11.1 PR #375); подпись
  кооператива через VaultDomainService по data.coopname.
- MarketplaceOrderSyncService extends AbstractEntitySyncService: подписка
  на `delta::marketplace::orders` через EventEmitter2 + fork handler с
  компенсирующим offerCounters.onOrderRolledBack для Order'ов в block-state.
- MarketplaceOrderCreateService: guard FR11a (Offer existence/ACTIVE/
  quantity_available); optimistic onOrderBlocked → chain createorder
  (o.wal.conv → o.mkt.assign → o.mkt.block в ledger2::apply) → compensating
  onOrderRolledBack при chain-failure → persist PG с tx_snapshot.
- GraphQL Mutation marketplaceCreateOrder (через DTO/Input + Resolver)
  под Order:create marketplace-access-matrix.
- Wiring marketplace-infrastructure.module + marketplace-application.module.

Tests:
- 7 сценариев composite-entity (db-only, db+bc, sync-key mismatch, update bc
  без затрагивания backend полей, derived флаги для каждого статуса,
  hash normalization, hash валидация).
- 7 сценариев create-service (Guard FR11a 4 ветки, quantity ≤ 0,
  compensating rollback при chain submit fail, happy path с проверкой
  всех 11 полей ICreateOrder payload).

UI (desktop/widgets/Marketplace/CreateOrderDialog):
- TakeoverDialog с формой quantity + KU-выбор через KUMapWithList +
  total-cost preview + mp-role-orderer обёртка; токены только через
  marketplace-tokens.scss; CSS-vars / no hard-coded colors.

Не входит в Story 4.1 (отложено по scope):
- Apollo-codegen интеграция CreateOrderDialog в MarketplaceCatalogPage —
  Story 4.6 («Мои заказы» нужен apollo) подключит mutation marketplaceCreateOrder
  и WalletTimeline mount после успеха.
- GraphQL Subscription marketplaceOrderUpdated — Story 4.6 для live-tracking.
- Удаление legacy controller/application/marketplace/* (interactor/service/
  resolver под клиринг) — отдельный refactor-PR; TSC уже сломан этим
  legacy после PR #385 (canonical cooptypes), не блокирующий Story 4.1
  canonical layer.
- Access-matrix регистрация Order:create permission — в общем update
  Story 4.6.

Spec и контракт интеграции — _bmad-output/implementation-artifacts/
spec-3-4-bc-integration.md (Эпик 3 → Эпик 4) + controller/CLAUDE.md
(Composite-entity / Dispatch pipeline / Fork handling).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] feat: добавить cycle-type-aware агрегацию Order'ов в консолидированную заявку — закрыть Story 4.2 Эпика 4

Backend-only (Locked Decision L10): on-chain представления заявки НЕТ;
агрегация целиком в PG. Order'ы привязываются через
marketplace_order.cycle_id.

Поведение per cycle_type (Locked Decision L11):

- time_based: cron `@Cron(EVERY_5_MINUTES)` сканирует ACTIVE Offer'ы
  с cycle_type='time_based' + cycle_days; для каждого находит cycle_start
  как min(blocked_at) среди unassigned active Order'ов; если now >=
  cycle_start + cycle_days решает:
    sum >= min_threshold (или null) → consolidated_request status=
    'PENDING_SUPPLIER_ACCEPT' + expires_at = now + 48ч + assignToCycle
    Orders → 'ACCEPTED_PENDING_SUPPLIER';
    sum < min_threshold → consolidated_request status='EXPIRED_NO_THRESHOLD'
    БЕЗ assignToCycle (Story 4.3 unblk per-Order'ам пула).

- volume_based: метод `evaluateVolumeBasedAfterCreate` вызывается
  СИНХРОННО из `MarketplaceOrderCreateService.applyCycleTypeHook` после
  persist нового Order'а; если sum >= target_volume → instant
  consolidated_request status='PENDING_SUPPLIER_ACCEPT'. Cron-fallback
  `aggregateVolumeBasedExpired` (EVERY_HOUR) — если объём не накоплен
  и now > cycle_start + max_wait_days → consolidated_request status=
  'EXPIRED_NO_VOLUME' (Story 4.3 unblk).

- open_subscription: backend НЕ создаёт автоматически. Mutation
  `marketplaceTriggerOpenSubscription` (Offer:update:own); поставщик
  жмёт «Запустить» → consolidated_request status='ACCEPTED' сразу,
  Orders пула → 'ACCEPTED' (нажатие = акцепт всего пула).

- individual: backend НЕ агрегирует. `applyCycleTypeHook` в
  OrderCreateService делает Order.status: ACTIVE →
  'ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL' сразу после persist.

Backend (controller/extensions/marketplace):
- domain/entities/marketplace-consolidated-request.{types,entity}.ts +
  spec (4 сценария: PENDING/ACCEPTED/terminal-таблица/open_subscription
  triggered).
- domain/repositories/marketplace-consolidated-request.repository.ts +
  infrastructure (TypeORM entity + mapper + adapter; индексы под
  offerer-стол / pre-aggregator idempotency / cron-scan).
- application/services/marketplace-cycle-aggregator.service.ts —
  @Cron(EVERY_5_MINUTES) aggregateTimeBased + @Cron(EVERY_HOUR)
  aggregateVolumeBasedExpired + evaluateVolumeBasedAfterCreate +
  triggerOpenSubscription. ScheduleModule.forRoot() добавлен в
  MarketplaceExtensionApplicationModule imports.
- application/resolvers/marketplace-cycle.resolver.ts — Mutation
  marketplaceTriggerOpenSubscription + Query marketplaceListConsolidatedRequests
  (Offer:update:own / Offer:read access).
- Расширены MarketplaceOrderDomainRepository (findUnassignedActiveByOffer
  / assignToCycle / sumUnassignedActiveByOffer) и
  MarketplaceOfferDomainRepository (listAllActiveTimeBased /
  listAllActiveVolumeBased).
- MarketplaceOrderCreateService.applyCycleTypeHook — per-cycle_type
  backend hook сразу после persist Order'а; для individual instant
  transition status, для volume_based — instant аггрегация при пороге.

Tests (10 сценариев):
- consolidated-request.entity.spec: PENDING/ACCEPTED/4 terminal/
  open_subscription triggered.
- cycle-aggregator.spec: aggregateTimeBased (3: достигнут + sum >= threshold;
  достигнут + sum < threshold; не достигнут), evaluateVolumeBasedAfterCreate
  (2: ниже target / достигнут), triggerOpenSubscription (4: happy /
  ForbiddenException не владелец / BadRequestException неправильный
  cycle_type / BadRequestException пустой пул).
- OrderCreateService spec обновлён под новый constructor signature
  (cycleAggregator dependency).

Не входит в Story 4.2 (отложено):
- UI для offerer-стола «Консолидированные заявки» (Story 4.5 supplier
  accept/decline UI) — там же добавится списочный widget на канон
  дизайн-системы.
- Mutation marketplaceAcceptConsolidatedRequest / DeclineConsolidatedRequest —
  Story 4.5.
- Auto-decline по expires_at (PENDING > 48ч) — Story 4.3 cron-cleanup.
- Persist полей `acceptance_window_hours` в config кооператива (сейчас
  захардкожено 48ч) — Story 9.x.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-9][@ant] feat: добавить auto-unblk Order'ов при провале триггера и cleanup непринятых заявок — закрыть Story 4.3 Эпика 4

Continuation Story 4.2 (PR #386? коммит 0ba2d51): на cron-агрегаторе и
volume_based-cron при достижении terminal-condition (sum < min_threshold
для time_based, sum < target_volume для volume_based, expires_at < now
для PENDING_SUPPLIER_ACCEPT) теперь автоматически разблокируются средства
каждого Order'а пула — без участия пайщика-заказчика или поставщика.

Что сделано (per acceptance criteria Story 4.3):

  1. Расширен canonical port (`MarketplaceCanonicalBlockchainPort`) +
     adapter (`MarketplaceCanonicalBlockchainAdapter`) методом
     `expireOrder({coopname, order_hash})` — обёртка над C++
     `marketplace::expireorder` (canonical action из PR #385 Story 11.1;
     authorization = кооператив, action_name = 'expireorder', серия
     o.mkt.unblk на total_cost + on-chain Order.status: active → cancelled).

  2. Расширен Order-domain-repository: `findByCycleId(coopname, cycle_id)`
     — все Order'ы привязанные к данной консолидированной заявке. Нужно
     для нового cron-cleanup'а — нужно итерироваться по пулу заявки,
     которая не дождалась ответа поставщика.

  3. `MarketplaceCycleAggregatorService` теперь полноценно завершает
     цикл при terminal-condition:

     - В `processTimeBasedOffer` после persist `consolidated_request`
       статуса `EXPIRED_NO_THRESHOLD` (sum < min_threshold) — серия
       `expireOrder` per-Order пула + counter `onOrderUnblocked` +
       Order.status → `EXPIRED_NO_THRESHOLD`.
     - В `aggregateVolumeBasedExpired` аналогично, но Order.status →
       `EXPIRED_NO_VOLUME` при sum < target_volume + now > cycle_start
       + max_wait_days.
     - Новый `@Cron('*/10 * * * *') expireUnacceptedPending()` — каждые
       10 минут сканирует `PENDING_SUPPLIER_ACCEPT` заявки с
       `expires_at < now` (поставщик не нажал ни Accept ни Decline в
       48-часовом окне): переводит заявку в `EXPIRED_NO_RESPONSE` с
       `decline_reason='expired_no_response'` + серия `expireOrder` пулу
       + Order.status → `CANCELLED_BY_SUPPLIER` с тем же reason.

  4. Private helper `expirePoolOnChain(pool, terminalStatus, reason)` —
     инкапсулирует pattern «per-Order chain.expireOrder → counter
     unblock → applyStatusTransition». Best-effort с partial-fail
     tolerance: chain-fail одного Order'а лог error + продолжаем
     остальные (следующий cron-тик подберёт повторно для time_based /
     volume_based; expireUnacceptedPending же — single-shot, mismatch
     админ-reconciliation Story 9.x). Counter-fail (Offer уже неактивен
     и т.п.) — лог warn + всё равно applyStatusTransition (on-chain unblk
     прошёл; единственное место рассинхронизации — quantity_available
     Offer'а, восстанавливается reconciliation).

  5. Side-by-side с `marketplace-order-delta.mapper.ts` поправлен
     устаревший импорт `Interfaces.IOrderRow` → `Interfaces.Marketplace.IOrder`
     (after PR #385 — единый `IOrder` тип).

Тесты (`marketplace-cycle-aggregator.service.spec.ts`):

  Расширены mocks (chainPort + offerCounters + findByCycleId +
  findExpiredAwaitingResponse + applyStatusTransition на orderRepo).
  Добавлены 5 новых сценариев Story 4.3:

  - **EXPIRED_NO_THRESHOLD pool unblk** — time_based ниже порога:
    `chainPort.expireOrder` per-Order × N, `onOrderUnblocked` per-Order × N,
    `applyStatusTransition(_, 'EXPIRED_NO_THRESHOLD', _)` × N.
  - **EXPIRED_NO_VOLUME pool unblk** — volume_based по истечении
    `max_wait_days` без `target_volume`: аналогично.
  - **partial chain fail tolerance** — один Order падает на `expireOrder`,
    остальные `succeeded`; лог error по failed, applyStatusTransition не
    вызывается на failed Order'е.
  - **counter onOrderUnblocked fail — applyStatusTransition всё равно** —
    Offer удалён / неактивен → counter throws, Order всё равно идёт в
    terminal-status (on-chain unblk уже прошёл).
  - **expireUnacceptedPending happy + empty** — pending > 48ч заявка →
    `EXPIRED_NO_RESPONSE` + Order'ы → `CANCELLED_BY_SUPPLIER`. Пустой
    список — early return без расходов.

  Существующий «EXPIRED_NO_THRESHOLD без assignToCycle» расширен Story 4.3
  ассертами (expireOrder × 2, onOrderUnblocked × 2, applyStatusTransition × 2).

НЕ сделано в Story 4.3 (отложено):

  - **acceptance_window_hours в config кооператива** — захардкожено 48ч,
    задача Story 9.x (общая параметризация кооператива).
  - **Реальные интеграционные тесты с C++ marketplace::expireorder** —
    blockchain-side test через nodeos staging, Story 4.6 / CI e2e.
  - **Manual reconciliation endpoint** для partial-fail случаев,
    которые cron не подобрал — Story 9.x (admin tool).
  - **GraphQL Subscription marketplaceOrderUpdated** — pubsub-канал
    Order при auto-expire должен пушиться, но Story 4.1 sync-сервис
    подписан на `delta::marketplace::orders` (on-chain expire триггерит
    дельту через parser2 → syncer → pubsub). Прямой emit pubsub из
    application-сервиса — anti-pattern (INV-12).

Файлы:

  - `domain/ports/marketplace-canonical-blockchain.port.ts` — +expireOrder
  - `domain/repositories/marketplace-order.repository.ts` — +findByCycleId
  - `infrastructure/adapters/marketplace-canonical-blockchain.adapter.ts`
    — +expireOrder impl
  - `infrastructure/adapters/marketplace-order-repository.adapter.ts`
    — +findByCycleId impl
  - `infrastructure/mappers/marketplace-order-delta.mapper.ts` —
    Interfaces.IOrderRow → Interfaces.Marketplace.IOrder fix
  - `application/services/marketplace-cycle-aggregator.service.ts` —
    constructor +chainPort +offerCounters, processTimeBasedOffer и
    aggregateVolumeBasedExpired вызывают expirePoolOnChain, +cron
    expireUnacceptedPending, +private expirePoolOnChain
  - `application/services/marketplace-cycle-aggregator.service.spec.ts`
    — 5 новых сценариев Story 4.3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-10][@ant] feat: добавить отмену Order'а заказчиком до акцепта поставщика — закрыть Story 4.4 Эпика 4

Story 4.4 закрывает разрыв между Story 4.1 («заказчик создаёт Order и
блокирует средства») и Story 4.5 («поставщик акцептует или declined'ит»):
до того, как поставщик нажал Accept (либо до того, как time/volume-
agregator закрыл цикл за него), заказчик может отозвать свой Order сам.

Реализация:

  1. Canonical port + adapter: `cancelOrder({coopname, orderer, order_hash})`
     — обёртка над C++ `marketplace::cancelorder` (action_name='cancelorder',
     authorization = кооператив; C++ внутри `require_auth(coopname)` +
     `eosio::check(actor == order.orderer)` + status==ACTIVE).
     Серия ledger2 `UNBLOCK_ON_CANCEL` (o.mkt.unblk на total_cost) +
     on-chain Order.status: ACTIVE → CANCELLED. Сумма возвращается на
     `w.mkt.member.available` пайщика — может быть потрачена на новый
     заказ или выведена через `o.mkt.recall`.

  2. Новый application service `MarketplaceOrderCancelService`:

     **Guard** — Order существует / coopname match / orderer match /
     status='ACTIVE'. Backend разрешает отмену только в ACTIVE (это
     match с C++ check). После того, как:
      - cron `aggregateTimeBased` / `aggregateVolumeBasedExpired`
        перевёл Order в `ACCEPTED_PENDING_SUPPLIER`, или
      - persist-hook `applyCycleTypeHook` перевёл в
        `ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL` (individual cycle),
     заказчик не может отменить сам — поставщик либо акцептует, либо
     declined'ит через Story 4.5. Это защищает поставщика от
     «закаривал-передумал» паттерна на пограничном acceptance-окне.

     **Chain submit** — `chainPort.cancelOrder({coopname, orderer,
     order_hash})`. Любой C++ check fail (например parallel acceptorder
     race) — clean `BadRequestException` с extracted assertion message;
     ни counter, ни Order.status не модифицируются.

     **Counter** — `offerCounters.onOrderUnblocked(offer_id, quantity)`
     best-effort: counter-fail (Offer уже не ACTIVE) → лог warn + всё
     равно applyStatusTransition (on-chain unblk прошёл).

     **Persist** — Order.status: ACTIVE → CANCELLED_BY_ORDERER с reason
     'Отменён заказчиком'. `cancelled_at` = now (через
     applyStatusTransition).

  3. GraphQL Mutation `marketplaceCancelOrder({order_id})` →
     `MarketplaceCancelOrderResult{order, tx_hash}`. ACL
     `@RequireMarketplaceAccess('Order', 'cancel:own')` — marketplace-role
     orderer + owner-check внутри сервиса (через guard orderer match).
     Полная регистрация permission'а — в Story 4.6 access-matrix update.

  4. UI: канон `OrderCard.vue` (`components/desktop/src/widgets/Marketplace/
     OrderCard`) — для orderer-роли в статусе `placed` (mapping канона
     для backend.ACTIVE) добавлена кнопка `{key:'cancel', label:'Отменить',
     kind:'danger'}`. Parent-страница (Story 4.6 «Мои заказы») перехватит
     `@action` событие с `key='cancel'` и оркестрирует confirm dialog +
     apollo `marketplaceCancelOrder` mutation. OrderCard сам по
     дизайн-канону не диалогизирует — лишь публикует event.

Тесты (`marketplace-order-cancel.service.spec.ts`, 8 сценариев):

  - **happy path** — chain.cancelOrder с {coopname, orderer, order_hash} →
    counter.onOrderUnblocked(offer_id, quantity) → applyStatusTransition
    (order.id, 'CANCELLED_BY_ORDERER', 'Отменён заказчиком'); result
    включает tx_hash и Order DTO.
  - **NotFound** — Order не существует.
  - **Forbidden** — Order чужого кооператива.
  - **Forbidden** — отменяет не заказчик-владелец.
  - **BadRequest для статусов** — `ACCEPTED_PENDING_SUPPLIER`,
    `ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL`, `ACCEPTED`, `RECEIVED`,
    `CANCELLED_BY_ORDERER`: jest.each покрывает 5 ветвей "статус ≠ ACTIVE".
  - **chain fail — clean BadRequest** — Order не модифицирован,
    counter не дёрнут.
  - **counter fail — applyStatusTransition всё равно** — best-effort
    (on-chain unblk уже прошёл, counter рассинхрон правится
    reconciliation Story 9.x).

НЕ сделано (отложено):

  - **Confirm dialog UI** — родительская страница «Мои заказы» Story 4.6
    обернёт `@action(cancel)` в `q-dialog` с подтверждением. На Story 4.4
    OrderCard.vue лишь публикует event (per канон-разделение).
  - **GraphQL Subscription marketplaceOrderUpdated** — pubsub Order при
    CANCELLED_BY_ORDERER должен идти по `delta::marketplace::orders`
    (parser2 → syncer → pubsub). Прямой emit из application — anti-pattern
    (controller/CLAUDE.md INV-12).
  - **Apollo client mutation hook + composable** — клиентская часть
    Story 4.6 («Мои заказы»).
  - **Полная регистрация Order:cancel:own permission в access-matrix** —
    Story 4.6 access-matrix-update package.

Файлы:

  - `domain/ports/marketplace-canonical-blockchain.port.ts` — +cancelOrder
  - `infrastructure/adapters/marketplace-canonical-blockchain.adapter.ts`
    — +cancelOrder impl (паттерн как expireOrder/createOrder)
  - `application/services/marketplace-order-cancel.service.ts` — новый
  - `application/services/marketplace-order-cancel.service.spec.ts` —
    новый (8 сценариев)
  - `application/dto/marketplace-order-input.dto.ts` — +MarketplaceCancelOrderInputDTO
  - `application/dto/marketplace-order.dto.ts` — +MarketplaceCancelOrderResultDTO
  - `application/resolvers/marketplace-order.resolver.ts` —
    +marketplaceCancelOrder Mutation
  - `application/marketplace-application.module.ts` — providers + exports
  - `components/desktop/src/widgets/Marketplace/OrderCard/OrderCard.vue`
    — orderer.placed += cancel/danger кнопка

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-11][@ant] feat: добавить accept/decline поставщика по cycle_type — закрыть Story 4.5 Эпика 4

— расширил port + adapter методами acceptOrder/declineOrder (canonical actions из cooptypes PR #385)
— MarketplaceConsolidatedRequestAcceptDeclineService для batch (time_based/volume_based): per-Order chain.acceptOrder | chain.declineOrder + counter.onOrderUnblocked + applyStatusTransition + consolidated_request → ACCEPTED|DECLINED_BY_SUPPLIER (L10)
— MarketplaceOrderSupplierActionService: acceptIndividual / declineIndividual для cycle_type=individual + declineFromOpenPool для частичного отказа из open_subscription до запуска поставки
— 5 GraphQL Mutation: marketplaceAcceptConsolidatedRequest / marketplaceDeclineConsolidatedRequest / marketplaceAcceptIndividualOrder / marketplaceDeclineIndividualOrder / marketplaceDeclineOrderFromOpenPool с ACL Offer:update:own
— DTO: MarketplaceConsolidatedRequestActionResult / MarketplaceSupplierOrderActionResult + 5 input-DTO
— Best-effort partial-fail tolerance для batch: chain-fail per-Order → лог error + продолжаем; counter-fail → warn + applyStatusTransition всё равно
— UI OrderCard.vue: offerer.placed += decline-кнопка (parent оркестрирует confirm-dialog в Story 4.6)
— Spec: 13 сценариев для consolidated (happy / partial-chain-fail / NotFound / Forbidden / cycle_type / status / counter-fail) + 14 для supplier-action (3 группы happy/forbidden/cycle/status/chain-fail)

* [598-12][@ant] feat: добавить orderer-стол «Мои заказы» с жизненным циклом и отменой — закрыть Story 4.6 Эпика 4

— 3 GraphQL Query в MarketplaceOrderResolver: marketplaceListMyOrders / marketplaceListSupplierOrders / marketplaceGetOrder (ACL Order:read:own | read:to-self | read:all)
— MarketplaceListOrdersInput + MarketplaceGetOrderInput + MarketplaceOrderPaginationResult DTO
— toMarketplaceOrderDTO / toMarketplaceOrderCreateTxSnapshotDTO в dto.ts (общие helpers вне resolver)
— access-matrix: admin += Order:read:all (Story 4.6 AC)
— UI desktop: pages/Marketplace/MyOrders с api/index.ts (raw POST /v1/graphql, паттерн каталога) + types.ts + MyOrdersPage.vue
— MyOrdersPage: grid canon OrderCard, фильтр по статусам, polling 10s (Subscription marketplaceOrderUpdated отложена до kernel pubsub Story 9.x), confirm-dialog для cancel (Story 4.4 mutation)
— mp-role-orderer обёртка + STATUS_TO_CARD маппинг доменных статусов на canon OrderCard статусы (UX-DR20 цветные точки)
— Route /:coopname/market/my-orders переключён с legacy UserSuppliesListPage на MyOrdersPage; legacy сохранён под hidden my-supplies-legacy

* [598-13][@ant] feat: cycle_type при публикации Offer'а — закрыть Story 4.7 Эпика 4

Backend (Story 4.7 / L11):
* MarketplaceOfferService.assertCycleConditionals — per cycle_type
  required-валидация для create и update:
    - time_based       → cycle_days REQUIRED (>=1); min_threshold optional
    - volume_based     → target_volume + max_wait_days REQUIRED (>=1)
    - open_subscription → ничего не required
    - individual        → ничего не required
* На update смена cycle_type валидируется по merged-снимку (текущее
  значение из БД + patch), частичный update без касания cycle-полей
  не валидируется заново.
* Reset status → PENDING_MODERATION при любом update уже работает с
  Story 3.2 (существенное изменение покрыто).
* +11 unit-тестов: create per cycle_type (4 invalid + 3 valid) + update
  cycle_type change (4 сценария).

UI (Story 4.7 canon-форма):
* Новая страница `pages/Marketplace/CreateMarketplaceOffer` —
  offerer-форма публикации canonical Offer'а через mutation
  `marketplaceCreateOffer` (раньше роут `create-offer` указывал на
  legacy CreateParentOfferPage с program_id/unit_cost через старый API).
* mp-role-offerer обёртка + токены `marketplace-tokens.scss`.
* q-option-group для cycle_type с подсказкой текущего варианта
  (4 значения, каждый со своей пометкой).
* Conditional cycle-fields:
    - time_based       → cycle_days (required) + min_threshold (опц.)
    - volume_based     → target_volume + max_wait_days (оба required)
    - open_subscription → max_wait_days (опц.)
    - individual        → нет полей
* При переключении cycle_type — defaults и зачистка чужих полей.
* Frontend-валидация дублирует backend per cycle_type (быстрый UX);
  бэкенд — source of truth.
* После успеха — Notify positive + redirect на `marketplace-user-offers`.
* Legacy CreateParentOfferPage спрятан под `create-offer-legacy`
  (hidden=true) на случай fallback'а; полное удаление — техдолг
  marketplace2.

Не вошло (deferred):
* Seed-миграция «legacy Offer'ы без cycle_type → time_based» не
  нужна: до Story 4.7 в БД нет Offer'ов с NULL cycle_type
  (Story 3.2 ввела NOT NULL колонку с дефолтом). Если позднее
  потребуется backfill старых данных — отдельная миграция Phase 2.
* Замена raw POST `/v1/graphql` на Zeus-типизированные Mutations
  отложена до регенерации `@coopenomics/sdk` (общий техдолг
  Marketplace UI).

Тесты:
* `controller`: marketplace-offer-service.test.ts → 35 passed (11 новых).
* TSC моих файлов: 0 ошибок (89 pre-existing legacy не в скоупе Story).

* [598-13][@ant] fix: правки ревью PR #386 Эпика 4 «Заказ и блокировка средств»

— описания GraphQL DTO/resolver переписаны бизнес-языком (без story/epic-меток)
— `cycle_type` / `status` в DTO и сервисах — через ENUM (registerEnumType + companion-константы), вместо строковых литералов
— стандартная пагинация `PaginationInputDTO + createPaginationResult` в `marketplaceListConsolidatedRequests`, `marketplaceListMyOrders`, `marketplaceListSupplierOrders`
— `ASSET_SYMBOL` / `ASSET_DECIMALS` из хардкода RUB/4 убраны в DI через `MARKETPLACE_ASSET_CONFIG` (factory из `config.blockchain.root_govern_symbol/precision`)
— user-facing ошибки в `marketplace-order-supplier-action.service` переписаны на понятный русский язык с уникальными маркерами поиска `[E4SAS-…]`
— комментарий в `marketplace-order-sync.service.afterForkProcessing` явно фиксирует разделение ответственности: сущности откатываются через `deleteByBlockNumGreaterThan` + parser2 replay, hook компенсирует только счётчики Offer'а
— legacy `CreateParentOfferPage` маршрут снесён вместе с зависимостями (`features/Request/CreateParentOffer/`, `pages/Marketplace/CreateParentOffer/`); канон — `CreateMarketplaceOfferPage`
— `frontend MyOrders/api` адаптирован к новому каноническому пагинационному аргументу `options`
— конфликт двух `@ObjectType('MarketplaceCategory')` разрешён переименованием в `category-tree.dto.ts` → `MarketplaceCategoryTreeNode`
— правила зафиксированы: `controller/CLAUDE.md` (описания, ENUM, пагинация), `desktop/extensions/market/CLAUDE.md` (Zeus pipeline)

Zeus regen на схеме блокирован устаревшим legacy `application/marketplace/*` стэком — отдельная story по cleanup'у; raw GraphQL во `CreateMarketplaceOffer/api` и `MyOrders/api` помечен явной TODO-меткой блокера.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] refactor: снести legacy marketplace стэк, regen Zeus, типизированный SDK для Эпика 4

Backend (controller):
— удалены `src/application/marketplace/*` и `src/domain/marketplace/*` (legacy
  cooplace/marketplace resolver, interactor, service, port, ~30 DTO);
— удалён `infrastructure/blockchain/adapters/marketplace-blockchain.adapter.ts`
  и его регистрация в `blockchain.module.ts`/`app.module.ts`;
— удалены `documents-dto/asset-contribution-*` и `documents-dto/return-by-asset-*`
  (6 DTO) — оставались сиротами после сноса legacy resolver'а;
— переименован `@ObjectType('MarketplaceCategory')` в `category-tree.dto.ts`
  → `MarketplaceCategoryTreeNode` (конфликт имени с baseline-категорией Эпика 3);
— `schema.gql` регенерирована.

SDK (`@coopenomics/sdk`):
— удалены 23 legacy zeus-обёртки в `mutations/marketplace/` (publishRequest,
  updateRequest, supplyOnRequest, unpublishRequest, receiveOnRequest,
  prohibitRequest, moderateRequest, declineRequest, deliverOnRequest,
  disputeOnRequest, completeRequest, cancelRequest, completeReceiveOnRequest,
  confirmSupplyOnRequest, acceptChildOrder, createChildOrder, createParentOffer,
  generateAssetContribution*, generateReturnByAsset*);
— Zeus client regenerated (`zeus/`, `types/controller/`);
— добавлены типизированные обёртки для нового стэка: `Mutations.Marketplace.{CreateOrder,
  CancelOrder, CreateOffer, DetailKU, SetKUStatus, RetryKUGeocode}` и
  `Queries.Marketplace.{ListMyOrders, GetOrder, ListCategories, ListKUDetails}`;
— добавлены селекторы `orderSelector` (Order/CreateResult/CancelResult/
  PaginationResult), `offerSelector` (Offer/Category);
— `DetailKUInput`/`SetKUStatusInput` обновлены до `MarketplaceDetailKUInput`/
  `MarketplaceSetKUStatusInput` (новые имена в схеме).

Desktop:
— удалены 16 legacy `features/Request/*` (Accept/Cancel/Complete/ConfirmRecieve/
  ConfirmSupply/CreateChildOrder/Decline/DeliverOn/DisputeOn/Moderate/Prohibit/
  Publish/RecieveOn/SupplyOn/Unpublish/UpdateRequest);
— удалены legacy `entities/Request/`, `widgets/Marketplace/CreateChildOrderCard/`,
  `widgets/Marketplace/SupplyOrderRequestCard/`, `widgets/Marketplace/RequestCard/`;
— удалены legacy страницы `OfferPage/UserSuppliesList/WarehousePage/ShipmentsPage/
  DisputePage/Moderation/Showcase/UserParentOffers/SuppliesList` и их маршруты;
— `pages/Marketplace/CreateMarketplaceOffer/api/index.ts` и `pages/Marketplace/
  MyOrders/api/index.ts` переписаны с raw GraphQL на типизированные
  `Mutations.Marketplace.*` / `Queries.Marketplace.*`;
— `entities/MarketplaceKUDetails/api/index.ts` — те же обёртки под новые
  имена мутаций;
— redirect после создания Offer'а → `marketplace-catalog` (вместо удалённого
  `marketplace-user-offers`);
— `AGENTS.md` обновлён под актуальный набор страниц.

Тесты:
— `tests/unit/marketplace/marketplace-{moderation,offer-counters}-service.test.ts`
  — добавлены `listAllActiveTimeBased` / `listAllActiveVolumeBased` в моки
  репозитория (приведено в соответствие с domain-интерфейсом).

Память: `feedback_graphql_no_raw_strings_desktop.md` обновлён — блокер
regen снят, raw GraphQL запрещён, процедура generate-schema/generate-client/
sdk build документирована.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:45:44 +05:00
Alex Ant b3c5c0b1a7 feat(cooptypes): canonical TS-foundation marketplace под Story 11.1 (#385)
Publish Docs / build-and-publish-docs (push) Failing after 15m59s
Зеркало TS-сторон контракта marketplace под членскую модель «Стола заказов»
(PR #375). Источник правды — components/contracts/build/contracts/marketplace/marketplace.abi.

Interfaces (src/interfaces/marketplace.ts):
- 18 action-payload интерфейсов: I{CreateOrder, CancelOrder, ExpireOrder,
  AcceptOrder, DeclineOrder, SignSupp, SignChair, SignIss1, SignIss2,
  SubmRetrn, AprRetRem, RejRetRem, AccRetrn, RejRetrn, PropWroff, ExecWroff,
  DeclWroff, Migrate}
- 4 entity-интерфейса: IOrder (с current_warehouse_braname / accept_braname /
  delivery_braname), IReturnRequest, IWriteoffProposal, IWroffItem (с executed)
- Базовые IDocument2 / ISignatureInfo

Contracts (src/contracts/marketplace/):
- Удалены 24 legacy action'а (createOffer/createOrder-old/publishRequest/
  acceptRequest/confirmReceive/coopstock/...) под старую клиринговую модель.
- 18 новых action-обёрток + tables/{orders, retrequests, wroffprops}.
- index.ts реэкспорт по процессам p.mkt.{supply,return,wroff}.

Build: pnpm --filter cooptypes build — green; tsc --noEmit — green.

⚠ Breaking change: legacy desktop-фичи (Request feature, SupplyOrderRequestCard,
OfferPage, CreateChildOrder и др.) сломаются при tsc — их перепишут Stories
эпиков 3.1+/4.x на новые типы. Это ожидаемо: данный PR — pre-foundation.

Discovery Story 4.1 (Эпик 4) показал блокер — без canonical TS Эпик 4 не может
стартовать. Этот PR разблокирует Stories 4.x.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 16:41:16 +05:00
Alex Ant ce3bee0aba refactor(marketplace): Story 3.5 — каталог переведён на канон дизайн-системы (Эпик 10) (#384)
- удалён локальный pages/.../CatalogOfferCard.vue (дублировал вёрстку карточки);
- MarketplaceCatalogPage импортирует canonical CatalogOfferCard из
  widgets/Marketplace/CatalogOfferCard (UX-DR10, Story 10.2.4);
- маппер MarketplaceOfferView → CatalogOffer: title/price/unit/stock/status;
  «Заказать» вынесено в slot actions виджета;
- корневой класс mp-role-orderer + токены --mp-* для spacing/colors,
  отказ от hard-coded color="grey-3/7" в фильтр-чипах;
- empty-state перекрашен через --mp-on-surface-muted.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:41:28 +05:00
Alex Ant cc694dedcc feat(marketplace): Эпик 10 — Дизайн-система Стола Заказов (Stories 10.1+10.2+10.3) (#383)
* [598-13][@ant] feat: добавить токены marketplace и каркас витрины дизайн-системы — Story 10.1 даёт владельцу продукта единую страницу /market/design-system для утверждения 13 UI-компонентов до их применения в функциональных эпиках 1-9 без локального запуска

- src/app/styles/marketplace-tokens.scss — spacing (UX-DR22), touch-targets 44/48 (UX-DR23), per-role layout (UX-DR31), statusbar (UX-DR20), helper-классы
- src/pages/Marketplace/DesignSystem/ — каркас витрины: aside-навигация по 13 компонентам, переключатели роли/темы/breakpoint в URL query, заглушки Placeholder для будущих секций
- TokensSection — превью палитры, типографики, spacing-scale, touch-targets, per-role layout
- extensions/market/install.ts — route marketplace-design-system под /market/design-system (roles: chairman, member)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить CatalogOfferCard (UX-DR10) — карточка предложения в каталоге Витрины становится каноническим компонентом Стола Заказов, обёртка над существующим RequestCard с расширенным API status (draft/moderation/published/paused/sold-out/completed) + slot actions + fallback изображения для применения в эпиках 3 (Витрина) и 4 (Заказ)

- widgets/Marketplace/CatalogOfferCard — типизированный CatalogOffer + 6 статусов + slot actions + per-role POS-увеличение title
- секция CatalogOfferCard в витрине дизайн-системы со всеми 6 статусами + edge cases (нет preview, длинный текст, slot actions)
- статус в каталоге: imported → ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить OrderCard (UX-DR9) — карточка заказа с 10 статусами жизненного цикла и per-роль actions для применения в эпиках 4 (Заказ), 5 (Поставка), 6 (Выдача), 9 (Склад) — Стол Заказов получает единый компонент списка заказов вместо ad-hoc вёрстки

- widgets/Marketplace/OrderCard — Order interface с 10 status (draft → issued + dispute/returned) + 4 роли (orderer/offerer/operator/admin) с собственным набором actions
- секция витрины переключает симуляцию роли и показывает все 10 статусов в одной сетке
- статус в каталоге: planned → ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить TakeoverDialog (UX-DR7) — full-screen takeover с 4 kind для критических действий выдачи (Эпик 6), отмены заказа (Эпик 4), спора (Эпик 7), списания скоропорта (Эпик 8) даёт единый паттерн необратимых операций со встроенным confirm-loader и slot actions

- widgets/Marketplace/TakeoverDialog — TakeoverKind info/success/warning/danger + цветной header-bar + slot для body и actions + confirm-loader
- секция витрины с 4 пресетами (open by kind) + комментарий + последнее событие
- статус в каталоге: planned → ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить WalletTimeline (UX-DR8) — лента движений кошелька с 6 типами операций (пополнение, блокировка, разблокировка, списание, возврат, выплата) для применения в эпиках 4 (Заказ и блокировка средств) и 9 (Склад и отчётность), даёт пайщику читаемую историю и связку с конкретными заказами

- widgets/Marketplace/WalletTimeline — WalletEntry + WalletEntryKind (6 типов) + цветовая семантика + знак суммы + связь с orderId
- секция витрины с фильтром по типам + рендер бок-о-бок normal vs empty state
- статус в каталоге: planned → ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить BarcodeScanner (UX-DR11) и BarcodeDisplay (UX-DR12) — mock-сканер с visual feedback вместо звукового пика (UX-DR26) и SVG-рендер штрих-кода для печати на ТТН — операторский стол ПВЗ (Эпик 5 Поставка, Эпик 6 Выдача) получает согласованный паттерн ввода/вывода штрих-кодов до подключения jsbarcode/quagga в функциональной реализации

- widgets/Marketplace/BarcodeScanner — 5 состояний (idle/requesting/scanning/success/error) с lazer-анимацией и flash-эффектом
- widgets/Marketplace/BarcodeDisplay — SVG-генерация с 3 size (sm/md/lg) и опцией без подписи
- секции витрины: успех vs ошибка камеры; sizes-grid + кастомный код в инпуте
- статусы в каталоге: planned → ready (для обоих)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить CorrectionTable (UX-DR13) и TTNPrintPreview (UX-DR15) — таблица корректировки приёмки факт vs план и предпросмотр печатной ТТН А5 со встроенным штрих-кодом — операторский стол ПВЗ (Эпики 5 Поставка и 6 Выдача) получает согласованный паттерн приёмки имущества и сопровождающего печатного документа

- widgets/Marketplace/CorrectionTable — inline-input факт, цветовая подсветка дельты, счётчики совпадений/недостач/избытков
- widgets/Marketplace/TTNPrintPreview — формат А5 (148×210 мм), BarcodeDisplay в шапке, таблица позиций, две подписи, @media print
- секции витрины: CorrectionTable с 5 примерами (совпадение/недостача/избыток/полное отсутствие); TTNPrintPreview с реалистичным mock-документом
- статусы в каталоге: planned → ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить ExpeditorGroupingBoard (UX-DR14) WarehouseSummaryGrid (UX-DR16) OnboardingCPPGate (UX-DR17) MultiChannelStatus (UX-DR18) и секцию KUMapWithList — закрыть оставшиеся 5 custom-компонентов Эпика 10, чтобы все 13 имели визуальный канон для эпиков 1 (Онбординг), 5 (Доставка/группировка), 9 (Склад), и cross-cutting NotificationCenter

- ExpeditorGroupingBoard — drag-n-drop карточек между маршрутами + empty-state, событие move
- WarehouseSummaryGrid — admin-плотная таблица приход/расход/остаток с фильтром и сортировкой
- OnboardingCPPGate — L3-gate с required/optional/locked документами, два примера (orderer vs offerer)
- MultiChannelStatus — chip-row push/email/SMS с tooltip-details и 6 status
- KUMapWithList — секция-обёртка над существующим widgets/KUMapWithList с пояснением что реальная карта работает на /market-pvz/list
- все 13 статусов в каталоге переведены в ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить Story 10.3 performance baseline и правила для агентов — закрыть Эпик 10 секцией performance в витрине, инструкцией PERFORMANCE.md с эталонными страницами P1-P4 и целевыми метриками MVP/Phase 2, плюс CLAUDE.md в extensions/market/ как канон-директива для всех агентов работающих с marketplace UI

- pages/Marketplace/DesignSystem/PERFORMANCE.md — manual Lighthouse workflow, NFR-P1/P2/P3, целевые метрики
- pages/Marketplace/DesignSystem/ui/sections/PerformanceSection.vue — секция витрины с эталонными страницами и таблицей метрик
- extensions/market/CLAUDE.md — таблица 13 канонических компонентов, 5 жёстких правил для агентов (inline-вёрстка запрещена, токены только через CSS-переменные, per-роль через корневой класс, новый компонент сначала в витрине, расширение API через витрину)
- статус в каталоге: добавлена секция performance (ready)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] fix: восстановить плоские реэкспорты в entities/MarketplaceKUDetails — индекс экспортировал ./model только через namespace MarketplaceKUDetailsModel, из-за чего useMarketplaceKUDetailsStore / IMarketplaceKUDetails / IWorkingHours не резолвились в PvzListPage, KUMapWithList и MarketplaceDetailKUDialog (vue-tsc валился на всех ветках marketplace2) — добавлен `export * from './model'` параллельно с namespace, обратной совместимости не теряем

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] fix: починить ESLint и pug-парсер на marketplace2 — pug-парсер в ShipmentsPage и DisputePage падал на `#{{ id }}` (символ # после текста = id-селектор Pug), из-за чего ESLint считал весь скрипт unused; заменены на `№{{ id }}`; параллельно убраны мелкие unused в WarehousePage, PvzListPage, useDesignSystemState, ExpeditorGroupingBoard — финально npm run lint чист на src/extensions

- pages/Marketplace/ShipmentsPage/ui/ShipmentsPage.vue — `Перевозка #{{ id }}` → `Перевозка №{{ id }}` (pug syntax fix)
- pages/Marketplace/DisputePage/ui/DisputePage.vue — `Претензия #{{ id }}` → `Претензия №{{ id }}` (pug syntax fix)
- pages/Marketplace/WarehousePage/ui/WarehousePage.vue — удалён неиспользуемый import client; параметр _request заглушен через void (ESLint не учитывает underscore-prefix)
- pages/Marketplace/PvzList/ui/PvzListPage.vue — _unused-driver и _pvz-параметр заглушены через void
- pages/Marketplace/DesignSystem/composables/useDesignSystemState.ts — убран лишний import ref
- widgets/Marketplace/ExpeditorGroupingBoard — убрано неиспользуемое `const props =`, defineProps без присваивания

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] refactor: минимализм Айва в токенах и каталоге — нейтральная шкала + статус-чипы с точкой + терминология «заказать/закончилось» — фидбэк владельца продукта 2026-05-15 о попугайных бэйджах, выпадающей кнопке «Купить» и расхождении со словарём ЦПП (это не торговля, а заказы)

* [598-13][@ant] refactor: адаптив карточек на xs/sm — meta-grid в OrderCard и карточный fallback в CorrectionTable с touch-friendly полем «факт» — фидбэк 2026-05-15 что на узких экранах верстка просто сжимается и в поле «факт» невозможно попасть пальцем; теперь meta переносится через auto-fit grid а на <600px CorrectionTable рендерится карточками

* [598-13][@ant] refactor: единый стиль кнопок и статуса в OrderCard — primary unelevated без тени + flat second-level + mp-status-chip вместо разноцветного q-badge — фидбэк 2026-05-15 что «Открыть» (с тенью) и «Подробнее» (flat) выглядят попугайно и не из одного дизайна; теперь все actions одной формы с единым радиусом

* [598-13][@ant] refactor: убрать «попугайную» цветную шапку TakeoverDialog — нейтральный header + 4px accent-stripe слева + цветная иконка — фидбэк 2026-05-15 что info-карточка с залитой primary-шапкой выглядит броско и одинаково для info и success; теперь акцент через 4px полосу (primary/positive/warning/negative)

* [598-13][@ant] fix: контраст BarcodeScanner на тёмной теме — явный rgba-overlay вместо опоры на text-grey-7 + flash-анимация возвращается в полупрозрачный чёрный а не в transparent — фидбэк 2026-05-15 что на dark внизу мелькает белая полоска и текст «Сканер готов» теряет фон

* [598-13][@ant] feat: ExpeditorGroupingBoard — снять инлайн text-color/bg + добавить move-all для оптом-переноса — фидбэк 2026-05-15 что на dark черный текст «3 заявки ожидают группировки» уходит в чёрный фон и что перетаскивать 10 единиц одного заказа по одной — неудобно; теперь токены поверхности + кнопка «Переместить всё из N» рядом с droptarget

* [598-13][@ant] refactor: WarehouseSummaryGrid — снять колонку статус «достаточно/мало» — фидбэк 2026-05-15 что КУ кооператива работает транзитом (пришло — ушло), это не торговая точка с минимальным остатком; ввели подсветку остатка серым при нуле — этого достаточно

* [598-13][@ant] refactor: OnboardingCPPGate — одна оферта ЦПП per-роль и нейтральный «обязательно»-чип — фидбэк 2026-05-15 что в архитектуре нет ПВЗ-логистики/ценовой политики/режима самозанятого и подписки на рассылку; L2 на регистрации уже принимает обработку данных и базовые правила платформы — здесь дублировать нельзя

* [598-13][@ant] refactor: MultiChannelStatus — нейтральные чипы с точкой состояния и нормальные отступы — фидбэк 2026-05-15 что разноцветные q-chip (info/positive/primary/negative) выглядят попугайно и прижаты друг к другу; теперь surface-1 фон + 1px-граница + цветная точка-индикатор + gap 16px

* [598-13][@ant] fix: безопасное переключение light/dark — снять хардкод-фоны на CatalogOfferCard статус-чипе, DesignSystemPage preview-обёртке и заменить bg-grey-2 в секциях на mp-event-banner поверх токенов — фидбэк 2026-05-15 что статус-чип «Распродано» сливается на dark и часть баннеров не реагирует на тему; теперь все surface — через --mp-surface-* и --mp-border-subtle, переключение работает автоматически

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:04:58 +05:00
Alex Ant 173014f712 feat(marketplace): Эпик 3 — Витрина: публикация, модерация, каталог — Stories 3.1+3.2+3.3+3.4+3.5 (#382)
* [598-6][@ant] feat: backend whitelist + дефолтная витрина — Story 3.1

Story 3.1 «Управление whitelist поставщиков + дефолтная витрина»:

— Domain entities `MarketplaceVitrineDomainEntity`,
  `MarketplaceWhitelistEntryDomainEntity` (role: 'auto-coop' | 'manual')
  и domain-репозитории `MarketplaceVitrineDomainRepository`,
  `MarketplaceWhitelistDomainRepository` (DIP — реализация в infra).
— TypeORM-entities `marketplace_vitrine` (PK=id, MVP всегда 'default'),
  `marketplace_whitelist` (UUID, UNIQUE(coop, member)). Подключение
  `marketplace` уже работает с `synchronize:true` — DDL автоматический.
— Mappers + adapters; идемпотентные `ensureDefault` / `add(role)`.
— Bootstrap-миграция v3 `marketplaceBootstrapV3Migration`: в afterMigrate
  создаёт `{id:'default', display_name:'Стол заказов'}` + auto-coop
  whitelist entry (FR5 — перепоставка остатков самим коопом).
— `MarketplaceWhitelistService.isOfferer` — источник `context.isOfferer`
  для `mapCoreRolesToMarketplaceRoles`: «открытая витрина» (только
  auto-coop) → true для всех User; «по whitelist» → true только для
  manual-записей. TTL-кеш 60s, инвалидируется при add/remove.
— `MarketplaceVitrineService.getDefault/list` — read-only API
  конфигурации витрины (конструктор кастомных витрин Out-of-MVP).
— GraphQL endpoints:
  `marketplaceListWhitelist` / `marketplaceAddToWhitelist` /
  `marketplaceRemoveFromWhitelist` под `@RequireMarketplaceAccess(
  'Whitelist','manage')` (admin); `marketplaceDefaultVitrine` под
  `'Vitrine','read'` (всем marketplace-ролям). auto-coop неудаляем.
— Access-matrix: добавлен `Vitrine:['read']` для offerer/operator,
  расширены `admin: Offer:['moderate','read']`, `Vitrine:['manage','read']`.
— `MarketplaceMembershipGuard` теперь async, дёргает
  `whitelistService.isOfferer(coopname, username)` для генерации
  marketplace-роли `offerer` на каждый GraphQL-запрос (через cache).
— Регистрация миграции v3 в `extension-domain.module.ts`, новых
  entities/adapters/mappers/repositories в `marketplace-infrastructure
  .module.ts`, сервисов/резолверов в `marketplace-application.module.ts`.

Тесты: 21 unit-кейс (`tests/unit/marketplace/`):
— marketplace-whitelist-service.test.ts (11): list, add, remove
  manual/auto-coop/missing, isOfferer семантика 3-х режимов + TTL-кеш
  + инвалидация на add/remove;
— marketplace-vitrine-service.test.ts (3): getDefault / null / list;
— marketplace-membership-guard.test.ts (7, обновлён под async + DI):
  open vitrine → +offerer, whitelist → no offerer, member/chairman
  роли, status/auth/server-secret bypass.

pnpm tsc проходит чисто на изменённых файлах (предсуществующие ошибки
в `file-storage/` не относятся к Story 3.1). targeted jest зелёный.

Зависимости: Эпик 1 (whitelist использует marketplace-roles.mapper,
access-matrix, MembershipGuard). Не блокирует и не блокируется

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-6][@ant] feat: backend Offer CRUD + 10 baseline-категорий — Story 3.2

Story 3.2 «Поставщик публикует и управляет жизненным циклом Offer'а»:

— Domain: `MarketplaceOfferDomainEntity` (полный набор полей под
  Stories 3.2/3.3/3.4 — quantity_available/blocked/consumed,
  cycle_type/days/target_volume/max_wait_days/min_threshold,
  status PENDING_MODERATION→ACTIVE→REJECTED/WITHDRAWN,
  approved_by/at, rejected_by/at, reject_reason);
  `MarketplaceCategoryDomainEntity` (справочник 10 baseline);
  типы `MarketplaceOfferStatus`/`CycleType`/`UnitOfMeasure`.
— TypeORM-entities `marketplace_offer` (UUID, 3 индекса под Stories 3.2/3.5)
  и `marketplace_category` (PK=integer, seed-таблица).
— Repositories: `MarketplaceOfferDomainRepository`
  (findById/list/countByCategory/countRecentCreatedBy/create/applyUpdate)
  + `MarketplaceCategoryDomainRepository` (listBaseline/findById/upsertBaseline).
— Adapters TypeORM-based с QueryBuilder (фильтры по supplier/status/
  category/available_only, сортировки по created_at/price);
  countByCategory считает только ACTIVE + (unlimited OR available>0).
— `MarketplaceOfferService` инкапсулирует AC Story 3.2:
  • create: PENDING_MODERATION + валидация (product_name 1..200,
    description ≤2000, baseline category 1..10, numeric price 0-4 знака,
    cycle/unit enums, quantity invariants);
  • rate-limit: 10 Offer'ов/час на supplier (`countRecentCreatedBy`);
  • update: ownership + reset в PENDING_MODERATION; REJECTED/WITHDRAWN → 403;
  • withdraw: ownership + status WITHDRAWN; stub `hasActiveOrders`=false
    (точка интеграции с Story 4.x order-репозиторием после merge #375);
  • unlimited_flag=true атомарно обнуляет quantity_available.
— `MarketplaceCategoryService.listBaseline` — read для UI (форма
  создания Offer'а + фильтр-чипы Story 3.5).
— GraphQL resolver: `marketplaceCreateOffer` / `marketplaceUpdateOffer` /
  `marketplaceWithdrawOffer` / `marketplaceListMyOffers` —
  `@RequireMarketplaceAccess('Offer','create:own'|'update:own'|'delete:own')`;
  `marketplaceListCategories` — `'Offer','read'`.
— DTO: MarketplaceOfferDTO + MarketplaceOfferPageDTO + MarketplaceCategoryDTO,
  Create/Update/Withdraw/ListMy Input DTOs (class-validator: Min/Max/Matches/
  IsIn для enum-полей).
— Bootstrap-миграция v4 (`marketplace-bootstrap-v4`): идемпотентный upsert
  10 категорий через `categoryRepo.upsertBaseline()`. DDL — synchronize:true.
— Регистрация: новые entities/adapters/mappers/repositories в
  infrastructure.module, services/resolver в application.module,
  миграция v4 в extension-domain.module.

Тесты: 24 unit-кейса в `marketplace-offer-service.test.ts`:
— create (11): happy path + rate-limit + категория вне baseline +
  отсутствующая в БД + product_name пустой/>200 + quantity invariants +
  unlimited_flag → 0 + неверные cycle/unit/price;
— update (6): reset в PENDING_MODERATION + ownership + WITHDRAWN/REJECTED 403
  + 404 + unlimited→0 + invalid category;
— withdraw (5): happy + ownership + уже WITHDRAWN + 404 + sentinel под
  active-orders;
— listMine + getById (2).

`pnpm tsc --noEmit` чисто; jest зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-6][@ant] feat: backend модерация Offer'ов админом — Story 3.3

Story 3.3 «Модерация Offer'ов админом (approve/reject + комментарий)»:

— Domain: `MarketplaceModerationLogDomainEntity` (offer_id, action,
  by_account, reason, created_at; append-only) + repository интерфейс
  `MarketplaceModerationLogDomainRepository.append/listByOffer`.
— TypeORM entity `marketplace_moderation_log` (UUID PK, индексы по
  offer_id+created_at и by_account); adapter + mapper.
— `MarketplaceModerationService`:
  • listPending — `repo.list({status:PENDING_MODERATION}, paging)`;
  • approve(offer_id, admin): PENDING → ACTIVE, approved_by/approved_at,
    очистка rejected_*; append log с action='approve';
    EventEmitter2.emit `marketplace.offer.approved` (порядок save→emit
    соблюдён, INV-12 controller/CLAUDE.md);
  • reject(offer_id, admin, reason): обязательное и trim'нутое reason
    1..1000 char; PENDING → REJECTED + rejected_by/at + reject_reason;
    log с action='reject'; emit `marketplace.offer.rejected`;
  • listLog — append-only история по offer'у;
  • Не-PENDING статус → 409 Conflict; missing → 404; reason invalid → 400.
— GraphQL resolver `marketplace-moderation.resolver.ts`:
  `marketplaceListPendingOffers` / `marketplaceApproveOffer` /
  `marketplaceRejectOffer` / `marketplaceListModerationLog` — все под
  `@RequireMarketplaceAccess('Offer','moderate')` (только admin).
— DTO: Approve/Reject/ListPending Input + ModerationLogEntry Output.
— Подключение в infrastructure + application модули; EventEmitter2 уже
  в core-ApplicationModule (через `@nestjs/event-emitter`).

Тесты: 10 unit-кейсов в `marketplace-moderation-service.test.ts`:
— approve (3): happy + 409 на не-PENDING + 404;
— reject (5): happy + empty reason 400 + >1000 chars 400 + 409 на
  уже-rejected + trim применяется к reason;
— listPending+listLog (2): корректная делегация с filter/paging.

`pnpm tsc --noEmit` чисто; jest зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-6][@ant] feat: backend counters available/blocked/consumed — Story 3.4

Story 3.4 «Backend ведёт available/blocked count Offer'ов»:

— Domain контракт `MarketplaceOfferDomainRepository` расширен 3-мя
  атомарными дельтами: `applyBlockDelta` / `applyUnblockDelta` /
  `applyConsumeDelta`. Возвращают `OfferCountersDeltaResult` —
  discriminated `{ok, reason?, offer?}` с reason'ами `insufficient_*`,
  `offer_not_active`, `offer_not_found`.
— Adapter реализован одним SQL UPDATE с WHERE-CAS-условием и RETURNING:
  • block: `quantity_blocked+=K`, `available -= K` (если не unlimited);
    WHERE status=ACTIVE AND (unlimited OR available>=K);
  • unblock: `quantity_blocked-=K`, `available += K` (если не unlimited);
    WHERE blocked>=K;
  • consume: `quantity_blocked-=K`, `quantity_consumed+=K`;
    WHERE blocked>=K.
  0 affected rows → fallback findOne + классификация причины. Атомарность
  обеспечивается одним SQL-statement'ом, гонки между параллельными
  Order-блокировками не разрушают инвариант.
— `MarketplaceOfferCountersService` — точка интеграции с Эпиком 4
  (`o.mkt.block/unblock/consume` canonical из PR #375): order-side
  syncer вызывает `onOrderBlocked/Unblocked/Consumed/Adjusted` внутри
  `dispatch` после `save` Order'а до `emit pubsub` (INV-12, см.
  controller/CLAUDE.md). Валидация qty > 0 integer; перевод reason →
  NestJS exception (404/400). EventEmitter2 emit
  `marketplace.offer.counters.changed` после успешной операции — Story
  3.5 каталог и offerer-«Активность» подписываются (Phase 2 GraphQL Sub).
— `onOrderAdjusted(qty_diff)` (FR23 «факт меньше заказа») семантически
  делегирует в `onOrderUnblocked`.

Инвариант (для не-unlimited):
  `available + blocked + consumed = lifetime_published`
поддерживается дельтами (изменение суммы 0 на каждом методе).

Подключение в `marketplace-application.module.ts`. Существующие моки в
тестах 3.2 / 3.3 расширены под новый интерфейс репозитория.

Тесты: 11 unit-кейсов в `marketplace-offer-counters-service.test.ts`:
— happy paths (4: block / unblock / consume / adjusted-delegate);
— qty validation (1: 0 / negative / non-integer);
— классификация reason → exception (4: insufficient_available/blocked,
  not_active, not_found);
— emit skip on error (1);
— math invariant (1): block 10 → unblock 3 → consume 7, net 0.

Атомарность SQL CAS-update — интеграционный уровень
(testcontainers PG, after merge Эпика 4). Точка интеграции зафиксирована
интерфейсом.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-6][@ant] feat: каталог Offer'ов на orderer-столе — Story 3.5

Story 3.5 «Каталог Offer'ов с фильтром-чипами по 10 категориям»:

Backend:
— `marketplaceListCatalog(input?: ListCatalogInput)`: только ACTIVE +
  (unlimited OR available>0), фильтр `category_id`, paging limit/offset
  (default 24/0), sort `created_at_desc` (default) | `price_asc` |
  `price_desc`. Доступ `@RequireMarketplaceAccess('Offer','read')` —
  всем marketplace-ролям.
— `marketplaceCategoryOfferCounts`: счётчики активных Offer'ов per
  category для фильтр-чипов. Гарантированно возвращает все 10
  baseline-категорий (даже count=0) — UX-DR10 рисует полный набор чипов.
— DTO: `MarketplaceListCatalogInput` (class-validator: category_id
  1..10, sort IsIn, paging Min/Max), `MarketplaceCategoryOfferCount`.
— Резолвер `MarketplaceCatalogResolver` подключён в application-module.

Frontend (FSD slim — отдельный pages/Marketplace/MarketplaceCatalog):
— `types.ts` — ручная типизация MarketplaceOfferView /
  CategoryOfferCount / CatalogSort / CatalogFilter (техдолг: после
  cooptypes:gen-zeus переписать на `Queries.Marketplace.*` из SDK,
  паттерн из PR #381).
— `api/index.ts` — raw GraphQL через `sendPOST('/v1/graphql')`
  (`fetchCatalog` / `fetchCategories` / `fetchCategoryOfferCounts`),
  обработка `body.errors` как throw.
— `ui/CatalogOfferCard.vue` (UX-DR10 — карточка):
  product_name (≤2 строки ellipsis), supplier, цена-за-единицу с
  правильным unit-label (шт/кг/л/упак), quantity_label («Без
  ограничений» или «Доступно: N ед»), cycle-label per cycle_type
  с правильными формулировками AC, warranty-чип, кнопка «Заказать»
  (disabled при отсутствии остатка); aria-label, ellipsis-2-lines.
— `ui/MarketplaceCatalogPage.vue`:
  • горизонтальная полоса фильтр-чипов «Все» + 10 категорий с
    counter-badge per чип (`scroll-x` под mobile);
  • сортировка через q-select (3 опции);
  • responsive grid (`col-12 col-sm-6 col-md-4 col-lg-3`), pagination
    24/страницу через `q-infinite-scroll`;
  • EmptyState компонент при пустом результате (фильтр / каталог);
  • aria-label на регион/чипы/карточки;
  • Order-форма (Эпик 4 Story 4.1) — пока Notify-stub с указанием
    точки интеграции.

`install.ts`:
— `defaultRoute: 'marketplace-catalog'` (новая страница);
— route `/:coopname/market/catalog` → `MarketplaceCatalogPage`;
— legacy `marketplace-showcase` помечен `hidden:true` (donor-страница
  на старой Marketplace архитектуре, см. project_stol_zakazov_mvp
  решение 2026-05-12 «donor переписывается без переходников»;
  переписывается в Phase 2 / последующем PR Stories 3.2 frontend).

vue-tsc: на новых файлах ошибок нет (`MapIterator` итерация заменена
на `Array.from(...).reduce`). Известные baseline-ошибки `Cannot find
module 'src/...'` во всех `extensions/*/install.ts` (включая
`branch`, `capital`, `chairman` — pre-existing).

Тесты: backend-резолвер — thin forward в `offerRepo.list` /
`countByCategory`, оба метода покрыты на adapter-уровне (тесты
catalog filtering — интеграционный testcontainers PG); карточная
логика label-форматтеров — компонентный тест Phase 2.

Limitations:
— Order-форма stub (Эпик 4);
— Zeus типы в SDK — техдолг (как в PR #381 marketplace KU details);
— WCAG 2.1 AA полный audit — Phase 2 (Эпик 10 Story 10.2);
— Donor-страницы Marketplace/Showcase, CreateParentOffer,
  UserParentOffers, Moderation не переписаны под новые GraphQL
  мутации Stories 3.2/3.3 — это работа отдельного UI-PR (текущий
  backend mutations работают, donor-UI закрыт legacy путём).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-6][@ant] chore: BC-sync integration seam — onOrderRolledBack + OrderSync scaffolding

Закрытие разрыва «где живёт blockchain-synchronization для Эпика 3»,
явный seam в коде вместо разбросанных JSDoc-комментариев.

Контекст: ревью пользователя 2026-05-15 — Эпик 3 был доставлен как
backend-only, и Story 3.4 counters стояли как stub-callback без явной
sync-точки. Слабая фиксация интеграции (только JSDoc) — исправляется.

Source-of-truth паттерна (ARCH-документы blago):
— `13-platforma-tsifrovogo-kooperativa/components/14-versiya-3/
   requirements/c3-arch-sinkhronizatsiya-uzla-s-blokcheynom-v1.md`
   (ADR-001 .. ADR-012);
— `4f-arch-integratsiya-kontrollera-s-parser2-v1.md` (DEC-T01..T12
   ParserClient).

Спецификация интеграции (для Эпика 4):
— `_bmad-output/implementation-artifacts/spec-3-4-bc-integration.md`
  (проект `1-prilozhenie-stol-zakazov`, компонент `3-minimalnyy-produkt`)
  — фиксирует contract Эпик 3 ↔ Эпик 4: какие методы counters-сервиса
  дёргаются на какие canonical actions (#375 PR), какой
  subscriptionId / consumerName / startFromBlock для ParserClient,
  какой ForkRegistry handler, чек-лист what Эпик 4 должен реализовать
  (composite-entity Order, mapper, repository, sync-service,
  subscription, side-effect injection, fork handler, write-mutation
  pool, GraphQL Subscription).

Изменения в коде:

1. `MarketplaceOfferDomainRepository.applyRollbackDelta(offer_id, qty)`
   — новый contract-метод для ADR-005 ForkRegistry handler'а. Реализован
   в TypeORM-adapter'е через SQL UPDATE **без** CAS-проверки
   `blocked>=qty` (rollback может приходить когда счётчик уже в
   consumed-состоянии — это ожидаемо при катастрофе fork-вне-Rollback-
   Horizon; manual reconciliation FR12 ARCH-sync).

2. `MarketplaceOfferCountersService.onOrderRolledBack(offer_id, qty)`
   — публичный метод target-сервиса. Дёргается из
   `MarketplaceOrderSyncService.handleFork` для каждого откатываемого
   Order'а в block-состоянии. Emit `marketplace.offer.counters.changed`
   с op='rollback' (Story 3.5 каталог-subscription и offerer-«Активность»
   получают update).

3. `extensions/marketplace/sync/marketplace-order-sync.service.ts` —
   **scaffolding** `MarketplaceOrderSyncService`. Skeleton-класс с
   тремя методами (`start`, `dispatch`, `handleFork`) — все throw
   `'NOT IMPLEMENTED — Эпик 4'`. В JSDoc — полный контракт:
   — `@DomainKey({primary:'id', sync:'order_id'})`,
   — `@SyncBehaviour({forkPolicy:'rollback-via-versions', dlq:true})`,
   — `@Versioned({strategy:'entity_versions'})`,
   — subscription `controller-${coopname}` `primary` `last_known`,
   — мапинг `p.mkt.supply.createorder` → `onOrderBlocked`,
     `cancelorder`/`expireorder`/`declineorder` → `onOrderUnblocked`,
     `consume`+`consume2` → `onOrderConsumed`,
     FR23 → `onOrderAdjusted`,
   — fork handler → `onOrderRolledBack` per Order;
   — ссылка на spec-3-4-bc-integration.md.

   Это фиксирует **место в коде** для Эпика 4 — а не «JSDoc в любом
   из 5 файлов»; чек-лист реализации виден один в одном файле.

4. Тесты:
   — `applyRollbackDelta` добавлен в моки `MarketplaceOfferDomainRepository`
     во всех 3-х existing test-файлах (offer-service, moderation-service,
     offer-counters-service);
   — новый кейс в `marketplace-offer-counters-service.test.ts`:
     `onOrderRolledBack (ADR-005 ForkRegistry handler) → applyRollbackDelta
     + emit op:rollback` — 12-й тест в этом файле, 67-й в Эпике 3.

`pnpm tsc` чисто; `pnpm jest tests/unit/marketplace/marketplace-offer-
counters-service.test.ts` — 12/12 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-6][@ant] refactor: review feedback PR #382 — coopname, общая пагинация, RU-сообщения, продовольственные категории

— coopname вместо cooperative_id во всём marketplace (поле, колонки, индексы, UNIQUE constraint, DTO, resolvers, миграции, spec); blockchain-aligned именование как в capital/reports.
— общий PaginationInputDTO/createPaginationResult/PaginationResultDomainInterface как в payment-methods и documents; MarketplaceOfferPaginationResultDTO заменил MarketplaceOfferPageDTO; List*InputDTO наследуют PaginationInputDTO с page/limit/sortBy/sortOrder.
— сообщения ошибок на пользовательский русский без терминов available/blocked/offer/withdraw; пользователь видит понятное «Недостаточно свободного количества», «Предложение неактивно», «Можно изменять только свои предложения».
— baseline-категории: 8 продовольственных (овощи/фрукты, молочные, мясо/птица, рыба/морепродукты, хлеб/выпечка, бакалея, напитки, готовая еда) + «Прочее»; удалены услуги ремонта/доставки, хозяйственные, стройматериалы и прочие непродовольственные (вне MVP по PRD 3.2.7).

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:55:12 +05:00
Alex Ant a02e0fe0bf feat(marketplace): Story 11.1 — 18 canonical actions через Ledger2::apply на 13 операциях (#375)
Publish Docs / build-and-publish-docs (push) Failing after 15m19s
* fix(ledger2): handle WalletOp::REVOKE in walletop dispatcher (Story 11.4)

REVOKE (op_code=6) was added to enum and OPERATION_REGISTRY for o.mkt.consum
(2026-05-11), but walletop.cpp rejected op_code > 4. Added explicit case:
списывает amount с wallet_from.blocked в пустоту, без зачисления; L3-зеркало
для USER_SHARED. NONE добавлен как unreachable case для исчерпывающего switch.

Прерывает блокер для Story 11.1 (canonical marketplace actions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(marketplace): canonical scaffolding — entity tables + 18 action skeletons (Story 11.1)

Удалены donor-actions старой клиринговой модели (AR30):
- 50 .cpp файлов в components/contracts/cpp/marketplace/src/{deliver_on_offer,
  deliver_on_order,shipment,dispute_on_offer,корень};
- legacy marketplace.{cpp,hpp} с ~30 actions (orderoffer/accept/supply/dispute/
  shipment/etc) на legacy Wallet:: API мимо ledger2;
- три donor entity-таблицы: lib/domain/table_marketplace_{requests,segments,
  shipments}.hpp;
- lib/core/marketplace/marketplace.hpp donor-helpers (get_request_by_hash,
  get_shipment_by_hash, namespace DocumentNames клирингового состава).

Создана canonical инфраструктура трёх процессов p.mkt.{supply,return,wroff}:
- lib/domain/table_marketplace_orders.hpp — Order entity с полным
  жизненным циклом p.mkt.supply (active → cancelled / accepted →
  ship_ready → supply_prepared → accepted_to_coop → ready_to_receive →
  received), 8 secondary индексов, поля для трассировки signsupp/signchair/
  signiss1/signiss2 АПП-документов и warranty_until для submretrn гарда.
- lib/domain/table_marketplace_return_requests.hpp — ReturnRequest entity
  для p.mkt.return (pending_review → approved_for_visit → return_accepted /
  rejected_at_ku / rejected_remote), original_consume_op_id для
  compensating-forward трассировки (d6 A4), photos vector<checksum256>.
- lib/domain/table_marketplace_writeoff_proposals.hpp — WriteoffProposal
  entity для p.mkt.wroff с vector<wroff_item> per-КУ позиций.
- lib/core/marketplace/marketplace.hpp — canonical helpers
  get_*_by_hash[_or_fail] / update_* + cross-contract get_user_wallet_balance
  для guard'а Locked Decision L6 в createorder.

Контракт marketplace.{cpp,hpp} переписан целиком под 18 canonical actions
(p.mkt.supply 10 + p.mkt.return 5 + p.mkt.wroff 3) согласно YAML-стандартам
рядом с .hpp. Каждый action — отдельный .cpp файл в marketplace/src/
по образцу Capital'а; на этом коммите все 18 — skeleton с
eosio::check(false, "TODO Story 11.1 Шаг N: <action> ещё не реализован")
и подробным @brief docstring'ом со ссылкой на Story / guards / ledger2-
операции из YAML. Реальная реализация actions — последующими коммитами
(Шаги 4-6 спецификации Story 11.1).

Batch (consolidated request) намеренно НЕ выведен в on-chain entity —
Locked Decision L10: backend-only сущность, on-chain действия per-Order;
batch_hash на Order'е — opaque ссылка для трассировки.

Foundation Story 11.4 на месте (предыдущий коммит): WalletOp::REVOKE
обработан walletop dispatcher'ом; OPERATION_REGISTRY содержит все 13
marketplace-операций; счета 10/91 в LEDGER2_ACCOUNT_MAP.

Spec артефакт: blago/production/1-prilozhenie-stol-zakazov/components/
3-minimalnyy-produkt/_bmad-output/implementation-artifacts/
spec-11-1-ledger2-marketplace-ops.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(marketplace): implement createorder action (Story 4.1, p.mkt.supply)

Первая реализация canonical action — proof-of-pattern для остальных 17.
Серия трёх ledger2-операций (атомарно в одной транзакции Antelope):
- o.wal.conv (conditional, недостача из w.wal.share);
- o.mkt.assign (conditional, недостача из w.wal.member);
- o.mkt.block (всегда, total_cost на w.mkt.member).

L6 guard через cross-contract read трёх кошельков (Marketplace::
get_user_wallet_balance) — фейлится с понятной ошибкой если суммарных
средств не хватает. process_hash для всех трёх ops = order_hash.

Создан Order entity со status=ACTIVE; cycle_type валидируется по
enum (timebased/volumebased/opensubscr/individual). actual_quantity
и fact_cost ставятся равными quantity/total_cost — обновятся в
signiss2 при выдаче.

Plus: rename WroffStatus::DRAFT → PROPOSED (DRAFT конфликтует с macro
из lib/consts.hpp; on-chain строка осталась "draft"_n как в YAML).

Build: pnpm build:contract:test marketplace — green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(marketplace): implement 17 canonical actions (Story 11.1, p.mkt.{supply,return,wroff})

Реализованы все оставшиеся 17 actions поверх createorder (proof-of-pattern):

p.mkt.supply (9 actions, дополнительно к уже реализованному createorder):
- cancelorder — отмена до акцепта; o.mkt.unblk; ACTIVE → CANCELLED.
- expirecycle — закрытие цикла; threshold_reached==false → o.mkt.unblk per
  order + ACTIVE → CANCELLED; threshold_reached==true — no-op (backend
  формирует batch).
- acceptbatch — поставщик акцепт; ACTIVE → ACCEPTED, batch_hash зафиксирован;
  без ledger2 (L10).
- declinebatch — отказ до акцепта; o.mkt.unblk per order + ACTIVE → CANCELLED.
- prepship — сборка партии; ACCEPTED → SHIP_READY; shipping_method
  валидируется (varianta | variantb).
- signsupp — первая подпись АПП приёмки поставщиком; verify_document_or_fail
  ({offerer}); SHIP_READY → SUPPLY_PREPARED + acceptance_act_signsupp.
- signchair — закрывающая подпись АПП приёмки председателем; per-Order
  Ledger2::apply(o.mkt.purch) + Ledger2::apply(o.mkt.payout) атомарно;
  SUPPLY_PREPARED → ACCEPTED_TO_COOP + acceptance_act_signchair.
- signiss1 — открытие выдачи председателем; verify_document_or_fail({chairman});
  ACCEPTED_TO_COOP → READY_TO_RECEIVE + issue_act_signiss1.
- signiss2 — финальная подпись заказчика с поддержкой actual_quantity ≠
  ordered: при actual<ordered — o.mkt.unblk на разницу; при actual>ordered —
  L6 guard на доступность diff в трёх кошельках, conditional o.wal.conv +
  o.mkt.assign + o.mkt.block на разницу; всегда — o.mkt.consum + o.mkt.consum2
  на fact_cost; READY_TO_RECEIVE → RECEIVED + warranty_until.

p.mkt.return (5 actions):
- submretrn — заявление на возврат; гард warranty_until > now; создание
  return_request в PENDING_REVIEW; order.return_request_id для двусторонней
  связи. photos обязательны.
- aprretrem — удалённое одобрение визита; PENDING_REVIEW → APPROVED_FOR_VISIT.
- rejretrem — удалённый отказ; PENDING_REVIEW → REJECTED_REMOTE + reason_remote.
- accretrn — приём возврата на очном осмотре; Ledger2::apply(o.mkt.return) +
  Ledger2::apply(o.mkt.return2) (compensating forward, не revert);
  APPROVED_FOR_VISIT → RETURN_ACCEPTED.
- rejretrn — отказ на очном осмотре; APPROVED_FOR_VISIT → REJECTED_AT_KU +
  reason_visit.

p.mkt.wroff (3 actions):
- propwroff — формирование проекта списания; total_amount = Σ items.amount;
  PROPOSED.
- execwroff — исполнение per-item: Ledger2::apply(o.mkt.wroff) +
  Ledger2::apply(o.mkt.wroff2) каждой позиции; PROPOSED → EXECUTED;
  process_hash единый = proposal.hash, item.ku_chairman как username для
  per-КУ аналитики.
- declwroff — отклонение советом; PROPOSED → REJECTED + reject_reason.

Все compose'ятся через единый Ledger2::apply orchestrator, не нарушая
foundation Story 11.4. Composite-операции (consum+consum2, return+return2,
wroff+wroff2) в одной транзакции Antelope обеспечивают атомарность L1
проводок через транзит счёта 91. Cross-contract balance read через
Marketplace::get_user_wallet_balance(_ledger2 scope) — для L6-гарда
createorder и signiss2 (correction>).

Build: pnpm build:contract:test marketplace — green.
Foundation: walletop REVOKE + 13 marketplace operations + WalletOp NONE/REVOKE
+ счета 10/91 + кошельки w.mkt.{member,payout} — на месте (предыдущие коммиты).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(marketplace): структура по процессам + чистка избыточных дат on-chain

Структура:
- src/p.mkt.supply/  — 10 actions процесса прямой поставки-приобретения
                       (createorder, cancelorder, expirecycle, acceptbatch,
                        declinebatch, prepship, signsupp, signchair,
                        signiss1, signiss2);
- src/p.mkt.return/  — 5 actions процесса гарантийного возврата
                       (submretrn, aprretrem, rejretrem, accretrn, rejretrn);
- src/p.mkt.wroff/   — 3 actions процесса утилизации скоропорта (списания)
                       (propwroff, execwroff, declwroff).
Имена подпапок 1:1 совпадают с process_type из YAML-стандартов рядом —
прозрачная навигация от .cpp к стандарту.

Удалены избыточные date-поля из 3 entity-таблиц (orders / return_requests /
writeoff_proposals): created_at, accepted_at, shipped_at, received_to_coop_at,
ready_at, received_at, cancelled_at, reviewed_at, resolved_at, proposed_at,
decided_at. Контракт ничего не проверяет по этим датам — все timestamp'ы
переходов состояний восстанавливаются на бэкенде из blockchain_actions[at]
по соответствующим actions через ParserClient. Сокращает RAM-стоимость
state на каждый Order / ReturnRequest / WriteoffProposal.

Сохранено on-chain:
- order.warranty_until — нужно для guard'а submretrn (`now() < warranty_until`)
  без cross-action lookup; вычисляется в signiss2 как
  signiss2.timestamp + warranty_period_secs.
- batch_hash, status, документы (acceptance_act_signsupp/signchair,
  issue_act_signiss1/signiss2, statement, decision_remote/visit,
  protocol, photos), reason fields, fact_cost / actual_quantity, items
  для wroff_proposal, links (return_request_id, original_order_id,
  original_consume_op_id) — это смысловая on-chain информация, не
  выводимая из истории.

Удалены secondary-индексы bycreated и byproposed (бэкенд индексирует
по сохранённым в Postgres timestamp'ам блоков, on-chain pagination
по чрон. порядку = по primary key id).

Build: pnpm build:contract:test marketplace — green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(marketplace): ревью PR #375 — braname/branch, per-Order, memos, без prepship

- ku_chairman заменён на braname (приёмка/выдача) в Order; в return_request
  привязка к КУ удалена — каждое решение принимает braname параметром, авторизация
  через Branch::is_user_authorized (председатель/trustee/доверенные)
- Все per-batch actions теперь per-Order (acceptorder/declineorder/expireorder,
  signsupp/signchair); векторов order_hashes нет — backend проходит циклом
- prepship удалён как избыточный шаг (после acceptorder сразу signsupp);
  OrderStatus::SHIP_READY убран
- execwroff per-item: один item за вызов с item_index; финализация PROPOSED → EXECUTED
  автоматически после исполнения последней позиции; declwroff фейлится если
  хотя бы одна позиция уже исполнена
- propwroff валидирует существование braname для каждого item через get_branch_or_fail
- Memos вынесены в lib/core/marketplace/memo.hpp по аналогии с Capital::Memo;
  все Ledger2::apply используют человекочитаемые memo с подстановкой id
- Все eosio::check сообщения переписаны на пользовательский русский текст
  без префиксов action/Locked Decision (UI показывает их напрямую)
- walletop REVOKE дополнен подробным docstring: разница с BURN (blocked vs
  available, обязательная пара Dr/Cr, отсутствие альтернативы через UNBLOCK+BURN)

Build: marketplace.wasm + ledger2.wasm — green под coopos 5.2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(marketplace): order.current_warehouse_braname (текущая точка хранения)

Поле введено сейчас, чтобы избежать binary_extension при включении полноценного
процесса перемещения имущества между КУ после релиза.

Заполнение:
- signchair (приёмка кооперативом): current_warehouse_braname = accept_braname
- signiss1 (готово к выдаче): current_warehouse_braname = delivery_braname

Промежуточные перемещения между приёмным и выдающим КУ контрактом не
подписываются (бездокументарно) — точка хранения переходит «скачком» в момент
готовности к выдаче. Подписание ТТН по внутренним передачам — отдельная
история после релиза, текущее поле даёт ей точку расширения без миграции.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:25:07 +05:00
Alex Ant 3f2f205abd feat(marketplace): Эпик 2 — Сеть кооперативных участков (ПВЗ) — Stories 2.1+2.2+2.3 (#381)
* [598-5][@ant] feat: backend marketplace_ku_details + Yandex Geocoder — Stories 2.1+2.2

Эпик 2 (Сеть кооперативных участков), бэкенд-часть:

— Story 2.1: marketplace_ku_details (1:1 расширение core coop_ku) — entity,
  domain, mapper, repository adapter; GraphQL мутации
  marketplaceDetailKU / marketplaceSetKUStatus / marketplaceRetryKUGeocode +
  query marketplaceListKUDetails. ACL — @AuthRoles(['chairman']) для
  mutations, ['chairman','member','user'] для list (Story 1.6/1.8 access-matrix
  не merged в marketplace2 — chairman ≈ marketplace-role admin).

— Story 2.2: GeocoderPort + YandexGeocoderAdapter с локальным rate-limit
  (sliding window) и таймаутом запроса; конфиг через
  YANDEX_GEOCODER_API_KEY/BASE_URL/RATE_LIMIT_RPS/TIMEOUT_MS env.
  Post-effect hook detailKU при смене addressFull сбрасывает координаты
  в PENDING и асинхронно перезаписывает на OK или FAILED + errorMessage,
  не прерывая основную мутацию.

Тесты: 18 unit-кейсов в tests/unit/marketplace (mapper round-trip,
service happy/edge paths, geocoder happy/empty/HTTP500/exception/no-key/
rate-limit). pnpm typecheck + targeted jest проходят.

* [598-5][@ant] feat: KUMapWithList + админ-страница ПВЗ — Story 2.3

Эпик 2, фронтенд:

— `entities/MarketplaceKUDetails` — FSD entity со store/api/types.
  API — raw GraphQL (fetch) поверх backend-резолверов Story 2.1/2.2.
  Типизация — ручная, до регенерации Zeus в `@coopenomics/sdk` (техдолг).
— `widgets/KUMapWithList` — компонент карта Yandex + список ПВЗ с
  синхронным выбором (клик в списке центрирует карту и открывает balloon
  пина; клик на пин подсвечивает карточку). Responsive: на мобильных
  layout вертикальный, карта сверху. Динамическая загрузка Yandex Maps
  SDK через `loadYandexMaps`; деградированный UI если ключа нет.
  Aria-label на регионе/карточках, q-skeleton на loading.
— `features/MarketplaceDetailKU` — диалог формы детализации ПВЗ
  (адрес, контакты, режим работы по дням). Используется на admin-странице
  для create/edit.
— `pages/Marketplace/PvzList` — admin/orderer/offerer страница списка ПВЗ.
  Для admin'а — кнопки edit/деактивировать/активировать/повторить
  геокодинг (Story 2.1 acceptance); slot `cardAction` позволяет
  заказчику/поставщику расширить под «Выбрать как место получения» /
  «Везти сюда» (передаётся при reuse).
— Регистрация роута `/:coopname/market/pvz` в `extensions/market/install.ts`.

Limitations (вынесены в PR-описание):
  • Зависит от регенерации Zeus типов в SDK — пока через raw GraphQL.
  • Не интегрирован в orderer/offerer create-order/pre-shipment flow —
    integration на стороне Эпиков 4-5.
  • WCAG 2.1 AA полный audit — Phase 2 post-MVP (Эпик 10 Story 10.2 notes).
  • Yandex Maps JS API требует `YANDEX_MAPS_API_KEY` в env (window.__APP_CONFIG__).

* [598-5][@ant] refactor: GeocoderPort провайдер-агностичен + чистка @description ревью PR #381

— env YANDEX_GEOCODER_* → GEOCODER_*; добавлен GEOCODER_PROVIDER (yandex|noop)
— infra adapters: NoopGeocoderAdapter + geocoderPortFactory; YandexGeocoderAdapter
  читает config.geocoder.*; module использует useFactory
— resolver / DTO: убраны ссылки на «Story X.Y / Эпик 2» из GraphQL @description
  (остаются в JSDoc-комментариях кода)
— jest: maxWorkers=1 в jest.config.js + явный --runInBand в package.json scripts
  (вешает dev-стек docker при параллельном пуле)
— schema.gql + controller/zeus регенерированы

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-5][@ant] feat: SDK marketplace KU details — селектор, 3 мутации, 1 query ревью PR #381

— Selectors.marketplaceKUDetailsSelector + workingHoursSelector/workingHoursDaySelector
— Mutations.Marketplace.DetailKU/SetKUStatus/RetryKUGeocode
— Queries.Marketplace.ListKUDetails (новый модуль queries/marketplace)
— zeus regenerated (controller schema → SDK)
— убирает техдолг raw-graphql во фронте

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-5][@ant] refactor: ПВЗ в отдельный workspace + переход с raw-graphql на Zeus ревью PR #381

— Стол ПВЗ — отдельный workspace 'market-pvz', defaultRoute marketplace-pvz,
  доступ roles:['chairman'] (заказчик/поставщик его не видят и не перегружены)
— entities/MarketplaceKUDetails/api: client.Query/Mutation из @coopenomics/sdk
  (Mutations.Marketplace.DetailKU/SetKUStatus/RetryKUGeocode +
  Queries.Marketplace.ListKUDetails); raw-graphql.ts удалён

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-5][@ant] refactor(sdk): kuDetailsSelector — _validate для всех селекторов как в других модулях

— rawWorkingHoursBreakSelector / Day / Sselector + rawMarketplaceKUDetailsSelector
— const _validate: MakeAllFieldsRequired<ValueTypes['Type']> = rawXxxSelector
  по аналогии с capital/voteSelector.ts: typecheck падёт, если в schema
  появится новое поле, не покрытое селектором.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 11:03:11 +05:00
Alex Ant d36b168752 feat(marketplace): Эпик 1 (Stories 1.4-1.11) — установка Стола и трёхуровневый онбординг (#380)
Publish Docs / build-and-publish-docs (push) Failing after 15m0s
[598-4][@ant]
* feat(marketplace): Story 1.2 — регистрация оферты в AgreementRegistry

Перепиcано через существующий core-механизм `AgreementRegistrationPort`
(закрытый PR #369 шёл через параллельный coop_cpp_registry — техдолг).
Аналогично тому, как Capital регистрирует свои оферты —
register-capital-in-agreement-registry.ts.

- constants/marketplace-agreement-ids.ts — MARKETPLACE_EXTENSION_NAME='market',
  MARKETPLACE_OFFER_AGREEMENT_ID='order_table_offer',
  MARKETPLACE_AGREEMENT_TYPE='order_table',
  MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID=0 (placeholder; Story 1.7 заменит
  на реальный registry_id из платформенной document factory, по аналогии с
  Cooperative.Registry.GeneratorOffer/BlagorostOffer).
- application/registration/register-marketplace-in-agreement-registry.ts —
  чистая функция: при ID>0 зовёт port.registerAgreement с одной офертой
  (applicable_account_types: individual + entrepreneur, order: 7); при ID<=0
  возвращает false без вызовов. Programs marketplace не регистрирует —
  Стол заказов ЦПП без program-надстройки.
- marketplace-extension.module — @Inject(AGREEMENT_REGISTRATION_PORT) +
  вызов registerMarketplaceInAgreementRegistry в initialize после initBucket;
  info-лог при skip и при успешной регистрации, error при exception.

Tests:
- tests/unit/marketplace/marketplace-plugin-register.test.ts (3 кейса):
  чистая функция — port не вызван пока placeholder=0, константы согласованы,
  programs не регистрируются.
- tests/unit/marketplace/marketplace-plugin-initialize.test.ts — обновлён
  конструктор (добавлен makeAgreementPort стаб); 4 кейса Story 1.1
  продолжают проходить.

10/10 unit-тестов зелёных, tsc 0 ошибок.

Ref: blago/.../components/3-minimalnyy-produkt/_bmad-output/implementation-artifacts/spec-1-2-cpp-registry-link.md
(spec будет переписан вместе с blago-issue после merge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(marketplace): MARKETPLACE_AGREEMENT_TYPE = 'marketplace' (валидный eosio::name)

Per review PR #370: 'order_table' содержит '_' — не валидный eosio::name
(допустимы только [.a-z1-5], max 12). В lib/consts.hpp уже определено
program-имя `_marketplace_program = "marketplace"_n` (program_id=2), его
и принимает `soviet::coagreements`/`sndagreement`.

- MARKETPLACE_AGREEMENT_TYPE: 'order_table' → 'marketplace'.
- MARKETPLACE_OFFER_AGREEMENT_ID: 'order_table_offer' → 'marketplace_offer'
  (консистентно с 'generator_offer'/'blagorost_offer' у Capital).
- AgreementRegistrationSpec.agreement_type — JSDoc уточнён: должен быть
  eosio::name (max 12, без `_`), пример обновлён.
- Тест проверяет eosio::name regex `/^[.a-z1-5]{1,12}$/` для type.

10/10 unit зелёные, tsc 0 ошибок.

* feat(marketplace): Story 1.3 — доступ к Столу заказов по членству

Реализует:
- MarketplaceMembershipGuard: 401 (нет JWT), 403 (status != active),
  server-secret bypass, формирует request.currentMember и
  ctx.currentMember для GraphQL.
- mapUserRoleToCoreRoles: user.role → core_roles[]
  - 'user'     → ['User']
  - 'member'   → ['User', 'Member']
  - 'chairman' → ['User', 'Member', 'Chairman']
  - 'admin'/unknown → []
- @CurrentMarketplaceMember decorator + MarketplaceCurrentMemberDTO
  ({username, core_roles[], marketplace_roles[]}).
- Query marketplaceWhoAmI(): возвращает контекст текущего пайщика для
  фронта (UI открывается на primary столе по роли).
- marketplace_roles[] пока пустой — наполняется в Story 1.6.

Тесты: 18/18 зелёных (5 mapper + 6 guard + 4 initialize + 3 register).
tsc 0 ошибок.

* feat(marketplace): Story 1.4 — L3 fallback gate онбординга

Domain service `MarketplaceOnboardingService.getOnboardingState(username)`:
- placeholder=0 (Story 1.7 не выполнена) → {requires_gate:false,
  source:'not_configured'} — фронт пропускает на стол.
- agreementRepository.findByUsername → найдена запись type='marketplace'
  c подходящим draft_id (или без draft_id как fallback) →
  {requires_gate:false, source:'agreement_signed', completed_at,
  agreement_id}.
- иначе {requires_gate:true, source:'gate_required'}.

Не заводим локальную таблицу marketplace_onboarding_state — core
AgreementRepository уже даёт state через PG-кеш agreements3
(AgreementSyncService); TTL 60s из PRD не нужен.

Mutation подписания не реализуется в этой Story — при placeholder=0 gate
не показывается, путь активируется после Story 1.7.

GraphQL Query `marketplaceOnboardingState: MarketplaceOnboardingState`
под GqlJwtAuthGuard + MarketplaceMembershipGuard. username берётся из
@CurrentMarketplaceMember, фронт не подменяет member_id.

Тесты: 22/22 зелёных (4 onboarding-service + 18 предыдущих).
tsc 0 ошибок.

* feat(marketplace): Story 1.5 — кошелёк ЦК пайщика на столе

Query marketplaceMemberWallet под GqlJwtAuthGuard + MarketplaceMembershipGuard,
делегирует чтение в core WalletService.getProgramWallet(coopname, username,
program_type=MAIN). PG-кеш ledger2::userwallets уже консистентен с
blockchain (UserWalletSyncService); локальная таблица
marketplace_member_wallet_link из PRD не заводится — техдолг PRD.

DTO MarketplaceMemberWallet возвращает:
- available/blocked — из w.wal.share части ledger2-кошелька;
- membership_contribution — w.wal.member.value;
- contract='wallet', coopname, username.

Если PG-кеш пуст → NotFoundException (CLAUDE.md запрещает RPC fallback).
Поля undefined нормализуются в '0'.

Тесты: 25/25 зелёных (3 wallet resolver + 22 предыдущих).
tsc 0 ошибок.

* feat(marketplace): Story 1.6 — массив marketplace-ролей + RoleGuard

mapCoreRolesToMarketplaceRoles(coreRoles[], context):
- User → [orderer] +opt[offerer] +opt[operator]
- Member → +board_readonly
- Chairman → +admin, +board
- без User → []

isOfferer/isKuChairman context-флаги зарезервированы (Эпики 2/3).

MarketplaceMembershipGuard теперь заполняет marketplace_roles[] через
mapper (раньше был placeholder []).

MarketplaceRoleGuard (новый): отдельный guard под
@RequireMarketplaceRole('admin', 'board') (OR-семантика). Если декоратор
не задан → guard разрешает. При запрете:
- ForbiddenException "Forbidden: marketplace role 'X' required, member has [...]"
- warn-лог forbidden-attempt: member=, action=, requested_role=[],
  actual_marketplace_roles=[], actual_core_roles=[].

server-secret bypass обоих guards (inter-service).

README рядом с guards описывает паттерн UseGuards + декоратор + Phase 2
миграцию в CASL (Guard остаётся, меняется источник policy).

Тесты: 40/40 зелёных (7 marketplace-mapper + 8 role-guard + 6 membership
+ 5 core-mapper + 4 onboarding + 3 wallet + 4 init + 3 register).
tsc 0 ошибок.

* feat(cooptypes,marketplace): Story 1.7 — template ЦПП «Стол заказов» в registry

cooptypes registry:
- 1100.MarketplaceOfferTemplate — шаблон оферты ЦПП marketplace
  (по аналогии с 999.BlagorostOfferTemplate). Содержит placeholder
  content (рыбу) с явным caveat-блоком; финальный юридический текст
  готовится отдельно и заменит content без изменения registry_id.
- 1101.MarketplaceOffer — instance, renderуется для пайщика при подписании
  (Story 1.4 L3 или Story 1.11 L2; аналог 1000.BlagorostOffer).
- Зарегистрированы в registry/index.ts.

marketplace controller:
- MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID импортируется из
  Cooperative.Registry.MarketplaceOfferTemplate.registry_id (=1100).
- Добавлена MARKETPLACE_OFFER_INSTANCE_REGISTRY_ID (=1101) для будущей
  mutation подписания.

Auto-activates:
- Story 1.2: registerMarketplaceInAgreementRegistry больше не пропускает
  регистрацию; port.registerAgreement вызывается при restart расширения.
- Story 1.4: marketplaceOnboardingState возвращает requires_gate=true для
  пайщика без подписи (а не not_configured).

Тесты обновлены под id=1100:
- marketplace-plugin-register: AC successful registration; rollback кейс
  через jest.doMock.
- marketplace-onboarding-service: placeholder=0 через jest.doMock; добавлен
  кейс «Story 1.7 размещена → requires_gate=true».

42/42 marketplace unit зелёных, tsc 0 ошибок.

Юридический финальный текст оферты — отдельный todo (todo-tspp-templates.md).

* feat(marketplace): Story 1.8 — централизованная access-matrix (CASL-compat)

marketplaceAccessMatrix: Record<MarketplaceRole, Record<Resource, Action[]>>
для всех 6 ролей. Покрывает Эпики 1-4 (Order, Offer, KU, Receiving,
Issuance, Warehouse, Vitrine, Whitelist, Agenda, Writeoff, Decision,
Extension). Эпики 5-10 расширят без правок resolver-ов.

canAccess(roles[], resource, action) — OR по ролям, exact-match action.
roleHasAnyAction(roles, resource, baseAction) — UI-фильтр меню.

Новый декоратор @RequireMarketplaceAccess(resource, action). Старый
@RequireMarketplaceRole сохранён для обратной совместимости и для
требований, не привязанных к resource.

MarketplaceRoleGuard расширен: читает оба декоратора через Reflector;
если оба заданы — AND-семантика. Лог-формат отказа по access:
`requested_access=Resource:action`.

README рядом с guards: секция access-matrix, нотация actions (:own/:all/
:to-self/:own-KU/:first), обновлённый Phase 2 plan (canAccess →
CASL ability.can без изменения вызывающего кода).

Тесты: 60/60 marketplace зелёных (10 suites). tsc 0 ошибок.

CI-checker orphan resolvers/matrix entries не реализован (требует
AST-парсер) — фиксируется как Phase 1.5 техдолг.

* feat(marketplace): Story 1.9 — L1 решение совета о принятии положения ЦПП

extension.config расширяется coopAcceptance: {accepted, document_registry_id,
accepted_at, accepted_by_board_decision_id}. Bootstrap-миграция v2 добавляет
поле к существующим конфигам идемпотентно.

MarketplaceCoopAcceptanceService:
- getStatus() → {status: 'active'|'not_accepted', ...} из extension.config.
- accept({document_registry_id, accepted_by_board_decision_id, accepted_at?})
  → пишет coopAcceptance в config, возвращает 'active' state.

GraphQL:
- Query marketplaceCppStatus — открыт всем активным пайщикам (фронт решает,
  показывать ли баннер «Не подключено»).
- Mutation marketplaceAcceptCpp — admin-only через
  @RequireMarketplaceAccess('Extension', 'configure').

MVP-stub: реальная интеграция с повесткой совета (FR40, Эпик 8) ещё не
готова — председатель передаёт board_decision_id как string. Phase 2 после
Эпика 8: mutation валидирует существование решения / переедет в side-effect
core-controlled повестки.

Тесты: 66/66 marketplace unit зелёных. tsc 0 ошибок.

* feat(marketplace): Story 1.10 — статус оферты в core AgreementRegistry

Query marketplaceRegistrationOfferStatus → читает через AGREEMENT_QUERY_PORT.
getAgreementById(MARKETPLACE_OFFER_AGREEMENT_ID), возвращает
{registered, agreement_id, registry_id, agreement_type, title,
applicable_account_types}. Используется admin UI для статуса «Оферта
зарегистрирована в registration-flow».

MarketplaceCoopAcceptanceService.accept (Story 1.9) теперь имеет
side-effect: re-register оферту в core AgreementRegistry через
registerMarketplaceInAgreementRegistry (идемпотентно). Покрывает кейс,
когда Story 1.7 поставила template позже restart-а расширения и
MarketplacePlugin.initialize пропустил регистрацию (placeholder=0 → ok).

AGREEMENT_REGISTRATION_PORT инжектируется @Optional — старые unit-тесты
Story 1.9 (constructor без порта) проходят.

AC PRD `coop_registration_offers_registry` = платформенный AgreementRegistry
с extension_name ключом. Per-coop scoping не нужен для одно-coop
controller'а; multi-coop приложит CooperativeConfigService без правок
marketplace-кода (тех-долг PRD, принят).

Тесты: 68/68 marketplace зелёных (новый 1.10 resolver + предыдущие).
tsc 0 ошибок.

* feat(marketplace): Story 1.11 — L2 онбординг через core registration-flow

Backend marketplace для L2 не вносит нового кода — flow держится на
уже реализованных компонентах + core registration-flow:

1. Story 1.2/1.7/1.10: marketplace_offer зарегистрирована в
   AgreementRegistry, видна core SignUp.
2. Core registration-flow рендерит instance 1101.MarketplaceOffer
   через documentFactory.
3. Подпись через wallet::signagree / soviet::sndagreement('marketplace')
   — работа core.
4. AgreementSyncService наполняет AgreementRepository.
5. Story 1.4 MarketplaceOnboardingService читает PG-кеш и НЕ показывает
   L3 gate если подпись есть.

Добавлено:
- onboarding/README.md — описание полного L1→L2→L3 цикла, какие компоненты
  предоставляет marketplace и что требует от core; раздел фоллоуапов.
- marketplace-l2-onboarding-flow.test.ts — 3 scenario-кейса (подпись через
  L2 → нет gate; нет подписи → gate; ownership by username).

Эпик 1 закрыт: 11/11 stories реализованы, 71 marketplace unit-тест
покрывает критические инварианты онбординга.

Открытые фоллоуапы (вынесены как отдельный backlog):
- L3 mutation marketplaceSignOnboardingOffer (write-pool + sndagreement).
- Source-маркер registration_flow vs extension_gate в DTO.
- Mutation marketplaceAcceptCpp валидация реальной повестки совета (Эпик 8).

* [58bfe503][@ant] fix: переписать marketplaceMemberWallet на массив кошельков по стандарту marketplace — устранить подгонку под старую нотацию membership_contribution, показывать share/member/mkt.member как есть из ledger2::userwallets

Review @dacom-dark-sun на PR #380 (line 13/53 в DTO+Resolver) указал: текущая
реализация Story 1.5 сворачивает w.wal.share + w.wal.member в свёрнутый
ProgramWallet и подставляет membership_contribution — это «полная хуйня,
старая нотация, кошельков система обновилась». По стандарту marketplace
(components/contracts/cpp/marketplace/p.mkt.supply.standard.yaml, секция wallets)
у пайщика 3 USER_SHARED-кошелька: w.wal.share (ЦК паевые), w.wal.member
(универсальный членский — транзитный share→mkt), w.mkt.member (программный
членский ЦПП Стол Заказов). Каждый со своим available+blocked.

Перепиcан DTO MarketplaceMemberWalletDTO: вместо плоских полей возвращается
массив MarketplaceWalletEntryDTO[{name, human_name, program_id, program_label,
kind, available, blocked}]. Resolver зовёт core UserWalletRepository.findByUsername
напрямую (PG-кеш ledger2::userwallets, ADR-011), фильтрует 3 релевантных wallet_name,
для каждого ставит 0/0 если L3-запись ещё не создана (w.mkt.member появляется
после первого orderoffer/createorder). WalletService.getProgramWallet больше не
зовётся — он сворачивает share+member и неприменим для UX, требующего видеть
каждый кошелёк отдельно. WalletModule убран из marketplace-application.module
(USER_WALLET_REPOSITORY доступен глобально из @Global TypeormModule).
Unit-тесты переписаны: 4 кейса (массив с заполнениями, нулевой пайщик, human_name
из cooptypes, w.mkt.payout COOPERATIVE не попадает в выдачу) — 4/4 зелёных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [58bfe503][@ant] fix: переименовать program_label → label с UX-нотацией «<тип взноса> | <программа>» — review @dacom-dark-sun PR #380 line 34

Прежние значения «ЦК» / «Marketplace» — это технические идентификаторы программ
из LEDGER2_USER_SHARED_PROGRAM_MAPPING, а не UX-метки. Review указал нужный
формат: «Паевой | Цифровой Кошелёк», «Членский | Цифровой Кошелёк», «Членский | Стол Заказов».

Поле `program_label` переименовано в `label`. Семантика: короткая
человеческая метка кошелька для panel/list. Длинное юридическое название
по-прежнему доступно в `human_name` (из cooptypes LEDGER2_WALLET_REGISTRY).
program_id (1/2) остаётся машинно-читаемым полем для группировки.

Тест переписан: 4/4 зелёных (label проверяется для всех 3 кошельков).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:30:35 +05:00
coopops 0a5dc3dabd Merge remote-tracking branch 'origin/dev' into marketplace2 2026-05-14 18:16:11 +00:00
Alex Ant 540c4ceab1 feat(marketplace): Story 1.3 — доступ к Столу по членству (#371)
* feat(marketplace): Story 1.2 — регистрация оферты в AgreementRegistry

Перепиcано через существующий core-механизм `AgreementRegistrationPort`
(закрытый PR #369 шёл через параллельный coop_cpp_registry — техдолг).
Аналогично тому, как Capital регистрирует свои оферты —
register-capital-in-agreement-registry.ts.

- constants/marketplace-agreement-ids.ts — MARKETPLACE_EXTENSION_NAME='market',
  MARKETPLACE_OFFER_AGREEMENT_ID='order_table_offer',
  MARKETPLACE_AGREEMENT_TYPE='order_table',
  MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID=0 (placeholder; Story 1.7 заменит
  на реальный registry_id из платформенной document factory, по аналогии с
  Cooperative.Registry.GeneratorOffer/BlagorostOffer).
- application/registration/register-marketplace-in-agreement-registry.ts —
  чистая функция: при ID>0 зовёт port.registerAgreement с одной офертой
  (applicable_account_types: individual + entrepreneur, order: 7); при ID<=0
  возвращает false без вызовов. Programs marketplace не регистрирует —
  Стол заказов ЦПП без program-надстройки.
- marketplace-extension.module — @Inject(AGREEMENT_REGISTRATION_PORT) +
  вызов registerMarketplaceInAgreementRegistry в initialize после initBucket;
  info-лог при skip и при успешной регистрации, error при exception.

Tests:
- tests/unit/marketplace/marketplace-plugin-register.test.ts (3 кейса):
  чистая функция — port не вызван пока placeholder=0, константы согласованы,
  programs не регистрируются.
- tests/unit/marketplace/marketplace-plugin-initialize.test.ts — обновлён
  конструктор (добавлен makeAgreementPort стаб); 4 кейса Story 1.1
  продолжают проходить.

10/10 unit-тестов зелёных, tsc 0 ошибок.

Ref: blago/.../components/3-minimalnyy-produkt/_bmad-output/implementation-artifacts/spec-1-2-cpp-registry-link.md
(spec будет переписан вместе с blago-issue после merge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(marketplace): MARKETPLACE_AGREEMENT_TYPE = 'marketplace' (валидный eosio::name)

Per review PR #370: 'order_table' содержит '_' — не валидный eosio::name
(допустимы только [.a-z1-5], max 12). В lib/consts.hpp уже определено
program-имя `_marketplace_program = "marketplace"_n` (program_id=2), его
и принимает `soviet::coagreements`/`sndagreement`.

- MARKETPLACE_AGREEMENT_TYPE: 'order_table' → 'marketplace'.
- MARKETPLACE_OFFER_AGREEMENT_ID: 'order_table_offer' → 'marketplace_offer'
  (консистентно с 'generator_offer'/'blagorost_offer' у Capital).
- AgreementRegistrationSpec.agreement_type — JSDoc уточнён: должен быть
  eosio::name (max 12, без `_`), пример обновлён.
- Тест проверяет eosio::name regex `/^[.a-z1-5]{1,12}$/` для type.

10/10 unit зелёные, tsc 0 ошибок.

* feat(marketplace): Story 1.3 — доступ к Столу заказов по членству

Реализует:
- MarketplaceMembershipGuard: 401 (нет JWT), 403 (status != active),
  server-secret bypass, формирует request.currentMember и
  ctx.currentMember для GraphQL.
- mapUserRoleToCoreRoles: user.role → core_roles[]
  - 'user'     → ['User']
  - 'member'   → ['User', 'Member']
  - 'chairman' → ['User', 'Member', 'Chairman']
  - 'admin'/unknown → []
- @CurrentMarketplaceMember decorator + MarketplaceCurrentMemberDTO
  ({username, core_roles[], marketplace_roles[]}).
- Query marketplaceWhoAmI(): возвращает контекст текущего пайщика для
  фронта (UI открывается на primary столе по роли).
- marketplace_roles[] пока пустой — наполняется в Story 1.6.

Тесты: 18/18 зелёных (5 mapper + 6 guard + 4 initialize + 3 register).
tsc 0 ошибок.

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:45:27 +05:00
Alex Ant db80dea2b6 feat(marketplace): Story 1.2 — регистрация оферты в AgreementRegistry (#370)
* feat(marketplace): Story 1.2 — регистрация оферты в AgreementRegistry

Перепиcано через существующий core-механизм `AgreementRegistrationPort`
(закрытый PR #369 шёл через параллельный coop_cpp_registry — техдолг).
Аналогично тому, как Capital регистрирует свои оферты —
register-capital-in-agreement-registry.ts.

- constants/marketplace-agreement-ids.ts — MARKETPLACE_EXTENSION_NAME='market',
  MARKETPLACE_OFFER_AGREEMENT_ID='order_table_offer',
  MARKETPLACE_AGREEMENT_TYPE='order_table',
  MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID=0 (placeholder; Story 1.7 заменит
  на реальный registry_id из платформенной document factory, по аналогии с
  Cooperative.Registry.GeneratorOffer/BlagorostOffer).
- application/registration/register-marketplace-in-agreement-registry.ts —
  чистая функция: при ID>0 зовёт port.registerAgreement с одной офертой
  (applicable_account_types: individual + entrepreneur, order: 7); при ID<=0
  возвращает false без вызовов. Programs marketplace не регистрирует —
  Стол заказов ЦПП без program-надстройки.
- marketplace-extension.module — @Inject(AGREEMENT_REGISTRATION_PORT) +
  вызов registerMarketplaceInAgreementRegistry в initialize после initBucket;
  info-лог при skip и при успешной регистрации, error при exception.

Tests:
- tests/unit/marketplace/marketplace-plugin-register.test.ts (3 кейса):
  чистая функция — port не вызван пока placeholder=0, константы согласованы,
  programs не регистрируются.
- tests/unit/marketplace/marketplace-plugin-initialize.test.ts — обновлён
  конструктор (добавлен makeAgreementPort стаб); 4 кейса Story 1.1
  продолжают проходить.

10/10 unit-тестов зелёных, tsc 0 ошибок.

Ref: blago/.../components/3-minimalnyy-produkt/_bmad-output/implementation-artifacts/spec-1-2-cpp-registry-link.md
(spec будет переписан вместе с blago-issue после merge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(marketplace): MARKETPLACE_AGREEMENT_TYPE = 'marketplace' (валидный eosio::name)

Per review PR #370: 'order_table' содержит '_' — не валидный eosio::name
(допустимы только [.a-z1-5], max 12). В lib/consts.hpp уже определено
program-имя `_marketplace_program = "marketplace"_n` (program_id=2), его
и принимает `soviet::coagreements`/`sndagreement`.

- MARKETPLACE_AGREEMENT_TYPE: 'order_table' → 'marketplace'.
- MARKETPLACE_OFFER_AGREEMENT_ID: 'order_table_offer' → 'marketplace_offer'
  (консистентно с 'generator_offer'/'blagorost_offer' у Capital).
- AgreementRegistrationSpec.agreement_type — JSDoc уточнён: должен быть
  eosio::name (max 12, без `_`), пример обновлён.
- Тест проверяет eosio::name regex `/^[.a-z1-5]{1,12}$/` для type.

10/10 unit зелёные, tsc 0 ошибок.

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:39:41 +05:00
Alex Ant ab8c644ef7 feat(marketplace): Story 1.1 — установка Стола заказов из Каталога приложений (#368)
Publish Docs / build-and-publish-docs (push) Failing after 13m10s
- Подключить MarketplacePluginModule в ExtensionsModule.register (расширение
  не загружалось в NestJS DI-контейнер).
- Унифицировать имя расширения: MarketplacePlugin.name = 'market' (совпадает
  с ключом AppRegistry; install через installExtension({name:"market"}) теперь
  не падает на findByName('marketplace')).
- Расширить MarketplacePlugin.initialize: bucket coop-<coopname> через
  optional file-storage порт (@Optional injection, fallback под warn-логом до
  merge PR #359); три AC-лога backend'а — 'Создан физический бакет ...',
  'File storage готов', 'marketplace-extension готов'.
- Bootstrap config-миграция v1 для расширения market.
- Atomic rollback в ExtensionInteractor.installApp: при провале runApp снести
  запись через uninstallApp, только если её не было до install; ошибка
  пробрасывается наверх (UI Каталога показывает 'Ошибка').
- Process locator: обновить TODO p.mkt.* — заглушки готовы, активация в
  Story 4.1 при создании marketplace::requests.

Unit-тесты:
- tests/unit/marketplace/marketplace-plugin-initialize.test.ts (4 проходят).
- tests/unit/appstore/extension-interactor-install-rollback.test.ts (3 проходят).

Refs: blago/production/1-prilozhenie-stol-zakazov/components/3-minimalnyy-produkt/_bmad-output/implementation-artifacts/spec-1-1-install-from-catalog.md

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:50:51 +05:00
coopops 467f33ff09 Merge remote-tracking branch 'origin/dev' into marketplace2
# Conflicts:
#	components/contracts/cpp/lib/core/ledger2/wallets.hpp
#	components/cooptypes/src/ledger2/wallets.generated.ts
2026-05-14 09:14:14 +00:00
coopops d98041d44a fix stantards 2026-05-12 04:25:54 +00:00
coopops 44a0030fd6 sync(controller/process-registry): убран legacy p.mkt.reqst, заглушки под членскую модель
- process-hash-locator.ts: удалён мёртвый ключ p.mkt.reqst (привязка к legacy таблице marketplace::requests). Добавлены пустые locators для p.mkt.supply / p.mkt.return / p.mkt.wroff — реализация контракта marketplace под членскую модель ещё не написана, операции на бэкенд не прилетают. TODO-комментарий зафиксирован: при реализации использовать поле request_hash (универсальное; order_hash избегаем — пересекается с offer_hash из orderoffer-actions).
- process-registry.service.ts: убрано упоминание p.mkt.reqst из устаревшего комментария-примера мульти-операционных процессов.

Без этого фикса не пуш-критично (Phase A никогда не маппила бы operation на удалённый process_type), но мёртвый ключ создавал бы путаницу.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:33:33 +00:00
coopops a0194f698b sync(cooptypes/ledger2): зеркало TS-реестров под членскую модель «Стола заказов»
- accounts.ts: + 10 «Материалы», 91 «Прочие доходы и расходы».
- processes.ts: убран legacy p.mkt.reqst (REQUEST), добавлены p.mkt.supply / p.mkt.return / p.mkt.wroff.
- operations.ts: WalletOp += 'REVOKE'; legacy o.mkt.supply/recv (CONFIRM_SUPPLY/RECEIPT) заменены на 12 операций членской модели (CONVERT_TO_MEMBER, ASSIGN_TO_PROGRAM, BLOCK_FOR_ORDER, UNBLOCK_ON_CANCEL, RECALL_TO_UNIVERSAL, PURCHASE_FROM_SUPPLIER, PAY_SUPPLIER, CONSUME_BY_MEMBER + CONSUME_TRANSIT_CLOSE, RETURN_BY_MEMBER + RETURN_TRANSIT_CLOSE, WRITE_OFF_PERISHABLE + WRITE_OFF_TRANSIT_CLOSE); composite-проводки через 91 разбиты на пары *...*2.
- wallets.generated.ts: auto-regen из wallets.hpp — +w.mkt.member (program_id=2).

Без этого в standards-site новые кошельки и операции marketplace показывались «нет в реестре».

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:08:09 +00:00
coopops 0a8e226417 [@ant] feat(ledger2): членская модель «Стола заказов» — 12 операций marketplace, счета 10/91, кошелёк w.mkt.member, WalletOp REVOKE; удалены legacy o.mkt.supply/recv (клиринг)
- operations.hpp: enum WalletOp + REVOKE (списание blocked + обяз. Dr/Cr); удалены CONFIRM_SUPPLY/CONFIRM_RECEIPT; добавлены 12 операций членской модели — CONVERT_TO_MEMBER в operations::wallet и 11 в operations::marketplace (ASSIGN_TO_PROGRAM, BLOCK_FOR_ORDER, UNBLOCK_ON_CANCEL, RECALL_TO_UNIVERSAL, PURCHASE_FROM_SUPPLIER, PAY_SUPPLIER, CONSUME_BY_MEMBER + CONSUME_TRANSIT_CLOSE, RETURN_BY_MEMBER + RETURN_TRANSIT_CLOSE, WRITE_OFF_PERISHABLE + WRITE_OFF_TRANSIT_CLOSE); composite-проводки через транзит счёта 91 «Прочие доходы и расходы» разбиты на пары операций (часть 1 + часть 2, *2-суффикс), вызываемые последовательно в одной транзакции Antelope — атомарность через транзакцию.
- accounts.hpp: добавлены счета 10 «Материалы» (А) и 91 «Прочие доходы и расходы» (А/П).
- wallets.hpp: добавлен USER_SHARED-кошелёк w.mkt.member («ЦПП Стол Заказов — программный членский») с program_id=2 (marketplace).
- processes.hpp: удалён p.mkt.reqst (клиринговый), добавлены p.mkt.supply / p.mkt.return / p.mkt.wroff.
- p.mkt.{supply,return,wroff}.standard.yaml: composite-проводки разнесены на пары operations[]; транзит через 91 явно описан в комментариях и description (нетто-эффект — Дт 86 / Кт 10 на выдаче/утилизации и Дт 10 / Кт 86 на возврате).

Согласовано с Ангелиной 2026-05-11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:08:09 +00:00
Alex Ant e9fca4138b update standart 2026-05-11 23:06:19 +05:00
coopops 2028f032bb Merge remote-tracking branch 'origin/dev' into marketplace2 2026-05-11 08:27:10 +00:00
coopops acd2a4afee feat(standards): p.reg.accept человеческим языком + регистрационный взнос; standards-site infra
- p.reg.accept.standard.yaml: прозаические поля (purpose/description/guards/pre/post) переписаны
  для НЕ-технарей — без имён экшенов, операций, кошельков, проводок, таблиц
- введён термин «регистрационный взнос» (вступительный + минимальный паевой) с раскрытием
  при первом упоминании
- standards-site/FocusBar.vue: ROLE_HUMAN += candidate → «Кандидат»
- standards-site/vite.config.ts: плагин watch-cpp-standards подключает ../cpp/ к vite watcher
  и инвалидирует loader.ts при изменении *.standard.yaml — HMR срабатывает без перезапуска

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:26:38 +00:00
coopops c87d6c658e Merge remote-tracking branch 'origin/ledger3' into marketplace2
# Conflicts:
#	components/contracts/cpp/capital/p.cap.rid.standard.yaml
2026-05-09 14:42:10 +00:00
coopops 93fa5c1210 refactor(standards): объединить summary+purpose — оставить только purpose
В §1 паспорта стандарта поле `summary` упразднено. Единственное прозаическое
описание процесса теперь в `purpose` — рендерится на главной странице
(line-clamp 3) и в карточке «Начало процесса». Чтобы не было соблазна писать
дублирующее «краткое описание процесса» вроде «от подписанного заявления до
карточки активного пайщика».

- 15 *.standard.yaml: блок summary удалён
- standards-site: type Standard.summary удалён, рендер в FocusBar/HomePage/StartNode/EndNode
  переключён на purpose; CSS-класс home-card__summary → home-card__purpose
- SKILL.md и a2-spetsifikatsiya: помечено что summary упразднён в v1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:39:48 +00:00
coopops b3cb03b38b feat(standards-site): WalletOp += 'NONE' — синхронизация с cooptypes
После мержа origin/ledger3 в WalletOp добавлен NONE (внутрибалансовые
проводки без движения кошелька, например ACCEPT_RID — Dr 04 / Cr 08
без перемещения w.cap.gen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 07:03:09 +00:00
coopops 3318b64602 Merge remote-tracking branch 'origin/ledger3' into marketplace2 2026-05-08 18:41:41 +00:00
coopops 6fed2e1af0 Merge remote-tracking branch 'origin/ledger3' into marketplace2
# Conflicts:
#	components/contracts/standards-site/src/components/FocusBar.vue
#	components/contracts/standards-site/src/types/standard.ts
2026-05-08 18:39:01 +00:00
coopops 8afcb0e0ba Merge remote-tracking branch 'origin/dev' into marketplace2 2026-05-08 05:43:55 +00:00
coopops 9dae639800 [mp2-23][@ant] refactor: убрать «Связанные стандарты» — концепция связей оказалась лишней — 15 YAML стандартов лишились блока related: и заголовка §7 (sed-truncate); из standards-site убраны: section в ProcessPage с slider'ом, computed relatedLinks, RELATION_HUMAN, relationHuman, RelatedView interface, импорты standardsIndex/RouterLink; из types/standard.ts удалены RelatedStandard, RelationKind, поле related; из graph/layout.ts — hasRelated; из EndNode.vue — prop hasRelated; SKILL.md — убрана §6 «Связи» (template и описание provides/triggers/affects); .process-page grid теперь auto / minmax(0,1fr) — рабочая зона растягивается на всю освободившуюся высоту 2026-05-04 19:28:11 +00:00
coopops 0428a9f1a1 [mp2-22][@ant] tune(standards-site): START_ZOOM 1.5 → 1.2 — отдалить стартовый кадр на ~1.2 zoom-позиции (шаг ×1.2 в Vue Flow), 1.5/1.2^1.2 ≈ 1.21 2026-05-04 19:03:57 +00:00
coopops 48a01b9deb [mp2-21][@ant] fix(standards-site): фиксированный стартовый zoom для всех стандартов — раньше считали targetZoom = fittedZoom × 4.0 (capped at 3.0), и fittedZoom зависел от bounding box графа: большие стандарты (supply с 8+ статусами) получали мелкий fitted ≈ 0.3 → старт 1.2, маленькие (wroff) — fitted ≈ 0.7 → старт 2.8 (упирался в потолок); пользователь видел разный масштаб при переходе между стандартами; теперь START_ZOOM = 1.5 константа — все стандарты открываются на одной и той же приближённости, ∅-узел всегда в центре канвы; fitView оставлен только как fallback на случай отсутствия start-узла 2026-05-04 18:58:28 +00:00
coopops f417a77d58 [mp2-20][@ant] feat(standards-site): FocusBar — отдельная колонка слева, не наложение поверх канвы — раньше панель была absolute-overlay 12px/12px от левого края процесс-графа поверх VueFlow, и центрирование активного узла на ←/→ работало по середине ВСЕЙ канвы — а это «середина» приходилась под левую панель, и узел вылезал из-под неё; теперь .process-graph = flex-row из двух соседей: .focus-panel (360px / max-width 38% / min-width 260px, левая колонка с border-right) и .process-graph__canvas (flex 1 1 0, правая зона); добавлен canvasRef отдельно от containerRef — все вычисления doFit / ensureInView идут от размеров КАНВЫ (а не всего процесс-графа), поэтому центрирование стало честным внутри видимой канвы; стартовая x-позиция 0.65 → 0.5 (середина канвы); фуллскрин по-прежнему на containerRef — раскрывает обе колонки целиком; пан/пинч 2 пальцами и колесо работают в правой канве, не дёргая страницу 2026-05-04 18:48:43 +00:00
coopops 9222fb9572 [mp2-19][@ant] feat(standards-site): включить двухпальцевый pan/zoom рабочей области — раньше pan-on-scroll/zoom-on-pinch были выключены, потому что viewpoint страницы дёргался при прокрутке колесом (мы листали страницу, а VueFlow ловил wheel-event и одновременно панорамировал); теперь страница залочена (overflow:hidden, mp2-17/18), скролл страницы невозможен в принципе — pan-on-scroll и zoom-on-pinch стали уместны: два пальца на трекпаде → панорама холста, pinch → зум; prevent-scrolling=true чтобы wheel-events точно не утекали наружу 2026-05-04 18:42:09 +00:00
coopops a3f9de9eab [mp2-18][@ant] fix(standards-site): добавить min-width:0 по цепочке grid/flex — слайдер связанных стандартов скроллится, viewport не растягивает рабочую зону — без minmax(0, 1fr) в .app-shell дочерний 1fr-трек получал implicit min-width:auto и расширялся под фактическую ширину контента (горизонтальный список карточек 320px × N), что выталкивало рабочую зону вправо за пределы экрана и прятало зум-контролы; теперь .app-shell = 240px / minmax(0,1fr), .app-main / .process-page / .process-page__workspace / .related / .graph-row все получили min-width:0 — overflow:auto в .related__list заработал, контент больше не «толкает» родителей наружу 2026-05-04 18:41:27 +00:00
coopops 3d4493e72b [mp2-17][@ant] feat(standards-site): полноэкранный layout без скролла страницы — header full-width / workspace 1fr / related-слайдер; убрать некорректный фрагмент purpose в supply.yaml — страница стандарта = grid auto/1fr/auto в 100vh app-main, скролл страницы заблокирован (overflow:hidden), 95% высоты — workspace; шапка с метой (Контракт/Процесс/Сущность/Зона/Статус) растянута в горизонтальную flex-полосу под заголовком, не прижата справа; «Связанные стандарты» — горизонтальный слайдер внизу с scroll-snap (фикс-карточки 320px, скролл колесом/жестом); ProcessGraph отвязан от calc(100vh) и растягивается через flex/grid stretch родителя; в supply.yaml вырезан второй абзац purpose («Используется, когда пайщики хотят провести сделку через кооператив...») — он не отражал реальный смысл процесса 2026-05-04 18:38:38 +00:00
coopops ef5746ac15 [mp2-16][@ant] feat(standards-site): описание процесса = карточка ∅-узла, viewport стартует в правой зоне — purpose/summary стандарта больше не отдельная секция над графом, а контент карточки FocusBar при фокусе на узле «начало процесса» (∅); FocusBar теперь раскрывается по умолчанию (фокус на ∅ — было исключено из v-if); viewport canvas: стартовый круглешок встаёт в правой зоне (cw*0.65 вместо 0.32) — слева остаётся место под FocusBar (~42% ширины) и контент не уезжает за панель; zoom +2 позиции (boost 2.8→4.0, max 2.0→3.0); рабочая область выше (min-height 520→640, height calc 240→160), потому что интро-секция убрана и осталось больше места под граф 2026-05-04 18:33:04 +00:00
coopops 609201e75f [mp2-15][@ant] refactor(standards): убрать бессмысленный f-префикс из имён операций marketplace — fconv→conv, ftarg→assign, fblock→block, funblk→unblk, fefund→recall, payspl→payout — f (от funds) не нёс информации и путал чтение, новые имена односложные и говорят сами за себя; зеркальные пары: assign↔recall (целевое назначение / отзыв), block↔unblk (unblock=13 не помещается в eosio::name); правки только в стандартах + sister-doc архитектуры — operations.ts/hpp ещё не содержат этих кодов (войдут одной chunk-PR при формализации стандарта с командой) 2026-05-04 18:25:13 +00:00
coopops 1fdbbba22e [mp2-14][@ant] fix(standards): o.wal.fconv — двусторонний L3 на двух USER_SHARED одного пайщика — конвертация паевого в членский = перевод между двумя USER_SHARED одного и того же пайщика, не выпуск; должно быть -X на available паевого (w.wal.share) и +X на available членского (w.wal.member); правка в standard и в SKILL.md-примере (там же переименован устаревший o.mkt.fconv в актуальный o.wal.fconv) 2026-05-04 18:14:42 +00:00
coopops 8188c83ca1 [mp2-13][@ant] docs(standards): переписать ВСЕ описания (actions.purpose, states.description, scenario.steps) простым языком — стандарт читают аналитик/председатель/пайщик/бухгалтер, не разработчик; не дублируем ledger2-операции / кошельки / проводки в прозу — они уже видны структурно в карточках L1/L2/L3 и графе состояний; правило расширено в SKILL.md и feedback memory с summary/purpose на ВСЕ прозаические поля стандарта 2026-05-04 18:07:09 +00:00
coopops dc57d280cb [mp2-12][@ant] refactor: customer/supplier → orderer/offerer + L1/L2/L3-карточки во всю ширину — кооперативный пайщик может быть и заказчиком и поставщиком (обратная поставка), нейтральные orderer/offerer держат симметрию ролей; standards-site рендерит каждую под-карточку (Проводки / Кошельки кооператива / Кошелёк пайщика / Сумма) отдельной строкой во всю ширину вместо grid auto-fit, где L1/L2 зажимались в узкую колонку 2026-05-04 17:58:26 +00:00
coopops fdc0035954 [mp2-11][@ant] feat(standards): двусторонний L3 + игнор Дт=Кт same-account — расширяем схему Ledger2Operation массивом l3[] для случаев перевода между двумя USER_SHARED одного пайщика (классика — w.wal.member ↔ w.mkt.member); FocusBar рендерит каждое движение отдельной строкой; supply ftarg/fefund переведены на массив l3 (-X на источнике, +X на получателе) + L1 проводки Дт 86 / Кт 86 убраны (нетто-нулевое изменение по 86 — смарт-контракт игнорирует, аналитика «по программе» фиксируется на L2/L3) 2026-05-04 17:50:56 +00:00
coopops 0f41f0d9d4 [mp2-10][@ant] feat(standards): трёхступенчатая модель членских взносов в marketplace — universal w.wal.member + programmatic w.mkt.member, серия createorder = o.wal.fconv → o.mkt.ftarg → o.mkt.fblock; cancel/return оставляют средства на w.mkt.member.available, fefund переопределён как явный вывод в общий членский кошелёк 2026-05-04 17:34:46 +00:00
coopops 28d4583a15 [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>
2026-05-04 17:21:23 +00:00
coopops 5ab5234507 [mp2-8][@ant] docs(standards): summary/purpose трёх стандартов marketplace переписаны простым языком — описание в шапке должно быть для председателя/пайщика, не для разработчика, отвечать на «что/зачем/когда»; идентификаторы кошельков (w.mkt.fund), имена операций (o.mkt.purch), проводки (Дт 10/Кт 86), Backend/крон/compensating forward — всё это уже расписано дальше по странице, в шапке только смысл процесса 2026-05-04 15:19:40 +00:00
coopops 75d4fff416 [mp2-7][@ant] chore: empty commit для перетриггера publish-docs на marketplace2 — coopenomics-docs теперь клонирует именно эту ветку, нужно ещё одна сборка чтобы реестр стандартов на docs.coopenomics.world подхватил три новых yaml (p.mkt.supply/return/wroff) 2026-05-04 14:59:03 +00:00
coopops 7da8ddec71 Merge branch 'marketplace2' of github.com:coopenomics/mono into marketplace2 2026-05-04 14:45:20 +00:00
coopops 1325943601 Merge branch 'reports' into marketplace2 2026-05-04 14:44:24 +00:00
coopops 2f9495ff83 [598-3][@ant] feat(standards): 3 стандарта Marketplace MVP под членскую модель
Добавлены кооперативные стандарты для процессов контракта marketplace
в режиме членских взносов (упрощённая модель с одним программным
кошельком w.mkt.fund, без транзитов):

  • p.mkt.supply.standard.yaml   — Прямая поставка-приобретение имущества
                                    (7 операций ledger2, двухвариантная приёмка А/Б,
                                     двойные подписи АПП приёмки и АПП выдачи)
  • p.mkt.return.standard.yaml   — Гарантийный возврат имущества
                                    (compensating forward o.mkt.return,
                                     без ledger2::revert)
  • p.mkt.wroff.standard.yaml    — Утилизация скоропорта
                                    (ACCOUNT_ONLY с транзитом через счёт 91)

Все wallet_from/wallet_to используют eosio::name-строки
(w.mkt.fund, w.wal.share, w.mkt.payout) согласно рефакторингу 2026-04-27
на ветке reports. Документы привязаны к существующим шаблонам:
  • АПП приёмки = registry_id 702 (AssetContributionAct)
  • АПП выдачи  = registry_id 802 (ReturnByAssetAct)
  • Заявление возврата = registry_id 800 (ReturnByAssetStatement)
  • ТТН и протокол списания = 0 + TODO (нет шаблонов в реестре).

Все операции имеют статус proposed; имена ≤12 chars eosio::name.
Бухгалтерские проводки помечены TBD-Standardization для последующей
сверки с Ангелиной/Игорем.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:44:14 +00:00
coopops 638824d9c7 [598-3][@ant] chore(standards): удалить устаревший p.mkt.reqst.standard.yaml
Стандарт описывал клиринговую модель donor'а (паевый взнос имуществом
+ возврат паевого взноса имуществом). На ветке marketplace2 заменён
тремя новыми стандартами под членскую модель Стол заказов MVP:
  • p.mkt.supply.standard.yaml   — Прямая поставка-приобретение имущества
  • p.mkt.return.standard.yaml   — Гарантийный возврат имущества
  • p.mkt.wroff.standard.yaml    — Утилизация скоропорта

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:09:37 +00:00
coopops c96518d4b3 Merge branch 'reports' into marketplace2 2026-05-04 11:27:16 +00:00
coopops 3a3144a8c3 Merge branch 'reports' into marketplace2 2026-05-04 11:24:36 +00:00
coopops d29a9d624f [598-3][@ant] refactor: унифицировать cooplace → marketplace по всему стеку
Цель — убрать дублирующее имя 'cooplace' в пользу единого 'marketplace'
во всех слоях (controller, sdk, desktop). Имя 'cooplace' было артефактом
переходного периода между плагин-стилем (старая marketplace ветка) и
extension-стилем (feat/marketplace-orders); оставалось 4 параллельных
каталога с одной семантикой.

Каталоги (git mv с историей):
- controller/src/application/cooplace → application/marketplace
- controller/src/domain/cooplace → domain/marketplace
- controller/src/extensions/cooplace → extensions/marketplace-cards
  (отдельная extension со своей сферой ProductCard/Category/SupplyOrder,
   именно поэтому marketplace-cards, а не marketplace — чтобы не
   коллизировать с extensions/marketplace, который про request/attribute)
- sdk/src/mutations/cooplace → mutations/marketplace
- sdk/src/selectors/cooplace → selectors/marketplace

Файлы:
- cooplace.module.ts → marketplace.module.ts (в application и domain)
- cooplace.service/interactor/resolver.ts → marketplace.*
- cooplace-extension.module.ts → marketplace-cards.module.ts
- cooplace-blockchain.port/adapter.ts → marketplace-blockchain.*

Классы:
- CooplaceModule → MarketplaceModule
- CooplaceDomainModule → MarketplaceDomainModule
- CooplaceExtensionModule → MarketplaceCardsModule
- CooplaceInteractor/Resolver/Service → Marketplace*
- CooplaceBlockchainPort/Adapter → MarketplaceBlockchainPort/Adapter
- COOPLACE_BLOCKCHAIN_PORT (Symbol) → MARKETPLACE_BLOCKCHAIN_PORT
- camelCase: cooplaceInteractor/Service/BlockchainPort → marketplace*

Чтобы избежать коллизии класс-имён в extensions/marketplace
(где уже были MarketplaceDomainModule и MarketplaceApplicationModule
для catalog-слоя), переименовал их:
- MarketplaceDomainModule → MarketplaceExtensionDomainModule
- MarketplaceApplicationModule → MarketplaceExtensionApplicationModule
Эти классы видны только внутри extensions/marketplace, наружу торчит
только MarketplacePluginModule + MarketplacePlugin.

SDK re-exports:
- mutations/index.ts: 'export * as Cooplace' → 'export * as Marketplace'
- selectors/index.ts: 'export * from ./cooplace' → './marketplace'

Сборки после изменений:
- controller tsc --noEmit: EXIT=0
- desktop vue-tsc --noEmit --skipLibCheck: EXIT=0
- контракт marketplace не затронут (cooplace там никогда не было)
2026-04-25 12:57:07 +00:00
coopops cfd98f780a [mp2-5][@ant] fix(desktop): vue-tsc зелёный после трансплантации
1. Убран лишний import { withDefaults } from 'vue' из 9 файлов:
   - features/Request/CreateChildOrder/ui/CreateChildOrderButton.vue
   - widgets/Marketplace/SupplyOrderRequestCard/ui/Steps/{First..Eighth}Step.vue
   В Vue 3 withDefaults — compiler macro, доступен глобально без импорта,
   явный import конфликтует с локальным declaration (TS2440).

2. widgets/Marketplace/SupplyOrderRequestCard/ui/Base/Base.vue —
   request.membership_fee_amount → request.membership_fee
   (поле в IRequestData/SDK называется membership_fee).

vue-tsc --noEmit --skipLibCheck: 0 ошибок.
2026-04-25 11:34:00 +00:00
coopops cdddea93b5 [mp2-4][@ant] feat: подцепить marketplace extensions в registries
controller:
- app.module.ts — импорт MarketplacePluginModule + CooplaceExtensionModule в imports
- extensions/extensions.registry.ts — заменить заглушку 'orders' на реальный
  entry 'market' с MarketplacePluginModule + MarketplacePlugin + MarketplaceSchema

desktop:
- src/processes/init-installed-extensions/extensions-registry.ts —
  импорт marketInstall из extensions/market/install и регистрация ключа 'market'

tsc --noEmit (controller): EXIT=0
2026-04-25 11:24:24 +00:00
coopops dd2220b299 [mp2-3][@ant] fix(controller): tsc --noEmit зелёный после трансплантации
1. extensions/marketplace/* — заменить пути:
   - ~/modules/auth/* → ~/application/auth/*
   - ~/modules/logger/logger-app.service → ~/application/logger/logger-app.service
   На reports эти каталоги называются application/, не modules/.

2. extensions/marketplace/marketplace-extension.module.ts — убрать
   ExtensionPortsModule (на reports его нет, capital его не использует —
   это артефакт marketplace-orders без поддержки на текущей базе).

3. infrastructure/blockchain/adapters/cooplace-blockchain.adapter.ts —
   взять с feat/marketplace-orders: добавлены 5 методов
   (reqReturn, coopstock, acceptStock, destroy, reoffer), которые
   требует CooplaceBlockchainPort.

cooptypes пересобран (pnpm -C cooptypes build) — Ledger2/Ledger2Contract
теперь экспортируются.

tsc --noEmit: EXIT=0
2026-04-25 11:22:51 +00:00
coopops 7c75408348 [mp2-2][@ant] fix(marketplace): добиться сборки контракта в режиме :test после трансплантации
Поправки на reports-base, чтобы новые actions из marketplace-orders
скомпилировались:

1. lib/core/document.hpp — добавлен Document::remove_document(docs, name)
   (вызывается в reqreturn.cpp; в исходном marketplace-orders отсутствовал —
   значит контракт там и не собирался).

2. lib/domain/table_marketplace_requests.hpp — добавлены поля request:
   - delivery_type (internal/external)
   - contribution_type (share/member)
   Эти поля устанавливаются в orderoffer/createorder и используются в
   проводках complete (см. MARKET-LOGIC.md, Правила 3 и 4).

3. marketplace.hpp — переключён include с ../lib/common.hpp на ../lib/index.hpp
   (на reports common.hpp удалён рефактором 1744fab580).

Сборка: bash build.sh marketplace test → marketplace.wasm OK.
2026-04-25 11:14:29 +00:00
coopops 9bf899ec2d [mp2-1.1] chore: убрать случайно добавленный current_project.md 2026-04-25 11:10:37 +00:00
coopops 6ef93b19b9 [mp2-1][@ant] feat(marketplace2): трансплантировать marketplace из feat/marketplace-orders
Перенесено с origin/feat/marketplace-orders (commit 585afb16eb):
- contracts/cpp/marketplace/* — новые actions deliver_on_order/{createorder,respondoffer},
  deliver_on_offer/{acceptstock,coopstock,destroy,reoffer,reqreturn}
- cooptypes/contracts/marketplace/* — типы новых actions
- sdk/mutations/cooplace/* — disputeOnRequest и др.
- controller/extensions/marketplace (87 файлов) + extensions/cooplace (split) — НОВЫЕ
- controller/application/cooplace + domain/cooplace — обновлены под новые actions
- desktop/pages/Marketplace/{DisputePage,ShipmentsPage,WarehousePage} — НОВЫЕ
- desktop/widgets/Marketplace/SupplyOrderRequestCard/ui/Steps/{ReqReturnStep,RetAuthorizedStep}
- desktop/features/Request — обновлены
- desktop/extensions/market — НОВОЕ
- boot/tests/marketplace.test.ts — НОВОЕ
- MARKET-LOGIC.md — описание бизнес-процессов

Поправлен include marketplace.hpp: ../lib/common.hpp → ../lib/index.hpp
(на reports common.hpp удалён, заменён на index.hpp).

shared_marketplace.hpp НЕ переносим — на reports его содержимое размазано
по lib/core/marketplace/marketplace.hpp + lib/domain/table_marketplace_*.hpp

Сборка пока не проверена — следующий шаг.
2026-04-25 11:10:29 +00:00
1127 changed files with 93074 additions and 16980 deletions
+1
View File
@@ -0,0 +1 @@
{"sessionId":"5b149d17-5cbc-4201-8975-a9def97637b9","pid":19728,"procStart":"44281","acquiredAt":1776935929080}
-223
View File
@@ -1,223 +0,0 @@
name: Build Docker Images
# Триггерится по успешному завершению `Build contracts container`. Это
# гарантирует, что к моменту webhook'а на тестнет/прод образ
# `dicoop/contracts:<branch>` уже обновлён в DockerHub.
#
# Раньше workflow висел на `push: tags`, и при релизе (`chore(release):
# publish` + git tag) запускался параллельно с build-contracts. Поскольку
# build-containers (~8 мин) короче build-contracts (~11 мин), webhook
# отлетал раньше окончания build-contracts на 2-3 минуты — ансибл на
# тестнете подтягивал ПРЕДЫДУЩИЙ образ контрактов и перетирал чейн
# старым wasm. Инцидент 2026-05-13 (ledger2 фикс walletop sweep'а
# откатился ансиблом).
#
# Теперь: build-contracts триггерится и по push веток, и по push тэгов
# (см. tags в build-contracts.yaml). После его success этот workflow
# резолвит — есть ли тэг на коммите, и если есть — собирает контейнеры
# и шлёт webhook. Если тэга нет (обычный push в ветку без релиза) —
# делаем no-op.
on:
workflow_run:
workflows: ["Build contracts container"]
types: [completed]
# Ручной fallback: запустить сборку контейнеров и деплой для конкретного
# тэга, если автоматический workflow_run не сработал (типичный кейс —
# миграционное окно: на default-ветке (main) старая версия build-containers
# с `push: tags`, а на dev/testnet уже новая с `workflow_run`. До мержа
# в main новая логика не активна).
workflow_dispatch:
inputs:
tag:
description: 'Git-тэг для сборки и деплоя (например v2026.5.13-alpha-3)'
required: true
jobs:
build:
runs-on: ubuntu-latest
# Job запускается, если: (а) workflow_run от build-contracts успешен,
# либо (б) ручной запуск через workflow_dispatch.
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout repository at triggering SHA
uses: actions/checkout@v3
with:
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.event.workflow_run.head_sha }}
fetch-depth: 0
- name: Resolve tag for triggering commit
id: resolve_tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG_NAME="${{ github.event.inputs.tag }}"
HEAD_SHA=$(git rev-parse "$TAG_NAME^{commit}")
echo "Ручной запуск: tag=$TAG_NAME sha=$HEAD_SHA"
else
HEAD_SHA="${{ env.HEAD_SHA }}"
# `git describe --exact-match` отдаёт имя тэга, лежащего ровно
# на этом коммите; код 128 если тэга нет — тогда выходим без
# сборки контейнеров (push в ветку без релизного тэга).
TAG_NAME=$(git describe --tags --exact-match "$HEAD_SHA" 2>/dev/null || true)
if [ -z "$TAG_NAME" ]; then
echo "На коммите $HEAD_SHA тэга нет — релиза не было, контейнеры не собираем."
echo "has_tag=false" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "Найден тэг: $TAG_NAME"
echo "has_tag=true" >> $GITHUB_OUTPUT
echo "DOCKER_TAG=$TAG_NAME" >> $GITHUB_ENV
echo "HEAD_SHA=$HEAD_SHA" >> $GITHUB_ENV
# Проверяем, является ли тег продакшн-тегом (не содержит alpha, beta, rc и т.д.)
if [[ ! $TAG_NAME =~ -(alpha|beta|rc|test) ]]; then
echo "IS_PRODUCTION_TAG=true" >> $GITHUB_ENV
echo "Это продакшн тег, будем добавлять latest"
else
echo "IS_PRODUCTION_TAG=false" >> $GITHUB_ENV
echo "Это не продакшн тег, latest не добавляем"
fi
- name: Debug info
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "Триггер: ${{ github.event_name }}"
echo "Триггерящий коммит: ${{ env.HEAD_SHA }}"
echo "Резолвенный тэг: ${{ env.DOCKER_TAG }}"
echo "Последние коммиты:"
git log -n 3 --oneline
echo "Проверяем файлы в директории components/desktop/src-ssr:"
ls -la components/desktop/src-ssr/ || echo "Директория не найдена!"
echo "Проверяем файлы в middlewares:"
ls -la components/desktop/src-ssr/middlewares/ || echo "Директория middlewares не найдена!"
- name: Login to DockerHub
if: steps.resolve_tag.outputs.has_tag == 'true'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Сначала собираем базовый образ с runtime
- name: Build base image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
docker build --target runtime -t dicoop/mono-base:${{ env.DOCKER_TAG }} .
docker push dicoop/mono-base:${{ env.DOCKER_TAG }}
# Если это продакшн тег, добавляем latest
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/mono-base:${{ env.DOCKER_TAG }} dicoop/mono-base:latest
docker push dicoop/mono-base:latest
fi
# Создаем сервисные образы на основе базового
- name: Build desktop image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.desktop
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/desktop\", \"run\", \"start\"]" >> Dockerfile.desktop
docker build -t dicoop/desktop:${{ env.DOCKER_TAG }} -f Dockerfile.desktop .
docker push dicoop/desktop:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/desktop:${{ env.DOCKER_TAG }} dicoop/desktop:latest
docker push dicoop/desktop:latest
fi
- name: Build controller image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.coopback
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/controller\", \"run\", \"start\"]" >> Dockerfile.coopback
docker build -t dicoop/coopback:${{ env.DOCKER_TAG }} -f Dockerfile.coopback .
docker push dicoop/coopback:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/coopback:${{ env.DOCKER_TAG }} dicoop/coopback:latest
docker push dicoop/coopback:latest
fi
# TEMP: парсер используется существующим деплоем как dicoop/cooparser,
# пока не мигрировали на dicoop/parser из отдельного coopenomics/parser repo.
# Оставляем сборку и пуш образа до завершения миграции потребителей.
- name: Build parser image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.cooparser
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/parser\", \"run\", \"start\"]" >> Dockerfile.cooparser
docker build -t dicoop/cooparser:${{ env.DOCKER_TAG }} -f Dockerfile.cooparser .
docker push dicoop/cooparser:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/cooparser:${{ env.DOCKER_TAG }} dicoop/cooparser:latest
docker push dicoop/cooparser:latest
fi
- name: Build notificator image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.notificator
echo "CMD [\"pnpm\", \"-F\", \"coop-notificator\", \"run\", \"start\"]" >> Dockerfile.notificator
docker build -t dicoop/notificator:${{ env.DOCKER_TAG }} -f Dockerfile.notificator .
docker push dicoop/notificator:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/notificator:${{ env.DOCKER_TAG }} dicoop/notificator:latest
docker push dicoop/notificator:latest
fi
- name: Build notifications image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.notifications
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/notifications\", \"run\", \"sync\"]" >> Dockerfile.notifications
docker build -t dicoop/notifications:${{ env.DOCKER_TAG }} -f Dockerfile.notifications .
docker push dicoop/notifications:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/notifications:${{ env.DOCKER_TAG }} dicoop/notifications:latest
docker push dicoop/notifications:latest
fi
# Отправка хука для деплоя. К этому моменту build-contracts.yaml
# уже завершился успешно (мы и есть его workflow_run-зависимый
# потребитель), значит образ `dicoop/contracts:<branch>` гарантированно
# обновлён в DockerHub — гонки 2026-05-13 больше не воспроизвести.
- name: Trigger deployment webhook
if: success() && steps.resolve_tag.outputs.has_tag == 'true'
run: |
if [[ "${{ env.DOCKER_TAG }}" == *alpha* ]]; then
# Хук для тестнета (alpha теги)
curl -X POST ${{ vars.TESTNET_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d '${{ env.DOCKER_TAG }}'
else
# Хук для продакшена (остальные теги)
curl -X POST ${{ vars.PRODUCTION_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d '${{ env.DOCKER_TAG }}'
fi
# Уведомление в Telegram об успехе
- name: Telegram notify success
if: success() && steps.resolve_tag.outputs.has_tag == 'true'
run: |
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
ADDITIONAL_INFO=" (с тегом latest)"
else
ADDITIONAL_INFO=""
fi
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="✅ [GITHUB MONO] Успешная сборка контейнеров: $GITHUB_REPOSITORY (sha=${{ env.HEAD_SHA }}) [${{ env.DOCKER_TAG }}]$ADDITIONAL_INFO"
# Уведомление в Telegram об ошибке (только если до этого был
# резолвен тэг — иначе no-op запуск без сборки и без алертов).
- name: Telegram notify failure
if: failure() && steps.resolve_tag.outputs.has_tag == 'true'
run: |
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="❌ [GITHUB MONO] Ошибка при сборке контейнеров: $GITHUB_REPOSITORY (sha=${{ env.HEAD_SHA }}) [${{ env.DOCKER_TAG }}]"
+33 -3
View File
@@ -1,18 +1,48 @@
# .github/workflows/trigger-coopenomics.yml
name: Trigger Contracts Docs Deploy
# Триггерит rebuild docs в coopenomics/coopenomics только по продакшн-
# тэгам на main. Раньше дёргался на каждый push в dev/testnet/main/capital,
# теперь — только формальный релиз (без -alpha/-beta/-rc/-test).
on:
push:
branches: [dev, testnet, main, capital] # или когда нужно триггерить
tags: ['v*']
jobs:
gate:
runs-on: ubuntu-latest
outputs:
should_trigger: ${{ steps.check.outputs.should_trigger }}
steps:
- name: Checkout (для git branch --contains)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Gate — main + non-alpha
id: check
run: |
TAG="${{ github.ref_name }}"
if [[ "$TAG" =~ -(alpha|beta|rc|test) ]]; then
echo "Тэг $TAG — pre-release, docs deploy не дёргаем"
echo "should_trigger=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! git branch -r --contains "${{ github.sha }}" | grep -qE "^[[:space:]]*origin/main$"; then
echo "Тэг $TAG не на main — docs deploy не дёргаем"
echo "should_trigger=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Тэг $TAG — продакшн релиз на main, дёргаем coopenomics"
echo "should_trigger=true" >> "$GITHUB_OUTPUT"
trigger-coopenomics:
needs: gate
if: needs.gate.outputs.should_trigger == 'true'
runs-on: ubuntu-latest
steps:
- name: Trigger Coopenomics deployment
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.COOPENOMICS_PAT }}
repository: coopenomics/coopenomics # укажи правильный owner/repo
repository: coopenomics/coopenomics
event-type: deploy_from_mono
client-payload: '{"repository": "${{ github.repository }}", "sha": "${{ github.sha }}", "ref": "${{ github.ref }}", "actor": "${{ github.actor }}"}'
+25 -56
View File
@@ -7,12 +7,18 @@ name: Build contracts container
# ke-bootstrap'ом (отдельный артефакт), который при старте ноды деплоит
# контракты через `cleos set contract`.
#
# Триггер — push в одну из веток-окружений (dev / testnet / main).
# Маппинг:
# Триггер — push в одну из веток-окружений (dev / testnet / main) с
# изменением .cpp/CMakeLists/build-скриптов. Маппинг:
# dev → IS_TESTNET=ON, tag=dev
# testnet → IS_TESTNET=ON, tag=testnet
# main → IS_TESTNET=OFF, tag=main + latest
#
# ВАЖНО: тэги `v*` тут НЕ обрабатываются — они уезжают в `release.yaml`,
# который делает релиз атомарно (контракты + контейнеры + webhook в
# одном job'е). См. release.yaml для контекста — там в комментарии
# описана причина (гонка build-contracts и build-containers по push'у
# тэга, инцидент 2026-05-13 с ledger2 walletop sweep'ом).
#
# Сам compile идёт через CDT-образ `dicoop/blockchain_v5.1.1:dev`
# (тот же, что использует `components/contracts/build.sh|build-all.sh`).
# Артефакты копируются в slim alpine-образ.
@@ -22,24 +28,12 @@ name: Build contracts container
# TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID — уведомления
on:
push:
branches: [dev, testnet, main]
paths:
- 'components/contracts/cpp/**'
- 'components/contracts/CMakeLists.txt'
- 'components/contracts/build.sh'
- 'components/contracts/build-all.sh'
- 'components/contracts/docker/**'
- '.github/workflows/build-contracts.yaml'
# Дополнительно ловим все push'и тэгов: build-containers.yaml зависит от
# успеха этой сборки через workflow_run, и без unconditional запуска по
# тегу build-containers не получит триггер для коммитов, где .cpp не
# менялся (например `chore(release): publish` правит только версии
# пакетов). См. историю гонки 2026-05-13: webhook деплоя сработал на
# 2:34 быстрее обновления `dicoop/contracts:testnet`, на чейн уехал
# старый wasm.
tags:
- '*'
# Только ручной запуск. Тэги (релизы) обрабатывает release.yaml,
# который собирает контракты атомарно вместе с контейнерами и
# webhook'ом — отдельная сборка по push'у в ветку только мешает
# (две параллельные сборки на каждый release-bump). Здесь оставляем
# workflow_dispatch для ручной пересборки `dicoop/contracts:<branch>`
# (например, чтобы прокатить wasm на dev-ноду без релизного тэга).
workflow_dispatch:
jobs:
@@ -48,55 +42,30 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# Полная история нужна, чтобы при push'е тэга резолвить ветку
# через `git branch -r --contains $SHA` (см. следующий step).
fetch-depth: 0
- name: Determine build mode and docker tag
# Источник режима — ВЕТКА, не имя тэга. Сначала пытаемся взять
# `github.ref_name` как ветку (push веток); если не подошло — это
# push тэга, и мы ищем какая из dev/testnet/main содержит коммит.
# Тэг такого окружения нести семантику не должен (например,
# `v2026.5.13-alpha-2` → ветка testnet, BUILD_MODE=test).
run: |
REF="${{ github.ref_name }}"
REF_TYPE="${{ github.ref_type }}"
BRANCH=""
if [ "$REF_TYPE" = "branch" ]; then
BRANCH="$REF"
else
SHA="${{ github.sha }}"
for candidate in main testnet dev; do
if git branch -r --contains "$SHA" 2>/dev/null | grep -qE "^[[:space:]]*origin/${candidate}$"; then
BRANCH="$candidate"
break
fi
done
fi
case "$BRANCH" in
case "${{ github.ref_name }}" in
main)
echo "BUILD_MODE=prod" >> $GITHUB_ENV
echo "DOCKER_TAG=main" >> $GITHUB_ENV
echo "EXTRA_TAG=latest" >> $GITHUB_ENV
echo "BUILD_MODE=prod" >> "$GITHUB_ENV"
echo "DOCKER_TAG=main" >> "$GITHUB_ENV"
echo "EXTRA_TAG=latest" >> "$GITHUB_ENV"
;;
testnet)
echo "BUILD_MODE=test" >> $GITHUB_ENV
echo "DOCKER_TAG=testnet" >> $GITHUB_ENV
echo "EXTRA_TAG=" >> $GITHUB_ENV
echo "BUILD_MODE=test" >> "$GITHUB_ENV"
echo "DOCKER_TAG=testnet" >> "$GITHUB_ENV"
echo "EXTRA_TAG=" >> "$GITHUB_ENV"
;;
dev)
echo "BUILD_MODE=test" >> $GITHUB_ENV
echo "DOCKER_TAG=dev" >> $GITHUB_ENV
echo "EXTRA_TAG=" >> $GITHUB_ENV
echo "BUILD_MODE=test" >> "$GITHUB_ENV"
echo "DOCKER_TAG=dev" >> "$GITHUB_ENV"
echo "EXTRA_TAG=" >> "$GITHUB_ENV"
;;
*)
echo "Unsupported ref: ref_name=$REF ref_type=$REF_TYPE resolved_branch='$BRANCH'" >&2
echo "Unsupported branch ${{ github.ref_name }}" >&2
exit 1
;;
esac
echo "Resolved BRANCH=$BRANCH (ref_name=$REF ref_type=$REF_TYPE)"
- name: Pull CDT toolchain image
run: docker pull dicoop/blockchain_v5.1.1:dev
+32 -6
View File
@@ -1,16 +1,42 @@
name: Publish Docs
# Публикация только по продакшн-тэгам, лежащим на main (без -alpha/-beta/-rc/-test).
# Раньше триггерилось на каждый push в main/testnet/dev/reports/marketplace2 →
# docs пересобирались по 5+ раз в день впустую. Теперь — только релиз.
on:
push:
branches:
- main
- testnet
- dev
- reports
- marketplace2
tags: ['v*']
jobs:
gate:
runs-on: ubuntu-latest
outputs:
should_publish: ${{ steps.check.outputs.should_publish }}
steps:
- name: Checkout (для git branch --contains)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Gate — main + non-alpha
id: check
run: |
TAG="${{ github.ref_name }}"
if [[ "$TAG" =~ -(alpha|beta|rc|test) ]]; then
echo "Тэг $TAG — pre-release, docs не публикуем"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! git branch -r --contains "${{ github.sha }}" | grep -qE "^[[:space:]]*origin/main$"; then
echo "Тэг $TAG не на main — docs не публикуем"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Тэг $TAG — продакшн релиз на main, публикуем docs"
echo "should_publish=true" >> "$GITHUB_OUTPUT"
build-and-publish-docs:
needs: gate
if: needs.gate.outputs.should_publish == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
+276
View File
@@ -0,0 +1,276 @@
name: Release
# Атомарный релизный workflow. Триггерится исключительно push'ем тэга
# `v*`. В одном job'е по порядку:
# 1) собирает контракты под mode (prod/test) и пушит `dicoop/contracts:<branch>`,
# 2) собирает базовый образ + сервисные контейнеры и пушит `dicoop/<svc>:<tag>`,
# 3) шлёт webhook деплоя (testnet/production по типу тэга).
#
# Зачем атомарно: раньше `build-contracts` и `build-containers`
# крутились параллельно по push'у тэга, и webhook от build-containers
# (~8 мин) обгонял окончание build-contracts (~11 мин) → ансибл на
# тестнете подхватывал ПРЕДЫДУЩИЙ образ контрактов и перетирал чейн
# старым wasm. Попытка связать их через workflow_run упёрлась в
# default-branch caveat и потерю head_sha в YAML-выражениях. Теперь
# нет двух workflow'ов — нет гонки, нет fallback'ов.
#
# Триггер по веткам (dev/testnet/main без тэга) тут НЕ обрабатывается —
# для CI-обновления `dicoop/contracts:<branch>` без релиза остаётся
# `build-contracts.yaml`.
#
# Резолв ветки. Тэг семантики окружения не несёт (например
# `v2026.5.13-alpha-2` может лежать на testnet или dev — определяет
# именно ветка, на которую сделан merge перед тэгом). Поэтому ищем
# первое совпадение SHA с remote-ветками main/testnet/dev.
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve branch, build mode and tags
run: |
TAG_NAME="${{ github.ref_name }}"
SHA="${{ github.sha }}"
BRANCH=""
for candidate in main testnet dev; do
if git branch -r --contains "$SHA" 2>/dev/null | grep -qE "^[[:space:]]*origin/${candidate}$"; then
BRANCH="$candidate"
break
fi
done
if [ -z "$BRANCH" ]; then
echo "::error::Тэг $TAG_NAME ($SHA) не лежит ни на одной из dev/testnet/main"
exit 1
fi
case "$BRANCH" in
main)
BUILD_MODE=prod
CONTRACTS_TAG=main
CONTRACTS_EXTRA=latest
;;
testnet)
BUILD_MODE=test
CONTRACTS_TAG=testnet
CONTRACTS_EXTRA=
;;
dev)
BUILD_MODE=test
CONTRACTS_TAG=dev
CONTRACTS_EXTRA=
;;
esac
# Прод-тэги (без -alpha/-beta/-rc/-test) уезжают на боевой
# webhook + получают тэг :latest у сервисных образов.
if [[ "$TAG_NAME" =~ -(alpha|beta|rc|test) ]]; then
IS_PROD=false
WEBHOOK_URL='${{ vars.TESTNET_WEBHOOK_URL }}'
else
IS_PROD=true
WEBHOOK_URL='${{ vars.PRODUCTION_WEBHOOK_URL }}'
fi
{
echo "TAG_NAME=$TAG_NAME"
echo "SHA=$SHA"
echo "BRANCH=$BRANCH"
echo "BUILD_MODE=$BUILD_MODE"
echo "CONTRACTS_TAG=$CONTRACTS_TAG"
echo "CONTRACTS_EXTRA=$CONTRACTS_EXTRA"
echo "IS_PROD=$IS_PROD"
echo "WEBHOOK_URL=$WEBHOOK_URL"
} >> "$GITHUB_ENV"
echo "Release $TAG_NAME → branch=$BRANCH, build_mode=$BUILD_MODE, contracts:$CONTRACTS_TAG, prod=$IS_PROD"
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# === Этап 1: контракты ===
- name: Pull CDT toolchain image
run: docker pull dicoop/blockchain_v5.1.1:dev
- name: Compile contracts
working-directory: components/contracts
run: |
./build-all.sh "$BUILD_MODE"
echo "--- build/contracts ---"
ls -la build/contracts/
- name: Stage contracts docker context
working-directory: components/contracts
run: |
ROOT="docker/.context"
rm -rf "$ROOT"
mkdir -p "$ROOT/contracts"
# Имена контрактов = аргументы add_contract_build(...) только в
# незакомменченных строках (CMake-комментарий начинается с `#`).
NAMES=$(grep -E '^[[:space:]]*add_contract_build\(' CMakeLists.txt \
| sed -E 's/^[[:space:]]*add_contract_build\(([^)]*)\).*/\1/')
echo "Contracts to package:"
echo "$NAMES"
MANIFEST="$ROOT/contracts/manifest.json"
pack_one() {
local NAME="$1" WASM="$2" ABI="$3"
if [ ! -f "$WASM" ] || [ ! -f "$ABI" ]; then
echo "::warning::Skipping $NAME: artifacts missing ($WASM / $ABI)" >&2
return 1
fi
mkdir -p "$ROOT/contracts/$NAME"
cp "$WASM" "$ROOT/contracts/$NAME/$NAME.wasm"
cp "$ABI" "$ROOT/contracts/$NAME/$NAME.abi"
local SHA_WASM=$(sha256sum "$WASM" | awk '{print $1}')
local SHA_ABI=$(sha256sum "$ABI" | awk '{print $1}')
printf ' {"name":"%s","sha256_wasm":"%s","sha256_abi":"%s"}' \
"$NAME" "$SHA_WASM" "$SHA_ABI"
}
{
printf '{\n'
printf ' "build_mode": "%s",\n' "$BUILD_MODE"
printf ' "git_sha": "%s",\n' "$SHA"
printf ' "tag": "%s",\n' "$TAG_NAME"
printf ' "branch": "%s",\n' "$BRANCH"
printf ' "built_at": "%s",\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf ' "contracts": [\n'
FIRST=1
emit() {
entry=$(pack_one "$@") || return 0
[ $FIRST -eq 0 ] && printf ',\n'
FIRST=0
printf '%s' "$entry"
}
for c in $NAMES; do
if [ "$c" = "system" ]; then
# У system многоконтрактная сборка: разносим под-контракты
# eosio.* как полноценные элементы каталога, сам "system"
# как имя пропускаем.
for sub in build/contracts/system/contracts/eosio.*/; do
[ -d "$sub" ] || continue
sub_name=$(basename "$sub")
emit "$sub_name" "$sub/$sub_name.wasm" "$sub/$sub_name.abi"
done
else
emit "$c" "build/contracts/$c/$c.wasm" "build/contracts/$c/$c.abi"
fi
done
printf '\n ]\n'
printf '}\n'
} > "$MANIFEST"
cp docker/Dockerfile "$ROOT/Dockerfile"
cp docker/entrypoint.sh "$ROOT/entrypoint.sh"
chmod +x "$ROOT/entrypoint.sh"
echo "--- manifest.json ---"
cat "$MANIFEST"
- name: Build and push contracts image
working-directory: components/contracts
run: |
IMAGE="dicoop/contracts"
SHORT_SHA="${SHA::7}"
docker build \
--label "org.opencontainers.image.revision=$SHA" \
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--label "build.mode=$BUILD_MODE" \
-t "$IMAGE:$CONTRACTS_TAG" \
./docker/.context
docker push "$IMAGE:$CONTRACTS_TAG"
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
docker push "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
if [ -n "$CONTRACTS_EXTRA" ]; then
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_EXTRA"
docker push "$IMAGE:$CONTRACTS_EXTRA"
fi
- name: Verify pushed contracts image
run: |
docker run --rm "dicoop/contracts:$CONTRACTS_TAG" list
echo "---"
docker run --rm "dicoop/contracts:$CONTRACTS_TAG" sha256
# === Этап 2: контейнеры приложений ===
- name: Build and push base image
run: |
docker build --target runtime -t "dicoop/mono-base:$TAG_NAME" .
docker push "dicoop/mono-base:$TAG_NAME"
if [ "$IS_PROD" = "true" ]; then
docker tag "dicoop/mono-base:$TAG_NAME" dicoop/mono-base:latest
docker push dicoop/mono-base:latest
fi
- name: Build and push service images
run: |
build_service() {
local SVC="$1" PKG="$2" CMD="$3"
local DOCKERFILE="Dockerfile.$SVC"
{
echo "FROM dicoop/mono-base:$TAG_NAME"
echo "CMD [\"pnpm\", \"-F\", \"$PKG\", \"run\", \"$CMD\"]"
} > "$DOCKERFILE"
docker build -t "dicoop/$SVC:$TAG_NAME" -f "$DOCKERFILE" .
docker push "dicoop/$SVC:$TAG_NAME"
if [ "$IS_PROD" = "true" ]; then
docker tag "dicoop/$SVC:$TAG_NAME" "dicoop/$SVC:latest"
docker push "dicoop/$SVC:latest"
fi
}
build_service desktop '@coopenomics/desktop' start
build_service coopback '@coopenomics/controller' start
# TEMP: cooparser — пока не мигрировали потребителей на dicoop/parser
# из отдельного coopenomics/parser repo.
build_service cooparser '@coopenomics/parser' start
build_service notificator 'coop-notificator' start
build_service notifications '@coopenomics/notifications' sync
# === Этап 3: webhook деплоя ===
- name: Trigger deployment webhook
run: |
curl -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$TAG_NAME"
- name: Telegram notify success
if: success()
run: |
EXTRA=""
if [ "$IS_PROD" = "true" ]; then
EXTRA=" (с тегом latest)"
fi
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=✅ [GITHUB MONO] Релиз $TAG_NAME ($BRANCH, contracts:$CONTRACTS_TAG, sha=${SHA::7})$EXTRA"
- name: Telegram notify failure
if: failure()
run: |
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=❌ [GITHUB MONO] Ошибка релиза $TAG_NAME ($BRANCH, sha=${SHA::7})"
+337
View File
@@ -0,0 +1,337 @@
# MARKET-LOGIC.md — Бизнес-процессы маркетплейса
## 2А. БИЗНЕС-ПРОЦЕССЫ СМАРТ-КОНТРАКТОВ
### Основные бизнес-сценарии
**Сценарий 1: Поставка-приобретение имущества**
- **Описание:** Обмен имущества между пайщиками через кооператив с блокировкой средств и документооборотом
- **Участники:** Заказчик, Поставщик, Совет кооператива, Председатель КУ
- **Бизнес-ценность:** Основной процесс кооперативного маркетплейса
**Сценарий 1А.** Прямая поставка (OFFER→ORDER) — поставщик публикует, заказчик откликается
**Сценарий 1Б.** Обратная поставка (ORDER→OFFER) — заказчик публикует, поставщики откликаются
**Сценарий 1В.** Из запасов кооператива (COOPSTOCK) — имущество уже на балансе
**Сценарий 2: Гарантийный возврат**
- **Описание:** Возврат бракованного имущества в течение гарантийного срока
- **Участники:** Заказчик, Поставщик, Председатель КУ, Совет
- **Бизнес-ценность:** Защита интересов пайщика и качества имущества
**Сценарий 3: Уничтожение/перепредложение**
- **Описание:** Утилизация просроченного или перепродажа по новой цене
- **Участники:** Председатель КУ, Совет
- **Бизнес-ценность:** Управление складскими запасами и минимизация потерь
**Сценарий 4: Транспортировка**
- **Описание:** Перевозка имущества между КУ группами
- **Участники:** Председатель КУ отправителя, Водитель, Председатель КУ получателя
- **Бизнес-ценность:** Логистика распределённой кооперативной сети
---
### Процесс 1А: Прямая поставка (OFFER→ORDER)
**Предусловия:**
- Поставщик создал карточку товара в БД (статус: `published`)
- Заказчик нашёл карточку на витрине и решил заказать
- У заказчика достаточно средств на цифровом кошельке
**Последовательность шагов:**
#### Шаг 1: orderoffer — Заказчик создаёт заявку
- **Предусловие:** Карточка товара в БД (published). При создании заявки происходит **match** — заявка публикуется в блокчейн
- **Исполнитель:** Заказчик (через контроллер, авторизация от кооператива)
- **Подписываемые документы:** Заявление на конвертацию из кошелька (convert_in)
- **Проводки по кошелькам:**
- Списать у заказчика `total_cost` из ЦПП «Цифровой Кошелёк»
- Начислить заказчику `total_cost` в ЦПП «Маркетплейс»
- Заблокировать у заказчика `total_cost` в ЦПП «Маркетплейс»
- **Статус заявки:** `active`
- **Параметры:** `delivery_type` (internal/external), `contribution_type` (share/member)
#### Шаг 2: accept — Поставщик принимает заявку
- **Исполнитель:** Поставщик
- **Подписываемые документы:**
- Заявление на конвертацию в кошелёк (convert_out)
- Заявление на имущественный паевой взнос (contribution_statement)
- **Проводки:** Нет
- **Эффект:** Создаётся **одно** заявление в совет — `authcontrib` (авторизация взноса)
- **Статус заявки:** `accepted`
#### Шаг 3: authcontrib — Совет авторизует взнос
- **Исполнитель:** Совет кооператива (автоматически через soviet контракт)
- **Подписываемые документы:** Решение совета об авторизации взноса
- **Проводки:** Нет
- **Статус заявки:** `authorized` — поставщик может начинать поставку
#### Шаг 4: supply — Поставщик поставляет имущество на КУ
- **Исполнитель:** Поставщик
- **Подписываемые документы:** Акт поставки (supply_act)
- **Проводки:** Нет
- **Статус заявки:** `supplied1`
#### Шаг 5: supplcnf — Председатель КУ подтверждает поставку
- **Исполнитель:** Председатель КУ поставщика
- **Подписываемые документы:** Акт подтверждения поставки (supply_act_conf)
- **Проводки по кошелькам:**
- Начислить поставщику `base_cost` в ЦПП «Маркетплейс»
- Заблокировать у поставщика `base_cost` в ЦПП «Маркетплейс»
- **Проводки по ledger:**
- Увеличить паевой фонд (счёт 80) на `total_cost`
- **Статус заявки:** `supplied2`, имущество на складе КУ
#### Шаг 6: [Транспортировка] — При необходимости (см. Сценарий 4)
#### Шаг 7: delivered — Готово к выдаче
- **Исполнитель:** Председатель КУ получателя
- **Подписываемые документы:** Нет
- **Проводки:** Нет
- **Статус заявки:** `delivered`
#### Шаг 8: reqreturn — Заказчик запрашивает возврат
- **Исполнитель:** Заказчик
- **Подписываемые документы:** Заявление на возврат паевого взноса имуществом (return_statement) — с актуальными данными о весе/составе
- **Проводки:** Нет
- **Эффект:** Создаётся заявление в совет — `authreturn`
- **Статус заявки:** `reqreturn`
#### Шаг 9: authreturn — Совет авторизует возврат
- **Исполнитель:** Совет кооператива
- **Подписываемые документы:** Решение совета об авторизации возврата
- **Проводки:** Нет
- **Статус заявки:** `retauthorized`
#### Шаг 10: receive — Председатель КУ передаёт имущество
- **Исполнитель:** Председатель КУ
- **Подписываемые документы:** Акт приёма-передачи (receive_act)
- **Проводки:** Нет
- **Статус заявки:** `received1`
#### Шаг 11: receivecnf — Заказчик подтверждает получение
- **Исполнитель:** Заказчик
- **Подписываемые документы:** Акт подтверждения получения (receive_act_conf)
- **Проводки по кошелькам:**
- Списать заблокированный баланс заказчика `total_cost` из ЦПП «Маркетплейс»
- **Проводки по ledger:**
- Уменьшить паевой фонд (счёт 80) на `base_cost`
- **Статус заявки:** `received2`
- **Эффект:** Устанавливается `warranty_delay_until` (текущее время + гарантийный срок)
#### Шаг 12: complete — Завершение после гарантии
- **Исполнитель:** Система (после истечения `warranty_delay_until`)
- **Проводки по кошелькам:**
- Списать заблокированный баланс поставщика `base_cost` из ЦПП «Маркетплейс»
- Начислить поставщику `base_cost` в ЦПП «Цифровой Кошелёк»
- **Статус:** Заявка удаляется из блокчейна
**Постусловия:**
- Заказчик получил имущество
- Поставщик получил средства на кошелёк
- Членские взносы распределены по фондам кооператива
---
### Процесс 1Б: Обратная поставка (ORDER→OFFER)
**Предусловия:**
- Заказчик создал карточку заказа в БД (статус: `published`, тип: `order`)
- Поставщик нашёл заказ и готов поставить
**Последовательность шагов:**
#### Шаг 1: createorder — Заказчик публикует заказ
- **Предусловие:** Карточка заказа в БД. При публикации — **match** в блокчейн
- **Проводки:** Списание + блокировка `total_cost` заказчика (аналогично orderoffer)
- **Статус:** `active`
#### Шаг 2: respondoffer — Поставщик откликается
- **Исполнитель:** Поставщик
- **Подписываемые документы:** Заявление на взнос + конвертация
- **Эффект:** Создаётся заявление в совет `authcontrib`
- **Статус предложения:** `accepted`
#### Шаги 3-12: Аналогичны Процессу 1А (authcontrib → complete)
---
### Процесс 1В: Из запасов кооператива (COOPSTOCK)
**Предусловия:**
- Имущество уже на балансе кооператива (на складе КУ)
- Председатель КУ создаёт предложение coopstock
**Последовательность шагов:**
#### Шаг 1: coopstock — Создание предложения
- **Исполнитель:** Председатель КУ
- **Проводки:** Нет (имущество уже на балансе)
- **Статус:** `delivered` (сразу готово к выдаче)
#### Шаг 2: acceptstock — Заказчик принимает
- **Исполнитель:** Заказчик
- **Подписываемые документы:** Конвертация + заявление на возврат
- **Проводки:** Блокировка `total_cost` заказчика
- **Эффект:** Сразу создаётся заявление в совет `authreturn`
- **Статус:** `reqreturn`
#### Шаги 3-6: authreturn → receive → receivecnf → complete (аналогично 1А шаги 9-12)
**Особенности:**
- Пропущены шаги accept, authcontrib, supply, supplcnf — не нужны
- Нет проводок по паевому фонду при поставке (имущество уже на балансе)
---
### Процесс 2: Гарантийный возврат
**Предусловия:**
- Заявка в статусе `received2` (имущество получено)
- Не истёк `warranty_delay_until`
- Заказчик обнаружил дефект
**Последовательность шагов:**
#### Шаг 1: dispute — Заказчик подаёт претензию
- **Подписываемые документы:** Претензия (wdispute) + фото/видео
- **Эффект:** Деньги поставщика дополнительно блокируются
#### Шаг 2: Рассмотрение на КУ (вне контракта)
- **Исполнитель:** Председатель КУ
- **Действия:** Осмотр, подтверждение/отклонение претензии
#### Шаг 3: wauthorize — Совет авторизует возврат
- **Подписываемые документы:** Решение о возврате (wreturn_auth) + решение о выдаче поставщику (wsupply_auth)
#### Шаг 4: wreturn — Возврат имущества в кооператив
- **Проводки:** Разблокировка средств заказчика, начисление на кошелёк
#### Шаг 5: woffer — Предложение имущества поставщику
#### Шаг 6: waccept — Поставщик принимает/отказывается
**Альтернативные потоки:**
- **Отклонение претензии:** Заказчик забирает имущество обратно
- **Поставщик не забрал:** Имущество перепредлагается (→ reoffer) или уничтожается (→ destroy)
---
### Процесс 3: Уничтожение имущества
**Предусловия:**
- Заявка в статусе `delivered` или `supplied2`
- Истёк `deadline_for_receipt` (заказчик не пришёл)
**Шаг 1: destroy — Уничтожение**
- **Исполнитель:** Председатель КУ (chairman)
- **Подписываемые документы:** Акт уничтожения (destruction_act)
- **Проводки:**
- Возврат заказчику: `total_cost - cancellation_fee` → разблокировать и вернуть в кошелёк
- Штраф `cancellation_fee` → в фонд членских взносов (spreadamount)
- Поставщику: `base_cost` → разблокировать и вернуть в кошелёк
- **Эффект:** Заявка удаляется из блокчейна
---
### Процесс 3Б: Перепредложение (reoffer)
**Предусловия:**
- Аналогичны destroy, но срок годности НЕ истёк
**Шаг 1: reoffer — Перепредложение по новой цене**
- **Исполнитель:** Председатель КУ
- **Проводки:** Аналогичны destroy (возврат средств)
- **Эффект:** Старая заявка удаляется, создаётся новая типа `coopstock` со статусом `delivered`
---
### Процесс 4: Транспортировка (Shipment)
**Предусловия:**
- Имущество на складе КУ (статус `supplied2` или `shiprecvd`)
- Нужно доставить на другой КУ
**Последовательность шагов:**
#### Шаг 1: createship — Создание перевозки
- **Исполнитель:** Представитель КУ отправителя
- **Документ:** Акт передачи (shsendact)
- **Статус перевозки:** `loading`
#### Шаг 2: signbydriver — Подпись водителя
- **Исполнитель:** Водитель-пайщик
- **Документ:** Акт приёма (shloadact)
- **Эффект:** Товары снимаются со склада
- **Статус:** `transit`
#### Шаг 3: arrived — Прибытие
- **Исполнитель:** Водитель
- **Документ:** Акт доставки (sharriveact)
- **Статус:** `arrived`
#### Шаг 4: receiveshipm — Приём на складе
- **Исполнитель:** Представитель КУ получателя
- **Документ:** Акт приёма на складе (shrecvact)
- **Эффект:** Товары ставятся на склад КУ назначения, перевозка удаляется
- **Статус заявок:** `shiprecvd`
**Альтернативный поток:** retransport — промежуточная перегрузка на другой маршрут
---
### Связи между процессами
- **Процесс 1 → Процесс 4:** После `supplcnf` (шаг 5) товар может пойти на транспортировку перед `delivered`
- **Процесс 1 → Процесс 2:** После `receivecnf` (шаг 11) возможен гарантийный возврат до истечения `warranty_delay_until`
- **Процесс 2 → Процесс 3:** Если поставщик не забирает возвращённое имущество → destroy/reoffer
- **Процесс 1 → Процесс 3:** Если заказчик не приходит за товаром → destroy/reoffer
- **Процесс 3Б → Процесс 1В:** reoffer создаёт coopstock → новый цикл 1В
### Бизнес-правила и ограничения
**Правило 1: Блокировка средств при заказе**
- **Описание:** Средства заказчика блокируются в момент создания заявки в блокчейне (match)
- **Применение:** orderoffer, createorder, acceptstock
- **Последствия нарушения:** Заявка не может быть создана без достаточных средств
**Правило 2: Одно заявление в совет при принятии**
- **Описание:** При accept создаётся только authcontrib (на взнос). authreturn — перед получением
- **Применение:** accept, reqreturn
- **Обоснование:** Заявление на возврат содержит точные данные (вес), доступные после доставки
**Правило 3: Тип доставки определяет маршрут**
- **Описание:** `delivery_type: internal` — между КУ через Shipment, `external` — через внешний сервис (СДЭК)
- **Применение:** Определяется в карточке при создании
**Правило 4: Тип взноса определяет проводки**
- **Описание:** `contribution_type: share` — паевой взнос (возврат имуществом), `member` — членский взнос (кооператив покупает)
- **Применение:** Влияет на проводки при complete
**Правило 5: Гарантийный период**
- **Описание:** `warranty_delay_until = received_at + warranty_period_secs`. До истечения — complete невозможен, dispute возможен
- **Применение:** complete, dispute
**Правило 6: Карточки в БД до match**
- **Описание:** Карточки хранятся в PostgreSQL (draft → moderation → published). В блокчейн уходят только при match
- **Применение:** Все процессы создания заявок
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/blago-cli",
"version": "2026.5.13-alpha-3",
"version": "2026.5.15-5",
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
"type": "module",
"private": true,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2026.5.13-alpha-3",
"version": "2026.5.15-5",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "CLI-утилита инициализации блокчейна и кооператива",
@@ -0,0 +1,133 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import Blockchain from '../blockchain'
import config from '../configs'
import { getTotalRamUsage, globalRamStats } from '../utils/getTotalRamUsage'
import { addUser } from '../init/participant'
import { generateRandomUsername } from '../utils/randomUsername'
const blockchain = new Blockchain(config.network, config.private_keys)
let supplier: string
let customer: string
const fakeDocument = {
hash: '157192B276DA23CC84AB078FC8755C051C5F0430BF4802E55718221E6B76C777',
public_key: 'PUB_K1_5JhMfxbsNebajHcTEK8yC9uNN9Dit9hEmzE8ri8yMhhzzEtUA4',
signature: 'SIG_K1_KmKWPBC8dZGGDGhbKEoZEzPr3h5crRrR2uLdGRF5DJbeibY1MY1bZ9sPwHsgmPfiGFv9psfoCVsXFh9TekcLuvaeuxRKA8',
meta: '{}',
}
const testHash = '0000000000000000000000000000000000000000000000000000000000000001'
beforeAll(async () => {
await blockchain.update_pass_instance()
supplier = generateRandomUsername()
customer = generateRandomUsername()
console.log('supplier:', supplier)
console.log('customer:', customer)
await addUser(supplier)
await addUser(customer)
}, 500_000)
afterAll(() => {
console.log('\n📊 **MARKETPLACE RAM USAGE** 📊')
let total = 0
for (const [key, ram] of Object.entries(globalRamStats)) {
console.log(` ${key} = ${(ram / 1024).toFixed(2)} kb`)
total += ram
}
console.log(`\n💾 **TOTAL**: ${(total / 1024).toFixed(2)} kb\n`)
})
describe('Marketplace — orderoffer flow', () => {
it('контракт marketplace задеплоен', async () => {
const info = await blockchain.api.v1.chain.get_info()
expect(info).toBeDefined()
expect(info.chain_id).toBeDefined()
})
it('создание заявки orderoffer (заказчик)', async () => {
const coopname = config.coopname
const result = await blockchain.transact({
account: 'marketplace',
name: 'orderoffer',
authorization: [{ actor: coopname, permission: 'active' }],
data: {
coopname,
receiver_braname: coopname,
username: customer,
hash: testHash,
units: 10,
unit_cost: '100.0000 RUB',
product_lifecycle_secs: 2592000,
warranty_period_secs: 604800,
membership_fee_amount: '50.0000 RUB',
cancellation_fee_amount: '10.0000 RUB',
convert_in: fakeDocument,
meta: JSON.stringify({ title: 'Тестовый товар', description: 'Описание' }),
},
})
expect(result).toBeDefined()
console.log('orderoffer tx:', result?.response?.transaction_id?.substring(0, 16))
const ramUsed = await getTotalRamUsage(coopname)
globalRamStats['orderoffer'] = ramUsed
})
it('принятие заявки поставщиком (accept)', async () => {
const coopname = config.coopname
const result = await blockchain.transact({
account: 'marketplace',
name: 'accept',
authorization: [{ actor: coopname, permission: 'active' }],
data: {
coopname,
supplier_braname: coopname,
username: supplier,
request_hash: testHash,
convert_out: fakeDocument,
product_contribution_statement: fakeDocument,
},
})
expect(result).toBeDefined()
console.log('accept tx:', result?.response?.transaction_id?.substring(0, 16))
const ramUsed = await getTotalRamUsage(coopname)
globalRamStats['accept'] = ramUsed
})
})
describe('Marketplace — coopstock flow', () => {
const stockHash = '0000000000000000000000000000000000000000000000000000000000000002'
it('создание предложения из запасов кооператива', async () => {
const coopname = config.coopname
const result = await blockchain.transact({
account: 'marketplace',
name: 'coopstock',
authorization: [{ actor: coopname, permission: 'active' }],
data: {
coopname,
braname: coopname,
hash: stockHash,
units: 5,
unit_cost: '50.0000 RUB',
product_lifecycle_secs: 1296000,
warranty_period_secs: 302400,
membership_fee_amount: '25.0000 RUB',
meta: JSON.stringify({ title: 'Уценённый товар', description: 'Из запасов' }),
},
})
expect(result).toBeDefined()
console.log('coopstock tx:', result?.response?.transaction_id?.substring(0, 16))
})
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/cleos",
"version": "2026.5.13-alpha-3",
"version": "2026.5.15-5",
"private": true,
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
"scripts": {
@@ -34,6 +34,10 @@ void capital::convertsegm(eosio::name coopname, eosio::name username,
document2 convert_statement) {
require_auth(coopname);
// Проверяем подпись заявления о трансляции паевого взноса (шаблон 1080).
// Без этого on-chain принял бы любую сконструированную document2 без валидной подписи.
verify_document_or_fail(convert_statement, {username});
// Получаем сегмент пайщика
auto segment = Capital::Segments::get_segment_or_fail(coopname, project_hash, username,
"Сегмент пайщика не найден");
@@ -108,6 +112,23 @@ void capital::convertsegm(eosio::name coopname, eosio::name username,
// Инкрементируем счётчик сконвертированных сегментов
Capital::Projects::increment_converted_segments(coopname, current_project.id);
// Линкуем заявление о трансляции (1080) к пакету процесса p.cap.rid
// (package = result_hash, ведущий документ пакета — заявление о внесении РИД, 1040).
// Не make_complete_document: тот вытаскивает 1080 на верхний уровень реестра
// как самостоятельный документ. Нужен newlink — тот же канон, что в signact1/signact2:
// off-chain controller добавляет документ в группу пакета по action+package, не как top-level.
// Делаем ДО delete_result, чтобы линковка прошла, пока result_hash ещё анкер процесса.
Action::send<newlink_interface>(
_soviet,
"newlink"_n,
_capital,
coopname,
username,
Names::Capital::CONVERT_SEGMENT,
result_hash,
convert_statement
);
// Удаляем сегмент после конвертации
Capital::Segments::remove_segment(coopname, segment.id);
@@ -20,9 +20,6 @@ title: Выдача займа пайщику
slug: debt
status: proposed
contract: capital
summary: >
Пайщик получает из паевого фонда кооператива беспроцентный целевой заём
на срок проекта; возврат происходит при сдаче акта-2 проекта.
purpose: >
«Заём пайщику» — кооператив выдаёт пайщику беспроцентный целевой заём
из паевого фонда на срок проекта. Заявку последовательно одобряют
@@ -281,17 +278,3 @@ operations:
(Кт 58), сумма зачитывается в паевой фонд (Дт 80) и появляется как
доступный остаток на SHARE_FUND_PAY пайщика.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
- process_type: p.cap.rid
id: public_capital_rid_process
relation: triggers
note: >
Возврат займа (o.cap.repay) технически происходит в процессе
«Приём РИД» при подписании акта-2 проекта.
- process_type: p.reg.accept
id: public_registrator_accept_process
relation: provides
note: >
Заём может получить только активный пайщик кооператива.
@@ -22,10 +22,6 @@ title: Приём инвестиции в программу
slug: invest
status: proposed
contract: capital
summary: >
Пайщик направляет ранее внесённые паевые взносы деньгами в программу
«Благорост» — без оформления договора, простой переброской средств между
своими кошельками.
purpose: >
«Приём инвестиции в программу» — пайщик переводит часть своих ранее
внесённых паевых средств в инвестицию в программу «Благорост».
@@ -125,18 +121,3 @@ operations:
кооператива, но в учёте пайщика они теперь считаются инвестицией
в «Благорост», а не свободными паевыми средствами.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
- process_type: p.wal.depo
id: public_wallet_deposit_process
relation: provides
note: >
Чтобы инвестировать, у пайщика должен быть остаток на SHARE_FUND_PAY —
его создаёт «Внесение паевого взноса» (p.wal.depo).
- process_type: p.cap.rid
id: public_capital_rid_process
relation: triggers
note: >
После инвестирования участник может коммитить РИД в проект
программы «Благорост» (p.cap.rid).
@@ -20,10 +20,6 @@ title: Приём имущественного паевого взноса
slug: property
status: proposed
contract: capital
summary: >
Пайщик передаёт кооперативу имущество (не деньги, не РИД) как паевой
взнос. Процесс многоступенчатый: предложение, одобрение, авторизация,
два акта приёма-передачи.
purpose: >
«Приём имущественного паевого взноса» — пайщик передаёт кооперативу
имущество (не деньги, не результат интеллектуальной деятельности)
@@ -265,18 +261,3 @@ operations:
«Благорост» у пайщика (w.cap.blago). Двойная запись Дт 51 / Кт 80 —
кооператив принял имущество и оно учтено как часть паевого фонда.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
- process_type: p.cap.rid
id: public_capital_rid_process
relation: affects
note: >
Альтернативный имущественный путь — приём результата интеллектуальной
деятельности (p.cap.rid). РИД отличается от обычного имущества схемой
двух последовательных операций и привязкой к проекту.
- process_type: p.reg.accept
id: public_registrator_accept_process
relation: provides
note: >
Имущественный взнос может внести только активный пайщик кооператива.
@@ -26,12 +26,6 @@ title: Приём результата интеллектуальной деят
slug: rid
status: proposed
contract: capital
summary: >
Участник программы «Благорост» оформляет результат интеллектуальной
деятельности (РИД) как имущественный паевой взнос: коммит → одобрение
мастера → заявление о результате → одобрение председателем → авторизация
советом → акт приёма-передачи (две подписи) → распределение паевого
взноса между Цифровым Кошельком и программой «Благорост».
purpose: >
«Приём результата интеллектуальной деятельности» — участник проекта
программы «Благорост» оформляет результат своей работы (РИД) как
@@ -584,32 +578,3 @@ operations:
пайщика. Бухгалтерская запись была сделана ранее в o.cap.accept
на полную сумму сегмента — здесь только перемещение по кошелькам.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
- process_type: p.cap.invest
id: public_capital_invest_process
relation: provides
note: >
Чтобы коммитить РИД в проект, участник должен быть инвестором
«Благорост» — это устанавливает p.cap.invest.
- process_type: sov.authpkg
relation: triggers
note: >
Авторизация результата советом (`authrslt`) выполняется через
универсальный процесс автоматизированного принятия решений.
- process_type: p.cap.debt
id: public_capital_debt_process
relation: triggers
note: >
Если у участника был беспроцентный заём проекта, на акте-2
срабатывает дополнительная операция o.cap.repay — заём закрывается
из паевого фонда.
- process_type: p.cap.prop
id: public_capital_property_process
relation: affects
note: >
Альтернативный имущественный путь — приём имущественного паевого
взноса (p.cap.prop), отдельный от РИД.
@@ -56,7 +56,9 @@ void ledger2::walletop(eosio::name coopname,
// op_code = 5 (NONE) намеренно не допускается: NONE-операции — это только
// бухпроводка без кошелькового движения, apply.cpp не диспатчит для них walletop.
// Прямой вызов с op_code=5 был бы no-op и сбил бы инвариант parity.
eosio::check(op_code <= 4, "walletop: неизвестный op_code");
// op_code = 6 (REVOKE) — целевое расходование blocked в пустоту; обязательная пара Dr/Cr;
// допускается, обрабатывается отдельным case ниже (введён 2026-05-11 под o.mkt.consum).
eosio::check(op_code <= 6 && op_code != 5, "walletop: неизвестный op_code");
wallets2_index wallets(get_self(), coopname.value);
userwallets_index user_wallets(get_self(), coopname.value);
@@ -298,6 +300,65 @@ void ledger2::walletop(eosio::name coopname,
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.available -= amount; });
}
cleanup_l2_if_empty(wallet_from);
cleanup_l3_if_empty(wallet_from);
break;
}
case WalletOp::NONE: {
// unreachable: apply.cpp не диспатчит walletop для NONE-операций
// (бухпроводка без кошелькового движения). Pre-check на op_code != 5 выше
// отсекает прямой вызов с этим кодом. case оставлен для исчерпывающего
// switch — иначе компилятор предупредит про unhandled enum value.
eosio::check(false, "walletop NONE: must be unreachable");
break;
}
case WalletOp::REVOKE: {
// Целевое расходование заблокированной суммы: amount списывается с
// wallet_from.blocked в пустоту, без зачисления на wallet_to.
//
// Отличие от BURN:
// - BURN списывает available (свободный остаток). Применяется к штатному
// «сжиганию» свободных средств (например, DROP_PREIMP при переходе на
// электронный учёт РИД-взноса). Dr/Cr пара опциональна.
// - REVOKE списывает blocked (зарезервированный остаток). Применяется на
// финализации заранее зарезервированной операции, когда резерв
// «уходит в потребление», а не возвращается. Обязательная пара Dr/Cr
// на уровне OPERATION_REGISTRY (compile-time validator
// revoke_pattern_correct в operations.hpp).
//
// Применение в членской модели Стола заказов: o.mkt.consum (выдача
// имущества пайщику по АПП выдачи). Пайщик заблокировал сумму под Order
// на createorder (BLOCK на w.mkt.member). При фактической выдаче
// имущества этот blocked-резерв должен «сгореть» как целевое
// потребление с одновременной фиксацией Дт 91 / Кт 10 (выбытие
// имущества со склада на счёт «прочие»). Альтернатива через
// UNBLOCK + BURN — это две операции с искажённой семантикой:
// «снял резерв» (которого по факту не снимал — потребил) +
// «сжёг свободные» (которые в моменте не были свободными). REVOKE
// одной операцией точно описывает целевое расходование blocked.
//
// Не путать с rollback: ledger2 не использует ledger2::revert в
// membership-модели marketplace; компенсирующие проводки делаются
// forward через обратную операцию (см. RETURN_BY_MEMBER + RETURN_TRANSIT_CLOSE).
eosio::check(wallet_from.value != 0, "walletop REVOKE: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop REVOKE: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"walletop REVOKE: недостаточно blocked на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) { w.blocked -= amount; });
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->blocked >= amount,
std::string{"walletop REVOKE: недостаточно L3-blocked у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.blocked -= amount; });
}
cleanup_l2_if_empty(wallet_from);
cleanup_l3_if_empty(wallet_from);
break;
+1
View File
@@ -67,6 +67,7 @@ static constexpr eosio::name _free_decision_action = "freedecision"_n;
static constexpr eosio::name _change_action = "change"_n;
static constexpr eosio::name _product_contribution_action = "productcntr"_n;
static constexpr eosio::name _product_return_action = "productrtrn"_n;
static constexpr eosio::name _marketplace_writeoff_action = "mktwroff"_n; ///< Списание скоропорта по решению совета (p.mkt.wroff)
// capitalization linked actions
@@ -73,4 +73,20 @@ namespace Document {
return document.hash == checksum256{};
}
/**
* @brief Удалить документ по имени из вектора
* @param docs Вектор документов
* @param name Имя документа для удаления
* @return true если документ был найден и удалён
*/
inline bool remove_document(std::vector<named_document>& docs, const name& name) {
for (auto it = docs.begin(); it != docs.end(); ++it) {
if (it->name == name) {
docs.erase(it);
return true;
}
}
return false;
}
}
@@ -32,17 +32,22 @@ enum class AccountType : uint8_t {
* состояния «принятый коммит» (паевой взнос имуществом в переходе РИД
* в программу Благорост): commit → Dr 08 / Cr 80, accept → Dr 04 / Cr 08.
*
* Состав (6 счетов):
* Состав (8 счетов):
*
* - 04 — Нематериальные активы (РИД, принятые в паевой фонд)
* - 08 — Вложения во внеоборотные активы (промежуточное состояние)
* - 10 — Материалы (склад имущества на кооперативных участках; per-КУ субсчета)
* - 51 — Расчётный счёт
* - 58 — Финансовые вложения (выданные пайщикам беспроцентные займы)
* - 80 — Паевой фонд (складочный капитал)
* - 86 — Целевое финансирование (без субсчетов)
* - 91 — Прочие доходы и расходы (транзит при выдаче имущества пайщику
* и при утилизации скоропорта по «Столу заказов»)
*
* Счёт 67 удалён: беспроцентные займы пайщикам идут через 58/51, без 67.
*
* Счета 10 и 91 добавлены 2026-05-11 под членскую модель «Стола заказов».
*
* Правило кодирования id: integer(code) * 1000.
* Детализация — через wallets (см. wallets.hpp).
*
@@ -52,12 +57,16 @@ struct ledger2_accounts {
// Активы
static constexpr uint64_t INTANGIBLE_ASSETS = 4 * 1000; ///< 04 — Нематериальные активы (А)
static constexpr uint64_t NON_CURRENT_INVESTMENTS = 8 * 1000; ///< 08 — Вложения во внеоборотные активы (А)
static constexpr uint64_t MATERIALS = 10 * 1000; ///< 10 — Материалы (А; склад имущества на КУ, per-КУ субсчета)
static constexpr uint64_t BANK_ACCOUNT = 51 * 1000; ///< 51 — Расчётный счёт (А)
static constexpr uint64_t FINANCIAL_INVESTMENTS = 58 * 1000; ///< 58 — Финансовые вложения (А)
// Пассивы
static constexpr uint64_t SHARE_FUND = 80 * 1000; ///< 80 — Паевой фонд (П)
static constexpr uint64_t TARGET_RECEIPTS = 86 * 1000; ///< 86 — Целевое финансирование (П)
// Активно-пассивный
static constexpr uint64_t OTHER_INCOME_EXPENSES = 91 * 1000; ///< 91 — Прочие доходы и расходы (А/П; транзит на выдаче и при утилизации скоропорта)
};
/**
@@ -74,18 +83,20 @@ struct Ledger2AccountMeta {
};
/**
* @brief Хардкод-справочник плана счетов (MVP, 7 записей).
* @brief Хардкод-справочник плана счетов (MVP, 8 записей).
*
* `constexpr std::array` + `string_view` — чтобы не было dynamic init
* при загрузке контракта и тип был полностью заморожен на этапе сборки.
*/
inline constexpr std::array<Ledger2AccountMeta, 6> LEDGER2_ACCOUNT_MAP = {{
inline constexpr std::array<Ledger2AccountMeta, 8> LEDGER2_ACCOUNT_MAP = {{
{ ledger2_accounts::INTANGIBLE_ASSETS, "Нематериальные активы", AccountType::ACTIVE },
{ ledger2_accounts::NON_CURRENT_INVESTMENTS, "Вложения во внеоборотные активы", AccountType::ACTIVE },
{ ledger2_accounts::MATERIALS, "Материалы", AccountType::ACTIVE },
{ ledger2_accounts::BANK_ACCOUNT, "Расчётный счёт", AccountType::ACTIVE },
{ ledger2_accounts::FINANCIAL_INVESTMENTS, "Финансовые вложения", AccountType::ACTIVE },
{ ledger2_accounts::SHARE_FUND, "Паевой фонд (складочный капитал)", AccountType::PASSIVE },
{ ledger2_accounts::TARGET_RECEIPTS, "Целевое финансирование", AccountType::PASSIVE },
{ ledger2_accounts::OTHER_INCOME_EXPENSES, "Прочие доходы и расходы", AccountType::ACTIVE_PASSIVE },
}};
static constexpr size_t LEDGER2_ACCOUNT_MAP_SIZE = LEDGER2_ACCOUNT_MAP.size();
@@ -64,6 +64,7 @@ namespace operations {
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → WITHDRAWALS_SINK).
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: BLOCK на SHARE_FUND_PAY (без Dr/Cr).
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: UNBLOCK на SHARE_FUND_PAY (без Dr/Cr).
inline constexpr eosio::name CONVERT_TO_MEMBER = "o.wal.conv"_n; ///< Конвертация цифрового рубля в универсальный членский кошелёк (Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → CK_MEMBER). Conditional-шаг серии createorder в «Столе заказов».
}
// capital
@@ -82,10 +83,26 @@ namespace operations {
inline constexpr eosio::name CONVERT_TO_BLAGO = "o.cap.cnvbl"_n; ///< Конвертация сегмента: РИД → ЦПП «Благорост» (TRANSFER GENERATOR_FUND → BLAGOROST_FUND, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
}
// marketplace
// marketplace — членская модель «Стола заказов» (refactor 2026-05-11; старые
// o.mkt.supply / o.mkt.recv — клиринговые — удалены).
//
// Составные бухгалтерские проводки через транзит счёта 91 «Прочие доходы
// и расходы» разбиты на пары операций (часть 1 + часть 2). Контракт
// вызывает обе последовательно в одной транзакции Antelope — атомарность
// обеспечивается транзакцией. Имена с суффиксом «2» — закрытие транзита.
namespace marketplace {
inline constexpr eosio::name CONFIRM_SUPPLY = "o.mkt.supply"_n; ///< Подтверждение поставки (Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY).
inline constexpr eosio::name CONFIRM_RECEIPT = "o.mkt.recv"_n; ///< Подтверждение получения (Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → SUPPLIER_PAYMENTS).
inline constexpr eosio::name ASSIGN_TO_PROGRAM = "o.mkt.assign"_n; ///< Целевое назначение членского взноса пайщика в программу Marketplace (TRANSFER CK_MEMBER → MARKETPLACE_MEMBER, без проводки — оба кошелька на счёте 86). Conditional-шаг серии createorder.
inline constexpr eosio::name BLOCK_FOR_ORDER = "o.mkt.block"_n; ///< Блокировка членского взноса заказчика под конкретный Order (BLOCK на MARKETPLACE_MEMBER, без Dr/Cr).
inline constexpr eosio::name UNBLOCK_ON_CANCEL = "o.mkt.unblk"_n; ///< Разблокировка членского взноса при отмене Order'а (UNBLOCK на MARKETPLACE_MEMBER, без Dr/Cr). Сумма остаётся на .available и может быть потрачена на следующий заказ в программе.
inline constexpr eosio::name RECALL_TO_UNIVERSAL = "o.mkt.recall"_n; ///< Вывод программного членского взноса в универсальный членский кошелёк пайщика (TRANSFER MARKETPLACE_MEMBER → CK_MEMBER, без проводки — оба кошелька на счёте 86). Явное действие пайщика, не часть авто-flow отмены.
inline constexpr eosio::name PURCHASE_FROM_SUPPLIER = "o.mkt.purch"_n; ///< Приёмка имущества кооперативом по АПП приёмки от поставщика (Dr 10 / Cr 86, NONE — только бухпроводка, кошельки не двигаются; имущество — аналитика по 10). Атомарно с PAY_SUPPLIER на закрывающей подписи председателя.
inline constexpr eosio::name PAY_SUPPLIER = "o.mkt.payout"_n; ///< Оплата поставщику с расчётного счёта по факту приёмки (Dr 86 / Cr 51, ISSUE ∅ → SUPPLIER_PAYMENTS). Атомарно с PURCHASE_FROM_SUPPLIER.
inline constexpr eosio::name CONSUME_BY_MEMBER = "o.mkt.consum"_n; ///< Выдача имущества пайщику по АПП выдачи (часть 1 композитной проводки через транзит 91): Dr 91 / Cr 10, REVOKE MARKETPLACE_MEMBER.blocked → 0 — выбытие имущества со склада на «прочие». Закрытие транзита — отдельной операцией CONSUME_TRANSIT_CLOSE, атомарно в той же транзакции signiss2.
inline constexpr eosio::name CONSUME_TRANSIT_CLOSE = "o.mkt.consum2"_n; ///< Выдача имущества пайщику (часть 2 композитной проводки): Dr 86 / Cr 91, NONE — закрытие транзита 91 на счёт ЦФ программы. Срабатывает после CONSUME_BY_MEMBER в той же транзакции.
inline constexpr eosio::name RETURN_BY_MEMBER = "o.mkt.return"_n; ///< Гарантийный возврат имущества пайщиком — compensating forward к CONSUME_BY_MEMBER (часть 1 композитной проводки): Dr 91 / Cr 86, ISSUE ∅ → MARKETPLACE_MEMBER — восстановление «прочих» за счёт ЦФ + восстановление .available заказчика. Реверты ledger2::revert в Столе заказов не используются.
inline constexpr eosio::name RETURN_TRANSIT_CLOSE = "o.mkt.return2"_n; ///< Гарантийный возврат (часть 2 композитной проводки): Dr 10 / Cr 91, NONE — имущество назад на склад через закрытие транзита. Срабатывает после RETURN_BY_MEMBER в той же транзакции decretvisit.
inline constexpr eosio::name WRITE_OFF_PERISHABLE = "o.mkt.wroff"_n; ///< Утилизация скоропорта со склада (часть 1 композитной проводки): Dr 91 / Cr 10, NONE — выбытие со склада на «прочие». По протоколу совета.
inline constexpr eosio::name WRITE_OFF_TRANSIT_CLOSE = "o.mkt.wroff2"_n; ///< Утилизация скоропорта (часть 2 композитной проводки): Dr 86 / Cr 91, NONE — закрытие транзита 91 на счёт ЦФ программы. Срабатывает после WRITE_OFF_PERISHABLE в той же транзакции execwroff.
}
// soviet
@@ -134,8 +151,9 @@ enum class WalletOp : uint8_t {
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
BURN = 4, ///< изъятие amount с wallet_from, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
BURN = 4, ///< изъятие amount с wallet_from.available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
REVOKE = 6, ///< изъятие amount с wallet_from.blocked в пустоту, без wallet_to. Обязательная пара Dr/Cr. Отличие от BURN — списание из blocked, а не из available; применяется на целевом расходовании предварительно заблокированной суммы (например, выдача имущества пайщику по АПП выдачи). Введён 2026-05-11 под членскую модель «Стола заказов» (o.mkt.consum).
};
/**
@@ -252,16 +270,115 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::SHARE_FUND, ledger2_accounts::FINANCIAL_INVESTMENTS,
"Возврат беспроцентного займа пайщика по акту-2" },
// 12. Подтверждение поставки: Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY
{ operations::marketplace::CONFIRM_SUPPLY, processes::marketplace::REQUEST, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::SHARE_FUND_PAY,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Подтверждение поставки товара/услуги" },
// ===== Marketplace: членская модель «Стола заказов» (refactor 2026-05-11) =====
// Старые операции o.mkt.supply (CONFIRM_SUPPLY) и o.mkt.recv (CONFIRM_RECEIPT)
// удалены вместе с клиринговой моделью (паевой взнос имуществом от
// поставщика + возврат паевого имуществом заказчику) — она объявлена
// out-of-MVP до изменений в законодательстве.
// 13. Подтверждение получения: Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → SUPPLIER_PAYMENTS
{ operations::marketplace::CONFIRM_RECEIPT, processes::marketplace::REQUEST, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::SUPPLIER_PAYMENTS,
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
"Подтверждение получения товара/услуги — выплата поставщику" },
// 12a. p.mkt.supply: Конвертация цифрового рубля заказчика в универсальный
// членский кошелёк пайщика (Dr 80 / Cr 86, TRANSFER w.wal.share → w.wal.member).
// Conditional-шаг серии createorder — выполняется только при недостаче
// на w.wal.member.available.
{ operations::wallet::CONVERT_TO_MEMBER, processes::marketplace::SUPPLY, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::CK_MEMBER,
ledger2_accounts::SHARE_FUND, ledger2_accounts::TARGET_RECEIPTS,
"Конвертация цифрового рубля в членский кошелёк пайщика" },
// 12b. p.mkt.supply: Целевое назначение членского в программу Marketplace
// (TRANSFER w.wal.member → w.mkt.member, без Dr/Cr — оба на 86, аналитика на L2).
// Conditional-шаг серии createorder.
{ operations::marketplace::ASSIGN_TO_PROGRAM, processes::marketplace::SUPPLY, WalletOp::TRANSFER,
ledger2_wallets::CK_MEMBER, ledger2_wallets::MARKETPLACE_MEMBER,
0, 0,
"Целевое назначение членского взноса в программу «Стол заказов»" },
// 12c. p.mkt.supply: Блокировка под Order (BLOCK на w.mkt.member, без Dr/Cr).
{ operations::marketplace::BLOCK_FOR_ORDER, processes::marketplace::SUPPLY, WalletOp::BLOCK,
ledger2_wallets::MARKETPLACE_MEMBER, eosio::name{},
0, 0,
"Блокировка членского взноса под заказ" },
// 12d. p.mkt.supply: Разблокировка при отмене Order'а (UNBLOCK, без Dr/Cr).
// Срабатывает на cancelorder / expirecycle / declinebatch. Сумма остаётся
// на w.mkt.member.available и может быть потрачена на следующий заказ.
{ operations::marketplace::UNBLOCK_ON_CANCEL, processes::marketplace::SUPPLY, WalletOp::UNBLOCK,
ledger2_wallets::MARKETPLACE_MEMBER, eosio::name{},
0, 0,
"Разблокировка членского взноса при отмене заказа" },
// 12e. p.mkt.supply: Вывод программного членского в универсальный членский
// (TRANSFER w.mkt.member → w.wal.member, без Dr/Cr — оба на 86).
// Явное действие пайщика — не часть авто-flow отмены.
{ operations::marketplace::RECALL_TO_UNIVERSAL, processes::marketplace::SUPPLY, WalletOp::TRANSFER,
ledger2_wallets::MARKETPLACE_MEMBER, ledger2_wallets::CK_MEMBER,
0, 0,
"Вывод членского взноса в универсальный членский кошелёк" },
// 12f. p.mkt.supply: Приёмка имущества кооперативом по АПП приёмки
// (Dr 10 / Cr 86, NONE — только бухпроводка, кошельки не двигаются).
// Имущество — аналитикой по счёту 10 (per-КУ субсчета), без отдельного кошелька.
// Атомарно с PAYOUT на закрывающей подписи председателя АПП приёмки.
{ operations::marketplace::PURCHASE_FROM_SUPPLIER, processes::marketplace::SUPPLY, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::MATERIALS, ledger2_accounts::TARGET_RECEIPTS,
"Приёмка имущества кооперативом по АПП приёмки" },
// 12g. p.mkt.supply: Оплата поставщику с расчётного счёта
// (Dr 86 / Cr 51, ISSUE ∅ → w.mkt.payout). Атомарно с PURCH.
{ operations::marketplace::PAY_SUPPLIER, processes::marketplace::SUPPLY, WalletOp::ISSUE,
eosio::name{}, ledger2_wallets::SUPPLIER_PAYMENTS,
ledger2_accounts::TARGET_RECEIPTS, ledger2_accounts::BANK_ACCOUNT,
"Оплата поставщику с расчётного счёта по факту приёмки" },
// 12h. p.mkt.supply: Выдача имущества пайщику по АПП выдачи — часть 1.
// REVOKE на w.mkt.member.blocked → 0 (целевой расход членского) +
// Dr 91 / Cr 10 (выбытие имущества со склада на «прочие»).
// Согласовано с Ангелиной 2026-05-11. Атомарность с CONSUME_TRANSIT_CLOSE
// обеспечивается транзакцией Antelope — контракт вызывает обе операции
// последовательно в signiss2.
{ operations::marketplace::CONSUME_BY_MEMBER, processes::marketplace::SUPPLY, WalletOp::REVOKE,
ledger2_wallets::MARKETPLACE_MEMBER, eosio::name{},
ledger2_accounts::OTHER_INCOME_EXPENSES, ledger2_accounts::MATERIALS,
"Выдача имущества пайщику по АПП выдачи — выбытие со склада" },
// 12h2. p.mkt.supply: Выдача — часть 2. NONE Dr 86 / Cr 91 (закрытие
// транзита 91 на счёт ЦФ программы). Кошельки не двигаются.
{ operations::marketplace::CONSUME_TRANSIT_CLOSE, processes::marketplace::SUPPLY, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::TARGET_RECEIPTS, ledger2_accounts::OTHER_INCOME_EXPENSES,
"Выдача имущества пайщику — закрытие транзита на ЦФ программы" },
// 12i. p.mkt.return: Гарантийный возврат — часть 1. ISSUE ∅ → w.mkt.member
// (восстановление средств заказчика в программе) + Dr 91 / Cr 86
// (восстановление «прочих» за счёт ЦФ — зеркало 12h2). Compensating
// forward к CONSUM; ledger2::revert в Столе заказов не используется.
// Атомарность с RETURN_TRANSIT_CLOSE — транзакция Antelope в decretvisit.
{ operations::marketplace::RETURN_BY_MEMBER, processes::marketplace::RETURN, WalletOp::ISSUE,
eosio::name{}, ledger2_wallets::MARKETPLACE_MEMBER,
ledger2_accounts::OTHER_INCOME_EXPENSES, ledger2_accounts::TARGET_RECEIPTS,
"Гарантийный возврат — восстановление средств в программе" },
// 12i2. p.mkt.return: Гарантийный возврат — часть 2. NONE Dr 10 / Cr 91
// (имущество назад на склад через транзит — зеркало 12h).
{ operations::marketplace::RETURN_TRANSIT_CLOSE, processes::marketplace::RETURN, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::MATERIALS, ledger2_accounts::OTHER_INCOME_EXPENSES,
"Гарантийный возврат — имущество назад на склад через транзит" },
// 12j. p.mkt.wroff: Утилизация скоропорта — часть 1. NONE Dr 91 / Cr 10
// (выбытие со склада на «прочие»). По протоколу совета.
{ operations::marketplace::WRITE_OFF_PERISHABLE, processes::marketplace::WRITEOFF, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::OTHER_INCOME_EXPENSES, ledger2_accounts::MATERIALS,
"Утилизация скоропорта — выбытие со склада" },
// 12j2. p.mkt.wroff: Утилизация скоропорта — часть 2. NONE Dr 86 / Cr 91
// (закрытие транзита). Атомарно с WROFF в execwroff.
{ operations::marketplace::WRITE_OFF_TRANSIT_CLOSE, processes::marketplace::WRITEOFF, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::TARGET_RECEIPTS, ledger2_accounts::OTHER_INCOME_EXPENSES,
"Утилизация скоропорта — закрытие транзита на ЦФ программы" },
// 14. Конвертация в AXN: Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → DELEGATE_FEES
{ operations::soviet::CONVERT_AXN, processes::soviet::AXN_CONVERT, WalletOp::TRANSFER,
@@ -391,6 +508,22 @@ namespace ledger2_registry_detail {
return true;
}
// Правило 6b: REVOKE — wallet_from required, wallet_to == 0,
// обязательная пара Dr/Cr ≠ 0 (списание blocked в пустоту требует
// фиксации целевого расхода и выбытия актива). Введён 2026-05-11
// под `o.mkt.consum` (выдача имущества пайщику по АПП выдачи).
constexpr bool revoke_pattern_correct() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
if (e.wallet_op != WalletOp::REVOKE) continue;
if (e.wallet_from.value == 0) return false;
if (e.wallet_to.value != 0) return false;
if (e.debit_account_id == 0) return false;
if (e.credit_account_id == 0) return false;
}
return true;
}
// Правило 8: NONE — оба wallet пустые, обе проводки обязательны (Dr ≠ 0, Cr ≠ 0).
// Семантика: только бухпроводка, кошельковое движение отсутствует.
constexpr bool none_pattern_correct() {
@@ -437,6 +570,8 @@ static_assert(ledger2_registry_detail::transfer_wallet_from_ne_to(),
"OPERATION_REGISTRY: TRANSFER с wallet_from == wallet_to или одним из них == 0");
static_assert(ledger2_registry_detail::burn_pattern_correct(),
"OPERATION_REGISTRY: BURN требует wallet_from ≠ 0 и wallet_to == 0");
static_assert(ledger2_registry_detail::revoke_pattern_correct(),
"OPERATION_REGISTRY: REVOKE требует wallet_from ≠ 0, wallet_to == 0 и обе проводки Dr/Cr ≠ 0");
static_assert(ledger2_registry_detail::none_pattern_correct(),
"OPERATION_REGISTRY: NONE требует wallet_from == 0, wallet_to == 0 и обе проводки заполненными");
static_assert(ledger2_registry_detail::accounts_exist_in_map(),
@@ -15,7 +15,11 @@
* явно разрешённая модель мульти-операционных процессов:
* - processes::registrator::ACCEPT ← o.reg.payent + o.reg.putmin
* - processes::capital::RID ← o.cap.commit + o.cap.accept (+ o.cap.repay) + o.cap.cnvshr/o.cap.cnvbl
* - processes::marketplace::REQUEST ← o.mkt.supply + o.mkt.recv
* - processes::marketplace::SUPPLY ← o.wal.conv + o.mkt.assign + o.mkt.block + o.mkt.unblk +
* o.mkt.recall + o.mkt.purch + o.mkt.payout +
* o.mkt.consum + o.mkt.consum2 (composite через 91)
* - processes::marketplace::RETURN ← o.mkt.return + o.mkt.return2 (composite через 91)
* - processes::marketplace::WRITEOFF ← o.mkt.wroff + o.mkt.wroff2 (composite через 91)
*
* Одноактовые процессы: `capital::IMPORT`, `capital::PROPERTY`,
* `capital::INVEST`, `soviet::AXN_CONVERT` (process_type совпадает с
@@ -57,7 +61,9 @@ namespace processes {
// marketplace
namespace marketplace {
inline constexpr eosio::name REQUEST = "p.mkt.reqst"_n; ///< Цикл запроса маркетплейса (o.mkt.supply + o.mkt.recv).
inline constexpr eosio::name SUPPLY = "p.mkt.supply"_n; ///< Прямая поставка-приобретение имущества (9 операций: o.wal.conv + o.mkt.assign + o.mkt.block + o.mkt.unblk + o.mkt.recall + o.mkt.purch + o.mkt.payout + o.mkt.consum + o.mkt.consum2 — последние две композитная проводка через транзит 91). Старый p.mkt.reqst (клиринговый) удалён 2026-05-11.
inline constexpr eosio::name RETURN = "p.mkt.return"_n; ///< Гарантийный возврат имущества пайщиком — compensating forward к o.mkt.consum (o.mkt.return + o.mkt.return2 — композитная проводка через транзит 91).
inline constexpr eosio::name WRITEOFF = "p.mkt.wroff"_n; ///< Утилизация скоропорта со склада КУ (o.mkt.wroff + o.mkt.wroff2 — композитная проводка через транзит 91, по протоколу совета).
}
// soviet
@@ -64,8 +64,9 @@ struct ledger2_wallets {
static constexpr eosio::name GENERATOR_FUND = "w.cap.gen"_n; ///< Генератор — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
static constexpr eosio::name PREIMP_FUND = "w.cap.preimp"_n; ///< Первичный учёт РИД-взносов до перехода на электронный учёт (USER_SHARED; o.cap.preimp / o.cap.drppre)
// marketplace — выплаты
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink RECEIVE_CONFIRM, COOPERATIVE)
// marketplace — программный членский + выплаты
static constexpr eosio::name MARKETPLACE_MEMBER = "w.mkt.member"_n; ///< ЦПП «Стол Заказов» — программный членский кошелёк пайщика (USER_SHARED; BLOCK/UNBLOCK/REVOKE под Order)
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink PAYOUT, COOPERATIVE)
};
/**
@@ -94,14 +95,15 @@ struct Ledger2WalletMeta {
WalletKind kind;
};
inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (6) — L3-разрез по пайщику
inline constexpr std::array<Ledger2WalletMeta, 15> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (7) — L3-разрез по пайщику
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::CK_MEMBER, "ЦК — членская часть пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
{ ledger2_wallets::MARKETPLACE_MEMBER, "ЦПП «Стол Заказов» — программный членский у пайщика", WalletKind::USER_SHARED },
// COOPERATIVE (8) — единый кооперативный баланс, без L3
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
@@ -224,13 +226,14 @@ struct Ledger2WalletProgramMapping {
uint64_t required_program_id; // 0 = исключение (без проверки)
};
inline constexpr std::array<Ledger2WalletProgramMapping, 6> LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{
inline constexpr std::array<Ledger2WalletProgramMapping, 7> LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{
{ ledger2_wallets::MIN_SHARE_FUND, 0 /* w.reg.minshr — без проверки */ },
{ ledger2_wallets::SHARE_FUND_PAY, 1 /* ЦК */ },
{ ledger2_wallets::CK_MEMBER, 1 /* ЦК */ },
{ ledger2_wallets::BLAGOROST_FUND, 4 /* Благорост */ },
{ ledger2_wallets::GENERATOR_FUND, 3 /* Генератор */ },
{ ledger2_wallets::PREIMP_FUND, 0 /* w.cap.preimp — РИД-учёт до перехода на электронный учёт, без проверки */ },
{ ledger2_wallets::MARKETPLACE_MEMBER, 2 /* Marketplace */ },
}};
/**
@@ -1,7 +1,7 @@
#pragma once
#include <functional>
#include <optional>
#include <set>
#include <string>
#include <eosio/crypto.hpp>
@@ -9,93 +9,132 @@
#include "../../consts.hpp"
#include "../../domain/document_core.hpp"
#include "../../domain/table_ledger2_userwallets.hpp"
#include "../../domain/table_marketplace_orders.hpp"
#include "../../domain/table_marketplace_return_requests.hpp"
#include "../../domain/table_marketplace_writeoff_proposals.hpp"
#define AUTH_SIGNATURE eosio::name coopname, checksum256 request_hash, document2 authorization
using auth_interface = void(AUTH_SIGNATURE);
/**
* @brief Canonical helpers контракта marketplace (Story 11.1).
*
* Donor-helpers (`get_request_by_hash`, `get_shipment_by_hash`, namespace
* `DocumentNames`, `marketplace_callback_actions`) удалены вместе с
* соответствующими actions и таблицами (AR30).
*
* Этот файл содержит только утилиты доступа к canonical-сущностям трёх
* процессов p.mkt.supply / p.mkt.return / p.mkt.wroff и helper'ы для
* проверки доступного баланса (для createorder guard'а Locked Decision L6).
*/
namespace Marketplace {
using namespace eosio;
static const std::set<eosio::name> marketplace_callback_actions = {
"authoffs2c"_n,
"authoffc2r"_n,
"authordcont"_n,
"authordret"_n,
"declineacc"_n,
// ── Orders ──────────────────────────────────────────────────────────────
inline std::optional<order> get_order_by_hash(eosio::name coopname, const checksum256& order_hash) {
orders_index orders(_marketplace, coopname.value);
auto idx = orders.get_index<"byhash"_n>();
auto it = idx.find(order_hash);
if (it == idx.end()) return std::nullopt;
return *it;
}
inline order get_order_by_hash_or_fail(eosio::name coopname, const checksum256& order_hash,
const std::string& msg = "Заказ не найден по хэшу") {
auto o = get_order_by_hash(coopname, order_hash);
eosio::check(o.has_value(), msg);
return *o;
}
inline void update_order(eosio::name coopname, uint64_t order_id, const std::function<void(order&)>& fn) {
orders_index orders(_marketplace, coopname.value);
auto it = orders.find(order_id);
eosio::check(it != orders.end(), "Заказ не найден по id");
orders.modify(it, _marketplace, [&](auto& o) { fn(o); });
}
// ── Return requests ─────────────────────────────────────────────────────
inline std::optional<return_request> get_return_request_by_hash(eosio::name coopname,
const checksum256& request_hash) {
return_requests_index requests(_marketplace, coopname.value);
auto idx = requests.get_index<"byhash"_n>();
auto it = idx.find(request_hash);
if (it == idx.end()) return std::nullopt;
return *it;
}
inline return_request get_return_request_by_hash_or_fail(eosio::name coopname,
const checksum256& request_hash,
const std::string& msg = "Заявление на возврат не найдено по хэшу") {
auto r = get_return_request_by_hash(coopname, request_hash);
eosio::check(r.has_value(), msg);
return *r;
}
inline void update_return_request(eosio::name coopname, uint64_t request_id,
const std::function<void(return_request&)>& fn) {
return_requests_index requests(_marketplace, coopname.value);
auto it = requests.find(request_id);
eosio::check(it != requests.end(), "Заявление на возврат не найдено по id");
requests.modify(it, _marketplace, [&](auto& r) { fn(r); });
}
// ── Writeoff proposals ──────────────────────────────────────────────────
inline std::optional<writeoff_proposal> get_writeoff_proposal_by_hash(eosio::name coopname,
const checksum256& proposal_hash) {
writeoff_proposals_index proposals(_marketplace, coopname.value);
auto idx = proposals.get_index<"byhash"_n>();
auto it = idx.find(proposal_hash);
if (it == idx.end()) return std::nullopt;
return *it;
}
inline writeoff_proposal get_writeoff_proposal_by_hash_or_fail(eosio::name coopname,
const checksum256& proposal_hash,
const std::string& msg = "Проект списания не найден по хэшу") {
auto p = get_writeoff_proposal_by_hash(coopname, proposal_hash);
eosio::check(p.has_value(), msg);
return *p;
}
inline void update_writeoff_proposal(eosio::name coopname, uint64_t proposal_id,
const std::function<void(writeoff_proposal&)>& fn) {
writeoff_proposals_index proposals(_marketplace, coopname.value);
auto it = proposals.find(proposal_id);
eosio::check(it != proposals.end(), "Проект списания не найден по id");
proposals.modify(it, _marketplace, [&](auto& p) { fn(p); });
}
// ── Cross-contract read: ledger2 wallet/userwallet balances ─────────────
//
// Используется в createorder для guard'а Locked Decision L6 (без отрицательного
// баланса) и для решения о пропуске conditional операций o.wal.conv / o.mkt.assign
// (если на целевом кошельке уже хватает available — соответствующий шаг скипается).
//
// ВАЖНО: контракт marketplace не вызывает ledger2::walletop напрямую, а только
// читает state (RAM-таблицы wallets2 / userwallets через cross-contract scope).
// Все мутации идут через `Ledger2::apply` (см. lib/core/ledger2/ledger2.hpp).
struct UserWalletAvailable {
eosio::asset available = eosio::asset(0, _root_govern_symbol);
eosio::asset blocked = eosio::asset(0, _root_govern_symbol);
bool exists = false;
};
inline eosio::name get_valid_marketplace_action(const eosio::name &action) {
eosio::check(marketplace_callback_actions.contains(action), "Недопустимое имя действия marketplace");
return action;
}
static std::optional<request> get_request_by_hash(eosio::name coopname, checksum256 request_hash) {
requests_index requests(_marketplace, coopname.value);
auto idx = requests.get_index<"byhash"_n>();
auto req = idx.find(request_hash);
if (req != idx.end()) {
return *req;
inline UserWalletAvailable get_user_wallet_balance(eosio::name coopname,
eosio::name wallet_id,
eosio::name username) {
// userwallets_index — глобальный typedef в lib/domain/table_ledger2_userwallets.hpp.
// Для cross-contract read берём scope = coopname.value, code = _ledger2.
userwallets_index user_wallets(_ledger2, coopname.value);
auto idx = user_wallets.get_index<"byuserwallet"_n>();
auto it = idx.find(combine_ids(wallet_id.value, username.value));
if (it == idx.end()) {
return UserWalletAvailable{};
}
return std::nullopt;
return UserWalletAvailable{ it->available, it->blocked, true };
}
static std::optional<shipment> get_shipment_by_hash(eosio::name coopname, checksum256 shipment_hash) {
shipments_index shipments(_marketplace, coopname.value);
auto idx = shipments.get_index<"byhash"_n>();
auto ship = idx.find(shipment_hash);
if (ship != idx.end()) {
return *ship;
}
return std::nullopt;
}
static request get_request_by_hash_or_fail(eosio::name coopname, checksum256 request_hash,
const std::string &error_msg = "Заявка не найдена по хэшу") {
auto request_opt = get_request_by_hash(coopname, request_hash);
eosio::check(request_opt.has_value(), error_msg);
return *request_opt;
}
static shipment get_shipment_by_hash_or_fail(eosio::name coopname, checksum256 shipment_hash,
const std::string &error_msg = "Перевозка не найдена по хэшу") {
auto shipment_opt = get_shipment_by_hash(coopname, shipment_hash);
eosio::check(shipment_opt.has_value(), error_msg);
return *shipment_opt;
}
namespace DocumentNames {
static constexpr const name RETURN_STMT = "returnstmt"_n;
static constexpr const name CONVERT_FROM = "convertfrom"_n;
static constexpr const name CONVERT_TO = "convertto"_n;
static constexpr const name CONTRIB_STMT = "contribstmt"_n;
static constexpr const name CONTRIB_AUTH = "contribauth"_n;
static constexpr const name RETURN_AUTH = "returnauth"_n;
static constexpr const name RECEIVE_ACT = "receiveact"_n;
static constexpr const name RECEIVE_ACT_CONF = "receiveconf"_n;
static constexpr const name TRANSPORT1 = "transport1"_n;
static constexpr const name TRANSPORT2 = "transport2"_n;
static constexpr const name TRANSPORT3 = "transport3"_n;
static constexpr const name TRANSPORT4 = "transport4"_n;
static constexpr const name SUPPLY_ACT = "supplyact"_n;
static constexpr const name SUPPLY_ACT_CONF = "supplyconf"_n;
static constexpr const name SHIPMENT_ACT = "shipmentact"_n;
static constexpr const name DELIVERY_ACT = "deliveryact"_n;
static constexpr const name SHIPMENT_SEND_ACT = "shsendact"_n;
static constexpr const name SHIPMENT_LOADING_ACT = "shloadact"_n;
static constexpr const name SHIPMENT_ARRIVE_ACT = "sharriveact"_n;
static constexpr const name SHIPMENT_RECV_ACT = "shrecvact"_n;
static constexpr const name WDISPUTE = "wdispute"_n;
static constexpr const name WRETURN_AUTH = "wreturnauth"_n;
static constexpr const name WSUPPLY_AUTH = "wsupplyauth"_n;
static constexpr const name WRETURN_ACT = "wreturnact"_n;
static constexpr const name WOFFER_ACT = "wofferact"_n;
static constexpr const name WACCEPT_ACT = "wacceptact"_n;
} // namespace DocumentNames
} // namespace Marketplace
@@ -0,0 +1,102 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include "../../core/utils.hpp"
/**
* @file memo.hpp
* @brief Человекочитаемые memo для marketplace ledger2-операций.
*
* Аналог `Capital::Memo` (см. `cpp/capital/domain/entities/memo.hpp`).
* Текст memo попадает в журнал ledger2 (поле `journal.memo`) и в выписки
* пайщика / отчёт бухгалтеру — это пользовательский слой, поэтому никаких
* технических токенов ("p.mkt.supply", "L6", op_code) здесь быть не должно.
*
* Параметризуется значимыми идентификаторами (id Order'а) — для трассировки
* в выписке без необходимости резолвить hash через backend.
*/
namespace Marketplace::Memo {
// ---------------------------------------------------------------- p.mkt.supply
inline std::string get_create_order_block_memo(uint64_t order_id) {
return "Резерв средств под заказ имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_create_order_assign_memo(uint64_t order_id) {
return "Целевое назначение взноса под заказ имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_create_order_convert_memo(uint64_t order_id) {
return "Конвертация паевого взноса в членский для заказа имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_cancel_order_memo(uint64_t order_id) {
return "Возврат резерва по отменённому заказу имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_decline_order_memo(uint64_t order_id) {
return "Возврат резерва по отклонённому поставщиком заказу имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_expire_order_memo(uint64_t order_id) {
return "Возврат резерва по неисполненному в срок заказу имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_purchase_from_supplier_memo(uint64_t order_id) {
return "Приёмка имущества от поставщика по заказу № " + std::to_string(order_id) + " на склад кооператива";
}
inline std::string get_pay_supplier_memo(uint64_t order_id) {
return "Оплата поставщику имущества по заказу № " + std::to_string(order_id);
}
inline std::string get_signiss2_correction_less_memo(uint64_t order_id) {
return "Возврат разницы пайщику: фактически выдано меньше заказанного по заказу имущества № " + std::to_string(order_id);
}
inline std::string get_signiss2_correction_more_convert_memo(uint64_t order_id) {
return "Конвертация паевого взноса в членский на доплату по заказу имущества № " + std::to_string(order_id) + " (фактически выдано больше заказанного)";
}
inline std::string get_signiss2_correction_more_assign_memo(uint64_t order_id) {
return "Целевое назначение взноса на доплату по заказу имущества № " + std::to_string(order_id) + " (фактически выдано больше заказанного)";
}
inline std::string get_signiss2_correction_more_block_memo(uint64_t order_id) {
return "Резерв на доплату по заказу имущества № " + std::to_string(order_id) + " (фактически выдано больше заказанного)";
}
inline std::string get_consume_by_member_memo(uint64_t order_id) {
return "Выдача имущества пайщику по заказу № " + std::to_string(order_id) + ": выбытие со склада";
}
inline std::string get_consume_transit_close_memo(uint64_t order_id) {
return "Выдача имущества пайщику по заказу № " + std::to_string(order_id) + ": списание целевого назначения членского взноса";
}
// ---------------------------------------------------------------- p.mkt.return
inline std::string get_return_by_member_memo(uint64_t return_request_id, uint64_t order_id) {
return "Гарантийный возврат имущества пайщиком по заявлению № " + std::to_string(return_request_id) + " (исходный заказ № " + std::to_string(order_id) + "): восстановление членского взноса";
}
inline std::string get_return_transit_close_memo(uint64_t return_request_id, uint64_t order_id) {
return "Гарантийный возврат имущества пайщиком по заявлению № " + std::to_string(return_request_id) + " (исходный заказ № " + std::to_string(order_id) + "): возврат имущества на склад";
}
// ---------------------------------------------------------------- p.mkt.wroff
inline std::string get_writeoff_memo(uint64_t proposal_id, uint64_t item_index) {
return "Утилизация скоропорта по решению совета № " + std::to_string(proposal_id) + ", позиция " + std::to_string(item_index + 1) + ": выбытие со склада";
}
inline std::string get_writeoff_transit_close_memo(uint64_t proposal_id, uint64_t item_index) {
return "Утилизация скоропорта по решению совета № " + std::to_string(proposal_id) + ", позиция " + std::to_string(item_index + 1) + ": списание целевого назначения";
}
} // namespace Marketplace::Memo
@@ -124,5 +124,6 @@ namespace Names {
constexpr eosio::name CREATE_WITHDRAW_2 = "createwthd2"_n; // акцепт возврата из проекта
constexpr eosio::name CREATE_WITHDRAW_3 = "createwthd3"_n; // акцепт возврата из программы
constexpr eosio::name CREATE_RESULT = "createresult"_n; // акцепт результата
constexpr eosio::name CONVERT_SEGMENT = "convertsegm"_n; // финальная фаза p.cap.rid — трансляция паевого взноса
}
}
@@ -65,9 +65,14 @@
#include "table_ledger2_meta.hpp"
#include "table_loan_debts.hpp"
#include "table_loan_summaries.hpp"
#include "table_marketplace_requests.hpp"
#include "table_marketplace_segments.hpp"
#include "table_marketplace_shipments.hpp"
// marketplace (Story 11.1, canonical) — anchor-таблицы трёх процессов
// p.mkt.supply / p.mkt.return / p.mkt.wroff. Donor-таблицы (requests/segments/
// shipments) удалены вместе с donor-actions (AR30). Batch (consolidated request)
// — backend-only, on-chain не хранится (Locked Decision L10).
#include "table_marketplace_orders.hpp"
#include "table_marketplace_return_requests.hpp"
#include "table_marketplace_writeoff_proposals.hpp"
// apps (каталог приложений)
#include "table_apps_packages.hpp"
@@ -0,0 +1,189 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include "../consts.hpp"
#include "../core/document.hpp"
#include "../core/utils.hpp"
namespace Marketplace {
using namespace eosio;
/**
* @brief Статусы Order'а в процессе p.mkt.supply.
*
* Граф: ∅ → active → cancelled (canceled by orderer | expireorder | declineorder)
* → accepted → supply_prepared → accepted_to_coop
* → ready_to_receive → received
*
* Источник правды — `p.mkt.supply.standard.yaml` секция `states:`.
*
* Промежуточный статус `ship_ready` (после prepship поставщика) удалён:
* после `acceptorder` поставщик автоматически считается обязанным доставить
* партию — двойного подтверждения «принял заявку» + «готов отгружать» не
* требуется (отдельная подпись «готов отгрузить» лишь добавляет шум в UX).
* Переход accepted → supply_prepared идёт сразу через signsupp.
*/
namespace OrderStatus {
inline constexpr eosio::name ACTIVE = "active"_n;
inline constexpr eosio::name CANCELLED = "cancelled"_n;
inline constexpr eosio::name ACCEPTED = "accepted"_n;
inline constexpr eosio::name SUPPLY_PREPARED = "supplyprep"_n;
inline constexpr eosio::name ACCEPTED_TO_COOP = "acceptcoop"_n;
inline constexpr eosio::name READY_TO_RECEIVE = "readyrecv"_n;
inline constexpr eosio::name RECEIVED = "received"_n;
}
/**
* @brief Состояние выплаты поставщику по Order'у (Locked Decision L12, E11
* техдолг 598-16). Выплата идёт через gateway::createoutpay → действие
* кассира → callback `marketplace::payconfirm` / `marketplace::paydecline`.
*
* Допустимые переходы:
* none → pending — `marketplace::payout` отправил inline в gateway.
* pending → completed — gateway::outcomplete → callback `payconfirm`.
* Здесь применяется o.mkt.payout (Дт 86 / Кт 51).
* pending → declined — gateway::outdecline → callback `paydecline`.
* Без ledger-движения; обязательство Кт 86 остаётся.
* declined → pending — повторная попытка `marketplace::payout` после
* исправления реквизитов кассиром.
*/
namespace OrderPayoutStatus {
inline constexpr eosio::name NONE = "none"_n;
inline constexpr eosio::name PENDING = "pending"_n;
inline constexpr eosio::name COMPLETED = "completed"_n;
inline constexpr eosio::name DECLINED = "declined"_n;
}
/**
* @brief Тип цикла отсечки заявок поставщика (атрибут Offer'а — Locked Decision L11).
*
* Сохраняется на Order'е, потому что фактический cycle_type фиксируется в момент
* createorder и не меняется при последующем редактировании Offer'а поставщиком.
*/
namespace CycleType {
inline constexpr eosio::name TIME_BASED = "timebased"_n;
inline constexpr eosio::name VOLUME_BASED = "volumebased"_n;
inline constexpr eosio::name OPEN_SUBSCRIPT = "opensubscr"_n;
inline constexpr eosio::name INDIVIDUAL = "individual"_n;
}
/**
* @brief On-chain Order — анкер процесса p.mkt.supply.
*
* scope = coopname; primary_key = id; уникальность через `byhash` индекс на
* `order.hash` — этот hash используется как `process_hash` во всех ledger2-
* операциях процесса (BLOCK/UNBLOCK/PURCH/PAYOUT/CONSUM/CONSUM2/RETURN).
*
* Привязка к кооперативным участкам (КУ) идёт через `braname` (см. контракт
* `branch`, таблица `branches`). Председатель / trustee / доверенные лица
* каждого КУ известны контракту `branch` — авторизация подписей актов
* выполняется через `Branch::is_user_authorized(coopname, braname, signer)`,
* а не по сохранённому имени председателя (председатель может делегировать
* подпись доверенному лицу из `coobranch.trusted[]`, состав которого может
* меняться независимо от Order'а).
*
* Точки контракта:
* - `delivery_braname` — КУ выдачи имущества пайщику; задаётся пайщиком на
* createorder и неизменна. Источник проверки signiss1/signiss2/p.mkt.return.
* - `accept_braname` — КУ приёмки от поставщика; заполняется на signsupp
* как параметр action'а (поставщик указывает, в какой КУ сдаёт партию).
* Источник проверки signchair.
* - `current_warehouse_braname` — текущая точка хранения имущества по этому
* Order'у. Заполняется на signchair (= `accept_braname`, имущество на
* приёмном складе) и обновляется на signiss1 (= `delivery_braname`, готово
* к выдаче — фиксирует факт логистической передачи). Бездокументарно —
* промежуточные перемещения по заготовочным КУ контрактом не подписываются;
* точка хранения переходит «скачком» в момент готовности к выдаче.
*
* История внутренних передач между КУ (заготовочный → точка выдачи и т.п.)
* с подписью ТТН — отложена. Backend может реконструировать движение из
* blockchain_actions если потребуется. Поле введено заранее, чтобы не
* добавлять его потом через binary_extension.
*
* `acceptance_act` (АПП приёмки) и `issue_act` (АПП выдачи) хранятся
* полным document2 — это дублирование в случае batch-поставки (один
* физический акт → копия в каждом order'е batch'а), но это допустимо для
* on-chain (минимум данных + hash) и упрощает аудит.
*
* `batch_hash` — opaque ссылка на off-chain consolidated request (Locked
* Decision L10: batch — backend-only сущность). Контракт не валидирует
* существование batch'а, только хранит ссылку для трассировки и группировки
* Order'ов в UI. Все per-batch операции на on-chain делаются per-Order
* (backend проходит циклом по orders батча) — векторов order'ов в action'ах нет.
*
* `actual_quantity` / `fact_cost` заполняются на signiss2 (Story 6.2/6.3).
* До signiss2 равны соответственно `quantity` / `total_cost`.
*
* `warranty_until` — рассчитывается в signiss2 как `now() + warranty_period_secs`
* (period приходит с Offer'а через backend; в `submretrn` валидируется только это поле).
*/
struct [[eosio::table, eosio::contract(MARKETPLACE)]] order {
uint64_t id; ///< внутренний ID
checksum256 hash; ///< process_hash для p.mkt.supply
eosio::name coopname; ///< scope-валидация
eosio::name orderer; ///< пайщик-заказчик
eosio::name offerer; ///< пайщик-поставщик из Offer'а (для acceptorder/declineorder/signsupp guard'а)
checksum256 offer_hash; ///< ссылка на Offer (off-chain в backend)
eosio::name delivery_braname; ///< КУ выдачи (выбран пайщиком на createorder); проверка signiss1/signiss2/p.mkt.return через Branch::is_user_authorized
eosio::name accept_braname; ///< КУ приёмки от поставщика (заполняется на signsupp); проверка signchair через Branch::is_user_authorized
eosio::name current_warehouse_braname; ///< текущая точка хранения; signchair: = accept_braname; signiss1: = delivery_braname (фиксация готовности к выдаче)
uint64_t quantity = 0; ///< заказанное количество
uint64_t actual_quantity = 0; ///< фактически выданное (signiss2); до signiss2 == quantity
eosio::asset unit_price = asset(0, _root_govern_symbol); ///< цена за единицу
eosio::asset total_cost = asset(0, _root_govern_symbol); ///< quantity * unit_price (заблокированная сумма)
eosio::asset fact_cost = asset(0, _root_govern_symbol); ///< actual_quantity * unit_price (после signiss2)
eosio::name cycle_type = CycleType::TIME_BASED; ///< снимок cycle_type Offer'а на момент createorder
uint32_t warranty_period_secs = 0; ///< из Offer'а — для submretrn гард'а
time_point_sec warranty_until = time_point_sec(0); ///< now() + warranty_period_secs (заполняется в signiss2)
eosio::name status = OrderStatus::ACTIVE; ///< canonical статус
checksum256 batch_hash; ///< opaque ссылка на consolidated request (off-chain)
document2 acceptance_act_signsupp; ///< АПП приёмки — первая подпись поставщика (signsupp)
document2 acceptance_act_signchair; ///< АПП приёмки — финальная подпись председателя приёмного КУ (signchair)
document2 issue_act_signiss1; ///< АПП выдачи — первая подпись председателя КУ выдачи (signiss1)
document2 issue_act_signiss2; ///< АПП выдачи — финальная подпись заказчика (signiss2)
eosio::name payout_status = OrderPayoutStatus::NONE; ///< Locked Decision L12 — состояние выплаты поставщику через gateway (см. namespace OrderPayoutStatus)
std::string payout_decline_reason; ///< Заполняется только при payout_status == DECLINED (текст причины из gateway::outdecline)
uint64_t return_request_id = 0; ///< 0 если активного гарантийного возврата нет
// Все timestamp'ы переходов состояний (createorder/accepted/received_to_coop/
// ready/received/cancelled) восстанавливаются на бэкенде из blockchain_actions[at]
// по соответствующим action'ам — нет смысла держать их в RAM-таблице.
// Единственное исключение — warranty_until (выше): нужен on-chain для
// submretrn guard `now() < warranty_until` без cross-action lookup.
uint64_t primary_key() const { return id; }
checksum256 by_hash() const { return hash; }
uint64_t by_orderer() const { return orderer.value; }
uint64_t by_offerer() const { return offerer.value; }
uint64_t by_status() const { return status.value; }
checksum256 by_batch() const { return batch_hash; }
checksum256 by_offer() const { return offer_hash; }
uint64_t by_delivery_bra() const { return delivery_braname.value; }
uint64_t by_accept_bra() const { return accept_braname.value; }
};
typedef eosio::multi_index<
"orders"_n, order,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<order, checksum256, &order::by_hash>>,
eosio::indexed_by<"byorderer"_n, eosio::const_mem_fun<order, uint64_t, &order::by_orderer>>,
eosio::indexed_by<"byofferer"_n, eosio::const_mem_fun<order, uint64_t, &order::by_offerer>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<order, uint64_t, &order::by_status>>,
eosio::indexed_by<"bybatch"_n, eosio::const_mem_fun<order, checksum256, &order::by_batch>>,
eosio::indexed_by<"byoffer"_n, eosio::const_mem_fun<order, checksum256, &order::by_offer>>,
eosio::indexed_by<"bydelivbra"_n, eosio::const_mem_fun<order, uint64_t, &order::by_delivery_bra>>,
eosio::indexed_by<"byacceptbra"_n, eosio::const_mem_fun<order, uint64_t, &order::by_accept_bra>>>
orders_index;
} // namespace Marketplace
@@ -1,109 +0,0 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include <vector>
#include "../consts.hpp"
#include "../core/document.hpp"
#include "../core/utils.hpp"
#include "document_core.hpp"
namespace Marketplace {
using namespace eosio;
struct [[eosio::table, eosio::contract(MARKETPLACE)]] request {
uint64_t id;
checksum256 hash;
name coopname;
name type;
name status;
name username;
name braname;
name warehouse;
name token_contract;
name receiver_braname;
name supplier_braname;
asset unit_cost;
asset base_cost;
asset membership_fee_amount;
asset total_cost;
uint64_t units;
std::string meta;
name money_contributor;
name product_contributor;
std::vector<Document::named_document> documents;
uint64_t product_lifecycle_secs;
uint64_t warranty_period_secs;
asset cancellation_fee_amount;
time_point_sec warranty_delay_until;
time_point_sec deadline_for_receipt;
bool is_warranty_return = false;
uint64_t warranty_return_id;
time_point_sec created_at;
time_point_sec accepted_at;
time_point_sec supplied_at;
time_point_sec delivered_at;
time_point_sec received_at;
time_point_sec completed_at;
time_point_sec declined_at;
time_point_sec disputed_at;
time_point_sec canceled_at;
uint64_t primary_key() const { return id; }
uint64_t by_coop() const { return coopname.value; }
uint64_t by_status() const { return status.value; }
uint64_t by_type() const { return type.value; }
checksum256 by_hash() const { return hash; }
uint64_t by_username() const { return username.value; }
uint64_t by_created() const { return created_at.sec_since_epoch(); }
uint64_t by_completed() const { return completed_at.sec_since_epoch(); }
uint64_t by_declined() const { return declined_at.sec_since_epoch(); }
uint64_t by_canceled() const { return canceled_at.sec_since_epoch(); }
uint64_t by_warranty_id() const { return warranty_return_id; }
name get_money_contributor() const {
return is_warranty_return ? product_contributor : money_contributor;
}
name get_product_contributor() const {
return is_warranty_return ? money_contributor : product_contributor;
}
name get_payer() const { return get_money_contributor(); }
name get_supplier() const { return get_product_contributor(); }
name get_product_backer() const { return money_contributor; }
name get_defective_supplier() const { return product_contributor; }
};
typedef eosio::multi_index<
"requests"_n, request,
eosio::indexed_by<"bycoop"_n, eosio::const_mem_fun<request, uint64_t, &request::by_coop>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<request, uint64_t, &request::by_status>>,
eosio::indexed_by<"bytype"_n, eosio::const_mem_fun<request, uint64_t, &request::by_type>>,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<request, checksum256, &request::by_hash>>,
eosio::indexed_by<"byusername"_n, eosio::const_mem_fun<request, uint64_t, &request::by_username>>,
eosio::indexed_by<"bycreated"_n, eosio::const_mem_fun<request, uint64_t, &request::by_created>>,
eosio::indexed_by<"bycompleted"_n, eosio::const_mem_fun<request, uint64_t, &request::by_completed>>,
eosio::indexed_by<"bydeclined"_n, eosio::const_mem_fun<request, uint64_t, &request::by_declined>>,
eosio::indexed_by<"bycanceled"_n, eosio::const_mem_fun<request, uint64_t, &request::by_canceled>>,
eosio::indexed_by<"bywarrantyid"_n, eosio::const_mem_fun<request, uint64_t, &request::by_warranty_id>>>
requests_index;
} // namespace Marketplace
@@ -0,0 +1,103 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include <vector>
#include "../consts.hpp"
#include "../core/document.hpp"
#include "../core/utils.hpp"
namespace Marketplace {
using namespace eosio;
/**
* @brief Статусы заявления на гарантийный возврат (процесс p.mkt.return).
*
* Граф: ∅ → pending_review → approved_for_visit → return_accepted (final)
* → rejected_at_ku (final)
* → rejected_remote (final)
*
* Источник правды — `p.mkt.return.standard.yaml` секция `states:`.
*/
namespace ReturnStatus {
inline constexpr eosio::name PENDING_REVIEW = "pendrev"_n;
inline constexpr eosio::name APPROVED_FOR_VISIT = "approvvisit"_n;
inline constexpr eosio::name RETURN_ACCEPTED = "accepted"_n;
inline constexpr eosio::name REJECTED_REMOTE = "rejremote"_n;
inline constexpr eosio::name REJECTED_AT_KU = "rejatku"_n;
}
/**
* @brief On-chain Заявление на гарантийный возврат — анкер процесса p.mkt.return.
*
* scope = coopname; primary_key = id; уникальность через `byhash` индекс на
* `return_request.hash` — этот hash используется как `process_hash` во всех
* ledger2-операциях процесса (RETURN + RETURN2).
*
* Привязка к КУ не сохраняется — она может измениться от шага к шагу
* (председатель delivery-КУ может рассмотреть удалённо, а очный осмотр сделать
* на любом другом КУ; состав доверенных лиц коробки `branches` тоже может
* меняться). На каждом действии (aprretrem/rejretrem/accretrn/rejretrn) braname
* приходит параметром action'а и валидируется через
* `Branch::is_user_authorized(coopname, braname, signer)`. Контракт хранит
* только неизменные участники процесса: orderer / offerer (через original Order)
* и coopname.
*
* Связь с исходным Order'ом — `original_order_id` + `original_order_hash`;
* Order.return_request_id ставится в submretrn для двусторонней связи.
*
* `photos` — vector<checksum256> хешей файлов в bucket'е stol-zakazov:images
* (Story 7.1, AR32). Реальные изображения off-chain в file-storage (PR #359);
* on-chain — только ссылки (hash для дедупликации + URL восстанавливает backend).
*
* `decision_remote` / `decision_visit` — декларативные документы решений
* председателя (rejretrem / accretrn / rejretrn). reason_remote / reason_visit
* — текстовые причины отказа для пользовательского UI (заполняются в
* rejretrem / rejretrn соответственно).
*/
struct [[eosio::table, eosio::contract(MARKETPLACE)]] return_request {
uint64_t id;
checksum256 hash; ///< process_hash для p.mkt.return
eosio::name coopname;
eosio::name orderer; ///< пайщик-заказчик (заявитель)
uint64_t original_order_id; ///< внутренний id Order'а
checksum256 original_order_hash; ///< process_hash оригинального p.mkt.supply
checksum256 original_consume_op_id; ///< ссылка на оригинальный o.mkt.consum (для journal трассировки compensating forward; см. d6 A4)
uint64_t actual_quantity = 0; ///< возвращаемое количество (по умолчанию = order.actual_quantity, может быть меньше)
eosio::asset fact_cost = asset(0, _root_govern_symbol); ///< возвращаемая сумма (actual_quantity * unit_price)
std::string reason_text; ///< причина обращения (≤ 500 символов)
std::vector<checksum256> photos; ///< хеши файлов в bucket'е stol-zakazov:images
eosio::name status = ReturnStatus::PENDING_REVIEW;
document2 statement; ///< заявление пайщика (опционально подписанное)
document2 decision_remote; ///< решение председателя удалённо (aprretrem | rejretrem)
document2 decision_visit; ///< решение председателя по итогам очного осмотра (accretrn | rejretrn)
std::string reason_remote; ///< причина отказа удалённо (для rejretrem)
std::string reason_visit; ///< причина отказа на очном осмотре (для rejretrn)
// Timestamp'ы submretrn/aprretrem/rejretrem/accretrn/rejretrn — на бэкенде
// из blockchain_actions[at]. В контракте никаких guard'ов по датам нет.
uint64_t primary_key() const { return id; }
checksum256 by_hash() const { return hash; }
uint64_t by_orderer() const { return orderer.value; }
uint64_t by_status() const { return status.value; }
uint64_t by_original_order() const { return original_order_id; }
};
typedef eosio::multi_index<
"retrequests"_n, return_request,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<return_request, checksum256, &return_request::by_hash>>,
eosio::indexed_by<"byorderer"_n, eosio::const_mem_fun<return_request, uint64_t, &return_request::by_orderer>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<return_request, uint64_t, &return_request::by_status>>,
eosio::indexed_by<"byorigorder"_n, eosio::const_mem_fun<return_request, uint64_t, &return_request::by_original_order>>>
return_requests_index;
} // namespace Marketplace
@@ -1,52 +0,0 @@
#pragma once
#include <eosio/eosio.hpp>
#include "../consts.hpp"
#include "document_core.hpp"
namespace Marketplace {
using namespace eosio;
struct [[eosio::table, eosio::contract(MARKETPLACE)]] segment {
uint64_t id;
uint64_t request_id;
name type;
name status;
document2 convert_in;
document2 statement;
uint64_t decision_id;
document2 authorization;
document2 act1;
document2 act2;
document2 convert_out;
document2 transport_act_1;
document2 transport_act_2;
document2 transport_act_3;
document2 transport_act_4;
name coopactor;
name username;
name driver_username;
name receive_from_driver_coopactor;
time_point_sec created_at;
time_point_sec updated_at;
uint64_t primary_key() const { return id; }
uint64_t by_request() const { return request_id; }
uint64_t by_type() const { return type.value; }
uint64_t by_status() const { return status.value; }
};
typedef eosio::multi_index<
"segments"_n, segment,
eosio::indexed_by<"byrequest"_n, eosio::const_mem_fun<segment, uint64_t, &segment::by_request>>,
eosio::indexed_by<"bytype"_n, eosio::const_mem_fun<segment, uint64_t, &segment::by_type>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<segment, uint64_t, &segment::by_status>>>
segments_index;
} // namespace Marketplace
@@ -1,55 +0,0 @@
#pragma once
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <vector>
#include "../consts.hpp"
#include "../core/document.hpp"
#include "../core/utils.hpp"
#include "document_core.hpp"
namespace Marketplace {
using namespace eosio;
struct [[eosio::table, eosio::contract(MARKETPLACE)]] shipment {
uint64_t id;
checksum256 hash;
name coopname;
name driver_username;
name source_braname;
name destination_braname;
name status;
std::vector<checksum256> request_hashes;
std::vector<Document::named_document> documents;
time_point_sec created_at;
time_point_sec loaded_at;
time_point_sec delivered_at;
time_point_sec completed_at;
uint64_t primary_key() const { return id; }
checksum256 by_hash() const { return hash; }
uint64_t by_coop() const { return coopname.value; }
uint64_t by_driver() const { return driver_username.value; }
uint64_t by_source() const { return source_braname.value; }
uint64_t by_destination() const { return destination_braname.value; }
uint64_t by_status() const { return status.value; }
uint64_t by_created() const { return created_at.sec_since_epoch(); }
};
typedef eosio::multi_index<
"shipments"_n, shipment,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<shipment, checksum256, &shipment::by_hash>>,
eosio::indexed_by<"bycoop"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_coop>>,
eosio::indexed_by<"bydriver"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_driver>>,
eosio::indexed_by<"bysource"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_source>>,
eosio::indexed_by<"bydest"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_destination>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_status>>,
eosio::indexed_by<"bycreated"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_created>>>
shipments_index;
} // namespace Marketplace
@@ -0,0 +1,121 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include <vector>
#include "../consts.hpp"
#include "../core/document.hpp"
#include "../core/utils.hpp"
namespace Marketplace {
using namespace eosio;
/**
* @brief Статусы проекта решения совета о списании скоропорта (процесс p.mkt.wroff).
*
* Граф (выровнен под канонический паттерн «решение совета»
* `soviet::createagenda` + callback'и):
*
* ∅
* ├─ propwroff (admin) ─────────────► proposed
* │ └─ soviet::createagenda(type=mktwroff, callback=onmktwoauth/onmktwodecl)
* ├─ onmktwoauth (callback от soviet) ► authorized (хранит protocol2)
* │ └─ execwroff per-item (backend цикл) ► executed (final)
* └─ onmktwodecl (callback от soviet) ► rejected (final, без ledger2-операций)
*
* Источник правды — `p.mkt.wroff.standard.yaml` секция `states:`.
*/
namespace WroffStatus {
// on-chain имена 1:1 совпадают с YAML; PROPOSED/AUTHORIZED/EXECUTED/REJECTED —
// C++-константы (имя PROPOSED вместо DRAFT — чтобы не конфликтовать с макросом
// DRAFT из lib/consts.hpp).
inline constexpr eosio::name PROPOSED = "proposed"_n;
inline constexpr eosio::name AUTHORIZED = "authorized"_n;
inline constexpr eosio::name EXECUTED = "executed"_n;
inline constexpr eosio::name REJECTED = "rejected"_n;
}
/**
* @brief Позиция к списанию в составе writeoff_proposal.
*
* Может ссылаться:
* - на конкретный Order (если позиция «не выдана первичному заказчику») —
* `source_order_id` != 0;
* - на излишек в результате signiss2 fact > ordered + остаток на складе —
* `source_order_id` == 0, аналитика только через `braname` + `meta`;
* - на возвращённое имущество из p.mkt.return — `source_order_id` ссылка
* через original Order, через который имущество физически вернулось.
*
* `braname` — кооперативный участок (склад) источник списания. Пер-КУ
* аналитика счёта 10. Подпись протокола проверяется через
* `Branch::is_user_authorized(coopname, braname, signer)` — председатель
* соответствующего КУ может делегировать подпись доверенному лицу.
*
* `amount` — сумма к списанию по этой позиции (Дт 91 / Кт 10 в части 1
* и Дт 86 / Кт 91 в части 2 — обе в одной транзакции execwroff).
*
* `meta` — произвольная строка для UI / отчёта (название позиции, причина);
* не валидируется контрактом.
*
* `executed` — true после успешного списания этой позиции (см. execwroff
* per-item action). Когда все items в proposal.items имеют executed=true,
* статус proposal автоматически переходит в EXECUTED.
*/
struct wroff_item {
uint64_t source_order_id = 0; ///< 0 если списание из складского остатка без привязки к order'у
eosio::name braname; ///< КУ-склад источник списания
eosio::asset amount = asset(0, _root_govern_symbol);
std::string meta;
bool executed = false; ///< true после execwroff(proposal_hash, item_index)
};
/**
* @brief On-chain Проект решения совета о списании скоропорта — анкер процесса p.mkt.wroff.
*
* scope = coopname; primary_key = id; уникальность через `byhash` индекс на
* `proposal.hash` — этот hash используется как `process_hash` во всех
* ledger2-операциях процесса (WROFF + WROFF2 — по паре per-item).
*
* `items` — vector<wroff_item> позиций к списанию; на execwroff контракт
* последовательно вызывает `Ledger2::apply(o.mkt.wroff, item.amount, …)` +
* `Ledger2::apply(o.mkt.wroff2, item.amount, …)` для каждой позиции в
* одной транзакции Antelope.
*
* `protocol` — document2 решения совета (signed_by: council_members).
* Подпись — через стандартный sov.decision-протокол (см. p.mkt.wroff.standard.yaml
* секция documents).
*/
struct [[eosio::table, eosio::contract(MARKETPLACE)]] writeoff_proposal {
uint64_t id;
checksum256 hash; ///< process_hash для p.mkt.wroff
eosio::name coopname;
eosio::name proposed_by; ///< backend / админ — инициатор propwroff
eosio::name decided_by; ///< actor execwroff/declwroff (председатель / совет)
std::vector<wroff_item> items;
eosio::asset total_amount = asset(0, _root_govern_symbol); ///< Σ items.amount (для UI / отчёта)
eosio::name status = WroffStatus::PROPOSED;
document2 protocol; ///< Подписанный советом протокол решения; кладётся в callback onmktwoauth/onmktwodecl
std::string reject_reason; ///< Причина отклонения (можно достать из meta protocol в onmktwodecl)
// Связка с soviet.decisions — через decisions.hash == proposal.hash; backend стыкует обе таблицы по hash.
// Timestamp'ы propwroff / onmktwoauth / onmktwodecl / execwroff фиксируются
// backend'ом из блокчейн-дельт (поле blockchain_actions[at]).
uint64_t primary_key() const { return id; }
checksum256 by_hash() const { return hash; }
uint64_t by_status() const { return status.value; }
};
typedef eosio::multi_index<
"wroffprops"_n, writeoff_proposal,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<writeoff_proposal, checksum256, &writeoff_proposal::by_hash>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<writeoff_proposal, uint64_t, &writeoff_proposal::by_status>>>
writeoff_proposals_index;
} // namespace Marketplace
+36 -30
View File
@@ -1,37 +1,43 @@
#include "marketplace.hpp"
#include <eosio/transaction.hpp>
// Процесс поставки по заявкам orderoffer
#include "src/deliver_on_offer/orderoffer.cpp"
#include "src/deliver_on_offer/accept.cpp"
#include "src/deliver_on_offer/authcontrib.cpp"
#include "src/deliver_on_offer/authreturn.cpp"
#include "src/deliver_on_offer/declineacc.cpp"
#include "src/deliver_on_offer/supply.cpp"
#include "src/deliver_on_offer/supplcnf.cpp"
// Раскладка по процессам соответствует YAML-стандартам рядом с этим файлом
// (p.mkt.supply.standard.yaml / p.mkt.return.standard.yaml /
// p.mkt.wroff.standard.yaml). Имена подпапок 1:1 совпадают с process_type
// — связь от файла → к стандарту прозрачная.
#include "src/deliver_on_offer/delivered.cpp"
#include "src/deliver_on_offer/receive.cpp"
#include "src/deliver_on_offer/receivecnf.cpp"
#include "src/deliver_on_offer/complete.cpp"
#include "src/deliver_on_offer/decline.cpp"
#include "src/deliver_on_offer/cancel.cpp"
// ── p.mkt.supply (9 actions) ─── Stories Эпиков 4-5-6 ──────────────────
#include "src/p.mkt.supply/createorder.cpp"
#include "src/p.mkt.supply/cancelorder.cpp"
#include "src/p.mkt.supply/expireorder.cpp"
#include "src/p.mkt.supply/acceptorder.cpp"
#include "src/p.mkt.supply/declineorder.cpp"
#include "src/p.mkt.supply/signsupp.cpp"
#include "src/p.mkt.supply/signchair.cpp"
#include "src/p.mkt.supply/payout.cpp"
#include "src/p.mkt.supply/payconfirm.cpp"
#include "src/p.mkt.supply/paydecline.cpp"
#include "src/p.mkt.supply/signiss1.cpp"
#include "src/p.mkt.supply/signiss2.cpp"
// Новая система перевозок
#include "src/shipment/createship.cpp"
#include "src/shipment/signbydriver.cpp"
#include "src/shipment/arrived.cpp"
#include "src/shipment/receiveshipm.cpp"
#include "src/shipment/retransport.cpp"
// ── p.mkt.return (5 actions) ──── Stories Эпика 7 ──────────────────────
#include "src/p.mkt.return/submretrn.cpp"
#include "src/p.mkt.return/aprretrem.cpp"
#include "src/p.mkt.return/rejretrem.cpp"
#include "src/p.mkt.return/accretrn.cpp"
#include "src/p.mkt.return/rejretrn.cpp"
// Диспуты
#include "src/dispute_on_offer/dispute.cpp"
#include "src/dispute_on_offer/wauthorize.cpp"
#include "src/dispute_on_offer/wreturn.cpp"
#include "src/dispute_on_offer/woffer.cpp"
#include "src/dispute_on_offer/waccept.cpp"
// ── p.mkt.wroff (4 actions) ───── Stories Эпика 8 ──────────────────────
// Канонический паттерн «решение совета»: propwroff (admin) → soviet::createagenda
// → onmktwoauth / onmktwodecl (callback от soviet после голосования) → execwroff
// per-item (backend цикл).
#include "src/p.mkt.wroff/propwroff.cpp"
#include "src/p.mkt.wroff/onmktwoauth.cpp"
#include "src/p.mkt.wroff/onmktwodecl.cpp"
#include "src/p.mkt.wroff/execwroff.cpp"
[[eosio::action]] void marketplace::migrate(){
// require_auth(_marketplace);
[[eosio::action]] void marketplace::migrate() {
// Donor-таблиц нет (AR30 — donor-actions удалены вместе с requests/segments/
// shipments). Заглушка остаётся для совместимости с прежним ABI; вызывать
// не имеет эффекта.
require_auth(_marketplace);
}
+343 -71
View File
@@ -1,3 +1,5 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/contract.hpp>
#include <eosio/crypto.hpp>
@@ -6,40 +8,68 @@
#include <eosio/system.hpp>
#include <eosio/time.hpp>
#include "../lib/index.hpp"
#include <string>
#include <vector>
#include "../lib/index.hpp"
#include "../lib/core/marketplace/marketplace.hpp"
#include "../lib/core/marketplace/memo.hpp"
#include "../lib/core/branch/branch.hpp"
#include "../lib/core/ledger2/ledger2.hpp"
using namespace eosio;
using namespace Marketplace;
/**
* \ingroup public_contracts
* @brief Класс `marketplace` предоставляет функционал кооперативного маркетплейса, позволяя пользователям
* создавать, обновлять, принимать и отменять заявки на обмен товаров и услуг. Этот контракт служит
* центральной точкой для всех операций обмена в рамках кооперативной экосистемы.
* \ingroup public_contracts
*
* Основные функции класса:
* - Создание и управление заявками типа orderoffer (заказчик → поставщик).
* - Операции обновления, принятия, отказа и завершения обменных операций.
* - Модерация и управление публикацией заявок на обмен.
* - Административные функции, такие как создание идентификаторов и авторизация операций.
*
* ## Процесс поставки orderoffer:
*
* 1. **orderoffer** - заказчик создает заявку с документами на возврат и конвертацию, средства блокируются
* 2. **accept** - поставщик принимает заявку и предоставляет документы на взнос и конвертацию
* 3. **authcontrib/authreturn** - совет авторизует оба заявления раздельно
* 4. **supply** → **supplcnf** - поставка товара и подтверждение председателем КУ
* 5. **deliver1** → **deliver2** → **deliver3** → **deliver4** - этапы транспортировки
* 6. **receive** → **receivecnf** - получение товара заказчиком
* 7. **complete** - завершение поставки после гарантийного периода
*
* ## Документооборот:
*
* Все документы сохраняются в векторе `std::vector<document2> documents` в заявке.
* Каждый этап процесса добавляет необходимые документы в этот вектор.
*
* \note Контракт маркетплейса является центральной точкой экономической активности на платформе.
* \note Система упрощена для работы с одной заявкой вместо двух встречных.
*/
* @brief Контракт `marketplace` — кооперативный «Стол заказов» в режиме
* членских взносов.
*
* Реализует canonical actions трёх процессов из YAML-стандартов:
* - **p.mkt.supply** (9 actions): createorder, cancelorder, expireorder,
* acceptorder, declineorder, signsupp, signchair, signiss1, signiss2.
* - **p.mkt.return** (5 actions): submretrn, aprretrem, rejretrem, accretrn,
* rejretrn.
* - **p.mkt.wroff** (4 actions): propwroff, execwroff, onmktwoauth, onmktwodecl.
* Cписание скоропорта идёт через канонический паттерн «решение совета»:
* backend подписывает Заявление о списании (registry 1106) ключом
* кооператива, вызывает `propwroff` (запись wroffprops::proposed) +
* `soviet::createagenda(type=mktwroff, callback=onmktwoauth/onmktwodecl)`.
* После голосования совета и подписи Протокола (registry 1105) chairman'ом
* soviet::exec автоматически вызывает callback `onmktwoauth` (PROPOSED →
* AUTHORIZED, сохраняется protocol2) или `onmktwodecl` (PROPOSED → REJECTED).
* Только после AUTHORIZED backend циклом по items вызывает `execwroff`
* per-item (atomic пара o.mkt.wroff + o.mkt.wroff2 на каждой позиции).
*
* Все per-batch операции на on-chain выполняются per-Order (бэкенд
* проходит циклом по Order'ам соответствующего batch'а, объединяя их по
* `batch_hash`). Векторов order_hashes в actions нет — это ограничение
* на размер транзакции в Antelope (тысячи orders в одной транзакции
* не пройдут).
*
* Все ledger2-движения средств — через `Ledger2::apply(_marketplace, …)`,
* никаких прямых wallet/account-операций. 13 marketplace-операций
* зарегистрированы в `lib/core/ledger2/operations.hpp` (`OPERATION_REGISTRY`).
*
* Composite-операции (consum+consum2, return+return2, wroff+wroff2) —
* последовательные `Ledger2::apply` в одной транзакции Antelope (атомарность
* через single-action wrapper).
*
* Авторизация подписей под актами / решениями привязана к кооперативному
* участку (КУ) через контракт `branch` и helper
* `Branch::is_user_authorized(coopname, braname, signer)` — председатель
* КУ может делегировать подпись доверенному лицу из `coobranch.trusted[]`.
*
* Источник правды по логике actions, гардам, state-переходам и
* операциям — три YAML-файла рядом с этим .hpp:
* - `p.mkt.supply.standard.yaml`
* - `p.mkt.return.standard.yaml`
* - `p.mkt.wroff.standard.yaml`
*
* Donor-actions старой клиринговой модели (FR19a, AR30) удалены вместе с
* соответствующими таблицами `Marketplace::request/segment/shipment`.
*/
class [[eosio::contract(MARKETPLACE)]] marketplace : public eosio::contract {
public:
@@ -47,47 +77,289 @@ public:
eosio::datastream<const char *> ds)
: eosio::contract(receiver, code, ds) {}
void apply(uint64_t receiver, uint64_t code, uint64_t action);
// ── p.mkt.supply ─────────────────────────────────────────────────────
/**
* @brief Заказчик размещает заказ на товар из каталога (Story 4.1).
* Серия: o.wal.conv (conditional) → o.mkt.assign (conditional) → o.mkt.block.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void createorder(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash,
checksum256 offer_hash,
eosio::name offerer,
eosio::name delivery_braname,
uint64_t quantity,
eosio::asset unit_price,
eosio::name cycle_type,
uint32_t warranty_period_secs,
checksum256 batch_hash);
/**
* @brief Заказчик отменяет заказ до акцепта (Story 4.4). Триггерит o.mkt.unblk.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void cancelorder(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash);
/**
* @brief Backend закрывает Order по таймауту цикла отсечки (Story 4.3).
* Per-Order: o.mkt.unblk + статус active → cancelled. Backend вычисляет
* threshold по batch'у вне контракта; для каждого истёкшего Order'а
* вызывается отдельный `expireorder`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void expireorder(eosio::name coopname,
checksum256 order_hash);
/**
* @brief Поставщик акцептует один Order (Story 4.5).
* Без ledger2-операций — статус active → accepted. Backend проходит циклом
* по orders соответствующего batch'а, вызывая `acceptorder` per Order.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void acceptorder(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash);
/**
* @brief Поставщик отказывается от одного Order'а до акцепта (Story 4.5).
* Per-Order: o.mkt.unblk на total_cost + статус active → cancelled.
* Backend проходит циклом по orders батча, вызывая `declineorder` per Order.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void declineorder(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash);
/**
* @brief Поставщик первой подписью на АПП приёмки фиксирует партию по одному
* Order'у (Story 5.3/5.4). Без ledger2-операций — статус accepted →
* supply_prepared. Параметр `accept_braname` указывает приёмный КУ; запись
* в Order. Подпись валидируется как `verify_document_or_fail(act, {offerer})`.
* Backend проходит циклом по orders батча с одинаковым `act`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void signsupp(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash,
eosio::name accept_braname,
document2 act);
/**
* @brief Председатель приёмного КУ ставит закрывающую подпись на АПП
* приёмки одного Order'а (Story 5.3/5.4). Per-Order: только o.mkt.purch
* (Дт 10 / Кт 86). Выплата поставщику (o.mkt.payout) — отдельным lazy
* action'ом `payout` после подтверждения кассиром фактического банковского
* перевода (E11 техдолг 598-16, Locked Decision L12). Авторизация подписи:
* председатель / trustee / trusted ∈ branches[o.accept_braname]. Backend
* проходит циклом по orders батча с одинаковым `act`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void signchair(eosio::name coopname,
eosio::name signer,
checksum256 order_hash,
document2 act);
/**
* @brief Инициация исходящей выплаты поставщику через контракт gateway по
* одному Order'у (E11 техдолг 598-16, Locked Decision L12). Per-Order:
* inline-вызов `gateway::createoutpay` с callback'ами на `payconfirm` /
* `paydecline`. Ledger2-операция o.mkt.payout (Дт 86 / Кт 51) применяется
* НЕ здесь, а в callback'е `payconfirm` после действия кассира. Статус
* Order'а не меняется; защита от двойного запроса — через
* `order.payout_status` (NONE/DECLINED → PENDING).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void payout(eosio::name coopname,
checksum256 order_hash);
/**
* @brief Callback от gateway::outcomplete — кассир подтвердил
* банковский перевод поставщику (E11 техдолг 598-16, Locked Decision L12).
* Здесь применяется o.mkt.payout (Дт 86 / Кт 51) и `payout_status`
* переходит PENDING → COMPLETED. Авторизация: `_gateway`. `outcome_hash`
* совпадает с `order.hash` (так задано при `payout`).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void payconfirm(eosio::name coopname,
checksum256 outcome_hash);
/**
* @brief Callback от gateway::outdecline — кассир отметил, что
* банковский перевод не состоялся (E11 техдолг 598-16, Locked Decision L12).
* Ledger2-операция НЕ применяется; обязательство Кт 86 остаётся открытым.
* `payout_status` PENDING → DECLINED; `payout_decline_reason` сохраняется.
* Авторизация: `_gateway`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void paydecline(eosio::name coopname,
checksum256 outcome_hash,
std::string reason);
/**
* @brief Председатель КУ выдачи открывает выдачу первой подписью АПП-выдачи
* (Story 6.1). Без ledger2-операций — статус ready_to_receive. Авторизация:
* подписант ∈ branches[o.delivery_braname].
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void signiss1(eosio::name coopname,
eosio::name signer,
checksum256 order_hash,
document2 act);
/**
* @brief Заказчик ставит финальную подпись АПП-выдачи (Story 6.3).
* Per-Order с поддержкой actual_quantity ≠ ordered (Story 6.2).
* Atomic: [o.mkt.unblk на разницу если actual<ordered |
* o.wal.conv+o.mkt.assign+o.mkt.block на разницу если actual>ordered]
* + o.mkt.consum + o.mkt.consum2.
* Подпись акта: orderer + любой авторизованный из branches[o.delivery_braname].
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void signiss2(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash,
uint64_t actual_quantity,
eosio::name delivery_signer,
document2 act);
// ── p.mkt.return ─────────────────────────────────────────────────────
/**
* @brief Пайщик подаёт заявление на гарантийный возврат (Story 7.1).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void submretrn(eosio::name coopname,
eosio::name orderer,
checksum256 request_hash,
checksum256 original_order_hash,
uint64_t actual_quantity,
std::string reason_text,
std::vector<checksum256> photos,
document2 statement);
/**
* @brief Председатель удалённо одобряет очный визит (Story 7.2). Авторизация:
* подписант ∈ branches[braname]; параметр `braname` фиксирует КУ, в котором
* рассматривается заявление.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void aprretrem(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
document2 decision);
/**
* @brief Председатель удалённо отказывает (Story 7.2). Авторизация:
* подписант ∈ branches[braname].
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void rejretrem(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
std::string reason,
document2 decision);
/**
* @brief Председатель принимает возврат на очном осмотре (Story 7.4).
* Atomic: o.mkt.return + o.mkt.return2 (compensating forward).
* Авторизация: подписант ∈ branches[braname].
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void accretrn(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
document2 decision);
/**
* @brief Председатель отказывает на очном осмотре (Story 7.3).
* Авторизация: подписант ∈ branches[braname].
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void rejretrn(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
std::string reason,
document2 decision);
// ── p.mkt.wroff ──────────────────────────────────────────────────────
/**
* @brief Backend выносит проект списания на повестку совета (Story 8.1).
* Без ledger2-операций — только создание proposal с N позициями (статус
* proposed). Сразу после этого backend вызывает `soviet::createagenda` с
* `callback_contract=marketplace`, `confirm_callback=onmktwoauth`,
* `decline_callback=onmktwodecl`, `type=mktwroff`, `hash=proposal_hash`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void propwroff(eosio::name coopname,
eosio::name proposed_by,
checksum256 proposal_hash,
std::vector<wroff_item> items);
/**
* @brief Callback от `soviet::exec` после авторизации Протокола совета
* (registry 1105) председателем (Story 8.4). PROPOSED → AUTHORIZED;
* сохраняется `authorization` в `wroffprops.protocol`. Цикл per-item
* списания запускает backend через `execwroff` после получения этой дельты.
*
* Авторизация: контракт `_soviet` (`require_auth(_soviet)`); сигнатура
* соответствует `authorize_action_effect` в soviet — `(coopname, hash,
* authorization)`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void onmktwoauth(eosio::name coopname,
checksum256 hash,
document2 authorization);
/**
* @brief Callback от `soviet::cancelexprd` (или от любого decline-эффекта в
* soviet) — повестка отклонена или просрочена (Story 8.4). PROPOSED →
* REJECTED; `reason` сохраняется в `wroffprops.reject_reason`. Без
* ledger2-движений.
*
* Сигнатура `(coopname, hash, reason)` соответствует
* `DECLINE_CALLBACK_SIGNATURE` в `lib/core/soviet/soviet.hpp:19`.
* Авторизация: `_soviet`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void onmktwodecl(eosio::name coopname,
checksum256 hash,
std::string reason);
/**
* @brief Backend исполняет одну позицию авторизованного проекта списания
* (Story 8.4). Per-item: `o.mkt.wroff + o.mkt.wroff2` (атомарно в одной
* транзакции), `items[item_index].executed = true`. Когда все items
* исполнены, статус AUTHORIZED → EXECUTED.
*
* Защита от газового лимита (тысячи позиций в одной транзакции Antelope
* не помещаются) — backend проходит цикл и вызывает `execwroff` per item.
*
* Guards:
* - proposal.status == AUTHORIZED (callback `onmktwoauth` уже отработал);
* - подписант (`signer`) авторизован для КУ-источника
* (`branches[items[item_index].braname]`).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void execwroff(eosio::name coopname,
eosio::name signer,
checksum256 proposal_hash,
uint64_t item_index);
// ── service ──────────────────────────────────────────────────────────
/**
* @brief Заглушка миграции — donor-таблиц нет, мигрировать нечего.
* Оставлена для совместимости с CMake-build и прежним ABI.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void migrate();
// Действия для создания заявок
[[eosio::action]] void orderoffer(eosio::name coopname, eosio::name receiver_braname, eosio::name username, checksum256 hash, uint64_t units, eosio::asset unit_cost, uint32_t product_lifecycle_secs, uint32_t warranty_period_secs, eosio::asset membership_fee_amount, eosio::asset cancellation_fee_amount, document2 product_return_statement, document2 convert_in, std::string meta);
static void cancel_request(eosio::name coopname, eosio::name username, checksum256 request_hash);
// Статические методы для отклонения заявок
static void decline_request(eosio::name coopname, const request& change);
// Методы для направления заявок
[[eosio::action]] void accept(eosio::name coopname, eosio::name supplier_braname, eosio::name username, checksum256 request_hash, document2 convert_out, document2 return_document);
[[eosio::action]] void authcontrib(eosio::name coopname, checksum256 request_hash, document2 authorization);
[[eosio::action]] void authreturn(eosio::name coopname, checksum256 request_hash, document2 authorization);
[[eosio::action]] void declineacc(eosio::name coopname, checksum256 hash, std::string reason);
[[eosio::action]] void supply(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 act);
[[eosio::action]] void supplcnf(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 act);
// Новая система перевозок
[[eosio::action]] void createship(eosio::name coopname, checksum256 hash, eosio::name driver_username, eosio::name source_braname, eosio::name destination_braname, std::vector<checksum256> request_hashes, document2 transport_act_sender);
[[eosio::action]] void signbydriver(eosio::name coopname, checksum256 hash, document2 transport_act_driver);
[[eosio::action]] void arrived(eosio::name coopname, checksum256 hash, document2 transport_act_delivery);
[[eosio::action]] void receiveshipm(eosio::name coopname, checksum256 hash, document2 warehouse_receipt_act);
[[eosio::action]] void retransport(eosio::name coopname, checksum256 completed_hash, eosio::name new_driver_username, eosio::name source_braname, eosio::name new_destination_braname, std::vector<checksum256> request_hashes, document2 transport_act_sender);
// Доставка заказчику
[[eosio::action]] void delivered(eosio::name coopname, eosio::name username, checksum256 request_hash);
[[eosio::action]] void receive(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document);
[[eosio::action]] void receivecnf(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document);
[[eosio::action]] void complete(eosio::name coopname, eosio::name username, checksum256 request_hash);
[[eosio::action]] void decline(eosio::name coopname, eosio::name username, checksum256 request_hash, std::string meta);
[[eosio::action]] void cancel(eosio::name coopname, eosio::name username, checksum256 request_hash);
// Методы для работы с диспутом (гарантийный возврат)
[[eosio::action]] void dispute(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document);
[[eosio::action]] void wauthorize(eosio::name coopname, checksum256 request_hash, uint64_t wreturn_decision_id, document2 wreturn_authorization, uint64_t wsupply_decision_id, document2 wsupply_authorization);
[[eosio::action]] void wreturn(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document);
[[eosio::action]] void woffer(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document);
[[eosio::action]] void waccept(eosio::name coopname, eosio::name username, checksum256 request_hash, bool accept, document2 document);
struct [[eosio::table, eosio::contract("marketplace")]] balances : balances_base {};
struct [[eosio::table, eosio::contract("marketplace")]] counts : counts_base {};
};
@@ -0,0 +1,392 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Гарантийный возврат имущества» — кооперативный процесс возврата
# имущества пайщиком на склад КУ в пределах гарантийного срока.
#
# Возврат реализован как **compensating forward** — отдельная именованная
# операция o.mkt.return с собственными проводками, семантически обратными
# исходной выдаче (o.mkt.consum). Откат через ledger2::revert НЕ
# используется (упрощение реализации MVP — реверты исключены из системы).
#
# **Модель кошельков (упрощённая, refinement 2026-05-04):** один программный
# кошелёк w.mkt.member (per-user) — членские взносы пайщика в программу.
# Возврат суммы при гарантии происходит на тот же программный кошелёк
# (.available восстанавливается); пайщик может потратить на следующий заказ
# или вывести в общий членский кошелёк (w.wal.member) через o.mkt.recall.
#
# Идентификация кошельков — eosio::name с префиксом w.<contract>.<waltype>
# (рефакторинг 2026-04-27 на ветке reports). Sentinel '' (пустая строка) —
# «кошелёк вне системы» для ISSUE/REVOKE/ACCOUNT_ONLY.
#
# Возврат поставщику и работа с поставщиком по претензиям — out of MVP;
# вернувшееся имущество остаётся на складе КУ как материальный остаток,
# его дальнейшая судьба вне этого процесса.
#
# В процессе участвуют 2 ledger2-операции (compensating-forward пара, зеркало
# выдачи на supply — через транзит счёта 91):
# • o.mkt.return — часть 1: ISSUE ∅ → w.mkt.member (восстановление
# .available заказчика в программе) + Дт 91 / Кт 86
# (восстановление «прочих» за счёт ЦФ)
# • o.mkt.return2 — часть 2: NONE Дт 10 / Кт 91 (имущество назад на склад
# через закрытие транзита). Атомарно с o.mkt.return
# в той же транзакции accretrn.
#
# Канон формата:
# coopenomics-docs/docs/standards/_spec/canon.md
# Источники правды в коде:
# • cpp/marketplace/marketplace.hpp — actions (status: proposed)
# • cpp/marketplace/src/return/ — реализация (предстоит)
# • 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.member (programmatic)
# • cpp/lib/core/ledger2/accounts.hpp — Целевое финансирование (86),
# Материалы (10),
# Прочие доходы и расходы (91 — NEW,
# транзит при возврате o.mkt.return)
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
process_type: p.mkt.return
id: public_marketplace_return_process
title: Гарантийный возврат имущества
slug: return
status: proposed
contract: marketplace
purpose: >
Гарантийная защита заказчика по сделкам «Стола заказов». Если после
получения товара заказчик обнаружил дефект, недокомплект или
истечение срока годности в пределах гарантии, заданной поставщиком,
он подаёт заявление на гарантийный возврат. Председатель участка
сначала рассматривает заявление удалённо — по фото и описанию — и
принимает одно из двух решений: одобрить очный визит или отказать
удалённо. Если визит одобрен, заказчик приходит с продукцией на
участок, председатель очно осматривает товар и выносит финальное
решение — принять возврат или отказать на месте. При принятии
возврата товар остаётся на складе участка, а сумма заказа
возвращается заказчику на программный членский кошелёк Стола
заказов: он может направить её на следующий заказ программы либо
вывести в универсальный членский кошелёк отдельным действием.
При отказе — на этом гарантийный возврат завершён, движений по
имуществу и средствам не происходит. Дальнейшая судьба возвращённого
имущества (возврат поставщику, перепоставка, списание) — вне этого
процесса.
roles:
- orderer # пайщик-заказчик, инициатор возврата
- chairman # председатель кооперативного участка (КУ)
# ── Секция 2. Действия контракта (блокчейн-уровень) ─────────────────────────
# Имена actions ≤12 символов eosio::name. Контракт описан в целевом виде —
# реализация в .cpp предстоит после согласования стандарта.
actions:
- name: marketplace::submretrn
human: Подать заявление на гарантийный возврат
actor: orderer
role: opener
purpose: >
Заказчик подаёт заявление на гарантийный возврат имущества:
указывает причину обращения (некондиция, истёк срок годности,
иное) и прикладывает фотографии товара. Подать заявление можно
только пока не истёк гарантийный срок, заданный поставщиком.
- name: marketplace::aprretrem
human: Одобрить очный визит
actor: chairman
role: progress
purpose: >
Председатель участка по результатам удалённого рассмотрения
решает, что для разбора обращения нужен очный осмотр товара, и
приглашает заказчика прийти на участок с продукцией.
- name: marketplace::rejretrem
human: Отказать удалённо
actor: chairman
role: reject
purpose: >
Председатель участка по результатам удалённого рассмотрения
решает отказать в гарантийном возврате с указанием причины — без
приглашения на очный осмотр. Решение финальное, движений по
имуществу и средствам не происходит.
- name: marketplace::accretrn
human: Принять возврат
actor: chairman
role: closer
purpose: >
Председатель по результатам очного осмотра принимает гарантийный
возврат: товар остаётся на складе участка, сумма заказа
возвращается заказчику на программный членский кошелёк Стола
заказов.
- name: marketplace::rejretrn
human: Отказать на месте
actor: chairman
role: reject
purpose: >
Председатель по результатам очного осмотра отказывает в
гарантийном возврате с указанием причины. Заказчик забирает
товар обратно. Решение финальное, движений по имуществу и
средствам не происходит.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
# Сущность: marketplace::return_request (таблица return_requests, scope=coopname).
entity: marketplace::return_request
entity_human: Заявление на гарантийный возврат
entity_source: cpp/marketplace/src/return/
states:
- name: pending_review
human: Заявление на рассмотрении
description: >
Заявление подано заказчиком и ждёт решения председателя участка.
kind: normal
- name: approved_for_visit
human: Очный визит одобрен
description: >
Председатель одобрил очное рассмотрение. Заказчику предстоит
прийти на участок с продукцией для очного осмотра.
kind: normal
- name: return_accepted
human: Возврат принят
description: >
Возврат принят кооперативом. Сумма заказа возвращена заказчику на
программный членский кошелёк Стола заказов — он может направить её
на следующий заказ программы либо вывести в универсальный членский
кошелёк отдельным действием. Дальнейшая судьба возвращённого
имущества (возврат поставщику, перепоставка, списание) — отдельная
процедура по регламенту кооператива; в рамках этого процесса товар
остаётся на складе участка.
kind: final
- name: rejected_remote
human: Отказ удалённо
description: >
Председатель отказал по результатам удалённого рассмотрения.
Гарантийный возврат завершён, движений по имуществу и средствам
не происходит.
kind: final
- name: rejected_at_ku
human: Отказ на месте
description: >
Председатель отказал по результатам очного осмотра. Заказчик
забирает товар обратно. Гарантийный возврат завершён, движений
по имуществу и средствам не происходит.
kind: final
transitions:
- from: "∅"
to: pending_review
action: marketplace::submretrn
actor: orderer
guards:
- Заявитель — заказчик-пайщик исходного заказа, по которому имущество уже выдано.
- Гарантийный срок, заданный поставщиком в предложении, ещё не истёк.
- К заявлению приложены фотографии товара и указана причина возврата.
- from: pending_review
to: approved_for_visit
action: marketplace::aprretrem
actor: chairman
guards:
- Председатель решил пригласить заказчика на очный осмотр.
- from: pending_review
to: rejected_remote
action: marketplace::rejretrem
actor: chairman
guards:
- Председатель решил отказать в гарантийном возврате удалённо с указанием причины.
- from: approved_for_visit
to: return_accepted
action: marketplace::accretrn
actor: chairman
ledger_code: p.mkt.return
operations:
- o.mkt.return
- o.mkt.return2
guards:
- Заказчик прибыл на участок с продукцией.
- Председатель очно осмотрел имущество и решил принять гарантийный возврат.
- Гарантийный срок ещё не истёк.
- from: approved_for_visit
to: rejected_at_ku
action: marketplace::rejretrn
actor: chairman
guards:
- Председатель очно осмотрел имущество и решил отказать в гарантийном возврате с указанием причины.
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
scenario:
steps:
- step: 1
title: Подача заявления
actor: orderer
action: marketplace::submretrn
description: >
Заказчик подаёт заявление на гарантийный возврат имущества:
указывает причину обращения, прикладывает фотографии товара,
ссылается на акт приёма-передачи, по которому получал имущество.
Подать заявление
можно только пока не истёк гарантийный срок, заданный
поставщиком.
pre:
- Заказ закрыт, имущество выдано заказчику.
- Гарантийный срок ещё не истёк.
- Приложены фотографии товара и указана причина обращения.
post:
- Заявление зарегистрировано и направлено председателю на рассмотрение.
- step: 2
title: Удалённое рассмотрение — приглашение на очный визит
actor: chairman
action: marketplace::aprretrem
description: >
Председатель участка изучает заявление и приложенные материалы
и решает, что для разбора обращения нужен очный осмотр товара.
Заказчику предстоит прийти на участок с продукцией.
pre:
- Заявление на рассмотрении.
post:
- Очный визит одобрен.
- step: 3
title: Очный осмотр — принятие возврата
actor: chairman
action: marketplace::accretrn
description: >
Заказчик приходит на участок с продукцией. Председатель очно
осматривает товар и принимает гарантийный возврат: товар
остаётся на складе участка, сумма заказа возвращается
заказчику на программный членский кошелёк Стола заказов.
pre:
- Очный визит одобрен.
- Заказчик прибыл на участок с продукцией.
post:
- Товар остаётся на складе участка.
- Сумма заказа возвращена заказчику на программный членский кошелёк.
alternatives:
- branch: Гарантийный срок истёк
at_step: 1
action: null
actor: orderer
description: >
Если гарантийный срок поставщика истёк, заявление на гарантийный
возврат подать нельзя.
- branch: Удалённое рассмотрение — отказ
at_step: 2
action: marketplace::rejretrem
actor: chairman
description: >
Председатель отказывает в гарантийном возврате удалённо с
указанием причины — например, обращение очевидно не подпадает
под гарантию. Решение финальное, движений по имуществу и
средствам не происходит.
- branch: Очный осмотр — отказ
at_step: 3
action: marketplace::rejretrn
actor: chairman
description: >
Председатель отказывает в гарантийном возврате по результатам
очного осмотра с указанием причины — например, характер
повреждений не покрывается гарантией. Заказчик забирает товар
обратно. Решение финальное, движений по имуществу и средствам
не происходит.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
# В процессе подписывается заявление пайщика на возврат с приложениями
# (фото товара). Решение председателя — процедурное действие в системе,
# отдельным документом не оформляется в MVP (фиксируется в стейт-машине
# return_request с кем и когда принято решение). Если регулятор / устав
# потребуют документного оформления решения председателя — это будет
# отдельным шаблоном (TODO).
documents:
- action: marketplace::submretrn
title: Заявление пайщика на гарантийный возврат имущества
registry_id: 800
signed_by: [orderer]
stored_in: return_requests.statement
note: "Используется существующий шаблон 800.ReturnByAssetStatement из cooptypes/cooperative/registry/. Оригинально создан под клиринговую модель donor'а («Заявление на возврат паевого взноса имуществом»); форма заявления пайщика на возврат структурно подходит и для членской модели гарантийного возврата. При необходимости методолог может создать специализированный шаблон в новой серии (1100+) — тогда registry_id обновится."
- action: marketplace::accretrn
title: Решение председателя КУ о принятии гарантийного возврата
registry_id: 0
signed_by: [chairman]
stored_in: return_requests.chairman_decision
note: "TODO: в MVP решение принимается единолично председателем КУ, не советом — существующий шаблон 801.ReturnByAssetDecision не подходит (рассчитан на коллегиальное решение совета по новации). Создать специализированный шаблон в registry либо оформлять как in-system запись без отдельного документа."
# ── Секция 6. Операции (Ledger2) ────────────────────────────────────────────
# Одна ledger2-операция — compensating forward к o.mkt.consum (без
# использования ledger2::revert). Атомарно: восстановление .available
# на программном кошельке пайщика + проводка Дт 10 / Кт 86 (обратная
# к выдаче).
operations:
- ledger_code: o.mkt.return
human_name: Гарантийный возврат — восстановление средств в программе
wallet_op: ISSUE
# L1 — часть 1 композитной проводки через транзит счёта 91 (зеркало
# выдачи): Дт 91 / Кт 86 — восстановление «прочих» за счёт ЦФ.
# Закрытие транзита выполняется отдельной операцией o.mkt.return2
# (Дт 10 / Кт 91), атомарно с o.mkt.return в той же транзакции
# accretrn. Согласовано с Ангелиной 2026-05-11.
debit: 91 # Прочие доходы и расходы (NEW)
credit: 86 # Целевое финансирование
# L2 — восстановление средств в программе
wallet_from: null # эмиссия — источника нет
wallet_to: w.mkt.member # ЦПП «Стол Заказов» — программный кошелёк
# L3 — пайщику восстанавливается available на программном кошельке
user_wallet: w.mkt.member
user_ref: return_request.orderer
available_delta: +order.fact_cost
blocked_delta: null
amount_ref: order.fact_cost
triggered_by: marketplace::accretrn
description: >
Compensating forward к выдаче имущества (o.mkt.consum), часть 1
композитной бухгалтерской проводки через транзит счёта 91. ISSUE на
пайщикском w.mkt.member — .available +fact_cost (восстановление ранее
списанной суммы); проводка Дт 91 / Кт 86 — восстановление «прочих»
за счёт ЦФ (зеркало 91-стороны от o.mkt.consum). Сразу после этой
операции в той же транзакции accretrn контракт вызывает
o.mkt.return2 (Дт 10 / Кт 91, NONE) — имущество назад на склад через
закрытие транзита. Транзит через 91 симметричен выбытию на выдаче —
согласовано с Ангелиной 2026-05-11. Журнал содержит payload
original_consume_op_id (ссылка на исходный o.mkt.consum для
трассировки) — это **прикладное поле**, не часть инфраструктуры
revert. Дальнейшая судьба возвращённого имущества — вне этого
процесса.
Пайщик может потратить возвращённую сумму на следующий заказ в Столе
заказов либо вывести в общий членский кошелёк (w.wal.member) через
отдельную операцию o.mkt.recall — там средства универсальны между
программами.
- ledger_code: o.mkt.return2
human_name: Гарантийный возврат — закрытие транзита
wallet_op: NONE
# L1 — часть 2 композитной проводки: Дт 10 / Кт 91 (имущество назад
# на склад через закрытие транзита). Зеркало 91-стороны от o.mkt.csmcls.
debit: 10 # Материалы (NEW) — имущество на складе
credit: 91 # Прочие доходы и расходы (NEW)
# L2 / L3 — без движения по кошелькам (склад — аналитика по счёту 10)
wallet_from: null
wallet_to: null
user_wallet: null
user_ref: null
available_delta: null
blocked_delta: null
amount_ref: order.fact_cost
triggered_by: marketplace::accretrn
description: >
Закрытие транзита счёта 91 после восстановления членского взноса
пайщика. NONE — только бухпроводка Дт 10 / Кт 91, кошельки не
двигаются. Срабатывает атомарно с o.mkt.return в той же транзакции
accretrn. По итогу пары operations нетто-эффект — Дт 10 / Кт 86
(имущество возвращается на склад + восстановление обязательства
по целевой программе), 91 проходит нулевым транзитом.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,330 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Утилизация скоропорта» — кооперативный процесс периодического
# списания со склада участка имущества, которое физически пропало или стало
# непригодным к выдаче заказчику (просрочка, повреждения, малоценные позиции).
#
# Процесс инициируется кооперативом по расписанию (по умолчанию раз в месяц;
# точную дату согласовывает бухгалтер). По итогам опроса складов председатель
# подписывает заявление о списании и вносит его на рассмотрение совета.
# Совет рассматривает заявление через типовой процесс решения совета
# (стандарт sov.decision) и подписывает протокол. По принятому протоколу
# кооператив списывает каждую позицию проекта со склада участка.
#
# Списание идёт **через транзит счёта 91 «Прочие доходы и расходы»** — это
# фиксирует, что имущество безвозвратно исчезло из кооператива (а не передано
# пайщику). По кошелькам заказчиков движений не происходит — это чисто
# кооперативный учётный расход.
#
# **Модель кошельков (упрощённая, refinement 2026-05-04):** процесс не двигает
# кошельки — это бухгалтерское событие через проводки счёта 10 (учёт
# имущества), 91 (транзит) и 86 (ЦФ программы). Имущество отслеживается
# аналитикой по счёту 10 (per-КУ субсчета), без отдельного кошелька.
#
# В процессе участвуют 2 ledger2-операции (композитная проводка через транзит
# счёта 91, срабатывают в одной транзакции списания позиции):
# • o.mkt.wroff — часть 1: Дт 91 / Кт 10 (выбытие со склада на «прочие»)
# • o.mkt.wroff2 — часть 2: Дт 86 / Кт 91 (закрытие транзита на ЦФ программы)
#
# Канон формата:
# coopenomics-docs/docs/standards/_spec/canon.md
# Источники правды в коде:
# • cpp/marketplace/marketplace.hpp — actions (status: proposed)
# • cpp/marketplace/src/p.mkt.wroff/ — реализация
# • cpp/lib/core/ledger2/operations.hpp — OPERATION_REGISTRY
# расширение o.mkt.wroff
# • cpp/lib/core/ledger2/processes.hpp — processes::marketplace::WRITEOFF
# • cpp/lib/core/ledger2/accounts.hpp — Целевое финансирование (86),
# Материалы (10),
# Прочие доходы и расходы (91)
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
process_type: p.mkt.wroff
id: public_marketplace_writeoff_process
title: Утилизация скоропорта
slug: writeoff
status: proposed
contract: marketplace
purpose: >
Штатный путь корректно отразить в учёте имущество, которое физически
пропало или стало непригодным к выдаче. Применяется, когда товар на
складе участка просрочился, испортился или иначе не может быть передан
заказчику. Без этого процесса такие позиции висели бы на складе
бесконечно — здесь кооператив честно фиксирует свои потери через
решение совета.
Процесс состоит из двух шагов. На первом шаге кооператив по расписанию
(по умолчанию раз в месяц; точную дату согласовывает бухгалтер)
опрашивает склады участков и собирает позиции, удовлетворяющие
критериям списания. Председатель оформляет проект списания, подписывает
заявление и вносит его на рассмотрение совета. На втором шаге совет
по типовому процессу решения совета рассматривает заявление и
подписывает протокол: при положительном решении каждая позиция
списывается со склада как безвозвратные потери; при отрицательном
проект отклоняется и позиции остаются на складе.
По кошелькам заказчиков движений не происходит — это чисто кооперативный
учётный расход через транзит счёта 91 «Прочие доходы и расходы» с
закрытием на счёт целевого финансирования программы.
roles:
- chairman # председатель кооператива (подписывает заявление, вносит на совет)
- council # совет кооператива (принимает решение по протоколу)
# ── Секция 2. Действия контракта (блокчейн-уровень) ─────────────────────────
# Имена actions ≤12 символов eosio::name. Заявление председателя на
# рассмотрение совета вносится по типовому процессу «Решение совета»
# (стандарт sov.decision), поэтому здесь перечислены только специфические
# для списания действия — внесение проекта и исполнение позиций по
# принятому протоколу совета.
actions:
- name: marketplace::propwroff
human: Внести проект списания на рассмотрение совета
actor: chairman
role: opener
purpose: >
Председатель подписывает заявление о списании скоропорта — список
позиций, которые невозможно выдать заказчику (просроченные и не
востребованные, повреждённые, малоценные), — и вносит проект на
рассмотрение совета. Дальнейшее движение проекта (повестка совета,
голосование, подписание протокола) — по типовому процессу решения
совета.
- name: marketplace::execwroff
human: Исполнить позицию списания по протоколу совета
actor: chairman
role: closer
purpose: >
После принятия советом положительного решения по каждой позиции
проекта выполняется списание: имущество выбывает со склада участка,
сумма закрывается на счёт целевого финансирования программы через
транзит счёта 91. На каждую позицию — атомарная пара операций
o.mkt.wroff + o.mkt.wroff2 в одной транзакции. Когда все позиции
проекта исполнены, проект считается завершённым.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
# Сущность: marketplace::writeoff_proposal (таблица wroffprops, scope=coopname).
# Запись агрегирует список позиций, попавших в один цикл списания, и проходит
# свой жизненный цикл (proposed → authorized → executed | rejected).
entity: marketplace::writeoff_proposal
entity_human: Проект списания скоропорта
entity_source: cpp/marketplace/src/p.mkt.wroff/
states:
- name: proposed
human: На рассмотрении совета
description: >
Председатель подписал заявление о списании и внёс проект на
рассмотрение совета. Ожидается решение совета по типовому процессу
решения совета.
kind: normal
- name: authorized
human: Решение совета принято
description: >
Совет принял положительное решение по проекту и подписал протокол.
По каждой позиции проекта последовательно выполняется списание со
склада участка.
kind: normal
- name: executed
human: Списание исполнено
description: >
Кооператив списал все позиции проекта со складов участков.
Имущество ушло из учёта как безвозвратные потери. По кошелькам
заказчиков движений не было.
kind: final
- name: rejected
human: Совет отклонил проект
description: >
Совет отклонил проект списания. Позиции остаются на складах
участков и могут попасть в следующий цикл списания.
kind: final
transitions:
- from: "∅"
to: proposed
action: marketplace::propwroff
actor: chairman
guards:
- Наступил регламентный срок очередного цикла списания (или ручной запуск председателем).
- Председатель подписал Заявление о списании скоропорта.
- На складах участков найдены позиции, удовлетворяющие критериям списания.
- from: proposed
to: authorized
actor: council
guards:
- Совет рассмотрел проект и принял положительное решение по типовому процессу решения совета.
- Председатель подписал Протокол совета о списании скоропорта.
- from: authorized
to: executed
action: marketplace::execwroff
actor: chairman
ledger_code: p.mkt.wroff
operations:
- o.mkt.wroff
- o.mkt.wroff2
guards:
- Решение совета по проекту принято и протокол подписан.
- Все позиции проекта последовательно исполнены.
- from: proposed
to: rejected
actor: council
guards:
- Совет рассмотрел проект и отклонил его по типовому процессу решения совета.
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
scenario:
steps:
- step: 1
title: Формирование и внесение проекта на рассмотрение совета
actor: chairman
action: marketplace::propwroff
description: >
Кооператив по расписанию (по умолчанию раз в месяц; точную дату
согласовывает бухгалтер) опрашивает склады участков и собирает
позиции, которые невозможно выдать заказчику: просроченные и не
востребованные, повреждённые, малоценные. Председатель оформляет
проект списания со списком позиций, подписывает заявление и
вносит проект на рассмотрение совета.
pre:
- Наступил регламентный срок очередного цикла списания.
- На складах участков найдены позиции, удовлетворяющие критериям списания.
- Председатель подписал Заявление о списании скоропорта.
post:
- Проект списания сформирован и подписан председателем.
- Проект внесён на рассмотрение совета.
- step: 2
title: Рассмотрение проекта советом
actor: council
action: null
description: >
Совет рассматривает заявление председателя по типовому процессу
решения совета — на очном заседании или в форме заочного
голосования — и принимает решение. При положительном решении
председатель подписывает протокол о списании. При отрицательном —
проект отклоняется.
pre:
- Проект внесён на рассмотрение совета.
post:
- Совет принял решение по проекту и подписал протокол.
- step: 3
title: Исполнение списания
actor: chairman
action: marketplace::execwroff
description: >
По принятому советом протоколу кооператив последовательно
списывает каждую позицию проекта со склада участка. Имущество
безвозвратно выбывает из учёта; сумма закрывается на счёт
целевого финансирования программы через транзит счёта 91. По
кошелькам заказчиков движений нет.
pre:
- Совет принял положительное решение по проекту.
- Протокол совета о списании подписан.
post:
- Все позиции проекта списаны со складов участков.
- Имущество ушло из учёта как безвозвратные потери.
alternatives:
- branch: Совет отклонил проект
at_step: 2
action: null
actor: council
description: >
Совет отклонил проект списания — например, требуется
дополнительная экспертиза — либо отложил рассмотрение. Позиции
остаются на складах участков и могут попасть в следующий цикл
списания. Движений по имуществу и средствам не происходит.
- branch: Альтернатива — переуступка имущества по сниженной цене
at_step: 1
action: null
actor: chairman
description: >
Альтернативный путь для имущества, ещё пригодного к выдаче —
например, накануне истечения срока годности: кооператив сам
выступает поставщиком в новом предложении по сниженной цене и
ставит имущество на следующий цикл отсечки заявок. В MVP только
помечен; реализуется отдельным процессом в более поздней фазе.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
# Заявление председателя — собственный документ процесса (вносится советом
# на рассмотрение). Протокол совета — документ типового процесса решения
# совета (по стандарту sov.decision), к которому привязан проект списания.
documents:
- action: marketplace::propwroff
title: Заявление о списании скоропорта
registry_id: 1106
signed_by: [chairman]
stored_in: writeoff_proposals.statement
note: "Подписывается председателем перед внесением проекта на рассмотрение совета. Содержит список позиций к списанию (участок, наименование, количество, сумма, причина). Источник правды — components/cooptypes/src/cooperative/registry/1106.MarketplaceWriteoffStatement"
- action: null
title: Протокол совета о списании скоропорта
registry_id: 1105
signed_by: [chairman]
stored_in: writeoff_proposals.protocol
note: "Протокол по типовому процессу решения совета (стандарт sov.decision). Подписывается председателем после принятия советом положительного решения. Источник правды — components/cooptypes/src/cooperative/registry/1105.MarketplaceWriteoffProtocol"
# ── Секция 6. Операции (Ledger2) ────────────────────────────────────────────
# Композитная бухгалтерская проводка через транзит счёта 91 «Прочие доходы
# и расходы» — разбита на пару операций, срабатывающих атомарно по каждой
# позиции проекта в одной транзакции исполнения списания.
operations:
- ledger_code: o.mkt.wroff
human_name: Утилизация скоропорта — выбытие со склада
wallet_op: NONE # только бухпроводка без движения по кошелькам
# L1 — часть 1 композитной проводки: Дт 91 / Кт 10 (выбытие имущества
# со склада на «прочие»). Закрытие транзита выполняется отдельной
# операцией o.mkt.wroff2 (Дт 86 / Кт 91), атомарно с o.mkt.wroff
# в той же транзакции исполнения списания.
debit: 91 # Прочие доходы и расходы
credit: 10 # Материалы
# L2 — без перевода между кошельками кооператива
wallet_from: null
wallet_to: null
# L3 — без движения по кошелькам пайщиков (чисто кооперативный расход)
user_wallet: null
user_ref: null
available_delta: null
blocked_delta: null
amount_ref: writeoff_item.cost
triggered_by: marketplace::execwroff
description: >
Списание имущества со склада участка — часть 1 композитной
бухгалтерской проводки через транзит счёта 91 «Прочие доходы и
расходы». Только бухпроводка Дт 91 / Кт 10, кошельки не двигаются.
Сразу после этой операции в той же транзакции вызывается o.mkt.wroff2
(Дт 86 / Кт 91) — закрытие транзита на счёт целевого финансирования
программы. Применяется по каждой позиции из списка проекта —
несколько последовательных пар в одной транзакции исполнения.
Согласовано с Ангелиной 2026-04-27.
- ledger_code: o.mkt.wroff2
human_name: Утилизация скоропорта — закрытие транзита
wallet_op: NONE
# L1 — часть 2 композитной проводки: Дт 86 / Кт 91 (закрытие «прочих»
# на счёт целевого финансирования программы).
debit: 86 # Целевое финансирование
credit: 91 # Прочие доходы и расходы
# L2 / L3 — без движения по кошелькам
wallet_from: null
wallet_to: null
user_wallet: null
user_ref: null
available_delta: null
blocked_delta: null
amount_ref: writeoff_item.cost
triggered_by: marketplace::execwroff
description: >
Закрытие транзита счёта 91 после выбытия имущества со склада. Только
бухпроводка Дт 86 / Кт 91, кошельки не двигаются. Срабатывает
атомарно с o.mkt.wroff в той же транзакции исполнения списания.
По итогу пары operations нетто-эффект — Дт 86 / Кт 10 (закрытие
целевого финансирования программы + выбытие имущества со склада),
91 проходит нулевым транзитом.
@@ -0,0 +1,113 @@
# Выплата поставщику через контракт gateway
Шпаргалка для агентов: как устроен поток исходящей выплаты поставщику в
процессе `p.mkt.supply` после разделения `signchair` и `payout` (E11 техдолг
598-16, Locked Decision L12).
## Зачем разделили
Прежний `signchair` атомарно выполнял `o.mkt.purch + o.mkt.payout`
кооператив закрывал обязательство перед поставщиком по счёту 51 ДО
фактического банковского перевода. Формальное расхождение между bookkeeping
и реальной кассой; недопустимо для регуляторного релиза.
L12 требует, чтобы `o.mkt.payout` (Дт 86 / Кт 51) применялся только по факту
реального банковского перевода. Реальный перевод делает кассир через свой
стол в контракте `gateway`а контракт `gateway` уже умеет дёргать обратно
contract-callback'ом, как только кассир подтвердил/отказал.
## Триплет действий
| Action | Кто вызывает | Что делает |
|---------------------------|------------------------|-----------------------------------------------------------------|
| `marketplace::payout` | backend (auth coopname) | inline `gateway::createoutpay` — регистрирует исходящий платёж |
| `marketplace::payconfirm` | gateway (auth _gateway) | callback от `gateway::outcomplete` — применяет `o.mkt.payout` |
| `marketplace::paydecline` | gateway (auth _gateway) | callback от `gateway::outdecline` — фиксирует отказ, без ledger |
## Под-граф `order.payout_status`
```
marketplace::payout marketplace::payconfirm
none ────────────────────────► pending ────────────────────────► completed
│ marketplace::paydecline
declined ──┐
▲ │ marketplace::payout (повтор)
└───────┘
```
- `none` — приёмка завершена, выплата ещё не инициировалась.
- `pending` — gateway хранит запись `outcomes` со статусом pending, ждёт
действия кассира.
- `completed` — gateway стёр запись (на outcomplete) и callback'ом дёрнул
`payconfirm`; применён o.mkt.payout, обязательство закрыто.
- `declined` — gateway стёр запись (на outdecline) и callback'ом дёрнул
`paydecline`; обязательство Кт 86 остаётся открытым, сохраняется
`payout_decline_reason`. Backend может повторить `payout`.
Статус самого `Order.status` (READY_TO_RECEIVE / RECEIVED) в этом поток
никак не участвует — выплата может идти параллельно шагам выдачи.
## Контрактные детали
- `outcome_hash` для gateway = `order.hash`. Уникальность гарантирована
индексом `byhash` orders. На decline gateway стирает свою запись, что
снимает коллизию для повторной инициации.
- `username` для gateway = `order.offerer` — это пайщик-поставщик, который
получит деньги.
- `quantity` = `order.total_cost`.
- `callback_contract = _marketplace`, `confirm_callback = "payconfirm"_n`,
`decline_callback = "paydecline"_n`.
- `marketplace` уже в `contracts_whitelist` в `lib/consts.hpp`, поэтому
inline-вызов `gateway::createoutpay` проходит auth-чек на gateway-стороне.
Гарды:
- `payout`: `order.status ∈ { accepted_to_coop, ready_to_receive, received }`
и `order.payout_status ∈ { none, declined }`.
- `payconfirm` / `paydecline`: `require_auth(_gateway)` +
`order.payout_status == pending`.
`payout_decline_reason` очищается при инициации новой попытки (`payout`) и
при успехе (`payconfirm`); заполняется только в `paydecline`.
## Что меняется в backend (controller)
- `MarketplaceCanonicalBlockchainPort.payOut(coopname, order_hash)`
единственный action, который backend сам отправляет. Кассир будет вызывать
его из админки через application service (Story 5.6 ещё не реализована —
port готов).
- `payconfirm` / `paydecline` backend сам **не отправляет**. Контракт
gateway вызывает их inline'ом, parser2 разбирает их как обычные
blockchain_actions, controller подхватывает через delta-stream и обновляет
`marketplace_outgoing_payment_request.status` (PENDING_CASHIER_ACTION →
CONFIRMED_BY_CASHIER → LEDGER_RECORDED при `payconfirm`; CASHIER_DECLINED
при `paydecline`). Это уже зона Story 5.6 — здесь сделан только
C++/cooptypes/port-foundation.
## Что НЕ делать (anti-patterns)
- Не дёргать `Ledger2::apply(o.mkt.payout, …)` где-либо кроме `payconfirm`.
- Не выставлять `payout_status = completed` ни в одном action кроме
`payconfirm`.
- Не отправлять `payout` если `payout_status == pending` — это создаст
дубль `outcomes` в gateway (gateway сам ругнётся, но лучше отсечь раньше).
- Не разрешать backend дёргать `payconfirm` / `paydecline` напрямую —
только gateway-контракт через inline-action.
- Не вводить отдельный `outcome_hash` поле в `order` — пока он совпадает
с `order.hash`, дублирование избыточно.
## Источники правды
- `components/contracts/cpp/marketplace/src/p.mkt.supply/payout.cpp` /
`payconfirm.cpp` / `paydecline.cpp` — реализация.
- `components/contracts/cpp/lib/domain/table_marketplace_orders.hpp`
`order.payout_status`, `payout_decline_reason`, `OrderPayoutStatus` enum.
- `components/contracts/cpp/marketplace/p.mkt.supply.standard.yaml`
states/transitions/operations/scenario (`step 6a` / `6b` + alternative
«Кассир отклонил банковский перевод»).
- `components/contracts/cpp/lib/core/gateway/gateway.hpp`
`Gateway::create_outcome` helper и сигнатура `CREATEOUTPAY_SIGNATURE`.
- `components/contracts/cpp/gateway/src/outpay/outcomplete.cpp` /
`outdecline.cpp` — где gateway отправляет callback.
@@ -1,55 +0,0 @@
/**
\ingroup public_actions
\brief Подтверждение готовности выполнить заявку.
@details Данный метод позволяет пользователю, который получил предложение по своей заявке, подтвердить свою готовность его принять и выполнить. При этом формируется пакет документов, который отправляется в совет на утверждение.
@param username Имя пользователя, подтверждающего готовность выполнить предложение.
@param exchange_id ID предложения, которое следует подтвердить.
@note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::accept(eosio::name coopname, eosio::name username, uint64_t exchange_id, document2 document) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> status == "published"_n, "Только заявка в статусе ожидания может быть принята");
auto parent_change = exchange.find(change -> parent_id);
eosio::check(parent_change != exchange.end(), "Родительская заявка не найдена");
eosio::check(parent_change -> username == username, "Недостаточно прав доступа");
eosio::check(parent_change -> remain_units >= change -> remain_units, "Недостаточно объектов для поставки");
// Проверяем подпись документа
verify_document_or_fail(document);
exchange.modify(parent_change, _marketplace, [&](auto &i) {
i.remain_units -= change -> remain_units;
i.supplier_amount = (parent_change -> remain_units - change -> remain_units ) * parent_change -> unit_cost;
i.blocked_units += change -> remain_units;
});
exchange.modify(change, _marketplace, [&](auto &o){
o.status = "accepted"_n;
o.blocked_units += change -> remain_units;
o.remain_units = 0;
o.accepted_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
if (change -> type == "order"_n) {
o.contribute_product_statement = document;
} else if (change -> type == "offer"_n) {
o.return_product_statement = document;
};
});
action(
permission_level{ _marketplace, "active"_n},
_soviet,
_change_action,
std::make_tuple(change -> coopname, username, change -> username, exchange_id, change -> money_contributor, change -> product_contributor)
).send();
}
@@ -1,28 +0,0 @@
/**
\ingroup public_actions
\brief Добавление единиц товара к заявке.
@details Метод позволяет владельцу заявки дополнительно увеличить количество товара, доступное для обмена в рамках указанной заявки.
Используется, когда у продавца появляется дополнительное количество товара, которое он хочет добавить к существующей заявке.
@param username Имя пользователя, инициировавшего добавление.
@param exchange_id Идентификатор заявки, к которой добавляются единицы товара.
@param units Количество новых единиц товара, которые следует добавить к заявке.
@note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::addunits(eosio::name coopname, eosio::name username, uint64_t exchange_id, uint64_t units) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change -> username == username, "У вас нет прав на редактирование данной заявки");
eosio::check(change -> parent_id == 0, "Нельзя отредактировать количество единиц во встречной заявке. Отмените её и пересоздайте");
exchange.modify(change, _marketplace, [&](auto &c){
c.remain_units += units;
c.supplier_amount = (change -> remain_units + units) * change -> unit_cost;
});
};
@@ -1,32 +0,0 @@
/**
\ingroup public_actions
\brief Авторизация обмена советом.
@details Метод используется для подтверждения согласия совета на заявленный обмен.
Обычно этот метод вызывается после прохождения определенного процесса голосования или принятия решения советом.
Авторизованный обмен считается утвержденным и может быть выполнен.
@param exchange_id Идентификатор заявки на обмен, которую следует авторизовать.
@note Авторизация требуется от аккаунта: @p _soviet
*/
[[eosio::action]] void marketplace::authorize(eosio::name coopname, uint64_t exchange_id, uint64_t contribution_product_decision_id, document2 contribution_product_authorization, uint64_t return_product_decision_id, document2 return_product_authorization) {
require_auth(_soviet);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Ордер не найден");
exchange.modify(change, _soviet, [&](auto &o) {
o.status = "authorized"_n;
o.contribution_product_decision_id = contribution_product_decision_id;
o.contribution_product_authorization = contribution_product_authorization;
o.return_product_decision_id = return_product_decision_id;
o.return_product_authorization = return_product_authorization;
});
};
@@ -1,27 +0,0 @@
/**
\ingroup public_actions
\brief Отмена заявки и возврат токенов.
@details Позволяет пользователю отменить родительскую или дочернюю заявку, а также обеспечивает возврат токенов владельцу (если применимо). При отмене проверяется наличие заявки и её текущий статус.
@param username Имя пользователя, инициировавшего отмену.
@param exchange_id Идентификатор заявки для отмены.
@note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::cancel(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
//TODO перенести под проверку пользователя и вообще сценарий отмены проверить полностью
eosio::check(change -> status != "accepted"_n, "Заявка не может быть отменена сейчас");
if (change -> parent_id == 0) {
marketplace::cancel_parent(coopname, username, exchange_id);
} else {
marketplace::cancel_child(coopname, username, exchange_id);
};
}
@@ -1,325 +0,0 @@
// /**
// * @mainpage Описание системы заявок
// *
// * В системе имеются два типа заявок: order (заказ) и offer (предложение), а также два уровня заявок: родительская (base) и встречная (quote).
// * Используется одна таблица заявок: exchange(_marketplace, _marketplace). Встречная заявка всегда должна быть противополжного типа к родительской.
// *
// * <b>Группы заявок:</b>
// *
// * <b>1. Группа предложений от родителя:</b>
// * - Parent: type == 'offer' && (parent_id == 0) => имущественный паевый взнос
// * - Child: type == 'order' && (parent_id > 0) => денежный паевый взнос
// *
// * <b>2. Группа заказа от родителя:</b>
// * - Parent: type == "order" && (parent_id == 0) => денежный паевый взнос
// * - Child: type == "offer" && (parent_id > 0) => имущественный паевый взнос
// *
// * <b>Обозначения:</b>
// * - order => денежный паевый взнос
// * - offer => имущественный паевый взнос
// */
/**
* @brief Создание заявки на обмен
*
* @param type Тип заявки
* @param params Параметры заявки
*
* Общая функция для создания как родительских, так и дочерних заявок.
*/
void marketplace::create (eosio::name type, const exchange_params& params) {
cooperatives2_index coops(_registrator, _registrator.value);
auto coop = coops.find(params.coopname.value);
eosio::check(coop != coops.end() && coop -> is_coop(), "Кооператив не найден");
eosio::check(params.unit_cost.symbol == coop -> initial.symbol, "Неверный символ токен");
eosio::check(params.units > 0, "Количество единиц в заявке должно быть больше нуля");
eosio::check(params.unit_cost.amount >= 0, "Цена не может быть отрицательной");
if (params.parent_id == 0) {
marketplace::create_parent(type, params);
} else {
marketplace::create_child(type, params);
};
};
/**
* @brief Создание родительской заявки
*
* @param type Тип заявки
* @param params Параметры заявки
*
* Специализированная функция для создания родительской заявки.
*/
void marketplace::create_parent(eosio::name type, const exchange_params& params) {
eosio::check(type == "offer"_n, "В родительском заявке может быть только предложение");
requests_index exchange(_marketplace, params.coopname.value);
uint64_t id = get_global_id(_marketplace, "exchange"_n);
eosio::check(params.parent_id == 0, "Родительская заявка создаётся без указания родителя");
cooperatives2_index coops(_registrator, _registrator.value);
auto coop = coops.find(params.coopname.value);
eosio::check(coop != coops.end(), "Кооператив не найден");
eosio::check(coop -> is_coop() == true, "Организация - не кооператив");
participants_index participants(_soviet, params.coopname.value);
auto participant = participants.find(params.username.value);
eosio::check(participant != participants.end(), "Вы не являетесь членом указанного кооператива");
auto program = get_program_or_fail(params.coopname, params.program_id);
//срок гарантийного возврата должен быть установлен
eosio::check(params.product_lifecycle_secs > 0, "Гарантийный срок возврата для имущества должен быть установлен");
exchange.emplace(_marketplace, [&](auto &i) {
i.id = id;
i.type = type;
i.program_id = params.program_id;
i.username = params.username;
i.coopname = params.coopname;
i.status = "moderation"_n;
i.remain_units = params.units;
i.unit_cost = params.unit_cost;
i.supplier_amount = params.unit_cost * params.units;
i.membership_fee = asset(0, coop -> initial.symbol);
i.total_cost = asset(0, coop -> initial.symbol);
i.product_lifecycle_secs = params.product_lifecycle_secs;
i.data = params.data;
i.meta = params.meta;
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
i.cancellation_fee_amount = asset(0, params.unit_cost.symbol);
});
action(
permission_level{ _marketplace, "active"_n},
_marketplace,
"newid"_n,
std::make_tuple(id, type)
).send();
};
/**
* @brief Создание дочерней заявки
*
* @param type Тип заявки
* @param params Параметры заявки
*
* Специализированная функция для создания дочерних заявок, связанных с родительской заявкой.
*/
void marketplace::create_child(eosio::name type, const exchange_params& params) {
eosio::check(type == "order"_n, "В дочерней заявки может быть только заказ");
requests_index exchange(_marketplace, params.coopname.value);
auto parent_change = exchange.find(params.parent_id);
eosio::check(parent_change != exchange.end(), "Заявка не обнаружена");
eosio::check(parent_change -> status == "published"_n, "Заявка не опубликована или не прошла модерацию");
cooperatives2_index coops(_registrator, _registrator.value);
auto coop = coops.find(params.coopname.value);
eosio::check(coop != coops.end(), "Кооператив не найден");
eosio::check(coop -> is_coop() == true, "Организация - не кооператив");
eosio::check(parent_change -> unit_cost.amount == params.unit_cost.amount, "Торги запрещены");
eosio::check(params.parent_id > 0, "Встречная заявка создаётся с указанием родителя");
eosio::check(params.document.has_value(), "Документ должен быть приложен к транзакции");
//проводим проверку подписи документа
verify_document_or_fail(*params.document);
uint64_t id = get_global_id(_marketplace, "exchange"_n);
auto program = get_program_or_fail(params.coopname, params.program_id);
eosio::check(parent_change -> program_id == params.program_id, "Целевые программы должны совпадать");
participants_index participants(_soviet, params.coopname.value);
auto participant = participants.find(params.username.value);
eosio::check(participant != participants.end(), "Вы не являетесь членом указанного кооператива");
uint64_t product_lifecycle_secs = 0;
eosio::asset membership_fee;
eosio::asset supplier_amount = params.unit_cost * params.units;
if (program.calculation_type == "absolute"_n) {
membership_fee = program.fixed_membership_contribution;
} else if (program.calculation_type == "relative"_n) {
membership_fee = supplier_amount * HUNDR_PERCENTS / program.membership_percent_fee;
} else if (program.calculation_type == "free"_n) {
membership_fee = asset(0, _root_govern_symbol);
};
eosio::asset total_cost = params.unit_cost * params.units + membership_fee;
//Специальные проверки
if (type == "offer"_n) {
//родительская заявка должна быть противоположного типа
eosio::check(parent_change -> type == "order"_n, "Неверный тип родительской заявки");
} else if(type == "order"_n) {
//родительская заявка должна быть противоположного типа
eosio::check(parent_change -> type == "offer"_n, "Неверный тип родительской заявки");
std::string memo = "Начало поставки по программе №" + std::to_string(params.program_id) + " с ID: " + std::to_string(id);
//Для блокировки средств необходимо их иметь на ЦПП, т.е. предварительно необходимо сконвертировать их с ЦПП кошелька
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"blockbal"_n,
std::make_tuple(params.coopname, params.username, params.program_id, total_cost, memo)
).send();
}
exchange.emplace(_marketplace, [&](auto &i) {
i.id = id;
i.parent_id = params.parent_id;
i.parent_username = parent_change -> username;
i.type = type;
i.program_id = parent_change -> program_id;
i.coopname = params.coopname;
i.username = params.username;
i.status = "published"_n;
i.remain_units = params.units;
i.unit_cost = params.unit_cost;
i.membership_fee = membership_fee;
i.supplier_amount = supplier_amount;
i.total_cost = total_cost;
// не дублируем информацию
// i.data = params.data;
// i.meta = params.meta;
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
i.cancellation_fee_amount = asset(0, params.unit_cost.symbol);
if (type == "order"_n) {
print("on create child order");
i.return_product_statement = *params.document;
i.money_contributor = params.username;
i.product_contributor = parent_change -> username;
i.product_lifecycle_secs = parent_change -> product_lifecycle_secs;
} else if (type == "offer"_n) {
print("on create child offer");
i.contribute_product_statement = *params.document;
i.money_contributor = parent_change -> username;
i.product_contributor = params.username;
i.product_lifecycle_secs = params.product_lifecycle_secs;
};
});
action(
permission_level{ _marketplace, "active"_n},
_marketplace,
"newid"_n,
std::make_tuple(id, type)
).send();
};
/**
* @brief Отмена родительской заявки.
*
* Вызывается из `cancel`, если заявка является родительской.
* Выполняется проверка, что заявка не имеет заблокированных единиц товара, и удаляется из хранилища.
*
* @param username Имя пользователя, осуществляющего отмену заявки.
* @param exchange_id ID родительской заявки, которую нужно отменить.
*/
void marketplace::cancel_parent(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
//Удаление, если заблокированных объектов на поставке - нет.
eosio::check(change -> remain_units + change -> blocked_units == 0, "Заявка не может быть отменена из-за наличия заблокированных единиц товара");
exchange.erase(change);
};
/**
* @brief Отмена дочерней заявки.
*
* Вызывается из `cancel`, если заявка является дочерней.
* Обновляет количество оставшихся и заблокированных единиц товара в родительской заявке и удаляет дочернюю заявку из хранилища.
* В зависимости от статуса и типа заявки возможен возврат токенов "покупателю".
*
* @param username Имя пользователя, осуществляющего отмену заявки.
* @param exchange_id ID дочерней заявки, которую нужно отменить.
*/
void marketplace::cancel_child(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
auto parent_change = exchange.find(change -> parent_id);
eosio::asset quantity = change -> unit_cost * change -> blocked_units;
// оповещаем совет об отмене и разблокируем средства
if (change -> type == "order"_n) {
std::string memo = "Отмена поставки по программе №" + std::to_string(change -> program_id) + " с ID: " + std::to_string(change -> id);
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"unblockbal"_n,
std::make_tuple(coopname, change -> money_contributor, change -> program_id, change -> total_cost, memo)
).send();
};
if (change -> status == "authorized"_n) {
//возвращаем единицы товара в родительскую заявку
exchange.modify(parent_change, _marketplace, [&](auto &e) {
e.remain_units += change -> blocked_units;
e.blocked_units -= change -> blocked_units;
e.supplier_amount += change -> supplier_amount;
});
exchange.modify(change, _marketplace, [&](auto &c){
c.status = "canceled"_n;
c.canceled_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
//удаляем дочернюю заявку
// exchange.erase(change);
} else if (change -> status == "published"_n) {
exchange.modify(change, _marketplace, [&](auto &c){
c.status = "canceled"_n;
c.canceled_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
//удаляем дочернюю заявку
// exchange.erase(change);
} else {
//TODO здесь должно быть допустимо, но для каждого статуса по-своему
eosio::check(false, "Заявка находится в недопустимом статусе для отмены");
}
}
@@ -1,91 +0,0 @@
/**
\ingroup public_actions
\brief Подписание акта о приёме-передаче имущества.
* @details После успешного получения товара, получатель подписывает акт о приёме-передаче, что свидетельствует о юридическом завершении сделки. Этот акт делает пакет документов по данной сделке полным. После проведения ряда проверок, обновляются статусы и количество объектов в основной заявке и предложении. Если все объекты основной заявки обработаны, заявка удаляется из публикации. В зависимости от типа предложения, может осуществляться перевод токенов.
* @param username Имя пользователя-получателя товара.
* @param exchange_id ID предложения, под которым следует подписать акт.
* @note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::complete(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> parent_id > 0, "У указанной заявки нет встречной заявки");
auto parent_change = exchange.find(change -> parent_id);
eosio::check(parent_change != exchange.end(), "Родительская заявка не найдена");
// eosio::check(change -> username == username, "Вы не можете подтвердить исполнение заявки");
eosio::check(change -> status == "recieved2"_n, "Заявка находится в неверном статусе для утверждения обмена");
eosio::check(change -> warranty_delay_until.sec_since_epoch() < eosio::current_time_point().sec_since_epoch(), "Время гарантийной задержки еще не истекло");
exchange.modify(parent_change, _marketplace, [&](auto &i) {
i.delivered_units += change -> blocked_units;
i.blocked_units -= change -> blocked_units;
if (i.blocked_units + parent_change -> remain_units == 0) {
i.status = "unpublished"_n; //снимаем родительскую заявку с публикации, если она исполнена
};
});
exchange.modify(change, _marketplace, [&](auto &o) {
o.status = "completed"_n;
o.delivered_units += change -> blocked_units;
o.blocked_units = 0;
o.completed_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
auto program = get_program_or_fail(coopname, change -> program_id);
std::string memo = "Успешное завершение поставки имущества по программе №" + std::to_string(change -> program_id) + " с ID: " + std::to_string(change -> id);
//Заказчику разблокируем баланс ЦПП кооплекса и списываем его
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"unblockbal"_n,
std::make_tuple(coopname, change -> money_contributor, change -> program_id, change -> total_cost, memo)
).send();
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"subbal"_n,
std::make_tuple(coopname, change -> money_contributor, change -> program_id, change -> total_cost, false, memo)
).send();
//Поставщику разблокируем средства в программе кооплейса
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"unblockbal"_n,
std::make_tuple(coopname, change -> product_contributor, change -> program_id, change -> supplier_amount, memo)
).send();
if (change -> membership_fee.amount > 0) {
//распределяем членские взносы по фондам
action(
permission_level{ _marketplace, "active"_n},
_fund,
"spreadamount"_n,
std::make_tuple(coopname, change -> membership_fee)
).send();
// отмечаем распределение членских взносов в программе и кошельке пользователя
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"addmemberfee"_n,
std::make_tuple(coopname, change -> money_contributor, change -> program_id, change -> membership_fee, memo)
).send();
}
}
@@ -1,48 +0,0 @@
/**
\ingroup public_actions
\brief Отказ от предложения.
* @details Этот метод позволяет пользователю отклонить предложение, представленное к его заявке.
* Выполняются следующие проверки:
* - Существование предложения с указанным ID.
* - Существование основной заявки.
* - Предложение находится в статусе "ожидание".
*
* Если отклонено предложение к заявке типа "order", осуществляется возврат токенов пользователю, которому были заблокированы токены при создании предложения.
*
* @param username Имя пользователя, отклоняющего предложение.
* @param exchange_id ID предложения, которое следует отклонить.
* @param meta Дополнительные метаданные, связанные с отказом.
*
* @note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::decline(eosio::name coopname, eosio::name username, uint64_t exchange_id, std::string meta) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
auto parent_change = exchange.find(change -> parent_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(parent_change != exchange.end(), "Родительская заявка не найдена");
eosio::check(change -> status == "published"_n, "Только заявка в статусе ожидания может быть отклонена");
exchange.modify(change, coopname, [&](auto &o){
o.status = "declined"_n;
o.declined_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
o.meta = meta;
});
if (change -> type == "order"_n) {
std::string memo = "Отказ в поставке по программе №" + std::to_string(change -> program_id) + " с ID: " + std::to_string(change -> id);
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"unblockbal"_n,
std::make_tuple(coopname, change -> money_contributor, change -> program_id, change -> total_cost, memo)
).send();
};
}
@@ -1,86 +0,0 @@
/**
\ingroup public_actions
\brief Принятие заявки поставщиком.
@details Поставщик принимает заявку orderoffer на поставку имущества и предоставляет необходимые документы.
@param coopname Имя кооператива
@param supplier_braname Имя кооперативного участка поставщика
@param username Имя поставщика
@param request_hash Хэш заявки
@param convert_out Заявление на конвертацию
@param product_contribution_statement Заявление на имущественный паевой взнос
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::accept(eosio::name coopname, eosio::name supplier_braname, eosio::name username, checksum256 request_hash, document2 convert_out, document2 product_contribution_statement) {
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "active"_n, "Только активная заявка может быть принята");
eosio::check(change.type == "orderoffer"_n, "Метод accept применим только к заявкам типа orderoffer");
// Проверяем существование кооперативного участка поставщика
get_branch_or_fail(coopname, supplier_braname);
// Проверяем подписи документов
verify_document_or_fail(convert_out);
verify_document_or_fail(product_contribution_statement);
// Валидируем документы по registry_id (пока что нули)
Document::validate_registry_id(convert_out, 0);
Document::validate_registry_id(product_contribution_statement, 0);
// Получаем первоначальный документ возврата из заявки по имени
document2 initial_return_statement;
bool found = Document::find_document(change.documents, DocumentNames::RETURN_STMT, initial_return_statement);
eosio::check(found, "В заявке отсутствует заявление на возврат имущества");
// Обновляем заявку
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.status = "accepted"_n;
o.accepted_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
o.product_contributor = username;
o.supplier_braname = supplier_braname;
// Добавляем новые документы с именами
Document::add_document(o.documents, DocumentNames::CONVERT_TO, convert_out);
Document::add_document(o.documents, DocumentNames::CONTRIB_STMT, product_contribution_statement);
});
// Используем хэш заявки как идентификатор пакета решений
checksum256 agenda_hash = change.hash;
// Отправляем ПЕРВЫЙ вопрос в совет - по заявлению на имущественный паевой взнос
::Soviet::create_agenda(
_marketplace,
coopname,
username,
get_valid_soviet_action("authcontrib"_n), // авторизация взноса имуществом
agenda_hash,
_marketplace,
Marketplace::get_valid_marketplace_action("authcontrib"_n),
"declineacc"_n,
product_contribution_statement,
std::string("")
);
// Отправляем ВТОРОЙ вопрос в совет - по заявлению на возврат имущества
::Soviet::create_agenda(
_marketplace,
coopname,
username,
get_valid_soviet_action("authreturn"_n), // авторизация возврата имуществом
agenda_hash,
_marketplace,
Marketplace::get_valid_marketplace_action("authreturn"_n),
"declineacc"_n,
initial_return_statement,
std::string("")
);
};
@@ -1,45 +0,0 @@
/**
\ingroup public_actions
\brief Авторизация заявления на имущественный паевой взнос советом кооператива.
@details Совет кооператива авторизует заявление на имущественный паевой взнос.
@param coopname Имя кооператива
@param request_hash Хэш заявки
@param authorization Документ авторизации от совета
@note Авторизация требуется от аккаунта: @p _soviet
**/
[[eosio::action]] void marketplace::authcontrib(eosio::name coopname, checksum256 request_hash, document2 authorization) {
require_auth(_soviet);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "accepted"_n, "Только принятая заявка может быть авторизована");
// Проверяем подпись документа
verify_document_or_fail(authorization);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(authorization, 0);
// Обновляем заявку
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
// Добавляем документ авторизации взноса с именем
Document::add_document(o.documents, DocumentNames::CONTRIB_AUTH, authorization);
// Проверяем, есть ли уже оба документа авторизации
bool has_contrib_auth = Document::has_document(o.documents, DocumentNames::CONTRIB_AUTH);
bool has_return_auth = Document::has_document(o.documents, DocumentNames::RETURN_AUTH);
// Если получили оба документа авторизации, переводим в статус authorized
if (has_contrib_auth && has_return_auth) {
o.status = "authorized"_n;
}
});
};
@@ -1,45 +0,0 @@
/**
\ingroup public_actions
\brief Авторизация заявления на возврат имущества советом кооператива.
@details Совет кооператива авторизует заявление на возврат имущества.
@param coopname Имя кооператива
@param request_hash Хэш заявки
@param authorization Документ авторизации от совета
@note Авторизация требуется от аккаунта: @p _soviet
**/
[[eosio::action]] void marketplace::authreturn(eosio::name coopname, checksum256 request_hash, document2 authorization) {
require_auth(_soviet);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "accepted"_n, "Только принятая заявка может быть авторизована");
// Проверяем подпись документа
verify_document_or_fail(authorization);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(authorization, 0);
// Обновляем заявку
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
// Добавляем документ авторизации возврата с именем
Document::add_document(o.documents, DocumentNames::RETURN_AUTH, authorization);
// Проверяем, есть ли уже оба документа авторизации
bool has_contrib_auth = Document::has_document(o.documents, DocumentNames::CONTRIB_AUTH);
bool has_return_auth = Document::has_document(o.documents, DocumentNames::RETURN_AUTH);
// Если получили оба документа авторизации, переводим в статус authorized
if (has_contrib_auth && has_return_auth) {
o.status = "authorized"_n;
}
});
};
@@ -1,61 +0,0 @@
/**
\ingroup public_actions
\brief Отмена заявки пользователем.
@details Пользователь может отменить свою заявку с учетом комиссии за отмену.
@param coopname Имя кооператива
@param username Имя пользователя
@param request_hash Хэш заявки
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::cancel(eosio::name coopname, eosio::name username, checksum256 request_hash) {
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
// Проверяем права доступа
eosio::check(change.username == username || change.product_contributor == username, "Недостаточно прав доступа");
// Ограничения отмены
eosio::check(change.status != "completed"_n, "Нельзя отменить завершенную заявку");
eosio::check(change.status != "canceled"_n, "Заявка уже отменена");
eosio::check(change.status != "declined"_n, "Нельзя отменить отклоненную заявку");
// Поставщик не может отменить заявку после поставки
if ((change.money_contributor != username && change.product_contributor != username) ||
(change.product_contributor == username && (change.status == "supplied1"_n || change.status == "supplied2"_n ||
change.status == "delivered"_n || change.status == "received1"_n || change.status == "received2"_n))) {
eosio::check(false, "Поставщик не может отменить заявку после поставки");
}
std::string memo = "Отмена заявки №" + std::to_string(change.id);
// Возвращаем средства заказчику с учетом комиссии за отмену
if (username == change.money_contributor) {
eosio::asset return_amount = change.total_cost - change.cancellation_fee_amount;
// Списываем заблокированные средства
Wallet::sub_blocked_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
// Возвращаем средства в кошелёк
Wallet::add_available_funds(_marketplace, coopname, change.money_contributor, return_amount, _wallet_program, memo);
// Направляем комиссию в фонд членских взносов
if (change.cancellation_fee_amount.amount > 0) {
// Wallet::add_member_fee(_marketplace, coopname, change.username, _marketplace_program_id, change.cancellation_fee_amount, memo);
}
}
// Удаляем заявку из системы
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для удаления");
requests.erase(change_itr);
};
@@ -1,40 +0,0 @@
/**
\ingroup public_actions
\brief Завершение поставки.
@details После истечения гарантийной задержки происходит завершение поставки.
@param coopname Имя кооператива
@param username Имя пользователя
@param request_hash Хэш заявки
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::complete(eosio::name coopname, eosio::name username, checksum256 request_hash) {
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "received"_n, "Завершение возможно только после статуса received");
eosio::check(eosio::time_point_sec(eosio::current_time_point()) >= change.warranty_delay_until, "Гарантийная задержка ещё не истекла");
// Проводки по кошельку
std::string memo = "Завершение поставки для заказа №" + std::to_string(change.id);
// Поставщик - списываем заблокированный баланс и начисляем в кошелёк
Wallet::sub_blocked_funds(_marketplace, coopname, change.product_contributor, change.base_cost, _marketplace_program, memo);
Wallet::add_available_funds(_marketplace, coopname, change.product_contributor, change.base_cost, _wallet_program, memo);
// Членские взносы (если больше нуля)
if (change.membership_fee_amount.amount > 0) {
// Wallet::add_member_fee(_marketplace, coopname, change.username, _marketplace_program_id, change.membership_fee_amount, memo);
}
// Удаляем заявку из системы
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для удаления");
requests.erase(change_itr);
};
@@ -1,39 +0,0 @@
/**
\ingroup public_actions
\brief Отклонение заявки.
@details Отклонение заявки с указанием причины.
@param coopname Имя кооператива
@param username Имя пользователя
@param request_hash Хэш заявки
@param meta Причина отклонения
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::decline(eosio::name coopname, eosio::name username, checksum256 request_hash, std::string meta) {
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "active"_n || change.status == "accepted"_n, "Можно отклонить только активную или принятую заявку");
std::string memo = "Отклонение заявки №" + std::to_string(change.id) + ": " + meta;
// Возвращаем средства заказчику если они были заблокированы
if (change.status == "active"_n && change.money_contributor != ""_n) {
// Списываем заблокированные средства
Wallet::sub_blocked_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
// Возвращаем средства в кошелёк
Wallet::add_available_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _wallet_program, memo);
}
// Удаляем заявку из системы
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для удаления");
requests.erase(change_itr);
};
@@ -1,62 +0,0 @@
/**
\ingroup public_actions
\brief Отклонение принятия заявки советом (declineacc).
@details Данный метод вызывается советом когда заявление на конвертацию, возврат или взнос отклоняется.
После рефакторинга работаем только с одной заявкой orderoffer.
@param coopname Имя кооператива
@param request_hash Хэш заявки, которая должна быть отменена
@param reason Причина отклонения
@note Авторизация требуется от аккаунта: @p _soviet
*/
[[eosio::action]] void marketplace::declineacc(eosio::name coopname, checksum256 request_hash, std::string reason) {
require_auth(_soviet);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
// Если заявка не найдена, ничего не делаем (возможно уже удалена)
if (!change_opt.has_value()) {
print("Заявка уже удалена или не найдена. Кооператив: ", coopname, ", причина: ", reason);
return;
}
auto change = change_opt.value();
// Логирование отклонения
print("Отклонение заявления советом. Заявка ID: ", change.id, ", причина: ", reason);
// Отклоняем заявку
decline_request(coopname, change);
}
/**
* @brief Статический метод для отклонения заявки (используется советом)
*/
void marketplace::decline_request(eosio::name coopname, const request& change) {
requests_index requests(_marketplace, coopname.value);
// Проверяем, что заявка все еще существует
auto change_itr = requests.find(change.id);
if (change_itr == requests.end()) {
return; // Заявка уже удалена
}
std::string memo = "Отклонение заявления советом по программе №" + std::to_string(_marketplace_program_id) + " для заявки ID: " + std::to_string(change.id);
// Возвращаем средства заказчику если они были заблокированы
if (change.type == "orderoffer"_n && change.total_cost.amount > 0 && change.money_contributor != ""_n) {
// Списываем заблокированные средства с маркетплейса
Wallet::sub_blocked_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
// Возвращаем средства в кошелёк
Wallet::add_available_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _wallet_program, memo);
}
// Удаляем заявку из системы
requests.erase(change_itr);
}
@@ -1,35 +0,0 @@
/**
\ingroup public_actions
\brief Перевод заявки в статус готово к выдаче.
@details Председатель КУ переводит заявку в статус delivered независимо от транспортировки.
Используется когда транспортировка между КУ не нужна или имущество уже находится на складе.
Просто перевод статуса без дополнительных документов.
@param coopname Имя кооператива
@param username Имя пользователя (может быть поставщиком или заказчиком)
@param request_hash Хэш заявки
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::delivered(eosio::name coopname, eosio::name username, checksum256 request_hash) {
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
// Проверяем что заявка в подходящем статусе (после поставки или перемещения со склада)
eosio::check(change.status == "supplied2"_n || change.status == "shiprecvd"_n, "Перевод в статус delivered возможен только после поставки (supplied2) или после получения на склад (shiprecvd)");
// Обновляем заявку - просто перевод статуса
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.status = "delivered"_n;
o.delivered_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
// Устанавливаем крайний срок получения (3 дня)
o.deadline_for_receipt = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch() + 3 * 24 * 60 * 60);
});
};
@@ -1,110 +0,0 @@
/**
\ingroup public_actions
\brief Создать заявку orderoffer - заказчик создает заявку на поставку товара от поставщика.
*
* Данный метод позволяет заказчику создать заявку на поставку товара от поставщика.
* Заявка содержит всю информацию о товаре, стоимости, документах и сразу блокирует средства заказчика.
*
* @param coopname Имя кооператива
* @param receiver_braname Имя кооперативного участка заказчика для получения товара
* @param username Имя заказчика
* @param hash Хэш заявки (уникальный идентификатор)
* @param units Количество единиц товара
* @param unit_cost Цена за единицу товара
* @param product_lifecycle_secs Время жизни продукта
* @param warranty_period_secs Гарантийный срок в секундах
* @param membership_fee_amount Сумма членского взноса
* @param cancellation_fee_amount Сумма комиссии за отмену заявки
* @param product_return_statement Заявление на возврат паевого взноса имуществом
* @param convert_in Заявление на конвертацию из кошелька в маркетплейс
* @param meta Метаданные о заявке
*
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void marketplace::orderoffer(eosio::name coopname, eosio::name receiver_braname, eosio::name username, checksum256 hash, uint64_t units, eosio::asset unit_cost, uint32_t product_lifecycle_secs, uint32_t warranty_period_secs, eosio::asset membership_fee_amount, eosio::asset cancellation_fee_amount, document2 product_return_statement, document2 convert_in, std::string meta) {
require_auth(coopname);
// Проверяем, что заявка с таким хэшем не существует
auto existing_request = get_request_by_hash(coopname, hash);
eosio::check(!existing_request.has_value(), "Заявка с таким хэшем уже существует");
// Проверяем, что символ токена совпадает с символом токена кооператива
auto coop = get_cooperative_or_fail(coopname);
eosio::check(unit_cost.symbol == coop.initial.symbol, "Неверный символ токена");
eosio::check(membership_fee_amount.symbol == coop.initial.symbol, "Неверный символ токена для членского взноса");
eosio::check(cancellation_fee_amount.symbol == coop.initial.symbol, "Неверный символ токена для комиссии отмены");
eosio::check(units > 0, "Количество единиц в заявке должно быть больше нуля");
eosio::check(unit_cost.amount >= 0, "Цена не может быть отрицательной");
eosio::check(membership_fee_amount.amount >= 0, "Членский взнос не может быть отрицательным");
eosio::check(cancellation_fee_amount.amount >= 0, "Комиссия за отмену не может быть отрицательной");
// Проверяем, что пользователь является пайщиком кооператива
get_participant_or_fail(coopname, username);
// Проверяем существование кооперативного участка заказчика
get_branch_or_fail(coopname, receiver_braname);
// Проверяем существование программы маркетплейса
auto program = get_program_or_fail(coopname, _marketplace_program_id);
// Гарантийный срок возврата должен быть установлен
eosio::check(product_lifecycle_secs > 0, "Гарантийный срок возврата для имущества должен быть установлен");
eosio::check(warranty_period_secs > 0, "Гарантийный срок должен быть больше нуля");
// Проводим проверку подписи документов
verify_document_or_fail(product_return_statement);
verify_document_or_fail(convert_in);
// Валидируем документы по registry_id (пока что нули как просил пользователь)
Document::validate_registry_id(product_return_statement, 0);
Document::validate_registry_id(convert_in, 0);
// Рассчитываем стоимость
eosio::asset base_cost = unit_cost * units;
eosio::asset total_cost = base_cost + membership_fee_amount;
// Проверяем что комиссия за отмену не превышает общую стоимость
eosio::check(cancellation_fee_amount <= total_cost, "Комиссия за отмену не может превышать общую стоимость заявки");
requests_index requests(_marketplace, coopname.value);
uint64_t request_id = get_global_id(_marketplace, "requests"_n);
std::string memo = "Начало поставки по программе №" + std::to_string(_marketplace_program_id) + " с ID: " + std::to_string(request_id);
// Создаем вектор именованных документов
std::vector<Document::named_document> documents;
Document::add_document(documents, DocumentNames::RETURN_STMT, product_return_statement);
Document::add_document(documents, DocumentNames::CONVERT_FROM, convert_in);
requests.emplace(_marketplace, [&](auto &i) {
i.id = request_id;
i.hash = hash;
i.type = "orderoffer"_n;
i.username = username;
i.coopname = coopname;
i.status = "active"_n;
i.units = units;
i.unit_cost = unit_cost;
i.base_cost = base_cost;
i.membership_fee_amount = membership_fee_amount;
i.total_cost = total_cost;
i.product_lifecycle_secs = product_lifecycle_secs;
i.warranty_period_secs = warranty_period_secs;
i.money_contributor = username;
i.meta = meta;
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
i.cancellation_fee_amount = cancellation_fee_amount;
i.receiver_braname = receiver_braname;
i.documents = documents;
});
// Конвертируем и блокируем средства заказчика
std::string convert_memo = "Конвертация средств из ЦПП 'Цифровой Кошелёк' в ЦПП 'Маркетплейс' для заказа №" + std::to_string(request_id);
Wallet::sub_available_funds(_marketplace, coopname, username, total_cost, _wallet_program, convert_memo);
// Добавляем средства на ЦПП маркетплейса
Wallet::add_available_funds(_marketplace, coopname, username, total_cost, _marketplace_program, convert_memo);
// Блокируем средства на программе маркетплейса
Wallet::block_funds(_marketplace, coopname, username, total_cost, _marketplace_program, memo);
};
@@ -1,39 +0,0 @@
/**
\ingroup public_actions
\brief Получение товара заказчиком.
@details Заказчик приходит на КУ для получения имущества. Председатель КУ подписывает акт и передаёт имущество заказчику.
@param coopname Имя кооператива
@param username Имя заказчика
@param request_hash Хэш заявки
@param document Акт получения имущества
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::receive(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document) {
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "delivered"_n, "Получение возможно только после статуса delivered");
eosio::check(change.money_contributor == username, "Недостаточно прав доступа");
// Проверяем подпись документа
verify_document_or_fail(document);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(document, 0);
// Обновляем заявку
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.status = "received1"_n;
// Добавляем акт получения с именем
Document::add_document(o.documents, DocumentNames::RECEIVE_ACT, document);
});
};
@@ -1,51 +0,0 @@
/**
\ingroup public_actions
\brief Подтверждение получения заказчиком.
@details Заказчик подтверждает факт получения имущества второй подписью акта приёма-передачи.
@param coopname Имя кооператива
@param username Имя заказчика
@param request_hash Хэш заявки
@param document Акт подтверждения получения
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::receivecnf(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document) {
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "received1"_n, "Подтверждение возможно только после статуса received1");
eosio::check(change.money_contributor == username, "Недостаточно прав доступа");
// Проверяем подпись документа
verify_document_or_fail(document);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(document, 0);
// Обновляем заявку
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.status = "received2"_n;
o.received_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
// Устанавливаем время окончания гарантийной задержки
o.warranty_delay_until = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch() + change.warranty_period_secs);
// Добавляем акт подтверждения с именем
Document::add_document(o.documents, DocumentNames::RECEIVE_ACT_CONF, document);
});
// Проводки по кошельку
std::string memo = "Подтверждение получения для заказа №" + std::to_string(change.id);
// Паевой фонд - уменьшаем циркуляцию через ledger2 (TRANSFER SHARE_FUND → SUPPLIER_PAYMENTS)
Ledger2::apply(_marketplace, coopname, operations::marketplace::CONFIRM_RECEIPT, change.base_cost, username, request_hash, memo);
// Заказчик - списываем заблокированный баланс
Wallet::sub_blocked_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
};
@@ -1,50 +0,0 @@
/**
\ingroup public_actions
\brief Подтверждение поставки председателем КУ.
@details Председатель кооперативного участка или доверенное им лицо подтверждает факт поставки имущества.
@param coopname Имя кооператива
@param username Имя представителя кооператива
@param request_hash Хэш заявки
@param act Акт подтверждения поставки
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::supplcnf(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 act) {
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "supplied1"_n, "Поставка должна быть подтверждена после статуса supplied1");
// Проверяем подпись документа
verify_document_or_fail(act);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(act, 0);
// Обновляем заявку
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.status = "supplied2"_n;
o.warehouse = change.supplier_braname; // Ставим имущество на склад КУ приёма
o.supplied_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
// Добавляем акт подтверждения с именем
Document::add_document(o.documents, DocumentNames::SUPPLY_ACT_CONF, act);
});
// Проводки по кошельку
std::string memo = "Подтверждение поставки для заказа №" + std::to_string(change.id);
// Паевой фонд - увеличиваем циркуляцию через ledger2 (ISSUE в SHARE_FUND)
Ledger2::apply(_marketplace, coopname, operations::marketplace::CONFIRM_SUPPLY, change.total_cost, username, request_hash, memo);
// Поставщик - начисляем и блокируем средства
Wallet::add_available_funds(_marketplace, coopname, change.product_contributor, change.base_cost, _marketplace_program, memo);
Wallet::block_funds(_marketplace, coopname, change.product_contributor, change.base_cost, _marketplace_program, memo);
};
@@ -1,40 +0,0 @@
/**
\ingroup public_actions
\brief Поставка имущества в кооператив.
@details Поставщик поставляет имущество на указанный кооперативный участок и предоставляет подписанный акт приёма-передачи.
@param coopname Имя кооператива
@param username Имя поставщика
@param request_hash Хэш заявки
@param act Акт поставки имущества
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::supply(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 act) {
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "authorized"_n, "Только авторизованная заявка может быть поставлена");
eosio::check(change.product_contributor == username, "Недостаточно прав доступа");
// Проверяем подпись документа
verify_document_or_fail(act);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(act, 0);
// Обновляем заявку
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.status = "supplied1"_n;
o.supplied_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
// Добавляем акт поставки с именем
Document::add_document(o.documents, DocumentNames::SUPPLY_ACT, act);
});
};
@@ -1,23 +0,0 @@
[[eosio::action]] void marketplace::delivered(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> parent_id > 0, "Только продукт по встречной заявке может быть доставлен");
eosio::check(change -> status == "supplied2"_n, "Продукт может быть поставлен только по заявке в статусе supplied2");
auto soviet = get_board_by_type_or_fail(coopname, "soviet"_n);
auto chairman = soviet.get_chairman();
eosio::check(username == chairman, "Недостаточно прав доступа для подтверждения доставки");
auto program = get_program_or_fail(coopname, change -> program_id);
exchange.modify(change, coopname, [&](auto &ch) {
ch.status = "delivered"_n;
ch.delivered_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
ch.deadline_for_receipt = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch() + change -> product_lifecycle_secs / 4);
});
}
@@ -1,113 +0,0 @@
[[eosio::action]] void marketplace::dispute(eosio::name coopname, eosio::name username, uint64_t exchange_id, document2 document){
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change -> type == "order"_n, "Спор может быть открыт только по заявке на поставку");
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> username == username, "Только заказчик может открыть спор");
eosio::check(change -> is_warranty_return == false, "Нельзя открыть спор на спор");
auto parent_change = exchange.find(change -> parent_id);
eosio::check(parent_change != exchange.end(), "Родительская заявка не найдена");
eosio::check(change -> status == "recieved2"_n, "Неверный статус для открытия спора");
exchange.modify(change, _marketplace, [&](auto &e){
e.status = "disputed"_n;
e.disputed_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
e.warranty_return_id = change -> id;
});
uint64_t new_id = get_global_id(_marketplace, "exchange"_n);
uint64_t new_parent_id = get_global_id(_marketplace, "exchange"_n);
auto cooperative = get_cooperative_or_fail(coopname);
// Проверяем подпись документа
verify_document_or_fail(document);
//открытая заявка от заказчика на поставку имущества поставщику
exchange.emplace(_marketplace, [&](auto &i) {
i.id = new_parent_id;
i.type = "offer"_n;
i.program_id = change -> program_id;
i.username = username;
i.parent_username = ""_n;
i.parent_id = 0;
i.coopname = coopname;
i.status = "published"_n;
i.remain_units = change -> remain_units;
i.unit_cost = change -> unit_cost;
i.supplier_amount = i.unit_cost * i.remain_units;
i.total_cost = i.supplier_amount;
i.membership_fee = asset(0, cooperative.initial.symbol);
i.cancellation_fee_amount = asset(0, cooperative.initial.symbol);
i.product_lifecycle_secs = change -> product_lifecycle_secs;
i.data = change -> data;
i.meta = change -> meta;
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
i.is_warranty_return = true;
i.warranty_return_id = change -> id;
});
action(
permission_level{ _marketplace, "active"_n},
_marketplace,
"newid"_n,
std::make_tuple(new_parent_id, "offer")
).send();
//новая встречная заявка для будущего возврата поставщику
exchange.emplace(_marketplace, [&](auto &i) {
i.id = new_id;
i.parent_id = new_parent_id;
i.type = "order"_n;
i.program_id = parent_change -> program_id;
i.coopname = coopname;
i.username = change -> parent_username;
i.parent_username = change -> username;
i.status = "published"_n;
i.remain_units = change -> remain_units;
i.unit_cost = change -> unit_cost;
i.supplier_amount = change -> supplier_amount;
i.total_cost = change -> supplier_amount;;
i.membership_fee = asset(0, cooperative.initial.symbol); //размер членского взноса должен установить председатель в методе resolve
i.cancellation_fee_amount = asset(0, cooperative.initial.symbol);
i.data = change -> data;
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
i.is_warranty_return = true;
i.warranty_return_id = exchange_id;
i.contribute_product_statement = document;
i.money_contributor = parent_change -> username;
i.product_contributor = change -> username;
i.product_lifecycle_secs = 0; //потому что гарантийного срока в споре нет
});
action(
permission_level{ _marketplace, "active"_n},
_marketplace,
"newid"_n,
std::make_tuple(new_id, "order"_n)
).send();
};
@@ -1,59 +0,0 @@
/**
\ingroup public_actions
\brief Открытие гарантийного спора по заявке.
@details Заказчик может открыть спор после получения товара, если есть проблемы с качеством или соответствием.
Создается претензия, которая сохраняется в документах заявки.
@param coopname Имя кооператива
@param username Имя заказчика, открывающего спор
@param request_hash Хэш заявки, по которой открывается спор
@param document Документ с описанием претензии
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::dispute(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document){
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.type == "orderoffer"_n, "Спор может быть открыт только по заявке orderoffer");
eosio::check(change.username == username, "Только заказчик может открыть спор");
eosio::check(change.is_warranty_return == false, "Нельзя открыть спор на спор");
eosio::check(change.status == "received"_n || change.status == "completed"_n, "Спор можно открыть только после получения товара");
// Проверяем подпись документа
verify_document_or_fail(document);
// Обновляем статус заявки на "disputed" и добавляем документ с претензией
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &e){
e.status = "disputed"_n;
e.disputed_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
e.is_warranty_return = true;
Document::add_document(e.documents, Marketplace::DocumentNames::WDISPUTE, document);
});
// Используем хэш заявки как идентификатор пакета решений
checksum256 agenda_hash = change.hash;
// Отправляем заявление на гарантийный возврат в совет
::Soviet::create_agenda(
_marketplace,
coopname,
username,
get_valid_soviet_action("mpwreturn"_n),
agenda_hash,
_marketplace,
Marketplace::get_valid_marketplace_action("wauthorize"_n),
"wdecline"_n,
document,
std::string("")
);
}
@@ -1,48 +0,0 @@
/**
\ingroup public_actions
\brief Принятие или отказ поставщика от товара в рамках гарантийного возврата
@details Поставщик может принять товар (accept=true) или отказаться от него (accept=false).
При принятии товар передается поставщику и диспут завершается.
При отказе товар остается у кооператива и диспут завершается.
@param coopname Имя кооператива
@param username Имя поставщика
@param request_hash Хэш заявки с диспутом
@param accept Принимает ли поставщик товар (true/false)
@param document Документ с решением поставщика
@note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void marketplace::waccept(eosio::name coopname, eosio::name username, checksum256 request_hash, bool accept, document2 document) {
require_auth(coopname);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "woffered"_n, "Товар должен быть предложен поставщику");
eosio::check(change.product_contributor == username, "Только поставщик может принять решение о товаре");
// Проверяем подпись документа
verify_document_or_fail(document);
requests_index requests(_marketplace, coopname.value);
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
if (accept) {
// Поставщик принимает товар
requests.modify(change_itr, _marketplace, [&](auto &ch) {
ch.status = "wcompleted"_n;
Document::add_document(ch.documents, Marketplace::DocumentNames::WACCEPT_ACT, document);
});
} else {
// Поставщик отказывается от товара
requests.modify(change_itr, _marketplace, [&](auto &ch) {
ch.status = "wdeclined"_n;
Document::add_document(ch.documents, Marketplace::DocumentNames::WACCEPT_ACT, document);
});
}
}
@@ -1,34 +0,0 @@
/**
\ingroup public_actions
\brief Авторизация гарантийного возврата советом
@details Совет авторизует принятие товара от заказчика и его последующую выдачу поставщику
@param coopname Имя кооператива
@param request_hash Хэш заявки с диспутом
@param wreturn_decision_id Идентификатор решения по принятию товара
@param wreturn_authorization Документ авторизации принятия товара
@param wsupply_decision_id Идентификатор решения по выдаче товара поставщику
@param wsupply_authorization Документ авторизации выдачи товара
@note Авторизация требуется от аккаунта: @p _soviet
*/
[[eosio::action]] void marketplace::wauthorize(eosio::name coopname, checksum256 request_hash, uint64_t wreturn_decision_id, document2 wreturn_authorization, uint64_t wsupply_decision_id, document2 wsupply_authorization) {
require_auth(_soviet);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "disputed"_n, "Заявка должна быть в статусе диспута");
// Обновляем статус заявки и добавляем документы авторизации
requests_index requests(_marketplace, coopname.value);
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _soviet, [&](auto &o) {
o.status = "wauthorized"_n;
Document::add_document(o.documents, Marketplace::DocumentNames::WRETURN_AUTH, wreturn_authorization);
Document::add_document(o.documents, Marketplace::DocumentNames::WSUPPLY_AUTH, wsupply_authorization);
});
};
@@ -1,40 +0,0 @@
/**
\ingroup public_actions
\brief Предложение товара поставщику в рамках гарантийного возврата
@details Кооператив предлагает поставщику забрать товар, возвращенный заказчиком.
Создается предложение с актом передачи.
@param coopname Имя кооператива
@param username Имя председателя, предлагающего товар
@param request_hash Хэш заявки с диспутом
@param document Акт передачи товара поставщику
@note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void marketplace::woffer(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document) {
require_auth(coopname);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "wreturned"_n, "Товар может быть предложен только после возврата в кооператив");
auto soviet = get_board_by_type_or_fail(coopname, "soviet"_n);
auto chairman = soviet.get_chairman();
eosio::check(username == chairman, "Недостаточно прав доступа для предложения товара");
// Проверяем подпись документа
verify_document_or_fail(document);
// Обновляем статус заявки и добавляем акт предложения товара
requests_index requests(_marketplace, coopname.value);
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &ch) {
ch.status = "woffered"_n;
Document::add_document(ch.documents, Marketplace::DocumentNames::WOFFER_ACT, document);
});
}
@@ -1,40 +0,0 @@
/**
\ingroup public_actions
\brief Возврат товара от заказчика в кооператив
@details Заказчик возвращает товар в кооператив в рамках гарантийного возврата.
Председатель принимает товар и подписывает акт приёма.
@param coopname Имя кооператива
@param username Имя председателя, принимающего товар
@param request_hash Хэш заявки с диспутом
@param document Акт приёма товара от заказчика
@note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void marketplace::wreturn(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document) {
require_auth(coopname);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "wauthorized"_n, "Товар может быть возвращен только по авторизованному диспуту");
auto soviet = get_board_by_type_or_fail(coopname, "soviet"_n);
auto chairman = soviet.get_chairman();
eosio::check(username == chairman, "Недостаточно прав доступа для принятия возврата");
// Проверяем подпись документа
verify_document_or_fail(document);
// Обновляем статус заявки и добавляем акт приёма возврата
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &ch) {
ch.status = "wreturned"_n;
Document::add_document(ch.documents, Marketplace::DocumentNames::WRETURN_ACT, document);
});
}
@@ -1,32 +0,0 @@
/**
\ingroup public_actions
\brief Модерация товара на маркетплейсе.
*
* Данный метод предназначен для модерации товара перед его публикацией на маркетплейсе.
* Метод может быть вызван только администратором маркетплейса.
*
* @param username Имя пользователя-администратора, который вызывает данный метод.
* @param exchange_id Уникальный идентификатор товара, который нужно опубликовать после модерации.
*
* @note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::moderate(eosio::name coopname, eosio::name username, uint64_t exchange_id, uint64_t cancellation_fee) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Объявление не найдено");
if (change -> status == "moderation"_n || change -> status == "prohibit"_n) {
eosio::check(cancellation_fee >= 0 && cancellation_fee < 100, "Комиссия отмены должна быть от 0 до 100 процентов");
eosio::asset cancellation_fee_amount = change -> total_cost * cancellation_fee / 100;
exchange.modify(change, username, [&](auto &o){
o.status = "published"_n;
o.cancellation_fee = cancellation_fee;
o.cancellation_fee_amount = cancellation_fee_amount;
});
}
};
@@ -1,16 +0,0 @@
/**
\ingroup public_actions
\brief Создать заявку на имущественный паевой взнос.
*
* Данный метод позволяет пользователю создать заявку на имущественный паевой взнос в системе.
*
* @param params Параметры для создания заявки на имущественный паевой взнос.
*
* @note Авторизация требуется от аккаунта: @p params.username
*/
[[eosio::action]] void marketplace::offer (const exchange_params& params) {
require_auth(params.coopname);
marketplace::create("offer"_n, params);
};
@@ -1,16 +0,0 @@
/**
\ingroup public_actions
\brief Создать заявку на денежный паевой взнос.
*
* Данный метод позволяет пользователю создать заявку на денежный паевой взнос в системе.
*
* @param params Параметры для создания заявки на денежный паевой взнос.
*
* @note Авторизация требуется от аккаунта: @p params.username
*/
[[eosio::action]] void marketplace::order (const exchange_params& params) {
require_auth(params.coopname);
marketplace::create("order"_n, params);
};
@@ -0,0 +1,55 @@
/**
* @brief Председатель принимает гарантийный возврат на очном осмотре (Story 7.4, p.mkt.return).
*
* Композитная транзакция (атомарно в одной Antelope tx):
* - Ledger2::apply(o.mkt.return, fact_cost, orderer, hash=request.hash) — Дт 91 / Кт 86, ISSUE w.mkt.member.
* - Ledger2::apply(o.mkt.return2, fact_cost, orderer, hash=request.hash) — Дт 10 / Кт 91, NONE.
*
* Compensating forward, не revert (Locked Decision L3 — AR14): новое событие в
* journal с прикладным полем `original_consume_op_id` (заполняется backend'ом
* в submretrn) для трассировки. Исходные o.mkt.consum / o.mkt.consum2 в
* журнале НЕ модифицируются.
*
* Status: approved_for_visit → return_accepted (final). Имущество возвращается
* на склад КУ; средства восстанавливаются на w.mkt.member.available заказчика.
*
* Guards:
* - Подписант (`signer`) авторизован для указанного КУ (`braname`).
* - return_request.status == approved_for_visit.
*
* @ingroup public_marketplace_actions
*/
void marketplace::accretrn(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
document2 decision) {
require_auth(coopname);
auto branch = get_branch_or_fail(coopname, braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен принимать возвраты данного кооперативного участка");
auto r = Marketplace::get_return_request_by_hash_or_fail(coopname, request_hash);
eosio::check(r.status == ReturnStatus::APPROVED_FOR_VISIT,
"Заявление не одобрено для очного осмотра");
if (!is_empty_document(decision)) {
verify_document_or_fail(decision, { signer });
}
// Композитная пара return + return2
Ledger2::apply(_marketplace, coopname,
operations::marketplace::RETURN_BY_MEMBER,
r.fact_cost, r.orderer, r.hash,
Marketplace::Memo::get_return_by_member_memo(r.id, r.original_order_id));
Ledger2::apply(_marketplace, coopname,
operations::marketplace::RETURN_TRANSIT_CLOSE,
r.fact_cost, r.orderer, r.hash,
Marketplace::Memo::get_return_transit_close_memo(r.id, r.original_order_id));
Marketplace::update_return_request(coopname, r.id, [&](auto& upd) {
upd.status = ReturnStatus::RETURN_ACCEPTED;
upd.decision_visit = decision;
});
}
@@ -0,0 +1,38 @@
/**
* @brief Председатель удалённо одобряет очный визит (Story 7.2, p.mkt.return).
*
* Без ledger2-операций. Статус return_request: pending_review → approved_for_visit.
* decision document сохраняется (опциональный — может быть пустой, тогда решение
* фиксируется только статусом + actor + blockchain_actions[at]).
*
* Guards:
* - Подписант (`signer`) авторизован для указанного КУ (`braname`):
* председатель / trustee / trusted в `branches[braname]`.
* - return_request.status == pending_review.
*
* @ingroup public_marketplace_actions
*/
void marketplace::aprretrem(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
document2 decision) {
require_auth(coopname);
auto branch = get_branch_or_fail(coopname, braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен принимать решения по заявлениям данного кооперативного участка");
auto r = Marketplace::get_return_request_by_hash_or_fail(coopname, request_hash);
eosio::check(r.status == ReturnStatus::PENDING_REVIEW,
"Заявление не находится на рассмотрении");
if (!is_empty_document(decision)) {
verify_document_or_fail(decision, { signer });
}
Marketplace::update_return_request(coopname, r.id, [&](auto& upd) {
upd.status = ReturnStatus::APPROVED_FOR_VISIT;
upd.decision_remote = decision;
});
}
@@ -0,0 +1,41 @@
/**
* @brief Председатель удалённо отказывает в гарантийном возврате (Story 7.2, p.mkt.return).
*
* Финальное решение, без ledger2-операций. Статус: pending_review → rejected_remote.
* reason сохраняется в return_request.reason_remote для UI заказчика.
*
* Guards:
* - Подписант (`signer`) авторизован для указанного КУ (`braname`).
* - return_request.status == pending_review.
* - reason.size() > 0.
*
* @ingroup public_marketplace_actions
*/
void marketplace::rejretrem(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
std::string reason,
document2 decision) {
require_auth(coopname);
eosio::check(reason.size() > 0 && reason.size() <= 500,
"Укажите причину отказа (от 1 до 500 символов)");
auto branch = get_branch_or_fail(coopname, braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен принимать решения по заявлениям данного кооперативного участка");
auto r = Marketplace::get_return_request_by_hash_or_fail(coopname, request_hash);
eosio::check(r.status == ReturnStatus::PENDING_REVIEW,
"Заявление не находится на рассмотрении");
if (!is_empty_document(decision)) {
verify_document_or_fail(decision, { signer });
}
Marketplace::update_return_request(coopname, r.id, [&](auto& upd) {
upd.status = ReturnStatus::REJECTED_REMOTE;
upd.decision_remote = decision;
upd.reason_remote = reason;
});
}
@@ -0,0 +1,41 @@
/**
* @brief Председатель отказывает в гарантийном возврате на очном осмотре (Story 7.3, p.mkt.return).
*
* Финальное решение, без ledger2-операций. Статус: approved_for_visit → rejected_at_ku.
* reason сохраняется в return_request.reason_visit для UI заказчика.
*
* Guards:
* - Подписант (`signer`) авторизован для указанного КУ (`braname`).
* - return_request.status == approved_for_visit.
* - reason.size() > 0.
*
* @ingroup public_marketplace_actions
*/
void marketplace::rejretrn(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
std::string reason,
document2 decision) {
require_auth(coopname);
eosio::check(reason.size() > 0 && reason.size() <= 500,
"Укажите причину отказа (от 1 до 500 символов)");
auto branch = get_branch_or_fail(coopname, braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен принимать решения по возвратам данного кооперативного участка");
auto r = Marketplace::get_return_request_by_hash_or_fail(coopname, request_hash);
eosio::check(r.status == ReturnStatus::APPROVED_FOR_VISIT,
"Заявление не одобрено для очного осмотра");
if (!is_empty_document(decision)) {
verify_document_or_fail(decision, { signer });
}
Marketplace::update_return_request(coopname, r.id, [&](auto& upd) {
upd.status = ReturnStatus::REJECTED_AT_KU;
upd.decision_visit = decision;
upd.reason_visit = reason;
});
}
@@ -0,0 +1,85 @@
/**
* @brief Пайщик подаёт заявление на гарантийный возврат (Story 7.1, p.mkt.return).
*
* Без ledger2-операций. Создаётся return_request в pending_review;
* order.return_request_id ставится для двусторонней связи. Привязка к
* конкретному КУ не сохраняется — каждое последующее действие
* (aprretrem/rejretrem/accretrn/rejretrn) принимает `braname` параметром
* и валидирует его через `Branch::is_user_authorized`.
*
* Guards (из p.mkt.return.standard.yaml):
* - actor == original_order.orderer.
* - original_order.status == received.
* - original_order.warranty_until > now() (гарантийный срок не истёк, если
* warranty_period_secs > 0; иначе возврат запрещён).
* - photos.size() > 0 (фото приложены).
* - actual_quantity > 0 && <= original_order.actual_quantity.
* - request_hash уникален.
* - Active возврат на этот order ещё не открыт (idempotency через
* order.return_request_id == 0).
*
* @ingroup public_marketplace_actions
*/
void marketplace::submretrn(eosio::name coopname,
eosio::name orderer,
checksum256 request_hash,
checksum256 original_order_hash,
uint64_t actual_quantity,
std::string reason_text,
std::vector<checksum256> photos,
document2 statement) {
require_auth(coopname);
eosio::check(actual_quantity > 0, "Возвращаемое количество должно быть больше нуля");
eosio::check(!photos.empty(), "Приложите хотя бы одну фотографию товара");
eosio::check(reason_text.size() > 0 && reason_text.size() <= 500,
"Опишите причину возврата (от 1 до 500 символов)");
eosio::check(!Marketplace::get_return_request_by_hash(coopname, request_hash).has_value(),
"Заявление с таким идентификатором уже подано");
auto o = Marketplace::get_order_by_hash_or_fail(coopname, original_order_hash);
eosio::check(o.orderer == orderer,
"Вы не заказчик исходного заказа");
eosio::check(o.status == OrderStatus::RECEIVED,
"Возврат возможен только по выданному заказу");
eosio::check(o.return_request_id == 0,
"По этому заказу уже открыто заявление на возврат");
eosio::check(actual_quantity <= o.actual_quantity,
"Нельзя вернуть больше единиц, чем было выдано");
eosio::check(o.warranty_period_secs > 0,
"По этому заказу гарантия не предусмотрена");
const auto now = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
eosio::check(now < o.warranty_until,
"Гарантийный срок по заказу истёк");
const eosio::asset fact_cost = eosio::asset(
static_cast<int64_t>(actual_quantity) * o.unit_price.amount,
_root_govern_symbol);
// Создание return_request entity
return_requests_index requests(_marketplace, coopname.value);
uint64_t request_id = requests.available_primary_key();
requests.emplace(_marketplace, [&](auto& r) {
r.id = request_id;
r.hash = request_hash;
r.coopname = coopname;
r.orderer = orderer;
r.original_order_id = o.id;
r.original_order_hash = o.hash;
// r.original_consume_op_id заполнит backend post-effect через ParserClient
// (подбор по journal с process_hash=order.hash + operation_code=o.mkt.consum).
r.actual_quantity = actual_quantity;
r.fact_cost = fact_cost;
r.reason_text = reason_text;
r.photos = photos;
r.status = ReturnStatus::PENDING_REVIEW;
r.statement = statement;
});
// Двусторонняя связь — order.return_request_id
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.return_request_id = request_id;
});
}
@@ -0,0 +1,32 @@
/**
* @brief Поставщик акцептует один Order (Story 4.5, p.mkt.supply).
*
* Без ledger2-операций — статус active → accepted. Backend проходит циклом
* по orders соответствующего batch'а, вызывая `acceptorder` per Order
* (контракт не принимает векторов order_hash — единичные транзакции
* масштабируются на любой размер batch'а).
*
* После акцепта поставщик считается обязанным доставить партию: отдельная
* подпись «готов отгрузить» (бывший prepship) удалена из процесса —
* следующий шаг сразу signsupp с актом приёмки.
*
* Guards:
* - Order существует и в статусе active.
* - actor == order.offerer.
*
* @ingroup public_marketplace_actions
*/
void marketplace::acceptorder(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.offerer == offerer, "Вы не поставщик этого заказа");
eosio::check(o.status == OrderStatus::ACTIVE,
"Заказ уже не в активном статусе — акцептовать нельзя");
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::ACCEPTED;
});
}
@@ -0,0 +1,34 @@
/**
* @brief Заказчик отменяет заказ до акцепта поставщиком (Story 4.4, p.mkt.supply).
*
* Триггерит `o.mkt.unblk` на full `order.total_cost`. Сумма остаётся на
* `w.mkt.member.available` заказчика — может быть потрачена на следующий
* заказ программы либо выведена явным `o.mkt.recall` (отдельное действие).
*
* Guards:
* - Order существует.
* - actor == order.orderer.
* - Order в статусе active (до acceptorder). После acceptorder отмена
* запрещена — поставщик уже взял обязательство.
*
* @ingroup public_marketplace_actions
*/
void marketplace::cancelorder(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.orderer == orderer, "Вы не заказчик этого заказа");
eosio::check(o.status == OrderStatus::ACTIVE,
"Нельзя отменить заказ: он уже акцептован поставщиком");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNBLOCK_ON_CANCEL,
o.total_cost, orderer, o.hash,
Marketplace::Memo::get_cancel_order_memo(o.id));
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::CANCELLED;
});
}
@@ -0,0 +1,150 @@
/**
* @brief Заказчик размещает заказ на товар (Story 4.1, p.mkt.supply шаг 1).
*
* Серия операций (атомарно в одной транзакции Antelope):
* 1. `o.wal.conv` (conditional) — TRANSFER w.wal.share → w.wal.member,
* Дт 80 / Кт 86. Только если на `w.wal.member.available` заказчика и
* на `w.mkt.member.available` суммарно не хватает суммы заказа.
* 2. `o.mkt.assign` (conditional) — TRANSFER w.wal.member → w.mkt.member,
* без проводки. Только если на `w.mkt.member.available` не хватает.
* 3. `o.mkt.block` (всегда) — BLOCK на `w.mkt.member` пайщика на total_cost.
*
* Guards (из p.mkt.supply.standard.yaml + Locked Decision L6):
* - quantity > 0; unit_price > 0 в _root_govern_symbol; cycle_type валидный.
* - Order с таким hash ещё не создан (idempotency).
* - Заказчик — активный пайщик кооператива (`get_participant_or_fail`).
* - `delivery_braname` существует в `branches` (КУ выдачи задаётся пайщиком
* из доступных и неизменен после создания Order'а).
* - Σ available трёх кошельков (w.wal.share + w.wal.member + w.mkt.member)
* >= total_cost; иначе createorder фейлится без создания Order'а.
* - Подписка пайщика на оферту ЦПП «Стол заказов» (L2/L3 онбординг) —
* автоматически проверяется в `ledger2::walletop` через
* `assert_program_signed` (cross-contract в `wallet::users.programs[]`)
* при первом ASSIGN/BLOCK на USER_SHARED-кошельке программы.
*
* Сообщения проверок — для прямого показа пользователю (UI ловит check'ом).
*
* @note process_hash для всех трёх ledger2-операций — `order_hash`. Это
* даёт backend'у одну точку группировки операций процесса
* p.mkt.supply через `getProcess(process_hash)` (Story 9.3).
*
* @ingroup public_marketplace_actions
*/
void marketplace::createorder(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash,
checksum256 offer_hash,
eosio::name offerer,
eosio::name delivery_braname,
uint64_t quantity,
eosio::asset unit_price,
eosio::name cycle_type,
uint32_t warranty_period_secs,
checksum256 batch_hash) {
require_auth(coopname);
// ── Базовая валидация параметров ────────────────────────────────────
eosio::check(quantity > 0, "Количество должно быть больше нуля");
eosio::check(unit_price.is_valid() && unit_price.amount > 0,
"Некорректная цена за единицу");
eosio::check(unit_price.symbol == _root_govern_symbol,
"Некорректный символ валюты в цене");
eosio::check(cycle_type == CycleType::TIME_BASED ||
cycle_type == CycleType::VOLUME_BASED ||
cycle_type == CycleType::OPEN_SUBSCRIPT ||
cycle_type == CycleType::INDIVIDUAL,
"Неизвестный тип цикла отсечки заявок");
// Idempotency: Order с таким hash не должен существовать
eosio::check(!Marketplace::get_order_by_hash(coopname, order_hash).has_value(),
"Заказ с таким идентификатором уже создан");
// Заказчик — активный пайщик кооператива (бросает если не найден / blocked)
get_participant_or_fail(coopname, orderer);
// КУ выдачи существует
get_branch_or_fail(coopname, delivery_braname);
// ── Расчёт total_cost ────────────────────────────────────────────────
eosio::asset total_cost = eosio::asset(
static_cast<int64_t>(quantity) * unit_price.amount,
_root_govern_symbol);
eosio::check(total_cost.amount > 0,
"Итоговая сумма заказа должна быть больше нуля");
// ── Достаточность средств: Σ available трёх кошельков >= total_cost ─
auto bal_share = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::SHARE_FUND_PAY, orderer);
auto bal_member = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::CK_MEMBER, orderer);
auto bal_mkt = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::MARKETPLACE_MEMBER, orderer);
eosio::asset total_available =
bal_share.available + bal_member.available + bal_mkt.available;
eosio::check(total_available >= total_cost,
std::string{"Недостаточно средств для заказа: требуется "} +
total_cost.to_string() + ", доступно " + total_available.to_string());
// ── Расчёт conditional-долей серии ──────────────────────────────────
// Сначала тратим w.mkt.member.available; недостающее берём из w.wal.member
// через assign; недостающее в w.wal.member берём из w.wal.share через conv.
const eosio::asset zero = eosio::asset(0, _root_govern_symbol);
eosio::asset need_to_assign = (bal_mkt.available >= total_cost)
? zero
: (total_cost - bal_mkt.available);
eosio::asset need_to_conv = (bal_member.available >= need_to_assign)
? zero
: (need_to_assign - bal_member.available);
// ── Создание Order entity (id потребуется для memo) ─────────────────
orders_index orders(_marketplace, coopname.value);
uint64_t new_id = orders.available_primary_key();
orders.emplace(_marketplace, [&](auto& o) {
o.id = new_id;
o.hash = order_hash;
o.coopname = coopname;
o.orderer = orderer;
o.offerer = offerer;
o.offer_hash = offer_hash;
o.delivery_braname = delivery_braname;
o.accept_braname = eosio::name{}; // заполняется на signsupp
o.quantity = quantity;
o.actual_quantity = quantity; // до signiss2 == quantity (Story 6.2/6.3)
o.unit_price = unit_price;
o.total_cost = total_cost;
o.fact_cost = total_cost; // до signiss2 == total_cost
o.cycle_type = cycle_type;
o.warranty_period_secs = warranty_period_secs;
o.status = OrderStatus::ACTIVE;
o.batch_hash = batch_hash;
});
// ── Шаг 1: o.wal.conv (conditional) ──────────────────────────────────
if (need_to_conv.amount > 0) {
Ledger2::apply(_marketplace, coopname,
operations::wallet::CONVERT_TO_MEMBER,
need_to_conv, orderer, order_hash,
Marketplace::Memo::get_create_order_convert_memo(new_id));
}
// ── Шаг 2: o.mkt.assign (conditional) ────────────────────────────────
if (need_to_assign.amount > 0) {
Ledger2::apply(_marketplace, coopname,
operations::marketplace::ASSIGN_TO_PROGRAM,
need_to_assign, orderer, order_hash,
Marketplace::Memo::get_create_order_assign_memo(new_id));
}
// ── Шаг 3: o.mkt.block (всегда) ──────────────────────────────────────
Ledger2::apply(_marketplace, coopname,
operations::marketplace::BLOCK_FOR_ORDER,
total_cost, orderer, order_hash,
Marketplace::Memo::get_create_order_block_memo(new_id));
}
@@ -0,0 +1,32 @@
/**
* @brief Поставщик отказывается от одного Order'а до акцепта (Story 4.5, p.mkt.supply).
*
* Per-Order: o.mkt.unblk на total_cost (резерв возвращается заказчику) +
* статус active → cancelled. Backend проходит циклом по orders соответствующего
* batch'а, вызывая `declineorder` per Order — векторов order_hash нет.
*
* Guards:
* - Order существует и в статусе active.
* - actor == order.offerer.
*
* @ingroup public_marketplace_actions
*/
void marketplace::declineorder(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.offerer == offerer, "Вы не поставщик этого заказа");
eosio::check(o.status == OrderStatus::ACTIVE,
"Заказ уже не в активном статусе — отклонить нельзя");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNBLOCK_ON_CANCEL,
o.total_cost, o.orderer, o.hash,
Marketplace::Memo::get_decline_order_memo(o.id));
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::CANCELLED;
});
}
@@ -0,0 +1,38 @@
/**
* @brief Backend закрывает один Order по таймауту цикла отсечки заявок
* (Story 4.3, p.mkt.supply).
*
* Вызывается бэкендом после расчёта по batch'у: если за время цикла Offer'а
* threshold не достигнут, бэкенд проходит циклом по всем active Order'ам
* этого batch'а и для каждого вызывает `expireorder`. Контракт не знает про
* threshold — это вычисление backend'а; on-chain — только закрытие конкретного
* Order'а с возвратом резерва.
*
* Per-Order: o.mkt.unblk на total_cost + статус active → cancelled.
*
* Guards:
* - Order существует и в статусе active (после акцепта поставщика
* expireorder не применим — поставщик уже взял обязательство; такие
* Order'ы должны идти через `signiss2` обычным порядком либо через
* отдельный механизм просрочки доставки).
* - require_auth(coopname) — backend от имени кооператива.
*
* @ingroup public_marketplace_actions
*/
void marketplace::expireorder(eosio::name coopname,
checksum256 order_hash) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.status == OrderStatus::ACTIVE,
"Закрыть по таймауту можно только активный заказ");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNBLOCK_ON_CANCEL,
o.total_cost, o.orderer, o.hash,
Marketplace::Memo::get_expire_order_memo(o.id));
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::CANCELLED;
});
}
@@ -0,0 +1,39 @@
/**
* @brief Callback от gateway о фактическом подтверждении исходящей выплаты
* поставщику (E11 техдолг 598-16, Locked Decision L12, p.mkt.supply).
*
* Inline-action отправляется контрактом gateway из `gateway::outcomplete`
* после того, как кассир в админке подтвердил реальный банковский перевод.
* Здесь — единственное место, где применяется бухгалтерская проводка
* выплаты:
*
* - Ledger2::apply(o.mkt.payout, total_cost, …, hash=order.hash) — Дт 86 / Кт 51.
*
* `outcome_hash` приходит из gateway и равен `order.hash` (так его задал
* marketplace::payout). Поиск Order'а — по индексу `byhash`.
*
* Guards:
* - require_auth(_gateway) — callback легитимен только от gateway-контракта.
* - Order найден по `outcome_hash`.
* - `payout_status == PENDING` — на NONE/COMPLETED/DECLINED callback не ждём.
*
* @ingroup public_marketplace_actions
*/
void marketplace::payconfirm(eosio::name coopname, checksum256 outcome_hash) {
require_auth(_gateway);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, outcome_hash,
"Order не найден по outcome_hash из callback'а gateway");
eosio::check(o.payout_status == OrderPayoutStatus::PENDING,
"Callback gateway::outcomplete получен на Order не в статусе ожидания выплаты");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::PAY_SUPPLIER,
o.total_cost, o.offerer, o.hash,
Marketplace::Memo::get_pay_supplier_memo(o.id));
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.payout_status = OrderPayoutStatus::COMPLETED;
upd.payout_decline_reason.clear();
});
}
@@ -0,0 +1,33 @@
/**
* @brief Callback от gateway об отклонении исходящей выплаты поставщику
* (E11 техдолг 598-16, Locked Decision L12, p.mkt.supply).
*
* Inline-action отправляется контрактом gateway из `gateway::outdecline` —
* кассир отметил, что банковский перевод не прошёл (нет реквизитов,
* платёж отменён банком, ошибка ввода). Бухгалтерия не двигается —
* обязательство Кт 86 перед поставщиком остаётся открытым. Backend может
* повторно вызвать `marketplace::payout` после исправления реквизитов;
* gateway-запись по этому outcome_hash уже стёрта на outdecline, поэтому
* повторная инициация проходит штатно (см. payout-гард `payout_status ∈ {
* NONE, DECLINED }`).
*
* Guards:
* - require_auth(_gateway) — callback легитимен только от gateway-контракта.
* - Order найден по `outcome_hash`.
* - `payout_status == PENDING`.
*
* @ingroup public_marketplace_actions
*/
void marketplace::paydecline(eosio::name coopname, checksum256 outcome_hash, std::string reason) {
require_auth(_gateway);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, outcome_hash,
"Order не найден по outcome_hash из callback'а gateway");
eosio::check(o.payout_status == OrderPayoutStatus::PENDING,
"Callback gateway::outdecline получен на Order не в статусе ожидания выплаты");
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.payout_status = OrderPayoutStatus::DECLINED;
upd.payout_decline_reason = reason;
});
}
@@ -0,0 +1,50 @@
/**
* @brief Инициация исходящей выплаты поставщику по одному Order'у через gateway
* (E11 техдолг 598-16, Locked Decision L12, p.mkt.supply).
*
* Backend дёргает это действие, когда кассир в админке отметил готовность
* проводить выплату поставщику. Действие НЕ применяет ledger2 — оно лишь
* inline-вызовом регистрирует в gateway::outcomes запись типа «исходящий
* платёж» со статусом pending и привязанным callback'ом на marketplace. Сам
* Дт 86 / Кт 51 произойдёт уже в callback'е `payconfirm` после фактического
* банковского перевода (gateway::outcomplete вызывает кассир через свой
* стол), либо отменится в `paydecline` (gateway::outdecline).
*
* Inline-вызов: `gateway::createoutpay` с `callback_contract = _marketplace`,
* `confirm_callback = "payconfirm"_n`, `decline_callback = "paydecline"_n`,
* `outcome_hash = order.hash` (уникальность гарантирована индексом orders).
*
* Status Order'а не меняется (выплата может идти параллельно шагам выдачи).
* payout_status переходит NONE/DECLINED → PENDING; declined-кейс — повторная
* попытка после исправления реквизитов (gateway-запись была стёрта на outdecline).
*
* Guards:
* - Order существует и приёмка завершена (статус ∈ accepted_to_coop /
* ready_to_receive / received).
* - payout_status ∈ { NONE, DECLINED } — нельзя инициировать выплату поверх
* pending или completed.
*
* @ingroup public_marketplace_actions
*/
void marketplace::payout(eosio::name coopname, checksum256 order_hash) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.status == OrderStatus::ACCEPTED_TO_COOP ||
o.status == OrderStatus::READY_TO_RECEIVE ||
o.status == OrderStatus::RECEIVED,
"Выплата возможна только после приёмки имущества кооперативом");
eosio::check(o.payout_status == OrderPayoutStatus::NONE ||
o.payout_status == OrderPayoutStatus::DECLINED,
"Выплата уже инициирована либо завершена");
// Регистрация исходящего платежа в gateway. Сам Дт 86 / Кт 51 произойдёт
// в callback'е `payconfirm` от gateway после действия кассира.
Gateway::create_outcome(_marketplace, coopname, o.offerer, o.hash, o.total_cost,
_marketplace, "payconfirm"_n, "paydecline"_n);
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.payout_status = OrderPayoutStatus::PENDING;
upd.payout_decline_reason.clear(); // на случай повторной инициации после DECLINED
});
}
@@ -0,0 +1,53 @@
/**
* @brief Председатель приёмного КУ ставит закрывающую подпись на АПП приёмки
* по одному Order'у (Story 5.3/5.4, p.mkt.supply).
*
* Per-Order — только бухгалтерская приёмка имущества:
* - Ledger2::apply(o.mkt.purch, total_cost, …, hash=order.hash) — Дт 10 / Кт 86.
*
* Имущество приходуется на склад приёмного КУ (`accept_braname`); у кооператива
* возникает обязательство Кт 86 перед поставщиком. Фактическая выплата деньгами
* (Дт 86 / Кт 51) — отдельным lazy action'ом `marketplace::payout` после
* подтверждения кассиром реального банковского перевода (Locked Decision L12,
* E11 техдолг 598-16).
*
* Status: supply_prepared → accepted_to_coop. acceptance_act_signchair
* сохраняется. `payout_done` не выставляется — это атрибут payout-действия.
*
* Guards:
* - Order существует и в статусе supply_prepared.
* - Подписант (`signer`) авторизован для приёмного КУ — председатель,
* trustee либо доверенное лицо в `branches[accept_braname].trusted[]`.
* - На акте есть подписи поставщика и подписанта приёмки.
*
* @ingroup public_marketplace_actions
*/
void marketplace::signchair(eosio::name coopname,
eosio::name signer,
checksum256 order_hash,
document2 act) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.status == OrderStatus::SUPPLY_PREPARED,
"Заказ не готов к приёмке кооперативом");
// Авторизация подписи: signer должен быть в trusted списке приёмного КУ.
auto branch = get_branch_or_fail(coopname, o.accept_braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен подписывать акты приёмки данного кооперативного участка");
verify_document_or_fail(act, { o.offerer, signer });
// Только приёмка имущества; payout — отдельный lazy action (L12).
Ledger2::apply(_marketplace, coopname,
operations::marketplace::PURCHASE_FROM_SUPPLIER,
o.total_cost, o.offerer, o.hash,
Marketplace::Memo::get_purchase_from_supplier_memo(o.id));
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::ACCEPTED_TO_COOP;
upd.acceptance_act_signchair = act;
upd.current_warehouse_braname = o.accept_braname; // имущество на приёмном складе
});
}
@@ -0,0 +1,44 @@
/**
* @brief Председатель КУ выдачи открывает выдачу первой подписью АПП-выдачи
* (Story 6.1, signiss1).
*
* Без ledger2-операций. Per-Order: статус accepted_to_coop → ready_to_receive;
* issue_act_signiss1 сохраняется; current_warehouse_braname обновляется на
* delivery_braname (фиксация факта логистической передачи имущества на склад
* выдачи — промежуточные перемещения по заготовочным КУ контрактом не
* подписываются, точка хранения переходит «скачком» в этот момент).
* Нотификация заказчику — post-effect в backend через ParserClient.
*
* Guards:
* - Order существует и в статусе accepted_to_coop.
* - Подписант (`signer`) авторизован для КУ выдачи (`o.delivery_braname`):
* председатель / trustee / trusted в `branches[delivery_braname]`.
* - verify_document_or_fail(act, {signer}).
* - Idempotency: is_empty_document(issue_act_signiss1).
*
* @ingroup public_marketplace_actions
*/
void marketplace::signiss1(eosio::name coopname,
eosio::name signer,
checksum256 order_hash,
document2 act) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.status == OrderStatus::ACCEPTED_TO_COOP,
"Заказ не готов к открытию выдачи");
eosio::check(is_empty_document(o.issue_act_signiss1),
"Первая подпись акта выдачи уже зафиксирована");
auto branch = get_branch_or_fail(coopname, o.delivery_braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен подписывать акты выдачи данного кооперативного участка");
verify_document_or_fail(act, { signer });
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::READY_TO_RECEIVE;
upd.issue_act_signiss1 = act;
upd.current_warehouse_braname = o.delivery_braname; // имущество готово к выдаче на КУ выдачи
});
}
@@ -0,0 +1,141 @@
/**
* @brief Заказчик закрывающей подписью АПП-выдачи получает имущество (Story 6.3, signiss2).
*
* Per-Order атомарная транзакция с поддержкой actual_quantity ≠ ordered (Story 6.2):
*
* 1) actual == ordered:
* Ledger2::apply(o.mkt.consum, fact_cost) — Дт 91 / Кт 10, REVOKE на blocked.
* Ledger2::apply(o.mkt.consum2, fact_cost) — Дт 86 / Кт 91, NONE.
*
* 2) actual < ordered (выдано меньше — остаток на blocked возвращается):
* Ledger2::apply(o.mkt.unblk, ordered_cost - fact_cost) — снятие резерва на разницу.
* затем — те же consum + consum2 на fact_cost.
*
* 3) actual > ordered (доплата с паевого):
* diff = fact_cost - ordered_cost
* проверка достаточности средств для diff (через w.wal.share + w.wal.member).
* Conditional o.wal.conv (если на w.wal.member недостача).
* Conditional o.mkt.assign (если на w.mkt.member недостача).
* o.mkt.block(diff) — резервируем доплату на этот же Order.
* затем — consum + consum2 на fact_cost.
*
* Status: ready_to_receive → received. actual_quantity, fact_cost,
* issue_act_signiss2, warranty_until заполняются.
*
* Guards:
* - actor == order.orderer; Order в ready_to_receive.
* - Подписант со стороны кооператива (`delivery_signer`) авторизован для
* КУ выдачи (`o.delivery_braname`).
* - verify_document_or_fail(act, {delivery_signer, orderer}).
* - actual_quantity > 0.
* - При actual > ordered и нехватке средств — транзакция фейлится с
* человеческим сообщением, которое UI показывает напрямую.
* - Idempotency: is_empty_document(issue_act_signiss2).
*
* @ingroup public_marketplace_actions
*/
void marketplace::signiss2(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash,
uint64_t actual_quantity,
eosio::name delivery_signer,
document2 act) {
require_auth(coopname);
eosio::check(actual_quantity > 0, "Фактическое количество должно быть больше нуля");
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.orderer == orderer, "Вы не заказчик этого заказа");
eosio::check(o.status == OrderStatus::READY_TO_RECEIVE,
"Заказ не готов к выдаче");
eosio::check(is_empty_document(o.issue_act_signiss2),
"Финальная подпись акта выдачи уже зафиксирована");
auto branch = get_branch_or_fail(coopname, o.delivery_braname);
eosio::check(branch.is_user_authorized(delivery_signer),
"Подписант со стороны кооператива не уполномочен подписывать акты выдачи данного кооперативного участка");
verify_document_or_fail(act, { delivery_signer, orderer });
const eosio::asset fact_cost = eosio::asset(
static_cast<int64_t>(actual_quantity) * o.unit_price.amount,
_root_govern_symbol);
eosio::check(fact_cost.amount > 0,
"Итоговая фактическая сумма заказа должна быть больше нуля");
// ── Корректирующие операции (если факт ≠ заказ) ─────────────────────
if (fact_cost < o.total_cost) {
// actual < ordered: возвращаем разницу с blocked → available
const eosio::asset diff = o.total_cost - fact_cost;
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNBLOCK_ON_CANCEL,
diff, orderer, o.hash,
Marketplace::Memo::get_signiss2_correction_less_memo(o.id));
} else if (fact_cost > o.total_cost) {
// actual > ordered: добираем разницу с паевого + assign + block
const eosio::asset diff = fact_cost - o.total_cost;
// Проверка доступности diff в трёх кошельках
auto bal_share = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::SHARE_FUND_PAY, orderer);
auto bal_member = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::CK_MEMBER, orderer);
auto bal_mkt = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::MARKETPLACE_MEMBER, orderer);
eosio::asset total_avail = bal_share.available + bal_member.available + bal_mkt.available;
eosio::check(total_avail >= diff,
std::string{"Недостаточно средств для дооплаты по факту: требуется "} +
diff.to_string() + ", доступно " + total_avail.to_string());
const eosio::asset zero = eosio::asset(0, _root_govern_symbol);
eosio::asset need_to_assign = (bal_mkt.available >= diff)
? zero
: (diff - bal_mkt.available);
eosio::asset need_to_conv = (bal_member.available >= need_to_assign)
? zero
: (need_to_assign - bal_member.available);
if (need_to_conv.amount > 0) {
Ledger2::apply(_marketplace, coopname,
operations::wallet::CONVERT_TO_MEMBER,
need_to_conv, orderer, o.hash,
Marketplace::Memo::get_signiss2_correction_more_convert_memo(o.id));
}
if (need_to_assign.amount > 0) {
Ledger2::apply(_marketplace, coopname,
operations::marketplace::ASSIGN_TO_PROGRAM,
need_to_assign, orderer, o.hash,
Marketplace::Memo::get_signiss2_correction_more_assign_memo(o.id));
}
Ledger2::apply(_marketplace, coopname,
operations::marketplace::BLOCK_FOR_ORDER,
diff, orderer, o.hash,
Marketplace::Memo::get_signiss2_correction_more_block_memo(o.id));
}
// fact == ordered — без корректировок
// ── Композитная пара consum + consum2 на fact_cost ──────────────────
Ledger2::apply(_marketplace, coopname,
operations::marketplace::CONSUME_BY_MEMBER,
fact_cost, orderer, o.hash,
Marketplace::Memo::get_consume_by_member_memo(o.id));
Ledger2::apply(_marketplace, coopname,
operations::marketplace::CONSUME_TRANSIT_CLOSE,
fact_cost, orderer, o.hash,
Marketplace::Memo::get_consume_transit_close_memo(o.id));
// ── Закрытие Order'а ────────────────────────────────────────────────
const auto now = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
const auto warranty_until = (o.warranty_period_secs > 0)
? eosio::time_point_sec(now.sec_since_epoch() + o.warranty_period_secs)
: eosio::time_point_sec(0);
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::RECEIVED;
upd.actual_quantity = actual_quantity;
upd.fact_cost = fact_cost;
upd.issue_act_signiss2 = act;
upd.warranty_until = warranty_until;
});
}
@@ -0,0 +1,48 @@
/**
* @brief Поставщик первой подписью на АПП приёмки фиксирует партию по одному
* Order'у (Story 5.3/5.4, p.mkt.supply).
*
* Без ledger2-операций (имущество физически на складе, но юридически не
* оприходовано до signchair). Per-Order: статус accepted → supply_prepared,
* параметр `accept_braname` указывает приёмный КУ — куда поставщик сдаёт
* партию. Документ acceptance_act_signsupp сохраняется (копия per Order
* — допустимо для on-chain).
*
* Backend проходит циклом по orders соответствующего batch'а с одним и тем
* же актом, вызывая `signsupp` per Order — это позволяет масштабировать
* batch на любой размер без риска превысить лимит транзакции Antelope.
*
* Guards:
* - Order существует и в статусе accepted.
* - actor == order.offerer.
* - `accept_braname` существует в `branches[coopname]`.
* - verify_document_or_fail(act, {offerer}) — поставщик подписал.
* - Idempotency: повторный вызов запрещён (`is_empty_document(acceptance_act_signsupp)`).
*
* @ingroup public_marketplace_actions
*/
void marketplace::signsupp(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash,
eosio::name accept_braname,
document2 act) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.offerer == offerer, "Вы не поставщик этого заказа");
eosio::check(o.status == OrderStatus::ACCEPTED,
"Заказ не в статусе акцепта — подпись поставщика на акт приёмки невозможна");
eosio::check(is_empty_document(o.acceptance_act_signsupp),
"Подпись поставщика на акт приёмки уже зафиксирована");
// КУ приёмки существует
get_branch_or_fail(coopname, accept_braname);
verify_document_or_fail(act, { offerer });
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::SUPPLY_PREPARED;
upd.acceptance_act_signsupp = act;
upd.accept_braname = accept_braname;
});
}
@@ -0,0 +1,65 @@
/**
* @brief Backend исполняет одну позицию авторизованного советом проекта
* списания скоропорта (Story 8.4, p.mkt.wroff).
*
* Per-item композитная транзакция (атомарно):
* - Ledger2::apply(o.mkt.wroff, item.amount, …, hash=proposal.hash) — Дт 91 / Кт 10.
* - Ledger2::apply(o.mkt.wroff2, item.amount, …, hash=proposal.hash) — Дт 86 / Кт 91.
*
* Вызывается ТОЛЬКО после того, как совет авторизовал проект (status =
* AUTHORIZED через callback `onmktwoauth`). Backend проходит циклом по
* неисполненным позициям, вызывая `execwroff(proposal_hash, item_index)`
* per item — это снимает ограничение на максимальный размер протокола.
*
* Status: AUTHORIZED → EXECUTED наступает автоматически после исполнения
* последней позиции (когда все items[i].executed становятся true).
*
* Подписанный советом protocol уже лежит в `proposal.protocol` (положен
* callback'ом `onmktwoauth`), отдельно его передавать не нужно.
*
* @ingroup public_marketplace_actions
*/
void marketplace::execwroff(eosio::name coopname,
eosio::name signer,
checksum256 proposal_hash,
uint64_t item_index) {
require_auth(coopname);
auto p = Marketplace::get_writeoff_proposal_by_hash_or_fail(coopname, proposal_hash);
eosio::check(p.status == WroffStatus::AUTHORIZED,
"Проект списания не авторизован советом");
eosio::check(item_index < p.items.size(),
"Указана несуществующая позиция в проекте списания");
eosio::check(!p.items[item_index].executed,
"Эта позиция проекта списания уже исполнена");
const auto& item = p.items[item_index];
auto branch = get_branch_or_fail(coopname, item.braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен исполнять списание данного кооперативного участка");
// Композитная пара wroff + wroff2 для одной позиции
Ledger2::apply(_marketplace, coopname,
operations::marketplace::WRITE_OFF_PERISHABLE,
item.amount, item.braname, p.hash,
Marketplace::Memo::get_writeoff_memo(p.id, item_index));
Ledger2::apply(_marketplace, coopname,
operations::marketplace::WRITE_OFF_TRANSIT_CLOSE,
item.amount, item.braname, p.hash,
Marketplace::Memo::get_writeoff_transit_close_memo(p.id, item_index));
// Помечаем позицию исполненной + финализируем proposal если все позиции готовы
Marketplace::update_writeoff_proposal(coopname, p.id, [&](auto& upd) {
upd.items[item_index].executed = true;
upd.decided_by = signer;
bool all_done = true;
for (const auto& it : upd.items) {
if (!it.executed) { all_done = false; break; }
}
if (all_done) {
upd.status = WroffStatus::EXECUTED;
}
});
}
@@ -0,0 +1,34 @@
/**
* @brief Callback от `soviet::exec` после авторизации Протокола совета о
* списании скоропорта (registry 1105) председателем (Story 8.4, p.mkt.wroff).
*
* Соглашение о сигнатуре — `(coopname, hash, authorization)` — задано в
* `soviet::createagenda::authorize_action_effect`. Контракт `soviet` зовёт
* `marketplace::onmktwoauth` от своего имени, поэтому единственно допустимая
* авторизация — `_soviet`.
*
* Эффект:
* - Находит wroffprops по proposal_hash == `hash`.
* - Проверяет, что текущий статус == PROPOSED (запрет повторного callback'а).
* - Записывает подписанный советом protocol2 в `proposal.protocol`.
* - Переводит статус PROPOSED → AUTHORIZED.
*
* Дальнейший шаг — backend через дельту/мониторинг видит AUTHORIZED и
* проходит циклом execwroff per-item (см. `execwroff.cpp`).
*
* @ingroup public_marketplace_actions
*/
void marketplace::onmktwoauth(eosio::name coopname,
checksum256 hash,
document2 authorization) {
require_auth(_soviet);
auto p = Marketplace::get_writeoff_proposal_by_hash_or_fail(coopname, hash);
eosio::check(p.status == WroffStatus::PROPOSED,
"Проект списания не находится на повестке (callback повторный или поздний)");
Marketplace::update_writeoff_proposal(coopname, p.id, [&](auto& upd) {
upd.status = WroffStatus::AUTHORIZED;
upd.protocol = authorization;
});
}
@@ -0,0 +1,34 @@
/**
* @brief Callback от `soviet` после отказа в Протоколе совета о списании
* скоропорта или истечения срока повестки (Story 8.4, p.mkt.wroff).
*
* Сигнатура `(coopname, hash, reason)` соответствует
* `DECLINE_CALLBACK_SIGNATURE` (см. `cpp/lib/core/soviet/soviet.hpp:19`).
* Контракт `soviet` вызывает action из `cancelexprd` (повестка просрочена)
* или вручную через `decline*` actions от своего имени, поэтому единственно
* допустимая авторизация — `_soviet`.
*
* Эффект:
* - Находит wroffprops по proposal_hash == `hash`.
* - Проверяет, что текущий статус == PROPOSED.
* - Записывает `reason` в `proposal.reject_reason`.
* - Переводит статус PROPOSED → REJECTED.
*
* Без ledger2-движений.
*
* @ingroup public_marketplace_actions
*/
void marketplace::onmktwodecl(eosio::name coopname,
checksum256 hash,
std::string reason) {
require_auth(_soviet);
auto p = Marketplace::get_writeoff_proposal_by_hash_or_fail(coopname, hash);
eosio::check(p.status == WroffStatus::PROPOSED,
"Проект списания не находится на повестке (callback повторный или поздний)");
Marketplace::update_writeoff_proposal(coopname, p.id, [&](auto& upd) {
upd.status = WroffStatus::REJECTED;
upd.reject_reason = reason;
});
}
@@ -0,0 +1,55 @@
/**
* @brief Backend выносит проект списания скоропорта на повестку совета
* (Story 8.1, p.mkt.wroff).
*
* Без ledger2-операций. Создаётся writeoff_proposal в статусе proposed;
* total_amount = Σ items.amount; все items создаются с executed=false.
* Сразу после этого action'а backend в той же транзакции вызывает
* `soviet::createagenda(type=mktwroff, callback_contract=_marketplace,
* confirm_callback=onmktwoauth, decline_callback=onmktwodecl, hash=
* proposal_hash, statement=signed_writeoff_statement)`. Списание
* выполняется per-item через `execwroff` только после callback'а
* `onmktwoauth` (status proposed → authorized).
*
* Guards:
* - actor backend (auth coopname).
* - items.size() > 0.
* - Все items.amount > 0 в _root_govern_symbol.
* - Все items.braname существуют в `branches[coopname]`.
* - proposal_hash уникален.
*
* @ingroup public_marketplace_actions
*/
void marketplace::propwroff(eosio::name coopname,
eosio::name proposed_by,
checksum256 proposal_hash,
std::vector<wroff_item> items) {
require_auth(coopname);
eosio::check(!items.empty(), "Список позиций к списанию пуст");
eosio::check(!Marketplace::get_writeoff_proposal_by_hash(coopname, proposal_hash).has_value(),
"Проект списания с таким идентификатором уже создан");
eosio::asset total = eosio::asset(0, _root_govern_symbol);
for (auto& item : items) {
eosio::check(item.amount.is_valid() && item.amount.amount > 0,
"Каждая позиция должна иметь положительную сумму");
eosio::check(item.amount.symbol == _root_govern_symbol,
"Некорректный символ валюты в позиции списания");
// КУ-источник существует
get_branch_or_fail(coopname, item.braname);
item.executed = false; // защита от случайно проставленного флага
total += item.amount;
}
writeoff_proposals_index proposals(_marketplace, coopname.value);
proposals.emplace(_marketplace, [&](auto& p) {
p.id = proposals.available_primary_key();
p.hash = proposal_hash;
p.coopname = coopname;
p.proposed_by = proposed_by;
p.items = items;
p.total_amount = total;
p.status = WroffStatus::PROPOSED;
});
}
@@ -1,28 +0,0 @@
/**
\ingroup public_actions
\brief Отказ в прохождении модерации заявки на товар.
*
* Этот метод предназначен для администраторов маркетплейса, чтобы отказать в публикации товара после его модерации.
* При отказе администратор указывает причину отказа в параметре `meta`.
*
* @param username Имя пользователя-администратора, который вызывает данный метод.
* @param exchange_id Уникальный идентификатор товара, публикацию которого нужно запретить.
* @param meta Строковое описание или причина, по которой товар не прошел модерацию.
*
* @note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::prohibit(eosio::name coopname, eosio::name username, uint64_t exchange_id, std::string meta) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Объявление не найдено");
exchange.modify(change, username, [&](auto &o){
o.status = "prohibit"_n;
o.meta = meta;
});
};
@@ -1,31 +0,0 @@
/**
\ingroup public_actions
\brief Опубликовать товар на маркетплейсе.
*
* Этот метод позволяет владельцу товара опубликовать его на маркетплейсе. Для публикации товар должен находиться в статусе "unpublished".
*
* @param username Имя пользователя, являющегося владельцем заявки.
* @param exchange_id Идентификатор заявки, которую следует опубликовать.
*
* @note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::publish(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
staff_index staff(_soviet, coopname.value);
auto persona = staff.find(username.value);
eosio::check(change != exchange.end(), "Объявление не найдено");
eosio::check(change->username == username || (persona != staff.end() && persona -> has_right(_marketplace, "unpublish"_n)), "У вас нет права на публикацию данной заявки");
eosio::check(change->status == "unpublished"_n || change->status == "prohibit"_n, "Неверный статус для публикации");
exchange.modify(change, username, [&](auto &o) {
if (change->status == "unpublished"_n)
o.status = "published"_n;
else
o.status = "moderation"_n;
});
}
@@ -1,42 +0,0 @@
/**
\ingroup public_actions
\brief Подпись акта получения имущества пайщиком
**/
[[eosio::action]] void marketplace::recieve(eosio::name coopname, eosio::name username, uint64_t exchange_id, document2 document) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> parent_id > 0, "Только продукт по встречной заявке может быть выдан");
eosio::check(change -> status == "delivered"_n, "Продукт может быть выдан только по заявке в статусе ожидания получения");
eosio::check(change -> deadline_for_receipt.sec_since_epoch() >= eosio::current_time_point().sec_since_epoch(), "Время на выдачу имущества истекло");
auto soviet = get_board_by_type_or_fail(coopname, "soviet"_n);
auto chairman = soviet.get_chairman();
// Проверяем подпись документа
verify_document_or_fail(document);
if (change -> type == "order"_n) { //если указанная заявка - это заказ продукта
//то получение продукта может осуществить только пользователь из username
eosio::check(change -> username == username, "Недостачно прав доступа для получения имущества");
} else { //если указанная заявка - это поставка продукта
//то поставку может осуществить только пользователь username в этой заявке
eosio::check(change -> parent_username == username, "Недостаточно прав доступа для получения имущества");
};
//подписываем акт приёма-передачи кооперативу пайщиком
exchange.modify(change, coopname, [&](auto &ch){
ch.status = "recieved1"_n;
ch.recieved_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
ch.warranty_delay_until = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch() + change -> product_lifecycle_secs / 4);
ch.product_recieve_act = document;
});
}
@@ -1,38 +0,0 @@
[[eosio::action]] void marketplace::recievecnfrm(eosio::name coopname, eosio::name username, uint64_t exchange_id, document2 document) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> parent_id > 0, "Только продукт по встречной заявке может быть поставлен");
eosio::check(change -> status == "recieved1"_n, "Продукт может быть поставлен только по заявке в статусе recieved1");
auto soviet = get_board_by_type_or_fail(coopname, "soviet"_n);
auto chairman = soviet.get_chairman();
eosio::check(username == chairman, "Недостачно прав доступа для подтверждения выдачи имущества");
// Проверяем подпись документа
verify_document_or_fail(document);
exchange.modify(change, _marketplace, [&](auto &ch) {
ch.status = "recieved2"_n;
ch.product_recieve_act_validation = document;
});
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"recieved"_n,
std::make_tuple(coopname, exchange_id)
).send();
//уменьшаем паевой фонд
action(
permission_level{ _marketplace, "active"_n},
_fund,
"subcirculate"_n,
std::make_tuple(coopname, change -> total_cost, false)
).send();
}
@@ -1,35 +0,0 @@
/**
\ingroup public_actions
\brief Перевозка прибыла в место назначения.
@details Перевозка переходит в статус arrived и ожидает подписи получателя.
@param coopname Имя кооператива
@param hash Идентификатор перевозки
@param transport_act_delivery Акт доставки подписанный водителем
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::arrived(eosio::name coopname, checksum256 hash, document2 transport_act_delivery) {
require_auth(coopname);
// Проверяем что перевозка существует
auto shipment = Marketplace::get_shipment_by_hash_or_fail(coopname, hash, "Перевозка не найдена");
eosio::check(shipment.status == "transit"_n, "Подписывать акт доставки можно только для перевозки в статусе transit");
// Проверяем подпись документа
verify_document_or_fail(transport_act_delivery, { shipment.driver_username });
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(transport_act_delivery, 0);
// Обновляем перевозку - добавляем третий акт
shipments_index shipments(_marketplace, coopname.value);
auto hash_index = shipments.get_index<"byhash"_n>();
auto shipment_itr = hash_index.find(hash);
hash_index.modify(shipment_itr, _marketplace, [&](auto &s) {
s.status = "arrived"_n; // Новый статус - прибыл, ожидает приёма получателем
Document::add_document(s.documents, DocumentNames::SHIPMENT_ARRIVE_ACT, transport_act_delivery);
s.delivered_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
};
@@ -1,71 +0,0 @@
/**
\ingroup public_actions
\brief Создание новой перевозки с массивом заявок.
@details Создает новую перевозку от одного КУ к другому с указанием водителя и массива заявок.
Представитель КУ отправления подписывает первый акт приёма-передачи.
Заявки остаются на складе до получения подписи от водителя.
@param coopname Имя кооператива
@param hash Внешний идентификатор перевозки
@param driver_username Имя водителя-пайщика
@param source_braname КУ отправителя
@param destination_braname КУ назначения
@param request_hashes Массив хэшей заявок для перевозки
@param transport_act_sender Акт приёма-передачи от представителя КУ отправления
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::createship(eosio::name coopname, checksum256 hash, eosio::name driver_username, eosio::name source_braname, eosio::name destination_braname, std::vector<checksum256> request_hashes, document2 transport_act_sender) {
require_auth(coopname);
// Проверяем что КУ отправителя и назначения разные
eosio::check(source_braname != destination_braname, "КУ отправителя и назначения не могут совпадать");
// Проверяем что есть хотя бы одна заявка
eosio::check(!request_hashes.empty(), "Должна быть указана хотя бы одна заявка");
// Проверяем что перевозка с таким hash не существует
auto existing_shipment = Marketplace::get_shipment_by_hash(coopname, hash);
eosio::check(!existing_shipment.has_value(), "Перевозка с таким идентификатором уже существует");
// Проверяем подпись документа
verify_document_or_fail(transport_act_sender);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(transport_act_sender, 0);
requests_index requests(_marketplace, coopname.value);
// Проверяем все заявки но НЕ снимаем их со склада (это будет при signbydriver)
for (auto& request_hash : request_hashes) {
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "supplied2"_n || change.status == "shiprecvd"_n || change.status == "delivered"_n, "Перевозить можно только заявки в статусе supplied2, shiprecvd или delivered");
eosio::check(change.type == "orderoffer"_n, "Перевозить можно только заявки типа orderoffer");
eosio::check(change.warehouse == source_braname, "Заявка должна находиться на складе КУ отправителя");
}
// Получаем новый ID для перевозки
uint64_t next_shipment_id = get_global_id_in_scope(_marketplace, coopname, "shipments"_n);
// Создаем новую перевозку
shipments_index shipments(_marketplace, coopname.value);
shipments.emplace(_marketplace, [&](auto &s) {
s.id = next_shipment_id;
s.hash = hash;
s.coopname = coopname;
s.driver_username = driver_username;
s.source_braname = source_braname;
s.destination_braname = destination_braname;
s.status = "loading"_n;
s.request_hashes = request_hashes;
// Добавляем первый документ
Document::add_document(s.documents, DocumentNames::SHIPMENT_SEND_ACT, transport_act_sender);
s.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
};
@@ -1,55 +0,0 @@
/**
\ingroup public_actions
\brief Приём имущества на склад по накладной.
@details Представитель КУ получения принимает имущество на склад по накладной.
Все заявки из перевозки переходят в статус delivered и ставятся на склад КУ получения.
Объект перевозки удаляется.
@param coopname Имя кооператива
@param hash Идентификатор перевозки
@param warehouse_receipt_act Акт приёма на склад подписанный получателем
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::receiveshipm(eosio::name coopname, checksum256 hash, document2 warehouse_receipt_act) {
require_auth(coopname);
// Проверяем что перевозка существует
auto shipment = Marketplace::get_shipment_by_hash_or_fail(coopname, hash, "Перевозка не найдена");
eosio::check(shipment.status == "arrived"_n, "Принимать имущество на склад можно только для перевозки в статусе arrived");
// Проверяем подпись документа
verify_document_or_fail(warehouse_receipt_act);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(warehouse_receipt_act, 0);
requests_index requests(_marketplace, coopname.value);
// Обновляем все заявки из перевозки - ставим их на склад получения
for (auto& request_hash : shipment.request_hashes) {
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
// Обновляем заявку
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.status = "shiprecvd"_n; // Доставка получена
o.warehouse = shipment.destination_braname; // Ставим на склад КУ получения
o.delivered_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
// Устанавливаем крайний срок получения (3 дня)
o.deadline_for_receipt = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch() + 3 * 24 * 60 * 60);
// Добавляем акт приёма на складе получателем
Document::add_document(o.documents, DocumentNames::SHIPMENT_RECV_ACT, warehouse_receipt_act);
});
}
// Удаляем объект перевозки после завершения
shipments_index shipments(_marketplace, coopname.value);
auto hash_index = shipments.get_index<"byhash"_n>();
auto shipment_itr = hash_index.find(hash);
hash_index.erase(shipment_itr);
};
@@ -1,66 +0,0 @@
/**
\ingroup public_actions
\brief Промежуточная передача товаров между складами.
@details Создает новую перевозку для доставленных заявок, позволяя передать все товары
из текущего склада в другое место назначения с новым водителем.
@param coopname Имя кооператива
@param completed_hash Внешний идентификатор новой перевозки
@param new_driver_username Имя нового водителя-пайщика
@param source_braname КУ с которого забираем товары
@param new_destination_braname Новый КУ назначения
@param request_hashes Массив хэшей заявок для переотправки
@param transport_act_sender Акт передачи от текущего склада
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::retransport(eosio::name coopname, checksum256 completed_hash, eosio::name new_driver_username, eosio::name source_braname, eosio::name new_destination_braname, std::vector<checksum256> request_hashes, document2 transport_act_sender) {
require_auth(coopname);
// Проверяем что есть хотя бы одна заявка
eosio::check(!request_hashes.empty(), "Должна быть указана хотя бы одна заявка");
// Проверяем что перевозка с таким hash не существует
auto existing_shipment = Marketplace::get_shipment_by_hash(coopname, completed_hash);
eosio::check(!existing_shipment.has_value(), "Перевозка с таким идентификатором уже существует");
// Проверяем подпись документа
verify_document_or_fail(transport_act_sender);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(transport_act_sender, 0);
requests_index requests(_marketplace, coopname.value);
// Проверяем все заявки но НЕ снимаем их со склада (это будет при signbydriver)
for (auto& request_hash : request_hashes) {
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.status == "delivered"_n, "Переотправлять можно только доставленные заявки");
eosio::check(change.warehouse == source_braname, "Заявка должна находиться на указанном складе");
}
// Получаем новый ID для перевозки
uint64_t next_shipment_id = get_global_id_in_scope(_marketplace, coopname, "shipments"_n);
// Создаем новую перевозку
shipments_index shipments(_marketplace, coopname.value);
shipments.emplace(_marketplace, [&](auto &s) {
s.id = next_shipment_id;
s.hash = completed_hash;
s.coopname = coopname;
s.driver_username = new_driver_username;
s.source_braname = source_braname;
s.destination_braname = new_destination_braname;
s.status = "loading"_n;
s.request_hashes = request_hashes;
// Добавляем первый документ
Document::add_document(s.documents, DocumentNames::SHIPMENT_SEND_ACT, transport_act_sender);
s.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
};
@@ -1,55 +0,0 @@
/**
\ingroup public_actions
\brief Подпись акта приёма-передачи водителем.
@details Водитель подписывает акт приёма имущества на транспортировку.
Все заявки снимаются со склада отправления.
Перевозка переходит в статус transit.
@param coopname Имя кооператива
@param hash Идентификатор перевозки
@param transport_act_driver Акт приёма-передачи подписанный водителем
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::signbydriver(eosio::name coopname, checksum256 hash, document2 transport_act_driver) {
require_auth(coopname);
// Проверяем что перевозка существует
auto shipment = Marketplace::get_shipment_by_hash_or_fail(coopname, hash, "Перевозка не найдена");
eosio::check(shipment.status == "loading"_n, "Подписывать акт водитель может только для перевозки в статусе loading");
// Проверяем подпись документа
verify_document_or_fail(transport_act_driver);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(transport_act_driver, 0);
requests_index requests(_marketplace, coopname.value);
// Снимаем все заявки со склада отправления
for (auto& request_hash : shipment.request_hashes) {
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
eosio::check(change_opt.has_value(), "Заявка не найдена");
auto change = change_opt.value();
eosio::check(change.warehouse == shipment.source_braname, "Заявка должна находиться на складе КУ отправителя");
// Снимаем заявку со склада
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.warehouse = ""_n; // Снимаем со склада
});
}
// Обновляем перевозку
shipments_index shipments(_marketplace, coopname.value);
auto hash_index = shipments.get_index<"byhash"_n>();
auto shipment_itr = hash_index.find(hash);
hash_index.modify(shipment_itr, _marketplace, [&](auto &s) {
s.status = "transit"_n;
Document::add_document(s.documents, DocumentNames::SHIPMENT_LOADING_ACT, transport_act_driver);
s.loaded_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
};
@@ -1,24 +0,0 @@
[[eosio::action]] void marketplace::supply(eosio::name coopname, eosio::name username, uint64_t exchange_id, document2 document) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> parent_id > 0, "Только продукт по встречной заявке может быть поставлен");
eosio::check(change -> status == "authorized"_n, "Продукт может быть поставлен только по заявке в статусе authorized");
auto soviet = get_board_by_type_or_fail(coopname, "soviet"_n);
auto chairman = soviet.get_chairman();
eosio::check(username == chairman, "Недостаточно прав доступа для подтверждения поставки");
// Проверяем подпись документа
verify_document_or_fail(document);
exchange.modify(change, _marketplace, [&](auto &ch) {
ch.supplied_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
ch.status = "supplied1"_n;
ch.product_contribution_act_validation = document;
});
}

Some files were not shown because too many files have changed in this diff Show More