Compare commits
435 Commits
element
...
audio-stream
| Author | SHA1 | Date | |
|---|---|---|---|
| e0e8fef265 | |||
| deb0f8344b | |||
| ed135950eb | |||
| ed7c547a06 | |||
| 1a9f927c4c | |||
| 350af86b65 | |||
| e136b73192 | |||
| b6bf2fec21 | |||
| 56daadbbca | |||
| 7de26ec9a7 | |||
| 7dbd3036df | |||
| 687085f9c7 | |||
| 96cfc7e184 | |||
| 0b26433b1a | |||
| 4f06bb40f8 | |||
| 46d571b65a | |||
| 7da1bfd315 | |||
| b30f47b5aa | |||
| ccc6c7206c | |||
| a549f4cae0 | |||
| 07c451ce22 | |||
| 62da624369 | |||
| a1a9b03180 | |||
| e4c1d37c35 | |||
| 42cacac256 | |||
| 2816970327 | |||
| 5744fcdcb2 | |||
| 3c412b394d | |||
| 4e64cea373 | |||
| db579db87f | |||
| f612a918d9 | |||
| 1a78e06a7a | |||
| 374a98ffc3 | |||
| d723e2e273 | |||
| 41030171b5 | |||
| aa31706bcc | |||
| 4e3810493d | |||
| eeb2db92ed | |||
| 5a2930029b | |||
| 2e9a73923b | |||
| 97e67f8a88 | |||
| 074849af74 | |||
| 6939783518 | |||
| 260e48fa48 | |||
| 52c4ef5df5 | |||
| 46b634e0fa | |||
| a556c29a17 | |||
| bbc4f508b5 | |||
| fe0243549a | |||
| d9196ba650 | |||
| a0ea1fa392 | |||
| 195bcb4495 | |||
| 929627a191 | |||
| 83a313b4b7 | |||
| f734bfb50f | |||
| 9efc22d195 | |||
| bd3bef5fb0 | |||
| ddbe49e847 | |||
| ed9bddec0b | |||
| 73a07eeb54 | |||
| 6e27aed804 | |||
| 5f955781a6 | |||
| c124f7310e | |||
| 73ffcae549 | |||
| 80e55ba0be | |||
| eecf955096 | |||
| 6426db9a5d | |||
| bf4753cd09 | |||
| a592eaf383 | |||
| 75d654cec3 | |||
| a18ed37600 | |||
| 6747a8dfbc | |||
| 583dfb8126 | |||
| bda31dd086 | |||
| 283af35f3b | |||
| 82564e2580 | |||
| fc6a9026e6 | |||
| e1538bb5ab | |||
| d47db969ea | |||
| f01de37a59 | |||
| cee429a0a0 | |||
| 321ee89676 | |||
| aa60484315 | |||
| b807df3ee8 | |||
| c11e922573 | |||
| 7135a3bbeb | |||
| 142b87dd67 | |||
| dff4579f28 | |||
| 2b3b662a9a | |||
| 98bc5195b1 | |||
| a02912ccbb | |||
| 2f1d29dfd2 | |||
| 11efacc4cb | |||
| 8133d0da3d | |||
| 093d63faae | |||
| 07f5e04488 | |||
| acfc97fc7c | |||
| 6e30554143 | |||
| fa1c788c41 | |||
| 3a688942c3 | |||
| 6797522618 | |||
| 4730a53313 | |||
| 5a538792f0 | |||
| 5f20289767 | |||
| 4c7ed97a76 | |||
| a9c2228e3c | |||
| ec44fcb7cb | |||
| 1f9c8d261b | |||
| 23ab3bd416 | |||
| 79482775d2 | |||
| 2dcb3678b0 | |||
| 8d7c206012 | |||
| c21c264426 | |||
| 77a39c18f1 | |||
| 65a00b0a8e | |||
| 0c2d5679f1 | |||
| 75d0e0de43 | |||
| 19ff864eaa | |||
| 6bbee7cb10 | |||
| e268923cf6 | |||
| 4fb3d7182c | |||
| 6e7a6ec032 | |||
| 0e92c92f87 | |||
| a6f1eb39ec | |||
| 21364f6239 | |||
| a0527094a9 | |||
| eda890c294 | |||
| 1ed8db87cc | |||
| 136892478f | |||
| aee90d31a0 | |||
| 6fe632fda3 | |||
| fa975a2f5b | |||
| bd5ab14d2f | |||
| 32890bfda9 | |||
| a3d4113b97 | |||
| 8acfa722f8 | |||
| 234beba06a | |||
| 90c27a0f73 | |||
| 2577a5d2c2 | |||
| 84bf10b65b | |||
| 5d70f5fd97 | |||
| f42cc59d53 | |||
| d12b222d52 | |||
| b663c7f9d4 | |||
| 30abfa7e23 | |||
| 0be32c34c5 | |||
| 8d4863b894 | |||
| ad1ae8fb54 | |||
| b31e41bfd9 | |||
| 999d5a937d | |||
| 45ea543a4b | |||
| 9d72a5e02d | |||
| 8973c45b4e | |||
| b3e5a5dc62 | |||
| d8bddadd42 | |||
| aa552d1c80 | |||
| 6daac5f442 | |||
| a8c453fbda | |||
| 3e15da5ad7 | |||
| 8e776e7d42 | |||
| de3d5654b4 | |||
| b0e19ffa30 | |||
| bacf451cf5 | |||
| 2a5600b6af | |||
| bfbc78d9b7 | |||
| d509281bdc | |||
| 866fdfb4f6 | |||
| 6c14d0f4c5 | |||
| b31a8e3bf8 | |||
| 7f3261e453 | |||
| 861c3617dd | |||
| 1f4b1f5421 | |||
| f851460a32 | |||
| 992ac6d9eb | |||
| 2e9ac57930 | |||
| ce8d090f27 | |||
| 4ec8b59fd6 | |||
| 6b086af8b4 | |||
| 52f95665ed | |||
| d1ce5ab105 | |||
| 0fd10649f4 | |||
| 3c65900768 | |||
| 0b962b4fee | |||
| a58633fc5f | |||
| 24c2ed3ae3 | |||
| 84a77d98cf | |||
| 20e50e02e9 | |||
| c8bb354972 | |||
| 3f9b73d211 | |||
| 40bbffed17 | |||
| b16ec4e809 | |||
| 9629c895d5 | |||
| f4420fcc03 | |||
| 17f8c3e134 | |||
| 02512a522c | |||
| 4974f99842 | |||
| a3d0c32be3 | |||
| 31523160fc | |||
| 183d5cdeec | |||
| 52fed810f2 | |||
| f58d63da8d | |||
| 6793e6fbdc | |||
| 4f88e9b97d | |||
| 5004cec9b3 | |||
| 76c81e4209 | |||
| 323043dc01 | |||
| 37902ce70a | |||
| c69e795d49 | |||
| c3b82af95a | |||
| 24510d4828 | |||
| 11a04a99f7 | |||
| 6e96eeaa05 | |||
| 2c16d4af5a | |||
| d1f9cf43de | |||
| 26fb11cb30 | |||
| a7a3eb8658 | |||
| fbedcd1f5f | |||
| 6033af0f23 | |||
| 4809786b16 | |||
| a37f40ff6e | |||
| 44f5cc9de0 | |||
| e059d0cbd9 | |||
| c51040b0de | |||
| eba2abc108 | |||
| ed7626009a | |||
| 442413ec6b | |||
| 90986df641 | |||
| 8b505dbce3 | |||
| 2b26e9643b | |||
| eb16f01839 | |||
| 872cf33d1c | |||
| afbce19028 | |||
| e27228e735 | |||
| 3125232114 | |||
| e552840cc4 | |||
| 569142845a | |||
| e62d837858 | |||
| 27badfb88a | |||
| e4d0781eb8 | |||
| 93c8373335 | |||
| 42bad6b4ea | |||
| 83d4bd88cf | |||
| 4141267104 | |||
| d5778f165b | |||
| 7128e26a27 | |||
| 78247e9969 | |||
| 66d36e034e | |||
| 4100ee842c | |||
| d4857c8e6e | |||
| 0f52781489 | |||
| 332f19cd22 | |||
| 53a1c103d6 | |||
| addf7a3e6b | |||
| c76078ea06 | |||
| 9299288e8a | |||
| dc34f24ee2 | |||
| fe83bf1483 | |||
| c184f9281d | |||
| 3127a69b67 | |||
| d7326c4f82 | |||
| f8478284b6 | |||
| 1c910de4b1 | |||
| 2c3cb955ef | |||
| 1d7746d839 | |||
| e16d2ba238 | |||
| 60f8b448ec | |||
| c0d61f29a8 | |||
| c47b99f9ee | |||
| 653429a6d6 | |||
| c2250e1817 | |||
| 92ca48d65f | |||
| 2f53d9a79d | |||
| d025bfe2da | |||
| fbc2460ac4 | |||
| 76c2800275 | |||
| 536bf984ee | |||
| 23f8af43ee | |||
| eba54bf2e7 | |||
| 50270e6541 | |||
| 5f7d9e80ee | |||
| a71ef35d5c | |||
| 2cf5291831 | |||
| 75e3e9dd7b | |||
| 7a75365d2f | |||
| 7a23d59aa1 | |||
| 2583423600 | |||
| 1d748a0424 | |||
| 86172790d1 | |||
| 92c9103059 | |||
| 16393dbc9d | |||
| e841b0ef03 | |||
| 27c7ae9d76 | |||
| dc3a40675d | |||
| 31e65ac2e3 | |||
| c2444d1b09 | |||
| 2faadae4cf | |||
| d24e51c355 | |||
| 8ed8af1b9c | |||
| 5f6b416f26 | |||
| 3671c05573 | |||
| db6eed9292 | |||
| 4c903ba0bb | |||
| 9eee216065 | |||
| 9e1dd02d70 | |||
| b986bf9bcc | |||
| a3e0e62efc | |||
| 8646c5acf6 | |||
| dab9465755 | |||
| 3a076cc4ea | |||
| 1b6d1ea0ef | |||
| c490284c65 | |||
| c3ca329138 | |||
| d0f73c32bb | |||
| 79e0da7cf2 | |||
| b261f505aa | |||
| 65c4c95cbe | |||
| 5b3c9c9055 | |||
| d32d4f845c | |||
| 637950d37f | |||
| 0e1ede58e5 | |||
| e914a11768 | |||
| 73d66dab35 | |||
| 634766a08e | |||
| 5fbc450e96 | |||
| 5acd7ae2c4 | |||
| 09578ddc5d | |||
| 8270801711 | |||
| de91c3cfc8 | |||
| 06888a2061 | |||
| cde2d33d03 | |||
| 9cf89f95d1 | |||
| 20486e15ea | |||
| 420c403da1 | |||
| cb8fdf3461 | |||
| 0937d08770 | |||
| 9b1fd8e1aa | |||
| d87861142c | |||
| ff5fc1d0f3 | |||
| d5a671cbb8 | |||
| d0508cbd96 | |||
| 12098cf8ac | |||
| 2557c35bea | |||
| 11f23bbd77 | |||
| 2b2ec729ac | |||
| 9292acec8c | |||
| 3095ed5375 | |||
| a333e303c1 | |||
| bd8512feab | |||
| c235fc4a7a | |||
| 9c283b9aee | |||
| 4e824d739b | |||
| fcbbc792b0 | |||
| ba7cf8e79e | |||
| 4ab49d8e43 | |||
| 7f864db80d | |||
| 772a163693 | |||
| c8e106f272 | |||
| ade4f0a1d4 | |||
| 4fcf933dd1 | |||
| 1fb60771ca | |||
| 2cd391e106 | |||
| 196f3bcba1 | |||
| bc73c5ca86 | |||
| f4badc15d1 | |||
| 76f731d0cd | |||
| 5e22858ab4 | |||
| bc1c54c43e | |||
| 95ac0d36f8 | |||
| 2bee3eb179 | |||
| 52165f2a0b | |||
| 9f2da226f4 | |||
| 46b999317d | |||
| 80bce3c9c2 | |||
| 90b25b4bd8 | |||
| f562f1eafd | |||
| ff520622ff | |||
| 1a6c6a4f31 | |||
| 4a9e0388f4 | |||
| e47b2a971e | |||
| 96777ec095 | |||
| 52019deef5 | |||
| c26a534ff6 | |||
| e5be5c0b05 | |||
| 5b07f52641 | |||
| e0109cb530 | |||
| 1496dd1598 | |||
| efcaeb2f00 | |||
| 1252946fe5 | |||
| 2913d5f636 | |||
| c50beb3d43 | |||
| 46b559001a | |||
| 55a2e2e060 | |||
| 12364a052f | |||
| 45ac971035 | |||
| 26b818c9a6 | |||
| 007836f52e | |||
| ed0982770b | |||
| dcc72e90b7 | |||
| b428660944 | |||
| 0008dcc0ee | |||
| e3fbf3981d | |||
| c5e9244a08 | |||
| e1fd823c13 | |||
| 346f0d636c | |||
| 315a259e5b | |||
| 3157a03c74 | |||
| 045807a748 | |||
| ea0e12ac80 | |||
| 692060af6c | |||
| d8b23a4b5c | |||
| 074bf55d6c | |||
| 84e505b817 | |||
| a12e6ff603 | |||
| b01d662a1c | |||
| e631dad499 | |||
| 7ab0bfdd6b | |||
| 35a4db43fb | |||
| 094028006b | |||
| c12f42fcac | |||
| 3e498b098b | |||
| bbf1a1a544 | |||
| 42d57f30bf | |||
| 19967a9ad0 | |||
| 9d1a8f12d8 | |||
| 57bebebe09 | |||
| d19303cd62 | |||
| 7c96e46a37 | |||
| f75e68f8ce | |||
| c8d9ee2457 | |||
| a29f23086a | |||
| 4a76365df4 | |||
| 972264844a | |||
| e23e6f3c1a | |||
| 03c10382d1 | |||
| 3abe24aa18 |
@@ -1,5 +1,6 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
globs: **/desktop/**
|
||||
---
|
||||
# DESKTOP
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
globs: notifications/src/workflows/**/*.ts
|
||||
---
|
||||
# Правила валидации воркфлоу
|
||||
|
||||
## 🚫 ID воркфлоу
|
||||
- **Длина ID не должна превышать 32 символа**
|
||||
- Используйте описательные, но короткие идентификаторы малыми латинскими буквами и тире.
|
||||
|
||||
## 🚫 Условия в шаблонах
|
||||
- **Запрещено использовать JavaScript выражения в шаблонах Novu**
|
||||
- Нельзя использовать:
|
||||
- Тернарные операторы: `{{condition ? "text1" : "text2"}}`
|
||||
- Логические операторы: `{{field && "text"}}`
|
||||
- Любые другие JS конструкции
|
||||
|
||||
## ✅ Рекомендации
|
||||
- Добавляйте текстовые поля в payload для условной логики
|
||||
- Вычисляйте значения на стороне сервера перед отправкой уведомления
|
||||
- Используйте только простые переменные: `{{payload.fieldName}}`
|
||||
@@ -2,8 +2,6 @@ name: Build Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- testnet
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
@@ -28,12 +26,23 @@ jobs:
|
||||
echo "Проверяем файлы в middlewares:"
|
||||
ls -la components/desktop/src-ssr/middlewares/ || echo "Директория middlewares не найдена!"
|
||||
|
||||
- name: Set docker tag
|
||||
- name: Set docker tags
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo "DOCKER_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||
echo "DOCKER_TAG=$TAG_NAME" >> $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
|
||||
else
|
||||
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
|
||||
echo "IS_PRODUCTION_TAG=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Login to DockerHub
|
||||
@@ -47,6 +56,12 @@ jobs:
|
||||
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
|
||||
@@ -55,6 +70,11 @@ jobs:
|
||||
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
|
||||
run: |
|
||||
@@ -62,6 +82,11 @@ jobs:
|
||||
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
|
||||
|
||||
- name: Build parser image
|
||||
run: |
|
||||
@@ -69,6 +94,11 @@ jobs:
|
||||
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
|
||||
run: |
|
||||
@@ -76,6 +106,11 @@ jobs:
|
||||
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
|
||||
run: |
|
||||
@@ -83,35 +118,41 @@ jobs:
|
||||
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
|
||||
|
||||
# Отправка хука для деплоя
|
||||
- name: Trigger deployment webhook
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/latest ]]; then
|
||||
# Хук для тестнета
|
||||
curl -X POST ${{ vars.TESTNET_WEBHOOK_URL }} \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '${{ env.DOCKER_TAG }}'
|
||||
if [[ $GITHUB_REF == refs/tags/*alpha* ]]; then
|
||||
# Хук для тестнета (alpha теги)
|
||||
curl -X POST ${{ vars.TESTNET_WEBHOOK_URL }} \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '${{ env.DOCKER_TAG }}'
|
||||
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
# Хук для продакшена
|
||||
curl -X POST ${{ vars.PRODUCTION_WEBHOOK_URL }} \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '${{ env.DOCKER_TAG }}'
|
||||
else
|
||||
# Хук для тестнета (обычная ветка)
|
||||
curl -X POST ${{ vars.TESTNET_WEBHOOK_URL }} \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '${{ env.DOCKER_TAG }}'
|
||||
# Хук для продакшена (остальные теги)
|
||||
curl -X POST ${{ vars.PRODUCTION_WEBHOOK_URL }} \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '${{ env.DOCKER_TAG }}'
|
||||
fi
|
||||
|
||||
# Уведомление в Telegram об успехе
|
||||
- name: Telegram notify success
|
||||
if: ${{ success() }}
|
||||
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 ($GITHUB_REF) [${{ env.DOCKER_TAG }}]"
|
||||
-d text="✅ [GITHUB MONO] Успешная сборка контейнеров: $GITHUB_REPOSITORY ($GITHUB_REF) [${{ env.DOCKER_TAG }}]$ADDITIONAL_INFO"
|
||||
|
||||
# Уведомление в Telegram об ошибке
|
||||
- name: Telegram notify failure
|
||||
|
||||
@@ -4,6 +4,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- testnet
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build-and-publish-docs:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -81,6 +84,18 @@ jobs:
|
||||
mkdocs build
|
||||
working-directory: ./components/docs
|
||||
|
||||
- name: Remove specific large file before publishing
|
||||
run: |
|
||||
# Удаляем конкретный большой файл sdk/typedoc.json
|
||||
rm -f ./components/docs/site/sdk/typedoc.json
|
||||
# Проверяем, что файл удален
|
||||
if [ -f "./components/docs/site/sdk/typedoc.json" ]; then
|
||||
echo "ERROR: typedoc.json still exists!"
|
||||
exit 1
|
||||
else
|
||||
echo "SUCCESS: typedoc.json removed successfully"
|
||||
fi
|
||||
|
||||
- name: Publish to GitHub Pages
|
||||
run: npx gh-pages --nojekyll -d site --repo https://x-access-token:${GITHUB_TOKEN}@github.com/coopenomics/mono.git
|
||||
working-directory: ./components/docs
|
||||
@@ -89,5 +104,4 @@ jobs:
|
||||
GIT_AUTHOR_EMAIL: github-actions@github.com
|
||||
GIT_COMMITTER_NAME: github-actions
|
||||
GIT_COMMITTER_EMAIL: github-actions@github.com
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -3,31 +3,29 @@ name: Publish Packages
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v') &&
|
||||
!contains(github.ref, '-alpha')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build all packages
|
||||
run: pnpm lerna run build
|
||||
|
||||
- name: Publish to npm
|
||||
- run: npm install -g pnpm
|
||||
- run: pnpm install
|
||||
- run: pnpm lerna run build
|
||||
- run: pnpm lerna publish from-package --yes
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: pnpm lerna publish from-package --yes --no-verify-access
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"plugins": [
|
||||
"prettier-plugin-sort-imports"
|
||||
]
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"vue.volar",
|
||||
"vue.vscode-typescript-vue-plugin"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+23
-8
@@ -13,9 +13,7 @@
|
||||
|
||||
// Оптимизация для монорепозитория
|
||||
"typescript.preferences.useAliasesForRenames": false,
|
||||
"typescript.preferences.include": false,
|
||||
"typescript.disableAutomaticTypeAcquisition": true,
|
||||
"typescript.preferences.includePackageJsonAutoImports": "off",
|
||||
"typescript.preferences.includePackageJsonAutoImports": "on",
|
||||
"typescript.suggest.autoImports": true,
|
||||
"typescript.suggest.paths": true,
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
@@ -47,13 +45,30 @@
|
||||
"**/node_modules/.cache": true
|
||||
},
|
||||
|
||||
// TypeScript server настройки
|
||||
// TypeScript server настройки для монорепозитория
|
||||
"typescript.tsserver.maxTsServerMemory": 8192,
|
||||
"typescript.tsserver.watchOptions": {
|
||||
"excludeFiles": [
|
||||
"**/node_modules/**/*",
|
||||
"**/dist/**/*",
|
||||
"**/.cache/**/*"
|
||||
"excludeDirectories": [
|
||||
"**/node_modules",
|
||||
"**/dist",
|
||||
"**/.cache",
|
||||
"**/.quasar",
|
||||
"**/build"
|
||||
]
|
||||
},
|
||||
|
||||
// Использовать локальный TypeScript из монорепо
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
|
||||
// КРИТИЧЕСКИ ВАЖНО: включить project references для монорепозитория
|
||||
"typescript.tsserver.useSyntaxServer": "auto",
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
||||
|
||||
// Оптимизация для больших монорепозиториев
|
||||
"typescript.disableAutomaticTypeAcquisition": true,
|
||||
"typescript.surveys.enabled": false,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
}
|
||||
}
|
||||
|
||||
+123
@@ -1,3 +1,126 @@
|
||||
# v2025.12.28-8
|
||||
|
||||
В этой версии представлен прототип трекера результатов интеллектуальной деятельности, реализован мост в 1С, обновлен интерфейс и существенно повышена стабильность системы.
|
||||
|
||||
---
|
||||
|
||||
### ✨ Новые функции
|
||||
- [#332](https://github.com/coopenomics/mono/issues/332): Прототип конструктора требований дополнительных документов при регистрации
|
||||
- [#330](https://github.com/coopenomics/mono/issues/330): Прототип моста в 1С:Бухгалтерию для передачи документов и проводок
|
||||
- [#329](https://github.com/coopenomics/mono/issues/329): Интеграция и тестирование LMS TUTOR для образовательных задач
|
||||
- [#328](https://github.com/coopenomics/mono/issues/328): Размещение прототипов мульти-лендингов на цифровой-кооператив.рф и coopenomics.world
|
||||
- [#326](https://github.com/coopenomics/mono/issues/326): Минимальный интерфейс трекера результатов интеллектуальной деятельности
|
||||
- [#324](https://github.com/coopenomics/mono/issues/324): Смарт-контракт генерации и капитализации результатов интеллектуальной деятельности ("Благорост")
|
||||
- [#322](https://github.com/coopenomics/mono/issues/322): Поставка обновлений ПО с нулевым даунтаймом по blue-green стратегии
|
||||
- [#321](https://github.com/coopenomics/mono/issues/321): Внедрение системы проводок по фондам для контрактов
|
||||
- [#319](https://github.com/coopenomics/mono/issues/319): Палитра команд и быстрый доступ к страницам рабочих столов (cmk+k)
|
||||
- [#316](https://github.com/coopenomics/mono/issues/316): Переход рабочего стола на GraphQL SDK
|
||||
- [#314](https://github.com/coopenomics/mono/issues/314): Развёртывание GlitchTip для мониторинга ошибок
|
||||
- [#306](https://github.com/coopenomics/mono/issues/306): Модуль запросов и мутаций для контракта капитализации
|
||||
|
||||
### 🐛 Исправления ошибок
|
||||
- [#312](https://github.com/coopenomics/mono/issues/312): Исправление подписки на изменение статуса коммитов
|
||||
- [#309](https://github.com/coopenomics/mono/issues/309): Исправление отображения чужих билетов времени в трекере
|
||||
|
||||
### 🔧 Улучшения
|
||||
- [#331](https://github.com/coopenomics/mono/issues/331): Настройка системы мониторинга сбоев и ошибок на базе GlitchTIP, Loki, Prometheus
|
||||
- [#327](https://github.com/coopenomics/mono/issues/327): Пользовательская документация по интерфейсам цифрового кооператива
|
||||
- [#325](https://github.com/coopenomics/mono/issues/325): Документирование смарт-контракта программы "Благорост"
|
||||
- [#308](https://github.com/coopenomics/mono/issues/308): Улучшение отображения рабочих столов в магазине приложений
|
||||
- [#307](https://github.com/coopenomics/mono/issues/307): Объединение настроек контракта с нативными настройками приложения
|
||||
- [#305](https://github.com/coopenomics/mono/issues/305): Объединение полей title и description в проекте
|
||||
- [#304](https://github.com/coopenomics/mono/issues/304): Доменная модель контракта капитализации на бэкенде
|
||||
- [#303](https://github.com/coopenomics/mono/issues/303): Пересмотр архитектуры парсера и формирования локальной истории
|
||||
- [#302](https://github.com/coopenomics/mono/issues/302): Рефакторинг архитектуры, внедрение двухконтурной шины данных и обработки микрофорков
|
||||
- [#301](https://github.com/coopenomics/mono/issues/301): Доработка и отладка контракта "Капитализация РИД" v0.2
|
||||
- [#222](https://github.com/coopenomics/mono/issues/222): Внедрение метода Водянова для распределения пула премий по программе "Благорост"
|
||||
- [#212](https://github.com/coopenomics/mono/issues/212): Снижение точности валютных значений до двух знаков после запятой в документах
|
||||
|
||||
#releases
|
||||
|
||||
---
|
||||
|
||||
# v2025.12.28
|
||||
|
||||
В этой версии представлен прототип трекера результатов интеллектуальной деятельности, реализован мост в 1С, обновлен интерфейс и существенно повышена стабильность системы.
|
||||
|
||||
---
|
||||
|
||||
### ✨ Новые функции
|
||||
- [#332](https://github.com/coopenomics/mono/issues/332): Прототип конструктора требований дополнительных документов при регистрации
|
||||
- [#330](https://github.com/coopenomics/mono/issues/330): Прототип моста в 1С:Бухгалтерию для передачи документов и проводок
|
||||
- [#329](https://github.com/coopenomics/mono/issues/329): Интеграция и тестирование LMS TUTOR для образовательных задач
|
||||
- [#328](https://github.com/coopenomics/mono/issues/328): Размещение прототипов мульти-лендингов на цифровой-кооператив.рф и coopenomics.world
|
||||
- [#326](https://github.com/coopenomics/mono/issues/326): Минимальный интерфейс трекера результатов интеллектуальной деятельности
|
||||
- [#324](https://github.com/coopenomics/mono/issues/324): Смарт-контракт генерации и капитализации результатов интеллектуальной деятельности ("Благорост")
|
||||
- [#322](https://github.com/coopenomics/mono/issues/322): Поставка обновлений ПО с нулевым даунтаймом по blue-green стратегии
|
||||
- [#321](https://github.com/coopenomics/mono/issues/321): Внедрение системы проводок по фондам для контрактов
|
||||
- [#319](https://github.com/coopenomics/mono/issues/319): Палитра команд и быстрый доступ к страницам рабочих столов (cmk+k)
|
||||
- [#316](https://github.com/coopenomics/mono/issues/316): Переход рабочего стола на GraphQL SDK
|
||||
- [#314](https://github.com/coopenomics/mono/issues/314): Развёртывание GlitchTip для мониторинга ошибок
|
||||
- [#306](https://github.com/coopenomics/mono/issues/306): Модуль запросов и мутаций для контракта капитализации
|
||||
|
||||
### 🐛 Исправления ошибок
|
||||
- [#312](https://github.com/coopenomics/mono/issues/312): Исправление подписки на изменение статуса коммитов
|
||||
- [#309](https://github.com/coopenomics/mono/issues/309): Исправление отображения чужих билетов времени в трекере
|
||||
|
||||
### 🔧 Улучшения
|
||||
- [#331](https://github.com/coopenomics/mono/issues/331): Настройка системы мониторинга сбоев и ошибок на базе GlitchTIP, Loki, Prometheus
|
||||
- [#327](https://github.com/coopenomics/mono/issues/327): Пользовательская документация по интерфейсам цифрового кооператива
|
||||
- [#325](https://github.com/coopenomics/mono/issues/325): Документирование смарт-контракта программы "Благорост"
|
||||
- [#308](https://github.com/coopenomics/mono/issues/308): Улучшение отображения рабочих столов в магазине приложений
|
||||
- [#307](https://github.com/coopenomics/mono/issues/307): Объединение настроек контракта с нативными настройками приложения
|
||||
- [#305](https://github.com/coopenomics/mono/issues/305): Объединение полей title и description в проекте
|
||||
- [#304](https://github.com/coopenomics/mono/issues/304): Доменная модель контракта капитализации на бэкенде
|
||||
- [#303](https://github.com/coopenomics/mono/issues/303): Пересмотр архитектуры парсера и формирования локальной истории
|
||||
- [#302](https://github.com/coopenomics/mono/issues/302): Рефакторинг архитектуры, внедрение двухконтурной шины данных и обработки микрофорков
|
||||
- [#301](https://github.com/coopenomics/mono/issues/301): Доработка и отладка контракта "Капитализация РИД" v0.2
|
||||
- [#222](https://github.com/coopenomics/mono/issues/222): Внедрение метода Водянова для распределения пула премий по программе "Благорост"
|
||||
- [#212](https://github.com/coopenomics/mono/issues/212): Снижение точности валютных значений до двух знаков после запятой в документах
|
||||
|
||||
#releases
|
||||
|
||||
---
|
||||
|
||||
# v2025.12.28
|
||||
|
||||
В этом релизе реализован смарт-контракт генерации и капитализации результатов интеллектуальной деятельности, завершена интеграция с учётными системами, улучшены интерфейсы и документация. Подробнее о контракте: https://coopenomics.world/contracts/group__public__capital.html
|
||||
|
||||
✨ Новые функции
|
||||
- [#324](https://github.com/coopenomics/mono/issues/324): Реализован смарт-контракт генерации и капитализации результатов интеллектуальной деятельности ("Благорост")
|
||||
- [#301](https://github.com/coopenomics/mono/issues/301): Контракт "Капитализация РИД" v0.2
|
||||
- [#330](https://github.com/coopenomics/mono/issues/330): Прототип моста в 1С:Бухгалтерию: выгрузка документов и проводки по счетам
|
||||
- [#322](https://github.com/coopenomics/mono/issues/322): Обновления ПО с нулевым даунтаймом по blue-green стратегии
|
||||
- [#319](https://github.com/coopenomics/mono/issues/319): Палитра команд и быстрый доступ к страницам рабочих столов (cmk+k)
|
||||
- [#308](https://github.com/coopenomics/mono/issues/308): Магазин приложений с поддержкой подключения нескольких рабочих столов одним приложением
|
||||
- [#326](https://github.com/coopenomics/mono/issues/326): Минимальный интерфейс трекера результатов интеллектуальной деятельности по программе "Благорост"
|
||||
- [#332](https://github.com/coopenomics/mono/issues/332): Прототип конструктора требований дополнительных документов при регистрации
|
||||
|
||||
🐛 Исправления ошибок
|
||||
- [#312](https://github.com/coopenomics/mono/issues/312): Исправлена ошибка со статусом коммитов — подписка теперь работает корректно
|
||||
- [#309](https://github.com/coopenomics/mono/issues/309): Исправлен баг с отображением чужих билетов времени в трекере
|
||||
|
||||
🔧 Улучшения
|
||||
- [#325](https://github.com/coopenomics/mono/issues/325): Документирован смарт-контракт программы "Благорост"
|
||||
- [#327](https://github.com/coopenomics/mono/issues/327): Подготовлена пользовательская документация цифрового кооператива по интерфейсам
|
||||
- [#329](https://github.com/coopenomics/mono/issues/329): Интеграция и тестирование образовательной платформы LMS TUTOR на Wordpress
|
||||
- [#328](https://github.com/coopenomics/mono/issues/328): Размещены прототипы мульти-лендингов на цифровой-кооператив.рф и coopenomics.world
|
||||
- [#321](https://github.com/coopenomics/mono/issues/321): Встроена система проводок по фондам и интеграция с контрактами
|
||||
- [#318](https://github.com/coopenomics/mono/issues/318): Настроены Loki & Grafana для выгрузки логов из контейнеров
|
||||
- [#316](https://github.com/coopenomics/mono/issues/316): Завершён переход рабочего стола на GraphQL SDK
|
||||
- [#314](https://github.com/coopenomics/mono/issues/314): Развёрнут GlitchTip как альтернатива Sentry
|
||||
- [#307](https://github.com/coopenomics/mono/issues/307): Интеграция настроек контракта с нативными настройками приложения, поддержка импорта после конфигурации
|
||||
- [#306](https://github.com/coopenomics/mono/issues/306): Собран модуль запросов и мутаций контракта капитализации
|
||||
- [#305](https://github.com/coopenomics/mono/issues/305): Упрощена структура проекта — title & description объединены в одно поле
|
||||
- [#304](https://github.com/coopenomics/mono/issues/304): Реализована доменная модель контракта капитализации на бэкенде с поддержкой микрофорков
|
||||
- [#303](https://github.com/coopenomics/mono/issues/303): Пересмотрена архитектура парсера и формирования локальной истории
|
||||
- [#302](https://github.com/coopenomics/mono/issues/302): Рефакторинг архитектуры, реализована двухконтурная шина данных и обработка микрофорков
|
||||
- [#212](https://github.com/coopenomics/mono/issues/212): Уменьшена точность валютных значений в документах с четырёх до двух знаков
|
||||
|
||||
#releases
|
||||
|
||||
---
|
||||
|
||||
# v2025.9.1
|
||||
|
||||
В системе Кооперативной Экономики развернут смарт-контракт CAPITAL v0.2 для генерации и капитализации результатов интеллектуальной деятельности. Контракт описывает и обеспечивает:
|
||||
|
||||
+19
-13
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine AS builder
|
||||
FROM node:20-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -12,23 +12,29 @@ RUN npm install -g pnpm lerna
|
||||
# Используем версию pnpm, совместимую с существующим lock-файлом
|
||||
RUN pnpm install
|
||||
|
||||
# Установка системных зависимостей для WeasyPrint
|
||||
RUN apk add --no-cache \
|
||||
# Установка системных зависимостей для WeasyPrint и диагностических утилит (Debian/Ubuntu версии)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
py3-pip \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
gcc \
|
||||
musl-dev \
|
||||
python3-dev \
|
||||
pango \
|
||||
zlib-dev \
|
||||
jpeg-dev \
|
||||
openjpeg-dev \
|
||||
g++ \
|
||||
python3-dev \
|
||||
libpango-1.0-0 \
|
||||
libpangoft2-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libcairo2 \
|
||||
libcairo2-dev \
|
||||
libffi-dev \
|
||||
harfbuzz-subset \
|
||||
shared-mime-info \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
libopenjp2-7-dev \
|
||||
procps \
|
||||
wget \
|
||||
&& python3 -m venv /venv \
|
||||
&& /venv/bin/pip install WeasyPrint==62.3 \
|
||||
&& rm -rf /var/cache/*
|
||||
&& /venv/bin/pip install WeasyPrint==67 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Сборка всех компонентов
|
||||
RUN lerna run build
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# MONO
|
||||
# Цифровой Кооператив
|
||||
|
||||
Система управления взаимоотношениями в кооперативе. Включает в себя полный комплект программного обеспечения для подключения к платформе "Кооперативная Экономика" и управления взаимоотношениями с пайщиками в кооперативе и самим кооперативом на основе смарт-контрактов и простой электронной подписи.
|
||||
|
||||
|
||||
@@ -8,3 +8,8 @@ SERVER_URL=http://127.0.0.1:2998
|
||||
MONGO_URI=mongodb://127.0.0.1:27017/cooperative-x
|
||||
SIMPLE_EXPLORER_API=http://localhost:4000
|
||||
SKIP_BLOCK_FETCH=TRUE
|
||||
POSTGRES_HOST=127.0.0.1
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USERNAME=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DATABASE=voskhod
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
// Enable the ESlint flat config support
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSave": false,
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/boot",
|
||||
"type": "module",
|
||||
"version": "2025.9.1",
|
||||
"version": "2026.2.22-alpha-4",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"description": "",
|
||||
@@ -15,12 +15,18 @@
|
||||
"cleos": "esno src/index.ts cleos",
|
||||
"unlock": "esno src/index.ts unlock",
|
||||
"boot": "esno src/index.ts boot",
|
||||
"clear": "esno src/index.ts clear",
|
||||
"boot:clean": "esno src/index.ts boot:clean",
|
||||
"boot:extra": "esno src/index.ts boot:extra",
|
||||
"reboot:clean": "cd components/boot/scripts && bash clean_reboot.sh",
|
||||
"reboot:extra": "cd components/boot/scripts && bash extra_reboot.sh",
|
||||
"create-coop": "esno src/index.ts create-coop",
|
||||
"clear": "./scripts/clear.sh",
|
||||
"start": "./scripts/restart.sh",
|
||||
"stop": "./scripts/stop.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coopenomics/factory": "workspace:*",
|
||||
"@types/pg": "^8.16.0",
|
||||
"axios": "^1.6.8",
|
||||
"chai": "^5.1.2",
|
||||
"commander": "^12.1.0",
|
||||
@@ -32,7 +38,8 @@
|
||||
"eosjs-ecc": "^4.0.7",
|
||||
"execa": "^9.5.2",
|
||||
"mocha": "^10.7.3",
|
||||
"mongoose": "^8.10.0"
|
||||
"mongoose": "^8.10.0",
|
||||
"pg": "^8.16.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.18.0",
|
||||
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Останавливаем и удаляем контейнеры вместе с volumes
|
||||
echo "Останавливаем и удаляем контейнеры с volumes..."
|
||||
docker compose down -v mongo postgres cooparser || true
|
||||
|
||||
# Останавливаем blockchain контейнер перед удалением данных
|
||||
echo "Останавливаем blockchain контейнер..."
|
||||
docker compose stop node || true
|
||||
|
||||
# Удаляем blockchain data
|
||||
echo "Удаляем blockchain data..."
|
||||
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
|
||||
sudo rm -rf ../blockchain-data/
|
||||
|
||||
# Пересоздаем и запускаем базы данных
|
||||
echo "Пересоздаем и запускаем базы данных..."
|
||||
docker compose up -d mongo postgres
|
||||
|
||||
# Ждем готовности MongoDB
|
||||
echo "Ждем готовности MongoDB..."
|
||||
until docker exec mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
|
||||
echo "MongoDB еще не готов, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
echo "MongoDB готов!"
|
||||
|
||||
# Ждем готовности PostgreSQL
|
||||
echo "Ждем готовности PostgreSQL..."
|
||||
until docker exec postgres pg_isready -U postgres -d voskhod > /dev/null 2>&1; do
|
||||
echo "PostgreSQL еще не готов, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL готов!"
|
||||
|
||||
# Запускаем boot процесс
|
||||
echo "Запускаем boot процесс..."
|
||||
pnpm run boot:clean
|
||||
|
||||
# Запускаем parser
|
||||
echo "Запускаем parser..."
|
||||
docker compose up -d cooparser
|
||||
|
||||
echo "Запускаем контроллер..."
|
||||
docker compose restart coopback || true
|
||||
|
||||
echo "Перезапуск завершен!"
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Останавливаем и удаляем контейнеры вместе с volumes
|
||||
echo "Останавливаем и удаляем контейнеры с volumes..."
|
||||
docker compose down -v mongo postgres cooparser || true
|
||||
|
||||
# Останавливаем blockchain контейнер перед удалением данных
|
||||
echo "Останавливаем blockchain контейнер..."
|
||||
docker compose stop node || true
|
||||
|
||||
# Удаляем blockchain data
|
||||
echo "Удаляем blockchain data..."
|
||||
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
|
||||
sudo rm -rf ../blockchain-data/
|
||||
|
||||
# Пересоздаем и запускаем базы данных
|
||||
echo "Пересоздаем и запускаем базы данных..."
|
||||
docker compose up -d mongo postgres
|
||||
|
||||
# Ждем готовности MongoDB
|
||||
echo "Ждем готовности MongoDB..."
|
||||
until docker exec mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
|
||||
echo "MongoDB еще не готов, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
echo "MongoDB готов!"
|
||||
|
||||
# Ждем готовности PostgreSQL
|
||||
echo "Ждем готовности PostgreSQL..."
|
||||
until docker exec postgres pg_isready -U postgres -d voskhod > /dev/null 2>&1; do
|
||||
echo "PostgreSQL еще не готов, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL готов!"
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Останавливаем контроллер перед очисткой данных
|
||||
echo "Останавливаем контроллер..."
|
||||
docker compose down coopback || true
|
||||
|
||||
# Останавливаем и удаляем контейнеры вместе с volumes
|
||||
echo "Останавливаем и удаляем контейнеры с volumes..."
|
||||
docker compose down -v mongo postgres cooparser || true
|
||||
|
||||
# Останавливаем blockchain контейнер перед удалением данных
|
||||
echo "Останавливаем blockchain контейнер..."
|
||||
docker compose stop node || true
|
||||
|
||||
# Удаляем blockchain data
|
||||
echo "Удаляем blockchain data..."
|
||||
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
|
||||
sudo rm -rf ../blockchain-data/
|
||||
|
||||
# Пересоздаем и запускаем базы данных
|
||||
echo "Пересоздаем и запускаем базы данных..."
|
||||
docker compose up -d mongo postgres
|
||||
|
||||
# Ждем готовности MongoDB
|
||||
echo "Ждем готовности MongoDB..."
|
||||
until docker exec mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
|
||||
echo "MongoDB еще не готов, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
echo "MongoDB готов!"
|
||||
|
||||
# Ждем готовности PostgreSQL
|
||||
echo "Ждем готовности PostgreSQL..."
|
||||
until docker exec postgres pg_isready -U postgres -d voskhod > /dev/null 2>&1; do
|
||||
echo "PostgreSQL еще не готов, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL готов!"
|
||||
|
||||
# Запускаем boot процесс
|
||||
echo "Запускаем boot процесс..."
|
||||
pnpm run boot:extra
|
||||
|
||||
# Запускаем parser
|
||||
echo "Запускаем parser..."
|
||||
docker compose up -d cooparser
|
||||
|
||||
echo "Запускаем контроллер..."
|
||||
docker compose up -d coopback
|
||||
|
||||
echo "Перезапуск завершен!"
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Останавливаем и удаляем контейнеры вместе с volumes
|
||||
echo "Останавливаем и удаляем контейнеры с volumes..."
|
||||
docker compose down -v mongo postgres cooparser || true
|
||||
|
||||
# Останавливаем blockchain контейнер перед удалением данных
|
||||
echo "Останавливаем blockchain контейнер..."
|
||||
docker compose stop node || true
|
||||
|
||||
# Удаляем blockchain data
|
||||
echo "Удаляем blockchain data..."
|
||||
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
|
||||
sudo rm -rf ../blockchain-data/
|
||||
|
||||
# Пересоздаем и запускаем базы данных
|
||||
echo "Пересоздаем и запускаем базы данных..."
|
||||
docker compose up -d mongo postgres
|
||||
|
||||
# Ждем готовности MongoDB
|
||||
echo "Ждем готовности MongoDB..."
|
||||
until docker exec mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
|
||||
echo "MongoDB еще не готов, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
echo "MongoDB готов!"
|
||||
|
||||
# Ждем готовности PostgreSQL
|
||||
echo "Ждем готовности PostgreSQL..."
|
||||
until docker exec postgres pg_isready -U postgres -d voskhod > /dev/null 2>&1; do
|
||||
echo "PostgreSQL еще не готов, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL готов!"
|
||||
|
||||
# Запускаем boot процесс
|
||||
echo "Запускаем boot процесс..."
|
||||
pnpm run boot
|
||||
|
||||
# Запускаем parser
|
||||
echo "Запускаем parser..."
|
||||
docker compose up -d cooparser
|
||||
|
||||
echo "Запускаем контроллер..."
|
||||
docker compose restart coopback || true
|
||||
|
||||
echo "Перезапуск завершен!"
|
||||
@@ -44,8 +44,8 @@ export default class Blockchain {
|
||||
this.api = new Api({
|
||||
rpc,
|
||||
signatureProvider,
|
||||
textDecoder: new TextDecoder(),
|
||||
textEncoder: new TextEncoder(),
|
||||
textDecoder: new TextDecoder() as any,
|
||||
textEncoder: new TextEncoder() as any,
|
||||
})
|
||||
this.api.read = await EosApi({ httpEndpoint: res })
|
||||
}
|
||||
@@ -637,6 +637,66 @@ export default class Blockchain {
|
||||
console.log('Новый пользователь: ', params)
|
||||
}
|
||||
|
||||
async addUser(
|
||||
params: RegistratorContract.Actions.AddUser.IAddUser,
|
||||
) {
|
||||
await this.update_pass_instance()
|
||||
console.dir(params, { depth: null })
|
||||
await this.api.transact(
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
account: RegistratorContract.contractName.production,
|
||||
name: RegistratorContract.Actions.AddUser.actionName,
|
||||
authorization: [
|
||||
{
|
||||
actor: params.coopname,
|
||||
permission: 'active',
|
||||
},
|
||||
],
|
||||
data: params,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
blocksBehind: 3,
|
||||
expireSeconds: 30,
|
||||
},
|
||||
)
|
||||
|
||||
console.log('Добавлен пользователь: ', params.username)
|
||||
}
|
||||
|
||||
async changeKey(
|
||||
params: RegistratorContract.Actions.ChangeKey.IChangeKey,
|
||||
) {
|
||||
await this.update_pass_instance()
|
||||
console.dir(params, { depth: null })
|
||||
await this.api.transact(
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
account: RegistratorContract.contractName.production,
|
||||
name: RegistratorContract.Actions.ChangeKey.actionName,
|
||||
authorization: [
|
||||
{
|
||||
actor: params.coopname,
|
||||
permission: 'active',
|
||||
},
|
||||
],
|
||||
data: params,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
blocksBehind: 3,
|
||||
expireSeconds: 30,
|
||||
},
|
||||
)
|
||||
|
||||
console.log('Изменён ключ для пользователя: ', params.username)
|
||||
}
|
||||
|
||||
async transfer(params: TokenContract.Actions.Transfer.ITransfer) {
|
||||
await this.update_pass_instance()
|
||||
console.dir(params, { depth: null })
|
||||
@@ -863,38 +923,6 @@ export default class Blockchain {
|
||||
console.log('Заявление на вступление отправлено в совет: ', params)
|
||||
}
|
||||
|
||||
async createBoard(
|
||||
params: SovietContract.Actions.Boards.CreateBoard.ICreateboard,
|
||||
) {
|
||||
await this.update_pass_instance()
|
||||
|
||||
await this.api.transact(
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
account: SovietContract.contractName.production,
|
||||
name: SovietContract.Actions.Boards.CreateBoard.actionName,
|
||||
authorization: [
|
||||
{
|
||||
actor: params.coopname,
|
||||
permission: 'active',
|
||||
},
|
||||
],
|
||||
data: {
|
||||
...params,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
blocksBehind: 3,
|
||||
expireSeconds: 30,
|
||||
},
|
||||
)
|
||||
|
||||
console.log('Совет создан: ', params)
|
||||
}
|
||||
|
||||
async createDraft(params: DraftContract.Actions.CreateDraft.ICreateDraft) {
|
||||
await this.update_pass_instance()
|
||||
|
||||
@@ -989,6 +1017,37 @@ export default class Blockchain {
|
||||
console.log('Программа установлена: ', params)
|
||||
}
|
||||
|
||||
async createBoard(params: SovietContract.Actions.Boards.CreateBoard.ICreateboard) {
|
||||
await this.update_pass_instance()
|
||||
console.dir(params, { depth: null })
|
||||
const result = await this.api.transact(
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
account: SovietContract.contractName.production,
|
||||
name: SovietContract.Actions.Boards.CreateBoard.actionName,
|
||||
authorization: [
|
||||
{
|
||||
actor: params.coopname,
|
||||
permission: 'active',
|
||||
},
|
||||
],
|
||||
data: {
|
||||
...params,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
blocksBehind: 3,
|
||||
expireSeconds: 30,
|
||||
},
|
||||
)
|
||||
|
||||
console.log('Создан совет: ', params)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет транзакцию и автоматически выводит консоль логи через consoleIt
|
||||
* @param actions - массив действий для транзакции
|
||||
|
||||
@@ -10,7 +10,7 @@ enable-stale-production = true
|
||||
read-only-read-window-time-us = 120000
|
||||
|
||||
net-threads = 2
|
||||
max-transaction-time=2000
|
||||
max-transaction-time=10000
|
||||
|
||||
http-server-address = 0.0.0.0:8888
|
||||
p2p-listen-endpoint = 0.0.0.0:9876
|
||||
@@ -26,8 +26,8 @@ max-body-size = 10485760
|
||||
abi-serializer-max-time-ms = 200000
|
||||
contracts-console = true
|
||||
|
||||
max-block-cpu-usage-threshold-us = 5000
|
||||
max-block-net-usage-threshold-bytes = 1024
|
||||
max-block-cpu-usage-threshold-us = 200000
|
||||
max-block-net-usage-threshold-bytes = 800000
|
||||
|
||||
verbose-http-errors = true
|
||||
chain-state-history = true
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"net_usage_leeway": 500,
|
||||
"context_free_discount_net_usage_num": 20,
|
||||
"context_free_discount_net_usage_den": 100,
|
||||
"max_block_cpu_usage": 300000,
|
||||
"target_block_cpu_usage_pct": 500,
|
||||
"max_block_cpu_usage": 1000000,
|
||||
"target_block_cpu_usage_pct": 1000,
|
||||
"max_transaction_cpu_usage": 290000,
|
||||
"min_transaction_cpu_usage": 100,
|
||||
"max_transaction_lifetime": 3600,
|
||||
|
||||
@@ -53,7 +53,7 @@ export default {
|
||||
allocations: [
|
||||
{
|
||||
to: 'eosio',
|
||||
quantity: `10000.0000 ${SYMBOL}`,
|
||||
quantity: `100000.0000 ${SYMBOL}`,
|
||||
},
|
||||
],
|
||||
accounts: [
|
||||
@@ -135,9 +135,9 @@ export default {
|
||||
name: 'contributor',
|
||||
code_permissions_to: ['contributor'],
|
||||
},
|
||||
{
|
||||
name: provider_chairman,
|
||||
},
|
||||
// {
|
||||
// name: provider_chairman,
|
||||
// },
|
||||
{
|
||||
name: provider,
|
||||
code_permissions_to: ['registrator'],
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as path from 'node:path'
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
export async function clearDB(): Promise<void> {
|
||||
// Очистка MongoDB
|
||||
// eslint-disable-next-line node/prefer-global/process
|
||||
await mongoose.connect(process.env.MONGO_URI as string)
|
||||
|
||||
@@ -37,6 +38,8 @@ export async function clearDB(): Promise<void> {
|
||||
catch (e) {
|
||||
console.error('Ошибка при удалении:', e)
|
||||
}
|
||||
|
||||
// PostgreSQL будет пересоздан полностью в скрипте extra_reboot.sh
|
||||
}
|
||||
|
||||
export async function clearDirectory(dirPath: string): Promise<void> {
|
||||
|
||||
@@ -6,7 +6,8 @@ import { config } from 'dotenv'
|
||||
import { execCommand } from './docker/exec'
|
||||
import { stopContainerByName } from './docker/stop'
|
||||
import { runContainer } from './docker/run'
|
||||
import { boot } from './init/booter'
|
||||
import { boot, bootClean, bootExtra } from './init/booter'
|
||||
import { startCoop } from './init/cooperative'
|
||||
import { sleep } from './utils'
|
||||
import { checkHealth } from './docker/health'
|
||||
import { clearDB, clearDirectory, deleteFile } from './docker/purge'
|
||||
@@ -120,15 +121,13 @@ program
|
||||
}
|
||||
|
||||
try {
|
||||
await clearDirectory(basePath)
|
||||
await clearDB()
|
||||
await runContainer()
|
||||
|
||||
await sleep(5000)
|
||||
await checkHealth()
|
||||
await boot()
|
||||
console.log(`
|
||||
|
||||
|
||||
Boot is
|
||||
.d8888b. .d88888b. 888b d888 8888888b. 888 8888888888 88888888888 8888888888
|
||||
d88P Y88b d88P" "Y88b 8888b d8888 888 Y88b 888 888 888 888
|
||||
@@ -138,7 +137,7 @@ d88P Y88b d88P" "Y88b 8888b d8888 888 Y88b 888 888 888
|
||||
888 888 888 888 888 Y8P 888 888 888 888 888 888
|
||||
Y88b d88P Y88b. .d88P 888 " 888 888 888 888 888 888
|
||||
"Y8888P" "Y88888P" 888 888 888 88888888 8888888888 888 8888888888
|
||||
|
||||
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
@@ -148,6 +147,111 @@ Y88b d88P Y88b. .d88P 888 " 888 888 888 888 888
|
||||
}
|
||||
})
|
||||
|
||||
// Команда для получения списка контейнеров
|
||||
program
|
||||
.command('boot:clean')
|
||||
.description('Boot infrastructure without initial data and cooperatives')
|
||||
.action(async () => {
|
||||
try {
|
||||
await stopContainerByName('node')
|
||||
}
|
||||
catch (e) {
|
||||
console.log('Нет контейнера для остановки. Стартуем новый..')
|
||||
}
|
||||
|
||||
try {
|
||||
await runContainer()
|
||||
|
||||
await sleep(5000)
|
||||
await checkHealth()
|
||||
await bootClean()
|
||||
console.log(`
|
||||
|
||||
Boot Clean is
|
||||
.d8888b. .d88888b. 888b d888 8888888b. 888 8888888888 88888888888 8888888888
|
||||
d88P Y88b d88P" "Y88b 8888b d8888 888 Y88b 888 888 888 888
|
||||
888 888 888 888 88888b.d88888 888 888 888 888 888 888
|
||||
888 888 888 888Y88888P888 888 d88P 888 8888888 888 8888888
|
||||
888 888 888 888 Y888P 888 8888888P" 888 888 888 888
|
||||
888 888 888 888 888 Y8P 888 888 888 888 888 888
|
||||
Y88b d88P Y88b. .d88P 888 " 888 888 888 888 888 888
|
||||
"Y8888P" "Y88888P" 888 888 888 88888888 8888888888 888 8888888888
|
||||
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to boot clean:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
// Команда для получения списка контейнеров
|
||||
program
|
||||
.command('boot:extra')
|
||||
.description('Boot infrastructure with extended initial data and additional shareholders')
|
||||
.action(async () => {
|
||||
try {
|
||||
await stopContainerByName('node')
|
||||
}
|
||||
catch (e) {
|
||||
console.log('Нет контейнера для остановки. Стартуем новый..')
|
||||
}
|
||||
|
||||
try {
|
||||
await runContainer()
|
||||
|
||||
await sleep(5000)
|
||||
await checkHealth()
|
||||
await bootExtra()
|
||||
const councilMembers = [
|
||||
{ username: 'ant', fullName: 'Иванов Иван Иванович', email: 'ivanov@example.com' },
|
||||
{ username: 'petr', fullName: 'Сидоров Петр Сергеевич', email: 'sidorov@example.com' },
|
||||
{ username: 'anna', fullName: 'Петрова Анна Ивановна', email: 'petrova@example.com' },
|
||||
{ username: 'mikhail', fullName: 'Кузнецов Михаил Андреевич', email: 'kuznetsov@example.com' },
|
||||
{ username: 'olga', fullName: 'Соколова Ольга Викторовна', email: 'sokolova@example.com' },
|
||||
]
|
||||
|
||||
console.log('\nЧлены совета (логин / ФИО / email):')
|
||||
for (const member of councilMembers)
|
||||
console.log(` - ${member.username}: ${member.fullName} / ${member.email}`)
|
||||
console.log(`
|
||||
|
||||
Boot Extra is
|
||||
.d8888b. .d88888b. 888b d888 8888888b. 888 8888888888 88888888888 8888888888
|
||||
d88P Y88b d88P" "Y88b 8888b d8888 888 Y88b 888 888 888 888
|
||||
888 888 888 888 88888b.d88888 888 888 888 888 888 888
|
||||
888 888 888 888Y88888P888 888 d88P 888 8888888 888 8888888
|
||||
888 888 888 888 Y888P 888 8888888P" 888 888 888 888
|
||||
888 888 888 888 888 Y8P 888 888 888 888 888 888
|
||||
Y88b d88P Y88b. .d88P 888 " 888 888 888 888 888 888
|
||||
"Y8888P" "Y88888P" 888 888 888 88888888 8888888888 888 8888888888
|
||||
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to boot extra:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
// Команда для создания кооператива
|
||||
program
|
||||
.command('create-coop')
|
||||
.description('Create cooperative after clean boot')
|
||||
.action(async () => {
|
||||
try {
|
||||
await startCoop()
|
||||
console.log('Кооператив успешно создан')
|
||||
process.exit(0)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to create cooperative:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
// Команда для получения списка контейнеров
|
||||
program
|
||||
.command('clear')
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
import { startInfra } from './infra'
|
||||
import { startCoop } from './cooperative'
|
||||
import config from '../configs'
|
||||
import { initExtensionsInPostgres, initSystemStatus } from '../postgres-init'
|
||||
import { installExtraData, installInitialData, startInfra } from './infra'
|
||||
import { CooperativeClass, startCoop } from './cooperative'
|
||||
|
||||
export async function boot() {
|
||||
await startInfra()
|
||||
const blockchain = await startInfra()
|
||||
await installInitialData(blockchain, false) // Создать базовый совет
|
||||
|
||||
console.log('Инициализируем статус системы в PostgreSQL')
|
||||
await initSystemStatus()
|
||||
|
||||
await startCoop()
|
||||
}
|
||||
|
||||
export async function bootClean() {
|
||||
const blockchain = await startInfra()
|
||||
|
||||
console.log('Создаём программы (Благорост и маркетплейс)')
|
||||
|
||||
const cooperative = new CooperativeClass(blockchain)
|
||||
await cooperative.createPrograms(config.provider)
|
||||
}
|
||||
|
||||
export async function bootExtra() {
|
||||
const blockchain = await startInfra()
|
||||
await installInitialData(blockchain, true) // Создать расширенный совет (устанавливает статус 'active' в MongoDB)
|
||||
await installExtraData(blockchain) // Добавить дополнительных пайщиков
|
||||
|
||||
console.log('Инициализируем статус системы в PostgreSQL')
|
||||
await initSystemStatus() // Устанавливает статус 'active' в PostgreSQL
|
||||
|
||||
console.log('Инициализируем extensions в PostgreSQL')
|
||||
await initExtensionsInPostgres() // Инициализирует таблицу extensions с данными capital
|
||||
}
|
||||
|
||||
@@ -54,6 +54,38 @@ export class CooperativeClass {
|
||||
meta: '',
|
||||
is_can_coop_spend_share_contributions: false,
|
||||
})
|
||||
|
||||
await this.blockchain.createProgram({
|
||||
coopname,
|
||||
username: coopname,
|
||||
type: 'generator',
|
||||
title: 'Целевая потребительская программа "Генератор"',
|
||||
announce: '',
|
||||
description: '',
|
||||
preview: '',
|
||||
images: '',
|
||||
calculation_type: 'free',
|
||||
fixed_membership_contribution: `${Number(0).toFixed(4)} ${GOVERN_SYMBOL}`,
|
||||
membership_percent_fee: 0, // 10%
|
||||
meta: '',
|
||||
is_can_coop_spend_share_contributions: true,
|
||||
})
|
||||
|
||||
await this.blockchain.createProgram({
|
||||
coopname,
|
||||
username: coopname,
|
||||
type: 'blagorost',
|
||||
title: 'Целевая потребительская программа "Благорост"',
|
||||
announce: '',
|
||||
description: '',
|
||||
preview: '',
|
||||
images: '',
|
||||
calculation_type: 'free',
|
||||
fixed_membership_contribution: `${Number(0).toFixed(4)} ${GOVERN_SYMBOL}`,
|
||||
membership_percent_fee: 0, // 10%
|
||||
meta: '',
|
||||
is_can_coop_spend_share_contributions: true,
|
||||
})
|
||||
}
|
||||
|
||||
async createCooperative(username: string, keys?: Keys) {
|
||||
@@ -170,18 +202,18 @@ export async function startCoop() {
|
||||
const blockchain = new Blockchain(config.network, config.private_keys)
|
||||
const cooperative = new CooperativeClass(blockchain)
|
||||
|
||||
await cooperative.createCooperative('cooperative1', {
|
||||
// eslint-disable-next-line node/prefer-global/process
|
||||
privateKey: process.env.EOSIO_PRV_KEY!,
|
||||
// eslint-disable-next-line node/prefer-global/process
|
||||
publicKey: process.env.EOSIO_PUB_KEY!,
|
||||
})
|
||||
// await cooperative.createCooperative('cooperative1', {
|
||||
// // eslint-disable-next-line node/prefer-global/process
|
||||
// privateKey: process.env.EOSIO_PRV_KEY!,
|
||||
// // eslint-disable-next-line node/prefer-global/process
|
||||
// publicKey: process.env.EOSIO_PUB_KEY!,
|
||||
// })
|
||||
|
||||
await blockchain.preInit({
|
||||
coopname: 'cooperative1',
|
||||
username: config.provider,
|
||||
status: 'active',
|
||||
})
|
||||
// await blockchain.preInit({
|
||||
// coopname: 'cooperative1',
|
||||
// username: config.provider,
|
||||
// status: 'active',
|
||||
// })
|
||||
|
||||
console.log('Кооператив предварительно подготовлен к установке совета.')
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import axios from 'axios'
|
||||
import { Generator, Registry } from '@coopenomics/factory'
|
||||
import type { Cooperative } from 'cooptypes'
|
||||
import { DraftContract } from 'cooptypes'
|
||||
import mongoose from 'mongoose'
|
||||
import mongoose, { Types } from 'mongoose'
|
||||
import type { Account, Contract } from '../types'
|
||||
import config from '../configs'
|
||||
import Blockchain from '../blockchain'
|
||||
import { sleep } from '../utils'
|
||||
import { initUsersInPostgres, initVaultInPostgres } from '../postgres-init'
|
||||
import { CooperativeClass } from './cooperative'
|
||||
|
||||
export async function startInfra() {
|
||||
@@ -145,10 +146,32 @@ export async function startInfra() {
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`Арендуем ресурсы провайдеру`)
|
||||
await blockchain.powerup({
|
||||
payer: 'eosio',
|
||||
receiver: config.provider,
|
||||
days: config.powerup.days,
|
||||
payment: `10000.0000 ${config.token.symbol}`,
|
||||
transfer: true,
|
||||
})
|
||||
|
||||
await blockchain.transfer({
|
||||
from: 'eosio',
|
||||
to: config.provider,
|
||||
quantity: `1000.0000 ${config.token.symbol}`,
|
||||
memo: '',
|
||||
})
|
||||
|
||||
console.log('Базовая инфраструктура установлена')
|
||||
|
||||
return blockchain
|
||||
}
|
||||
|
||||
export async function installInitialData(blockchain: Blockchain, isExtended = false) {
|
||||
const organizationData: Cooperative.Users.IOrganizationData = {
|
||||
username: 'voskhod',
|
||||
type: 'coop',
|
||||
short_name: '"ПК Восход"',
|
||||
short_name: 'ПК "Восход"',
|
||||
full_name: 'Потребительский Кооператив "ВОСХОД"',
|
||||
represented_by: {
|
||||
first_name: 'Иван',
|
||||
@@ -220,9 +243,9 @@ export async function startInfra() {
|
||||
// добавляем переменные кооператива
|
||||
const vars: Cooperative.Model.IVars = {
|
||||
coopname: 'voskhod',
|
||||
full_abbr: 'потребительский кооператив',
|
||||
full_abbr_genitive: 'потребительского кооператива',
|
||||
full_abbr_dative: 'потребительскому кооперативу',
|
||||
full_abbr: 'Потребительский Кооператив',
|
||||
full_abbr_genitive: 'Потребительского Кооператива',
|
||||
full_abbr_dative: 'Потребительскому Кооперативу',
|
||||
short_abbr: 'ПК',
|
||||
website: 'цифровой-кооператив.рф',
|
||||
name: 'Восход',
|
||||
@@ -250,13 +273,55 @@ export async function startInfra() {
|
||||
protocol_number: '10-04-2024',
|
||||
protocol_day_month_year: '10 апреля 2024 г.',
|
||||
},
|
||||
generator_program: {
|
||||
protocol_number: '1',
|
||||
protocol_day_month_year: '09.02.2026 10:24',
|
||||
},
|
||||
generation_contract_template: {
|
||||
protocol_number: '2',
|
||||
protocol_day_month_year: '09.02.2026 10:24',
|
||||
},
|
||||
blagorost_program: {
|
||||
protocol_number: '3',
|
||||
protocol_day_month_year: '09.02.2026 10:24',
|
||||
},
|
||||
generator_offer_template: {
|
||||
protocol_number: '4',
|
||||
protocol_day_month_year: '09.02.2026 10:27',
|
||||
},
|
||||
blagorost_offer_template: {
|
||||
protocol_number: '5',
|
||||
protocol_day_month_year: '09.02.2026 10:27',
|
||||
},
|
||||
deleted: false,
|
||||
block_num: 1,
|
||||
}
|
||||
|
||||
await generator.save('vars', vars)
|
||||
|
||||
// Сохраняем vars с указанием конкретного _id и _created_at
|
||||
// eslint-disable-next-line node/prefer-global/process
|
||||
await mongoose.connect(process.env.MONGO_URI as string)
|
||||
|
||||
try {
|
||||
await mongoose.connection.collection('vars').insertOne({
|
||||
_id: new Types.ObjectId('69898c7d996550b4db4b1a36'),
|
||||
_created_at: new Date('2026-02-08T13:29:12.423Z'),
|
||||
...vars,
|
||||
})
|
||||
console.log('Vars сохранены с указанным _id и _created_at')
|
||||
}
|
||||
catch (e) {
|
||||
console.log('Vars уже существуют, обновляем...')
|
||||
await mongoose.connection.collection('vars').updateOne(
|
||||
{ coopname: 'voskhod' },
|
||||
{
|
||||
$set: {
|
||||
...vars,
|
||||
_created_at: new Date('2026-02-08T13:29:12.423Z'),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await mongoose.connection.collection('sync').deleteMany({})
|
||||
console.log('Все документы удалены из коллекции sync')
|
||||
@@ -281,17 +346,17 @@ export async function startInfra() {
|
||||
console.error('Ошибка при удалении:', e)
|
||||
}
|
||||
|
||||
// добавляем пользователя для подключений
|
||||
try {
|
||||
await mongoose.connection.collection('users').insertOne({
|
||||
email: 'ivanov@example.com',
|
||||
// Собираем пользователей для инициализации в PostgreSQL
|
||||
const usersToInit = [
|
||||
{
|
||||
username: 'ant',
|
||||
type: 'individual',
|
||||
email: 'ivanov@example.com',
|
||||
type: 'individual' as const,
|
||||
role: 'chairman',
|
||||
status: 'active',
|
||||
is_registered: true,
|
||||
})
|
||||
}
|
||||
catch (e) { console.log('user is exist') }
|
||||
},
|
||||
]
|
||||
|
||||
// имитируем установку
|
||||
try {
|
||||
@@ -312,27 +377,195 @@ export async function startInfra() {
|
||||
})
|
||||
}
|
||||
catch (e) { console.log('vault is exist') }
|
||||
|
||||
console.log('Добавляем пайщика ant')
|
||||
|
||||
await blockchain.addUser({
|
||||
coopname: 'voskhod',
|
||||
referer: '',
|
||||
username: 'ant',
|
||||
type: 'individual',
|
||||
created_at: '2025-01-15T10:00:00',
|
||||
initial: '100.0000 RUB',
|
||||
minimum: '200.0000 RUB',
|
||||
spread_initial: true,
|
||||
meta: 'Основатель кооператива ВОСХОД',
|
||||
})
|
||||
|
||||
console.log('Устанавливаем дефолтный публичный ключ для ant')
|
||||
|
||||
await blockchain.changeKey({
|
||||
coopname: 'voskhod',
|
||||
changer: 'voskhod',
|
||||
username: 'ant',
|
||||
public_key: config.default_public_key,
|
||||
})
|
||||
|
||||
// Если расширенный режим, сначала добавляем дополнительных пайщиков
|
||||
if (isExtended) {
|
||||
console.log('Добавляем дополнительных пайщиков для расширенного совета')
|
||||
|
||||
const extraUsers = [
|
||||
{
|
||||
username: 'petr',
|
||||
first_name: 'Петр',
|
||||
last_name: 'Сидоров',
|
||||
middle_name: 'Сергеевич',
|
||||
email: 'sidorov@example.com',
|
||||
},
|
||||
{
|
||||
username: 'anna',
|
||||
first_name: 'Анна',
|
||||
last_name: 'Петрова',
|
||||
middle_name: 'Ивановна',
|
||||
email: 'petrova@example.com',
|
||||
},
|
||||
{
|
||||
username: 'mikhail',
|
||||
first_name: 'Михаил',
|
||||
last_name: 'Кузнецов',
|
||||
middle_name: 'Андреевич',
|
||||
email: 'kuznetsov@example.com',
|
||||
},
|
||||
{
|
||||
username: 'olga',
|
||||
first_name: 'Ольга',
|
||||
last_name: 'Соколова',
|
||||
middle_name: 'Викторовна',
|
||||
email: 'sokolova@example.com',
|
||||
},
|
||||
]
|
||||
|
||||
for (const user of extraUsers) {
|
||||
// Добавляем в MongoDB
|
||||
const userData: Cooperative.Users.IIndividualData = {
|
||||
username: user.username,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
middle_name: user.middle_name,
|
||||
birthdate: '1990/04/01',
|
||||
phone: '+71234567890',
|
||||
email: user.email,
|
||||
full_address: 'г. Москва, ул. Примерная д. 1',
|
||||
passport: {
|
||||
series: 7122,
|
||||
number: Math.floor(Math.random() * 900000) + 100000,
|
||||
issued_by: 'отделом УФМС по г. Москва',
|
||||
issued_at: '2010/05/10',
|
||||
code: '111-232',
|
||||
},
|
||||
}
|
||||
|
||||
await generator.save('individual', userData)
|
||||
|
||||
// Добавляем пользователя в список для PostgreSQL
|
||||
usersToInit.push({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
type: 'individual' as const,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
is_registered: true,
|
||||
})
|
||||
|
||||
console.log(`Добавляем пайщика ${user.username}`)
|
||||
|
||||
// Добавляем в блокчейн
|
||||
await blockchain.addUser({
|
||||
coopname: 'voskhod',
|
||||
referer: user.username === 'petr' ? '' : 'petr',
|
||||
username: user.username,
|
||||
type: 'individual',
|
||||
created_at: '2025-01-15T10:00:00',
|
||||
initial: '100.0000 RUB',
|
||||
minimum: '300.0000 RUB',
|
||||
spread_initial: true,
|
||||
meta: `Член совета кооператива ВОСХОД - ${user.first_name} ${user.middle_name} ${user.last_name}`,
|
||||
})
|
||||
|
||||
console.log(`Устанавливаем дефолтный публичный ключ для ${user.username}`)
|
||||
|
||||
await blockchain.changeKey({
|
||||
coopname: 'voskhod',
|
||||
changer: 'voskhod',
|
||||
username: user.username,
|
||||
public_key: config.default_public_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Инициализируем пользователей в PostgreSQL')
|
||||
await initUsersInPostgres(usersToInit)
|
||||
|
||||
console.log('Инициализируем vault в PostgreSQL')
|
||||
await initVaultInPostgres()
|
||||
|
||||
console.log('Создаём совет')
|
||||
|
||||
const boardMembers: Array<{
|
||||
username: string
|
||||
is_voting: boolean
|
||||
position_title: string
|
||||
position: 'chairman' | 'member'
|
||||
}> = [
|
||||
{
|
||||
username: 'ant',
|
||||
is_voting: true,
|
||||
position_title: 'Председатель совета',
|
||||
position: 'chairman',
|
||||
},
|
||||
]
|
||||
|
||||
// Если расширенный режим, добавляем дополнительных членов
|
||||
if (isExtended) {
|
||||
boardMembers.push(
|
||||
{
|
||||
username: 'petr',
|
||||
is_voting: true,
|
||||
position_title: 'Член совета',
|
||||
position: 'member',
|
||||
},
|
||||
{
|
||||
username: 'anna',
|
||||
is_voting: true,
|
||||
position_title: 'Член совета',
|
||||
position: 'member',
|
||||
},
|
||||
{
|
||||
username: 'mikhail',
|
||||
is_voting: true,
|
||||
position_title: 'Член совета',
|
||||
position: 'member',
|
||||
},
|
||||
{
|
||||
username: 'olga',
|
||||
is_voting: true,
|
||||
position_title: 'Член совета',
|
||||
position: 'member',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
await blockchain.createBoard({
|
||||
coopname: 'voskhod',
|
||||
username: 'ant',
|
||||
type: 'soviet',
|
||||
members: boardMembers,
|
||||
name: 'Совет',
|
||||
description: isExtended ? 'Совет кооператива ВОСХОД (расширенный состав)' : 'Совет кооператива ВОСХОД',
|
||||
})
|
||||
|
||||
console.log('Создаём программы и соглашения')
|
||||
|
||||
const cooperative = new CooperativeClass(blockchain)
|
||||
|
||||
await cooperative.createPrograms(config.provider)
|
||||
|
||||
console.log(`Арендуем ресурсы провайдеру`)
|
||||
await blockchain.powerup({
|
||||
payer: 'eosio',
|
||||
receiver: config.provider,
|
||||
days: config.powerup.days,
|
||||
payment: `100.0000 ${config.token.symbol}`,
|
||||
transfer: true,
|
||||
})
|
||||
|
||||
await blockchain.transfer({
|
||||
from: 'eosio',
|
||||
to: config.provider,
|
||||
quantity: `1000.0000 ${config.token.symbol}`,
|
||||
memo: '',
|
||||
})
|
||||
|
||||
console.log('Базовая установка завершена')
|
||||
console.log('Начальные данные установлены')
|
||||
}
|
||||
|
||||
export async function installExtraData(blockchain: Blockchain) {
|
||||
// В расширенном режиме пайщики уже добавлены в installInitialData
|
||||
// Здесь можно добавить дополнительную логику инициализации если потребуется
|
||||
console.log('Дополнительная инициализация для расширенного режима выполнена')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
/* eslint-disable node/prefer-global/process */
|
||||
import { Client } from 'pg'
|
||||
import type { Cooperative } from 'cooptypes'
|
||||
|
||||
export async function initSystemStatus() {
|
||||
console.log('Инициализация статуса системы для coopname: voskhod')
|
||||
|
||||
const client = new Client({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
user: process.env.POSTGRES_USERNAME,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: process.env.POSTGRES_DATABASE,
|
||||
})
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
console.log('Подключение к PostgreSQL установлено для initSystemStatus')
|
||||
|
||||
// Создаем enum тип для статуса системы
|
||||
try {
|
||||
await client.query(`
|
||||
CREATE TYPE public.system_status_status_enum AS ENUM ('install', 'initialized', 'active', 'maintenance')
|
||||
`)
|
||||
}
|
||||
catch (error) {
|
||||
// Тип уже существует, продолжаем
|
||||
console.log('Enum тип system_status_status_enum уже существует, пропускаем создание')
|
||||
}
|
||||
|
||||
// Создаем таблицу system_status правильно, как в TypeORM entity
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS public.system_status (
|
||||
coopname varchar(12) NOT NULL,
|
||||
install_code varchar(255) NULL,
|
||||
install_code_expires_at timestamp NULL,
|
||||
init_by_server bool NOT NULL DEFAULT false,
|
||||
created_at timestamp NOT NULL DEFAULT now(),
|
||||
updated_at timestamp NOT NULL DEFAULT now(),
|
||||
status public.system_status_status_enum NOT NULL DEFAULT 'install'::system_status_status_enum,
|
||||
CONSTRAINT system_status_pkey PRIMARY KEY (coopname)
|
||||
)
|
||||
`)
|
||||
|
||||
try {
|
||||
// Устанавливаем начальный статус для voskhod (active - система готова к работе)
|
||||
await client.query(`
|
||||
INSERT INTO system_status (coopname, status)
|
||||
VALUES ('voskhod', 'active')
|
||||
ON CONFLICT (coopname) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`)
|
||||
|
||||
// Проверяем, что статус действительно установлен
|
||||
const result = await client.query(`
|
||||
SELECT status FROM system_status WHERE coopname = 'voskhod'
|
||||
`)
|
||||
console.log('Статус системы в PostgreSQL после установки:', result.rows[0]?.status)
|
||||
}
|
||||
catch (queryError) {
|
||||
console.error('Ошибка при установке статуса системы в PostgreSQL:', queryError)
|
||||
throw queryError
|
||||
}
|
||||
|
||||
console.log('Статус системы инициализирован в PostgreSQL')
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Ошибка инициализации статуса системы в PostgreSQL:', error)
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
export async function initUsersInPostgres(
|
||||
users: Array<{
|
||||
username: string
|
||||
email: string
|
||||
type: 'individual' | 'entrepreneur' | 'organization'
|
||||
role: string
|
||||
status: string
|
||||
is_registered: boolean
|
||||
}>,
|
||||
) {
|
||||
const client = new Client({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
user: process.env.POSTGRES_USERNAME,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: process.env.POSTGRES_DATABASE,
|
||||
})
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
console.log('Подключение к PostgreSQL установлено для инициализации пользователей')
|
||||
|
||||
// Создаем таблицу users, если она не существует
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'created',
|
||||
message TEXT DEFAULT '',
|
||||
is_registered BOOLEAN DEFAULT FALSE,
|
||||
has_account BOOLEAN DEFAULT FALSE,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
public_key TEXT DEFAULT '',
|
||||
referer VARCHAR(100) DEFAULT '',
|
||||
email VARCHAR(255),
|
||||
role VARCHAR(20) DEFAULT 'user',
|
||||
is_email_verified BOOLEAN DEFAULT FALSE,
|
||||
subscriber_id VARCHAR(100) DEFAULT '',
|
||||
subscriber_hash VARCHAR(255) DEFAULT '',
|
||||
legacy_mongo_id VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
|
||||
// Создаем пользователей
|
||||
for (const user of users) {
|
||||
// Проверяем, что username не null и не пустой
|
||||
if (!user.username || user.username.trim() === '') {
|
||||
console.warn(`Пропускаем пользователя с пустым username:`, user)
|
||||
continue
|
||||
}
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO "users" (
|
||||
username, email, type, role, status, is_registered,
|
||||
has_account, is_email_verified, created_at, updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (username) DO NOTHING
|
||||
`, [
|
||||
user.username,
|
||||
user.email,
|
||||
user.type,
|
||||
user.role,
|
||||
user.status,
|
||||
user.is_registered,
|
||||
false, // has_account
|
||||
true, // is_email_verified
|
||||
])
|
||||
}
|
||||
|
||||
console.log(`Инициализировано ${users.length} пользователей в PostgreSQL`)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Ошибка инициализации пользователей в PostgreSQL:', error)
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
export async function initVaultInPostgres() {
|
||||
const client = new Client({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
user: process.env.POSTGRES_USERNAME,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: process.env.POSTGRES_DATABASE,
|
||||
})
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
console.log('Подключение к PostgreSQL установлено для инициализации vault')
|
||||
|
||||
// Создаем таблицу vaults, если она не существует
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS "vaults" (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username VARCHAR(50) NOT NULL,
|
||||
permission VARCHAR(20) DEFAULT 'active',
|
||||
wif TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(username, permission)
|
||||
)
|
||||
`)
|
||||
|
||||
// Создаем индексы
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_vaults_username ON "vaults"(username)`)
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_vaults_username_permission ON "vaults"(username, permission)`)
|
||||
|
||||
// Сохраняем зашифрованный ключ в vault
|
||||
await client.query(`
|
||||
INSERT INTO "vaults" (username, permission, wif, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (username, permission) DO NOTHING
|
||||
`, [
|
||||
'voskhod',
|
||||
'active',
|
||||
'9d6479a9d77ead53fb0e5e54b3608a95:2046ee3c1577d48aecbee49e8f25c4c2df37ab02f15d73d0d1b6352f53a4b774cb9e71b6028fd7caf64568e195c7878dfbb5d2bf10a3766d90ba9e92ea724428',
|
||||
])
|
||||
|
||||
console.log('Vault инициализирован в PostgreSQL')
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Ошибка инициализации vault в PostgreSQL:', error)
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
export async function initExtensionsInPostgres() {
|
||||
const client = new Client({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
user: process.env.POSTGRES_USERNAME,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: process.env.POSTGRES_DATABASE,
|
||||
})
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
console.log('Подключение к PostgreSQL установлено для инициализации extensions')
|
||||
|
||||
// Создаем таблицу extensions по аналогии с ExtensionEntity
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS "extensions" (
|
||||
name VARCHAR(12) PRIMARY KEY,
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
config JSONB DEFAULT '{}',
|
||||
schema_version INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
|
||||
// Создаем индексы
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_extensions_name ON "extensions"(name)`)
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_extensions_enabled ON "extensions"(enabled)`)
|
||||
|
||||
// Вставляем запись для capital extension
|
||||
const capitalConfig = {
|
||||
level_depth_base: 100000000,
|
||||
github_repository: "coopenomics/results-test",
|
||||
onboarding_init_at: "2026-02-09T07:16:18.380Z",
|
||||
expense_pool_percent: 100,
|
||||
onboarding_expire_at: "2026-03-11T07:16:18.380Z",
|
||||
voting_period_in_days: 1,
|
||||
authors_voting_percent: 62.8,
|
||||
creators_voting_percent: 62.8,
|
||||
energy_gain_coefficient: 1,
|
||||
level_growth_coefficient: 1.5,
|
||||
coordinator_bonus_percent: 5,
|
||||
energy_decay_rate_per_day: 0.02,
|
||||
coordinator_invite_validity_days: 30,
|
||||
onboarding_blagorost_provision_done: true,
|
||||
onboarding_blagorost_provision_hash: "CDE57D987E3C945E79E108920CE02A4A80CFA7980CAA912949BB6C2111B7027A",
|
||||
onboarding_blagorost_offer_template_done: true,
|
||||
onboarding_blagorost_offer_template_hash: "5CA88BBD303E5CCDA01E565FFE47E51855515176EC6C957F0FBDBCA4C53DBFD2",
|
||||
onboarding_generator_offer_template_done: true,
|
||||
onboarding_generator_offer_template_hash: "8DA31574E8CC764C3A1FCAE1172726656A3DCDB1BB82AB0E567E2732070C3A44",
|
||||
onboarding_generator_program_template_done: true,
|
||||
onboarding_generator_program_template_hash: "E55564D8946C55C93490B5277968FC890FDCB10A049DB5B2E0FE9F67FDA80896",
|
||||
onboarding_generation_contract_template_done: true,
|
||||
onboarding_generation_contract_template_hash: "A4BD579D6130CCE2D8C34337DFA591807C1F028A148DD53689881B12AC2627E2"
|
||||
}
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO "extensions" (name, enabled, config, schema_version, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (name) DO UPDATE SET
|
||||
enabled = EXCLUDED.enabled,
|
||||
config = EXCLUDED.config,
|
||||
schema_version = EXCLUDED.schema_version,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`, [
|
||||
'capital',
|
||||
true,
|
||||
JSON.stringify(capitalConfig),
|
||||
1,
|
||||
new Date('2026-02-09T02:13:06.620Z'),
|
||||
new Date('2026-02-09T02:27:57.155Z')
|
||||
])
|
||||
|
||||
console.log('Extensions инициализированы в PostgreSQL')
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Ошибка инициализации extensions в PostgreSQL:', error)
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,7 @@ describe('тест контракта CAPITAL', () => {
|
||||
fixed_membership_contribution: '0.0000 RUB',
|
||||
membership_percent_fee: '0',
|
||||
meta: '',
|
||||
type: 'source',
|
||||
type: 'generator',
|
||||
}
|
||||
|
||||
const result = await blockchain.api.transact(
|
||||
@@ -234,7 +234,7 @@ describe('тест контракта CAPITAL', () => {
|
||||
fixed_membership_contribution: '0.0000 RUB',
|
||||
membership_percent_fee: '0',
|
||||
meta: '',
|
||||
type: 'capital',
|
||||
type: 'blagorost',
|
||||
}
|
||||
|
||||
const result = await blockchain.api.transact(
|
||||
@@ -346,6 +346,10 @@ describe('тест контракта CAPITAL', () => {
|
||||
voting_period_in_days: 7,
|
||||
authors_voting_percent: 38.2,
|
||||
creators_voting_percent: 38.2,
|
||||
energy_decay_rate_per_day: 0.11,
|
||||
level_depth_base: 1000,
|
||||
level_growth_coefficient: 1.5,
|
||||
energy_gain_coefficient: 0.01,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -63,6 +63,8 @@ export async function commitToResult(
|
||||
project_hash: projectHash,
|
||||
commit_hash: commitHash,
|
||||
creator_hours: spendHours,
|
||||
description: `Коммит ${commitHash}`,
|
||||
meta: `{"hours": ${spendHours}, "creator": "${creator}"}`,
|
||||
}
|
||||
|
||||
const createCommitResult = await blockchain.api.transact(
|
||||
|
||||
@@ -2,6 +2,6 @@ export const sourceProgramId = 3
|
||||
export const capitalProgramId = 4
|
||||
export const walletProgramId = 1
|
||||
export const ratePerHour = '1000.0000 RUB'
|
||||
export const sourceProgramName = 'source'
|
||||
export const capitalProgramName = 'capital'
|
||||
export const circulationAccountId = 80
|
||||
export const sourceProgramName = 'generator'
|
||||
export const capitalProgramName = 'blagorost'
|
||||
export const circulationAccountId = 80
|
||||
|
||||
@@ -4,9 +4,10 @@ import { getTotalRamUsage } from '../../utils/getTotalRamUsage'
|
||||
import { generateRandomSHA256 } from '../../utils/randomHash'
|
||||
import { getCoopProgramWallet, getUserProgramWallet } from '../wallet/walletUtils'
|
||||
import { processDecision } from '../soviet/processDecision'
|
||||
import { consoleIt } from '../shared'
|
||||
import { processApprove } from './processApprove'
|
||||
import { getSegment } from './getSegment'
|
||||
import { sourceProgramId } from './consts'
|
||||
import { capitalProgramId, walletProgramId } from './consts'
|
||||
|
||||
export async function investInProject(
|
||||
blockchain: any,
|
||||
@@ -30,14 +31,16 @@ export async function investInProject(
|
||||
'sha256',
|
||||
))[0] || { invested: '0.0000 RUB', available: '0.0000 RUB' }
|
||||
|
||||
const prevUserWallet = await getUserProgramWallet(blockchain, coopname, investor, sourceProgramId) || { blocked: '0.0000 RUB' }
|
||||
const prevProgramWallet = await getCoopProgramWallet(blockchain, coopname, sourceProgramId) || { blocked: '0.0000 RUB', share_contributions: '0.0000 RUB' }
|
||||
const prevWalletWallet = await getUserProgramWallet(blockchain, coopname, investor, walletProgramId) || { blocked: '0.0000 RUB' }
|
||||
const prevUserWallet = await getUserProgramWallet(blockchain, coopname, investor, capitalProgramId) || { blocked: '0.0000 RUB' }
|
||||
const prevProgramWallet = await getCoopProgramWallet(blockchain, coopname, capitalProgramId) || { blocked: '0.0000 RUB', share_contributions: '0.0000 RUB' }
|
||||
|
||||
console.log('📊 Балансы до инвестиции:')
|
||||
console.log('▶ Проект:', prevProject)
|
||||
console.log('▶ Кошелек пользователя:', prevUserWallet)
|
||||
console.log('▶ Кошелек программы:', prevProgramWallet)
|
||||
|
||||
console.log('▶ Главный кошелек пользователя:', prevWalletWallet)
|
||||
console.log('▶ Кошелек пользователя (благорост):', prevUserWallet)
|
||||
console.log('▶ Кошелек программы (благорост):', prevProgramWallet)
|
||||
console.log('▶ Сумма инвестиции: ', investAmount)
|
||||
// Создание инвестиции
|
||||
const createInvestData: CapitalContract.Actions.CreateProjectInvest.ICreateInvest = {
|
||||
coopname,
|
||||
@@ -65,28 +68,28 @@ export async function investInProject(
|
||||
expireSeconds: 30,
|
||||
},
|
||||
)
|
||||
|
||||
consoleIt(createInvestResult)
|
||||
getTotalRamUsage(createInvestResult)
|
||||
expect(createInvestResult.transaction_id).toBeDefined()
|
||||
|
||||
const blockchainInvest = (await blockchain.getTableRows(
|
||||
CapitalContract.contractName.production,
|
||||
coopname,
|
||||
'invests',
|
||||
1,
|
||||
investHash,
|
||||
investHash,
|
||||
2,
|
||||
'sha256',
|
||||
))[0]
|
||||
// const blockchainInvest = (await blockchain.getTableRows(
|
||||
// CapitalContract.contractName.production,
|
||||
// coopname,
|
||||
// 'invests',
|
||||
// 1,
|
||||
// investHash,
|
||||
// investHash,
|
||||
// 2,
|
||||
// 'sha256',
|
||||
// ))[0]
|
||||
|
||||
console.log('🔍 Инвестиция в блокчейне:', blockchainInvest)
|
||||
expect(blockchainInvest).toBeDefined()
|
||||
expect(blockchainInvest.status).toBe('created')
|
||||
// console.log('🔍 Инвестиция в блокчейне:', blockchainInvest)
|
||||
// expect(blockchainInvest).toBeDefined()
|
||||
// expect(blockchainInvest.status).toBe('created')
|
||||
|
||||
// Утверждение инвестиции
|
||||
console.log(`\n✅ Подтверждение инвестиции ${investHash}`)
|
||||
const approveInvestResult = await processApprove(blockchain, coopname, investHash)
|
||||
// console.log(`\n✅ Подтверждение инвестиции ${investHash}`)
|
||||
// const approveInvestResult = await processApprove(blockchain, coopname, investHash)
|
||||
|
||||
// Проверка утвержденной инвестиции
|
||||
const blockchainEmptyInvest = (await blockchain.getTableRows(
|
||||
@@ -114,8 +117,8 @@ export async function investInProject(
|
||||
'sha256',
|
||||
))[0]
|
||||
|
||||
const finalUserWallet = await getUserProgramWallet(blockchain, coopname, investor, sourceProgramId)
|
||||
const finalProgramWallet = await getCoopProgramWallet(blockchain, coopname, sourceProgramId)
|
||||
const finalUserWallet = await getUserProgramWallet(blockchain, coopname, investor, capitalProgramId)
|
||||
const finalProgramWallet = await getCoopProgramWallet(blockchain, coopname, capitalProgramId)
|
||||
|
||||
// Получение сегмента инвестора для данного проекта
|
||||
const segment = await getSegment(blockchain, coopname, projectHash, investor)
|
||||
@@ -123,8 +126,8 @@ export async function investInProject(
|
||||
console.log('\n📊 Балансы после инвестиции:')
|
||||
console.log('▶ Проект:', finalProject)
|
||||
console.log('▶ Сегмент:', segment)
|
||||
console.log('▶ Кошелек пользователя:', finalUserWallet)
|
||||
console.log('▶ Кошелек программы:', finalProgramWallet)
|
||||
console.log('▶ Кошелек пользователя (благорост):', finalUserWallet)
|
||||
console.log('▶ Кошелек программы (благорост):', finalProgramWallet)
|
||||
|
||||
// Проверка изменения балансов
|
||||
expect(parseFloat(finalUserWallet.blocked)).toBeCloseTo(parseFloat(prevUserWallet.blocked) + parseFloat(investAmount), 1)
|
||||
@@ -134,8 +137,8 @@ export async function investInProject(
|
||||
|
||||
return {
|
||||
investHash,
|
||||
invest: blockchainInvest,
|
||||
transactionId: approveInvestResult.transaction_id,
|
||||
// invest: blockchainInvest,
|
||||
transactionId: createInvestResult.transaction_id,
|
||||
prevProject,
|
||||
project: finalProject,
|
||||
segment,
|
||||
|
||||
@@ -8,6 +8,7 @@ export async function processApprove(blockchain: any, coopname: string, approval
|
||||
|
||||
const data: SovietContract.Actions.Approves.ConfirmApprove.IConfirmApprove = {
|
||||
coopname,
|
||||
username: 'ant',
|
||||
approval_hash: approvalHash,
|
||||
approved_document: fakeDocument,
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ export async function processCompleteVoting(
|
||||
console.log('▶ Голосов получено:', projectAfter.voting.votes_received)
|
||||
console.log('▶ Всего участников голосования:', projectAfter.voting.total_voters)
|
||||
|
||||
// Проверяем что статус изменился на completed
|
||||
expect(projectAfter.status).toBe('completed')
|
||||
// Проверяем что статус изменился на result
|
||||
expect(projectAfter.status).toBe('result')
|
||||
|
||||
console.log(`\n✅ Голосование для проекта ${project_hash} успешно завершено!`)
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ export async function processConvertSegment(
|
||||
convert_hash: convertHash,
|
||||
wallet_amount: walletAmount,
|
||||
capital_amount: capitalAmount,
|
||||
project_amount: projectAmount,
|
||||
convert_statement: fakeDocument,
|
||||
}
|
||||
|
||||
@@ -121,22 +120,26 @@ export async function processConvertSegment(
|
||||
}
|
||||
|
||||
// Проверяем глобальный кошелек программы капитализации (capital_amount)
|
||||
// investor_base уже заблокирован в _capital_program при инвестировании (createinvest)
|
||||
// В capitalAmount теперь передается только чистая дельта интеллектуального вклада
|
||||
if (capitalAmount !== '0.0000 RUB') {
|
||||
const capitalAmountValue = parseFloat(capitalAmount.split(' ')[0])
|
||||
const actualCapitalIncrease = capitalAmountValue
|
||||
const beforeBlocked = capitalWalletBefore ? parseFloat(capitalWalletBefore.blocked.split(' ')[0]) : 0
|
||||
const afterBlocked = capitalWalletAfter ? parseFloat(capitalWalletAfter.blocked.split(' ')[0]) : 0
|
||||
const expectedIncrease = beforeBlocked + capitalAmountValue
|
||||
console.log(`✅ Глобальный кошелек программы капитализации: ${beforeBlocked} → ${afterBlocked} (+${capitalAmountValue})`)
|
||||
const expectedIncrease = beforeBlocked + actualCapitalIncrease
|
||||
console.log(`✅ Глобальный кошелек программы капитализации: ${beforeBlocked} → ${afterBlocked} (+${actualCapitalIncrease})`)
|
||||
expect(afterBlocked).toBeCloseTo(expectedIncrease, 1)
|
||||
}
|
||||
|
||||
// Проверяем кошелек пользователя в программе капитализации (capital_amount)
|
||||
if (capitalAmount !== '0.0000 RUB') {
|
||||
const capitalAmountValue = parseFloat(capitalAmount.split(' ')[0])
|
||||
const actualCapitalIncrease = capitalAmountValue
|
||||
const beforeBlocked = userCapitalWalletBefore ? parseFloat(userCapitalWalletBefore.blocked.split(' ')[0]) : 0
|
||||
const afterBlocked = userCapitalWalletAfter ? parseFloat(userCapitalWalletAfter.blocked.split(' ')[0]) : 0
|
||||
const expectedIncrease = beforeBlocked + capitalAmountValue
|
||||
console.log(`✅ Кошелек пользователя в программе капитализации: ${beforeBlocked} → ${afterBlocked} (+${capitalAmountValue})`)
|
||||
const expectedIncrease = beforeBlocked + actualCapitalIncrease
|
||||
console.log(`✅ Кошелек пользователя в программе капитализации: ${beforeBlocked} → ${afterBlocked} (+${actualCapitalIncrease})`)
|
||||
expect(afterBlocked).toBeCloseTo(expectedIncrease, 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -95,13 +95,13 @@ export async function processDebt(
|
||||
// 2. Одобряем долг через processApprove (soviet контракт)
|
||||
console.log(`\n✅ Подтверждение долга ${debtHash} через soviet`)
|
||||
await processApprove(blockchain, coopname, debtHash)
|
||||
console.log('✅ Долг одобрен советом')
|
||||
console.log('✅ Долг одобрен председателем (создана agenda)')
|
||||
|
||||
// 3. Процессим решение совета
|
||||
// 3. Процессим решение совета (agenda для авторизации долга)
|
||||
await processLastDecision(blockchain, coopname)
|
||||
console.log('✅ Решение совета принято')
|
||||
console.log('✅ Решение совета принято (долг авторизован, outcome создан)')
|
||||
|
||||
// 5. Подтверждаем завершение вывода (gateway сам вызовет callback на capital)
|
||||
// 4. Подтверждаем завершение вывода (gateway сам вызовет callback на capital)
|
||||
const confirmOutcomeData: GatewayContract.Actions.CompleteOutcome.ICompleteOutcome = {
|
||||
coopname,
|
||||
outcome_hash: debtHash,
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function processStartVoting(
|
||||
3,
|
||||
'sha256',
|
||||
))[0]
|
||||
|
||||
console.log('projectBefore: ', projectBefore)
|
||||
// 1) Запускаем голосование по проекту
|
||||
const txStart = await blockchain.api.transact(
|
||||
{
|
||||
@@ -49,7 +49,7 @@ export async function processStartVoting(
|
||||
3,
|
||||
'sha256',
|
||||
))[0]
|
||||
|
||||
console.log('projectAfter: ', projectAfter)
|
||||
return {
|
||||
projectHash: project_hash,
|
||||
txStartId: txStart.transaction_id,
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function registerContributor(
|
||||
username: string,
|
||||
contributorHash: string,
|
||||
ratePerHour: string,
|
||||
hoursPerDay: number = 8,
|
||||
) {
|
||||
const contract = fakeDocument
|
||||
contract.signatures[0].signer = username
|
||||
@@ -18,8 +19,12 @@ export async function registerContributor(
|
||||
username,
|
||||
contributor_hash: contributorHash,
|
||||
rate_per_hour: ratePerHour,
|
||||
hours_per_day: hoursPerDay,
|
||||
is_external_contract: false,
|
||||
contract,
|
||||
storage_agreement: fakeDocument,
|
||||
generator_agreement: fakeDocument,
|
||||
blagorost_agreement: fakeDocument,
|
||||
}
|
||||
|
||||
const result = await blockchain.api.transact(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { signAgreement } from '../soviet/signAgreement'
|
||||
import { getCoopProgramWallet, getUserProgramWallet } from '../wallet/walletUtils'
|
||||
import { sourceProgramId, sourceProgramName } from './consts'
|
||||
|
||||
export async function signGenerationAgreement(
|
||||
export async function signGenerationContract(
|
||||
blockchain: any,
|
||||
coopname: string,
|
||||
username: string,
|
||||
|
||||
@@ -8,9 +8,11 @@ export async function processLastDecision(blockchain: Blockchain, coopname: stri
|
||||
SovietContract.contractName.production,
|
||||
coopname,
|
||||
SovietContract.Tables.Decisions.tableName,
|
||||
1,
|
||||
100, // берем больше, чтобы гарантированно получить все
|
||||
)
|
||||
const lastDecision = decisions[0]
|
||||
console.log('Активные вопросы на повестке: ', decisions)
|
||||
// Берем ПОСЛЕДНИЙ элемент массива (самый свежий)
|
||||
const lastDecision = decisions[decisions.length - 1]
|
||||
|
||||
// 3. Голосуем за решение совета
|
||||
await processDecision(blockchain, lastDecision.id)
|
||||
|
||||
@@ -16,3 +16,5 @@ export async function sendPostToCoopbackWithSecret(url: string, data: any) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export * from './randomData'
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { randomBytes } from 'node:crypto'
|
||||
|
||||
/**
|
||||
* Генерирует случайное текстовое содержимое для проектов
|
||||
* @param minLength минимальная длина текста (по умолчанию 500)
|
||||
* @param maxLength максимальная длина текста (по умолчанию 2000)
|
||||
* @returns строка с случайным текстовым содержимым
|
||||
*/
|
||||
export function generateRandomProjectData(minLength = 500, maxLength = 2000): string {
|
||||
const templates = [
|
||||
'Описание проекта',
|
||||
'Цели и задачи',
|
||||
'Ожидаемые результаты',
|
||||
'Этапы реализации',
|
||||
'Требования к участникам',
|
||||
'Технические спецификации',
|
||||
'План работ',
|
||||
'Бюджет проекта',
|
||||
'Риски и ограничения',
|
||||
'Методология',
|
||||
]
|
||||
|
||||
const words = [
|
||||
'разработка',
|
||||
'система',
|
||||
'модуль',
|
||||
'компонент',
|
||||
'интерфейс',
|
||||
'база данных',
|
||||
'архитектура',
|
||||
'тестирование',
|
||||
'оптимизация',
|
||||
'интеграция',
|
||||
'документация',
|
||||
'анализ',
|
||||
'проектирование',
|
||||
'внедрение',
|
||||
'поддержка',
|
||||
'масштабирование',
|
||||
'безопасность',
|
||||
'производительность',
|
||||
'надежность',
|
||||
'мониторинг',
|
||||
'отчетность',
|
||||
'функциональность',
|
||||
'пользователь',
|
||||
'администратор',
|
||||
'контент',
|
||||
'данные',
|
||||
'процесс',
|
||||
'workflow',
|
||||
'автоматизация',
|
||||
'управление',
|
||||
'контроль',
|
||||
]
|
||||
|
||||
const targetLength = Math.floor(Math.random() * (maxLength - minLength)) + minLength
|
||||
let content = ''
|
||||
|
||||
// Добавляем секции
|
||||
while (content.length < targetLength) {
|
||||
const template = templates[Math.floor(Math.random() * templates.length)]
|
||||
content += `\n\n## ${template}\n\n`
|
||||
|
||||
// Добавляем параграфы
|
||||
const paragraphCount = Math.floor(Math.random() * 3) + 1
|
||||
for (let i = 0; i < paragraphCount; i++) {
|
||||
const sentenceCount = Math.floor(Math.random() * 5) + 3
|
||||
for (let j = 0; j < sentenceCount; j++) {
|
||||
const wordCount = Math.floor(Math.random() * 10) + 5
|
||||
const sentence = []
|
||||
for (let k = 0; k < wordCount; k++) {
|
||||
sentence.push(words[Math.floor(Math.random() * words.length)])
|
||||
}
|
||||
content += `${sentence.join(' ')}. `
|
||||
}
|
||||
content += '\n\n'
|
||||
}
|
||||
|
||||
// Добавляем случайные данные для разнообразия
|
||||
if (Math.random() > 0.7) {
|
||||
content += `\n### Дополнительная информация\n\n`
|
||||
content += `ID: ${randomBytes(8).toString('hex')}\n`
|
||||
content += `Timestamp: ${new Date().toISOString()}\n`
|
||||
content += `Hash: ${randomBytes(16).toString('hex')}\n\n`
|
||||
}
|
||||
}
|
||||
|
||||
return content.substring(0, targetLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует расширенное описание проекта
|
||||
*/
|
||||
export function generateRandomDescription(): string {
|
||||
const descriptions = [
|
||||
'Инновационный проект по разработке современного решения для автоматизации бизнес-процессов',
|
||||
'Комплексная система управления ресурсами предприятия с использованием передовых технологий',
|
||||
'Платформа для координации работы распределенных команд и управления проектами',
|
||||
'Решение для цифровой трансформации традиционных процессов с акцентом на эффективность',
|
||||
'Система интеграции данных из различных источников с возможностью аналитики в реальном времени',
|
||||
]
|
||||
|
||||
const extras = [
|
||||
'Включает модули отчетности, мониторинга и аналитики.',
|
||||
'Поддерживает интеграцию с внешними системами через API.',
|
||||
'Обеспечивает высокую производительность и масштабируемость.',
|
||||
'Реализует современные подходы к безопасности данных.',
|
||||
'Предоставляет интуитивный пользовательский интерфейс.',
|
||||
]
|
||||
|
||||
let result = descriptions[Math.floor(Math.random() * descriptions.length)]
|
||||
result += ` ${extras[Math.floor(Math.random() * extras.length)]}`
|
||||
result += ` ${extras[Math.floor(Math.random() * extras.length)]}`
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует метаданные проекта в виде JSON
|
||||
*/
|
||||
export function generateRandomMeta(): string {
|
||||
const meta = {
|
||||
version: `${Math.floor(Math.random() * 5) + 1}.${Math.floor(Math.random() * 10)}.${Math.floor(Math.random() * 100)}`,
|
||||
category: ['development', 'infrastructure', 'analytics', 'automation'][Math.floor(Math.random() * 4)],
|
||||
priority: ['low', 'medium', 'high', 'critical'][Math.floor(Math.random() * 4)],
|
||||
tags: Array.from({ length: Math.floor(Math.random() * 5) + 3 }, () =>
|
||||
['backend', 'frontend', 'devops', 'testing', 'documentation', 'security', 'performance'][
|
||||
Math.floor(Math.random() * 7)
|
||||
]),
|
||||
created_by: 'system',
|
||||
created_at: new Date().toISOString(),
|
||||
last_modified: new Date().toISOString(),
|
||||
complexity: Math.floor(Math.random() * 10) + 1,
|
||||
estimated_duration_days: Math.floor(Math.random() * 180) + 30,
|
||||
}
|
||||
|
||||
return JSON.stringify(meta)
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует описание имущества
|
||||
*/
|
||||
export function generateRandomPropertyDescription(): string {
|
||||
const types = [
|
||||
'Серверное оборудование',
|
||||
'Лицензии на программное обеспечение',
|
||||
'Офисное оборудование',
|
||||
'Интеллектуальная собственность',
|
||||
'Доменные имена и хостинг',
|
||||
'Техническая документация',
|
||||
'Базы данных',
|
||||
'Исследовательские материалы',
|
||||
]
|
||||
|
||||
const details = [
|
||||
'включает все необходимые компоненты и документацию',
|
||||
'с полным комплектом сопроводительных материалов',
|
||||
'в отличном техническом состоянии',
|
||||
'прошедшее полную проверку и тестирование',
|
||||
'с гарантийным обслуживанием',
|
||||
'соответствует всем требованиям и стандартам',
|
||||
]
|
||||
|
||||
const type = types[Math.floor(Math.random() * types.length)]
|
||||
const detail = details[Math.floor(Math.random() * details.length)]
|
||||
const additional = `ID: ${randomBytes(4).toString('hex').toUpperCase()}`
|
||||
|
||||
return `${type} - ${detail}. ${additional}. ${generateRandomDescription().slice(0, 100)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует случайную сумму взноса для контрибьютора
|
||||
* @param min минимальная сумма (по умолчанию 500)
|
||||
* @param max максимальная сумма (по умолчанию 5000)
|
||||
*/
|
||||
export function generateRandomContributionAmount(min = 500, max = 2000): string {
|
||||
const amount = Math.floor(Math.random() * (max - min)) + min
|
||||
return `${amount}.0000 RUB`
|
||||
}
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Конфигурация
|
||||
TOTAL_RUNS=100
|
||||
LOG_FILE="stress-test-$(date +%Y%m%d-%H%M%S).log"
|
||||
FAILED=false
|
||||
SUCCESS_COUNT=0
|
||||
FAILED_COUNT=0
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
# Функция для отображения прогресса
|
||||
show_progress() {
|
||||
local current=$1
|
||||
local total=$2
|
||||
local success=$3
|
||||
local failed=$4
|
||||
|
||||
local percent=0
|
||||
if [ $total -gt 0 ]; then
|
||||
percent=$((current * 100 / total))
|
||||
fi
|
||||
local progress_bar=""
|
||||
local bar_width=50
|
||||
local filled=0
|
||||
if [ $total -gt 0 ]; then
|
||||
filled=$((current * bar_width / total))
|
||||
fi
|
||||
|
||||
for ((j=1; j<=bar_width; j++)); do
|
||||
if [ $j -le $filled ]; then
|
||||
progress_bar="${progress_bar}█"
|
||||
else
|
||||
progress_bar="${progress_bar}░"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Прогресс: [${progress_bar}] ${percent}% (${current}/${total}) | ✅ ${success} | ❌ ${failed}"
|
||||
}
|
||||
|
||||
echo "════════════════════════════════════════════════════" | tee -a "$LOG_FILE"
|
||||
echo " СТРЕСС-ТЕСТИРОВАНИЕ CAPITAL CONTRACT" | tee -a "$LOG_FILE"
|
||||
echo "════════════════════════════════════════════════════" | tee -a "$LOG_FILE"
|
||||
echo "Запуск: $(date)" | tee -a "$LOG_FILE"
|
||||
echo "Количество прогонов: $TOTAL_RUNS" | tee -a "$LOG_FILE"
|
||||
echo "Лог-файл: $LOG_FILE" | tee -a "$LOG_FILE"
|
||||
echo "════════════════════════════════════════════════════" | tee -a "$LOG_FILE"
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
|
||||
for ((i=1; i<=TOTAL_RUNS; i++)); do
|
||||
RUN_START=$(date +%s)
|
||||
|
||||
echo "════════════════════════════════════════" | tee -a "$LOG_FILE"
|
||||
echo "📊 Запуск #$i из $TOTAL_RUNS" | tee -a "$LOG_FILE"
|
||||
echo "════════════════════════════════════════" | tee -a "$LOG_FILE"
|
||||
|
||||
# Запускаем тест и сохраняем вывод
|
||||
npx vitest run capital.test --run --testTimeout=60000 2>&1 | tee -a "$LOG_FILE"
|
||||
TEST_EXIT_CODE=${PIPESTATUS[0]}
|
||||
|
||||
if [ $TEST_EXIT_CODE -eq 0 ]; then
|
||||
RUN_END=$(date +%s)
|
||||
RUN_DURATION=$((RUN_END - RUN_START))
|
||||
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||
echo "✅ Запуск #$i успешен (время: ${RUN_DURATION}с)" | tee -a "$LOG_FILE"
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
else
|
||||
RUN_END=$(date +%s)
|
||||
RUN_DURATION=$((RUN_END - RUN_START))
|
||||
FAILED_COUNT=$((FAILED_COUNT + 1))
|
||||
echo "❌ ТЕСТ УПАЛ НА ЗАПУСКЕ #$i (время: ${RUN_DURATION}с)" | tee -a "$LOG_FILE"
|
||||
echo "⚠️ Проверка на access violation или другие ошибки..." | tee -a "$LOG_FILE"
|
||||
FAILED=true
|
||||
break
|
||||
fi
|
||||
|
||||
# Обновляем прогресс после каждого успешного теста
|
||||
show_progress $((SUCCESS_COUNT + FAILED_COUNT)) $TOTAL_RUNS $SUCCESS_COUNT $FAILED_COUNT
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
done
|
||||
|
||||
END_TIME=$(date +%s)
|
||||
TOTAL_DURATION=$((END_TIME - START_TIME))
|
||||
AVG_DURATION=$((SUCCESS_COUNT > 0 ? TOTAL_DURATION / SUCCESS_COUNT : 0))
|
||||
|
||||
# Финальный показ прогресса
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
show_progress $((SUCCESS_COUNT + FAILED_COUNT)) $TOTAL_RUNS $SUCCESS_COUNT $FAILED_COUNT
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
echo "════════════════════════════════════════════════════" | tee -a "$LOG_FILE"
|
||||
echo " ИТОГОВАЯ СТАТИСТИКА" | tee -a "$LOG_FILE"
|
||||
echo "════════════════════════════════════════════════════" | tee -a "$LOG_FILE"
|
||||
echo "Завершено: $(date)" | tee -a "$LOG_FILE"
|
||||
echo "Успешных прогонов: $SUCCESS_COUNT" | tee -a "$LOG_FILE"
|
||||
echo "Проваленных прогонов: $FAILED_COUNT" | tee -a "$LOG_FILE"
|
||||
echo "Всего прогонов: $((SUCCESS_COUNT + FAILED_COUNT)) из $TOTAL_RUNS" | tee -a "$LOG_FILE"
|
||||
echo "Общее время: ${TOTAL_DURATION}с ($(($TOTAL_DURATION / 60))м)" | tee -a "$LOG_FILE"
|
||||
if [ $SUCCESS_COUNT -gt 0 ]; then
|
||||
echo "Среднее время на прогон: ${AVG_DURATION}с" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
if [ "$FAILED" = true ]; then
|
||||
echo "Статус: ❌ ПРОВАЛЕН" | tee -a "$LOG_FILE"
|
||||
echo "════════════════════════════════════════════════════" | tee -a "$LOG_FILE"
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
echo "⚠️ Тестирование прервано из-за ошибки на запуске #$i" | tee -a "$LOG_FILE"
|
||||
echo "📋 Полный лог сохранен в: $LOG_FILE" | tee -a "$LOG_FILE"
|
||||
exit 1
|
||||
else
|
||||
echo "Статус: ✅ УСПЕШНО" | tee -a "$LOG_FILE"
|
||||
echo "════════════════════════════════════════════════════" | tee -a "$LOG_FILE"
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
echo "🎉 Все $TOTAL_RUNS запусков успешны!" | tee -a "$LOG_FILE"
|
||||
echo "📋 Полный лог сохранен в: $LOG_FILE" | tee -a "$LOG_FILE"
|
||||
exit 0
|
||||
fi
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/cleos",
|
||||
"version": "2025.9.1",
|
||||
"version": "2026.2.22-alpha-4",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
---
|
||||
name: Конструктор программ регистрации
|
||||
overview: Создание гибкого конструктора программ регистрации с выбором дополнительных соглашений на основе типа аккаунта и кооператива. Для Восхода физики и ИП выбирают между программами "Генерация" и "Капитализация".
|
||||
todos:
|
||||
- id: backend-interfaces
|
||||
content: Создать новые интерфейсы для программ регистрации в agreement-config.interface.ts
|
||||
status: completed
|
||||
- id: backend-programs-config
|
||||
content: Создать конфигурацию программ registration-programs.config.ts с программами для voskhod
|
||||
status: completed
|
||||
- id: backend-agreement-service
|
||||
content: Обновить AgreementConfigurationService с методами работы с программами
|
||||
status: completed
|
||||
- id: backend-documents-service
|
||||
content: Добавить поддержку program_key в RegistrationDocumentsService
|
||||
status: completed
|
||||
- id: backend-dto
|
||||
content: Обновить DTO для добавления program_key и создать RegistrationConfigDTO
|
||||
status: completed
|
||||
- id: backend-resolver
|
||||
content: Добавить query getRegistrationConfig в SystemResolver
|
||||
status: completed
|
||||
- id: frontend-store
|
||||
content: Обновить Registrator store для хранения selectedProgramKey и добавить шаг SelectProgram
|
||||
status: completed
|
||||
- id: frontend-select-component
|
||||
content: Создать компонент SelectProgram.vue для выбора программы
|
||||
status: completed
|
||||
- id: frontend-conditional-rendering
|
||||
content: Добавить условный рендеринг шага SelectProgram в SignUp.vue
|
||||
status: completed
|
||||
- id: frontend-pass-program-key
|
||||
content: Обновить вызов generateRegistrationDocuments для передачи program_key
|
||||
status: completed
|
||||
- id: sdk-update
|
||||
content: Обновить SDK мутацию и добавить query getRegistrationConfig
|
||||
status: completed
|
||||
- id: add-generator-agreement
|
||||
content: Добавить соглашение generator_offer в registration-agreements.config.ts
|
||||
status: completed
|
||||
---
|
||||
|
||||
# План реализации конструктора программ регистрации
|
||||
|
||||
## Архитектура решения
|
||||
|
||||
Система будет состоять из трёх уровней:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Frontend: SelectProgram шаг] --> B[Backend: getRegistrationConfig query]
|
||||
A --> C[Backend: generateRegistrationDocuments mutation]
|
||||
B --> D[RegistrationProgramsConfig]
|
||||
C --> D
|
||||
D --> E[AgreementConfigurationService]
|
||||
E --> F[Генерация документов]
|
||||
```
|
||||
|
||||
## 1. Backend: Расширение конфигурации соглашений
|
||||
|
||||
### 1.1 Новые интерфейсы конфигурации
|
||||
|
||||
**Файл:** [`components/controller/src/domain/registration/config/agreement-config.interface.ts`](components/controller/src/domain/registration/config/agreement-config.interface.ts)
|
||||
|
||||
Добавить новые интерфейсы для программ регистрации:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Описание программы регистрации с её соглашениями
|
||||
*/
|
||||
export interface IRegistrationProgram {
|
||||
/** Уникальный ключ программы (например, "generation", "capitalization") */
|
||||
key: string;
|
||||
|
||||
/** Название программы для отображения */
|
||||
title: string;
|
||||
|
||||
/** Описание программы */
|
||||
description: string;
|
||||
|
||||
/** URL изображения (опционально) */
|
||||
image_url?: string;
|
||||
|
||||
/** Минимальные требования для участия */
|
||||
requirements?: string;
|
||||
|
||||
/** Для каких типов аккаунтов доступна эта программа */
|
||||
applicable_account_types: AccountType[];
|
||||
|
||||
/** Список ID соглашений, которые требуются для этой программы */
|
||||
agreement_ids: string[];
|
||||
|
||||
/** Порядок отображения */
|
||||
order: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Конфигурация программ регистрации для кооператива
|
||||
*/
|
||||
export interface ICooperativeRegistrationPrograms {
|
||||
/** Название кооператива */
|
||||
coopname: string;
|
||||
|
||||
/** Доступные программы */
|
||||
programs: IRegistrationProgram[];
|
||||
|
||||
/** Нужен ли выбор программы (если false - используется первая подходящая) */
|
||||
requires_selection: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Создание конфигурации программ
|
||||
|
||||
**Файл:** `components/controller/src/domain/registration/config/registration-programs.config.ts` (новый)
|
||||
|
||||
```typescript
|
||||
export const REGISTRATION_PROGRAMS_CONFIG: ICooperativeRegistrationPrograms[] = [
|
||||
{
|
||||
coopname: 'voskhod',
|
||||
requires_selection: true,
|
||||
programs: [
|
||||
{
|
||||
key: 'generation',
|
||||
title: 'Программа Генерация',
|
||||
description: 'Участвовать в производстве Кооперативной Экономики через вклад временем, имуществом или деньгами в конкретные проекты. Минимальный взнос 10 часов в месяц.',
|
||||
applicable_account_types: [AccountType.individual, AccountType.entrepreneur],
|
||||
agreement_ids: ['generator_offer'], // новое соглашение
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
key: 'capitalization',
|
||||
title: 'Программа Капитализация',
|
||||
description: 'Участвовать в производстве Кооперативной Экономики через вклад имуществом или денег в систему. Минимальный взнос 100 000 руб в течение 14 дней.',
|
||||
applicable_account_types: [AccountType.individual, AccountType.entrepreneur],
|
||||
agreement_ids: ['blagorost_offer'],
|
||||
order: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 1.3 Добавление нового соглашения generator_offer
|
||||
|
||||
**Файл:** [`components/controller/src/domain/registration/config/registration-agreements.config.ts`](components/controller/src/domain/registration/config/registration-agreements.config.ts)
|
||||
|
||||
Добавить новое соглашение:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'generator_offer',
|
||||
registry_id: Cooperative.Registry.GeneratorOffer.registry_id, // 996
|
||||
agreement_type: 'generator',
|
||||
title: 'Оферта по целевой потребительской программе "Генератор"',
|
||||
checkbox_text: 'Я прочитал и принимаю',
|
||||
link_text: 'оферту по целевой потребительской программе "Генератор"',
|
||||
is_blockchain_agreement: true,
|
||||
link_to_statement: true,
|
||||
applicable_account_types: [], // не используется напрямую, только через программы
|
||||
order: 6,
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Обновление AgreementConfigurationService
|
||||
|
||||
**Файл:** [`components/controller/src/domain/registration/services/agreement-configuration.service.ts`](components/controller/src/domain/registration/services/agreement-configuration.service.ts)
|
||||
|
||||
Добавить методы для работы с программами:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Получить доступные программы регистрации для кооператива и типа аккаунта
|
||||
*/
|
||||
getAvailablePrograms(coopname: string, accountType: AccountType): IRegistrationProgram[] {
|
||||
const config = REGISTRATION_PROGRAMS_CONFIG.find(c => c.coopname === coopname);
|
||||
if (!config) return [];
|
||||
|
||||
return config.programs
|
||||
.filter(p => p.applicable_account_types.includes(accountType))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить конфигурацию программ для кооператива
|
||||
*/
|
||||
getCooperativeProgramsConfig(coopname: string): ICooperativeRegistrationPrograms | null {
|
||||
return REGISTRATION_PROGRAMS_CONFIG.find(c => c.coopname === coopname) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить соглашения для программы
|
||||
*/
|
||||
getAgreementsForProgram(programKey: string, coopname: string): IAgreementConfigItem[] {
|
||||
const config = REGISTRATION_PROGRAMS_CONFIG.find(c => c.coopname === coopname);
|
||||
if (!config) return [];
|
||||
|
||||
const program = config.programs.find(p => p.key === programKey);
|
||||
if (!program) return [];
|
||||
|
||||
return program.agreement_ids
|
||||
.map(id => this.getAgreementById(id))
|
||||
.filter((a): a is IAgreementConfigItem => a !== null);
|
||||
}
|
||||
```
|
||||
|
||||
Обновить метод `getAgreementsForAccountType`:
|
||||
|
||||
```typescript
|
||||
getAgreementsForAccountType(
|
||||
accountType: AccountType,
|
||||
coopname?: string,
|
||||
programKey?: string
|
||||
): IAgreementConfigItem[] {
|
||||
// Базовые соглашения (всегда включаются)
|
||||
const baseAgreements = REGISTRATION_AGREEMENTS_CONFIG.agreements
|
||||
.filter(a => a.applicable_account_types.includes(accountType))
|
||||
.filter(a => a.id !== 'blagorost_offer' && a.id !== 'generator_offer');
|
||||
|
||||
// Если указана программа - добавляем её соглашения
|
||||
if (programKey && coopname) {
|
||||
const programAgreements = this.getAgreementsForProgram(programKey, coopname);
|
||||
return [...baseAgreements, ...programAgreements].sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
// Старая логика для обратной совместимости (voskhod без program_key)
|
||||
if (coopname === 'voskhod' && accountType === AccountType.individual) {
|
||||
const capitalization = this.getAgreementById('blagorost_offer');
|
||||
if (capitalization) {
|
||||
return [...baseAgreements, capitalization].sort((a, b) => a.order - b.order);
|
||||
}
|
||||
}
|
||||
|
||||
return baseAgreements.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 Обновление RegistrationDocumentsService
|
||||
|
||||
**Файл:** [`components/controller/src/domain/registration/services/registration-documents.service.ts`](components/controller/src/domain/registration/services/registration-documents.service.ts)
|
||||
|
||||
Добавить `program_key` в интерфейс:
|
||||
|
||||
```typescript
|
||||
// В интерфейсе IGenerateRegistrationDocumentsInput
|
||||
program_key?: string;
|
||||
```
|
||||
|
||||
Обновить метод `generateRegistrationDocuments`:
|
||||
|
||||
```typescript
|
||||
const agreementsConfig = this.agreementConfigService.getAgreementsForAccountType(
|
||||
account_type,
|
||||
coopname,
|
||||
input.program_key
|
||||
);
|
||||
```
|
||||
|
||||
### 1.6 Обновление DTO
|
||||
|
||||
**Файл:** [`components/controller/src/application/user/dto/generate-registration-documents-input.dto.ts`](components/controller/src/application/user/dto/generate-registration-documents-input.dto.ts)
|
||||
|
||||
```typescript
|
||||
@Field({ nullable: true, description: 'Ключ выбранной программы регистрации (опционально)' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
program_key?: string;
|
||||
```
|
||||
|
||||
### 1.7 Новый Query для получения конфигурации
|
||||
|
||||
**Файл:** [`components/controller/src/application/system/resolvers/system.resolver.ts`](components/controller/src/application/system/resolvers/system.resolver.ts)
|
||||
|
||||
Добавить новый query:
|
||||
|
||||
```typescript
|
||||
@Query(() => RegistrationConfigDTO, {
|
||||
name: 'getRegistrationConfig',
|
||||
description: 'Получить конфигурацию программ регистрации для кооператива',
|
||||
})
|
||||
async getRegistrationConfig(
|
||||
@Args('coopname') coopname: string,
|
||||
@Args('account_type', { type: () => AccountType }) accountType: AccountType,
|
||||
): Promise<RegistrationConfigDTO> {
|
||||
return this.systemService.getRegistrationConfig(coopname, accountType);
|
||||
}
|
||||
```
|
||||
|
||||
**Файл:** `components/controller/src/application/system/dto/registration-config.dto.ts` (новый)
|
||||
|
||||
```typescript
|
||||
@ObjectType('RegistrationProgram')
|
||||
export class RegistrationProgramDTO {
|
||||
@Field() key!: string;
|
||||
@Field() title!: string;
|
||||
@Field() description!: string;
|
||||
@Field({ nullable: true }) image_url?: string;
|
||||
@Field({ nullable: true }) requirements?: string;
|
||||
@Field(() => [AccountType]) applicable_account_types!: AccountType[];
|
||||
@Field(() => Int) order!: number;
|
||||
}
|
||||
|
||||
@ObjectType('RegistrationConfig')
|
||||
export class RegistrationConfigDTO {
|
||||
@Field() requires_selection!: boolean;
|
||||
@Field(() => [RegistrationProgramDTO]) programs!: RegistrationProgramDTO[];
|
||||
}
|
||||
```
|
||||
|
||||
**Файл:** [`components/controller/src/application/system/services/system.service.ts`](components/controller/src/application/system/services/system.service.ts)
|
||||
|
||||
```typescript
|
||||
async getRegistrationConfig(
|
||||
coopname: string,
|
||||
accountType: AccountType
|
||||
): Promise<RegistrationConfigDTO> {
|
||||
const config = this.agreementConfigService.getCooperativeProgramsConfig(coopname);
|
||||
|
||||
if (!config) {
|
||||
return new RegistrationConfigDTO({
|
||||
requires_selection: false,
|
||||
programs: [],
|
||||
});
|
||||
}
|
||||
|
||||
const programs = this.agreementConfigService.getAvailablePrograms(coopname, accountType);
|
||||
|
||||
return new RegistrationConfigDTO({
|
||||
requires_selection: config.requires_selection && programs.length > 1,
|
||||
programs,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Frontend: Новый шаг выбора программы
|
||||
|
||||
### 2.1 Обновление Registrator Store
|
||||
|
||||
**Файл:** [`components/desktop/src/entities/Registrator/model/store.ts`](components/desktop/src/entities/Registrator/model/store.ts)
|
||||
|
||||
Добавить в state:
|
||||
|
||||
```typescript
|
||||
const state = reactive({
|
||||
// ... существующие поля
|
||||
selectedProgramKey: '', // выбранный ключ программы
|
||||
});
|
||||
```
|
||||
|
||||
Добавить шаг `SelectProgram` в массив stepNames (между `SetUserData` и `GenerateAccount`).
|
||||
|
||||
### 2.2 Создание компонента SelectProgram
|
||||
|
||||
**Файл:** `components/desktop/src/pages/Registrator/SignUp/SelectProgram.vue` (новый)
|
||||
|
||||
```vue
|
||||
<template lang="pug">
|
||||
div
|
||||
q-step(
|
||||
:name='registratorStore.steps.SelectProgram',
|
||||
title='Выберите программу участия',
|
||||
:done='registratorStore.isStepDone("SelectProgram")'
|
||||
)
|
||||
div(v-if='isLoading').full-width.text-center.q-mt-lg
|
||||
Loader(text='Загружаем доступные программы...')
|
||||
|
||||
div(v-else-if='programs.length > 0')
|
||||
p.text-body1 Выберите программу, в которой вы хотите участвовать:
|
||||
|
||||
q-list.q-mt-md
|
||||
q-item(
|
||||
v-for='program in programs',
|
||||
:key='program.key',
|
||||
clickable,
|
||||
v-ripple,
|
||||
:active='registratorStore.state.selectedProgramKey === program.key',
|
||||
@click='selectProgram(program.key)'
|
||||
)
|
||||
q-item-section
|
||||
q-item-label.text-h6 {{ program.title }}
|
||||
q-item-label(caption).q-mt-sm {{ program.description }}
|
||||
q-item-label(v-if='program.requirements', caption).q-mt-xs.text-weight-bold
|
||||
| {{ program.requirements }}
|
||||
q-item-section(side)
|
||||
q-radio(
|
||||
:model-value='registratorStore.state.selectedProgramKey',
|
||||
:val='program.key',
|
||||
@update:model-value='selectProgram(program.key)'
|
||||
)
|
||||
|
||||
div.q-mt-lg
|
||||
q-btn.col-md-6.col-xs-12(flat @click='registratorStore.prev()')
|
||||
i.fa.fa-arrow-left
|
||||
span.q-ml-md назад
|
||||
|
||||
q-btn.q-mt-lg.q-mb-lg(
|
||||
color='primary',
|
||||
label='Продолжить',
|
||||
:disabled='!registratorStore.state.selectedProgramKey',
|
||||
@click='registratorStore.next()'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRegistratorStore } from 'src/entities/Registrator';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { Loader } from 'src/shared/ui/Loader';
|
||||
import { FailAlert } from 'src/shared/api';
|
||||
// TODO: добавить API метод для получения конфигурации
|
||||
|
||||
const registratorStore = useRegistratorStore();
|
||||
const systemStore = useSystemStore();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const programs = ref<any[]>([]);
|
||||
|
||||
const loadPrograms = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
// TODO: вызов API getRegistrationConfig
|
||||
const config = await getRegistrationConfig(
|
||||
systemStore.info.coopname,
|
||||
registratorStore.state.userData.type
|
||||
);
|
||||
programs.value = config.programs;
|
||||
} catch (e: any) {
|
||||
FailAlert(e);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const selectProgram = (key: string) => {
|
||||
registratorStore.state.selectedProgramKey = key;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (registratorStore.state.step === registratorStore.steps.SelectProgram) {
|
||||
loadPrograms();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2.3 Условный рендеринг шага
|
||||
|
||||
**Файл:** [`components/desktop/src/pages/Registrator/SignUp/SignUp.vue`](components/desktop/src/pages/Registrator/SignUp/SignUp.vue)
|
||||
|
||||
Добавить условный рендеринг:
|
||||
|
||||
```pug
|
||||
SelectProgram(v-if='shouldShowProgramSelection')
|
||||
```
|
||||
|
||||
Добавить computed для определения необходимости показа:
|
||||
|
||||
```typescript
|
||||
const shouldShowProgramSelection = computed(() => {
|
||||
// Показываем только если есть конфигурация программ для данного кооператива
|
||||
// и тип аккаунта выбран
|
||||
return (
|
||||
registratorStore.state.userData.type &&
|
||||
// TODO: проверка наличия программ из конфигурации
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### 2.4 Обновление вызова generateRegistrationDocuments
|
||||
|
||||
**Файл:** `components/desktop/src/features/User/CreateUser` (или где происходит вызов)
|
||||
|
||||
Передавать `program_key` при генерации документов:
|
||||
|
||||
```typescript
|
||||
await generateAllRegistrationDocuments(
|
||||
coopname,
|
||||
username,
|
||||
accountType,
|
||||
registratorStore.state.selectedProgramKey // новый параметр
|
||||
);
|
||||
```
|
||||
|
||||
## 3. SDK: Обновление мутации
|
||||
|
||||
**Файл:** [`components/sdk/src/mutations/registration/generateRegistrationDocuments.ts`](components/sdk/src/mutations/registration/generateRegistrationDocuments.ts) (если существует, иначе найти где определена)
|
||||
|
||||
Добавить опциональное поле `program_key` в input мутации.
|
||||
|
||||
**Файл:** Где определен query `getRegistrationConfig` (создать новый)
|
||||
|
||||
```typescript
|
||||
export const getRegistrationConfig = {
|
||||
name: 'getRegistrationConfig',
|
||||
query: (coopname: string, account_type: string) => ({
|
||||
getRegistrationConfig: [
|
||||
{ coopname, account_type },
|
||||
{
|
||||
requires_selection: true,
|
||||
programs: {
|
||||
key: true,
|
||||
title: true,
|
||||
description: true,
|
||||
image_url: true,
|
||||
requirements: true,
|
||||
order: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
```
|
||||
|
||||
## 4. Обновление логики перехода между шагами
|
||||
|
||||
В [`components/desktop/src/pages/Registrator/SignUp/SetUserData.vue`](components/desktop/src/pages/Registrator/SignUp/SetUserData.vue) при клике "Продолжить" нужно:
|
||||
|
||||
1. Проверить, нужен ли шаг SelectProgram
|
||||
2. Если да - перейти на него
|
||||
3. Если нет - перейти на GenerateAccount (как сейчас)
|
||||
|
||||
## Итоговая последовательность шагов регистрации
|
||||
|
||||
1. **EmailInput** - ввод email
|
||||
2. **SetUserData** - выбор типа аккаунта и заполнение данных
|
||||
3. **SelectProgram** *(условный)* - выбор программы (только для voskhod, физики/ИП)
|
||||
4. **GenerateAccount** - генерация аккаунта
|
||||
5. **SelectBranch** *(условный)* - выбор филиала
|
||||
6. **ReadStatement** - чтение и принятие соглашений (уже учитывают program_key)
|
||||
7. **SignStatement** - подпись
|
||||
8. **PayInitial** - оплата
|
||||
9. **WaitingRegistration** - ожидание
|
||||
|
||||
## Миграция и обратная совместимость
|
||||
|
||||
- Для всех кооперативов кроме voskhod ничего не меняется
|
||||
- Для voskhod: если не передан `program_key`, используется старая логика (blagorost_offer для физиков)
|
||||
- После развертывания можно убрать хардкод для voskhod из `AgreementConfigurationService`
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
description: Общие правила
|
||||
alwaysApply: true
|
||||
---
|
||||
Мы работаем в МОНО-репозитории pnpm, любая установка пакетов производится ЧЕРЕЗ ФИЛЬТР компонента в корне: pnpm add glob --filter <package>.
|
||||
|
||||
НИКОГДА НЕ ЗАПУСКАЙ ПРИЛОЖЕНИЕ ИЛИ ЕГО СБОРКУ!!! Просто отчитайся что всё сделал.
|
||||
|
||||
НИКОГДА НЕ ЗАПУСКАЙ ТЕСТЫ ИЛИ ПРОВЕРКУ ТИПОВ ИЛИ ЛИНТЕР!!!
|
||||
|
||||
Никаких ANY типов! Строгая типизация всегда.
|
||||
@@ -0,0 +1,247 @@
|
||||
---
|
||||
description: Правила работы с пагинацией в контроллере
|
||||
alwaysApply: false
|
||||
---
|
||||
# Правила работы с пагинацией в Controller
|
||||
|
||||
## Общие принципы
|
||||
|
||||
**Всегда используйте доменные интерфейсы пагинации** вместо примитивных типов (`skip`, `take`, `limit`, `offset`).
|
||||
Расчет общего количества записей **всегда** производится через `count()` в репозитории.
|
||||
|
||||
## Архитектурные слои и интерфейсы
|
||||
|
||||
### 1. DTO слой (GraphQL)
|
||||
```typescript
|
||||
// Входные параметры от клиента
|
||||
PaginationInputDTO {
|
||||
page: number; // Номер страницы (начиная с 1)
|
||||
limit: number; // Количество элементов на странице
|
||||
sortBy?: string; // Поле сортировки
|
||||
sortOrder: 'ASC' | 'DESC'; // Направление сортировки
|
||||
}
|
||||
|
||||
// Результат для клиента
|
||||
PaginationResult<T> {
|
||||
items: T[]; // Элементы текущей страницы
|
||||
totalCount: number; // Общее количество элементов
|
||||
totalPages: number; // Общее количество страниц
|
||||
currentPage: number; // Текущая страница
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Домен слой
|
||||
```typescript
|
||||
// Входные параметры для домена
|
||||
PaginationInputDomainInterface {
|
||||
page: number;
|
||||
limit: number;
|
||||
sortBy?: string;
|
||||
sortOrder: 'ASC' | 'DESC';
|
||||
}
|
||||
|
||||
// Результат из домена
|
||||
PaginationResultDomainInterface<T> {
|
||||
items: T[];
|
||||
totalCount: number;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Поток данных
|
||||
|
||||
### В Resolver (GraphQL слой)
|
||||
```typescript
|
||||
// Создание GraphQL типа для пагинированного результата
|
||||
const paginatedEntitiesResult = createPaginationResult(EntityDTO, 'PaginatedEntities');
|
||||
|
||||
@Query(() => paginatedEntitiesResult)
|
||||
async getEntities(
|
||||
@Args('filter') filter?: FilterDTO,
|
||||
@Args('options') options?: PaginationInputDTO
|
||||
): Promise<PaginationResult<EntityDTO>> {
|
||||
|
||||
// Преобразование в доменный интерфейс
|
||||
const domainOptions: PaginationInputDomainInterface | undefined = options
|
||||
? {
|
||||
page: options.page,
|
||||
limit: options.limit,
|
||||
sortBy: options.sortBy,
|
||||
sortOrder: options.sortOrder,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Вызов сервиса
|
||||
const result = await this.service.getEntities(filter, domainOptions);
|
||||
|
||||
// DTO готовится в сервисе, резолвер проксирует результат
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### В Service (Прикладной слой)
|
||||
```typescript
|
||||
async getEntities(
|
||||
filter?: FilterDTO,
|
||||
options?: PaginationInputDomainInterface
|
||||
): Promise<PaginationResult<EntityDTO>> {
|
||||
|
||||
// Получение результата из домена
|
||||
const domainResult = await this.interactor.getEntities(filter, options);
|
||||
|
||||
// Преобразование доменных сущностей в DTO
|
||||
const items = domainResult.items.map(item => this.mapToDTO(item));
|
||||
|
||||
// Возврат полного пагинированного результата с DTO
|
||||
return {
|
||||
items,
|
||||
totalCount: domainResult.totalCount,
|
||||
totalPages: domainResult.totalPages,
|
||||
currentPage: domainResult.currentPage,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### В Interactor/UseCase (Домен слой)
|
||||
```typescript
|
||||
async getEntities(
|
||||
filter?: DomainFilter,
|
||||
options?: PaginationInputDomainInterface
|
||||
): Promise<PaginationResultDomainInterface<DomainEntity>> {
|
||||
|
||||
// Вызов репозитория
|
||||
return await this.repository.findAllPaginated(filter, options);
|
||||
}
|
||||
```
|
||||
|
||||
### В Repository (Инфраструктурный слой)
|
||||
```typescript
|
||||
async findAllPaginated(
|
||||
filter?: Filter,
|
||||
options?: PaginationInputDomainInterface
|
||||
): Promise<PaginationResultDomainInterface<Entity>> {
|
||||
|
||||
// 1. Валидация параметров
|
||||
const validatedOptions = options
|
||||
? PaginationUtils.validatePaginationOptions(options)
|
||||
: { page: 1, limit: 10, sortOrder: 'ASC' };
|
||||
|
||||
// 2. Получение SQL параметров
|
||||
const { limit, offset } = PaginationUtils.getSqlPaginationParams(validatedOptions);
|
||||
|
||||
// 3. Подсчет общего количества (ОБЯЗАТЕЛЬНО!)
|
||||
const totalCount = await this.repository.count({ where });
|
||||
|
||||
// 4. Получение данных с пагинацией
|
||||
const entities = await this.repository.find({
|
||||
where,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
order: orderBy,
|
||||
});
|
||||
|
||||
// 5. Преобразование в доменные сущности
|
||||
const items = entities.map(entity => this.mapper.toDomain(entity));
|
||||
|
||||
// 6. Создание результата через утилиту
|
||||
return PaginationUtils.createPaginationResult(items, totalCount, validatedOptions);
|
||||
}
|
||||
```
|
||||
|
||||
## GraphQL типы пагинации
|
||||
|
||||
Для создания GraphQL типов пагинированных результатов используйте `createPaginationResult`:
|
||||
|
||||
```typescript
|
||||
import { createPaginationResult } from '~/application/common/dto/pagination.dto';
|
||||
|
||||
// Создание GraphQL типа для пагинированного результата
|
||||
const paginatedEntitiesResult = createPaginationResult(EntityDTO, 'PaginatedEntities');
|
||||
|
||||
// Использование в резолвере
|
||||
@Query(() => paginatedEntitiesResult)
|
||||
async getEntities(...) : Promise<PaginationResult<EntityDTO>>
|
||||
```
|
||||
|
||||
Функция `createPaginationResult` создает GraphQL DTO тип для пагинированного ответа, но не готовит сами данные.
|
||||
|
||||
## Утилиты пагинации
|
||||
|
||||
Используйте `PaginationUtils` для стандартных операций:
|
||||
|
||||
```typescript
|
||||
import { PaginationUtils } from '~/shared/utils/pagination.utils';
|
||||
|
||||
// Валидация параметров
|
||||
const validatedOptions = PaginationUtils.validatePaginationOptions(options);
|
||||
|
||||
// Получение SQL параметров (limit, offset)
|
||||
const { limit, offset } = PaginationUtils.getSqlPaginationParams(options);
|
||||
|
||||
// Создание результата пагинации
|
||||
const result = PaginationUtils.createPaginationResult(items, totalCount, options);
|
||||
```
|
||||
|
||||
## Валидация параметров
|
||||
|
||||
- `page >= 1` - номер страницы должен быть положительным
|
||||
- `1 <= limit <= 1000` - ограничение на количество элементов
|
||||
- `sortOrder` должен быть `'ASC'` или `'DESC'`
|
||||
|
||||
## Запрещенные паттерны
|
||||
|
||||
❌ **НЕ ПРАВИЛЬНО:**
|
||||
```typescript
|
||||
// Не используйте примитивные типы
|
||||
async getApprovals(filter?: Filter, skip = 0, take = 50)
|
||||
|
||||
// Не рассчитывайте пагинацию вручную
|
||||
const totalPages = Math.ceil(totalCount / limit); // Делайте это в утилите
|
||||
|
||||
// Не возвращайте сырые данные без пагинации
|
||||
return await this.repository.findAll(); // Всегда используйте пагинацию
|
||||
```
|
||||
|
||||
✅ **ПРАВИЛЬНО:**
|
||||
```typescript
|
||||
// Сервис возвращает полный пагинированный результат с DTO
|
||||
async getApprovals(
|
||||
filter?: ApprovalFilterInput,
|
||||
options?: PaginationInputDomainInterface
|
||||
): Promise<PaginationResult<ApprovalDTO>> {
|
||||
// Получение доменного результата
|
||||
const domainResult = await this.repository.findAllPaginated(filter, options);
|
||||
|
||||
// Преобразование в DTO
|
||||
const items = domainResult.items.map(item => this.toDTO(item));
|
||||
|
||||
// Возврат полного результата
|
||||
return {
|
||||
items,
|
||||
totalCount: domainResult.totalCount,
|
||||
totalPages: domainResult.totalPages,
|
||||
currentPage: domainResult.currentPage,
|
||||
};
|
||||
}
|
||||
|
||||
// Резолвер проксирует результат от сервиса
|
||||
@Query(() => paginatedApprovalsResult)
|
||||
async getApprovals(filter, options): Promise<PaginationResult<ApprovalDTO>> {
|
||||
return await this.service.getApprovals(filter, domainOptions);
|
||||
}
|
||||
```
|
||||
|
||||
## Ключевые моменты
|
||||
|
||||
1. **DTO готовится в Service** - сервис получает доменные сущности и преобразует их в DTO перед возвратом
|
||||
2. **GraphQL типы создаются через createPaginationResult** - это создает схему GraphQL, а не данные
|
||||
3. **Резолвер проксирует результат** - резолвер просто вызывает сервис и возвращает результат
|
||||
4. **Полный пагинированный результат** - всегда возвращается PaginationResult<T> с items, totalCount, totalPages, currentPage
|
||||
|
||||
## Примеры из кода
|
||||
|
||||
- ✅ `ApprovalService` - правильная реализация с полным PaginationResult
|
||||
- ✅ `InvestsManagementService` - прямое преобразование Domain → DTO в сервисе
|
||||
- ✅ `TimeTrackingService` - явное преобразование полей в DTO
|
||||
- ✅ `InvestTypeormRepository` - полная реализация с валидацией через PaginationUtils
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
description: О правилах на бэкенде
|
||||
globs: **/controller/**/*.ts
|
||||
---
|
||||
# NestJS Controller и Чистая архитектура
|
||||
|
||||
## Основные принципы
|
||||
1. Чистая архитектура - главный архитектурный подход. Пиши код с комментариями.
|
||||
2. Типы IName, IChecksum256, ITimePointSec - это просто строки.
|
||||
3. Домен должен быть полностью изолирован от инфраструктурных деталей.
|
||||
4. Направление зависимостей - внутрь (к ядру, домену).
|
||||
5. Домен, инфраструктура и приложение связаны через App.module, НЕ НУЖНО импортировать их друг в друга, а достаточно просто импортировать домен на уровне приложения чтобы использовать все экспорты домена.
|
||||
|
||||
## Структура проекта
|
||||
- `domain/` - доменный слой (бизнес-логика, независимая от инфраструктуры)
|
||||
- `infrastructure/` - инфраструктурный слой (адаптеры к внешним системам)
|
||||
- `modules/` - слой приложения (DTO, резолверы, сервисы)
|
||||
|
||||
## Слои и их взаимодействие
|
||||
1. **Домен**:
|
||||
- Содержит чистые доменные интерфейсы и реализованные на них сущности
|
||||
- НЕ должен зависеть от `cooptypes` (инфраструктурный контракт)
|
||||
- Использует собственные доменные типы (например, Date вместо строковых timestamp)
|
||||
- Интерфейсы портов описывают взаимодействие с внешними системами
|
||||
|
||||
2. **Инфраструктура**:
|
||||
- Содержит адаптеры к внешним системам (блокчейн, БД и т.д.)
|
||||
- Осуществляет преобразование между доменными и инфраструктурными типами
|
||||
- Общая логика преобразования выносится в утилитарные классы (например, `DomainToBlockchainUtils`)
|
||||
|
||||
3. **Модули (приложение)**:
|
||||
- DTO имплементируют доменные интерфейсы напрямую
|
||||
- Резолвер вызывает сервис, который принимает DTO
|
||||
- Сервис передает объекты DTO в интерактор приложения (юз-кейс)
|
||||
|
||||
## Правила
|
||||
|
||||
Чистая фрактальная трехуровневая архитектура срезов домена (domain) и инфраструктуры (infrastructure), оркестрируемая приложением (application). Фрактал раскрывается в расширениях (extensions), каждое из которых также выстраивается по правилам чистой трехуровневой архитектуры срезов домена и инфраструктуры с окрестрацией на уровне своего приложения.
|
||||
|
||||
Общая архитектура связей:
|
||||
- AppResolver -> AppService[DTO<->Domain] -> AppInteractor -> DomainService -> DomainPort <- InfraAdapter -> [при необходимости] AnyAppOrDomainService
|
||||
|
||||
Все сервисы внедряются через инъекцию (DI) с указанием символа реализации. Импорты разрешены только вглубь - к домену. Из домена переход разрешен только в инфраструктуру через порт и его адаптер.
|
||||
|
||||
Прямые импорты без DI запрещены. Импорты из соседних срезов того же архитектурного уровня запрещены. Направление зависимостей от приложения - к домену, от домена через порт в его инфраструктурный адаптер.
|
||||
|
||||
Взаимодействие расширений с основным приложением осуществляется только через порты домена основного приложения.
|
||||
|
||||
Глобальные модули запрещены. В редких случаях сложных циклических зависимостей на период рефакторинга разрешается обоснованно использовать forwardRef.
|
||||
|
||||
Уровень приложения оркестрирует вызовы доменов через порты в инфраструктуру. Домены не обращаются друг к друга. Срезы уровня приложений не обращаются друг к другу, а только вызывают порты домена.
|
||||
|
||||
```
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
description: SDK мутации и запросы для подключения на рабочем столе
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
Бэкенд реализован здесь components/controller, sdk здесь components/sdk.
|
||||
|
||||
Пример создания Query к API через SDK:
|
||||
|
||||
``` ts
|
||||
async function loadBranches(data: IGetBranchesInput): Promise<IBranch[]> {
|
||||
const { [Queries.Branches.GetBranches.name]: output } = await client.Query(
|
||||
Queries.Branches.GetBranches.query,
|
||||
{
|
||||
variables: {
|
||||
data
|
||||
}
|
||||
}
|
||||
);
|
||||
return output;
|
||||
}
|
||||
```
|
||||
|
||||
Такие запросы обычно хранятся в папке Entities и вызываются из его store.
|
||||
|
||||
Пример мутации:
|
||||
|
||||
``` ts
|
||||
async function selectBranch(data: ISelectBranchInput): Promise<boolean>{
|
||||
const {[Mutations.Branches.SelectBranch.name]: result} = await client.Mutation(Mutations.Branches.SelectBranch.mutation, {variables: {
|
||||
data
|
||||
}})
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
Мутации обычно храним в папке features.
|
||||
|
||||
Как можешь обратить внимание, все мутации и запросы строятся по одному шаблону. Вся необходимая информация уже есть в SDK.
|
||||
|
||||
## Генерация и подпись документов
|
||||
|
||||
### Генерация документов
|
||||
|
||||
Для генерации документов используются мутации SDK типа `Generate[DocumentName]`. Например:
|
||||
|
||||
```ts
|
||||
import { client } from 'src/shared/api/client';
|
||||
import { Mutations } from '@coopenomics/sdk';
|
||||
|
||||
async function generateDocument(data: IGenerateDocumentInput): Promise<IGeneratedDocumentOutput> {
|
||||
const { [Mutations.Capital.GenerateGenerationContract.name]: result } =
|
||||
await client.Mutation(Mutations.Capital.GenerateGenerationContract.mutation, {
|
||||
variables: {
|
||||
data: {
|
||||
coopname: 'coopname',
|
||||
username: 'username',
|
||||
lang: 'ru'
|
||||
},
|
||||
options
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**Composable для генерации документов:**
|
||||
```ts
|
||||
import { ref } from 'vue';
|
||||
import { api } from '../api';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
|
||||
export function useGenerateDocument() {
|
||||
const system = useSystemStore();
|
||||
const session = useSessionStore();
|
||||
|
||||
const isGenerating = ref(false);
|
||||
const generatedDocument = ref<IGeneratedDocumentOutput | null>(null);
|
||||
|
||||
const generate = async () => {
|
||||
isGenerating.value = true;
|
||||
try {
|
||||
const data = {
|
||||
coopname: system.info.coopname,
|
||||
username: session.username,
|
||||
lang: 'ru',
|
||||
};
|
||||
|
||||
generatedDocument.value = await api.generateDocument(data);
|
||||
return generatedDocument.value;
|
||||
} finally {
|
||||
isGenerating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
generate,
|
||||
isGenerating,
|
||||
generatedDocument,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Подпись документов
|
||||
|
||||
Для подписи документов используется класс `DigitalDocument`:
|
||||
|
||||
```ts
|
||||
import { DigitalDocument } from 'src/shared/lib/document';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
|
||||
const session = useSessionStore();
|
||||
const digitalDocument = new DigitalDocument(generatedDocument);
|
||||
|
||||
// Подпись документа
|
||||
const signedDoc = await digitalDocument.sign(session.username);
|
||||
```
|
||||
|
||||
### Стандартный путь: генерация + подпись
|
||||
|
||||
**Стандартный путь (без демонстрации):**
|
||||
```ts
|
||||
// 1. Генерируем документ
|
||||
const document = await generateDocument({
|
||||
coopname: system.info.coopname,
|
||||
username: session.username,
|
||||
lang: 'ru'
|
||||
});
|
||||
|
||||
// 2. Подписываем документ
|
||||
const digitalDocument = new DigitalDocument(document);
|
||||
const signedDoc = await digitalDocument.sign(session.username);
|
||||
|
||||
// 3. Отправляем подписанный документ на бэкенд
|
||||
await submitDocumentMutation(signedDoc);
|
||||
```
|
||||
|
||||
### Демонстрация документов (опционально)
|
||||
|
||||
Демонстрация документов не всегда требуется. Если нужно показать документ пользователю перед подписью:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<DocumentHtmlReader v-if="generatedDocument" :html="generatedDocument.html" />
|
||||
<q-btn @click="signAndSubmit" label="Подписать документ" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { generate, isGenerating, generatedDocument } = useGenerateDocument();
|
||||
|
||||
// Генерация при монтировании
|
||||
onMounted(async () => {
|
||||
await generate();
|
||||
});
|
||||
|
||||
// Подпись и отправка
|
||||
const signAndSubmit = async () => {
|
||||
const digitalDocument = new DigitalDocument(generatedDocument.value);
|
||||
const signedDoc = await digitalDocument.sign(session.username);
|
||||
await submitDocumentMutation(signedDoc);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
**Важно:**
|
||||
- Демонстрация документов опциональна
|
||||
- Стандартный путь: генерация → подпись → отправка
|
||||
- Используй `DocumentHtmlReader` для отображения HTML документов
|
||||
- Все документы подписываются через `DigitalDocument.sign()`
|
||||
- Данные для генерации обычно включают `coopname`, `username`, `lang`
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
description: О правилах на фронтенде
|
||||
globs: **/desktop/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# DESKTOP
|
||||
|
||||
- Пишем код фронтенда в архитектуре Feature Sliced Design (FSD).
|
||||
- Стиль шаблонов PUG на composite API.
|
||||
- Всегда создаём index.ts файлы для vue компонент.
|
||||
- Создавай индексные файлы для vue через export {default as NAME} from './where', а на уровне выше, если это необходимо, делай export * from './ui'
|
||||
- Всегда удаляем неиспользуемые импорты.
|
||||
- Не используем emit избыточно. Вместо них используем композабл функции моделей фич, если необходимо. Emit только в крайнем случае.
|
||||
- Архитектура приложения состоит из папки src, которая формирует core приложение по FSD, и папки extensions, в каждой папке которого есть подобная FSD структура. Т.е. в src есть features, widgets, entities, shared для core приложения, и в extensions/<name>/ - тоже есть features, widgets, entities, shared для конкретного расширения. По-возможности делаем их изолированными, т.е. импорты между src и extensions ЗАПРЕЩЕНЫ. Исключением являются FailAlert и SuccessAlert, которые допустимо импортировать.
|
||||
- Не создаём не используемые переменные!
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
description: Правила работы с макросом main.py в компоненте docs
|
||||
alwaysApply: false
|
||||
---
|
||||
Файл [main.py](mdc:monocoop/monocoop/monocoop/components/docs/main.py) автоматически генерит ссылки на документацию SDK и GraphQL, которые формируются и публикуются автоматически. При создании документации к методам всегда применяй ссылки на SDK и GraphQL по форме:
|
||||
{{ get_sdk_doc("Mutations", "Accounts", "RegisterAccount") }} | {{ get_graphql_doc("Mutation.registerAccount") }}
|
||||
|
||||
### 🎯 Основные паттерны использования
|
||||
|
||||
**1. Стандартная структура для методов API:**
|
||||
```markdown
|
||||
## Название действия
|
||||
{{ get_sdk_doc("Namespace", "Module", "Method") }} | {{ get_graphql_doc("Type.methodName") }}
|
||||
|
||||
{{ get_typedoc_input("Namespace.Module.Method") }}
|
||||
|
||||
Результат:
|
||||
{{ get_typedoc_definition("Namespace.Module.Method", "IOutput") }}
|
||||
```
|
||||
|
||||
**2. Описание + пример (для сложных методов):**
|
||||
```markdown
|
||||
{{ get_typedoc_desc("Namespace.Module.Method") }}
|
||||
|
||||
{{ get_typedoc_input("Namespace.Module.Method") }}
|
||||
```
|
||||
|
||||
**3. Ссылки на типы данных:**
|
||||
```markdown
|
||||
У каждого аккаунта есть объект {{ get_graphql_definition("Account") }} в GraphQL-API
|
||||
```
|
||||
|
||||
### 🔧 Макросы и их применение
|
||||
|
||||
| Макрос | Назначение | Пример использования |
|
||||
|--------|------------|---------------------|
|
||||
| `get_sdk_doc` | Ссылка на SDK документацию | `{{ get_sdk_doc("Mutations", "Payments", "CreateDeposit") }}` |
|
||||
| `get_graphql_doc` | Ссылка на GraphQL операцию | `{{ get_graphql_doc("Query.getAccount") }}` |
|
||||
| `get_graphql_definition` | Ссылка на GraphQL тип | `{{ get_graphql_definition("Account") }}` |
|
||||
| `get_class_doc` | Ссылка на класс/метод SDK | `{{ get_class_doc("Document", "sign") }}` |
|
||||
| `get_typedoc_input` | **Полный пример** TypeScript вызова | `{{ get_typedoc_input("Mutations.Auth.Login") }}` |
|
||||
| `get_typedoc_definition` | **Структура интерфейса** | `{{ get_typedoc_definition("Mutations.Auth.Login", "IOutput") }}` |
|
||||
| `get_typedoc_desc` | Описание + примеры | `{{ get_typedoc_desc("Mutations.Auth.Login") }}` |
|
||||
| `get_typedoc_value` | Значение константы | `{{ get_typedoc_value("Constants.API_VERSION") }}` |
|
||||
|
||||
### 📝 Правила оформления
|
||||
|
||||
**ОБЯЗАТЕЛЬНО:**
|
||||
- Всегда используй `get_typedoc_input` для демонстрации **КАК вызывать** метод
|
||||
- Всегда используй `get_typedoc_definition` для показа **ЧТО возвращается**
|
||||
- Комбинируй SDK и GraphQL ссылки через ` | `: `{{ get_sdk_doc(...) }} | {{ get_graphql_doc(...) }}`
|
||||
|
||||
**ЖЕЛАТЕЛЬНО:**
|
||||
- Для сложных методов добавляй `get_typedoc_desc` в начало
|
||||
- Используй `get_graphql_definition` для ссылок на типы данных в тексте
|
||||
- Группируй связанные операции в одном разделе
|
||||
|
||||
**ФОРМАТ ССЫЛОК:**
|
||||
- SDK: `"Namespace", "Module", "Method"` (3 аргумента)
|
||||
- GraphQL: `"Type.methodName"` (точка между типом и методом)
|
||||
- Определения: просто имя типа `"TypeName"`
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
description: Правила формирования агрегатов документов в controller
|
||||
alwaysApply: false
|
||||
---
|
||||
## 📋 Правила формирования агрегатов документов
|
||||
|
||||
### 📝 **В сервисах**
|
||||
1. **Инжект DocumentAggregationService** в конструктор сервиса
|
||||
2. **Создай async метод toDTO** для преобразования доменных сущностей в DTO
|
||||
3. **Вызывай buildDocumentAggregate()** для поля документа:
|
||||
```typescript
|
||||
const document = await this.documentAggregationService.buildDocumentAggregate(entity.documentField);
|
||||
```
|
||||
4. **Добавь поле document в возвращаемый объект**
|
||||
|
||||
### 🎯 **В DTO**
|
||||
1. **Импорт DocumentAggregateDTO**
|
||||
2. **Добавь поле document** с типом `DocumentAggregateDTO | null`
|
||||
3. **nullable: true** в GraphQL декораторе
|
||||
|
||||
### 🔄 **В резолверах**
|
||||
- **Ничего не меняй** - резолверы автоматически получают DTO с полем document
|
||||
|
||||
### ⚡ **Чек-лист**
|
||||
- ✅ DocumentAggregationService инжектирован
|
||||
- ✅ Метод toDTO стал async
|
||||
- ✅ Все вызовы toDTO стали await
|
||||
- ✅ Поле document добавлено в DTO
|
||||
- ✅ Импорт DocumentAggregateDTO добавлен
|
||||
- ✅ Тип поля корректный (DocumentAggregateDTO | null)
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
description: Правила работы с двойными подписями документов
|
||||
alwaysApply: false
|
||||
---
|
||||
# Правила работы с двойными подписями документов
|
||||
|
||||
## Общая концепция
|
||||
|
||||
Двойная подпись означает, что документ подписывается двумя разными участниками процесса. Каждая подпись имеет свой уникальный идентификатор (`signatureId`).
|
||||
|
||||
## Типы документов
|
||||
|
||||
### IDocumentAggregate
|
||||
Структура агрегата документа, содержащая:
|
||||
- `document`: `SignedDigitalDocument` - подписанный документ с подписями
|
||||
- `hash`: `string` - хеш документа
|
||||
- `rawDocument`: `GeneratedDocument` - сырой (неподписанный) документ, необходимый для генерации новых подписей
|
||||
|
||||
### SignedDigitalDocument
|
||||
Подписанный документ, содержащий:
|
||||
- `doc_hash`: хеш содержимого
|
||||
- `hash`: общий хеш
|
||||
- `meta`: метаданные документа
|
||||
- `meta_hash`: хеш метаданных
|
||||
- `signatures`: массив подписей
|
||||
- `version`: версия стандарта
|
||||
|
||||
### ZGeneratedDocument (из cooptypes)
|
||||
Базовый документ для подписи:
|
||||
- `full_title`: полное название
|
||||
- `html`: HTML содержимое
|
||||
- `hash`: хеш документа
|
||||
- `meta`: метаданные
|
||||
- `binary`: бинарные данные (Uint8Array или string)
|
||||
|
||||
## Процесс двойной подписи
|
||||
|
||||
### 1. Подготовка данных
|
||||
Для добавления второй подписи к документу необходимо иметь:
|
||||
- `rawDocument` (ZGeneratedDocument) - базовый документ без подписей
|
||||
- `existingSignedDocuments` - массив уже существующих подписанных документов
|
||||
|
||||
### 2. Вызов signDocument
|
||||
```typescript
|
||||
const doubleSignedDocument = await signDocument(
|
||||
rawDocument, // ZGeneratedDocument - базовый документ
|
||||
username, // string - имя пользователя, подписывающего документ
|
||||
2, // signatureId - идентификатор подписи (2 для второй)
|
||||
[existingDocument] // existingSignedDocuments - массив существующих подписей
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Параметры signDocument
|
||||
- **document**: `ZGeneratedDocument` - базовый документ (обязательно `rawDocument` из `IDocumentAggregate`)
|
||||
- **account**: `string` - имя аккаунта подписанта
|
||||
- **signatureId**: `number` - идентификатор подписи:
|
||||
- `1` - первая подпись
|
||||
- `2` - вторая подпись
|
||||
- `3` - третья подпись (если необходимо)
|
||||
- **existingSignedDocuments**: `ISignedDocument2[]` - массив документов с существующими подписями
|
||||
|
||||
## Пример реализации
|
||||
|
||||
```typescript
|
||||
export function useConfirmApproval() {
|
||||
const { signDocument } = useSignDocument();
|
||||
const { username } = useSessionStore();
|
||||
|
||||
const confirmApproval = async (
|
||||
coopname: string,
|
||||
approved_document: IDocumentAggregate
|
||||
): Promise<IConfirmApprovalOutput> => {
|
||||
if (!approved_document.rawDocument) {
|
||||
throw new Error('Документ не найден');
|
||||
}
|
||||
|
||||
// Подписываем документ второй подписью
|
||||
const doubleSignedDocument = await signDocument(
|
||||
approved_document.rawDocument, // ZGeneratedDocument
|
||||
username, // string
|
||||
2, // signatureId для второй подписи
|
||||
[approved_document.document], // existingSignedDocuments
|
||||
);
|
||||
|
||||
// Отправляем документ с двойной подписью
|
||||
return await api.confirmApproval({
|
||||
coopname,
|
||||
approval_hash: approved_document.hash,
|
||||
approved_document: doubleSignedDocument,
|
||||
});
|
||||
};
|
||||
|
||||
return { confirmApproval };
|
||||
}
|
||||
```
|
||||
|
||||
## Важные замечания
|
||||
|
||||
1. **rawDocument обязателен** - без него невозможно добавить новую подпись
|
||||
2. **signatureId должен быть уникальным** - каждая подпись имеет свой идентификатор
|
||||
3. **existingSignedDocuments** - содержит все предыдущие подписи документа
|
||||
4. **Типизация** - строго следить за типами `IDocumentAggregate`, `ZGeneratedDocument`, `SignedDigitalDocument`
|
||||
5. **Session Store** - использовать `useSessionStore().username` для получения имени подписанта
|
||||
|
||||
## Распространенные ошибки
|
||||
|
||||
- Попытка подписать `SignedDigitalDocument` напрямую (нужен `ZGeneratedDocument`)
|
||||
- Отсутствие `rawDocument` в `IDocumentAggregate`
|
||||
- Неправильный `signatureId` (не уникальный)
|
||||
- Отсутствие `existingSignedDocuments` при добавлении второй подписи
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
description: Правила контроля изменений документов в фабрике (factory)
|
||||
alwaysApply: false
|
||||
---
|
||||
# Структура файлов шаблонов документов и алгоритм правки
|
||||
|
||||
## Основные директории и файлы
|
||||
|
||||
### 1. Определение типов и интерфейсов (cooptypes)
|
||||
**Путь:** `/cooptypes/src/cooperative/registry/[ID.DocumentName]/index.ts`
|
||||
|
||||
В этих файлах определяются:
|
||||
- Интерфейсы данных (`IAgendaMeet`, etc.)
|
||||
- Основная модель данных документа (`Model`)
|
||||
- HTML шаблон документа (`context`)
|
||||
- Переводы строк (`translations`)
|
||||
- Пример данных для тестирования (`exampleData`)
|
||||
|
||||
**Для изменения шаблона нужно править:**
|
||||
- Интерфейсы модели данных при изменении структуры
|
||||
- HTML в переменной `context` для изменения внешнего вида
|
||||
- Строки переводов в `translations.ru`
|
||||
|
||||
### 2. Схемы валидации (factory)
|
||||
**Путь:** `/factory/src/Schema/[SchemaName].ts`
|
||||
|
||||
Содержат:
|
||||
- JSON схемы для валидации данных
|
||||
- Определение обязательных полей
|
||||
|
||||
**При изменении структуры данных:**
|
||||
- Обновите соответствующую схему валидации
|
||||
- Обновите список обязательных полей в `required`
|
||||
|
||||
### 3. Фабрики документов (factory)
|
||||
**Путь:** `/factory/src/Actions/[ID.DocumentName].ts`
|
||||
|
||||
Обрабатывают:
|
||||
- Получение данных из разных источников
|
||||
- Сборку модели для шаблонизатора
|
||||
- Валидацию данных по схеме
|
||||
- Генерацию PDF
|
||||
|
||||
**При изменении логики сборки документа:**
|
||||
- Обновите метод `generateDocument()`
|
||||
- Добавьте новые источники данных
|
||||
|
||||
### 4. Шаблоны (factory)
|
||||
**Путь:** `/factory/src/Templates/[ID.DocumentName].ts`
|
||||
|
||||
Связывают:
|
||||
- Шаблон из cooptypes
|
||||
- Схему валидации
|
||||
- Метаданные документа
|
||||
|
||||
**После изменения в cooptypes:**
|
||||
- Убедитесь, что схема в Templates соответствует новой модели
|
||||
|
||||
### 5. Тесты (factory)
|
||||
**Путь:** `/factory/test/[category].test.ts`
|
||||
|
||||
Содержат:
|
||||
- Тестовые данные для генерации документов
|
||||
- Вызовы функции `testDocumentGeneration`
|
||||
|
||||
**После внесения изменений:**
|
||||
- Обновите тестовые данные в соответствии с новой структурой
|
||||
- Запустите тесты командой `pnpm test`
|
||||
|
||||
## Алгоритм внесения изменений
|
||||
|
||||
1. **Модификация типов**
|
||||
- Изменить файл в `/cooptypes/src/cooperative/registry/[ID.DocumentName]/index.ts`
|
||||
- Обновить интерфейсы, шаблон HTML и переводы
|
||||
|
||||
2. **Обновление схемы валидации**
|
||||
- Изменить соответствующую схему в `/factory/src/Schema/`
|
||||
- Убедиться, что обязательные поля совпадают с интерфейсом
|
||||
|
||||
3. **Компиляция библиотеки типов**
|
||||
- Выполнить `cd /cooptypes && pnpm build`
|
||||
|
||||
4. **Обновление тестовых данных**
|
||||
- Привести тестовые данные в соответствие с новой структурой
|
||||
|
||||
5. **Запуск тестов**
|
||||
- Выполнить `cd /factory && pnpm test [filename].test.ts`
|
||||
|
||||
## Особенности шаблонизации
|
||||
|
||||
- Используется Nunjucks для шаблонов
|
||||
- Поддерживаются конструкции: `{% if %}`, `{% for %}`, `{% trans %}`
|
||||
- Переменные вставляются через `{{ variable }}`
|
||||
- Переводы через `{% trans 'KEY' %}`
|
||||
- Дополнительно поддерживается передача переменной в перевод через `{% trans 'KEY', some_var1, some_var2 %}`, при этом в самом переводе необходимо использовать порядковые ключи {0}, {1} для доступа к переданным переменным.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
description: Typeorm entity name. Выбрать имя для typeorm сущности.
|
||||
alwaysApply: false
|
||||
---
|
||||
Имя сущность typeorm должно быть в паттерне **/entities/*entity.ts
|
||||
|
||||
'src/infrastructure/**/entities/*entity.{ts,js}',
|
||||
'src/extensions/**/entities/*entity.{ts,js}',
|
||||
'src/shared/**/entities/*entity.{ts,js}',
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# Архитектура расширений в MonoCoop
|
||||
|
||||
## Основные принципы
|
||||
1. Расширения построены на модулях NestJS с использованием шаблона "Порты и адаптеры"
|
||||
2. Каждое расширение наследуется от `BaseExtModule` и реализует интерфейс `OnModuleInit`
|
||||
3. Расширения могут взаимодействовать с блокчейном через соответствующие порты
|
||||
4. Конфигурации расширений хранятся в БД и описываются с помощью Zod-схем
|
||||
5. Расширения регистрируются в глобальном реестре `AppRegistry`
|
||||
|
||||
## Структура расширения
|
||||
Минимальная структура расширения включает:
|
||||
- `XXX-extension.module.ts` - основной модуль расширения
|
||||
- `package.json` - информация о пакете
|
||||
- `README.md` - документация
|
||||
- `INSTALL.md` - инструкции по установке
|
||||
- `CHANGELOG.md` - история изменений
|
||||
|
||||
## Создание нового расширения
|
||||
1. Создайте директорию для расширения в `components/controller/src/extensions/`
|
||||
2. Создайте основной класс расширения, наследующийся от `BaseExtModule`
|
||||
3. Определите Zod-схему для конфигурации
|
||||
4. Реализуйте метод `initialize()`
|
||||
5. Зарегистрируйте расширение в `extensions.registry.ts`
|
||||
6. Добавьте расширение в список дефолтных приложений в `extension-domain.service.ts`
|
||||
|
||||
## Пример структуры модуля расширения
|
||||
```typescript
|
||||
// XXX-extension.module.ts
|
||||
export class XXXPlugin extends BaseExtModule {
|
||||
constructor(...) {
|
||||
super();
|
||||
}
|
||||
|
||||
name = 'xxx';
|
||||
plugin!: ExtensionDomainEntity<IConfig>;
|
||||
public configSchemas = Schema;
|
||||
|
||||
async initialize() {
|
||||
// Инициализация расширения
|
||||
// Настройка cron-задач
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [XXXPlugin],
|
||||
})
|
||||
export class XXXPluginModule {
|
||||
constructor(private readonly xxxPlugin: XXXPlugin) {}
|
||||
|
||||
async initialize() {
|
||||
await this.xxxPlugin.initialize();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Управление отображением конфигурации
|
||||
Zod-схемы используются для автоматического отображения формы настроек в интерфейсе пользователя.
|
||||
Через интерфейс `DeserializedDescriptionOfExtension` из `components/controller/src/types/shared/extension.types.ts`
|
||||
можно управлять отображением полей формы:
|
||||
|
||||
```typescript
|
||||
export const Schema = z.object({
|
||||
// Базовое поле с меткой
|
||||
simpleField: z.string().describe(
|
||||
describeField({
|
||||
label: 'Название поля',
|
||||
note: 'Подсказка под полем'
|
||||
})
|
||||
),
|
||||
|
||||
// Скрытое поле для служебного использования
|
||||
hiddenField: z.string().describe(
|
||||
describeField({
|
||||
label: 'Скрытое поле',
|
||||
visible: false
|
||||
})
|
||||
),
|
||||
|
||||
// Поле с проверкой значения
|
||||
validatedField: z.number().describe(
|
||||
describeField({
|
||||
label: 'Поле с валидацией',
|
||||
rules: ['val >= 5', 'val <= 100'],
|
||||
})
|
||||
),
|
||||
|
||||
// Форматированное поле с префиксом и суффиксом
|
||||
formattedField: z.number().describe(
|
||||
describeField({
|
||||
label: 'Форматированное поле',
|
||||
prepend: '$',
|
||||
append: 'USD',
|
||||
})
|
||||
),
|
||||
|
||||
// Многострочное текстовое поле
|
||||
multilineField: z.string().describe(
|
||||
describeField({
|
||||
label: 'Многострочное поле',
|
||||
maxRows: 5,
|
||||
minLength: 10,
|
||||
maxLength: 1000
|
||||
})
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
Доступные поля для управления отображением:
|
||||
- `label` - название поля (обязательное)
|
||||
- `note` - пояснение или подсказка
|
||||
- `visible` - видимость поля (по умолчанию true)
|
||||
- `rules` - правила валидации в виде строковых выражений
|
||||
- `mask` - маска для ввода
|
||||
- `fillMask` - автозаполнение маски
|
||||
- `minLength` / `maxLength` - ограничения длины для текстовых полей
|
||||
- `maxRows` - количество строк для многострочного ввода
|
||||
- `append` / `prepend` - текст до/после значения поля
|
||||
|
||||
## Взаимодействие с блокчейном
|
||||
Для взаимодействия с блокчейном:
|
||||
1. Определите порт в доменном слое (например, `SovietBlockchainPort`)
|
||||
2. Инжектируйте порт в конструкторе расширения через DI
|
||||
3. Используйте методы порта для взаимодействия с блокчейном
|
||||
|
||||
```typescript
|
||||
@Inject(SOVIET_BLOCKCHAIN_PORT) private readonly sovietBlockchainPort: SovietBlockchainPort
|
||||
// ...
|
||||
const decisions = await this.sovietBlockchainPort.getDecisions(coopname);
|
||||
```
|
||||
|
||||
## Настройка планировщика задач
|
||||
Расширения могут использовать cron-задачи для периодического выполнения операций:
|
||||
|
||||
```typescript
|
||||
import cron from 'node-cron';
|
||||
|
||||
// Регистрация cron-задачи (каждые N минут)
|
||||
const cronExpression = `*/${this.plugin.config.checkInterval} * * * *`;
|
||||
cron.schedule(cronExpression, () => {
|
||||
this.logger.info('Запуск запланированной задачи');
|
||||
this.runTask();
|
||||
});
|
||||
```
|
||||
|
||||
## Работа с конфигурацией
|
||||
1. Определите Zod-схему для конфигурации
|
||||
2. Используйте `describeField` для добавления UI-метаданных к полям
|
||||
3. Получайте и обновляйте конфигурацию через репозиторий `extensionRepository`
|
||||
|
||||
```typescript
|
||||
export const Schema = z.object({
|
||||
checkInterval: z.number().describe(
|
||||
describeField({
|
||||
label: 'Интервал проверки (в минутах)',
|
||||
note: 'Минимум: 5 минут',
|
||||
rules: ['val >= 5'],
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
// Обновление конфигурации
|
||||
this.plugin.config.lastCheckDate = new Date().toISOString();
|
||||
await this.extensionRepository.update(this.plugin);
|
||||
```
|
||||
|
||||
## Логирование действий
|
||||
Расширения должны логировать свои действия:
|
||||
1. Используйте `WinstonLoggerService` для системного логирования
|
||||
2. Используйте `LogExtensionDomainRepository` для хранения логов в БД
|
||||
|
||||
```typescript
|
||||
// Системное логирование
|
||||
this.logger.info(`Выполнение операции для ${id}`);
|
||||
|
||||
// Сохранение лога в БД
|
||||
await this.logExtensionRepository.push(this.name, {
|
||||
type: 'operation',
|
||||
timestamp: new Date().toISOString(),
|
||||
data: { ... },
|
||||
});
|
||||
```
|
||||
|
||||
## Регистрация расширения
|
||||
После создания расширения, добавьте его в `extensions.registry.ts`:
|
||||
|
||||
```typescript
|
||||
export const AppRegistry: INamedExtension = {
|
||||
myExtension: {
|
||||
is_builtin: false,
|
||||
is_internal: true,
|
||||
is_available: true,
|
||||
is_desktop: false,
|
||||
title: 'Моё расширение',
|
||||
description: 'Описание функциональности.',
|
||||
image: 'https://example.com/image.png',
|
||||
class: MyExtensionPluginModule,
|
||||
schema: MyExtensionSchema,
|
||||
tags: ['тег1', 'тег2'],
|
||||
readme: getReadmeContent('./myExtension'),
|
||||
instructions: getInstructionsContent('./myExtension'),
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Дефолтные настройки
|
||||
Добавьте расширение в список дефолтных приложений в `extension-domain.service.ts`:
|
||||
|
||||
```typescript
|
||||
getDefaultApps(): Partial<ExtensionDomainEntity>[] {
|
||||
return [
|
||||
// ...
|
||||
{
|
||||
name: 'myExtension',
|
||||
enabled: true,
|
||||
config: {
|
||||
// Дефолтные значения конфигурации
|
||||
parameter1: 'value1',
|
||||
parameter2: 42,
|
||||
},
|
||||
},
|
||||
// ...
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
description: Архитектура и правила расширений extensions в controller
|
||||
alwaysApply: false
|
||||
---
|
||||
# Архитектура расширений в MonoCoop
|
||||
|
||||
## Основные принципы
|
||||
1. Расширения построены на модулях NestJS с использованием шаблона "Порты и адаптеры"
|
||||
2. Каждое расширение наследуется от `BaseExtModule` и реализует интерфейс `OnModuleInit`
|
||||
3. Расширения могут взаимодействовать с блокчейном через соответствующие порты
|
||||
4. Конфигурации расширений хранятся в БД и описываются с помощью Zod-схем
|
||||
5. Расширения регистрируются в глобальном реестре `AppRegistry`
|
||||
|
||||
## Структура расширения
|
||||
Минимальная структура расширения включает:
|
||||
- `XXX-extension.module.ts` - основной модуль расширения
|
||||
- `package.json` - информация о пакете
|
||||
- `README.md` - документация
|
||||
- `INSTALL.md` - инструкции по установке
|
||||
- `CHANGELOG.md` - история изменений
|
||||
|
||||
## Создание нового расширения
|
||||
1. Создайте директорию для расширения в `components/controller/src/extensions/`
|
||||
2. Создайте основной класс расширения, наследующийся от `BaseExtModule`
|
||||
3. Определите Zod-схему для конфигурации
|
||||
4. Реализуйте метод `initialize()`
|
||||
5. Зарегистрируйте расширение в `extensions.registry.ts`
|
||||
6. Добавьте расширение в список дефолтных приложений в `extension-domain.service.ts`
|
||||
|
||||
## Пример структуры модуля расширения
|
||||
```typescript
|
||||
// XXX-extension.module.ts
|
||||
export class XXXPlugin extends BaseExtModule {
|
||||
constructor(...) {
|
||||
super();
|
||||
}
|
||||
|
||||
name = 'xxx';
|
||||
plugin!: ExtensionDomainEntity<IConfig>;
|
||||
public configSchemas = Schema;
|
||||
|
||||
async initialize() {
|
||||
// Инициализация расширения
|
||||
// Настройка cron-задач
|
||||
}
|
||||
|
||||
// Дополнительные методы расширения
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [XXXPlugin],
|
||||
})
|
||||
export class XXXPluginModule {
|
||||
constructor(private readonly xxxPlugin: XXXPlugin) {}
|
||||
|
||||
async initialize() {
|
||||
await this.xxxPlugin.initialize();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Взаимодействие с блокчейном
|
||||
Для взаимодействия с блокчейном:
|
||||
1. Определите порт в доменном слое (например, `SovietBlockchainPort`)
|
||||
2. Инжектируйте порт в конструкторе расширения через DI
|
||||
3. Используйте методы порта для взаимодействия с блокчейном
|
||||
|
||||
```typescript
|
||||
@Inject(SOVIET_BLOCKCHAIN_PORT) private readonly sovietBlockchainPort: SovietBlockchainPort
|
||||
// ...
|
||||
const decisions = await this.sovietBlockchainPort.getDecisions(coopname);
|
||||
```
|
||||
|
||||
## Настройка планировщика задач
|
||||
Расширения могут использовать cron-задачи для периодического выполнения операций:
|
||||
|
||||
```typescript
|
||||
import cron from 'node-cron';
|
||||
|
||||
// Регистрация cron-задачи (каждые N минут)
|
||||
const cronExpression = `*/${this.plugin.config.checkInterval} * * * *`;
|
||||
cron.schedule(cronExpression, () => {
|
||||
this.logger.info('Запуск запланированной задачи');
|
||||
this.runTask();
|
||||
});
|
||||
```
|
||||
|
||||
## Работа с конфигурацией
|
||||
1. Определите Zod-схему для конфигурации
|
||||
2. Используйте `describeField` для добавления UI-метаданных к полям
|
||||
3. Получайте и обновляйте конфигурацию через репозиторий `extensionRepository`
|
||||
|
||||
```typescript
|
||||
export const Schema = z.object({
|
||||
checkInterval: z.number().describe(
|
||||
describeField({
|
||||
label: 'Интервал проверки (в минутах)',
|
||||
note: 'Минимум: 5 минут',
|
||||
rules: ['val >= 5'],
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
// Обновление конфигурации
|
||||
this.plugin.config.lastCheckDate = new Date().toISOString();
|
||||
await this.extensionRepository.update(this.plugin);
|
||||
```
|
||||
|
||||
## Логирование действий
|
||||
Расширения должны логировать свои действия:
|
||||
1. Используйте `WinstonLoggerService` для системного логирования
|
||||
2. Используйте `LogExtensionDomainRepository` для хранения логов в БД
|
||||
|
||||
```typescript
|
||||
// Системное логирование
|
||||
this.logger.info(`Выполнение операции для ${id}`);
|
||||
|
||||
// Сохранение лога в БД
|
||||
await this.logExtensionRepository.push(this.name, {
|
||||
type: 'operation',
|
||||
timestamp: new Date().toISOString(),
|
||||
data: { ... },
|
||||
});
|
||||
```
|
||||
|
||||
## Регистрация расширения
|
||||
После создания расширения, добавьте его в `extensions.registry.ts`:
|
||||
|
||||
```typescript
|
||||
export const AppRegistry: INamedExtension = {
|
||||
myExtension: {
|
||||
is_builtin: false,
|
||||
is_internal: true,
|
||||
is_available: true,
|
||||
is_desktop: false,
|
||||
title: 'Моё расширение',
|
||||
description: 'Описание функциональности.',
|
||||
image: 'https://example.com/image.png',
|
||||
class: MyExtensionPluginModule,
|
||||
schema: MyExtensionSchema,
|
||||
tags: ['тег1', 'тег2'],
|
||||
readme: getReadmeContent('./myExtension'),
|
||||
instructions: getInstructionsContent('./myExtension'),
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Дефолтные настройки
|
||||
Добавьте расширение в список дефолтных приложений в `extension-domain.service.ts`:
|
||||
|
||||
```typescript
|
||||
getDefaultApps(): Partial<ExtensionDomainEntity>[] {
|
||||
return [
|
||||
// ...
|
||||
{
|
||||
name: 'myExtension',
|
||||
enabled: true,
|
||||
config: {
|
||||
// Дефолтные значения конфигурации
|
||||
parameter1: 'value1',
|
||||
parameter2: 42,
|
||||
},
|
||||
},
|
||||
// ...
|
||||
];
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
description: Добавить документ в фабрику документов. Создать шаблон документа.
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
## Создание шаблона
|
||||
|
||||
В библиотеке реестра типов документов находятся шаблоны: cooptypes/src/cooperative/registry/1.WalletAgreement
|
||||
|
||||
В каждом шаблоне определяется часть Action, которая отвечает за передаваемые извне параметры для модели документа. И Model, которая отвечает за общую модель документа, которая может включать в себя не только данные, которые передаются извне в действии по генерации документа, но и извлекаются самой фабрикой в процессе генерации. Meta данные при этом являются объединением обоих интерфейсов и на основе этих данных можно произвести полную регенерацию документа.
|
||||
|
||||
Содержание каждого index файла включает в себя интерфейс Action для генерации. Model с теми данными которые могут быть предоставлены самой фабрикой документов. Title, description, context и translations и exampleData.
|
||||
|
||||
Context включает в себя строковый шаблон jinja2, который использует данные из Action и Meta, а также translations для переводов на русский язык. Кроме того, каждый шаблон содержит стандартную css разметку для заголовка, места подписи, и т.д.
|
||||
|
||||
exampleData мы прописываем как пример данных, которые программа-визуализатор может подставить и показать визуально-заполненный шаблон. Потому когда ты создаешь новый документ ты должен в точности соблюдать заполнение всех полей. Во-первых ты должен декомпозировать документ на элементы переводов, затем эти элементы переводов вставить в текст и разбавить использованием переменных. Местами ты можешь использовать вот такие конструкции шаблона для передачи переменных прямо в перевод: {% trans 'phone_and_email_notice', individual.phone, individual.email %}
|
||||
|
||||
В объекте перевода:
|
||||
...phone_and_email_notice: 'номер телефона с активированной функцией получения sms: {0}, адрес электронной почты: {1}.',
|
||||
|
||||
Но обычно достаточно просто использовать переменные из Action и Model напрямую.
|
||||
|
||||
## Создание схемы
|
||||
|
||||
После того, как шаблон создан и экспортирован, необходимо создать схему. Схема создается в factory/src/Templates
|
||||
|
||||
Там реэкспортируются Action, Model как типы. И определяется схема ajv. Там есть стандартные, определенные части схемы, которые находятся в factory/src/Schema. Все части моделей Action и Model должны быть определены в одной схеме данных и затем экспортированы в реестр схем factory/src/Templates/registry.ts.
|
||||
|
||||
## Создание станка
|
||||
|
||||
Далее необходимо создать станок для обработки генерации документа в factory/src/Actions. Он дожен включать в себя интерфейс действия и всю подготовительную обработку данных для передачи информации в шаблон согласно схемы. В качестве ФИО/наименование пайщика используем factory/src/Schema/CommonUserSchema.ts.
|
||||
|
||||
## Экспорт станка
|
||||
|
||||
Станок необходимо экспортировать в src/index.ts через все реестры. Чтобы в итоге вызов метода генерации был делегирован корректному станку.
|
||||
|
||||
|
||||
## Создание теста
|
||||
|
||||
В папке factory/src/tests создаем или используем существующий и подходящий по контексту файл теста для генерации. Используем метод для генерации
|
||||
|
||||
it('генерируем соглашение о капитализации', async () => {
|
||||
await testDocumentGeneration({
|
||||
registry_id: 1000,
|
||||
coopname: 'voskhod',
|
||||
username: 'ant',
|
||||
lang: 'ru',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
description: Создание шаблона документа без шапки из его основной версии в фабрике документов
|
||||
alwaysApply: false
|
||||
---
|
||||
Смотри, у меня есть проект документа оферты, @cooptypes/src/cooperative/registry/999.BlagorostOfferTemplate/index.ts мы его принимаем затем, чтобы получить данные для генерации @cooptypes/src/cooperative/registry/1000.BlagorostOffer/index.ts уже самой пользовательской оферты. Они в целом почти абсолютно идентичны за той лишь разницей, что в проекте кругом прочерки остаются вместо переменных для заполнения, и нет шапки с надписью "УТВЕРЖДЕНО" протоколом решения № ___ и дата ___. Т.е. мы как бы из первого в итоге получаем второе. Приняв первое - мы можем начать генерировать второе.
|
||||
|
||||
Мне надо, чтобы ты сделал аналогично для указанного мной документа. Для этого нужно переменные по аналогии заполнить пробелами и убрать шапку. Т.е. в итоге мы должны получить шаблон документа, который можно распечатать и заполнить ручкой. Вот такой вот документ мне надо чтоб ты создал. Он должен обладать принадлежностью к конкретному кооперативу, т.е. vars и chairman мы используем, как и coop. Всё остальное по сути нужно заменить на прочерки. Создай этот шаблон документа, исследовав что нужно тебе для этого.
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
description: Правила работы с фабрикой документов кооперативов (factory)
|
||||
alwaysApply: false
|
||||
---
|
||||
# Фабрика Документов Кооперативов
|
||||
|
||||
## Общая Архитектура
|
||||
|
||||
Фабрика документов — это система генерации PDF документов для кооперативов, построенная на TypeScript с использованием MongoDB для хранения данных. Система состоит из трех основных частей:
|
||||
|
||||
1. **registry/** — JSON-шаблоны документов (статические данные)
|
||||
2. **factory/** — основная фабрика с логикой генерации
|
||||
3. **cooptypes/** — типы данных и интерфейсы
|
||||
|
||||
## Структура Registry
|
||||
|
||||
В корневой папке `registry/` находятся JSON файлы с номерными названиями, представляющие шаблоны документов.
|
||||
|
||||
|
||||
### Структура JSON-шаблона:
|
||||
```json
|
||||
{
|
||||
"context": "<div>...HTML шаблон с переменными...</div>",
|
||||
"model": {...данные для примера...},
|
||||
"translation": {...переводы ключей...},
|
||||
"object_model": {...схема объектной модели...}
|
||||
}
|
||||
```
|
||||
|
||||
## Фабрика (factory/)
|
||||
|
||||
### Основные компоненты:
|
||||
|
||||
#### src/index.ts — Главный класс Generator
|
||||
```typescript
|
||||
export class Generator implements IGenerator {
|
||||
// Хранилище фабрик для каждого типа документа
|
||||
factories: { [K in Numbers]: DocFactory<IGenerate> }
|
||||
|
||||
// MongoDB коннектор
|
||||
public storage: MongoDBConnector
|
||||
|
||||
// Основной метод генерации
|
||||
async generate(data: IGenerate, options?: IGenerationOptions): Promise<IGeneratedDocument>
|
||||
}
|
||||
```
|
||||
|
||||
#### Архитектура Factory Pattern:
|
||||
- Базовый класс `DocFactory<T>` в `src/Factory/index.ts`
|
||||
- Каждый документ имеет свою фабрику в `src/Actions/`
|
||||
- Фабрики наследуются от `DocFactory` и реализуют метод `generateDocument()`
|
||||
|
||||
### Сервисы:
|
||||
|
||||
#### Services/Generator/ — PDF генерация
|
||||
- `PDFService` — конвертирует HTML в PDF через WeasyPrint
|
||||
- Использует шрифт Arial (base64)
|
||||
- Добавляет метаданные в PDF
|
||||
- Вычисляет SHA-256 хеш документа
|
||||
|
||||
#### Services/Templator/ — Шаблонизация
|
||||
- Основан на Nunjucks
|
||||
- Поддерживает кастомное расширение `{% trans %}` для переводов
|
||||
- Рендерит HTML из шаблона с подстановкой переменных
|
||||
|
||||
#### Services/Validator/ — Валидация
|
||||
- Использует AJV для JSON Schema валидации
|
||||
- Поддерживает кастомные форматы (телефон)
|
||||
- Локализация ошибок на русском языке
|
||||
|
||||
#### Services/Databazor/ — База данных
|
||||
- `MongoDBConnector` — работа с MongoDB
|
||||
- `DataService` — абстракция над данными
|
||||
- Коллекции: `deltas`, `actions`, `documents`, и другие
|
||||
|
||||
### Модели данных (src/Models/):
|
||||
|
||||
#### Основные типы пользователей:
|
||||
- `Individual` — физические лица (ФИО, паспорт, адрес)
|
||||
- `Organization` — организации (ИНН, ОГРН, представитель)
|
||||
- `Entrepreneur` — ИП (ФИО + ИНН/ОГРН)
|
||||
|
||||
#### Кооперативные данные:
|
||||
- `Cooperative` — данные кооператива
|
||||
- `PaymentMethod` — платежные методы
|
||||
- `Vars` — переменные кооператива
|
||||
- `Project` — проекты
|
||||
|
||||
### Система Action-ов:
|
||||
|
||||
Каждый документ имеет Action класс в `src/Actions/` с методом `generateDocument()`:
|
||||
|
||||
1. **Получение шаблона** — из локального Registry или MongoDB
|
||||
2. **Сбор данных** — пользователь, кооператив, переменные, специфичные данные
|
||||
3. **Валидация** — проверка по JSON схеме
|
||||
4. **Рендеринг** — HTML из шаблона + данные
|
||||
5. **PDF генерация** — HTML → PDF с метаданными
|
||||
6. **Сохранение** — в MongoDB (если не skip_save)
|
||||
|
||||
## Система типов (cooptypes/)
|
||||
|
||||
### cooperative/registry/ — Типы документов
|
||||
Каждый документ имеет папку с интерфейсами:
|
||||
- `Action` — входные данные для генерации
|
||||
- `Model` — модель данных для шаблона
|
||||
- `Template` — структура шаблона
|
||||
|
||||
### contracts/ — Блокчейн контракты
|
||||
- `registrator/` — регистрация кооперативов
|
||||
- `soviet/` — управление советом
|
||||
- `meet/` — общие собрания
|
||||
- `wallet/`, `capital/`, `fund/` — финансовые операции
|
||||
|
||||
## Особенности реализации
|
||||
|
||||
### Шаблонизация:
|
||||
- HTML шаблоны с CSS стилями
|
||||
- Переменные в формате `{{variable.field}}`
|
||||
- Условная логика `{% if condition %}`
|
||||
- Циклы `{% for item in array %}`
|
||||
- Переводы `{% trans 'KEY', var1, var2 %}`
|
||||
|
||||
### Подписи:
|
||||
- Цифровые подписи вместо физических
|
||||
- Текст "Подписано электронной подписью"
|
||||
- Убраны подчеркивания для подписей
|
||||
|
||||
### Типы собраний:
|
||||
- `regular` — очередное
|
||||
- `extraordinary` — внеочередное
|
||||
- Условная логика в шаблонах
|
||||
|
||||
### Филиалы:
|
||||
- `coop.is_branched` — проверка на наличие филиалов
|
||||
- "пайщиков" vs "уполномоченных" в зависимости от типа
|
||||
|
||||
### Форматирование дат:
|
||||
- Формат: "г. Москва, 15 декабря 2024 г."
|
||||
- Без кавычек вокруг дат
|
||||
- Запятая после города
|
||||
|
||||
## Тестирование
|
||||
|
||||
### test/utils/index.ts — Тестовые утилиты:
|
||||
- `preLoading()` — инициализация тестовых данных
|
||||
- Создание кооператива, пользователей, платежных методов
|
||||
- Настройка данных собраний и решений
|
||||
- Очистка временных файлов
|
||||
|
||||
### Тестовые данные:
|
||||
- Кооператив "ВОСХОД"
|
||||
- Пользователи: ant, individual, entrepreneur
|
||||
- Организации: voskhod, branch, exampleorg
|
||||
- Собрания с вопросами и решениями
|
||||
|
||||
Фабрика поддерживает полный цикл создания документов кооператива от заявлений до протоколов собраний с возможностью кастомизации под разные типы кооперативов и требования.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
description: Логирование и логгер на бэкенде (controller)
|
||||
alwaysApply: false
|
||||
---
|
||||
# Logger Rule
|
||||
|
||||
## Правильное использование логгера:
|
||||
|
||||
1. **Импорт**: `import { WinstonLoggerService } from '~/application/logger/logger-app.service';`
|
||||
|
||||
2. **Инъекция**: Добавить в конструктор как `private readonly logger: WinstonLoggerService`
|
||||
|
||||
3. **Контекст**: В конструкторе `this.logger.setContext(ClassName.name);`
|
||||
|
||||
4. **Использование**: `this.logger.debug/info/error('message', meta?)`
|
||||
|
||||
5. **НЕ использовать**: `new Logger()` из `@nestjs/common`
|
||||
# Logger Rule
|
||||
|
||||
## Правильное использование логгера:
|
||||
|
||||
1. **Импорт**: `import { WinstonLoggerService } from '~/application/logger/logger-app.service';`
|
||||
|
||||
2. **Инъекция**: Добавить в конструктор как `private readonly logger: WinstonLoggerService`
|
||||
|
||||
3. **Контекст**: В конструкторе `this.logger.setContext(ClassName.name);`
|
||||
|
||||
4. **Использование**: `this.logger.debug/info/error('message', meta?)`
|
||||
|
||||
5. **НЕ использовать**: `new Logger()` из `@nestjs/common`
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
description: Как добавлять селекторы, мутации и запросы в SDK
|
||||
globs: **/sdk/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# Руководство по работе с GraphQL Zeus в SDK
|
||||
|
||||
## Основной процесс
|
||||
|
||||
1. **Анализ DTO бэкенда**:
|
||||
- Изучите структуру DTO классов (`@ObjectType`) в бэкенде
|
||||
- Обратите внимание на имена типов в декораторах `@ObjectType('ИмяТипа')`
|
||||
- Отметьте поля, связи и вложенные объекты
|
||||
|
||||
2. **Создание селекторов**:
|
||||
- Для каждого DTO создайте соответствующий селектор с именем `raw<ИмяТипа>Selector`
|
||||
- Селектор - это объект, где ключи соответствуют полям DTO, а значения - `true`
|
||||
- Для вложенных объектов используйте вложенные селекторы: `field: nestedSelector`
|
||||
- Для списков используйте один селектор без массива: `items: itemSelector`
|
||||
|
||||
3. **Валидация селекторов**:
|
||||
- Для каждого селектора создайте проверку типа:
|
||||
```typescript
|
||||
const _validate: MakeAllFieldsRequired<ValueTypes['ТочноеИмяГрафКьЭлТипа']> = rawSelector
|
||||
```
|
||||
- Имя типа должно точно совпадать с именем в декораторе `@ObjectType`
|
||||
|
||||
4. **Экспорт селекторов**:
|
||||
- Создайте финальный селектор с помощью функции `Selector`:
|
||||
```typescript
|
||||
export const typeSelector = Selector('ТочноеИмяГрафКьЭлТипа')(rawTypeSelector)
|
||||
```
|
||||
- Экспортируйте сырой селектор для переиспользования
|
||||
- Экспортируйте тип модели: `export type modelType = ModelTypes['ТочноеИмяГрафКьЭлТипа']`
|
||||
|
||||
5. **Создание запросов/мутаций**:
|
||||
- Используйте селекторы в запросах и мутациях:
|
||||
```typescript
|
||||
export const query = Selector('Query')({
|
||||
queryName: [{ data: $('data', 'ТочноеИмяВходногоТипа!') }, exportedSelector]
|
||||
})
|
||||
```
|
||||
- Для параметров используйте оператор `$` с точным именем входного типа
|
||||
- Создайте интерфейс входных данных:
|
||||
```typescript
|
||||
export interface IInput {
|
||||
data: ModelTypes['ТочноеИмяВходногоТипа']
|
||||
}
|
||||
```
|
||||
|
||||
## Особенности работы
|
||||
|
||||
- **Документы**: всегда сохраняйте структуру `{ hash, signatures, rawDocument }`
|
||||
- **Сложные DTO**: разбивайте на атомарные селекторы и комбинируйте их
|
||||
- **Типы в Zeus**: часто отличаются от имен классов в бэкенде, всегда проверяйте в `schema.gql`
|
||||
- **Массивы**: Zeus автоматически обрабатывает массивы, не используйте `[selector]`
|
||||
|
||||
Это руководство поможет правильно структурировать работу с SDK и избежать типичных ошибок при работе с Zeus.
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
description: Правила работы с одинарными подписями документов
|
||||
alwaysApply: false
|
||||
---
|
||||
# Правила работы с одинарными подписями документов
|
||||
|
||||
## Общая концепция
|
||||
|
||||
Одинарная подпись означает, что документ подписывается одним участником процесса. Подпись имеет идентификатор `signatureId = 1` (по умолчанию).
|
||||
|
||||
## Типы документов
|
||||
|
||||
### ZGeneratedDocument (из cooptypes)
|
||||
Базовый документ для подписи:
|
||||
- `full_title`: полное название
|
||||
- `html`: HTML содержимое
|
||||
- `hash`: хеш документа
|
||||
- `meta`: метаданные
|
||||
- `binary`: бинарные данные (Uint8Array или string)
|
||||
|
||||
### SignedDigitalDocument
|
||||
Подписанный документ, содержащий:
|
||||
- `doc_hash`: хеш содержимого
|
||||
- `hash`: общий хеш
|
||||
- `meta`: метаданные документа
|
||||
- `meta_hash`: хеш метаданных
|
||||
- `signatures`: массив подписей
|
||||
- `version`: версия стандарта
|
||||
|
||||
## Способ подписания документа
|
||||
|
||||
### Использование useSignDocument
|
||||
|
||||
```typescript
|
||||
const { signDocument } = useSignDocument();
|
||||
|
||||
const signedDocument = await signDocument(
|
||||
document, // ZGeneratedDocument - базовый документ
|
||||
account, // string - имя пользователя, подписывающего документ
|
||||
1, // signatureId - идентификатор подписи (1 для первой/единственной)
|
||||
);
|
||||
```
|
||||
|
||||
## Параметры для подписания
|
||||
|
||||
- **document**: `ZGeneratedDocument` - базовый документ, полученный после генерации
|
||||
- **account**: `string` - имя аккаунта подписанта (обычно `useSessionStore().username`)
|
||||
- **signatureId**: `number` - идентификатор подписи (для одинарной подписи всегда `1`)
|
||||
|
||||
## Примеры реализации
|
||||
|
||||
### Пример 1: Голосование на собрании
|
||||
|
||||
```typescript
|
||||
export const useVoteOnMeet = () => {
|
||||
const { signDocument } = useSignDocument();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const vote = async (input: IVoteOnMeetInput): Promise<void> => {
|
||||
// Генерируем бюллетень для голосования
|
||||
const generatedBallot = await generateBallot({
|
||||
coopname: input.coopname,
|
||||
username: sessionStore.username,
|
||||
meet_hash: input.meetHash,
|
||||
answers: input.answers,
|
||||
});
|
||||
|
||||
// Подписываем бюллетень одинарной подписью
|
||||
const signedBallot = await signDocument(
|
||||
generatedBallot, // ZGeneratedDocument
|
||||
sessionStore.username, // string
|
||||
1, // signatureId = 1
|
||||
);
|
||||
|
||||
// Отправляем подписанный бюллетень
|
||||
await api.voteOnMeet({
|
||||
...input,
|
||||
ballot: signedBallot,
|
||||
});
|
||||
};
|
||||
|
||||
return { vote };
|
||||
};
|
||||
```
|
||||
|
||||
### Пример 2: Подписание соглашения
|
||||
|
||||
```typescript
|
||||
export const useSignAgreement = () => {
|
||||
const { signDocument } = useSignDocument();
|
||||
const { info } = useSystemStore();
|
||||
const { username } = useSessionStore();
|
||||
|
||||
const signAgreement = async (agreement_type: string, agreement: ZGeneratedDocument) => {
|
||||
// Подписываем документ одинарной подписью
|
||||
const signedDocument = await signDocument(
|
||||
agreement, // ZGeneratedDocument
|
||||
username, // string
|
||||
1, // signatureId = 1
|
||||
);
|
||||
|
||||
// Отправляем подписанное соглашение
|
||||
await api.sendAgreement({
|
||||
coopname: info.coopname,
|
||||
administrator: info.coopname,
|
||||
username,
|
||||
agreement_type,
|
||||
document: {...signedDocument, meta: JSON.stringify(signedDocument.meta)}
|
||||
});
|
||||
};
|
||||
|
||||
return { signAgreement };
|
||||
};
|
||||
```
|
||||
|
||||
## Важные замечания
|
||||
|
||||
1. **Генерация документа обязательна** - перед подписью документ должен быть сгенерирован через фабрику документов
|
||||
2. **signatureId = 1** - для одинарной подписи всегда используется идентификатор `1`
|
||||
3. **Session Store** - использовать `useSessionStore().username` для получения имени подписанта
|
||||
4. **Ключ подписи** - должен быть установлен в Global Store (`wif` ключ)
|
||||
|
||||
## Распространенные ошибки
|
||||
|
||||
- Попытка подписать документ без предварительной генерации
|
||||
- Использование неправильного `signatureId` (не 1 для одинарной подписи)
|
||||
- Отсутствие приватного ключа в Global Store
|
||||
- Попытка подписать уже подписанный документ (для одинарной подписи использовать только свежесгенерированные документы)
|
||||
@@ -0,0 +1,12 @@
|
||||
project_name: "Project Filters"
|
||||
project_type: "library"
|
||||
project_level: 1
|
||||
output_folder: "docs"
|
||||
language: "ru"
|
||||
bmm:
|
||||
workflow_status_file: "docs/bmm-workflow-status.yaml"
|
||||
sprint_status_file: "docs/sprint-status.yaml"
|
||||
paths:
|
||||
docs: "docs"
|
||||
stories: "docs/stories"
|
||||
tests: "tests"
|
||||
@@ -0,0 +1,37 @@
|
||||
version: "6.0"
|
||||
project_name: "Project Filters"
|
||||
project_type: "library"
|
||||
project_level: 1
|
||||
language: "ru"
|
||||
initialized_at: "2026-02-04T12:00:00.000Z"
|
||||
last_updated: "2026-02-04T12:30:00.000Z"
|
||||
|
||||
workflow_phases:
|
||||
phase1_product_brief:
|
||||
status: "completed"
|
||||
required: false
|
||||
description: "Краткое описание продукта"
|
||||
completed_at: "2026-02-04T12:00:00.000Z"
|
||||
output_file: "docs/product-brief-project-filters-2026-02-04.md"
|
||||
|
||||
phase2_requirements:
|
||||
status: "pending"
|
||||
required: true
|
||||
description: "Технические требования"
|
||||
completed_at: null
|
||||
|
||||
phase3_architecture:
|
||||
status: "pending"
|
||||
required: false
|
||||
description: "Архитектурное решение"
|
||||
completed_at: null
|
||||
|
||||
phase4_implementation:
|
||||
status: "completed"
|
||||
required: true
|
||||
description: "Реализация историй"
|
||||
completed_at: "2026-02-04T12:00:00.000Z"
|
||||
output_file: "Реализован диалог фильтров с кнопкой в интерфейсе"
|
||||
|
||||
current_phase: "phase1_product_brief"
|
||||
next_recommended_action: "Создать краткое описание продукта (product brief)"
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
# Product Brief: Project Filters
|
||||
**Дата:** 2026-02-04
|
||||
**Проект:** Project Filters
|
||||
**Тип:** Библиотека / Фреймворк
|
||||
**Уровень:** 1 (Маленький проект)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Мы создаем систему диалоговой фильтрации проектов для кооперативной платформы, которая позволит пользователям эффективно фильтровать и находить нужные проекты и задачи. Система предназначена для участников кооперативов - разработчиков, менеджеров проектов и администраторов, которые ежедневно работают с большим количеством проектов и задач. Это важно, потому что без эффективной фильтрации пользователи тратят много времени на поиск нужной информации, что снижает продуктивность работы.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
В текущей системе проекты отображаются в виде длинного списка без возможности гибкой фильтрации. Пользователи не могут быстро найти проекты по статусу задач, приоритетам, ответственным лицам или мастерам. Это приводит к тому, что:
|
||||
|
||||
- Участники тратят много времени на ручной поиск нужных проектов
|
||||
- Сложно отслеживать проекты с определенными статусами (например, только завершенные или в работе)
|
||||
- Нет возможности фильтровать по ответственным лицам (создателям и мастерам проектов)
|
||||
- Отсутствует возможность комбинировать фильтры для точного поиска
|
||||
|
||||
## Why Now
|
||||
|
||||
С ростом количества проектов и участников в кооперативе становится критически важно иметь эффективные инструменты поиска и фильтрации. Текущая система уже достигла предела usability - пользователи жалуются на сложности с навигацией. Реализация фильтров сейчас позволит заложить фундамент для масштабирования платформы и улучшения пользовательского опыта.
|
||||
|
||||
## Solution Overview
|
||||
|
||||
Мы создадим диалоговое окно фильтров, которое заменит текущие горизонтальные кнопки фильтрации. Диалог будет содержать четыре типа фильтров:
|
||||
|
||||
1. **Фильтр по статусам задач** - множественный выбор из: Все статусы, Бэклог, К выполнению, В работе, На проверке, Выполнено, Отменено
|
||||
2. **Фильтр по приоритетам** - множественный выбор из: Все приоритеты, Срочный, Высокий, Средний, Низкий
|
||||
3. **Фильтр по исполнителю** - выбор конкретного участника или опция "Я"
|
||||
4. **Фильтр по мастеру** - выбор конкретного мастера проекта или опция "Я"
|
||||
|
||||
Диалог будет открываться по нажатию кнопки "Фильтры" в шапке страницы проектов.
|
||||
|
||||
## Target Audience
|
||||
|
||||
### Primary Users
|
||||
- **Разработчики** - ежедневно работают с задачами, нуждаются в фильтрации по статусам и приоритетам
|
||||
- **Менеджеры проектов** - отслеживают прогресс проектов, фильтруют по ответственным лицам
|
||||
- **Администраторы кооператива** - мониторят все проекты, используют комплексную фильтрацию
|
||||
|
||||
### Secondary Users
|
||||
- **Новые участники** - изучают структуру проектов через фильтры
|
||||
- **Внешние консультанты** - временно работают с проектами кооператива
|
||||
|
||||
Основные потребности:
|
||||
1. Быстрый поиск проектов по статусу задач
|
||||
2. Фильтрация по приоритетам для prioritization
|
||||
3. Поиск проектов по ответственным лицам
|
||||
4. Комбинирование фильтров для точного поиска
|
||||
|
||||
## Business Objectives
|
||||
|
||||
**Основные цели:**
|
||||
- Сократить время поиска проектов на 60%
|
||||
- Увеличить удовлетворенность пользователей работой с платформой
|
||||
- Улучшить прозрачность работы кооператива через эффективную навигацию
|
||||
|
||||
**Метрики успеха:**
|
||||
- Время на поиск конкретного проекта: < 30 секунд
|
||||
- Количество жалоб на навигацию: уменьшить на 80%
|
||||
- Уровень использования фильтров: > 70% активных пользователей
|
||||
|
||||
**Бизнес-ценность:**
|
||||
- Повышение продуктивности участников кооператива
|
||||
- Снижение операционных издержек на поддержку пользователей
|
||||
- Улучшение retention участников платформы
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
- Диалоговое окно с четырьмя типами фильтров
|
||||
- Интеграция с существующей системой фильтрации
|
||||
- Кнопка "Фильтры" в шапке страницы проектов
|
||||
- Сохранение состояния фильтров
|
||||
- Валидация и обработка ошибок
|
||||
|
||||
### Out of Scope
|
||||
- Расширенные фильтры (по дате создания, бюджету и т.д.)
|
||||
- Сохранение персональных настроек фильтров
|
||||
- Экспорт отфильтрованных данных
|
||||
- Интеграция с мобильной версией
|
||||
|
||||
### Future Considerations
|
||||
- Добавление фильтров по датам
|
||||
- Персональные шаблоны фильтров
|
||||
- Интеграция с системой уведомлений
|
||||
|
||||
## Stakeholders
|
||||
|
||||
- **Продуктовый менеджер** - определение требований и приоритетов
|
||||
- **UX/UI дизайнер** - проектирование интерфейса диалога
|
||||
- **Frontend разработчики** - реализация компонентов
|
||||
- **Backend разработчики** - поддержка API фильтрации
|
||||
- **QA инженеры** - тестирование функциональности
|
||||
- **Пользователи кооператива** - валидация решения
|
||||
|
||||
## Constraints and Assumptions
|
||||
|
||||
### Constraints
|
||||
- Должны использовать существующую архитектуру FSD
|
||||
- Интеграция только с текущими компонентами фильтрации
|
||||
- Ограничения по времени: 2 недели на реализацию
|
||||
|
||||
### Assumptions
|
||||
- Пользователи знакомы с базовыми принципами фильтрации
|
||||
- Backend API поддерживает все необходимые параметры фильтрации
|
||||
- Текущая система компонентов (ContributorSelector, etc.) стабильна
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Диалог открывается и закрывается корректно
|
||||
- Все четыре типа фильтров работают правильно
|
||||
- Фильтры применяются мгновенно без перезагрузки страницы
|
||||
- Кнопка "Фильтры" интегрирована в шапку без конфликтов
|
||||
- Код соответствует архитектуре FSD и проходит все тесты
|
||||
|
||||
## Timeline
|
||||
|
||||
**Целевая дата запуска:** Через 2 недели после начала разработки
|
||||
|
||||
**Ключевые этапы:**
|
||||
1. Проектирование интерфейса (3 дня)
|
||||
2. Реализация компонентов (5 дней)
|
||||
3. Интеграция и тестирование (4 дня)
|
||||
4. Финальное тестирование и развертывание (2 дня)
|
||||
|
||||
## Risks
|
||||
|
||||
- **Технический риск:** Конфликты с существующими компонентами фильтрации
|
||||
- **Вероятность:** Средняя
|
||||
- **МитIGATION:** Тщательное тестирование интеграции
|
||||
|
||||
- **UX риск:** Пользователи не поймут новый интерфейс
|
||||
- **Вероятность:** Низкая
|
||||
- **МитIGATION:** Соблюдение существующих паттернов интерфейса
|
||||
|
||||
- **Производительность:** Диалог замедлит загрузку страницы
|
||||
- **Вероятность:** Низкая
|
||||
- **МитIGATION:** Ленивая загрузка компонентов
|
||||
@@ -0,0 +1,12 @@
|
||||
project_name: "Виджет участников проекта"
|
||||
project_type: "web-app"
|
||||
project_level: 1
|
||||
output_folder: "docs"
|
||||
language: "ru"
|
||||
bmm:
|
||||
workflow_status_file: "docs/bmm-workflow-status.yaml"
|
||||
sprint_status_file: "docs/sprint-status.yaml"
|
||||
paths:
|
||||
docs: "docs"
|
||||
stories: "docs/stories"
|
||||
tests: "tests"
|
||||
@@ -0,0 +1,42 @@
|
||||
timestamp: "2026-02-04T14:30:00.000Z"
|
||||
project_name: "Виджет участников проекта"
|
||||
project_type: "web-app"
|
||||
project_level: 1
|
||||
language: "ru"
|
||||
|
||||
workflow_phases:
|
||||
phase_1_product_brief:
|
||||
status: "completed"
|
||||
required: "recommended"
|
||||
completed: true
|
||||
artifacts:
|
||||
- "docs/product-brief-виджет-участников-проекта-2026-02-04.md"
|
||||
|
||||
phase_2_requirements:
|
||||
status: "completed"
|
||||
required: "required"
|
||||
completed: true
|
||||
artifacts:
|
||||
- "docs/tech-spec-виджет-участников-проекта-2026-02-04.md"
|
||||
|
||||
phase_3_architecture:
|
||||
status: "pending"
|
||||
required: "optional"
|
||||
completed: false
|
||||
artifacts: []
|
||||
|
||||
phase_4_implementation:
|
||||
status: "in_progress"
|
||||
required: "required"
|
||||
completed: false
|
||||
artifacts:
|
||||
- "docs/sprint-plan-виджет-участников-проекта-2026-02-04.md"
|
||||
- "docs/sprint-status.yaml"
|
||||
|
||||
current_phase: "phase_4_implementation"
|
||||
next_recommended_action: "Начать реализацию Sprint 1: STORY-001, STORY-002, STORY-003"
|
||||
|
||||
metadata:
|
||||
created_at: "2026-02-04T12:00:00.000Z"
|
||||
updated_at: "2026-02-04T15:00:00.000Z"
|
||||
version: "6.1"
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
# Product Brief: Виджет участников проекта
|
||||
|
||||
**Date:** 2026-02-04
|
||||
**Author:** AI Assistant
|
||||
**Version:** 1.1
|
||||
**Project Type:** web-app
|
||||
**Project Level:** 1
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Виджет для отображения информации об участниках проекта в графической форме. Показывает количество вкладов участников и информацию о том, как стать участником проекта. Это важно для понимания степени участия пользователей в проектах и их вклада в интеллектуальную собственность.
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
### The Problem
|
||||
|
||||
Пользователи не имеют полной информации о своем участии в проекте. Существует две критические проблемы:
|
||||
|
||||
1. **Отсутствие информации о вкладах**: После регистрации в проекте участник получает долю авторского права, но эта информация не отображается в интерфейсе. Виджет показывает только текущую информацию об участии, но не исторические данные о вкладах.
|
||||
|
||||
2. **Неправильная логика отображения участников**: Участники отображаются в виджете только после того, как сделали конкретный вклад в проект. Однако участники должны отображаться сразу после подачи заявления на участие и получения доступа к проекту. Это приводит к тому, что новые участники не видят себя в списке участников до тех пор, пока не внесут вклад.
|
||||
|
||||
### Why Now?
|
||||
|
||||
С ростом числа проектов и участников становится критически важно показывать прозрачную информацию об участии. Без этого пользователи не понимают ценность своего вклада и не могут оценить свою роль в проекте.
|
||||
|
||||
### Impact if Unsolved
|
||||
|
||||
Участники не будут понимать ценность своего участия, что приведет к снижению мотивации и вовлеченности. Новые участники не видят себя в списке участников проекта, что создает путаницу и неуверенность. Проекты потеряют прозрачность, что может привести к конфликтам и недоверию среди участников. Увеличивается риск потери потенциальных контрибьюторов, которые не понимают свой статус в проекте.
|
||||
|
||||
---
|
||||
|
||||
## Target Audience
|
||||
|
||||
### Primary Users
|
||||
|
||||
Участники проектов (ранние участники), которые приходят в проекты для получения доли в результате интеллектуальной деятельности. Они активно участвуют в проектах и используют виджет для отслеживания своего вклада.
|
||||
|
||||
### Secondary Users
|
||||
|
||||
Администраторы проектов, которые могут использовать виджет для оценки вклада участников.
|
||||
|
||||
### User Needs
|
||||
|
||||
- Видеть себя в списке участников сразу после получения доступа к проекту
|
||||
- Видеть количество своих вкладов в проект
|
||||
- Понимать, как стать участником проекта
|
||||
- Иметь прозрачную информацию об участии в графической форме
|
||||
- Отслеживать свой прогресс и ценность вклада
|
||||
- Видеть свою роль в проекте после назначения
|
||||
|
||||
---
|
||||
|
||||
## Solution Overview
|
||||
|
||||
### Proposed Solution
|
||||
|
||||
Комплексное улучшение виджета участников проекта:
|
||||
|
||||
1. **Исправление логики отображения участников**: Участники должны отображаться сразу после получения доступа к проекту (после подачи заявления и подписания соглашения об участии), а не только после внесения вклада.
|
||||
|
||||
2. **Добавление информации о вкладах**: Графическое отображение количества вкладов участника и информации о процессе становления участником проекта.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Исправление логики отображения**: Участники отображаются сразу после получения доступа к проекту
|
||||
- Графическое отображение количества вкладов участника
|
||||
- Информация о том, как стать участником проекта
|
||||
- Отображение ролей участников после их назначения
|
||||
- Интеграция с существующей системой учета вкладов
|
||||
- Адаптивный дизайн для различных устройств
|
||||
|
||||
### Value Proposition
|
||||
|
||||
Виджет обеспечивает прозрачность и мотивацию участников, показывая их реальный вклад в проект. Это помогает участникам лучше понимать ценность своего участия и стимулирует дальнейшую активность.
|
||||
|
||||
---
|
||||
|
||||
## Business Objectives
|
||||
|
||||
### Goals
|
||||
|
||||
- Увеличить прозрачность информации об участии в проектах
|
||||
- Повысить мотивацию участников через демонстрацию их вклада
|
||||
- Улучшить пользовательский опыт работы с проектами
|
||||
- Снизить количество вопросов о статусе участия
|
||||
|
||||
### Success Metrics
|
||||
|
||||
- Процент участников, просматривающих информацию о вкладах
|
||||
- Уровень удовлетворенности пользователей виджетом
|
||||
- Количество активных участников проектов
|
||||
|
||||
### Business Value
|
||||
|
||||
Повышение вовлеченности участников приводит к более качественной разработке проектов и лучшему распределению интеллектуальной собственности.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Исправление логики отображения участников (отображение сразу после получения доступа)
|
||||
- Добавление поля с количеством вкладов в графической форме
|
||||
- Интеграция с API для получения данных о вкладах и статусе участников
|
||||
- Добавление секции "Как стать участником"
|
||||
- Отображение ролей участников
|
||||
- Тестирование новой функциональности
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Изменение существующей логики учета вкладов
|
||||
- Переработка дизайна всего виджета
|
||||
- Добавление новых типов участников
|
||||
- Интеграция с внешними системами учета
|
||||
|
||||
### Future Considerations
|
||||
|
||||
- Добавление детальной истории вкладов
|
||||
- Персонализация отображения информации
|
||||
- Интеграция с системой уведомлений
|
||||
|
||||
---
|
||||
|
||||
## Key Stakeholders
|
||||
|
||||
- **Разработчики проекта** - Высокий уровень влияния. Отвечают за реализацию функциональности
|
||||
- **Участники проектов** - Средний уровень влияния. Основные пользователи виджета
|
||||
- **Администраторы проектов** - Средний уровень влияния. Могут использовать виджет для оценки вклада
|
||||
|
||||
---
|
||||
|
||||
## Constraints and Assumptions
|
||||
|
||||
### Constraints
|
||||
|
||||
- Интеграция должна быть совместима с существующей архитектурой
|
||||
- Ограничения по времени на реализацию (короткий срок)
|
||||
- Необходимость поддерживать существующий дизайн-систем
|
||||
|
||||
### Assumptions
|
||||
|
||||
- API для получения данных о вкладах уже существует
|
||||
- Пользователи имеют базовый уровень технической подготовки
|
||||
- Существующая инфраструктура может обработать дополнительную нагрузку
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Участники отображаются в виджете сразу после получения доступа к проекту (подписания соглашения об участии)
|
||||
- Виджет корректно отображает количество вкладов участников
|
||||
- Информация о вкладах представлена в понятной графической форме
|
||||
- Пользователи могут легко найти информацию о том, как стать участником
|
||||
- Роли участников корректно отображаются после назначения
|
||||
- Новая функциональность не нарушает работу существующего виджета
|
||||
- Положительная обратная связь от пользователей
|
||||
|
||||
---
|
||||
|
||||
## Timeline and Milestones
|
||||
|
||||
### Target Launch
|
||||
|
||||
Запуск новой версии виджета в течение 2 недель после начала разработки.
|
||||
|
||||
### Key Milestones
|
||||
|
||||
- Завершение анализа требований: 1 день
|
||||
- Разработка UI компонентов: 3 дня
|
||||
- Интеграция с API: 2 дня
|
||||
- Тестирование: 2 дня
|
||||
- Деплой и мониторинг: 1 день
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigation
|
||||
|
||||
### Risks
|
||||
|
||||
- **Технические сложности интеграции с API**
|
||||
- **Likelihood:** Средний
|
||||
- **Mitigation:** Предварительное тестирование API и наличие плана отката
|
||||
|
||||
- **Несовместимость с существующим дизайном**
|
||||
- **Likelihood:** Низкий
|
||||
- **Mitigation:** Использование существующей дизайн-системы
|
||||
|
||||
- **Недостаточная производительность**
|
||||
- **Likelihood:** Низкий
|
||||
- **Mitigation:** Оптимизация запросов к API
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Create Tech Spec - `/tech-spec`
|
||||
2. Conduct user research (optional) - `/research`
|
||||
3. Create UX design (if UI-heavy) - `/create-ux-design`
|
||||
|
||||
---
|
||||
|
||||
**This document was created using BMAD Method v6 - Phase 1 (Analysis)**
|
||||
|
||||
*To continue: Run `/workflow-status` to see your progress and next recommended action.*
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
# Sprint Plan: Виджет участников проекта
|
||||
|
||||
**Date:** 2026-02-04
|
||||
**Scrum Master:** AI Assistant
|
||||
**Project Level:** 1
|
||||
**Total Stories:** 5
|
||||
**Total Points:** 12
|
||||
**Planned Sprints:** 2
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
План спринта для улучшения виджета участников проекта включает исправление логики отображения участников и добавление информации о вкладах. Проект разделен на 2 спринта по 1 неделе каждый с общим объемом 12 story points.
|
||||
|
||||
**Key Metrics:**
|
||||
- Total Stories: 5
|
||||
- Total Points: 12
|
||||
- Sprints: 2 (по 1 неделе)
|
||||
- Team Capacity: 8 points per sprint
|
||||
- Target Completion: 2026-02-18
|
||||
|
||||
---
|
||||
|
||||
## Story Inventory
|
||||
|
||||
### STORY-001: Исправить логику создания участников
|
||||
|
||||
**Priority:** Must Have
|
||||
|
||||
**User Story:**
|
||||
As a: Пользователь кооператива
|
||||
I want to: Видеть себя в списке участников сразу после получения доступа к проекту
|
||||
So that: Я понимаю свой статус участия без необходимости делать вклад
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Участник отображается в виджете сразу после подписания соглашения об участии
|
||||
- [ ] Не требуется делать вклад для отображения в списке участников
|
||||
- [ ] Логика создания Contributor изменена с "при первом вкладе" на "при подтверждении доступа"
|
||||
|
||||
**Technical Notes:**
|
||||
Изменить условие создания Contributor entity в backend. Добавить проверку статуса appendix вместо проверки наличия вкладов.
|
||||
|
||||
**Dependencies:**
|
||||
Нет
|
||||
|
||||
**Points:** 2
|
||||
|
||||
### STORY-002: Добавить расчет количества вкладов
|
||||
|
||||
**Priority:** Must Have
|
||||
|
||||
**User Story:**
|
||||
As a: Пользователь кооператива
|
||||
I want to: Видеть количество своих вкладов в проект
|
||||
So that: Я понимаю свой вклад в развитие проекта
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Для каждого участника рассчитывается общее количество вкладов
|
||||
- [ ] Данные агрегируются из таблицы segments эффективно
|
||||
- [ ] Расчет не вызывает N+1 запросов к базе данных
|
||||
- [ ] Поле contributed_total добавлено в Contributor entity
|
||||
|
||||
**Technical Notes:**
|
||||
Создать функцию агрегации данных из segments. Оптимизировать запросы с использованием индексов БД. Добавить поле в Contributor entity.
|
||||
|
||||
**Dependencies:**
|
||||
STORY-001 (нужна правильная логика создания участников)
|
||||
|
||||
**Points:** 3
|
||||
|
||||
### STORY-003: Обновить GraphQL селектор
|
||||
|
||||
**Priority:** Must Have
|
||||
|
||||
**User Story:**
|
||||
As a: Frontend разработчик
|
||||
I want to: Получать данные о количестве вкладов через GraphQL API
|
||||
So that: Могу отображать информацию о вкладах в UI
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] GraphQL query capitalContributors возвращает поле totalContributions
|
||||
- [ ] Селектор ContributorSelector включает новое поле
|
||||
- [ ] API корректно агрегирует данные из backend
|
||||
- [ ] Типизация TypeScript обновлена
|
||||
|
||||
**Technical Notes:**
|
||||
Добавить поле totalContributions в ContributorSelector. Обновить GraphQL schema и resolvers. Протестировать типизацию.
|
||||
|
||||
**Dependencies:**
|
||||
STORY-002 (нужен расчет количества вкладов)
|
||||
|
||||
**Points:** 2
|
||||
|
||||
### STORY-004: Обновить UI компонент
|
||||
|
||||
**Priority:** Must Have
|
||||
|
||||
**User Story:**
|
||||
As a: Пользователь кооператива
|
||||
I want to: Видеть количество вкладов участников в графической форме
|
||||
So that: Визуально понимаю вклад каждого участника
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] ContributorsListWidget отображает количество вкладов
|
||||
- [ ] Информация представлена в понятной графической форме
|
||||
- [ ] Дизайн адаптивен для различных устройств
|
||||
- [ ] Роли участников корректно отображаются
|
||||
|
||||
**Technical Notes:**
|
||||
Обновить ContributorsListWidget.vue для отображения поля totalContributions. Добавить графические элементы (иконки, бейджи). Протестировать адаптивность.
|
||||
|
||||
**Dependencies:**
|
||||
STORY-003 (нужен GraphQL API)
|
||||
|
||||
**Points:** 2
|
||||
|
||||
### STORY-005: Тестирование новой функциональности
|
||||
|
||||
**Priority:** Must Have
|
||||
|
||||
**User Story:**
|
||||
As a: QA инженер
|
||||
I want to: Убедиться что новая функциональность работает корректно
|
||||
So that: Уверен в качестве релиза
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Интеграционное тестирование всех изменений проведено
|
||||
- [ ] Существующие функции не нарушены
|
||||
- [ ] Время загрузки списка участников не превышает 2 секунды
|
||||
- [ ] Пагинация работает плавно при большом количестве участников
|
||||
- [ ] Все acceptance criteria из tech-spec выполнены
|
||||
|
||||
**Technical Notes:**
|
||||
Создать интеграционные тесты. Провести ручное тестирование всех сценариев. Замерить производительность. Проверить кросс-браузерную совместимость.
|
||||
|
||||
**Dependencies:**
|
||||
STORY-001, STORY-002, STORY-003, STORY-004 (все изменения должны быть готовы)
|
||||
|
||||
**Points:** 3
|
||||
|
||||
---
|
||||
|
||||
## Sprint Allocation
|
||||
|
||||
### Sprint 1 (Неделя 1) - 7/8 points
|
||||
|
||||
**Goal:** Реализовать backend изменения для корректного отображения участников и их вкладов
|
||||
|
||||
**Stories:**
|
||||
- STORY-001: Исправить логику создания участников (2 points) - Must Have
|
||||
- STORY-002: Добавить расчет количества вкладов (3 points) - Must Have
|
||||
- STORY-003: Обновить GraphQL селектор (2 points) - Must Have
|
||||
|
||||
**Total:** 7 points / 8 capacity (88% utilization)
|
||||
|
||||
**Risks:**
|
||||
- Сложности с изменением логики создания участников (mitigation: предварительный анализ кода)
|
||||
|
||||
**Dependencies:**
|
||||
- Доступ к backend инфраструктуре
|
||||
|
||||
---
|
||||
|
||||
### Sprint 2 (Неделя 2) - 5/8 points
|
||||
|
||||
**Goal:** Завершить frontend обновления и протестировать новую функциональность виджета
|
||||
|
||||
**Stories:**
|
||||
- STORY-004: Обновить UI компонент (2 points) - Must Have
|
||||
- STORY-005: Тестирование новой функциональности (3 points) - Must Have
|
||||
|
||||
**Total:** 5 points / 8 capacity (63% utilization)
|
||||
|
||||
**Risks:**
|
||||
- Проблемы с производительностью при большом количестве участников (mitigation: оптимизация запросов)
|
||||
|
||||
**Dependencies:**
|
||||
- Завершенные backend изменения из Sprint 1
|
||||
|
||||
---
|
||||
|
||||
## Epic Traceability
|
||||
|
||||
| Epic ID | Epic Name | Stories | Total Points | Sprint |
|
||||
|---------|-----------|---------|--------------|--------|
|
||||
| EPIC-001 | Улучшение виджета участников проекта | STORY-001, 002, 003, 004, 005 | 12 points | Sprint 1-2 |
|
||||
|
||||
---
|
||||
|
||||
## Requirements Coverage
|
||||
|
||||
| FR ID | FR Name | Story | Sprint |
|
||||
|-------|---------|-------|--------|
|
||||
| FR-001 | Исправление логики отображения участников | STORY-001 | 1 |
|
||||
| FR-002 | Добавление поля с количеством вкладов | STORY-002, 003, 004 | 1-2 |
|
||||
| FR-003 | Интеграция с API для данных о вкладах | STORY-003 | 1 |
|
||||
| FR-004 | Отображение ролей участников | STORY-004 | 2 |
|
||||
| FR-005 | Тестирование новой функциональности | STORY-005 | 2 |
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigation
|
||||
|
||||
**High:**
|
||||
- Технические сложности изменения логики создания участников
|
||||
- **Likelihood:** Средний
|
||||
- **Mitigation:** Предварительный анализ кода, создание плана отката
|
||||
|
||||
**Medium:**
|
||||
- Проблемы с производительностью при расчете вкладов
|
||||
- **Likelihood:** Средний
|
||||
- **Mitigation:** Оптимизация запросов, использование индексов БД
|
||||
|
||||
**Low:**
|
||||
- Несовместимость с существующей синхронизацией блокчейна
|
||||
- **Likelihood:** Низкий
|
||||
- **Mitigation:** Тщательное тестирование синхронизации данных
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
|
||||
For a story to be considered complete:
|
||||
- [ ] Code implemented and committed
|
||||
- [ ] Unit tests written and passing (≥80% coverage)
|
||||
- [ ] Integration tests passing
|
||||
- [ ] Code reviewed and approved
|
||||
- [ ] Documentation updated
|
||||
- [ ] Acceptance criteria validated
|
||||
- [ ] Non-functional requirements met (performance, security)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
**Immediate:** Begin Sprint 1
|
||||
|
||||
Run /create-story to create detailed story documents for Sprint 1 stories, or run /dev-story STORY-001 to implement a specific story.
|
||||
|
||||
**Sprint cadence:**
|
||||
- Sprint length: 1 week
|
||||
- Sprint planning: Monday Week 1
|
||||
- Sprint review: Friday Week 1
|
||||
- Sprint retrospective: Friday Week 1
|
||||
|
||||
**Recommended:** Start with /dev-story STORY-001 to begin backend changes
|
||||
|
||||
---
|
||||
|
||||
**This plan was created using BMAD Method v6 - Phase 4 (Implementation Planning)**
|
||||
@@ -0,0 +1,90 @@
|
||||
version: "6.0.0"
|
||||
project_name: "Виджет участников проекта"
|
||||
project_type: "web-app"
|
||||
project_level: 1
|
||||
sprint_plan_path: "docs/sprint-plan-виджет-участников-проекта-2026-02-04.md"
|
||||
current_sprint: 1
|
||||
sprint_length_weeks: 1
|
||||
|
||||
sprints:
|
||||
- sprint_number: 1
|
||||
start_date: "2026-02-04"
|
||||
end_date: "2026-02-11"
|
||||
capacity_points: 8
|
||||
committed_points: 7
|
||||
completed_points: 0
|
||||
status: "not_started"
|
||||
goal: "Реализовать backend изменения для корректного отображения участников и их вкладов"
|
||||
stories:
|
||||
- story_id: "STORY-001"
|
||||
title: "Исправить логику создания участников"
|
||||
points: 2
|
||||
status: "not_started"
|
||||
assigned_to: null
|
||||
acceptance_criteria:
|
||||
- "Участник отображается в виджете сразу после подписания соглашения об участии"
|
||||
- "Не требуется делать вклад для отображения в списке участников"
|
||||
- "Логика создания Contributor изменена с 'при первом вкладе' на 'при подтверждении доступа'"
|
||||
- story_id: "STORY-002"
|
||||
title: "Добавить расчет количества вкладов"
|
||||
points: 3
|
||||
status: "not_started"
|
||||
assigned_to: null
|
||||
acceptance_criteria:
|
||||
- "Для каждого участника рассчитывается общее количество вкладов"
|
||||
- "Данные агрегируются из таблицы segments эффективно"
|
||||
- "Расчет не вызывает N+1 запросов к базе данных"
|
||||
- "Поле contributed_total добавлено в Contributor entity"
|
||||
- story_id: "STORY-003"
|
||||
title: "Обновить GraphQL селектор"
|
||||
points: 2
|
||||
status: "not_started"
|
||||
assigned_to: null
|
||||
acceptance_criteria:
|
||||
- "GraphQL query capitalContributors возвращает поле totalContributions"
|
||||
- "Селектор ContributorSelector включает новое поле"
|
||||
- "API корректно агрегирует данные из backend"
|
||||
- "Типизация TypeScript обновлена"
|
||||
|
||||
- sprint_number: 2
|
||||
start_date: "2026-02-11"
|
||||
end_date: "2026-02-18"
|
||||
capacity_points: 8
|
||||
committed_points: 5
|
||||
completed_points: 0
|
||||
status: "pending"
|
||||
goal: "Завершить frontend обновления и протестировать новую функциональность виджета"
|
||||
stories:
|
||||
- story_id: "STORY-004"
|
||||
title: "Обновить UI компонент"
|
||||
points: 2
|
||||
status: "pending"
|
||||
assigned_to: null
|
||||
acceptance_criteria:
|
||||
- "ContributorsListWidget отображает количество вкладов"
|
||||
- "Информация представлена в понятной графической форме"
|
||||
- "Дизайн адаптивен для различных устройств"
|
||||
- "Роли участников корректно отображаются"
|
||||
- story_id: "STORY-005"
|
||||
title: "Тестирование новой функциональности"
|
||||
points: 3
|
||||
status: "pending"
|
||||
assigned_to: null
|
||||
acceptance_criteria:
|
||||
- "Интеграционное тестирование всех изменений проведено"
|
||||
- "Существующие функции не нарушены"
|
||||
- "Время загрузки списка участников не превышает 2 секунды"
|
||||
- "Пагинация работает плавно при большом количестве участников"
|
||||
- "Все acceptance criteria из tech-spec выполнены"
|
||||
|
||||
velocity:
|
||||
sprint_1: null # Будет заполнено после завершения спринта
|
||||
sprint_2: null
|
||||
rolling_average: null
|
||||
|
||||
team:
|
||||
size: 1
|
||||
sprint_length_weeks: 1
|
||||
capacity_per_sprint: 8
|
||||
developer_level: "mid"
|
||||
productive_hours_per_day: 5
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
# Technical Specification: Виджет участников проекта
|
||||
|
||||
**Date:** 2026-02-04
|
||||
**Author:** AI Assistant
|
||||
**Version:** 1.0
|
||||
**Project Type:** web-app
|
||||
**Project Level:** 1
|
||||
**Status:** Draft
|
||||
|
||||
---
|
||||
|
||||
## Document Overview
|
||||
|
||||
This Technical Specification provides focused technical planning for Виджет участников проекта. It is designed for smaller projects (Level 0-1) that need clear requirements without heavyweight PRD overhead.
|
||||
|
||||
**Related Documents:**
|
||||
- Product Brief: docs/product-brief-виджет-участников-проекта-2026-02-04.md
|
||||
|
||||
---
|
||||
|
||||
## Problem & Solution
|
||||
|
||||
### Problem Statement
|
||||
|
||||
Пользователи не имеют полной информации о своем участии в проекте. Существует две критические проблемы:
|
||||
|
||||
1. **Отсутствие информации о вкладах**: После регистрации в проекте участник получает долю авторского права, но эта информация не отображается в интерфейсе. Виджет показывает только текущую информацию об участии, но не исторические данные о вкладах.
|
||||
|
||||
2. **Неправильная логика отображения участников**: Участники отображаются в виджете только после того, как сделали конкретный вклад в проект. Однако участники должны отображаться сразу после подачи заявления на участие и получения доступа к проекту. Это приводит к тому, что новые участники не видят себя в списке участников до тех пор, пока не внесут вклад.
|
||||
|
||||
### Proposed Solution
|
||||
|
||||
Комплексное улучшение виджета участников проекта:
|
||||
|
||||
1. **Исправление логики отображения участников**: Участники должны отображаться сразу после получения доступа к проекту (после подачи заявления и подписания соглашения об участии), а не только после внесения вклада.
|
||||
|
||||
2. **Добавление информации о вкладах**: Графическое отображение количества вкладов участника и информации о процессе становления участником проекта.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### What Needs to Be Built
|
||||
|
||||
- Исправление логики отображения участников (отображение сразу после получения доступа)
|
||||
- Добавление поля с количеством вкладов в графической форме
|
||||
- Интеграция с API для получения данных о вкладах и статусе участников
|
||||
- Отображение ролей участников
|
||||
- Тестирование новой функциональности
|
||||
|
||||
### What This Does NOT Include
|
||||
|
||||
- Изменение существующей логики учета вкладов
|
||||
- Переработка дизайна всего виджета
|
||||
- Добавление новых типов участников
|
||||
- Интеграция с внешними системами учета
|
||||
|
||||
---
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **Frontend Framework:** Vue 3 + TypeScript + Vite
|
||||
- **UI Library:** Quasar Framework
|
||||
- **State Management:** Pinia
|
||||
- **API:** GraphQL (Zeus SDK)
|
||||
- **Backend:** NestJS + TypeORM + PostgreSQL
|
||||
- **Blockchain Integration:** CoopTypes contracts
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
Проект использует FSD архитектуру с разделением на:
|
||||
- **Widgets Layer:** ContributorsListWidget - компонент отображения таблицы участников
|
||||
- **Entities Layer:** Contributor entity с store для управления данными
|
||||
- **Shared Layer:** Утилиты форматирования и API клиенты
|
||||
|
||||
Данные участников хранятся в композитной сущности: часть в PostgreSQL БД, часть синхронизируется из блокчейна. Логика создания участников должна быть изменена с "при первом вкладе" на "при подтверждении доступа к проекту".
|
||||
|
||||
### Data Model (if applicable)
|
||||
|
||||
**Contributor Entity:**
|
||||
- id, username, coopname - базовые данные
|
||||
- contributor_hash - уникальный идентификатор в блокчейне
|
||||
- appendixes[] - массив хэшей проектов с доступом
|
||||
- contributed_as_* - поля с количеством вкладов по ролям
|
||||
- status - статус участника
|
||||
- created_at - дата создания
|
||||
|
||||
**Segments Table:**
|
||||
- Связь между участниками и их вкладами
|
||||
- Используется для расчета contributed_as_* полей
|
||||
|
||||
### API Design (if applicable)
|
||||
|
||||
**GraphQL Queries:**
|
||||
- `capitalContributors(filter, options)` - получение списка участников с пагинацией
|
||||
- Фильтры: coopname, project_hash (опционально)
|
||||
- Пагинация: page, limit, sortBy, descending
|
||||
|
||||
**Требуемые изменения:**
|
||||
- Модификация логики создания Contributor - создавать при подтверждении appendix вместо первого вклада
|
||||
- Добавление расчета количества вкладов для каждого участника
|
||||
- Обновление ContributorSelector для включения поля общего количества вкладов
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Stories
|
||||
|
||||
1. **Исправить логику создания участников** - Изменить процесс создания Contributor с "при первом вкладе" на "при подтверждении доступа к проекту"
|
||||
2. **Добавить расчет количества вкладов** - Создать логику агрегации данных из segments для каждого участника
|
||||
3. **Обновить GraphQL селектор** - Добавить поле общего количества вкладов в ContributorSelector
|
||||
4. **Обновить UI компонент** - Добавить отображение количества вкладов в ContributorsListWidget
|
||||
5. **Тестирование новой функциональности** - Провести интеграционное тестирование всех изменений
|
||||
|
||||
### Development Phases
|
||||
|
||||
**Phase 1: Backend Changes (3 дня)**
|
||||
- Изменение логики создания участников
|
||||
- Добавление расчета количества вкладов
|
||||
- Обновление GraphQL селектора
|
||||
|
||||
**Phase 2: Frontend Updates (2 дня)**
|
||||
- Обновление UI компонента
|
||||
- Тестирование отображения
|
||||
|
||||
**Phase 3: Integration & Testing (2 дня)**
|
||||
- Интеграционное тестирование
|
||||
- Проверка существующих функций
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
How we'll know it's done:
|
||||
|
||||
- Участники отображаются в виджете сразу после получения доступа к проекту (подписания соглашения об участии)
|
||||
- Виджет корректно отображает количество вкладов участников
|
||||
- Информация о вкладах представлена в понятной графической форме
|
||||
- Роли участников корректно отображаются после назначения
|
||||
- Новая функциональность не нарушает работу существующего виджета
|
||||
- Все acceptance criteria из Product Brief выполнены
|
||||
|
||||
---
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
### Performance
|
||||
|
||||
- Время загрузки списка участников не должно превышать 2 секунды
|
||||
- Расчет количества вкладов должен выполняться эффективно без дополнительных N+1 запросов
|
||||
- Пагинация должна работать плавно при большом количестве участников
|
||||
|
||||
### Security
|
||||
|
||||
- Доступ к данным участников только для авторизованных пользователей кооператива
|
||||
- Валидация всех входных данных на backend
|
||||
- Логирование операций с данными участников
|
||||
|
||||
### Other
|
||||
|
||||
- Адаптивный дизайн для различных устройств
|
||||
- Поддержка существующих браузеров (Chrome, Firefox, Safari)
|
||||
- Локализация на русский язык
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Доступ к существующей инфраструктуре PostgreSQL и блокчейна
|
||||
- Существующие API для работы с appendix и segments
|
||||
- Команда backend разработчиков для изменений в логике создания участников
|
||||
|
||||
---
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
- **Технические сложности изменения логики создания участников**
|
||||
- **Likelihood:** Средний
|
||||
- **Mitigation:** Предварительный анализ кода и создание плана отката
|
||||
|
||||
- **Проблемы с производительностью при расчете вкладов**
|
||||
- **Likelihood:** Средний
|
||||
- **Mitigation:** Оптимизация запросов, использование индексов БД
|
||||
|
||||
- **Несовместимость с существующей синхронизацией блокчейна**
|
||||
- **Likelihood:** Низкий
|
||||
- **Mitigation:** Тщательное тестирование синхронизации данных
|
||||
|
||||
---
|
||||
|
||||
## Timeline
|
||||
|
||||
**Target Completion:** 2026-02-18 (2 недели после начала разработки)
|
||||
|
||||
**Milestones:**
|
||||
- Завершение анализа требований: 2026-02-04 (1 день)
|
||||
- Разработка backend изменений: 2026-02-05 - 2026-02-07 (3 дня)
|
||||
- Frontend обновления: 2026-02-10 - 2026-02-11 (2 дня)
|
||||
- Интеграционное тестирование: 2026-02-12 - 2026-02-13 (2 дня)
|
||||
- Деплой и мониторинг: 2026-02-14 - 2026-02-18 (1 неделя)
|
||||
|
||||
---
|
||||
|
||||
## Approval
|
||||
|
||||
**Reviewed By:**
|
||||
- [ ] AI Assistant (Author)
|
||||
- [ ] Technical Lead
|
||||
- [ ] Product Owner
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Phase 4: Implementation
|
||||
|
||||
For Level 1 projects (1-10 stories):
|
||||
- Run `/sprint-planning` to plan your sprint
|
||||
- Then create and implement stories
|
||||
|
||||
---
|
||||
|
||||
**This document was created using BMAD Method v6 - Phase 2 (Planning)**
|
||||
|
||||
*To continue: Run `/workflow-status` to see your progress and next recommended workflow.*
|
||||
@@ -0,0 +1,346 @@
|
||||
# Руководство по добавлению документов в онбординг председателя кооператива (Благорост)
|
||||
|
||||
## Обзор системы
|
||||
|
||||
Онбординг председателя кооператива по программе "Благорост" представляет собой последовательность шагов, где каждый шаг - это утверждение определенного документа советом кооператива.
|
||||
|
||||
### Как работает система:
|
||||
|
||||
1. **Председатель** отправляет документ на рассмотрение совета через интерфейс онбординга
|
||||
2. **Система** создает проект свободного решения с вопросом в повестке заседания совета
|
||||
3. **Совет** голосует по вопросу и принимает решение
|
||||
4. **Система отслеживания решений** автоматически отмечает шаг онбординга как завершенный
|
||||
5. **Интерфейс** обновляется, показывая завершенный шаг
|
||||
|
||||
### Архитектура:
|
||||
|
||||
- **Frontend**: Vue.js компонент `CapitalOnboardingCard.vue` отображает список шагов
|
||||
- **Backend**: NestJS сервисы обрабатывают создание решений и отслеживание их принятия
|
||||
- **Database**: Конфигурация расширения `capital` хранит состояние онбординга
|
||||
- **Blockchain**: Решения совета записываются в блокчейн
|
||||
- **Factory**: Генерирует документы на основе шаблонов
|
||||
|
||||
## Шаги добавления нового документа в онбординг
|
||||
|
||||
### 1. Создание документа в Factory
|
||||
|
||||
#### 1.1 Создать Action (src/Actions/{registry_id}.{DocumentName}.ts)
|
||||
```typescript
|
||||
import { DraftContract } from 'cooptypes'
|
||||
import { DocumentName } from '../Templates'
|
||||
import { DocFactory } from '../Factory'
|
||||
import type { IGeneratedDocument, IGenerationOptions, IMetaDocument, ITemplate } from '../Interfaces'
|
||||
import type { MongoDBConnector } from '../Services/Databazor'
|
||||
|
||||
export { DocumentName as Template } from '../Templates'
|
||||
|
||||
export class Factory extends DocFactory<DocumentName.Action> {
|
||||
constructor(storage: MongoDBConnector) {
|
||||
super(storage)
|
||||
}
|
||||
|
||||
async generateDocument(data: DocumentName.Action, options?: IGenerationOptions): Promise<IGeneratedDocument> {
|
||||
let template: ITemplate<DocumentName.Model>
|
||||
|
||||
if (process.env.SOURCE === 'local') {
|
||||
template = DocumentName.Template
|
||||
} else {
|
||||
template = await this.getTemplate(DraftContract.contractName.production, DocumentName.registry_id, data.block_num)
|
||||
}
|
||||
|
||||
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
|
||||
const vars = await this.getVars(data.coopname)
|
||||
const coop = await this.getCooperative(data.coopname)
|
||||
|
||||
const combinedData: DocumentName.Model = {
|
||||
meta,
|
||||
coop,
|
||||
vars,
|
||||
}
|
||||
|
||||
await this.validate(combinedData, template.model)
|
||||
|
||||
const translation = template.translations[meta.lang]
|
||||
const document: IGeneratedDocument = await this.generatePDF('', template.context, combinedData, translation, meta, options?.skip_save)
|
||||
|
||||
return document
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Создать Template (src/Templates/{registry_id}.{DocumentName}.ts)
|
||||
```typescript
|
||||
import type { JSONSchemaType } from 'ajv'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
import type { ITemplate } from '../Interfaces'
|
||||
import { IMetaJSONSchema } from '../Schema/MetaSchema'
|
||||
import { CommonUserSchema, CooperativeSchema, VarsSchema } from '../Schema'
|
||||
|
||||
export const registry_id = Cooperative.Registry.DocumentName.registry_id
|
||||
|
||||
// Модель действия для генерации
|
||||
export type Action = Cooperative.Registry.DocumentName.Action
|
||||
|
||||
// Модель данных
|
||||
export type Model = Cooperative.Registry.DocumentName.Model
|
||||
|
||||
// Схема для сверки
|
||||
export const Schema: JSONSchemaType<Model> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
meta: IMetaJSONSchema,
|
||||
coop: CooperativeSchema,
|
||||
vars: VarsSchema,
|
||||
},
|
||||
required: ['meta', 'coop', 'vars'],
|
||||
additionalProperties: true,
|
||||
}
|
||||
|
||||
export const Template: ITemplate<Model> = {
|
||||
title: Cooperative.Registry.DocumentName.title,
|
||||
description: Cooperative.Registry.DocumentName.description,
|
||||
model: Schema,
|
||||
context: Cooperative.Registry.DocumentName.context,
|
||||
translations: Cooperative.Registry.DocumentName.translations,
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 Добавить экспорт в src/Actions/index.ts
|
||||
```typescript
|
||||
export * as DocumentName from './{registry_id}.DocumentName'
|
||||
```
|
||||
|
||||
### 2. Создание документа в CoopTypes
|
||||
|
||||
#### 2.1 Создать определение в cooperative/registry/{registry_id}.{DocumentName}/index.ts
|
||||
```typescript
|
||||
import type { IGenerate, IMetaDocument } from '../../document'
|
||||
import type { ICommonUser, ICooperativeData, IVars } from '../../model'
|
||||
|
||||
export const registry_id = {registry_id}
|
||||
|
||||
// Модель действия для генерации
|
||||
export interface Action extends IGenerate {
|
||||
registry_id: number
|
||||
}
|
||||
|
||||
export type Meta = IMetaDocument & Action
|
||||
|
||||
// Модель данных документа
|
||||
export interface Model {
|
||||
meta: IMetaDocument
|
||||
coop: ICooperativeData
|
||||
vars: IVars
|
||||
}
|
||||
|
||||
export const title = 'Название документа'
|
||||
export const description = 'Описание документа'
|
||||
export const context = `<div class="digital-document"><!-- HTML контент --></div>`
|
||||
export const translations = {
|
||||
ru: {
|
||||
// переводы
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Добавить в cooperative/registry/index.ts
|
||||
```typescript
|
||||
export * as DocumentName from './{registry_id}.DocumentName'
|
||||
```
|
||||
|
||||
### 3. Обновление Frontend
|
||||
|
||||
#### 3.1 Обновить composable.ts (desktop/extensions/capital/features/Onboarding/model/composable.ts)
|
||||
```typescript
|
||||
// Добавить в stepToRegistryId
|
||||
const stepToRegistryId: Record<string, number> = {
|
||||
'document_name': {registry_id},
|
||||
// ... остальные
|
||||
};
|
||||
|
||||
// Добавить шаг в stepsConfig (в нужной позиции)
|
||||
{
|
||||
id: 'document_name',
|
||||
title: 'Название документа для отображения',
|
||||
description: 'Описание что делает этот шаг',
|
||||
question: 'Вопрос для повестки совета',
|
||||
decision: '',
|
||||
decisionPrefix: 'Утвердить документ:',
|
||||
status: state?.document_name_done ? 'completed' :
|
||||
state?.onboarding_document_name_hash ? 'in_progress' : 'pending',
|
||||
hash: typeof state?.onboarding_document_name_hash === 'string' && state.onboarding_document_name_hash ? state.onboarding_document_name_hash : null,
|
||||
// depends_on: ['other_step'] // если есть зависимости
|
||||
},
|
||||
```
|
||||
|
||||
### 4. Обновление Backend DTO
|
||||
|
||||
#### 4.1 Обновить enum в onboarding.dto.ts
|
||||
```typescript
|
||||
export enum CapitalOnboardingStepEnum {
|
||||
document_name = 'document_name',
|
||||
// ... остальные
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 Добавить поля в CapitalOnboardingStateDTO
|
||||
```typescript
|
||||
@Field(() => Boolean)
|
||||
document_name_done!: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
onboarding_document_name_hash?: string | null;
|
||||
```
|
||||
|
||||
### 5. Обновление Backend Service
|
||||
|
||||
#### 5.1 Обновить типы в onboarding.service.ts
|
||||
```typescript
|
||||
type OnboardingFlagKey =
|
||||
| 'onboarding_document_name_done'
|
||||
| // ... остальные
|
||||
|
||||
type OnboardingHashKey =
|
||||
| 'onboarding_document_name_hash'
|
||||
| // ... остальные
|
||||
```
|
||||
|
||||
#### 5.2 Обновить методы маппинга
|
||||
```typescript
|
||||
private mapStepToFlag(step: CapitalOnboardingStepEnum): OnboardingFlagKey {
|
||||
switch (step) {
|
||||
case CapitalOnboardingStepEnum.document_name:
|
||||
return 'onboarding_document_name_done';
|
||||
// ... остальные
|
||||
}
|
||||
}
|
||||
|
||||
private mapStepToHash(step: CapitalOnboardingStepEnum): OnboardingHashKey {
|
||||
switch (step) {
|
||||
case CapitalOnboardingStepEnum.document_name:
|
||||
return 'onboarding_document_name_hash';
|
||||
// ... остальные
|
||||
}
|
||||
}
|
||||
|
||||
private mapStepToVarsField(step: CapitalOnboardingStepEnum): string {
|
||||
switch (step) {
|
||||
case CapitalOnboardingStepEnum.document_name:
|
||||
return 'document_name';
|
||||
// ... остальные
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.3 Обновить buildState метод
|
||||
```typescript
|
||||
private buildState(pluginConfig: IConfig & Record<string, any>): CapitalOnboardingStateDTO {
|
||||
return {
|
||||
document_name_done: !!pluginConfig.onboarding_document_name_done,
|
||||
onboarding_document_name_hash: pluginConfig.onboarding_document_name_hash || null,
|
||||
// ... остальные поля
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Обновление GraphQL Schema
|
||||
|
||||
#### 6.1 Добавить шаг в enum CapitalOnboardingStep
|
||||
```graphql
|
||||
enum CapitalOnboardingStep {
|
||||
document_name
|
||||
# ... остальные
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2 Добавить поля в type CapitalOnboardingState
|
||||
```graphql
|
||||
type CapitalOnboardingState {
|
||||
document_name_done: Boolean!
|
||||
onboarding_document_name_hash: String
|
||||
# ... остальные поля
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Обновление SDK
|
||||
|
||||
#### 7.1 Обновить selectors/capital/onboardingStateSelector.ts
|
||||
```typescript
|
||||
const onboardingStateFields = {
|
||||
document_name_done: true,
|
||||
onboarding_document_name_hash: true,
|
||||
// ... остальные
|
||||
} as const
|
||||
```
|
||||
|
||||
### 8. Обновление сервиса обработки событий
|
||||
|
||||
#### 8.1 Обновить CapitalOnboardingEventsService
|
||||
```typescript
|
||||
private mapStepToFlag(step: string): keyof IConfig | null {
|
||||
const mapping: Record<string, keyof IConfig> = {
|
||||
document_name: 'onboarding_document_name_done',
|
||||
// ... остальные
|
||||
};
|
||||
|
||||
return mapping[step] || null;
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Обновление конфигурации расширения
|
||||
|
||||
#### 9.1 Добавить поля в defaultConfig (capital-extension.module.ts)
|
||||
```typescript
|
||||
export const defaultConfig = {
|
||||
// ... другие поля
|
||||
|
||||
// Онбординг флаги
|
||||
onboarding_document_name_done: false,
|
||||
// ... остальные onboarding флаги
|
||||
} as const;
|
||||
```
|
||||
|
||||
#### 9.2 Добавить поля в Zod-схему (capital-extension.module.ts)
|
||||
```typescript
|
||||
// Онбординг флаги
|
||||
onboarding_document_name_done: z
|
||||
.boolean()
|
||||
.default(defaultConfig.onboarding_document_name_done)
|
||||
.describe(describeField({ label: 'Описание шага онбординга', visible: false })),
|
||||
// ... остальные onboarding поля
|
||||
```
|
||||
|
||||
**Примечание:** Hash поля (`onboarding_document_name_hash`) не нужно добавлять в конфигурацию, так как они используются опционально и инициализируются как `null` по умолчанию.
|
||||
|
||||
## Проверка работы
|
||||
|
||||
1. **Проверить отсутствие ошибок TypeScript** в файлах:
|
||||
- `capital-extension.module.ts`
|
||||
- `onboarding-events.service.ts`
|
||||
- Других обновленных файлах
|
||||
|
||||
2. Перезапустить сервер
|
||||
3. Открыть интерфейс онбординга
|
||||
4. Проверить что новый документ появился в списке
|
||||
5. Отправить документ на рассмотрение совета
|
||||
6. Проверить логи на наличие предупреждений о неизвестном шаге
|
||||
7. Принять решение советом
|
||||
8. Проверить что шаг отмечен как завершенный
|
||||
|
||||
## Возможные проблемы
|
||||
|
||||
1. **"Неизвестный шаг онбординга"** - проверить маппинг в CapitalOnboardingEventsService
|
||||
2. **Документ не генерируется** - проверить Action и Template в Factory
|
||||
3. **Шаг не отображается** - проверить composable.ts на фронтенде
|
||||
4. **GraphQL ошибки** - проверить схему и DTO
|
||||
|
||||
## Полезные команды
|
||||
|
||||
```bash
|
||||
# Проверить логи контроллера
|
||||
tail -f logs/controller.log
|
||||
|
||||
# Перезапустить сервисы
|
||||
pnpm restart:controller
|
||||
pnpm restart:desktop
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
# Система регистрации в CAPITAL: выбор пути
|
||||
|
||||
## Обзор
|
||||
|
||||
Система регистрации в CAPITAL поддерживает два пути регистрации участников кооператива: **Благорост** и **Генератор**. Каждый путь определяет набор документов, которые необходимо подписать участнику для завершения регистрации.
|
||||
|
||||
## Архитектура потоков
|
||||
|
||||
### 1. Первоначальный выбор программы (SignUp.vue)
|
||||
|
||||
**Местоположение:** `desktop/src/pages/Registrator/SignUp/SignUp.vue`
|
||||
|
||||
На этапе первичной регистрации пользователь выбирает одну из двух программ:
|
||||
- **Программа Благорост** - подписывается `BlagorostOffer` (офферта Благорост)
|
||||
- **Программа Генератор** - подписывается `GeneratorOffer` (офферта Генератор)
|
||||
|
||||
Выбор сохраняется в поле `program_key` сущности `Contributor`:
|
||||
- `GENERATION` - для программы Генератор
|
||||
- `CAPITALIZATION` - для программы Благорост
|
||||
|
||||
### 2. Завершение регистрации (CapitalRegistrationPage.vue)
|
||||
|
||||
**Местоположение:** `desktop/extensions/capital/pages/CapitalRegistrationPage/ui/CapitalRegistrationPage.vue`
|
||||
|
||||
После первоначального выбора на странице завершения регистрации генерируется разный набор документов:
|
||||
|
||||
#### Путь Генератора (`program_key = GENERATION`)
|
||||
Генерируются и подписываются **3 документа**:
|
||||
1. `GenerationContract` (Договор УХД) - всегда
|
||||
2. `StorageAgreement` (Соглашение о хранении имущества) - всегда
|
||||
3. `BlagorostAgreement` (Соглашение Благорост) - только для этого пути
|
||||
|
||||
#### Путь Благороста (`program_key = CAPITALIZATION`)
|
||||
Генерируются и подписываются **2 документа**:
|
||||
1. `GenerationContract` (Договор УХД) - всегда
|
||||
2. `StorageAgreement` (Соглашение о хранении имущества) - всегда
|
||||
3. `BlagorostAgreement` НЕ генерируется (уже подписано через оферту)
|
||||
|
||||
## Сохранение данных
|
||||
|
||||
### Contributor entity
|
||||
```typescript
|
||||
program_key?: string; // Ключ выбранной программы
|
||||
blagorost_offer_hash?: string; // Хеш подписанной оферты Благорост
|
||||
generator_offer_hash?: string; // Хеш подписанной оферты Генератор
|
||||
generation_contract_hash?: string; // Хеш подписанного договора УХД
|
||||
storage_agreement_hash?: string; // Хеш подписанного соглашения о хранении
|
||||
blagorost_agreement_hash?: string; // Хеш подписанного соглашения Благорост
|
||||
```
|
||||
|
||||
### Udata параметры документов
|
||||
Для каждого документа генерируются уникальные параметры:
|
||||
```typescript
|
||||
// Благорост
|
||||
blagorost_agreement_number: string; // Номер соглашения
|
||||
blagorost_agreement_created_at: string; // Дата создания
|
||||
|
||||
// Генератор
|
||||
generator_agreement_number: string; // Номер соглашения
|
||||
generator_agreement_created_at: string; // Дата создания
|
||||
|
||||
// Договор УХД
|
||||
generation_contract_number: string; // Номер договора
|
||||
generation_contract_created_at: string; // Дата создания
|
||||
|
||||
// Соглашение о хранении
|
||||
storage_agreement_number: string; // Номер соглашения
|
||||
storage_agreement_created_at: string; // Дата создания
|
||||
```
|
||||
|
||||
## Ключевые особенности
|
||||
|
||||
1. **Условная генерация**: Набор документов зависит от первоначального выбора программы
|
||||
2. **Единая точка сохранения**: Один ключ `blagorost_agreement_number` используется для параметров соглашения Благорост, независимо от того, подписано ли оно через оферту или отдельно
|
||||
3. **Проверка завершения**: Регистрация считается завершенной при наличии `generation_contract_hash` и `storage_agreement_hash`
|
||||
4. **Блокчейн интеграция**: Все подписанные документы отправляются в блокчейн через действие `regcontrib`
|
||||
|
||||
## API endpoints
|
||||
|
||||
### Генерация документов
|
||||
```graphql
|
||||
mutation GenerateCapitalRegistrationDocuments($data: GenerateCapitalRegistrationDocumentsInputDTO!) {
|
||||
generateCapitalRegistrationDocuments(data: $data) {
|
||||
generation_contract { html, hash }
|
||||
storage_agreement { html, hash }
|
||||
blagorost_agreement { html, hash } # опционально
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Отправка в блокчейн
|
||||
```graphql
|
||||
mutation CompleteCapitalRegistration($data: CompleteCapitalRegistrationInputDTO!) {
|
||||
completeCapitalRegistration(data: $data) {
|
||||
transaction_id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Расширения и сервисы
|
||||
|
||||
- **ParticipationManagementInteractor**: бизнес-логика регистрации
|
||||
- **UdataDocumentParametersService**: генерация параметров документов
|
||||
- **RegistrationDocumentsService**: генерация пакета документов
|
||||
- **useGenerateCapitalRegistrationDocuments**: фронтенд-композабл для генерации
|
||||
- **useCompleteCapitalRegistration**: фронтенд-композабл для отправки в блокчейн
|
||||
@@ -0,0 +1,151 @@
|
||||
# Руководство по связям между сущностями
|
||||
|
||||
## Архитектура связей
|
||||
|
||||
### Основные сущности и их связи:
|
||||
|
||||
```
|
||||
Project (Проект)
|
||||
├── OneToMany → Issue (Задачи проекта)
|
||||
├── OneToMany → Story (Проектные истории - где issue_id = null)
|
||||
│
|
||||
Issue (Задача)
|
||||
├── ManyToOne → Project (Принадлежит проекту)
|
||||
├── ManyToOne → Cycle (Принадлежит циклу)
|
||||
├── OneToMany → Comment (Комментарии к задаче)
|
||||
├── OneToMany → Story (Истории задачи - где issue_id = issue.id)
|
||||
│
|
||||
Cycle (Цикл)
|
||||
├── OneToMany → Issue (Задачи цикла)
|
||||
│
|
||||
Story (История/Критерий)
|
||||
├── ManyToOne → Project (Проектная история)
|
||||
├── ManyToOne → Issue (История задачи)
|
||||
│
|
||||
Comment (Комментарий)
|
||||
├── ManyToOne → Issue (Принадлежит задаче)
|
||||
```
|
||||
|
||||
## Методы загрузки связанных данных
|
||||
|
||||
### Project Repository
|
||||
|
||||
```typescript
|
||||
// Найти проект с задачами
|
||||
const projectWithIssues = await projectRepository.findByIdWithIssues(projectHash);
|
||||
|
||||
// Найти проект с историями
|
||||
const projectWithStories = await projectRepository.findByIdWithStories(projectHash);
|
||||
|
||||
// Найти проект со всеми связанными данными
|
||||
const projectWithAll = await projectRepository.findByIdWithAllRelations(projectHash);
|
||||
```
|
||||
|
||||
### Issue Repository
|
||||
|
||||
```typescript
|
||||
// Найти задачу с комментариями
|
||||
const issueWithComments = await issueRepository.findByIdWithComments(issueId);
|
||||
|
||||
// Найти задачу с историями
|
||||
const issueWithStories = await issueRepository.findByIdWithStories(issueId);
|
||||
|
||||
// Найти задачу со всеми связанными данными
|
||||
const issueWithAll = await issueRepository.findByIdWithAllRelations(issueId);
|
||||
|
||||
// Найти задачи проекта с комментариями
|
||||
const issuesWithComments = await issueRepository.findByProjectHashWithComments(projectHash);
|
||||
|
||||
// Найти задачи проекта с историями
|
||||
const issuesWithStories = await issueRepository.findByProjectHashWithStories(projectHash);
|
||||
```
|
||||
|
||||
### Story Repository
|
||||
|
||||
```typescript
|
||||
// Найти только проектные истории (issue_id = null)
|
||||
const projectStories = await storyRepository.findByProjectHash(projectHash);
|
||||
|
||||
// Найти все истории проекта (проектные + истории всех задач проекта)
|
||||
const allProjectStories = await storyRepository.findAllByProjectHash(projectHash);
|
||||
|
||||
// Найти только проектные истории (явный метод)
|
||||
const projectOnlyStories = await storyRepository.findProjectStories(projectHash);
|
||||
|
||||
// Найти истории конкретной задачи
|
||||
const issueStories = await storyRepository.findByIssueId(issueId);
|
||||
```
|
||||
|
||||
### Cycle Repository
|
||||
|
||||
```typescript
|
||||
// Найти цикл с задачами
|
||||
const cycleWithIssues = await cycleRepository.findByIdWithIssues(cycleId);
|
||||
|
||||
// Найти активный цикл с задачами
|
||||
const activeCycleWithIssues = await cycleRepository.findActiveCycleWithIssues();
|
||||
```
|
||||
|
||||
### Comment Repository
|
||||
|
||||
```typescript
|
||||
// Найти комментарий с задачей
|
||||
const commentWithIssue = await commentRepository.findByIdWithIssue(commentId);
|
||||
|
||||
// Найти комментарии по комментатору
|
||||
const commentsByCommentor = await commentRepository.findByCommentorId(commentorId);
|
||||
|
||||
// Найти комментарии задачи с комментаторами
|
||||
const commentsWithCommentors = await commentRepository.findByIssueIdWithCommentors(issueId);
|
||||
```
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Создание новой задачи с историями
|
||||
|
||||
```typescript
|
||||
// Создать задачу
|
||||
const issue = await issueRepository.create({
|
||||
title: 'Разработать API',
|
||||
project_hash: 'project123',
|
||||
created_by: 'user123',
|
||||
creators_ids: ['user123']
|
||||
});
|
||||
|
||||
// Создать историю для задачи
|
||||
const story = await storyRepository.create({
|
||||
title: 'Реализовать эндпоинт POST /api/users',
|
||||
description: 'Должен принимать JSON с полями name, email',
|
||||
project_hash: 'project123',
|
||||
issue_id: issue._id,
|
||||
created_by: 'user123'
|
||||
});
|
||||
```
|
||||
|
||||
### Получение полного проекта со всеми данными
|
||||
|
||||
```typescript
|
||||
const project = await projectRepository.findByIdWithAllRelations('project123');
|
||||
// Теперь project.issues содержит все задачи
|
||||
// project.issues[0].comments содержит комментарии к задачам
|
||||
// project.issues[0].stories содержит истории задач
|
||||
// project.stories содержит только проектные истории
|
||||
```
|
||||
|
||||
## Важные замечания
|
||||
|
||||
### Фильтрация историй проекта
|
||||
|
||||
- `findByProjectHash()` - возвращает только проектные истории (где `issue_id = null`)
|
||||
- `findAllByProjectHash()` - возвращает все истории проекта (проектные + истории всех задач проекта)
|
||||
- `findProjectStories()` - явный метод для проектных историй
|
||||
|
||||
### Каскадные операции
|
||||
|
||||
- При удалении проекта удаляются все связанные задачи, комментарии и истории
|
||||
- При удалении задачи удаляются связанные комментарии и истории
|
||||
- При удалении цикла задачи остаются, но `cycle_id` становится `null`
|
||||
|
||||
### Индексы
|
||||
|
||||
Все связи поддерживаются соответствующими индексами в базе данных для оптимальной производительности.
|
||||
@@ -0,0 +1,147 @@
|
||||
# 🚨 Быстрое исправление циклических зависимостей
|
||||
|
||||
## Увидел ошибку? Действуй!
|
||||
|
||||
```
|
||||
Error: A circular dependency has been detected inside DocumentDomainModule
|
||||
```
|
||||
|
||||
### Шаг 1: Запусти анализ (30 секунд)
|
||||
|
||||
```bash
|
||||
pnpm analyze:modules
|
||||
```
|
||||
|
||||
Откроются 2 файла:
|
||||
- `potential-circular-dependencies.md` - **НАЧНИ С ЭТОГО!**
|
||||
- `module-dependency-graph.md` - полный граф
|
||||
|
||||
### Шаг 2: Найди проблему в отчёте
|
||||
|
||||
Открой `potential-circular-dependencies.md` и ищи:
|
||||
|
||||
#### A) Секция "CRITICAL: Global modules importing non-global"
|
||||
```md
|
||||
### ❌ BlockchainModule (@Global)
|
||||
**Problematic imports:**
|
||||
- ❌ RegistrationDomainModule (not global) 👈 ВОТ ОНО!
|
||||
```
|
||||
|
||||
**Решение:**
|
||||
```typescript
|
||||
// Вариант 1: Убери импорт
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [], // Убрал RegistrationDomainModule
|
||||
})
|
||||
export class BlockchainModule {}
|
||||
|
||||
// Вариант 2: Сделай импортируемый модуль тоже глобальным
|
||||
@Global() // Добавил
|
||||
@Module({...})
|
||||
export class RegistrationDomainModule {}
|
||||
```
|
||||
|
||||
#### B) Секция "Highly Coupled Modules"
|
||||
```md
|
||||
### DocumentDomainModule 📦
|
||||
⚠️ WARNING: Non-global module used by global module(s)! 👈 ПРОБЛЕМА!
|
||||
```
|
||||
|
||||
**Решение:**
|
||||
```typescript
|
||||
// В глобальном модуле используй forwardRef
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [forwardRef(() => DocumentDomainModule)],
|
||||
})
|
||||
```
|
||||
|
||||
### Шаг 3: Если анализ не помог - метод комментирования
|
||||
|
||||
**В `app.module.ts`:**
|
||||
|
||||
```typescript
|
||||
@Module({
|
||||
imports: [
|
||||
// Инфраструктура
|
||||
DatabaseModule,
|
||||
RedisModule,
|
||||
|
||||
// Domain - КОММЕНТИРУЙ ПО ОДНОМУ!
|
||||
// AuthDomainModule,
|
||||
// AccountDomainModule,
|
||||
RegistrationDomainModule, // 👈 Раскомментируй это
|
||||
// DocumentDomainModule, // 👈 Потом это
|
||||
// ParticipantDomainModule, // 👈 Потом это
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
**Алгоритм:**
|
||||
1. Закомментируй ВСЕ domain-модули
|
||||
2. Раскомментируй по одному
|
||||
3. После каждого раскомментирования запускай: `pnpm dev`
|
||||
4. Когда ошибка появится → ты нашёл виновника!
|
||||
|
||||
### Шаг 4: Применяй стандартные решения
|
||||
|
||||
#### Решение 1: forwardRef в imports
|
||||
```typescript
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => AccountDomainModule),
|
||||
forwardRef(() => DocumentDomainModule),
|
||||
],
|
||||
})
|
||||
export class ParticipantDomainModule {}
|
||||
```
|
||||
|
||||
#### Решение 2: forwardRef в инжекции
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(
|
||||
@Inject(forwardRef(() => SOME_SERVICE))
|
||||
private readonly someService: SomeService
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
#### Решение 3: Убрать лишний модуль
|
||||
```typescript
|
||||
// ❌ БЫЛО два модуля
|
||||
// document/document.module.ts
|
||||
// document/document-validation.module.ts
|
||||
|
||||
// ✅ СТАЛО один модуль
|
||||
@Module({
|
||||
providers: [
|
||||
DocumentService,
|
||||
DocumentValidationService, // Просто добавили сюда
|
||||
],
|
||||
})
|
||||
export class DocumentDomainModule {}
|
||||
```
|
||||
|
||||
## 🎯 Чеклист: Что НЕ делать
|
||||
|
||||
- ❌ Не меняй порядок импортов (не поможет!)
|
||||
- ❌ Не делай модуль глобальным без необходимости
|
||||
- ❌ Не создавай отдельный модуль для одного сервиса
|
||||
- ❌ Не импортируй не-глобальные модули в глобальные (без forwardRef)
|
||||
|
||||
## 🎯 Чеклист: Что делать
|
||||
|
||||
- ✅ Используй `pnpm analyze:modules` ПЕРВЫМ делом
|
||||
- ✅ Используй forwardRef для domain-модулей
|
||||
- ✅ Проверь, не является ли проблемный модуль глобальным
|
||||
- ✅ Используй метод комментирования для точной локализации
|
||||
|
||||
## 📚 Подробности
|
||||
|
||||
См. полное руководство: `CIRCULAR_DEPENDENCIES_GUIDE.md`
|
||||
|
||||
---
|
||||
|
||||
*Сохрани этот файл в закладки - он спасёт тебя часы отладки!*
|
||||
@@ -0,0 +1,44 @@
|
||||
Правило 0. Secondary index используется ровно один раз — для получения primary key. Любые чтения и все изменения состояния выполняются только по primary key.
|
||||
|
||||
Запреты
|
||||
- ❌ Повторный get_*_by_hash() после emplace/modify
|
||||
- ❌ Передача hash в write-функции
|
||||
- ❌ Возврат row-объектов для дальнейшей мутации
|
||||
|
||||
Разрешено
|
||||
✅ find(id) сколько угодно раз
|
||||
✅ modify(id) после любых изменений
|
||||
✅ Передача id между всеми слоями
|
||||
|
||||
В прикладной форме
|
||||
- checksum256 / secondary_id — только вход
|
||||
- uint64_t (primary key) — вся внутренняя логика
|
||||
|
||||
В одном action:
|
||||
- byhash.find() → получить id
|
||||
- дальше запрещены любые повторные lookup по secondary index
|
||||
|
||||
Правило 1. Поиск по secondary_index'ам должен быть однократным. Запрещено в рамках одного действия повторно обращаться к поиску по secondary_index.
|
||||
|
||||
ПРАВИЛЬНО:
|
||||
auto project = get_project_or_fail(coopname, project_hash);
|
||||
uint64_t project_id = project.id;
|
||||
|
||||
// дальше ВЕЗДЕ
|
||||
set_master(coopname, project_id, master);
|
||||
increment_total_authors(coopname, project_id);
|
||||
upsert_author_segment(coopname, project_id, master);
|
||||
|
||||
|
||||
НЕ ПРАВИЛЬНО:
|
||||
increment_total_authors(coopname, project_hash); // внутри снова lookup
|
||||
|
||||
Правило 3. При работе с итераторами не используй перевод в объект
|
||||
segments_index segments(_capital, coopname.value);
|
||||
auto segment = segments.find(segment_id);
|
||||
|
||||
auto amount = segment->debt_amount // ПРАВИЛЬНО
|
||||
|
||||
auto segment_ = *segment // НЕ ПРАВИЛЬНО
|
||||
auto amount = segment.amount
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
# 🎮 Система "Уровень и Энергия" - Благорост
|
||||
|
||||
## 🌿 Что это такое
|
||||
|
||||
**Благорост** — это система роста благосостояния через создание и использование результатов интеллектуальной деятельности. Система "Уровень и Энергия" создаёт живую модель роста, где участники чувствуют не только экономический эффект, но и личную динамику вовлечённости.
|
||||
|
||||
### Основные понятия
|
||||
|
||||
**Уровень** — отражает достигнутую ступень развития участника. Он показывает, насколько человек укоренён в системе, сколько уже вложил и создал. Уровень **может только расти** — это накопленный результат, фундамент, который не теряется.
|
||||
|
||||
**Энергия** — отражает текущую активность. Она **увеличивается**, когда человек проявляется через взносы, и **уменьшается**, когда он бездействует. Это показатель "жизни участия".
|
||||
|
||||
### Как это работает
|
||||
|
||||
- Каждый участник имеет числовые показатели: `level` (целое число) и `energy` (0–100%)
|
||||
- При внесении взноса (деньги, труд, идеи) участник получает **прирост энергии (gain)**
|
||||
- Каждый день энергия **естественно уменьшается (decay_rate)** при отсутствии новых проявлений
|
||||
- При достижении энергии 100% происходит **переход на следующий уровень** с обнулением энергии
|
||||
|
||||
### Поведение и мотивация
|
||||
|
||||
Уровень не снижается — он фиксирует достигнутое. Энергия постоянно колеблется: при активности растёт, при бездействии падает. Это создаёт **мягкое давление к развитию** — человек не теряет результат, но теряет "тонус" при остановке.
|
||||
|
||||
В отличие от чисто финансовых показателей, система делает взнос **ощутимым во времени**: активность чувствуется как движение вверх, бездействие — как постепенное угасание.
|
||||
|
||||
> **Человек растёт, пока проявляется. Благорост делает этот рост видимым.**
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Параметры конфигурации
|
||||
|
||||
Система управляется четырьмя основными параметрами, которые определяют баланс и динамику игры:
|
||||
|
||||
### `level_depth_base` - Базовый взнос для уровня 1
|
||||
**Значение:** 10,000 RUB (по умолчанию)
|
||||
**На что влияет:**
|
||||
- Определяет стоимость достижения первого уровня
|
||||
- Влияет на общую сложность прогрессии
|
||||
- Базовый взнос, от которого рассчитываются все последующие уровни
|
||||
|
||||
**Примеры:**
|
||||
- `10,000 RUB` → первый уровень требует 10,000 RUB взноса
|
||||
- `5,000 RUB` → упрощает вход в систему
|
||||
- `50,000 RUB` → усложняет вход в систему
|
||||
|
||||
### `level_growth_coefficient` - Коэффициент роста уровней
|
||||
**Значение:** 1.5 (по умолчанию)
|
||||
**На что влияет:**
|
||||
- Определяет, насколько быстрее растут требования к последующим уровням
|
||||
- Влияет на общую кривизну прогрессии
|
||||
|
||||
**Примеры:**
|
||||
- `1.0` → линейный рост (каждый уровень требует одинаково больше)
|
||||
- `1.5` → экспоненциальный рост (требования растут в 1.5 раза)
|
||||
- `2.0` → агрессивный рост (требования удваиваются каждый уровень)
|
||||
|
||||
### `energy_gain_coefficient` - Коэффициент прироста энергии
|
||||
**Значение:** 10.0 (по умолчанию)
|
||||
**На что влияет:**
|
||||
- Определяет, сколько энергии даёт каждый взнос
|
||||
- Влияет на скорость набора энергии и частоту переходов уровней
|
||||
|
||||
**Примеры:**
|
||||
- `5.0` → взносы дают меньше энергии (медленнее прогресс)
|
||||
- `10.0` → сбалансированный прирост энергии
|
||||
- `20.0` → взносы дают много энергии (быстрее прогресс)
|
||||
|
||||
### `energy_decay_rate_per_day` - Скорость снижения энергии
|
||||
**Значение:** 1.11% в день (по умолчанию)
|
||||
**На что влияет:**
|
||||
- Определяет, как быстро угасает энергия без активности
|
||||
- Влияет на требуемую регулярность участия
|
||||
|
||||
**Примеры:**
|
||||
- `0.5%` → энергия угасает медленно (200 дней до нуля)
|
||||
- `1.11%` → сбалансированное угасание (90 дней до нуля)
|
||||
- `2.0%` → энергия угасает быстро (50 дней до нуля)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Математическая модель
|
||||
|
||||
### Расчёт требований для уровня N
|
||||
```
|
||||
level_requirement(N) = level_depth_base × level_growth_coefficient^(N-1)
|
||||
```
|
||||
|
||||
**Примеры прогрессии** (при базовых настройках):
|
||||
- Уровень 1: 10,000 RUB
|
||||
- Уровень 2: 15,000 RUB
|
||||
- Уровень 3: 22,500 RUB
|
||||
- Уровень 4: 33,750 RUB
|
||||
- Уровень 5: 50,625 RUB
|
||||
- Уровень 10: ~383,437 RUB
|
||||
|
||||
### Расчёт прироста энергии от взноса
|
||||
```
|
||||
gain = (contribution_amount / level_requirement(current_level)) × energy_gain_coefficient
|
||||
```
|
||||
|
||||
**Пример:** взнос 1,000 RUB на уровне 1 даёт 1% энергии (при коэффициенте 10.0)
|
||||
|
||||
### Расчёт снижения энергии со временем
|
||||
```
|
||||
days_passed = (current_time - last_energy_update) / 86400
|
||||
decay = days_passed × energy_decay_rate_per_day
|
||||
new_energy = max(0, current_energy - decay)
|
||||
```
|
||||
|
||||
**Пример:** За 90 дней энергия снижается от 99.9% до 0% (при decay_rate = 1.11%)
|
||||
|
||||
### Переход на новый уровень (с поддержкой перескока)
|
||||
```
|
||||
if (energy >= 100.0) {
|
||||
levels_gained = floor(energy / 100.0) // Количество уровней для перехода
|
||||
level += levels_gained // Перескок через несколько уровней
|
||||
energy = energy % 100.0 // Остаток энергии (0.0-99.9)
|
||||
// Отправка уведомления о переходе уровня
|
||||
}
|
||||
```
|
||||
|
||||
**Примеры перескока:**
|
||||
- Энергия 250% → +2 уровня, остаток 50%
|
||||
- Энергия 500% → +5 уровней, остаток 0%
|
||||
- Энергия 99.9% → +0 уровней, остаток 99.9%
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Техническая реализация
|
||||
|
||||
### Структура данных участника
|
||||
```cpp
|
||||
struct contributor {
|
||||
// ... существующие поля ...
|
||||
|
||||
// Геймификация: уровень и энергия
|
||||
uint32_t level = 1; // Уровень участника (от 1 и выше, только растет)
|
||||
double energy = 99.9999999999; // Текущая энергия участника (0.0 - 100.0)
|
||||
time_point_sec last_energy_update; // Время последнего обновления энергии
|
||||
};
|
||||
```
|
||||
|
||||
### Функциональность
|
||||
|
||||
#### Расчетные функции
|
||||
- `calculate_level_requirement(level, config)` — требования для уровня
|
||||
- `calculate_energy_gain(amount, level, config)` — прирост энергии от взноса
|
||||
- `update_energy_with_decay(coopname, username)` — применение decay
|
||||
- `add_energy_and_check_levelup(coopname, username, gain)` — добавление энергии и проверка перехода
|
||||
- `update_gamification_from_segment(coopname, segment)` — обновление из сегмента
|
||||
|
||||
#### Действия контракта
|
||||
|
||||
**refreshcontr** — обновление энергии с decay
|
||||
```cpp
|
||||
void capital::refreshcontr(eosio::name coopname, eosio::name username);
|
||||
```
|
||||
- Авторизация: coopname
|
||||
- Назначение: периодическое обновление энергии участников
|
||||
|
||||
**lvlnotify** — уведомление о переходе уровня
|
||||
```cpp
|
||||
void capital::lvlnotify(eosio::name coopname, eosio::name username,
|
||||
uint32_t prev_level, uint32_t new_level);
|
||||
```
|
||||
- Авторизация: _capital (inline action)
|
||||
- Назначение: автоматическое уведомление при переходе уровня
|
||||
|
||||
### Интеграция с бизнес-процессами
|
||||
|
||||
#### Автоматическое обновление при взносах
|
||||
Приём результата (действие `signact2`):
|
||||
```
|
||||
signact2()
|
||||
→ update_contributor_ratings_from_segment()
|
||||
→ update_gamification_from_segment()
|
||||
→ add_energy_and_check_levelup()
|
||||
→ [если energy >= 100] lvlnotify()
|
||||
```
|
||||
|
||||
#### Начальное состояние участника
|
||||
При создании или импорте:
|
||||
- `level = 1`
|
||||
- `energy = 99.9999999999` (почти полная энергия для старта)
|
||||
- `last_energy_update = current_time_point()`
|
||||
|
||||
### Жизненный цикл энергии
|
||||
|
||||
```
|
||||
99.9999999999% (старт, уровень 1)
|
||||
↓
|
||||
[взносы добавляют энергию]
|
||||
↓
|
||||
100% → ПЕРЕХОД УРОВНЯ (+1 или больше) → остаток энергии (0.0-99.9%)
|
||||
↓
|
||||
[взносы добавляют энергию]
|
||||
↓
|
||||
[время уменьшает энергию (decay)]
|
||||
↓
|
||||
[цикл повторяется]
|
||||
```
|
||||
|
||||
**Пример большого взноса:**
|
||||
```
|
||||
Уровень 1, энергия 50%
|
||||
↓
|
||||
взнос даёт +250% энергии → всего 300%
|
||||
↓
|
||||
300% / 100 = 3 уровня → Уровень 4, остаток 0%
|
||||
```
|
||||
|
||||
### Сценарии использования
|
||||
|
||||
#### Активный участник
|
||||
- День 1: Регистрация, energy=99.9999999999%
|
||||
- День 5: взнос 5,000 RUB → energy=99.9999999999% + 5% = 100% → Уровень 2
|
||||
- День 10: взнос 10,000 RUB → energy=0% + 6.67% = 6.67%
|
||||
|
||||
#### Неактивный участник
|
||||
- День 1: Регистрация, energy=99.9999999999%
|
||||
- День 30: Нет взносов, energy=99.9999999999% - 33.3% = 66.6%
|
||||
- День 90: Нет взносов, energy=0%
|
||||
- День 95: взнос 10,000 RUB → energy=0% + 10% = 10%
|
||||
|
||||
### Настройка системы
|
||||
|
||||
#### Быстрая прогрессия
|
||||
```cpp
|
||||
energy_decay_rate_per_day = 2.0; // Снижение за 50 дней
|
||||
level_growth_coefficient = 1.3; // Пологая прогрессия
|
||||
energy_gain_coefficient = 15.0; // Быстрый прирост энергии
|
||||
```
|
||||
|
||||
#### Долгосрочная мотивация
|
||||
```cpp
|
||||
energy_decay_rate_per_day = 0.5; // Снижение за 200 дней
|
||||
level_growth_coefficient = 2.0; // Крутая прогрессия
|
||||
energy_gain_coefficient = 5.0; // Медленный прирост энергии
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Эксплуатация системы
|
||||
|
||||
### Обязательные действия контроллера
|
||||
|
||||
#### 1. Планировщик decay (критично)
|
||||
```typescript
|
||||
cron.schedule('0 0 * * *', async () => {
|
||||
const contributors = await getActiveContributors();
|
||||
for (const contributor of contributors) {
|
||||
await contract.refreshcontr(coopname, contributor.username);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. Обработчик уведомлений (важно)
|
||||
```typescript
|
||||
contract.on('lvlnotify', async ({ coopname, username, prev_level, new_level }) => {
|
||||
await notificationService.send(username, {
|
||||
title: '🎉 Поздравляем!',
|
||||
message: `Вы достигли ${new_level} уровня!`,
|
||||
data: { prev_level, new_level }
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Рекомендуемые действия
|
||||
|
||||
#### Интерфейс
|
||||
Отображать:
|
||||
- Текущий уровень и энергию
|
||||
- Прогресс до следующего уровня
|
||||
- Требования для следующего уровня
|
||||
- Историю достижений
|
||||
|
||||
#### Балансировка
|
||||
Периодически анализировать статистику переходов уровней и корректировать параметры конфигурации для достижения желаемой динамики роста.
|
||||
|
||||
### Валидация параметров
|
||||
- `energy_decay_rate_per_day`: 0.0 - 100.0
|
||||
- `level_depth_base`: > 0
|
||||
- `level_growth_coefficient`: 1.0 - 3.0
|
||||
- `energy_gain_coefficient`: 0.0 - 100.0
|
||||
@@ -0,0 +1,160 @@
|
||||
# Реализация системы аппрувов мастера проекта
|
||||
|
||||
## Обзор
|
||||
|
||||
Реализована переиспользуемая система одобрений (аппрувов) для контракта Capital, которая позволяет мастеру проекта одобрять или отклонять коммиты через единообразный интерфейс.
|
||||
|
||||
## Архитектурные изменения
|
||||
|
||||
### 1. Переиспользуемая базовая структура (`lib/shared_approver.hpp`)
|
||||
|
||||
Создана базовая структура `approval_base`, которая не привязана к конкретному контракту и может быть переиспользована:
|
||||
|
||||
```cpp
|
||||
struct approval_base {
|
||||
uint64_t id;
|
||||
eosio::name coopname;
|
||||
eosio::name username;
|
||||
eosio::name type;
|
||||
document2 document;
|
||||
checksum256 approval_hash;
|
||||
eosio::name callback_contract;
|
||||
eosio::name callback_action_approve;
|
||||
eosio::name callback_action_decline;
|
||||
std::string meta;
|
||||
eosio::time_point_sec created_at;
|
||||
// ... индексы
|
||||
};
|
||||
```
|
||||
|
||||
Добавлены макросы для создания специализированных версий:
|
||||
- `DEFINE_APPROVAL_STRUCT(name, contract)` - создает структуру для конкретного контракта
|
||||
- `DEFINE_APPROVALS_INDEX(index_name, struct_name)` - создает мультииндекс
|
||||
|
||||
### 2. Библиотека для Capital (`lib/shared_capital.hpp`)
|
||||
|
||||
Создан новый файл с:
|
||||
- Сигнатурами действий для аппрувов мастера
|
||||
- Namespace `Master` с таблицей аппрувов для контракта Capital
|
||||
- Функцией `Master::create_approval()` для создания аппрувов
|
||||
|
||||
### 3. Действия в контракте Capital
|
||||
|
||||
Добавлены три новых действия в `capital.hpp`:
|
||||
|
||||
#### `createapprv` - Создание аппрува
|
||||
- **Авторизация**: `_capital`
|
||||
- Создает запись в таблице аппрувов мастера
|
||||
- Используется внутренне при создании коммита
|
||||
|
||||
#### `confirmapprv` - Подтверждение аппрува
|
||||
- **Авторизация**: `coopname`
|
||||
- Находит аппрув по хешу
|
||||
- Вызывает колбэк одобрения
|
||||
- Удаляет аппрув из таблицы
|
||||
|
||||
#### `declineapprv` - Отклонение аппрува
|
||||
- **Авторизация**: `coopname`
|
||||
- Находит аппрув по хешу
|
||||
- Вызывает колбэк отклонения
|
||||
- Удаляет аппрув из таблицы
|
||||
|
||||
### 4. Интеграция с процессом создания коммитов
|
||||
|
||||
#### Изменения в `createcmmt`
|
||||
Теперь при создании коммита:
|
||||
1. Создается запись коммита в таблице
|
||||
2. Создается аппрув через `Master::create_approval()` с:
|
||||
- `type` = "commit"
|
||||
- `approval_hash` = `commit_hash`
|
||||
- `callback_action_approve` = "approvecmmt"
|
||||
- `callback_action_decline` = "declinecmmt"
|
||||
|
||||
#### Изменения в `approvecmmt`
|
||||
Теперь работает как колбэк:
|
||||
- **Авторизация**: `_capital` (вместо `coopname`)
|
||||
- **Сигнатура**: `(coopname, commit_hash, approved_document)`
|
||||
- Вызывается автоматически из `confirmapprv`
|
||||
- Убрана проверка мастера (она выполняется на уровне фронтенда)
|
||||
|
||||
#### Изменения в `declinecmmt`
|
||||
Теперь работает как колбэк:
|
||||
- **Авторизация**: `_capital` (вместо `coopname`)
|
||||
- **Сигнатура**: `(coopname, commit_hash, reason)`
|
||||
- Вызывается автоматически из `declineapprv`
|
||||
- Убрана проверка мастера (она выполняется на уровне фронтенда)
|
||||
|
||||
## Структура файлов
|
||||
|
||||
```
|
||||
contracts/cpp/
|
||||
├── lib/
|
||||
│ ├── shared_approver.hpp # Переиспользуемая базовая структура
|
||||
│ ├── shared_capital.hpp # NEW: Методы для аппрувов мастера
|
||||
│ └── common.hpp # Обновлен: добавлен include shared_capital.hpp
|
||||
└── capital/
|
||||
├── capital.hpp # Обновлен: добавлены действия createapprv/confirmapprv/declineapprv
|
||||
├── capital.cpp # Обновлен: добавлены includes для master_approval
|
||||
└── app/
|
||||
├── master_approval/ # NEW: Директория с аппрувами мастера
|
||||
│ ├── createapprv.cpp
|
||||
│ ├── confirmapprv.cpp
|
||||
│ ├── declineapprv.cpp
|
||||
│ └── master-approval-process.dox # Документация процесса
|
||||
└── generation/
|
||||
└── create_commit/
|
||||
├── createcmmt.cpp # Обновлен: добавлено создание аппрува
|
||||
├── approvecmmt.cpp # Обновлен: теперь колбэк
|
||||
├── declinecmmt.cpp # Обновлен: теперь колбэк
|
||||
└── commit-creation-process.dox # Обновлена документация
|
||||
```
|
||||
|
||||
## Процесс работы
|
||||
|
||||
### Создание коммита
|
||||
```
|
||||
Исполнитель → createcmmt → Создание коммита → Master::create_approval() → Аппрув в таблице
|
||||
```
|
||||
|
||||
### Одобрение коммита
|
||||
```
|
||||
Мастер → confirmapprv → Поиск аппрува → Колбэк approvecmmt → Обработка коммита → Удаление аппрува
|
||||
```
|
||||
|
||||
### Отклонение коммита
|
||||
```
|
||||
Мастер → declineapprv → Поиск аппрува → Колбэк declinecmmt → Удаление коммита → Удаление аппрува
|
||||
```
|
||||
|
||||
## Преимущества реализации
|
||||
|
||||
1. **Переиспользуемость**: Базовая структура `approval_base` может быть использована в любых других контрактах
|
||||
2. **Единообразие**: Аппрувы председателя и мастера используют одинаковую структуру и логику
|
||||
3. **Разделение ответственности**: Таблицы аппрувов разделены (soviet и capital), данные не перемешиваются
|
||||
4. **Гибкость**: Легко добавить новые типы аппрувов в будущем
|
||||
5. **Колбэки**: Унифицированный механизм обратных вызовов при одобрении/отклонении
|
||||
|
||||
## Дальнейшее использование
|
||||
|
||||
Для создания аналогичной системы в других контрактах:
|
||||
|
||||
1. Определить сигнатуры действий в `lib/shared_<contract>.hpp`
|
||||
2. Использовать макросы `DEFINE_APPROVAL_STRUCT` и `DEFINE_APPROVALS_INDEX`
|
||||
3. Реализовать действия `createapprv`, `confirmapprv`, `declineapprv`
|
||||
4. Создать функцию `create_approval()` в namespace контракта
|
||||
5. Интегрировать с бизнес-процессами через колбэки
|
||||
|
||||
## Миграция данных
|
||||
|
||||
Не требуется - это новая функциональность, существующие данные не затронуты.
|
||||
|
||||
## Тестирование
|
||||
|
||||
Рекомендуется протестировать:
|
||||
1. Создание коммита и аппрува
|
||||
2. Одобрение коммита мастером
|
||||
3. Отклонение коммита мастером
|
||||
4. Проверку прав доступа (только кооператив может подтверждать/отклонять)
|
||||
5. Корректность колбэков
|
||||
6. Удаление аппрувов после обработки
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Чистая фрактальная трехуровневая архитектура срезов домена (domain) и инфраструктуры (infrastructure), оркестрируемая приложением (application). Фрактал раскрывается в расширениях (extensions), каждое из которых также выстраивается по правилам чистой трехуровневой архитектуры срезов домена и инфраструктуры с окрестрацией на уровне своего приложения.
|
||||
|
||||
Общая архитектура связей:
|
||||
- AppResolver -> AppService[DTO<->Domain] -> AppInteractor -> DomainService -> DomainPort <- InfraAdapter -> [при необходимости] AnyAppOrDomainService
|
||||
|
||||
Все сервисы внедряются через инъекцию (DI) с указанием символа реализации. Импорты разрешены только вглубь - к домену. Из домена переход разрешен только в инфраструктуру через порт и его адаптер.
|
||||
|
||||
Прямые импорты без DI запрещены. Импорты из соседних срезов того же архитектурного уровня запрещены. Направление зависимостей от приложения - к домену, от домена через порт в его инфраструктурный адаптер.
|
||||
|
||||
Взаимодействие расширений с основным приложением осуществляется только через порты домена основного приложения.
|
||||
|
||||
Глобальные модули запрещены. В редких случаях сложных циклических зависимостей на период рефакторинга разрешается обоснованно использовать forwardRef.
|
||||
|
||||
Уровень приложения оркестрирует вызовы доменов через порты в инфраструктуру. Домены не обращаются друг к друга. Срезы уровня приложений не обращаются друг к другу, а только вызывают порты домена.
|
||||
@@ -0,0 +1,289 @@
|
||||
# Система синхронизации блокчейн-данных Capital
|
||||
|
||||
Данная система обеспечивает автоматическую синхронизацию данных между блокчейном EOSIO и локальной базой данных PostgreSQL для расширения Capital.
|
||||
|
||||
## Архитектура
|
||||
|
||||
### Основные компоненты
|
||||
|
||||
1. **AbstractEntitySyncService** - базовый класс для синхронизации сущностей
|
||||
2. **ProjectSyncService** - конкретная реализация синхронизации проектов
|
||||
3. **SegmentSyncService** - синхронизация сегментов участников
|
||||
4. **CapitalSyncInteractor** - координирует синхронизацию всех сущностей
|
||||
|
||||
### Принципы работы
|
||||
|
||||
1. **Отслеживание блоков**: Каждая сущность хранит номер блока последнего обновления (`block_num`)
|
||||
2. **Дельта-синхронизация**: При получении дельты из блокчейна происходит обновление соответствующей сущности
|
||||
3. **Обработка форков**: При форке удаляются все данные после указанного блока
|
||||
4. **Создание при отсутствии**: Если сущности нет в базе, она создается из блокчейн-данных
|
||||
|
||||
## Поток данных
|
||||
|
||||
```
|
||||
Блокчейн → BlockchainConsumerService → [ProjectSyncService|SegmentSyncService|...] → База данных
|
||||
```
|
||||
|
||||
### События
|
||||
|
||||
- `delta::capital::*` - все дельты capital контракта (от BlockchainConsumerService)
|
||||
- `fork::*` - события форка для всех контрактов (от BlockchainConsumerService)
|
||||
|
||||
## Использование
|
||||
|
||||
### Добавление новой сущности для синхронизации
|
||||
|
||||
1. **Создайте доменную сущность** с реализацией `IBlockchainSynchronizable`:
|
||||
```typescript
|
||||
export class NewEntityDomain implements IBlockchainSynchronizable {
|
||||
// Статические ключи для синхронизации
|
||||
private static primary_key = 'id'; // Ключ для поиска в блокчейне
|
||||
private static sync_key = 'custom_field'; // Ключ для синхронизации с БД
|
||||
|
||||
getPrimaryKey(): string { return this.id.toString(); }
|
||||
getSyncKey(): string { return this.custom_field; }
|
||||
getBlockNum(): number | null { return this.block_num; }
|
||||
updateFromBlockchain(data: any, blockNum: number): void { /* логика обновления */ }
|
||||
|
||||
static getSyncKey(): string { return NewEntityDomain.sync_key; }
|
||||
}
|
||||
```
|
||||
|
||||
2. **Обновите TypeORM сущность** добавив поле `block_num`:
|
||||
```typescript
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
blockNum?: number;
|
||||
```
|
||||
|
||||
3. **Создайте маппер дельт**:
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class NewEntityDeltaMapper implements IBlockchainDeltaMapper<INewEntityBlockchainData> {
|
||||
extractSyncValue(delta: IDelta): string {
|
||||
// Извлекаем значение из дельты для синхронизации
|
||||
return delta.primary_key.toString();
|
||||
}
|
||||
|
||||
extractSyncKey(): string {
|
||||
// Возвращаем ключ синхронизации из доменной сущности
|
||||
return NewEntityDomain.getSyncKey();
|
||||
}
|
||||
|
||||
// остальные методы
|
||||
}
|
||||
```
|
||||
|
||||
4. **Создайте сервис синхронизации**:
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class NewEntitySyncService
|
||||
extends AbstractEntitySyncService<NewEntityDomain, INewEntityBlockchainData>
|
||||
implements OnModuleInit
|
||||
{
|
||||
protected readonly entityName = 'NewEntity';
|
||||
|
||||
constructor(
|
||||
@Inject(NEW_ENTITY_REPOSITORY)
|
||||
newEntityRepository: NewEntityRepository,
|
||||
newEntityDeltaMapper: NewEntityDeltaMapper,
|
||||
logger: WinstonLoggerService,
|
||||
private readonly eventEmitter: EventEmitter2
|
||||
) {
|
||||
super(newEntityRepository, newEntityDeltaMapper, logger);
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
const supportedVersions = this.getSupportedVersions();
|
||||
this.logger.debug(
|
||||
`Сервис синхронизации новой сущности инициализирован. Поддерживаемые контракты: [${supportedVersions.contracts.join(
|
||||
', '
|
||||
)}], таблицы: [${supportedVersions.tables.join(', ')}]`
|
||||
);
|
||||
|
||||
// Программная подписка на все поддерживаемые паттерны событий
|
||||
const allPatterns = this.getAllEventPatterns();
|
||||
this.logger.debug(`Подписка на ${allPatterns.length} паттернов событий: ${allPatterns.join(', ')}`);
|
||||
|
||||
// Подписываемся на каждый паттерн программно
|
||||
allPatterns.forEach((pattern) => {
|
||||
this.eventEmitter.on(pattern, this.processDelta.bind(this));
|
||||
});
|
||||
|
||||
this.logger.debug('Сервис синхронизации новой сущности полностью инициализирован с подписками на паттерны');
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка форков для новой сущности
|
||||
* Теперь подписывается на все форки независимо от контракта
|
||||
*/
|
||||
@OnEvent('fork::*')
|
||||
async handleNewEntityFork(forkData: { block_num: number }): Promise<void> {
|
||||
await this.handleFork(forkData.block_num);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **Добавьте компоненты в CapitalContractInfoService**:
|
||||
```typescript
|
||||
// В capital-contract-info.service.ts добавьте паттерн для новой таблицы
|
||||
private readonly tablePatterns: Record<string, string[]> = {
|
||||
// ...
|
||||
newentities: ['newentities', 'newentities*'], // Для сущностей ≤ 12 символов
|
||||
// или для сущностей = 12 символов:
|
||||
// newentities12: ['newentities12', 'newentities1*'],
|
||||
};
|
||||
```
|
||||
|
||||
6. **Зарегистрируйте компоненты в модулях**:
|
||||
|
||||
**В capital-database.module.ts**:
|
||||
```typescript
|
||||
import { NewEntityTypeormEntity } from '../entities/new-entity.typeorm-entity';
|
||||
|
||||
// Добавьте в entities массив:
|
||||
entities: [
|
||||
// ...
|
||||
NewEntityTypeormEntity,
|
||||
EntityVersionTypeormEntity,
|
||||
],
|
||||
|
||||
// Добавьте в TypeOrmModule.forFeature:
|
||||
TypeOrmModule.forFeature([
|
||||
// ...
|
||||
NewEntityTypeormEntity,
|
||||
EntityVersionTypeormEntity,
|
||||
], CAPITAL_DATABASE_CONNECTION)
|
||||
```
|
||||
|
||||
**В capital-extension.module.ts**:
|
||||
```typescript
|
||||
import { NewEntityTypeormRepository } from './infrastructure/repositories/new-entity.typeorm-repository';
|
||||
import { NewEntityDeltaMapper } from './infrastructure/blockchain/mappers/new-entity-delta.mapper';
|
||||
import { NewEntitySyncService } from './infrastructure/blockchain/services/new-entity-sync.service';
|
||||
import { NEW_ENTITY_REPOSITORY } from './domain/repositories/new-entity.repository';
|
||||
|
||||
// Добавьте в providers:
|
||||
providers: [
|
||||
// ...
|
||||
// Repositories
|
||||
{
|
||||
provide: NEW_ENTITY_REPOSITORY,
|
||||
useClass: NewEntityTypeormRepository,
|
||||
},
|
||||
|
||||
// Blockchain Sync Services
|
||||
NewEntityDeltaMapper,
|
||||
NewEntitySyncService,
|
||||
|
||||
// ... остальные провайдеры
|
||||
],
|
||||
```
|
||||
|
||||
### Обработка форков
|
||||
|
||||
Форки обрабатываются автоматически через события `fork::*` (независимо от контракта). Все синхронизируемые сущности удаляют данные после указанного блока и ждут новых дельт для пересинхронизации.
|
||||
|
||||
Каждый сервис синхронизации должен иметь обработчик:
|
||||
```typescript
|
||||
@OnEvent('fork::*')
|
||||
async handleEntityFork(forkData: { block_num: number }): Promise<void> {
|
||||
await this.handleFork(forkData.block_num);
|
||||
}
|
||||
```
|
||||
|
||||
### Мониторинг
|
||||
|
||||
Используйте `CapitalSyncInteractor` для получения статистики:
|
||||
|
||||
```typescript
|
||||
const stats = await capitalSyncInteractor.getSyncStatistics();
|
||||
const health = await capitalSyncInteractor.checkSyncHealth();
|
||||
```
|
||||
|
||||
## Важные детали
|
||||
|
||||
### Документы в блокчейне
|
||||
|
||||
В блокчейне Capital контракта некоторые поля содержат подписанные документы. Эти документы должны быть правильно распарсены при получении дельт:
|
||||
|
||||
**Поля документов:**
|
||||
- `statement` - выписка/заявление (в results, expenses, debts, invests)
|
||||
- `approved_statement` - одобренное заявление (в expenses, debts)
|
||||
- `authorization` - авторизация (в results, expenses, debts)
|
||||
- `act` - акт (в results)
|
||||
- `contract` - контракт (в contributors)
|
||||
|
||||
**Сегменты (segments):**
|
||||
Таблица `segments` содержит информацию о вкладах участников в проекты капитализации. Особенности:
|
||||
- Составной ключ синхронизации: `project_hash + username`
|
||||
- Содержит роли участника (author, creator, coordinator, etc.)
|
||||
- Финансовые данные: инвестиции, бонусы, премии
|
||||
- CRPS поля для распределения наград
|
||||
- Статусы: generation, ready, contributed, accepted, completed
|
||||
|
||||
**Парсинг документов:**
|
||||
```typescript
|
||||
// В дельта-маппере для каждой сущности
|
||||
if (value.statement) {
|
||||
value.statement = DomainToBlockchainUtils.convertChainDocumentToDomainFormat(value.statement);
|
||||
}
|
||||
if (value.authorization) {
|
||||
value.authorization = DomainToBlockchainUtils.convertChainDocumentToDomainFormat(value.authorization);
|
||||
}
|
||||
// ... остальные документы
|
||||
```
|
||||
|
||||
**Типы в TypeORM сущностях:**
|
||||
```typescript
|
||||
@Column({ type: 'json' })
|
||||
statement!: ISignedDocumentDomainInterface;
|
||||
|
||||
@Column({ type: 'json' })
|
||||
approved_statement!: ISignedDocumentDomainInterface;
|
||||
```
|
||||
|
||||
### Кастомные ключи синхронизации
|
||||
|
||||
Система поддерживает гибкую настройку ключей для синхронизации:
|
||||
|
||||
**Primary Key** - ключ для поиска сущности в блокчейне:
|
||||
- Определяется в доменной сущности как `primary_key`
|
||||
- Обычно это поле `id` из таблицы блокчейна
|
||||
|
||||
**Sync Key** - ключ для синхронизации между блокчейном и БД:
|
||||
- Определяется в доменной сущности как `sync_key`
|
||||
- Может быть любое уникальное поле (например, `project_hash`)
|
||||
- Используется для поиска существующих записей в БД при синхронизации
|
||||
|
||||
Пример для проектов:
|
||||
```typescript
|
||||
export class ProjectDomainEntity {
|
||||
private static primary_key = 'id'; // Поиск в блокчейне по id
|
||||
private static sync_key = 'project_hash'; // Синхронизация по project_hash
|
||||
}
|
||||
```
|
||||
|
||||
### Номера блоков
|
||||
|
||||
- `block_num = null` - сущность создана до синхронизации с блокчейном
|
||||
- `block_num > 0` - номер блока последнего обновления из блокчейна
|
||||
|
||||
### Обработка устаревших обновлений
|
||||
|
||||
Система автоматически игнорирует обновления из старых блоков, предотвращая откат данных при получении дельт не в порядке.
|
||||
|
||||
### Производительность
|
||||
|
||||
- Дельты обрабатываются асинхронно
|
||||
- Форки обрабатываются в фоне
|
||||
- Статистика кешируется для быстрого доступа
|
||||
|
||||
## Расширение системы
|
||||
|
||||
Для добавления новых контрактов создайте аналогичную структуру:
|
||||
1. Свой `DeltaTrackerService` для контракта
|
||||
2. Маппер дельт для каждой таблицы
|
||||
3. Сервис синхронизации для каждой сущности
|
||||
4. Интерактор для координации
|
||||
|
||||
Система спроектирована для масштабирования и легкого добавления новых сущностей.
|
||||
@@ -0,0 +1,239 @@
|
||||
# Фабрика отслеживания решений (Decision Tracking Factory)
|
||||
|
||||
## Назначение
|
||||
|
||||
Фабрика отслеживания решений — это переиспользуемый сервис для автоматического обновления системных переменных (vars) при принятии решений советом или общим собранием пайщиков.
|
||||
|
||||
## Архитектура
|
||||
|
||||
Система построена согласно чистой архитектуре:
|
||||
|
||||
```
|
||||
domain/decision-tracking/ # Домен
|
||||
├── interfaces/ # Интерфейсы
|
||||
│ └── tracking-rule-domain.interface.ts
|
||||
├── ports/ # Порты
|
||||
│ └── decision-tracking.port.ts
|
||||
├── events/ # События
|
||||
│ └── decision-tracked.event.ts
|
||||
└── index.ts
|
||||
|
||||
infrastructure/decision-tracking/ # Инфраструктура
|
||||
├── adapters/ # Адаптеры
|
||||
│ └── decision-tracking.adapter.ts
|
||||
├── repositories/ # Репозитории
|
||||
│ └── tracking-rule.repository.ts
|
||||
└── decision-tracking-infrastructure.module.ts
|
||||
```
|
||||
|
||||
## Принцип работы
|
||||
|
||||
### 1. Регистрация правила отслеживания
|
||||
|
||||
Расширение регистрирует правило отслеживания при создании решения:
|
||||
|
||||
```typescript
|
||||
await decisionTrackingPort.registerTrackingRule({
|
||||
hash: documentHash,
|
||||
event_type: DecisionEventType.SOVIET_DECISION, // или MEET_DECISION
|
||||
vars_field: 'wallet_agreement',
|
||||
metadata: {
|
||||
onboarding_step: 'wallet_agreement',
|
||||
custom_data: '...'
|
||||
},
|
||||
expires_at: new Date('2025-02-15')
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Автоматическое отслеживание
|
||||
|
||||
Фабрика автоматически подписывается на события блокчейна:
|
||||
- `action::soviet::newresolved` - решения совета
|
||||
- `action::meet::newresolved` - решения общих собраний
|
||||
- `action::meet::restartmeet` - перезапуск общих собраний
|
||||
|
||||
### 3. Обработка совпадений
|
||||
|
||||
При получении события с подходящим hash фабрика:
|
||||
1. Находит соответствующее правило отслеживания
|
||||
2. Обновляет поле в vars (если указаны decision_id и decision_date)
|
||||
3. Деактивирует правило
|
||||
4. Эмитит событие `decision.tracked`
|
||||
|
||||
### 4. Пост-обработка через события
|
||||
|
||||
Расширения могут подписаться на событие `decision.tracked` для дополнительной обработки:
|
||||
|
||||
```typescript
|
||||
@OnEvent(DecisionTrackedEvent.eventName)
|
||||
async handleDecisionTracked(event: DecisionTrackedEvent): Promise<void> {
|
||||
const { result } = event;
|
||||
|
||||
// Выполнить дополнительные действия
|
||||
if (result.metadata?.onboarding_step) {
|
||||
await this.updateOnboardingProgress(result.metadata.onboarding_step);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Использование
|
||||
|
||||
### В расширениях
|
||||
|
||||
1. Импортировать модуль инфраструктуры:
|
||||
|
||||
```typescript
|
||||
@Module({
|
||||
imports: [
|
||||
DecisionTrackingInfrastructureModule,
|
||||
// ...
|
||||
],
|
||||
})
|
||||
export class MyExtensionModule {}
|
||||
```
|
||||
|
||||
2. Инжектировать порт:
|
||||
|
||||
```typescript
|
||||
constructor(
|
||||
@Inject(DECISION_TRACKING_PORT)
|
||||
private readonly decisionTrackingPort: DecisionTrackingPort
|
||||
) {}
|
||||
```
|
||||
|
||||
3. Регистрировать правила при создании решений:
|
||||
|
||||
```typescript
|
||||
await this.decisionTrackingPort.registerTrackingRule({
|
||||
hash: documentHash,
|
||||
event_type: DecisionEventType.SOVIET_DECISION,
|
||||
vars_field: 'my_agreement',
|
||||
metadata: { /* custom data */ }
|
||||
});
|
||||
```
|
||||
|
||||
4. Подписаться на события отслеживания (опционально):
|
||||
|
||||
```typescript
|
||||
@OnEvent(DecisionTrackedEvent.eventName)
|
||||
async handleTracked(event: DecisionTrackedEvent) {
|
||||
// Пост-обработка
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### DecisionTrackingPort
|
||||
|
||||
#### registerTrackingRule(input)
|
||||
Регистрирует новое правило отслеживания.
|
||||
|
||||
**Параметры:**
|
||||
- `hash` - Hash документа для отслеживания
|
||||
- `event_type` - Тип события (SOVIET_DECISION или MEET_DECISION)
|
||||
- `vars_field` - Ключ в vars для обновления
|
||||
- `metadata` - Метаданные для пост-обработки
|
||||
- `expires_at` - Дата истечения правила (опционально)
|
||||
|
||||
**Возвращает:** `TrackingRuleDomainInterface`
|
||||
|
||||
#### updateTrackingRuleHash(oldHash, newHash)
|
||||
Обновляет hash в существующем правиле (используется при перезапуске общих собраний).
|
||||
|
||||
#### getActiveRules()
|
||||
Получает все активные правила отслеживания.
|
||||
|
||||
#### getRuleByHash(hash)
|
||||
Получает правило по hash документа.
|
||||
|
||||
#### deactivateRule(id)
|
||||
Деактивирует правило отслеживания.
|
||||
|
||||
#### deleteRule(id)
|
||||
Удаляет правило отслеживания.
|
||||
|
||||
|
||||
## События
|
||||
|
||||
### DecisionTrackedEvent
|
||||
|
||||
Эмитится при успешном отслеживании и обработке решения.
|
||||
|
||||
**Данные события:**
|
||||
```typescript
|
||||
{
|
||||
matched: true,
|
||||
rule_id: 'uuid',
|
||||
hash: 'document_hash',
|
||||
event_type: DecisionEventType.SOVIET_DECISION,
|
||||
vars_field: 'wallet_agreement',
|
||||
decision_id: '123',
|
||||
decision_date: '2025-01-17T12:00:00Z',
|
||||
metadata: { /* custom data */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Особенности
|
||||
|
||||
### Общие собрания
|
||||
Для общих собраний поддерживается автоматическое обновление hash при перезапуске через событие `restartmeet`.
|
||||
|
||||
### Управление правилами
|
||||
Расширения сами управляют своими правилами отслеживания. Если нужно отключить отслеживание, расширение должно вызвать `deactivateRule()` или `deleteRule()`. Фабрика не имеет встроенной логики истечения правил.
|
||||
|
||||
### TypeORM хранилище
|
||||
Используется TypeORM репозиторий с PostgreSQL для персистентного хранения правил отслеживания. Правила сохраняются в таблице `tracking_rules`.
|
||||
|
||||
## Пример: Онбординг председателя
|
||||
|
||||
```typescript
|
||||
// Регистрация отслеживания решения
|
||||
const document = await this.generateDecisionDocument(...);
|
||||
|
||||
await this.decisionTrackingPort.registerTrackingRule({
|
||||
hash: document.hash,
|
||||
event_type: DecisionEventType.SOVIET_DECISION,
|
||||
vars_field: 'wallet_agreement',
|
||||
metadata: {
|
||||
onboarding_step: 'wallet_agreement',
|
||||
},
|
||||
expires_at: onboardingExpireDate,
|
||||
});
|
||||
|
||||
// Подписка на событие для обновления статуса онбординга
|
||||
@OnEvent(DecisionTrackedEvent.eventName)
|
||||
async handleDecisionTracked(event: DecisionTrackedEvent) {
|
||||
if (event.result.metadata?.onboarding_step) {
|
||||
await this.markOnboardingStepComplete(
|
||||
event.result.metadata.onboarding_step
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Миграции базы данных
|
||||
|
||||
При первом запуске будет автоматически создана таблица `tracking_rules`:
|
||||
|
||||
```sql
|
||||
CREATE TABLE tracking_rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
event_type VARCHAR(20) NOT NULL,
|
||||
vars_field VARCHAR(50) NOT NULL,
|
||||
metadata JSONB,
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tracking_rules_hash ON tracking_rules(hash);
|
||||
```
|
||||
|
||||
## Преимущества
|
||||
|
||||
1. **Переиспользуемость** - Единая логика отслеживания для всех расширений
|
||||
2. **Разделение ответственности** - Фабрика отвечает только за отслеживание и обновление vars
|
||||
3. **Расширяемость** - Пост-обработка через события не нарушает DI
|
||||
4. **Автоматизация** - Отслеживание происходит автоматически без ручного вмешательства
|
||||
5. **Чистая архитектура** - Соблюдение принципов разделения на домен и инфраструктуру
|
||||
@@ -0,0 +1,958 @@
|
||||
# Руководство по добавлению новой сущности синхронизации
|
||||
|
||||
## Обзор процесса
|
||||
|
||||
Это руководство описывает полный процесс добавления новой сущности для синхронизации с блокчейном. **Всегда следуйте этому порядку создания файлов и проверяйте типы после каждого шага.**
|
||||
|
||||
### 📋 Шаблон на примере сущности "Expense" (Расход)
|
||||
|
||||
Пример демонстрирует добавление сущности **расход** (`expense`) для синхронизации с таблицей `expenses` контракта Capital.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Шаг 1: Доменный уровень (Domain Layer)
|
||||
|
||||
### 1.1 Создание доменной сущности
|
||||
|
||||
**Файл:** `domain/entities/expense.entity.ts`
|
||||
|
||||
```typescript
|
||||
import { ExpenseStatus } from '../enums/expense-status.enum';
|
||||
import type { IExpenseDatabaseData } from '../interfaces/expense-database.interface';
|
||||
import type { IExpenseBlockchainData } from '../interfaces/expense-blockchain.interface';
|
||||
import type { IBlockchainSynchronizable } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
/**
|
||||
* Доменная сущность расхода
|
||||
*
|
||||
* Полностью агрегирует данные из двух источников:
|
||||
* - База данных: внутренний ID, ссылка на блокчейн
|
||||
* - Блокчейн: все данные расхода из таблицы expenses
|
||||
*/
|
||||
export class ExpenseDomainEntity implements IBlockchainSynchronizable {
|
||||
// Статические ключи для синхронизации
|
||||
private static primary_key = 'id'; // Ключ для поиска в блокчейне
|
||||
private static sync_key = 'expense_hash'; // Ключ для синхронизации с БД
|
||||
|
||||
// Поля из базы данных
|
||||
public _id: string; // Внутренний ID базы данных
|
||||
public id?: number; // ID в блокчейне
|
||||
public block_num: number | undefined; // Номер блока последнего обновления
|
||||
public present = false; // Существует ли запись в блокчейне
|
||||
|
||||
// Доменные поля (расширения)
|
||||
public status: ExpenseStatus;
|
||||
|
||||
// Поля из блокчейна (expenses.hpp)
|
||||
public coopname: IExpenseBlockchainData['coopname'];
|
||||
public username: IExpenseBlockchainData['username'];
|
||||
public project_hash: IExpenseBlockchainData['project_hash'];
|
||||
public expense_hash: IExpenseBlockchainData['expense_hash'];
|
||||
public fund_id: IExpenseBlockchainData['fund_id'];
|
||||
public blockchainStatus: IExpenseBlockchainData['status']; // Статус из блокчейна
|
||||
public amount: IExpenseBlockchainData['amount'];
|
||||
public description: IExpenseBlockchainData['description'];
|
||||
public expense_statement: ISignedDocumentDomainInterface;
|
||||
public approved_statement: ISignedDocumentDomainInterface;
|
||||
public authorization: ISignedDocumentDomainInterface;
|
||||
public spended_at: IExpenseBlockchainData['spended_at'];
|
||||
|
||||
constructor(databaseData: IExpenseDatabaseData, blockchainData: IExpenseBlockchainData) {
|
||||
// Данные из базы данных
|
||||
this._id = databaseData._id;
|
||||
this.id = blockchainData.id;
|
||||
this.block_num = databaseData.block_num;
|
||||
|
||||
// Данные из блокчейна
|
||||
this.coopname = blockchainData.coopname;
|
||||
this.username = blockchainData.username;
|
||||
this.project_hash = blockchainData.project_hash;
|
||||
this.expense_hash = blockchainData.expense_hash;
|
||||
this.fund_id = blockchainData.fund_id;
|
||||
this.blockchainStatus = blockchainData.status;
|
||||
this.amount = blockchainData.amount;
|
||||
this.description = blockchainData.description;
|
||||
this.expense_statement = blockchainData.expense_statement;
|
||||
this.approved_statement = blockchainData.approved_statement;
|
||||
this.authorization = blockchainData.authorization;
|
||||
this.spended_at = blockchainData.spended_at;
|
||||
|
||||
// Синхронизация статуса с блокчейн данными
|
||||
this.status = this.mapBlockchainStatusToDomain(blockchainData.status);
|
||||
}
|
||||
|
||||
// Статические методы для получения ключей
|
||||
public static getPrimaryKey(): string {
|
||||
return ExpenseDomainEntity.primary_key;
|
||||
}
|
||||
|
||||
public static getSyncKey(): string {
|
||||
return ExpenseDomainEntity.sync_key;
|
||||
}
|
||||
|
||||
// Реализация IBlockchainSynchronizable
|
||||
getPrimaryKey(): string {
|
||||
return this.id?.toString() || '';
|
||||
}
|
||||
|
||||
getSyncKey(): string {
|
||||
return this.expense_hash;
|
||||
}
|
||||
|
||||
getBlockNum(): number | undefined {
|
||||
return this.block_num;
|
||||
}
|
||||
|
||||
updateFromBlockchain(blockchainData: IExpenseBlockchainData, blockNum: number, present = true): void {
|
||||
// Обновляем все поля из блокчейна
|
||||
this.coopname = blockchainData.coopname;
|
||||
this.username = blockchainData.username;
|
||||
this.project_hash = blockchainData.project_hash;
|
||||
this.expense_hash = blockchainData.expense_hash;
|
||||
this.fund_id = blockchainData.fund_id;
|
||||
this.blockchainStatus = blockchainData.status;
|
||||
this.amount = blockchainData.amount;
|
||||
this.description = blockchainData.description;
|
||||
this.expense_statement = blockchainData.expense_statement;
|
||||
this.approved_statement = blockchainData.approved_statement;
|
||||
this.authorization = blockchainData.authorization;
|
||||
this.spended_at = blockchainData.spended_at;
|
||||
this.status = this.mapBlockchainStatusToDomain(blockchainData.status);
|
||||
this.block_num = blockNum;
|
||||
this.present = present;
|
||||
}
|
||||
|
||||
private mapBlockchainStatusToDomain(blockchainStatus: IExpenseBlockchainData['status']): ExpenseStatus {
|
||||
const statusValue = blockchainStatus.toString();
|
||||
|
||||
switch (statusValue) {
|
||||
case 'pending': return ExpenseStatus.PENDING;
|
||||
case 'approved': return ExpenseStatus.APPROVED;
|
||||
case 'paid': return ExpenseStatus.PAID;
|
||||
case 'declined': return ExpenseStatus.DECLINED;
|
||||
case 'cancelled': return ExpenseStatus.CANCELLED;
|
||||
default:
|
||||
console.warn(`Неизвестный статус блокчейна: ${statusValue}, устанавливаем CANCELLED`);
|
||||
return ExpenseStatus.CANCELLED;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Что проверять:**
|
||||
- ✅ Импорт `IBlockchainSynchronizable`
|
||||
- ✅ Правильные типы документов `ISignedDocumentDomainInterface`
|
||||
- ✅ Реализация всех методов интерфейса
|
||||
- ✅ Корректное маппинг статусов
|
||||
|
||||
### 1.2 Создание перечисления статусов
|
||||
|
||||
**Файл:** `domain/enums/expense-status.enum.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Перечисление статусов расходов
|
||||
* Синхронизировано с константами из expenses.hpp блокчейн контракта
|
||||
*/
|
||||
export enum ExpenseStatus {
|
||||
PENDING = 'pending',
|
||||
APPROVED = 'approved',
|
||||
PAID = 'paid',
|
||||
DECLINED = 'declined',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Создание интерфейсов базы данных
|
||||
|
||||
**Файл:** `domain/interfaces/expense-database.interface.ts`
|
||||
|
||||
```typescript
|
||||
import type { IBaseDatabaseData } from './base-database.interface';
|
||||
|
||||
/**
|
||||
* Интерфейс данных расхода из базы данных
|
||||
*/
|
||||
export interface IExpenseDatabaseData extends IBaseDatabaseData {
|
||||
// Дополнительные поля базы данных, если нужны
|
||||
expense_hash?: string; // Ключ синхронизации
|
||||
}
|
||||
```
|
||||
|
||||
**Файл:** `domain/interfaces/base-database.interface.ts` (уже существует)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Базовый интерфейс данных из базы данных для всех синхронизируемых сущностей
|
||||
*/
|
||||
export interface IBaseDatabaseData {
|
||||
_id: string; // Внутренний ID базы данных
|
||||
id?: number; // ID в блокчейне (опциональный)
|
||||
block_num: number | undefined; // Номер блока последнего обновления
|
||||
present: boolean; // Существует ли запись в блокчейне
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Создание интерфейса данных блокчейна
|
||||
|
||||
**Файл:** `domain/interfaces/expense-blockchain.interface.ts`
|
||||
|
||||
```typescript
|
||||
import type { CapitalContract } from 'cooptypes';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
/**
|
||||
* Интерфейс данных расхода из блокчейна
|
||||
*/
|
||||
export type IExpenseBlockchainData = Omit<
|
||||
CapitalContract.Tables.Expenses.IExpense,
|
||||
'expense_statement' | 'approved_statement' | 'authorization'
|
||||
> & {
|
||||
expense_statement: ISignedDocumentDomainInterface;
|
||||
approved_statement: ISignedDocumentDomainInterface;
|
||||
authorization: ISignedDocumentDomainInterface;
|
||||
};
|
||||
```
|
||||
|
||||
**Что проверять:**
|
||||
- ✅ Импорт типов из `cooptypes`
|
||||
- ✅ Правильные имена полей документов (должны совпадать с блокчейном)
|
||||
- ✅ Использование `ISignedDocumentDomainInterface` для документов
|
||||
|
||||
### 1.5 Создание интерфейса репозитория
|
||||
|
||||
**Файл:** `domain/repositories/expense.repository.ts`
|
||||
|
||||
```typescript
|
||||
import type { ExpenseDomainEntity } from '../entities/expense.entity';
|
||||
import type { IBlockchainSyncRepository } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
|
||||
export const EXPENSE_REPOSITORY = Symbol('EXPENSE_REPOSITORY');
|
||||
|
||||
/**
|
||||
* Интерфейс репозитория расходов
|
||||
*/
|
||||
export interface ExpenseRepository extends IBlockchainSyncRepository<ExpenseDomainEntity> {
|
||||
// Дополнительные методы репозитория расходов, если нужны
|
||||
// findByProjectHash(projectHash: string): Promise<ExpenseDomainEntity[]>;
|
||||
// findByUsername(username: string): Promise<ExpenseDomainEntity[]>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Шаг 2: Уровень инфраструктуры (Infrastructure Layer)
|
||||
|
||||
### 2.1 Создание TypeORM сущности
|
||||
|
||||
**Файл:** `infrastructure/entities/expense.typeorm-entity.ts`
|
||||
|
||||
```typescript
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm';
|
||||
import { ExpenseStatus } from '../../domain/enums/expense-status.enum';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
export const EntityName = 'capital_expenses';
|
||||
@Entity(EntityName)
|
||||
@Index(`idx_${EntityName}_id`, ['id'])
|
||||
@Index(`idx_${EntityName}_expense_hash`, ['expense_hash'])
|
||||
@Index(`idx_${EntityName}_username`, ['username'])
|
||||
@Index(`idx_${EntityName}_project_hash`, ['project_hash'])
|
||||
@Index(`idx_${EntityName}_status`, ['status'])
|
||||
export class ExpenseTypeormEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
_id!: string;
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
id?: number;
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
block_num?: number;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
present!: boolean;
|
||||
|
||||
// Поля из блокчейна (expenses.hpp)
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
coopname!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
username!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 64 })
|
||||
project_hash!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 64 })
|
||||
expense_hash!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 64 })
|
||||
fund_id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20 })
|
||||
blockchain_status!: string;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
amount!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
description!: string;
|
||||
|
||||
@Column({ type: 'json' })
|
||||
expense_statement!: ISignedDocumentDomainInterface;
|
||||
|
||||
@Column({ type: 'json' })
|
||||
approved_statement!: ISignedDocumentDomainInterface;
|
||||
|
||||
@Column({ type: 'json' })
|
||||
authorization!: ISignedDocumentDomainInterface;
|
||||
|
||||
@Column({ type: 'timestamp' })
|
||||
spended_at!: Date;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp' })
|
||||
created_at!: Date;
|
||||
|
||||
// Доменные поля (расширения)
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ExpenseStatus,
|
||||
default: ExpenseStatus.PENDING,
|
||||
})
|
||||
status!: ExpenseStatus;
|
||||
}
|
||||
```
|
||||
|
||||
**Что проверять:**
|
||||
- ✅ `@Column({ type: 'json' })` для всех полей документов
|
||||
- ✅ Правильные индексы для поиска
|
||||
- ✅ Типы полей совпадают с интерфейсами
|
||||
|
||||
### 2.2 Создание маппера домен ↔ TypeORM
|
||||
|
||||
**Файл:** `infrastructure/mappers/expense.mapper.ts`
|
||||
|
||||
```typescript
|
||||
import { ExpenseDomainEntity } from '../../domain/entities/expense.entity';
|
||||
import { ExpenseTypeormEntity } from '../entities/expense.typeorm-entity';
|
||||
import type { IExpenseDatabaseData } from '../../domain/interfaces/expense-database.interface';
|
||||
import type { IExpenseBlockchainData } from '../../domain/interfaces/expense-blockchain.interface';
|
||||
|
||||
/**
|
||||
* Маппер для преобразования между доменной сущностью расхода и TypeORM сущностью
|
||||
*/
|
||||
export class ExpenseMapper {
|
||||
/**
|
||||
* Преобразование TypeORM сущности в доменную сущность
|
||||
*/
|
||||
static toDomain(entity: ExpenseTypeormEntity): ExpenseDomainEntity {
|
||||
const databaseData: IExpenseDatabaseData = {
|
||||
_id: entity._id,
|
||||
id: entity.id,
|
||||
block_num: entity.block_num,
|
||||
present: entity.present,
|
||||
expense_hash: entity.expense_hash,
|
||||
};
|
||||
|
||||
// Используем данные из TypeORM сущности
|
||||
const blockchainData: IExpenseBlockchainData = {
|
||||
id: entity.id || 0,
|
||||
coopname: entity.coopname,
|
||||
username: entity.username,
|
||||
project_hash: entity.project_hash,
|
||||
expense_hash: entity.expense_hash,
|
||||
fund_id: entity.fund_id,
|
||||
status: entity.blockchain_status as any, // Приведение типа статуса
|
||||
amount: entity.amount,
|
||||
description: entity.description,
|
||||
expense_statement: entity.expense_statement,
|
||||
approved_statement: entity.approved_statement,
|
||||
authorization: entity.authorization,
|
||||
spended_at: entity.spended_at.toISOString(),
|
||||
};
|
||||
|
||||
return new ExpenseDomainEntity(databaseData, blockchainData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразование доменной сущности в TypeORM сущность для создания
|
||||
*/
|
||||
static toEntity(domain: Partial<ExpenseDomainEntity>): Partial<ExpenseTypeormEntity> {
|
||||
const entity: Partial<ExpenseTypeormEntity> = {
|
||||
_id: domain._id,
|
||||
id: domain.id,
|
||||
block_num: domain.block_num,
|
||||
present: domain.present,
|
||||
};
|
||||
|
||||
// Поля из блокчейна
|
||||
if (domain.coopname !== undefined) entity.coopname = domain.coopname;
|
||||
if (domain.username !== undefined) entity.username = domain.username;
|
||||
if (domain.project_hash !== undefined) entity.project_hash = domain.project_hash;
|
||||
if (domain.expense_hash !== undefined) entity.expense_hash = domain.expense_hash;
|
||||
if (domain.fund_id !== undefined) entity.fund_id = domain.fund_id;
|
||||
if (domain.blockchainStatus !== undefined) entity.blockchain_status = domain.blockchainStatus.toString();
|
||||
if (domain.amount !== undefined) entity.amount = domain.amount;
|
||||
if (domain.description !== undefined) entity.description = domain.description;
|
||||
if (domain.expense_statement !== undefined) entity.expense_statement = domain.expense_statement;
|
||||
if (domain.approved_statement !== undefined) entity.approved_statement = domain.approved_statement;
|
||||
if (domain.authorization !== undefined) entity.authorization = domain.authorization;
|
||||
if (domain.spended_at !== undefined) entity.spended_at = new Date(domain.spended_at);
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Создание дельта-маппера
|
||||
|
||||
**Файл:** `infrastructure/blockchain/mappers/expense-delta.mapper.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { IDelta } from '~/types/common';
|
||||
import { ExpenseDomainEntity } from '../../../domain/entities/expense.entity';
|
||||
import type { IExpenseBlockchainData } from '../../../domain/interfaces/expense-blockchain.interface';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import type { IBlockchainDeltaMapper } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import { CapitalContractInfoService } from '../../services/capital-contract-info.service';
|
||||
import { DomainToBlockchainUtils } from '~/shared/utils/domain-to-blockchain.utils';
|
||||
import type { CapitalContract } from 'cooptypes';
|
||||
|
||||
/**
|
||||
* Маппер для преобразования дельт блокчейна в данные расхода
|
||||
*/
|
||||
@Injectable()
|
||||
export class ExpenseDeltaMapper implements IBlockchainDeltaMapper<IExpenseBlockchainData, ExpenseDomainEntity> {
|
||||
constructor(
|
||||
private readonly logger: WinstonLoggerService,
|
||||
private readonly contractInfo: CapitalContractInfoService
|
||||
) {
|
||||
this.logger.setContext(ExpenseDeltaMapper.name);
|
||||
}
|
||||
|
||||
mapDeltaToBlockchainData(delta: IDelta): IExpenseBlockchainData | null {
|
||||
try {
|
||||
if (!this.isRelevantDelta(delta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Дельта содержит данные в поле value
|
||||
const value = delta.value as CapitalContract.Tables.Expenses.IExpense;
|
||||
if (!value) {
|
||||
this.logger.warn(`Delta has no value: table=${delta.table}, key=${delta.primary_key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 🔥 ВАЖНО: Парсим документы ПЕРЕД возвратом
|
||||
const expense_statement = DomainToBlockchainUtils.convertChainDocumentToDomainFormat(value.expense_statement);
|
||||
const approved_statement = DomainToBlockchainUtils.convertChainDocumentToDomainFormat(value.approved_statement);
|
||||
const authorization = DomainToBlockchainUtils.convertChainDocumentToDomainFormat(value.authorization);
|
||||
|
||||
// Парсим документы
|
||||
return { ...value, expense_statement, approved_statement, authorization };
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error mapping delta to blockchain data: ${error.message}`, error.stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extractSyncValue(delta: IDelta): string {
|
||||
// В таблице expenses primary_key является ID расхода
|
||||
return delta.primary_key.toString();
|
||||
}
|
||||
|
||||
isRelevantDelta(delta: IDelta): boolean {
|
||||
const isRelevantContract = this.contractInfo.isContractSupported(delta.code);
|
||||
const isRelevantTable = delta.table === 'expenses' || delta.table === 'expenses*' || delta.table.includes('expenses');
|
||||
|
||||
return isRelevantContract && isRelevantTable;
|
||||
}
|
||||
|
||||
|
||||
getSupportedTableNames(): string[] {
|
||||
return ['expenses', 'expenses*'];
|
||||
}
|
||||
|
||||
getSupportedContractNames(): string[] {
|
||||
return this.contractInfo.getSupportedContractNames();
|
||||
}
|
||||
|
||||
getAllEventPatterns(): string[] {
|
||||
const patterns: string[] = [];
|
||||
const supportedContracts = this.contractInfo.getSupportedContractNames();
|
||||
const supportedTables = this.getSupportedTableNames();
|
||||
|
||||
for (const contractName of supportedContracts) {
|
||||
for (const tableName of supportedTables) {
|
||||
patterns.push(`delta::${contractName}::${tableName}`);
|
||||
}
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**🔥 КЛЮЧЕВЫЕ МОМЕНТЫ В ДЕЛЬТА-МАППЕРЕ:**
|
||||
- ✅ `const value = delta.value as CapitalContract.Tables.Expenses.IExpense;` - приведение типа
|
||||
- ✅ Парсинг документов через `DomainToBlockchainUtils.convertChainDocumentToDomainFormat`
|
||||
- ✅ Валидация всех обязательных полей
|
||||
- ✅ Правильные паттерны событий
|
||||
|
||||
### 2.4 Создание сервиса синхронизации
|
||||
|
||||
**Файл:** `infrastructure/blockchain/services/expense-sync.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable, OnModuleInit, Inject } from '@nestjs/common';
|
||||
import { OnEvent, EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import type { IDelta } from '~/types/common';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { AbstractEntitySyncService } from '../../../../../shared/services/abstract-entity-sync.service';
|
||||
import { ExpenseDomainEntity } from '../../../domain/entities/expense.entity';
|
||||
import { ExpenseRepository, EXPENSE_REPOSITORY } from '../../../domain/repositories/expense.repository';
|
||||
import { ExpenseDeltaMapper } from '../mappers/expense-delta.mapper';
|
||||
import type { IExpenseBlockchainData } from '../../../domain/interfaces/expense-blockchain.interface';
|
||||
|
||||
/**
|
||||
* Сервис синхронизации расходов с блокчейном
|
||||
*
|
||||
* Подписывается на дельты таблицы expenses контракта capital
|
||||
* и синхронизирует данные расходов в локальной базе данных
|
||||
*/
|
||||
@Injectable()
|
||||
export class ExpenseSyncService
|
||||
extends AbstractEntitySyncService<ExpenseDomainEntity, IExpenseBlockchainData>
|
||||
implements OnModuleInit
|
||||
{
|
||||
protected readonly entityName = 'Expense';
|
||||
|
||||
constructor(
|
||||
@Inject(EXPENSE_REPOSITORY)
|
||||
expenseRepository: ExpenseRepository,
|
||||
expenseDeltaMapper: ExpenseDeltaMapper,
|
||||
logger: WinstonLoggerService,
|
||||
private readonly eventEmitter: EventEmitter2
|
||||
) {
|
||||
super(expenseRepository, expenseDeltaMapper, logger);
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
const supportedVersions = this.getSupportedVersions();
|
||||
this.logger.debug(
|
||||
`Сервис синхронизации расходов инициализирован. Поддерживаемые контракты: [${supportedVersions.contracts.join(
|
||||
', '
|
||||
)}], таблицы: [${supportedVersions.tables.join(', ')}]`
|
||||
);
|
||||
|
||||
// Программная подписка на все поддерживаемые паттерны событий
|
||||
const allPatterns = this.getAllEventPatterns();
|
||||
this.logger.debug(`Подписка на ${allPatterns.length} паттернов событий: ${allPatterns.join(', ')}`);
|
||||
|
||||
// Подписываемся на каждый паттерн программно
|
||||
allPatterns.forEach((pattern) => {
|
||||
this.eventEmitter.on(pattern, this.processDelta.bind(this));
|
||||
});
|
||||
|
||||
this.logger.debug('Сервис синхронизации расходов полностью инициализирован с подписками на паттерны');
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка форков для расходов
|
||||
* Теперь подписывается на все форки независимо от контракта
|
||||
*/
|
||||
@OnEvent('fork::*')
|
||||
async handleExpenseFork(forkData: { block_num: number }): Promise<void> {
|
||||
await this.handleFork(forkData.block_num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение поддерживаемых версий контрактов и таблиц
|
||||
*/
|
||||
public getSupportedVersions(): { contracts: string[]; tables: string[] } {
|
||||
return {
|
||||
contracts: this.expenseDeltaMapper.getSupportedContractNames(),
|
||||
tables: this.expenseDeltaMapper.getSupportedTableNames(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех паттернов событий для подписки
|
||||
*/
|
||||
public getAllEventPatterns(): string[] {
|
||||
return this.expenseDeltaMapper.getAllEventPatterns();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 Создание TypeORM репозитория
|
||||
|
||||
**Файл:** `infrastructure/repositories/expense.typeorm-repository.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ExpenseRepository } from '../../domain/repositories/expense.repository';
|
||||
import { ExpenseDomainEntity } from '../../domain/entities/expense.entity';
|
||||
import { ExpenseTypeormEntity } from '../entities/expense.typeorm-entity';
|
||||
import { ExpenseMapper } from '../mappers/expense.mapper';
|
||||
import { CAPITAL_DATABASE_CONNECTION } from '../database/capital-database.module';
|
||||
import type { IBlockchainSyncRepository } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import { BaseBlockchainRepository } from '~/shared/sync/repositories/base-blockchain.repository';
|
||||
import { EntityVersioningService } from '~/shared/sync/services/entity-versioning.service';
|
||||
import type { IExpenseBlockchainData } from '../../domain/interfaces/expense-blockchain.interface';
|
||||
import type { IExpenseDatabaseData } from '../../domain/interfaces/expense-database.interface';
|
||||
|
||||
/**
|
||||
* TypeORM реализация репозитория расходов
|
||||
*/
|
||||
@Injectable()
|
||||
export class ExpenseTypeormRepository
|
||||
extends BaseBlockchainRepository<ExpenseDomainEntity, ExpenseTypeormEntity>
|
||||
implements ExpenseRepository, IBlockchainSyncRepository<ExpenseDomainEntity>
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(ExpenseTypeormEntity, CAPITAL_DATABASE_CONNECTION)
|
||||
repository: Repository<ExpenseTypeormEntity>,
|
||||
entityVersioningService: EntityVersioningService
|
||||
) {
|
||||
super(repository, entityVersioningService);
|
||||
}
|
||||
|
||||
protected getMapper() {
|
||||
return {
|
||||
toDomain: ExpenseMapper.toDomain,
|
||||
toEntity: ExpenseMapper.toEntity,
|
||||
};
|
||||
}
|
||||
|
||||
protected createDomainEntity(
|
||||
databaseData: IExpenseDatabaseData,
|
||||
blockchainData: IExpenseBlockchainData
|
||||
): ExpenseDomainEntity {
|
||||
return new ExpenseDomainEntity(databaseData, blockchainData);
|
||||
}
|
||||
|
||||
protected getSyncKey(): string {
|
||||
return ExpenseDomainEntity.getSyncKey();
|
||||
}
|
||||
|
||||
// Специфичные методы репозитория расходов
|
||||
async findByUsername(username: string): Promise<ExpenseDomainEntity[]> {
|
||||
const entities = await this.repository.find({ where: { username } });
|
||||
return entities.map((entity) => ExpenseMapper.toDomain(entity));
|
||||
}
|
||||
|
||||
async findByProjectHash(projectHash: string): Promise<ExpenseDomainEntity[]> {
|
||||
const entities = await this.repository.find({ where: { project_hash: projectHash } });
|
||||
return entities.map((entity) => ExpenseMapper.toDomain(entity));
|
||||
}
|
||||
|
||||
async findByStatus(status: string): Promise<ExpenseDomainEntity[]> {
|
||||
const entities = await this.repository.find({ where: { status: status as any } });
|
||||
return entities.map((entity) => ExpenseMapper.toDomain(entity));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Шаг 3: Регистрация компонентов
|
||||
|
||||
### 3.1 Регистрация в модуле базы данных
|
||||
|
||||
**Файл:** `infrastructure/database/capital-database.module.ts`
|
||||
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
// ... другие импорты
|
||||
import { ExpenseTypeormEntity } from '../entities/expense.typeorm-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
// ... другие сущности
|
||||
ExpenseTypeormEntity,
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class CapitalDatabaseModule {}
|
||||
```
|
||||
|
||||
### 3.2 Регистрация в основном модуле расширения
|
||||
|
||||
**Файл:** `capital-extension.module.ts`
|
||||
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
// ... другие импорты
|
||||
import { ExpenseRepository, EXPENSE_REPOSITORY } from './domain/repositories/expense.repository';
|
||||
import { ExpenseTypeormRepository } from './infrastructure/repositories/expense.typeorm-repository';
|
||||
import { ExpenseDeltaMapper } from './infrastructure/blockchain/mappers/expense-delta.mapper';
|
||||
import { ExpenseSyncService } from './infrastructure/blockchain/services/expense-sync.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ... другие импорты
|
||||
],
|
||||
providers: [
|
||||
// ... другие провайдеры
|
||||
|
||||
// Expense компоненты
|
||||
{
|
||||
provide: EXPENSE_REPOSITORY,
|
||||
useClass: ExpenseTypeormRepository,
|
||||
},
|
||||
ExpenseTypeormRepository,
|
||||
ExpenseDeltaMapper,
|
||||
ExpenseSyncService,
|
||||
],
|
||||
exports: [
|
||||
// ... другие экспорты
|
||||
ExpenseSyncService,
|
||||
],
|
||||
})
|
||||
export class CapitalExtensionModule {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Шаг 4: Проверка и тестирование
|
||||
|
||||
### 4.1 Проверка типов TypeScript
|
||||
```bash
|
||||
# Проверить типы
|
||||
npm run type-check
|
||||
|
||||
# Или в IDE посмотреть на ошибки
|
||||
```
|
||||
|
||||
### 4.2 Проверка линтера
|
||||
```bash
|
||||
# Проверить линтер
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### 4.3 Тестирование синхронизации
|
||||
```typescript
|
||||
// В коде приложения
|
||||
const expenseSync = await capitalSyncInteractor.getExpenseSyncService();
|
||||
const stats = await expenseSync.getSyncStatistics();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Список всех файлов для сущности "Expense"
|
||||
|
||||
### Доменный уровень:
|
||||
- ✅ `domain/entities/expense.entity.ts`
|
||||
- ✅ `domain/enums/expense-status.enum.ts`
|
||||
- ✅ `domain/interfaces/expense-database.interface.ts`
|
||||
- ✅ `domain/interfaces/expense-blockchain.interface.ts`
|
||||
- ✅ `domain/repositories/expense.repository.ts`
|
||||
|
||||
### Уровень инфраструктуры:
|
||||
- ✅ `infrastructure/entities/expense.typeorm-entity.ts`
|
||||
- ✅ `infrastructure/mappers/expense.mapper.ts`
|
||||
- ✅ `infrastructure/blockchain/mappers/expense-delta.mapper.ts`
|
||||
- ✅ `infrastructure/blockchain/services/expense-sync.service.ts`
|
||||
- ✅ `infrastructure/repositories/expense.typeorm-repository.ts`
|
||||
|
||||
### Регистрация:
|
||||
- ✅ `infrastructure/database/capital-database.module.ts` (добавить сущность)
|
||||
- ✅ `capital-extension.module.ts` (добавить провайдеры)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Важные замечания
|
||||
|
||||
### Документы в блокчейне
|
||||
- ✅ Всегда используйте `DomainToBlockchainUtils.convertChainDocumentToDomainFormat()` для парсинга
|
||||
- ✅ Проверяйте названия полей документов в блокчейне
|
||||
- ✅ Используйте `@Column({ type: 'json' })` для хранения документов
|
||||
|
||||
### Типы и интерфейсы
|
||||
- ✅ Реализуйте `IBlockchainSynchronizable` в доменной сущности
|
||||
- ✅ Добавьте `block_num` и `present` поля
|
||||
- ✅ Используйте правильные типы из `cooptypes`
|
||||
|
||||
### События и синхронизация
|
||||
- ✅ Подписывайтесь на правильные паттерны событий
|
||||
- ✅ Обрабатывайте форки через `capital::fork`
|
||||
- ✅ Валидируйте все обязательные поля
|
||||
|
||||
### Репозитории
|
||||
- ✅ Реализуйте `IBlockchainSyncRepository`
|
||||
- ✅ Добавьте методы `findByBlockchainId`, `findByBlockNumGreaterThan`, `createIfNotExists`, `deleteByBlockNumGreaterThan`
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Шаг 3: Использование BaseBlockchainRepository (Рекомендуемый подход)
|
||||
|
||||
### 3.1 Обзор базового репозитория
|
||||
|
||||
Начиная с версии системы синхронизации, **все репозитории должны наследоваться от `BaseBlockchainRepository`**. Это обеспечивает:
|
||||
|
||||
- ✅ Единообразие кода синхронизации
|
||||
- ✅ Автоматическую реализацию стандартных методов
|
||||
- ✅ Упрощение поддержки и расширение
|
||||
- ✅ Снижение дублирования кода
|
||||
|
||||
### 3.2 Структура базового репозитория
|
||||
|
||||
**Файл:** `infrastructure/repositories/base-blockchain.repository.ts`
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export abstract class BaseBlockchainRepository<
|
||||
TDomainEntity extends IBlockchainSynchronizable,
|
||||
TTypeormEntity extends IBaseDatabaseData
|
||||
> implements IBlockchainSyncRepository<TDomainEntity>
|
||||
{
|
||||
protected constructor(protected readonly repository: Repository<TTypeormEntity>) {}
|
||||
|
||||
// Абстрактные методы для реализации в наследниках
|
||||
protected abstract getMapper(): {
|
||||
toDomain: (typeormEntity: TTypeormEntity) => TDomainEntity;
|
||||
toEntity: (domainEntity: Partial<TDomainEntity>) => Partial<TTypeormEntity>;
|
||||
};
|
||||
|
||||
protected abstract createDomainEntity(
|
||||
databaseData: { id: string; blockchain_id: string; block_num: number; present: boolean },
|
||||
blockchainData: any
|
||||
): TDomainEntity;
|
||||
|
||||
// Готовые реализации методов синхронизации
|
||||
async findByBlockchainId(blockchainId: string): Promise<TDomainEntity | null>
|
||||
async findByBlockNumGreaterThan(blockNum: number): Promise<TDomainEntity[]>
|
||||
async createIfNotExists(blockchainData: any, blockNum: number, present = true): Promise<TDomainEntity>
|
||||
async deleteByBlockNumGreaterThan(blockNum: number): Promise<void>
|
||||
async update(entity: TDomainEntity): Promise<TDomainEntity>
|
||||
async save(entity: TDomainEntity): Promise<TDomainEntity>
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Пример использования базового репозитория
|
||||
|
||||
**Файл:** `infrastructure/repositories/expense.typeorm-repository.ts`
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class ExpenseTypeormRepository
|
||||
extends BaseBlockchainRepository<ExpenseDomainEntity, ExpenseTypeormEntity>
|
||||
implements ExpenseRepository, IBlockchainSyncRepository<ExpenseDomainEntity>
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(ExpenseTypeormEntity, CAPITAL_DATABASE_CONNECTION)
|
||||
repository: Repository<ExpenseTypeormEntity>
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
protected getMapper() {
|
||||
return {
|
||||
toDomain: ExpenseMapper.toDomain,
|
||||
toEntity: ExpenseMapper.toEntity,
|
||||
};
|
||||
}
|
||||
|
||||
protected createDomainEntity(
|
||||
databaseData: { _id: string; id?: number; block_num: number | undefined; present: boolean },
|
||||
blockchainData: any
|
||||
): ExpenseDomainEntity {
|
||||
return new ExpenseDomainEntity(databaseData, blockchainData);
|
||||
}
|
||||
|
||||
protected getSyncKey(): string {
|
||||
return ExpenseDomainEntity.getSyncKey();
|
||||
}
|
||||
|
||||
// Специфичные методы репозитория расходов
|
||||
async findByProjectHash(projectHash: string): Promise<ExpenseDomainEntity[]> {
|
||||
// Специфичная логика для расходов
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Особенности для разных типов сущностей
|
||||
|
||||
#### Для State сущности (использует coopname вместо id):
|
||||
```typescript
|
||||
// Переопределить метод createIfNotExists
|
||||
async createIfNotExists(blockchainData: any, blockNum: number, present = true): Promise<StateDomainEntity> {
|
||||
const blockchainId = blockchainData.coopname; // Особенность state
|
||||
|
||||
// ... остальная логика
|
||||
}
|
||||
```
|
||||
|
||||
#### Для Program сущностей:
|
||||
```typescript
|
||||
// Используют стандартную реализацию без изменений
|
||||
// Все методы синхронизации наследуются от базового класса
|
||||
```
|
||||
|
||||
### 3.5 Преимущества использования базового репозитория
|
||||
|
||||
1. **Стандартизация**: Все репозитории имеют одинаковый интерфейс синхронизации
|
||||
2. **Автоматизация**: Методы `findByBlockchainId`, `createIfNotExists` и др. реализованы автоматически
|
||||
3. **Упрощение**: Меньше кода для написания и поддержки
|
||||
4. **Надежность**: Общая логика тестируется и отлаживается один раз
|
||||
5. **Расширяемость**: Легко добавить новую функциональность во все репозитории
|
||||
|
||||
### 3.6 Миграция существующих репозиториев
|
||||
|
||||
При переводе существующего репозитория на использование `BaseBlockchainRepository`:
|
||||
|
||||
1. **Наследуйтесь** от `BaseBlockchainRepository<TDomain, TEntity>`
|
||||
2. **Реализуйте** абстрактные методы `getMapper()` и `createDomainEntity()`
|
||||
3. **Удалите** дублированные методы синхронизации
|
||||
4. **Замените** `this.repositoryName` на `this.repository`
|
||||
5. **Протестируйте** работу синхронизации
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Следующие шаги
|
||||
|
||||
1. **Создайте все файлы по шаблону выше**
|
||||
2. **Наследуйтесь от BaseBlockchainRepository** (см. Шаг 3)
|
||||
3. **Проверьте TypeScript типы**
|
||||
4. **Зарегистрируйте компоненты в модулях**
|
||||
5. **Протестируйте синхронизацию**
|
||||
6. **Добавьте в документацию BLOCKCHAIN_SYNC.md**
|
||||
|
||||
**При изменении названий документов в блокчейне - проверяйте все дельта-мапперы!**
|
||||
|
||||
**Все новые репозитории ДОЛЖНЫ использовать BaseBlockchainRepository для обеспечения единообразия и надежности системы синхронизации.**
|
||||
|
||||
## ⚠️ Важные замечания для новых сущностей
|
||||
|
||||
### Программная подписка на события
|
||||
- **НЕ используйте** `@OnEvent('contract::delta::table')` декораторы
|
||||
- **Всегда используйте** программную подписку через `eventEmitter.on()` в `onModuleInit()`
|
||||
- Это позволяет поддерживать множество паттернов событий для разных контрактов
|
||||
|
||||
### Сообщения в логах
|
||||
- **Всегда пишите** сообщения на русском языке
|
||||
- Используйте `logger.debug()` для подробной информации
|
||||
- Добавляйте финальный лог "Сервис синхронизации [сущность] полностью инициализирован с подписками на паттерны"
|
||||
|
||||
### Регистрация в CapitalContractInfoService
|
||||
- **Всегда добавляйте** новую таблицу в `tablePatterns` сервиса `CapitalContractInfoService`
|
||||
- Для таблиц ≤ 12 символов: `['tablename', 'tablename*']`
|
||||
- Для таблиц = 12 символов: `['tablename12', 'tablename1*']`
|
||||
|
||||
### Обработка форков
|
||||
- **Всегда используйте** `@OnEvent('fork::*')` для обработки форков
|
||||
- Вызывайте `await this.handleFork(forkData.block_num)` в обработчике
|
||||
|
||||
### Segments особенности
|
||||
- Таблица `segments` содержит информацию о вкладах участников в проекты
|
||||
- Использует составной ключ синхронизации по `project_hash + username`
|
||||
- Содержит роли участников (author, creator, coordinator, etc.)
|
||||
- Финансовые данные: инвестиции, бонусы, премии
|
||||
- CRPS поля для распределения наград
|
||||
- Статусы: generation, ready, contributed, accepted, completed
|
||||
@@ -0,0 +1,399 @@
|
||||
# Система учёта времени (Time Tracking) в CAPITAL расширении
|
||||
|
||||
## Общее описание
|
||||
|
||||
Система учёта времени предназначена для автоматического отслеживания рабочего времени участников в проектах кооператива. Основная идея заключается в том, что участники получают "пассивный доход" времени за активные задачи, которые они ведут в проектах.
|
||||
|
||||
### Основные принципы работы
|
||||
|
||||
1. **Автоматический учёт**: Система работает по крону каждый час и автоматически начисляет время участникам за активные задачи (IN_PROGRESS)
|
||||
2. **Индивидуальные лимиты**: Каждый участник имеет индивидуальный лимит часов в день (поле `hours_per_day`), по умолчанию 0 часов
|
||||
3. **Равномерное распределение**: Если у участника несколько активных задач, общее доступное время (согласно индивидуальному лимиту) распределяется между ними поровну
|
||||
4. **Условная доступность**: Время становится доступным для фиксации в коммит только после завершения соответствующих задач (DONE статус)
|
||||
|
||||
## Архитектура системы
|
||||
|
||||
### Основные компоненты
|
||||
|
||||
1. **TimeTrackingInteractor** - основной бизнес-логика учёта времени, включая расчёт available/pending часов
|
||||
2. **TimeTrackingService** - сервис для обработки запросов от резолверов
|
||||
3. **TimeTrackingSchedulerService** - планировщик cron задач
|
||||
4. **Репозитории** - доступ к данным (TimeEntry, Project, Contributor, Issue) - только базовые CRUD операции
|
||||
|
||||
### Сущности данных
|
||||
|
||||
#### TimeEntry (Запись времени)
|
||||
```typescript
|
||||
{
|
||||
_id: string,
|
||||
contributor_hash: string, // Хеш участника
|
||||
issue_hash: string, // Хеш задачи
|
||||
project_hash: string, // Хеш проекта
|
||||
coopname: string, // Название кооператива
|
||||
date: string, // Дата в формате YYYY-MM-DD
|
||||
hours: number, // Количество часов
|
||||
commit_hash?: string, // Хеш коммита (если зафиксировано)
|
||||
is_committed: boolean, // Зафиксировано ли время
|
||||
block_num: number, // Номер блока
|
||||
present: boolean, // Активна ли запись
|
||||
status: string, // Статус записи
|
||||
_created_at: Date, // Дата создания
|
||||
_updated_at: Date // Дата обновления
|
||||
}
|
||||
```
|
||||
|
||||
## Логика учёта времени
|
||||
|
||||
### Cron-задача (каждый час)
|
||||
|
||||
```typescript
|
||||
// Запускается каждый час по cron выражению '0 * * * *'
|
||||
async trackTime(): Promise<void> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Получаем всех активных участников из всех кооперативов
|
||||
const activeContributors = await this.getAllActiveContributors();
|
||||
|
||||
// Обрабатываем каждого участника
|
||||
for (const contributor of activeContributors) {
|
||||
await this.trackTimeForContributor(contributor, today);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Обработка участника
|
||||
|
||||
```typescript
|
||||
private async trackTimeForContributor(contributor, date): Promise<void> {
|
||||
// 1. Получаем активные задачи участника
|
||||
const activeIssues = await this.getContributorActiveIssues(contributor);
|
||||
|
||||
// 2. Рассчитываем распределение времени
|
||||
const hoursPerIssue = await this.calculateTimeDistributionPerIssue(contributor, activeIssues, date);
|
||||
|
||||
// 3. Создаём/обновляем записи времени для каждой задачи
|
||||
for (const issue of activeIssues) {
|
||||
const hours = hoursPerIssue[issue.issue_hash] || 0;
|
||||
// Создаём или обновляем TimeEntry запись
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Распределение времени между задачами
|
||||
|
||||
```typescript
|
||||
private async calculateTimeDistributionPerIssue(contributor, activeIssues, date) {
|
||||
const HOURS_PER_DAY = contributor.hours_per_day || 0; // Индивидуальный лимит часов или 0 по умолчанию
|
||||
const HOURS_PER_HOUR = 1; // Добавляем каждый час
|
||||
|
||||
// Проверяем уже отработанное время за сегодня
|
||||
const existingEntries = await this.timeEntryRepository.findByContributorAndDate(contributor_hash, date);
|
||||
const totalExistingHours = existingEntries.reduce((sum, entry) => sum + entry.hours, 0);
|
||||
|
||||
// Если уже отработан лимит часов - выходим
|
||||
if (totalExistingHours >= HOURS_PER_DAY) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Распределяем доступное время равномерно между задачами
|
||||
const availableHours = HOURS_PER_DAY - totalExistingHours;
|
||||
const hoursToDistribute = Math.min(HOURS_PER_HOUR, availableHours);
|
||||
const hoursPerIssue = hoursToDistribute / activeIssues.length;
|
||||
|
||||
// Возвращаем распределение: { issue_hash: hours }
|
||||
return { [issue_hash]: hoursPerIssue, ... };
|
||||
}
|
||||
```
|
||||
|
||||
### Примеры распределения времени
|
||||
|
||||
#### Сценарий 1: 1 активная задача (лимита 8 часов)
|
||||
- Участник имеет 1 активную задачу
|
||||
- Каждый час начисляется 1 час на эту задачу
|
||||
- Максимум 8 часов в день
|
||||
|
||||
#### Сценарий 2: 2 активные задачи (лимита 8 часов)
|
||||
- Участник имеет 2 активные задачи
|
||||
- Каждый час начисляется по 0.5 часа на каждую задачу
|
||||
- Максимум 4 часа на задачу в день (8/2)
|
||||
|
||||
#### Сценарий 3: 4 активные задачи (лимита 8 часов)
|
||||
- Участник имеет 4 активные задачи
|
||||
- Каждый час начисляется по 0.25 часа на каждую задачу
|
||||
- Максимум 2 часа на задачу в день (8/4)
|
||||
|
||||
#### Сценарий 4: Участник с индивидуальным лимитом 6 часов
|
||||
- Участник имеет 2 активные задачи, лимит 6 часов в день
|
||||
- Каждый час начисляется по 0.5 часа на каждую задачу
|
||||
- Максимум 3 часа на задачу в день (6/2)
|
||||
|
||||
## Новая логика доступности времени
|
||||
|
||||
### Изменения в системе
|
||||
|
||||
**До изменений:**
|
||||
- Время начислялось за активные задачи (IN_PROGRESS)
|
||||
- Любое начисленное время было доступно для коммита
|
||||
|
||||
**После изменений:**
|
||||
- Время начисляется за активные задачи (IN_PROGRESS)
|
||||
- Для коммита доступно **только время по завершённым задачам** (DONE)
|
||||
- Добавлено понятие **pending_hours** - время по незавершённым задачам
|
||||
|
||||
### Примеры сценариев
|
||||
|
||||
#### Сценарий 1: Участник работает над задачей, но не завершает её
|
||||
- Участник имеет активную задачу IN_PROGRESS
|
||||
- Каждый час начисляется 1 час в pending_hours
|
||||
- available_hours = 0 (нет завершённых задач)
|
||||
- total_uncommitted_hours = pending_hours
|
||||
|
||||
#### Сценарий 2: Участник завершает задачу
|
||||
- Участник работает над задачей IN_PROGRESS (начисляется время в pending_hours)
|
||||
- Переводит задачу в DONE
|
||||
- Время перемещается из pending_hours в available_hours
|
||||
- Теперь время доступно для коммита
|
||||
|
||||
#### Сценарий 3: Смешанный сценарий
|
||||
- Участник имеет 2 активные задачи IN_PROGRESS (начисляется по 0.5 часа на каждую в pending_hours)
|
||||
- Завершает одну задачу (DONE)
|
||||
- Время по завершённой задаче перемещается в available_hours
|
||||
- available_hours = 4 часа, pending_hours = 4 часа (по второй задаче)
|
||||
- total_uncommitted_hours = 8 часов
|
||||
|
||||
## Активные задачи участника
|
||||
|
||||
Задача считается активной, если:
|
||||
- Статус = `IssueStatus.IN_PROGRESS`
|
||||
- Участник является создателем задачи (`creator_hash` содержит хеш участника)
|
||||
|
||||
```typescript
|
||||
private async getContributorActiveIssues(contributor): Promise<any[]> {
|
||||
return await this.issueRepository.findByStatusAndCreatorsHashs(
|
||||
IssueStatus.IN_PROGRESS,
|
||||
[contributor.contributor_hash]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Коммиты времени
|
||||
|
||||
### Фиксация времени в коммит
|
||||
|
||||
Когда участник делает коммит в проект, можно зафиксировать часть незакоммиченного времени, но **только по завершённым задачам** (статус DONE):
|
||||
|
||||
```typescript
|
||||
async commitTime(contributorHash, projectHash, hours, commitHash): Promise<void> {
|
||||
// Получаем незакоммиченные записи для проекта
|
||||
const uncommittedEntries = await this.timeEntryRepository.findUncommittedByProjectAndContributor(
|
||||
projectHash, contributorHash
|
||||
);
|
||||
|
||||
// Получаем завершённые задачи участника в этом проекте
|
||||
const completedIssues = await this.issueRepository.findCompletedByProjectAndCreatorsHashs(
|
||||
projectHash, [contributorHash]
|
||||
);
|
||||
|
||||
// Фильтруем записи времени только по завершённым задачам
|
||||
const availableEntries = uncommittedEntries.filter(entry =>
|
||||
completedIssues.some(issue => issue.issue_hash === entry.issue_hash)
|
||||
);
|
||||
|
||||
// Сортируем по дате (старые сначала)
|
||||
availableEntries.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
// Фиксируем указанное количество часов
|
||||
let remainingHours = hours;
|
||||
// ... логика фиксации
|
||||
}
|
||||
```
|
||||
|
||||
### Доступное время для коммита
|
||||
|
||||
Доступное время для коммита рассчитывается **только по завершённым задачам**. **Ограничение в 8 часов НЕ применяется** - можно использовать всё накопленное время по завершённым задачам:
|
||||
|
||||
```typescript
|
||||
async getAvailableCommitHours(contributorHash, projectHash): Promise<number> {
|
||||
// Получаем базовую статистику
|
||||
const basicStats = await this.timeEntryRepository.getContributorProjectStats(contributorHash, projectHash);
|
||||
|
||||
// Рассчитываем детальную статистику
|
||||
const detailedStats = await this.calculateDetailedProjectStats(contributorHash, projectHash, basicStats);
|
||||
|
||||
return detailedStats.available_hours; // Без ограничения - можно использовать всё накопленное время по завершённым задачам
|
||||
}
|
||||
```
|
||||
|
||||
### Создание коммита с указанием часов
|
||||
|
||||
Пользователь указывает точное количество часов для коммита, система проверяет доступность:
|
||||
|
||||
```typescript
|
||||
async createCommit(data: CreateCommitDomainInput): Promise<TransactResult> {
|
||||
// Получаем доступное время
|
||||
const availableHours = await this.timeTrackingService.getAvailableCommitHours(
|
||||
contributorHash, projectHash
|
||||
);
|
||||
|
||||
// Проверяем что запрошенное количество часов не превышает доступное
|
||||
if (data.commit_hours > availableHours) {
|
||||
throw new Error(`Requested commit hours exceed available hours`);
|
||||
}
|
||||
|
||||
// Фиксируем указанное количество времени
|
||||
await this.timeTrackingService.commitTime(
|
||||
contributorHash, projectHash, data.commit_hours, commitHash
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Статистика и отчёты
|
||||
|
||||
### Что возвращает статистика сейчас
|
||||
|
||||
После внесённых изменений статистика возвращает детальную разбивку времени:
|
||||
|
||||
- **committed_hours**: Время, зафиксированное в коммитах (уже оплачено/учтено)
|
||||
- **available_hours**: Время по завершённым задачам (DONE), доступное для фиксации в коммит
|
||||
- **pending_hours**: Время по незавершённым задачам (IN_PROGRESS), которое станет доступным после завершения задач
|
||||
- **total_uncommitted_hours**: available_hours + pending_hours (весь незакоммиченный объём)
|
||||
|
||||
### Статистика по проекту для участника
|
||||
|
||||
```typescript
|
||||
interface TimeStatsDomainInterface {
|
||||
contributor_hash: string;
|
||||
project_hash: string;
|
||||
total_committed_hours: number; // Зафиксированные часы
|
||||
total_uncommitted_hours: number; // Незакоммиченные часы (available + pending)
|
||||
available_hours: number; // Доступные для коммита часы (по завершённым задачам)
|
||||
pending_hours: number; // Часы в ожидании (по незавершённым задачам)
|
||||
}
|
||||
```
|
||||
|
||||
### Проекты участника со статистикой
|
||||
|
||||
```typescript
|
||||
interface ContributorProjectsTimeStatsDomainInterface {
|
||||
contributor_hash: string;
|
||||
projects: Array<{
|
||||
project_hash: string;
|
||||
project_name: string;
|
||||
contributor_hash: string;
|
||||
total_committed_hours: number;
|
||||
total_uncommitted_hours: number;
|
||||
available_hours: number;
|
||||
pending_hours: number;
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
## API методы
|
||||
|
||||
### Основные методы TimeTrackingInteractor
|
||||
|
||||
1. **getTimeStats(data)** - Статистика времени участника по проекту
|
||||
2. **getContributorProjectsTimeStats(data)** - Проекты участника со статистикой
|
||||
3. **getTimeEntries(data, options)** - Пагинированные записи времени
|
||||
4. **getFlexibleTimeStats(data, options)** - Гибкий запрос статистики
|
||||
5. **trackTime()** - Основная логика учёта (cron)
|
||||
6. **commitTime(contributorHash, projectHash, hours, commitHash)** - Фиксация времени
|
||||
7. **getAvailableCommitHours(contributorHash, projectHash)** - Доступное время
|
||||
|
||||
### Основные методы TimeTrackingService
|
||||
|
||||
1. **commitTime(contributorHash, projectHash, hours, commitHash)** - Фиксация времени
|
||||
2. **getAvailableCommitHours(contributorHash, projectHash)** - Доступное время
|
||||
3. **getTimeStats(contributorHash, projectHash)** - Статистика по проекту
|
||||
4. **getContributorProjectsTimeStats(data)** - Проекты со статистикой
|
||||
5. **getTimeEntriesByProject(filter, options)** - Записи времени по проекту
|
||||
6. **getFlexibleTimeStats(data, options)** - Гибкая статистика
|
||||
|
||||
### CreateCommit API
|
||||
|
||||
**Входные параметры:**
|
||||
- `coopname` - имя кооператива
|
||||
- `username` - имя пользователя
|
||||
- `project_hash` - хэш проекта
|
||||
- `commit_hash` - хэш коммита
|
||||
- `commit_hours` - **новое поле**: количество часов для фиксации
|
||||
|
||||
**Валидация:**
|
||||
- `commit_hours > 0`
|
||||
- `commit_hours <= available_hours` (доступное время по завершённым задачам)
|
||||
|
||||
**Процесс:**
|
||||
1. Проверка существования пользователя
|
||||
2. Расчёт доступного времени (только по завершённым задачам)
|
||||
3. Валидация `commit_hours <= available_hours`
|
||||
4. Фиксация указанного количества часов
|
||||
5. Создание транзакции в блокчейне
|
||||
|
||||
## Сценарии использования
|
||||
|
||||
### 1. Автоматический учёт времени
|
||||
- Каждый час система проверяет активных участников
|
||||
- Для каждого участника начисляет время за активные задачи
|
||||
- Время распределяется равномерно между задачами
|
||||
|
||||
### 2. Просмотр статистики
|
||||
- Участник может посмотреть время по проектам
|
||||
- Видит committed/uncommitted/available часы
|
||||
- Может планировать коммиты
|
||||
|
||||
### 3. Фиксация времени в коммит
|
||||
- При создании коммита пользователь **явно указывает количество часов** для фиксации
|
||||
- Система проверяет что указанное количество не превышает available_hours (только по завершённым задачам)
|
||||
- Время переносится из uncommitted в committed
|
||||
- Если запрошено больше часов чем доступно - возвращается ошибка
|
||||
|
||||
### 4. Отчёты и аналитика
|
||||
- Гибкие запросы статистики по разным фильтрам
|
||||
- Пагинация для больших объёмов данных
|
||||
- Группировка по проектам/участникам
|
||||
|
||||
## Архитектурные принципы
|
||||
|
||||
### Domain-Driven Design (DDD)
|
||||
|
||||
Система следует принципам Domain-Driven Design:
|
||||
|
||||
1. **Репозитории** - только базовые операции с данными (CRUD), без бизнес-логики
|
||||
2. **Интеракторы** - содержат всю бизнес-логику, включая агрегацию данных из разных источников
|
||||
3. **Изоляция** - репозитории не должны обращаться друг к другу
|
||||
4. **Единая ответственность** - каждый компонент отвечает только за свою область
|
||||
|
||||
### Логика распределения ответственности
|
||||
|
||||
- **Репозиторий TimeEntry**: Возвращает базовую статистику (committed/uncommitted) из time_entries таблицы через `ContributorProjectBasicTimeStatsDomainInterface`
|
||||
- **Интерактор TimeTracking**: Рассчитывает available/pending часы, агрегируя данные из time_entries и issues, возвращает полную статистику через `ContributorProjectTimeStatsDomainInterface`
|
||||
- **Сервис TimeTracking**: Конвертирует доменные объекты в DTO для внешних интерфейсов
|
||||
|
||||
## Особенности реализации
|
||||
|
||||
1. **Индивидуальные лимиты**: Каждый участник имеет индивидуальный лимит часов в день (поле `hours_per_day`), по умолчанию 8 часов (применяется только в периодическом распределении времени)
|
||||
2. **Равномерность**: Равное распределение между активными задачами (IN_PROGRESS)
|
||||
3. **Условная доступность**: Время для коммита доступно только после завершения задач (DONE)
|
||||
4. **Без ограничений при коммите**: При фиксации времени в коммит можно использовать всё накопленное время по завершённым задачам
|
||||
5. **Pending vs Available**: pending_hours (незавершённые задачи) vs available_hours (завершённые задачи)
|
||||
6. **Фильтрация по статусу**: Разделение логики учёта и фиксации времени
|
||||
7. **DDD архитектура**: Соблюдение принципов Domain-Driven Design
|
||||
8. **Персистентность**: Все изменения сохраняются в MongoDB
|
||||
9. **Атомарность**: Операции фиксации времени атомарны
|
||||
10. **Производительность**: Пагинация для больших запросов
|
||||
|
||||
## Мониторинг и отладка
|
||||
|
||||
- Логирование всех операций в TimeTrackingInteractor
|
||||
- Отслеживание ошибок обработки отдельных участников
|
||||
- Проверка консистентности данных
|
||||
- Мониторинг работы cron задач
|
||||
|
||||
## Расширение системы
|
||||
|
||||
Возможные улучшения:
|
||||
- Настраиваемые лимиты времени на участника
|
||||
- Разные стратегии распределения времени
|
||||
- Приоритеты задач
|
||||
- Уведомления о начислении времени
|
||||
- Интеграция с внешними системами учёта времени
|
||||
@@ -0,0 +1,118 @@
|
||||
# Техническое задание: Архитектура маркетплейса приложений для распределенных бэкендов
|
||||
|
||||
## Контекст проблемы
|
||||
|
||||
Мы имеем распределенную систему из множества бэкендов, каждый из которых обладает собственным локальным магазином приложений. Требуется централизовать управление приложениями через единый маркетплейс, обеспечив контроль доступа и монетизацию.
|
||||
|
||||
## Основные требования
|
||||
|
||||
1. **Централизованный каталог** приложений с возможностью монетизации
|
||||
2. **Установка приложений на уровне бэкенда** (не пользователя)
|
||||
3. **Контроль доступа** к платным приложениям
|
||||
4. **Минимальная нагрузка** на систему валидации
|
||||
5. **Безопасность** без единой точки отказа
|
||||
|
||||
## Предлагаемая архитектура
|
||||
|
||||
### 1. Компоненты системы
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ Маркетплейс │────│ Распределенные │────│ Приложения │
|
||||
│ (Central API) │ │ Бэкенды │ │ (Developers) │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 2. Модель токенов
|
||||
|
||||
**Backend Admin Token**
|
||||
- Долгоживущий (365 дней)
|
||||
- Права: установка/удаление приложений, управление лицензиями
|
||||
- Используется только в server-to-server коммуникации
|
||||
|
||||
**Backend User Token**
|
||||
- Краткосрочный (1 час)
|
||||
- Права: чтение списка установленных приложений
|
||||
- Используется для загрузки каталога пользователями
|
||||
|
||||
**User App Access Token**
|
||||
- Краткосрочный (30 минут)
|
||||
- Содержит: backend_id, user_id, app_id, permissions
|
||||
- Передается в IFrame приложений для валидации доступа
|
||||
|
||||
### 3. Workflow установки приложений
|
||||
|
||||
**Для администраторов бэкенда:**
|
||||
1. Админ запрашивает полный каталог через Backend Admin Token
|
||||
2. Маркетплейс возвращает все приложения со статусами лицензий
|
||||
3. Админ выбирает приложение для установки
|
||||
4. Система проверяет наличие активной лицензии
|
||||
5. При успехе - создается запись об установке
|
||||
6. Бэкенд скачивает файлы приложения с серверов разработчика
|
||||
|
||||
**Для обычных пользователей:**
|
||||
1. Пользователь заходит на сайт бэкенда
|
||||
2. Система запрашивает список установленных приложений через Backend User Token
|
||||
3. Маркетплейс возвращает только доступные для данного бэкенда приложения
|
||||
4. Пользователь выбирает приложение
|
||||
5. Бэкенд генерирует User App Access Token
|
||||
6. Приложение загружается в IFrame с переданным токеном
|
||||
|
||||
### 4. Механизм контроля доступа
|
||||
|
||||
**Трехуровневая валидация:**
|
||||
|
||||
1. **Предварительная проверка** - маркетплейс фильтрует доступные приложения
|
||||
2. **Валидация при загрузке** - приложение проверяет JWT токен локально
|
||||
3. **Периодическая ревалидация** - приложение проверяет токен каждые 5 минут
|
||||
|
||||
**Техническая реализация валидации:**
|
||||
- Приложения кэшируют публичные ключи бэкендов (24 часа)
|
||||
- Валидация JWT происходит статически без запросов к API
|
||||
- Короткое время жизни токенов (30 мин) минимизирует риски
|
||||
|
||||
### 5. Монетизация и лицензии
|
||||
|
||||
**Модель лицензирования:**
|
||||
- Лицензия выдается на уровень бэкенда
|
||||
- Поддержка различных моделей оплаты (месячная, годовая, perpetual)
|
||||
- Trial-периоды для тестирования
|
||||
- Автоматическое отключение при неоплате
|
||||
|
||||
**Схема данных лицензий:**
|
||||
```sql
|
||||
backend_licenses (backend_id, app_id, status, expires_at, payment_plan)
|
||||
installations (backend_id, app_id, installed_at, config)
|
||||
```
|
||||
|
||||
### 6. Безопасность и производительность
|
||||
|
||||
**Меры безопасности:**
|
||||
- Разделение прав через разные типы токенов
|
||||
- Короткое время жизни пользовательских токенов
|
||||
- Статическая валидация без зависимостей от API маркетплейса
|
||||
- Кэширование криптографических ключей
|
||||
|
||||
**Оптимизация производительности:**
|
||||
- Локальная проверка JWT подписей (~1-5ms)
|
||||
- Кэширование публичных ключей (24 часа)
|
||||
- Минимальные запросы к центральному API
|
||||
- Предварительная фильтрация на стороне маркетплейса
|
||||
|
||||
## Критические вопросы для дальнейшего исследования
|
||||
|
||||
1. **Механизм отзыва доступа** - как оперативно блокировать приложения при неоплате?
|
||||
2. **Кэширование и инвалидация** - стратегии обновления публичных ключей
|
||||
3. **Мониторинг и аналитика** - отслеживание использования платных функций
|
||||
4. **Grace period** -如何处理 situations when payment is delayed?
|
||||
5. **Миграция с текущей системы** - план перехода с локальных магазинов
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
- Единая точка управления приложениями
|
||||
- Контроль доступа к платному контенту
|
||||
- Масштабируемая архитектура
|
||||
- Минимальное влияние на производительность
|
||||
- Гибкая система монетизации
|
||||
|
||||
Требуется проработка каждого компонента с учетом конкретных технических ограничений и бизнес-требований.
|
||||
@@ -0,0 +1,94 @@
|
||||
# ProjectVotingSegmentsWidget
|
||||
|
||||
Виджет для отображения участников голосования по методу Водянова с возможностью распределения голосов и просмотра результатов.
|
||||
|
||||
## Назначение
|
||||
|
||||
Виджет предназначен для:
|
||||
- Отображения участников проекта, имеющих право голоса (фильтр `has_vote: true`)
|
||||
- Распределения голосующей суммы между участниками (до завершения голосования)
|
||||
- Валидации корректности распределения голосов
|
||||
- Отправки голосов через кнопку `SubmitVoteButton`
|
||||
- Просмотра результатов голосования после его завершения
|
||||
|
||||
## Особенности
|
||||
|
||||
### Визуальное оформление
|
||||
|
||||
Виджет использует `ColorCard` компоненты в двух строках для отображения информации о голосовании:
|
||||
|
||||
**Первая строка (3 колонки):**
|
||||
- **Синяя карта**: Общая сумма на распределение
|
||||
- **Фиолетовая карта**: Голосующая сумма для распределения (только для участников)
|
||||
- **Зеленая/оранжевая карта**: Статус голосования (зависит от состояния)
|
||||
|
||||
**Вторая строка (2 колонки, только для участников до завершения):**
|
||||
- **Зеленая карта**: Распределенная сумма
|
||||
- **Красная карта**: Остаток для распределения
|
||||
|
||||
### До завершения голосования
|
||||
|
||||
1. **Автоматическая фильтрация**: Загружает только сегменты с правом голоса
|
||||
2. **Проверка участия в голосовании**: Показывает поля ввода только участникам голосования
|
||||
3. **Валидация голосования**:
|
||||
- Нельзя голосовать за себя
|
||||
- Должны быть распределены голоса всем участникам кроме себя
|
||||
- Общая сумма должна равняться активной голосующей сумме
|
||||
4. **Визуальная обратная связь**: Показывает распределенную сумму (зеленая карта) и остаток (красная карта) в отдельных колонках
|
||||
5. **Блокировка после голосования**: После отправки голосов виджет переходит в режим readonly
|
||||
6. **Блокировка разворота**: Кнопка expand заблокирована с tooltip подсказкой, нельзя просмотреть голоса других участников до завершения голосования
|
||||
7. **Сообщение для не участников**: Не участники видят сообщение о том, что голосование только для авторов и создателей
|
||||
|
||||
### После завершения голосования
|
||||
|
||||
1. **Отображение результатов**: Показывает результаты голосования (`voting_bonus`) для всех участников
|
||||
2. **Разблокировка разворота**: Кнопка expand становится активной, можно просмотреть детальные голоса каждого участника
|
||||
3. **Доступность для всех**: Результаты видят все пользователи (участники и не участники)
|
||||
|
||||
## Props
|
||||
|
||||
- `projectHash` (string) - Хеш проекта для голосования
|
||||
- `coopname` (string) - Наименование кооператива
|
||||
- `expanded` (Record<string, boolean>) - Состояние разворота участников
|
||||
- `project` (IProject) - Объект проекта с данными голосования
|
||||
- `currentUsername` (string) - Имя текущего пользователя
|
||||
|
||||
## События
|
||||
|
||||
- `toggle-expand` - Переключение разворота участника
|
||||
- `segment-click` - Клик на строку участника
|
||||
- `data-loaded` - Загружены данные участников
|
||||
|
||||
## Слоты
|
||||
|
||||
- `segment-content` - Контент для отображения под строкой участника (например, `SegmentVotesWidget`)
|
||||
|
||||
## Определение статуса голосования
|
||||
|
||||
Голосование считается завершенным если:
|
||||
- Статус проекта равен `COMPLETED`
|
||||
- ИЛИ все участники проголосовали (`votes_received === total_voters`)
|
||||
|
||||
## Пример использования
|
||||
|
||||
```vue
|
||||
<ProjectVotingSegmentsWidget
|
||||
:project-hash="project.project_hash"
|
||||
:coopname="info.coopname"
|
||||
:expanded="expandedSegments"
|
||||
:project="project"
|
||||
:current-username="username"
|
||||
@toggle-expand="handleSegmentToggleExpand"
|
||||
@segment-click="handleSegmentClick"
|
||||
@data-loaded="handleSegmentsDataLoaded"
|
||||
>
|
||||
<template #segment-content="{ segment }">
|
||||
<SegmentVotesWidget
|
||||
:project-hash="project.project_hash"
|
||||
:coopname="info.coopname"
|
||||
:segment-username="segment.username"
|
||||
/>
|
||||
</template>
|
||||
</ProjectVotingSegmentsWidget>
|
||||
```
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# SegmentVotesWidget
|
||||
|
||||
Виджет для отображения голосов конкретного участника в режиме readonly.
|
||||
|
||||
## Назначение
|
||||
|
||||
Виджет предназначен для:
|
||||
- Просмотра голосов, отданных конкретным участником
|
||||
- Отображения получателей голосов и их сумм
|
||||
- Используется как третий уровень в иерархии виджетов голосования
|
||||
|
||||
## Особенности
|
||||
|
||||
1. **Режим readonly**: Только просмотр, без возможности редактирования
|
||||
2. **Автоматическая загрузка**: Загружает голоса при монтировании
|
||||
3. **Реактивность**: Перезагружает данные при изменении `segmentUsername`
|
||||
|
||||
## Props
|
||||
|
||||
- `projectHash` (string) - Хеш проекта для голосования
|
||||
- `coopname` (string) - Наименование кооператива
|
||||
- `segmentUsername` (string) - Имя участника, чьи голоса отображаются
|
||||
|
||||
## Пример использования
|
||||
|
||||
```vue
|
||||
<SegmentVotesWidget
|
||||
:project-hash="project.project_hash"
|
||||
:coopname="info.coopname"
|
||||
:segment-username="segment.username"
|
||||
/>
|
||||
```
|
||||
|
||||
## Структура данных
|
||||
|
||||
Виджет загружает голоса с фильтром:
|
||||
```typescript
|
||||
{
|
||||
filter: {
|
||||
coopname: string,
|
||||
project_hash: string,
|
||||
voter: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Каждый голос содержит:
|
||||
- `recipient` - Получатель голоса
|
||||
- `amount` - Сумма голоса с валютой
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
## Подключение (frontend)
|
||||
|
||||
Зачем: дать кооперативу пройти «подключение» на платформе, фиксируя шаги через документы/решения.
|
||||
|
||||
Как работает:
|
||||
- Страница `ConnectPage` (маршрут `/chairman/connect`) выводит 6 шагов: 5 предложений повестки (кошелёк, ПЭП, privacy, ПС, заявления) + 1 шаг общего собрания.
|
||||
- Шаги 1–5: кнопка открывает диалог с автотекстом → фронт шлёт `completeChairmanAgendaStep`, бэк генерирует и сразу публикует проект решения в блокчейн, сохраняет hash документа и ждёт подтверждения решения (событие `newresolved`). До события шаг помечается «в процессе».
|
||||
- Шаг 6: используется стандартный поток `createMeetWithAgenda` (генерация + подпись повестки), фронт берёт hash повестки и передаёт в `completeChairmanGeneralMeetStep`; бэк ждёт `newresolved` с тем же hash.
|
||||
- Отображение статуса: флаги из `getChairmanOnboardingState`. Для шагов 1–5, если флага нет, но в `vars` уже есть шапка документа — считаем выполненным (приоритет: vars).
|
||||
- Таймер: показывается время до `onboarding_expire_at` (30 дней от старта, выдаёт бэк). После истечения шаги не принимаются автоматически (ждём обновлений с сервера).
|
||||
@@ -0,0 +1,65 @@
|
||||
# ChatCoop Extension
|
||||
|
||||
Расширение для интеграции Matrix чата в кооперативную систему.
|
||||
|
||||
## Описание
|
||||
|
||||
ChatCoop предоставляет встроенный Matrix чат для пользователей кооператива. Расширение:
|
||||
|
||||
- Получает временный токен аутентификации через GraphQL API
|
||||
- Отображает Matrix клиент (Element Web) в iframe
|
||||
- Автоматически аутентифицирует пользователя с полученным токеном
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
chatcoop/
|
||||
├── install.ts # Конфигурация рабочего стола
|
||||
├── entities/
|
||||
│ └── ChatCoopChat/ # Entity для работы с Matrix токенами
|
||||
│ ├── api/ # GraphQL запросы через SDK
|
||||
│ └── model/ # Store и типы данных из SDK
|
||||
├── pages/
|
||||
│ └── ChatCoopPage/ # Главная страница с iframe
|
||||
├── shared/ # Общие компоненты
|
||||
└── widgets/ # Виджеты
|
||||
```
|
||||
|
||||
## SDK интеграция
|
||||
|
||||
Расширение использует типизированные запросы из SDK:
|
||||
|
||||
- **Query**: `Queries.ChatCoop.GetToken`
|
||||
- **Selector**: `chatcoopTokenSelector`
|
||||
- **Типы**: Автоматически генерируются из GraphQL схемы
|
||||
|
||||
## Работа с iframe URL
|
||||
|
||||
1. При загрузке страницы вызывается `chatcoopStore.loadToken()`
|
||||
2. Entity отправляет типизированный GraphQL запрос через SDK: `Queries.ChatCoop.GetToken`
|
||||
3. Бэкенд проверяет/создает Matrix пользователя и генерирует токен
|
||||
4. Бэкенд формирует полную iframe URL с токеном аутентификации
|
||||
5. Store сохраняет полученную ссылку в состоянии (типизировано через SDK)
|
||||
6. Страница загружает iframe с готовой ссылкой
|
||||
7. Пользователь автоматически входит в Matrix чат
|
||||
|
||||
## Конфигурация
|
||||
|
||||
- **MATRIX_CLIENT_URL**: URL Element Web клиента
|
||||
- **workspace**: 'chatcoop'
|
||||
- **defaultRoute**: 'chat'
|
||||
|
||||
## Архитектура
|
||||
|
||||
Расширение построено по принципам Feature-Sliced Design (FSD):
|
||||
|
||||
- **Entities**: `ChatCoopChat` - бизнес-сущность для работы с Matrix токенами
|
||||
- **Pages**: `ChatCoopPage` - UI страница без бизнес-логики
|
||||
- **Store**: Pinia store для управления состоянием токена
|
||||
- **API**: Функции для GraphQL запросов
|
||||
|
||||
## Зависимости
|
||||
|
||||
- GraphQL API с resolver `getToken`
|
||||
- Matrix Synapse сервер
|
||||
- Element Web клиент
|
||||
@@ -0,0 +1,121 @@
|
||||
# Справка по обновленным текстам системы Powerup
|
||||
|
||||
## Обзор изменений
|
||||
|
||||
Все тексты интерфейса системы powerup переработаны для понятного объяснения работы системы пользователям-кооперативам без технического образования.
|
||||
|
||||
## Основные концепции, раскрытые в текстах
|
||||
|
||||
### 1. Система квот и ресурсов
|
||||
- **1 квота = 5 AXON** на 24 часа
|
||||
- Распределение: 50% RAM (~24.4 КБ), 25% CPU, 25% NET
|
||||
- Курс конвертации: **1 AXON = 10 RUB**
|
||||
- Курс RAM: **1 байт = 0.0001 AXON**
|
||||
|
||||
### 2. Пакеты документов
|
||||
- Один пакет документов ≈ 10 КБ RAM
|
||||
- Одна квота позволяет хранить ~2 пакета документов одновременно
|
||||
- **Ключевой момент**: квоту можно использовать многократно в течение 24 часов после освобождения памяти
|
||||
- Квота ограничивает **одновременное** использование, а не общее количество операций за сутки
|
||||
|
||||
### 3. Регистрация аккаунтов
|
||||
- **1 AXON за аккаунт** (единоразовая оплата за постоянное хранение)
|
||||
- Дополнительно требуются квоты для обработки регистрационных документов
|
||||
|
||||
### 4. Автоматическое управление
|
||||
- При использовании >70% любого ресурса → автоматическая аренда квоты за 5 AXON
|
||||
- Ежедневное автоматическое пополнение в 00:00 на минимальную квоту (5 AXON)
|
||||
- Автоматическое продление аренды при возврате квот, если память используется
|
||||
|
||||
### 5. Делегаты
|
||||
- Операторы серверов, предоставляющие вычислительные мощности
|
||||
- Получают AXON за предоставление ресурсов
|
||||
- Могут обменять AXON на RUB через паевой взнос у кооператива-оператора
|
||||
|
||||
## Измененные файлы
|
||||
|
||||
### 1. ResourceInfoWidget.vue
|
||||
**Основной информационный блок**
|
||||
- Обновлено краткое описание системы аренды
|
||||
- Полностью переработан диалог "Как это работает" с разделами:
|
||||
- Что такое вычислительные ресурсы
|
||||
- Система квот: аренда ресурсов
|
||||
- Пакеты документов
|
||||
- Многократное использование
|
||||
- Возврат и продление аренды
|
||||
- Кто предоставляет ресурсы (делегаты)
|
||||
- Регистрация аккаунтов пайщиков
|
||||
- Автоматическое управление
|
||||
|
||||
**Добавлен FAQ с 7 вопросами:**
|
||||
1. Из чего складывается стоимость пакета документов?
|
||||
2. Сколько пакетов документов я могу обработать за день на одной квоте?
|
||||
3. Почему мне списали больше, чем минимальные 5 AXON в день?
|
||||
4. Что будет, если у меня закончатся AXON?
|
||||
5. Как узнать, сколько AXON мне нужно в месяц?
|
||||
6. Могу ли я вернуть неиспользованные AXON?
|
||||
|
||||
### 2. CpuResourceWidget.vue
|
||||
Обновлена подсказка:
|
||||
- Старая: "CPU ресурсы восстанавливаются со временем. Текущее использование показывает нагрузку в данный момент."
|
||||
- Новая: "CPU используется для вычислений при обработке документов. Квота восстанавливается автоматически через несколько минут после использования."
|
||||
|
||||
### 3. NetResourceWidget.vue
|
||||
Обновлена подсказка:
|
||||
- Старая: "Сетевые ресурсы восстанавливаются со временем. Показывают объем переданных данных в байтах."
|
||||
- Новая: "NET используется для передачи данных документов между узлами блокчейна. Квота восстанавливается автоматически через несколько минут."
|
||||
|
||||
### 4. RamResourceWidget.vue
|
||||
Обновлена подсказка:
|
||||
- Старая: "RAM ресурсы приобретаются и освобождаются. Показывают объем занятой памяти в байтах."
|
||||
- Новая: "RAM используется для хранения данных пайщиков и документов в смарт-контрактах. Освобождается после завершения обработки документов."
|
||||
|
||||
### 5. AxonWalletCard.vue
|
||||
Обновлена подсказка:
|
||||
- Старая: "AXON используется для оплаты пакетов документов. Минимально 5 AXON в день, по факту - от использования."
|
||||
- Новая: "AXON используется для аренды вычислительных ресурсов (минимум 5 AXON/день) и регистрации пайщиков (1 AXON/аккаунт). Курс: 1 AXON = 10 RUB."
|
||||
|
||||
## Примеры расчетов для пользователей
|
||||
|
||||
### Минимальный бюджет (1500 RUB / 150 AXON в месяц)
|
||||
- 30 дней × 5 AXON = 150 AXON
|
||||
- Покрывает только минимальные ежедневные квоты
|
||||
- Регистрация пайщиков требует дополнительных AXON
|
||||
|
||||
### Средний кооператив (50 пайщиков, умеренная активность)
|
||||
- Базовые квоты: ~150 AXON
|
||||
- Дополнительные квоты при активности: ~30 AXON
|
||||
- Буфер безопасности: ~20 AXON
|
||||
- **Итого: ~200 AXON в месяц (2000 RUB)**
|
||||
|
||||
### Регистрация пайщиков
|
||||
- **Последовательно**: 145 пайщиков за день на одной квоте (если быстро обрабатываются)
|
||||
- **Одновременно**: ~6 пайщиков (3 пакета по 2 пайщика) на одной квоте
|
||||
- **При массовой регистрации**: планировать дополнительные квоты
|
||||
|
||||
## Ключевые сообщения для пользователей
|
||||
|
||||
1. **Вы платите за реальное использование**, а не за абстрактные единицы
|
||||
2. **Квоты можно использовать многократно** в течение 24 часов
|
||||
3. **Стоимость прозрачна и предсказуема**: 1 AXON = 10 RUB
|
||||
4. **Система автоматическая**: не требует постоянного контроля
|
||||
5. **Делегаты получают справедливую оплату** за поддержку инфраструктуры
|
||||
6. **Рекомендуемый буфер**: 50-100 AXON для бесперебойной работы
|
||||
|
||||
## Стиль подачи информации
|
||||
|
||||
- ✅ Понятные аналогии (аренда вычислительных мощностей)
|
||||
- ✅ Конкретные примеры расчетов
|
||||
- ✅ Акцент на пакеты документов (знакомая концепция)
|
||||
- ✅ Объяснение динамической стоимости
|
||||
- ✅ Практические рекомендации
|
||||
- ❌ Без технического жаргона
|
||||
- ❌ Без лишнего пафоса
|
||||
- ❌ Без упрощения технических деталей (мс, КБ оставлены)
|
||||
|
||||
## Примечания для дальнейшего развития
|
||||
|
||||
1. FAQ будет дополняться на основе реальных вопросов пользователей
|
||||
2. Пороги автоматического пополнения сейчас в абсолютных величинах (конфиг), планируется переход на проценты
|
||||
3. Все расчеты основаны на текущем курсе: 1 AXON = 10 RUB, 1 байт = 0.0001 AXON
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# Руководство по системе вычислительных ресурсов COOPOS
|
||||
|
||||
## Для кого это руководство
|
||||
|
||||
Это руководство предназначено для администраторов кооперативов, которые управляют бюджетом вычислительных ресурсов в системе COOPOS.
|
||||
|
||||
---
|
||||
|
||||
## Что нужно знать
|
||||
|
||||
### 💰 Базовая экономика
|
||||
|
||||
**1 AXON = 10 рублей**
|
||||
|
||||
Это служебный токен для оплаты вычислительных ресурсов в блокчейне COOPOS.
|
||||
|
||||
### 📦 Что такое квота
|
||||
|
||||
**1 квота = 5 AXON = 50 рублей**
|
||||
|
||||
Квота — это пакет вычислительных ресурсов на 24 часа:
|
||||
- 24.4 КБ оперативной памяти (RAM) для хранения данных документов
|
||||
- Процессорное время (CPU) для выполнения вычислений
|
||||
- Сетевой трафик (NET) для передачи данных
|
||||
|
||||
### 📄 Пакеты документов
|
||||
|
||||
Каждая операция в системе оформляется пакетом документов:
|
||||
- Регистрация пайщика → заявление + протокол совета + решение
|
||||
- Собрание → повестка + протокол + бюллетени
|
||||
- И так далее...
|
||||
|
||||
**Один пакет документов ≈ 10 КБ памяти**
|
||||
|
||||
Значит, одна квота позволяет одновременно обрабатывать 2 полных пакета документов.
|
||||
|
||||
---
|
||||
|
||||
## Как это работает
|
||||
|
||||
### 🔄 Многократное использование
|
||||
|
||||
**Важно!** Квоту можно использовать многократно в течение 24 часов.
|
||||
|
||||
После того как документ обработан и память освободилась, вы можете использовать ту же квоту для следующих документов.
|
||||
|
||||
**Пример:**
|
||||
- Утром зарегистрировали пайщика (10 КБ памяти)
|
||||
- Через час документы обработаны, память освободилась
|
||||
- Днём провели собрание (10 КБ памяти) на той же квоте
|
||||
- Вечером оформили паевой взнос — снова на той же квоте
|
||||
|
||||
### ⏰ Срок действия квоты
|
||||
|
||||
Квота арендуется на 24 часа и возвращается в тот же час, когда была получена.
|
||||
|
||||
**Что происходит при возврате:**
|
||||
- Если память не используется → ресурсы возвращаются в общий пул
|
||||
- Если в памяти есть активные данные → система автоматически продлевает аренду при наличии средств
|
||||
|
||||
### ⚙️ Автоматическое управление
|
||||
|
||||
Система сама следит за ресурсами и арендует квоты, когда нужно:
|
||||
|
||||
1. **Ежедневное пополнение** (00:00) → 5 AXON
|
||||
2. **При использовании >70%** любого ресурса → дополнительно 5 AXON
|
||||
3. **При возврате квот** с активными данными → продление аренды
|
||||
|
||||
---
|
||||
|
||||
## Что за что платится
|
||||
|
||||
### 🆕 Регистрация аккаунта пайщика
|
||||
|
||||
**1 AXON за аккаунт (единоразово)**
|
||||
|
||||
Это оплата за постоянное хранение базовой информации об аккаунте в блокчейне.
|
||||
|
||||
Плюс нужны квоты для обработки регистрационных документов.
|
||||
|
||||
### 📋 Обработка документов
|
||||
|
||||
**Стоимость зависит от:**
|
||||
- Объёма данных (паспорт, адрес, реквизиты)
|
||||
- Сложности проверок (подписи, валидация)
|
||||
- Времени хранения в памяти
|
||||
|
||||
**В среднем:** один пакет документов при хранении весь день ≈ 2.5 AXON (половина квоты)
|
||||
|
||||
Но если обработка быстрая (1-2 часа), та же квота используется повторно.
|
||||
|
||||
### 🖥️ Кому идут деньги
|
||||
|
||||
**90% → Делегатам сети**
|
||||
|
||||
Делегаты — это операторы серверов, которые:
|
||||
- Инвестируют в оборудование (серверы, память, процессоры)
|
||||
- Поддерживают работу блокчейна 24/7
|
||||
- Предоставляют вычислительные мощности
|
||||
|
||||
Полученные AXON делегаты могут обменять на рубли через кооператив-оператор.
|
||||
|
||||
**10% → Системный фонд**
|
||||
|
||||
Для покрытия операционных расходов и последующего распределения среди пайщиков.
|
||||
|
||||
---
|
||||
|
||||
## Сколько нужно AXON в месяц
|
||||
|
||||
### Минимальный кооператив (без активности)
|
||||
|
||||
**150 AXON = 1500 рублей**
|
||||
|
||||
- 30 дней × 5 AXON = 150 AXON
|
||||
- Только минимальные ежедневные квоты
|
||||
- Без регистрации новых пайщиков
|
||||
|
||||
### Средний кооператив (50 пайщиков, умеренная активность)
|
||||
|
||||
**200 AXON = 2000 рублей**
|
||||
|
||||
- Базовые квоты: 150 AXON
|
||||
- Дополнительные квоты при активности: 30 AXON
|
||||
- Буфер безопасности: 20 AXON
|
||||
|
||||
### Активный кооператив (100+ пайщиков, регулярные собрания)
|
||||
|
||||
**300-400 AXON = 3000-4000 рублей**
|
||||
|
||||
- Базовые квоты: 150 AXON
|
||||
- Частые операции: 100-150 AXON
|
||||
- Регистрация новых пайщиков: 50-100 AXON
|
||||
|
||||
---
|
||||
|
||||
## Примеры расчётов
|
||||
|
||||
### Регистрация 10 пайщиков за день
|
||||
|
||||
**Последовательно (по одному):**
|
||||
- 10 × 1 AXON = 10 AXON (оплата аккаунтов)
|
||||
- 1 квота = 5 AXON (если быстро обрабатывается)
|
||||
- **Итого: 15 AXON**
|
||||
|
||||
**Одновременно (пакетами по 3):**
|
||||
- 10 × 1 AXON = 10 AXON (оплата аккаунтов)
|
||||
- 2 квоты × 5 AXON = 10 AXON (для параллельной обработки)
|
||||
- **Итого: 20 AXON**
|
||||
|
||||
### Проведение общего собрания (50 участников)
|
||||
|
||||
- 1 квота (5 AXON) для подготовки повестки
|
||||
- 1-2 квоты (5-10 AXON) для обработки бюллетеней
|
||||
- 1 квота (5 AXON) для протокола
|
||||
- **Итого: 15-20 AXON**
|
||||
|
||||
### Месяц стандартной работы
|
||||
|
||||
- Минимальные ежедневные квоты: 150 AXON
|
||||
- 2 собрания: 40 AXON
|
||||
- 5 новых пайщиков: 10 AXON
|
||||
- Текущие операции: 20 AXON
|
||||
- **Итого: 220 AXON (2200 рублей)**
|
||||
|
||||
---
|
||||
|
||||
## Важные рекомендации
|
||||
|
||||
### ✅ Что делать
|
||||
|
||||
1. **Поддерживайте буфер 50-100 AXON** для бесперебойной работы
|
||||
2. **Регулярно проверяйте баланс** через панель мониторинга
|
||||
3. **Планируйте массовые операции** заранее (регистрация пайщиков, собрания)
|
||||
4. **Отслеживайте фактическое потребление** в логах расширения powerup
|
||||
|
||||
### ❌ Чего не делать
|
||||
|
||||
1. **Не допускайте нулевой баланс AXON** — это заблокирует все операции
|
||||
2. **Не паникуйте при дополнительных списаниях** — проверьте логи, это нормально при активной работе
|
||||
3. **Не покупайте AXON "впрок"** больше, чем нужно — планируйте на месяц
|
||||
|
||||
---
|
||||
|
||||
## Где пополнить AXON
|
||||
|
||||
Пополнение баланса AXON происходит через личный кабинет кооператива-оператора:
|
||||
|
||||
**https://лк.цифровой-кооператив.рф**
|
||||
|
||||
Курс обмена: **1 AXON = 10 RUB**
|
||||
|
||||
---
|
||||
|
||||
## Часто задаваемые вопросы
|
||||
|
||||
### Почему списалось больше, чем 5 AXON в день?
|
||||
|
||||
Дополнительные списания происходят при:
|
||||
- Автоматическом пополнении (когда использование ресурсов >70%)
|
||||
- Регистрации новых аккаунтов (1 AXON за аккаунт)
|
||||
|
||||
### Что будет, если закончатся AXON?
|
||||
|
||||
- ❌ Система не сможет автоматически пополнить квоты
|
||||
- ❌ Когда текущая квота исчерпается, все операции заблокируются
|
||||
- ❌ Пайщики не смогут подавать заявления
|
||||
- ❌ Невозможно зарегистрировать новых пайщиков
|
||||
|
||||
**Критично:** следите за балансом!
|
||||
|
||||
### Можно ли вернуть неиспользованные AXON?
|
||||
|
||||
Нет. AXON идут делегатам в качестве оплаты за предоставленные вычислительные мощности.
|
||||
|
||||
Однако неиспользованные ресурсы в рамках квоты можно использовать повторно в течение 24 часов.
|
||||
|
||||
### Сколько документов можно обработать на одной квоте?
|
||||
|
||||
**Одновременно:** 2 полных пакета документов (~10 КБ каждый)
|
||||
|
||||
**Последовательно за сутки:** от 24 до 48 пакетов (если обработка занимает 1-2 часа)
|
||||
|
||||
### Как оптимизировать расходы?
|
||||
|
||||
1. Планируйте регистрацию пайщиков последовательно, а не массово
|
||||
2. Проводите собрания в удобное время, когда ресурсов достаточно
|
||||
3. Отслеживайте пики потребления и корректируйте активность
|
||||
4. Используйте систему в рабочие часы для максимальной эффективности
|
||||
|
||||
---
|
||||
|
||||
## Техническая поддержка
|
||||
|
||||
Если у вас возникли вопросы по работе системы вычислительных ресурсов, обращайтесь в техническую поддержку кооператива-оператора.
|
||||
|
||||
---
|
||||
|
||||
**Последнее обновление:** 21 ноября 2024
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
# Архитектура Системы Документов
|
||||
|
||||
## Общее описание
|
||||
|
||||
Система документов кооператива построена на гибридной архитектуре, объединяющей блокчейн (распределённый реестр) и приватную базу данных MongoDB. Блокчейн хранит неизменяемые записи о подписанных документах с анонимизированными данными, а MongoDB содержит приватные данные для деанонимизации и полные тексты документов.
|
||||
|
||||
## Источники данных
|
||||
|
||||
### Блокчейн (Simple Explorer API)
|
||||
|
||||
Блокчейн хранит действия (actions) смарт-контракта `soviet`, которые фиксируют факты работы с документами:
|
||||
|
||||
| Действие | Описание |
|
||||
|----------|----------|
|
||||
| `newsubmitted` | Входящий пакет документов, ожидающий утверждения |
|
||||
| `newresolved` | Утверждённый пакет документов после завершения бизнес-логики |
|
||||
| `newdecision` | Протокол решения совета кооператива |
|
||||
| `newact` | Акт (например, акт приёма-передачи) |
|
||||
| `newagreement` | Связанное соглашение |
|
||||
| `newlink` | Связанный документ |
|
||||
|
||||
#### Структура блокчейн-действия
|
||||
|
||||
```typescript
|
||||
interface IAction {
|
||||
block_num: number;
|
||||
transaction_id: string;
|
||||
account: string; // 'soviet'
|
||||
name: string; // 'newsubmitted' | 'newresolved' | ...
|
||||
receiver: string; // имя кооператива
|
||||
data: {
|
||||
coopname: string; // имя кооператива
|
||||
username: string; // имя пользователя
|
||||
action: string; // тип действия ('regcoop', 'joincoop', ...)
|
||||
package: string; // идентификатор пакета документов (SHA-256)
|
||||
document: {
|
||||
version: string; // версия формата документа
|
||||
hash: string; // общий хеш
|
||||
doc_hash: string; // хеш содержимого документа
|
||||
meta_hash: string; // хеш метаданных
|
||||
meta: string; // JSON-строка метаданных
|
||||
signatures: ISignature[];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MongoDB (Приватная база данных)
|
||||
|
||||
MongoDB хранит:
|
||||
- Полные тексты документов (PDF, HTML)
|
||||
- Приватные данные пользователей (ФИО, паспортные данные, адреса)
|
||||
- Данные организаций и ИП
|
||||
|
||||
Документы извлекаются по `doc_hash` из блокчейн-записи.
|
||||
|
||||
## Жизненный цикл документа
|
||||
|
||||
### 1. Подача заявления (newsubmitted)
|
||||
|
||||
Когда пользователь подаёт заявление (регистрация в кооперативе, взнос, и т.д.):
|
||||
|
||||
1. Документ генерируется и сохраняется в MongoDB
|
||||
2. Хеш документа и подпись фиксируются в блокчейне через действие `newsubmitted`
|
||||
3. Документ получает уникальный `package` идентификатор
|
||||
|
||||
### 2. Принятие решения (newresolved)
|
||||
|
||||
После прохождения бизнес-логики (решение совета, подписание актов):
|
||||
|
||||
1. Действие `newresolved` фиксируется в блокчейне с тем же `package`
|
||||
2. Связанные действия (`newdecision`, `newact`) ссылаются на этот `package`
|
||||
3. Пакет документов считается полностью завершённым
|
||||
|
||||
## Архитектура агрегации
|
||||
|
||||
### Слои архитектуры
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ ┌──────────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ DocumentResolver │ │ GetDocumentsInputDTO │ │
|
||||
│ │ (GraphQL) │ │ - username, filter, type │ │
|
||||
│ └──────────────────────┘ │ - after_block, before_block │ │
|
||||
│ │ │ - actions (фильтр по действиям)│ │
|
||||
│ ▼ └─────────────────────────────────┘ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ DocumentService │ │
|
||||
│ └──────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Domain Layer │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │DocumentDomainInteract│ ◄── GetDocumentsInputDomainInterface │
|
||||
│ └──────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │DocumentDomainService │ │ DocumentPackageAggregator │ │
|
||||
│ │ │ │ ├── DocumentPackageV0Aggregator│ │
|
||||
│ │ - generateDocument │ │ └── DocumentPackageV1Aggregator│ │
|
||||
│ │ - getImmutableSigned│ └─────────────────────────────────┘ │
|
||||
│ └──────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ Simple Explorer API │ │ DocumentAggregator │ │
|
||||
│ │ (Блокчейн данные) │ │ (Сборка агрегата документа) │ │
|
||||
│ └──────────────────────┘ └─────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Infrastructure Layer │
|
||||
│ ┌──────────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ DocumentRepository │ │ GeneratorInfrastructureService │ │
|
||||
│ │ (MongoDB) │ │ (Генерация PDF) │ │
|
||||
│ └──────────────────────┘ └─────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Агрегат пакета документов (DocumentPackageAggregate)
|
||||
|
||||
Пакет документов агрегирует все связанные документы:
|
||||
|
||||
```typescript
|
||||
interface DocumentPackageAggregateDomainInterface {
|
||||
statement: StatementDetailAggregateDomainInterface | null; // Заявление
|
||||
decision: DecisionDetailAggregateDomainInterface | null; // Решение совета
|
||||
acts: ActDetailAggregateDomainInterface[]; // Акты
|
||||
links: DocumentAggregateDomainInterface[]; // Связанные документы
|
||||
}
|
||||
```
|
||||
|
||||
### Детали заявления (StatementDetail)
|
||||
|
||||
```typescript
|
||||
interface StatementDetailAggregateDomainInterface {
|
||||
action: ExtendedBlockchainActionDomainInterface; // Блокчейн-действие
|
||||
documentAggregate: DocumentAggregateDomainInterface; // Агрегат документа
|
||||
}
|
||||
```
|
||||
|
||||
### Агрегат документа (DocumentAggregate)
|
||||
|
||||
```typescript
|
||||
interface DocumentAggregateDomainInterface {
|
||||
hash: string;
|
||||
signedDocument: ExtendedSignedDocumentDomainInterface; // Подписанный документ
|
||||
fullDocument: DocumentDomainEntity; // Полный документ из MongoDB
|
||||
}
|
||||
```
|
||||
|
||||
## Процесс агрегации
|
||||
|
||||
### 1. Запрос документов
|
||||
|
||||
```typescript
|
||||
// GraphQL Query
|
||||
query {
|
||||
getDocuments(data: {
|
||||
username: "cooperative1"
|
||||
type: "newsubmitted"
|
||||
actions: ["regcoop", "joincoop"]
|
||||
after_block: 100
|
||||
before_block: 500
|
||||
}) {
|
||||
items { ... }
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Формирование фильтра
|
||||
|
||||
```typescript
|
||||
// DocumentDomainInteractor
|
||||
const blockFilters = {
|
||||
receiver: username,
|
||||
block_num: { $gte: after_block, $lte: before_block },
|
||||
'data.action': { $in: actions } // Фильтр по типам действий
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Запрос к блокчейну
|
||||
|
||||
```typescript
|
||||
// getActions -> Simple Explorer API
|
||||
GET /get-actions?filter={
|
||||
"account": "soviet",
|
||||
"name": "newsubmitted",
|
||||
"receiver": "cooperative1",
|
||||
"data.action": { "$in": ["regcoop", "joincoop"] },
|
||||
"block_num": { "$gte": 100, "$lte": 500 }
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Агрегация данных
|
||||
|
||||
Для каждого блокчейн-действия:
|
||||
|
||||
1. **Определение версии документа** (v0 или v1+)
|
||||
2. **Получение заявления (Statement)**:
|
||||
- Извлечение документа из MongoDB по `doc_hash`
|
||||
- Получение данных подписанта из приватной базы
|
||||
- Создание сертификата пользователя
|
||||
3. **Получение решения (Decision)**:
|
||||
- Поиск `newdecision` по `package`
|
||||
- Извлечение документа решения
|
||||
4. **Получение актов (Acts)**:
|
||||
- Поиск всех `newact` по `package`
|
||||
- Извлечение документов актов
|
||||
5. **Получение связанных документов (Links)**:
|
||||
- Поиск `newlink` и `newagreement`
|
||||
- Обработка ссылок в метаданных документов
|
||||
|
||||
## Типы действий (data.action)
|
||||
|
||||
Типы действий определены в enum `DocumentAction`:
|
||||
|
||||
```typescript
|
||||
export enum DocumentAction {
|
||||
REGCOOP = 'regcoop', // Регистрация кооператива
|
||||
JOINCOOP = 'joincoop', // Вступление в кооператив
|
||||
CONTRIBUTE = 'contribute', // Взнос в кооператив
|
||||
WITHDRAW = 'withdraw', // Выход из кооператива
|
||||
// Другие действия могут быть добавлены по мере необходимости
|
||||
}
|
||||
```
|
||||
|
||||
| Действие | Описание |
|
||||
|----------|----------|
|
||||
| `REGCOOP` | Регистрация кооператива |
|
||||
| `JOINCOOP` | Вступление в кооператив |
|
||||
| `CONTRIBUTE` | Взнос в кооператив |
|
||||
| `WITHDRAW` | Выход из кооператива |
|
||||
|
||||
## Версионирование документов
|
||||
|
||||
### Версия 0 (Legacy)
|
||||
|
||||
Старый формат без поля `version` в документе. Обрабатывается `DocumentPackageV0Aggregator`.
|
||||
|
||||
### Версия 1+
|
||||
|
||||
Новый формат с явным полем `version`. Обрабатывается `DocumentPackageV1Aggregator`.
|
||||
|
||||
```typescript
|
||||
// Определение версии
|
||||
const docVersion = rawAction.data?.document?.version ?? '0';
|
||||
```
|
||||
|
||||
## Хеширование и подписи
|
||||
|
||||
### Типы хешей
|
||||
|
||||
| Хеш | Описание |
|
||||
|-----|----------|
|
||||
| `hash` | Общий хеш документа |
|
||||
| `doc_hash` | Хеш содержимого документа (используется для извлечения из MongoDB) |
|
||||
| `meta_hash` | Хеш метаданных |
|
||||
|
||||
### Структура подписи
|
||||
|
||||
```typescript
|
||||
interface ISignature {
|
||||
id: number;
|
||||
signed_hash: string;
|
||||
signer: string; // username подписанта
|
||||
public_key: string;
|
||||
signature: string;
|
||||
signed_at: string;
|
||||
meta: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Фильтрация и пагинация
|
||||
|
||||
### Доступные фильтры
|
||||
|
||||
| Параметр | Тип | Описание |
|
||||
|----------|-----|----------|
|
||||
| `type` | `'newsubmitted' \| 'newresolved'` | Тип действия (входящие/утверждённые) |
|
||||
| `username` | `string` | Имя пользователя (receiver) |
|
||||
| `filter` | `Record<string, unknown>` | Произвольный MongoDB-фильтр |
|
||||
| `actions` | `DocumentAction[]` | Массив типов действий для фильтрации |
|
||||
| `after_block` | `number` | Блок начала диапазона |
|
||||
| `before_block` | `number` | Блок конца диапазона |
|
||||
| `page` | `number` | Номер страницы |
|
||||
| `limit` | `number` | Количество записей на странице |
|
||||
|
||||
### Пример использования фильтра actions
|
||||
|
||||
```typescript
|
||||
import { DocumentAction } from '~/domain/document/enums/document-action.enum';
|
||||
|
||||
// В коде:
|
||||
const actions = [DocumentAction.REGCOOP, DocumentAction.JOINCOOP];
|
||||
```
|
||||
|
||||
```graphql
|
||||
query {
|
||||
getDocuments(data: {
|
||||
username: "cooperative1"
|
||||
type: "newsubmitted"
|
||||
actions: ["regcoop"] # Только документы регистрации кооператива
|
||||
filter: {}
|
||||
}) {
|
||||
items {
|
||||
statement {
|
||||
action {
|
||||
name
|
||||
data
|
||||
}
|
||||
documentAggregate {
|
||||
hash
|
||||
}
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Деанонимизация данных
|
||||
|
||||
Процесс деанонимизации происходит на этапе агрегации:
|
||||
|
||||
1. **Извлечение username** из блокчейн-действия
|
||||
2. **Запрос приватных данных** из MongoDB через `AccountDomainService`
|
||||
3. **Создание сертификата** пользователя через `UserCertificateDomainService`
|
||||
4. **Прикрепление сертификата** к `ExtendedBlockchainActionDomainInterface`
|
||||
|
||||
```typescript
|
||||
const account = await this.accountDomainService.getPrivateAccount(rawData.username);
|
||||
const actor_certificate = this.userCertificateService.createCertificateFromUserData(account);
|
||||
```
|
||||
|
||||
## Связанные файлы
|
||||
|
||||
### Application Layer
|
||||
- `src/application/document/dto/get-documents-input.dto.ts`
|
||||
- `src/application/document/services/document.service.ts`
|
||||
- `src/application/document/resolvers/document.resolver.ts`
|
||||
|
||||
### Domain Layer
|
||||
- `src/domain/document/interactors/document.interactor.ts`
|
||||
- `src/domain/document/services/document-domain.service.ts`
|
||||
- `src/domain/document/aggregators/document-package.aggregator.ts`
|
||||
- `src/domain/document/aggregators/document-package-v1.aggregator.ts`
|
||||
- `src/domain/document/aggregators/document.aggregator.ts`
|
||||
|
||||
### Enums
|
||||
- `src/domain/document/enums/document-action.enum.ts`
|
||||
|
||||
### Interfaces
|
||||
- `src/domain/document/interfaces/document-package-aggregate-domain.interface.ts`
|
||||
- `src/domain/document/interfaces/statement-detail-aggregate-domain.interface.ts`
|
||||
- `src/domain/document/interfaces/decision-detail-aggregate-domain.interface.ts`
|
||||
- `src/domain/document/interfaces/get-documents-input-domain.interface.ts`
|
||||
|
||||
## Известные особенности и TODO
|
||||
|
||||
1. **Путаница hash/doc_hash**: В некоторых местах используется `hash`, в других `doc_hash`. Требуется унификация.
|
||||
|
||||
2. **Версионирование агрегаторов**: Существуют V0 и V1 агрегаторы. V0 — legacy, требует миграции.
|
||||
|
||||
3. **Производительность**: Агрегация делает множество последовательных запросов к API и MongoDB. Возможна оптимизация через batch-запросы.
|
||||
|
||||
4. **Валидация подписей**: Подписи валидируются через `Classes.Document.validateSignature()`, но результат не всегда используется для бизнес-логики.
|
||||
|
||||
5. **Миграция в PostgresQL**: предстоит миграция из MongoDB в postgres.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user