Compare commits

...

435 Commits

Author SHA1 Message Date
Alex Ant e0e8fef265 audio-stream 2026-02-23 20:21:46 +05:00
Alex Ant deb0f8344b chore(release): publish 2026-02-22 19:48:58 +05:00
Alex Ant ed135950eb openReplay ingest make throw proxy 2026-02-22 19:48:09 +05:00
Alex Ant ed7c547a06 chore(release): publish 2026-02-22 19:35:00 +05:00
Alex Ant 1a9f927c4c service-worker update fix & voting console.error log 2026-02-22 19:33:41 +05:00
Alex Ant 350af86b65 chore(release): publish 2026-02-22 14:17:08 +05:00
Alex Ant e136b73192 chore(release): publish 2026-02-22 13:08:00 +05:00
Alex Ant b6bf2fec21 notification center repair 2026-02-22 13:07:02 +05:00
Alex Ant 56daadbbca chore(release): publish 2026-02-21 21:06:27 +05:00
Alex Ant 7de26ec9a7 chore(release): publish 2026-02-21 20:19:13 +05:00
Alex Ant 7dbd3036df expired decision & walletPage & 30 days for expiration & formatDateFromNow with local time 2026-02-21 20:17:34 +05:00
Alex Ant 687085f9c7 chore(release): publish 2026-02-21 12:43:35 +05:00
Alex Ant 96cfc7e184 chore(release): publish 2026-02-21 12:42:06 +05:00
Alex Ant 0b26433b1a update 2026-02-21 12:40:54 +05:00
Alex Ant 4f06bb40f8 chore(release): publish 2026-02-21 12:34:00 +05:00
Alex Ant 46d571b65a update 2026-02-21 12:32:34 +05:00
Alex Ant 7da1bfd315 chore(release): publish 2026-02-21 12:15:08 +05:00
Alex Ant b30f47b5aa chore(release): publish 2026-02-21 12:13:59 +05:00
Alex Ant ccc6c7206c publish packages fix 2026-02-21 12:11:38 +05:00
Alex Ant a549f4cae0 chore(release): publish 2026-02-21 11:38:11 +05:00
Alex Ant 07c451ce22 fix npm publish-packages 2026-02-21 11:36:49 +05:00
Alex Ant 62da624369 chore(release): publish 2026-02-21 11:31:18 +05:00
Alex Ant a1a9b03180 chore(release): publish 2026-02-20 16:55:19 +05:00
Alex Ant e4c1d37c35 VideoPlayer and wallet fix 2026-02-20 16:54:06 +05:00
Alex Ant 42cacac256 chore(release): publish 2026-02-20 15:28:41 +05:00
Alex Ant 2816970327 update 2026-02-20 15:27:33 +05:00
Alex Ant 5744fcdcb2 chore(release): publish 2026-02-20 15:21:56 +05:00
Alex Ant 3c412b394d capital -> blagorost program type migration 2026-02-20 15:20:53 +05:00
Alex Ant 4e64cea373 update 2026-02-20 13:17:30 +05:00
Alex Ant db579db87f update 2026-02-20 13:01:59 +05:00
Alex Ant f612a918d9 update 2026-02-20 13:00:39 +05:00
Alex Ant 1a78e06a7a chore(release): publish 2026-02-20 12:49:32 +05:00
Alex Ant 374a98ffc3 update 2026-02-20 12:29:49 +05:00
Alex Ant d723e2e273 chore(release): publish 2026-02-20 11:39:36 +05:00
Alex Ant 41030171b5 update 2026-02-20 11:38:29 +05:00
Alex Ant aa31706bcc chore(release): publish 2026-02-20 11:26:19 +05:00
Alex Ant 4e3810493d update 2026-02-20 11:25:24 +05:00
Alex Ant eeb2db92ed chore(release): publish 2026-02-20 11:09:44 +05:00
Alex Ant 5a2930029b update 2026-02-20 11:09:15 +05:00
Alex Ant 2e9a73923b chore(release): publish 2026-02-20 10:19:35 +05:00
Alex Ant 97e67f8a88 fix workflow for package deployment 2026-02-20 10:18:47 +05:00
Alex Ant 074849af74 chore(release): publish 2026-02-20 09:30:23 +05:00
Alex Ant 6939783518 update 2026-02-20 09:30:01 +05:00
Alex Ant 260e48fa48 chore(release): publish 2026-02-20 09:26:36 +05:00
Alex Ant 52c4ef5df5 fix builder 2026-02-20 09:26:10 +05:00
Alex Ant 46b634e0fa chore(release): publish 2026-02-20 00:12:44 +05:00
Alex Ant a556c29a17 fix docker base image 2026-02-20 00:12:02 +05:00
Alex Ant bbc4f508b5 chore(release): publish 2026-02-19 23:10:26 +05:00
Alex Ant fe0243549a update .env-example 2026-02-19 23:07:12 +05:00
Alex Ant d9196ba650 add stub usernames 2026-02-19 22:53:22 +05:00
Alex Ant a0ea1fa392 pre-release 2026-02-19 22:01:50 +05:00
Alex Ant 195bcb4495 WTF 2026-02-19 16:41:15 +05:00
Alex Ant 929627a191 candidates page and contract calculation fix 2026-02-17 09:37:01 +05:00
Alex Ant 83a313b4b7 tests and fixes 2026-02-15 11:18:32 +05:00
Alex Ant f734bfb50f documents fixs & transcription page 2026-02-12 23:27:21 +05:00
Alex Ant 9efc22d195 chatcoop secretary transcriptor 2026-02-12 16:58:24 +05:00
Alex Ant bd3bef5fb0 chatcoop default room settings change 2026-02-12 11:29:32 +05:00
Alex Ant ddbe49e847 result submission 2026-02-12 10:43:48 +05:00
Alex Ant ed9bddec0b investors are invest to blagorost program directly, not to generation contract 2026-02-10 11:38:32 +05:00
Alex Ant 73a07eeb54 intellectual investments 2026-02-10 10:40:04 +05:00
Alex Ant 6e27aed804 registration process fixs 2026-02-09 23:44:34 +05:00
Alex Ant 5f955781a6 download document package button 2026-02-07 12:41:06 +05:00
Alex Ant c124f7310e some fixes 2026-02-07 12:09:43 +05:00
Alex Ant 73ffcae549 github sync subsystem on blagorost module 2026-02-06 21:15:13 +05:00
Alex Ant 80e55ba0be make requirements as documents and editor readonly mode 2026-02-06 13:46:35 +05:00
Alex Ant eecf955096 import contributors with some peculiarities 2026-02-06 11:16:29 +05:00
Alex Ant 6426db9a5d wallet sync and contributors import 2026-02-05 22:15:42 +05:00
Alex Ant bf4753cd09 gamification fix 2026-02-05 12:50:50 +05:00
Alex Ant a592eaf383 replace editor 2026-02-05 11:18:17 +05:00
Alex Ant 75d654cec3 feat: STORY-001 Исправить логику создания участников
Изменена логика создания Contributor с "при первом вкладе" на "при подтверждении доступа к проекту".
Теперь Contributor создается автоматически при одобрении appendix (confirmClearance).

Acceptance Criteria:
- Участник отображается в виджете сразу после подписания соглашения об участии
- Не требуется делать вклад для отображения в списке участников
- Логика создания Contributor изменена с "при первом вкладе" на "при подтверждении доступа"

Technical Changes:
- Добавлена логика создания Contributor в ClearanceManagementInteractor.handleConfirmClearance()
- Contributor создается со статусом APPROVED при подтверждении доступа
- Добавлены необходимые импорты и зависимости
- Обновлен статус спринта

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-04 11:42:55 +05:00
Alex Ant a18ed37600 blagorost test restore 2026-02-04 11:07:53 +05:00
Alex Ant 6747a8dfbc refactoring possible complete 2026-02-01 13:29:29 +05:00
Alex Ant 583dfb8126 before capital contract refactoring 2026-01-31 11:36:00 +05:00
Alex Ant bda31dd086 access violation bugfix with wharfkit rollback 2026-01-29 23:19:22 +05:00
Alex Ant 283af35f3b looking for access violation bug 2026-01-29 15:51:35 +05:00
Alex Ant 82564e2580 contract access violation bug fix | documents 2026-01-29 01:56:40 +05:00
Alex Ant fc6a9026e6 blagorost documents preparing 2026-01-27 23:00:27 +05:00
Alex Ant e1538bb5ab contract optional blagorost agreement on registration 2026-01-26 10:54:53 +05:00
Alex Ant d47db969ea refactoring vars on blagorost documents 2026-01-25 22:51:35 +05:00
Alex Ant f01de37a59 before make new document models 2026-01-25 13:40:56 +05:00
Alex Ant cee429a0a0 add udata model to a factory 2026-01-25 11:48:49 +05:00
Alex Ant 321ee89676 added all blagorost documents 2026-01-25 10:30:57 +05:00
Alex Ant aa60484315 make invest statement 2026-01-23 13:00:24 +05:00
Alex Ant b807df3ee8 document names refactoring & context component 2026-01-22 18:13:45 +05:00
Alex Ant c11e922573 registration with programs and agreements 2026-01-22 13:41:02 +05:00
Alex Ant 7135a3bbeb new documents 2026-01-21 21:49:35 +05:00
Alex Ant 142b87dd67 before make git 2026-01-20 21:23:02 +05:00
Alex Ant dff4579f28 project & component initialization & InitProjectStatement & InitProjectDecision 2026-01-19 17:39:20 +05:00
Alex Ant 2b3b662a9a generationAgreement Document & project initialization process 2026-01-19 14:24:05 +05:00
Alex Ant 98bc5195b1 blagorost decision tracking factory 2026-01-18 17:31:10 +05:00
Alex Ant a02912ccbb document updates 2026-01-17 11:47:48 +05:00
Alex Ant 2f1d29dfd2 Merge branch 'project-authorization-on-create' into dev 2026-01-16 22:02:48 +05:00
Alex Ant 11efacc4cb 1002 document generation agreement 2026-01-16 22:00:43 +05:00
Alex Ant 8133d0da3d register contributor with UHD 2026-01-16 12:34:47 +05:00
Alex Ant 093d63faae add zeus 2026-01-16 12:34:18 +05:00
Alex Ant 07f5e04488 authorization 2026-01-16 12:32:34 +05:00
Alex Ant acfc97fc7c permissions and boot fixs 2026-01-15 12:15:14 +05:00
Alex Ant 6e30554143 fix button 2026-01-13 18:25:54 +05:00
Alex Ant fa1c788c41 issue and project permissions & selectors 2026-01-13 18:01:59 +05:00
Alex Ant 3a688942c3 finalize_project and convert unused project funds to global program fund 2026-01-09 18:17:34 +05:00
Alex Ant 6797522618 blagorost masterplace 2026-01-07 17:11:47 +05:00
Alex Ant 4730a53313 requirements project -> component -> issue 2026-01-06 00:26:11 +05:00
Alex Ant 5a538792f0 project logs 2026-01-05 23:46:42 +05:00
Alex Ant 5f20289767 blagorost human project log 2026-01-04 23:43:54 +05:00
Alex Ant 4c7ed97a76 blagorost convert segment dialog 2026-01-04 21:43:23 +05:00
Alex Ant a9c2228e3c repair blagorost tests 2026-01-02 19:19:55 +05:00
Alex Ant ec44fcb7cb refactoring: user and certificates 2026-01-02 18:03:17 +05:00
Alex Ant 1f9c8d261b refactoring: wallet & user-certificate 2026-01-02 17:15:43 +05:00
Alex Ant 23ab3bd416 refactoring: system 2026-01-02 16:42:15 +05:00
Alex Ant 79482775d2 refactoring: settings 2026-01-02 10:40:27 +05:00
Alex Ant 2dcb3678b0 refactoring: payment-method 2026-01-02 10:28:48 +05:00
Alex Ant 8d7c206012 refactoring: notification 2026-01-02 10:03:37 +05:00
Alex Ant c21c264426 refactoring: meet interactor 2026-01-01 20:42:29 +05:00
Alex Ant 77a39c18f1 refactoring: make adapters for extensions 2026-01-01 20:11:25 +05:00
Alex Ant 65a00b0a8e refactoring: ledger 2026-01-01 19:11:56 +05:00
Alex Ant 0c2d5679f1 extensions reinit 2026-01-01 19:05:12 +05:00
Alex Ant 75d0e0de43 gateway refactoring 2026-01-01 18:49:59 +05:00
Alex Ant 19ff864eaa gateway refactoring 2026-01-01 14:30:37 +05:00
Alex Ant 6bbee7cb10 refactoring -> clean architecture 2026-01-01 12:57:37 +05:00
Alex Ant e268923cf6 down logger lever for powerup overdrawn balance 2025-12-31 13:14:30 +05:00
Alex Ant 4fb3d7182c chore(release): publish 2025-12-31 00:08:59 +05:00
Alex Ant 6e7a6ec032 chore(release): publish 2025-12-31 00:08:00 +05:00
Alex Ant 0e92c92f87 bug: повторное общее собрание не завершало онбординг кооператива 2025-12-30 21:37:37 +05:00
Alex Ant a6f1eb39ec chore(release): publish 2025-12-30 20:49:54 +05:00
Alex Ant 21364f6239 chore(release): publish 2025-12-30 18:24:48 +05:00
Alex Ant a0527094a9 fix a little 2025-12-30 18:24:17 +05:00
Alex Ant eda890c294 chore(release): publish 2025-12-30 17:59:11 +05:00
Alex Ant 1ed8db87cc openReplay http watcher 2025-12-30 17:57:57 +05:00
Alex Ant 136892478f chore(release): publish 2025-12-30 14:41:51 +05:00
Alex Ant aee90d31a0 chore(release): publish 2025-12-30 14:33:54 +05:00
Alex Ant 6fe632fda3 add repairdec action in soviet contract 2025-12-30 14:33:00 +05:00
Alex Ant fa975a2f5b auto cancel decisions full disable and postgres booter 2025-12-30 13:45:40 +05:00
Alex Ant bd5ab14d2f chore(release): publish 2025-12-28 20:44:51 +05:00
Alex Ant 32890bfda9 chore(release): publish 2025-12-28 15:10:11 +05:00
Alex Ant a3d4113b97 sentry backend integration 2025-12-28 15:09:13 +05:00
Alex Ant 8acfa722f8 chore(release): publish 2025-12-28 14:26:21 +05:00
Alex Ant 234beba06a sentry env 2025-12-28 14:24:55 +05:00
Alex Ant 90c27a0f73 chore(release): publish 2025-12-28 12:47:59 +05:00
Alex Ant 2577a5d2c2 remove logs 2025-12-28 12:47:25 +05:00
Alex Ant 84bf10b65b chore(release): publish 2025-12-28 12:15:59 +05:00
Alex Ant 5d70f5fd97 env refactoring rollback 2025-12-28 12:15:29 +05:00
Alex Ant f42cc59d53 chore(release): publish 2025-12-28 11:33:03 +05:00
Alex Ant d12b222d52 fix tracker assistant (disable it) 2025-12-28 11:32:22 +05:00
Alex Ant b663c7f9d4 env config refactoring 2025-12-28 11:24:26 +05:00
Alex Ant 30abfa7e23 chore(release): publish 2025-12-28 10:58:32 +05:00
Alex Ant 0be32c34c5 update 2025-12-28 10:58:09 +05:00
Alex Ant 8d4863b894 chore(release): publish 2025-12-28 10:43:19 +05:00
Alex Ant ad1ae8fb54 update openScreen assistant 2025-12-28 10:42:46 +05:00
Alex Ant b31e41bfd9 chore(release): publish 2025-12-27 20:49:02 +05:00
Alex Ant 999d5a937d SSR fix 2025-12-27 20:48:15 +05:00
Alex Ant 45ea543a4b chore(release): publish 2025-12-27 20:23:58 +05:00
Alex Ant 9d72a5e02d openScreen 2025-12-27 20:21:56 +05:00
Alex Ant 8973c45b4e chore(release): publish 2025-12-27 20:04:57 +05:00
Alex Ant b3e5a5dc62 complete refactoring for a clean architecture 2025-12-27 20:03:53 +05:00
Alex Ant d8bddadd42 chore(release): publish 2025-12-27 11:42:16 +05:00
Alex Ant aa552d1c80 refactoring #4 2025-12-27 11:41:23 +05:00
Alex Ant 6daac5f442 chore(release): publish 2025-12-26 23:53:27 +05:00
Alex Ant a8c453fbda refactoring #2 2025-12-26 23:52:43 +05:00
Alex Ant 3e15da5ad7 chore(release): publish 2025-12-26 19:46:46 +05:00
Alex Ant 8e776e7d42 chore(release): publish 2025-12-26 18:48:28 +05:00
Alex Ant de3d5654b4 mono monitor mongo -> psql migration & ipn model migration 2025-12-26 18:47:31 +05:00
Alex Ant b0e19ffa30 chore(release): publish 2025-12-26 17:22:25 +05:00
Alex Ant bacf451cf5 chore(release): publish 2025-12-26 16:58:44 +05:00
Alex Ant 2a5600b6af login bugfix 2025-12-26 16:58:13 +05:00
Alex Ant bfbc78d9b7 chore(release): publish 2025-12-26 13:41:36 +05:00
Alex Ant d509281bdc delete some migrations 2025-12-26 13:40:55 +05:00
Alex Ant 866fdfb4f6 chore(release): publish 2025-12-26 13:27:55 +05:00
Alex Ant 6c14d0f4c5 fix generator port 2025-12-26 13:27:23 +05:00
Alex Ant b31a8e3bf8 chore(release): publish 2025-12-26 12:46:11 +05:00
Alex Ant 7f3261e453 migrate tokens from mongo to postgres and clean arch 2025-12-26 12:45:09 +05:00
Alex Ant 861c3617dd payment providers refactoring 2025-12-26 10:51:57 +05:00
Alex Ant 1f4b1f5421 chore(release): publish 2025-12-26 10:04:34 +05:00
Alex Ant f851460a32 fix package 2025-12-26 10:03:09 +05:00
Alex Ant 992ac6d9eb chore(release): publish 2025-12-25 23:46:03 +05:00
Alex Ant 2e9ac57930 big service refactoring 2025-12-25 23:44:07 +05:00
Alex Ant ce8d090f27 chore(release): publish 2025-12-25 14:34:36 +05:00
Alex Ant 4ec8b59fd6 chore(release): publish 2025-12-25 14:32:42 +05:00
Alex Ant 6b086af8b4 upgrade chatwoot sdk 2025-12-25 14:30:57 +05:00
Alex Ant 52f95665ed chore(release): publish 2025-12-25 10:13:13 +05:00
Alex Ant d1ce5ab105 fix build chatwoot 2025-12-25 10:12:39 +05:00
Alex Ant 0fd10649f4 chore(release): publish 2025-12-25 00:24:37 +05:00
Alex Ant 3c65900768 support widget 2025-12-25 00:23:31 +05:00
Alex Ant 0b962b4fee board members refactoring 2025-12-24 17:06:18 +05:00
Alex Ant a58633fc5f chore(release): publish 2025-12-24 13:24:14 +05:00
Alex Ant 24c2ed3ae3 chore(release): publish 2025-12-23 23:22:46 +05:00
Alex Ant 84a77d98cf refactoring sendPOST and sendGET from desktop 2025-12-23 23:19:55 +05:00
Alex Ant 20e50e02e9 delete @coopenomics/controller package from desktop 2025-12-23 19:46:51 +05:00
Alex Ant c8bb354972 chore(release): publish 2025-12-22 18:47:11 +05:00
Alex Ant 3f9b73d211 chore(release): publish 2025-12-22 18:46:36 +05:00
Alex Ant 40bbffed17 bug fix 2025-12-22 18:46:11 +05:00
Alex Ant b16ec4e809 fix blagorost contract 2025-12-22 18:39:56 +05:00
Alex Ant 9629c895d5 chore(release): publish 2025-12-17 23:29:39 +05:00
Alex Ant f4420fcc03 doc update 2025-12-17 23:28:34 +05:00
Alex Ant 17f8c3e134 chore(release): publish 2025-12-17 22:20:38 +05:00
Alex Ant 02512a522c chore(release): publish 2025-12-17 21:47:10 +05:00
Alex Ant 4974f99842 blagorost off 2025-12-17 21:46:08 +05:00
Alex Ant a3d0c32be3 chore(release): publish 2025-12-17 21:33:01 +05:00
Alex Ant 31523160fc chore(release): publish 2025-12-17 21:25:53 +05:00
Alex Ant 183d5cdeec migration and little fix 2025-12-17 21:25:02 +05:00
Alex Ant 52fed810f2 chore(release): publish 2025-12-17 20:53:43 +05:00
Alex Ant f58d63da8d blagorost documents agenda proposals 2025-12-17 20:51:34 +05:00
Alex Ant 6793e6fbdc blagorost documents 2025-12-17 18:21:24 +05:00
Alex Ant 4f88e9b97d blagorost imgs, profile and other fixs 2025-12-17 09:35:32 +05:00
Alex Ant 5004cec9b3 chore(release): publish 2025-12-14 23:42:01 +05:00
Alex Ant 76c81e4209 chore(release): publish 2025-12-14 23:40:47 +05:00
Alex Ant 323043dc01 документация по общему собранию и т.п. 2025-12-14 23:39:41 +05:00
Alex Ant 37902ce70a chore(release): publish 2025-12-13 15:14:11 +05:00
Alex Ant c69e795d49 chore(release): publish 2025-12-13 15:12:28 +05:00
Alex Ant c3b82af95a 1c doc 2025-12-13 15:11:58 +05:00
Alex Ant 24510d4828 chore(release): publish 2025-12-13 13:45:35 +05:00
Alex Ant 11a04a99f7 1ccoop availability 2025-12-13 13:44:11 +05:00
Alex Ant 6e96eeaa05 chore(release): publish 2025-12-13 13:22:25 +05:00
Alex Ant 2c16d4af5a fix 2025-12-13 13:21:46 +05:00
Alex Ant d1f9cf43de chore(release): publish 2025-12-13 12:44:06 +05:00
Alex Ant 26fb11cb30 chore(release): publish 2025-12-13 12:21:45 +05:00
Alex Ant a7a3eb8658 weasyprint -> 67 2025-12-13 12:21:17 +05:00
Alex Ant fbedcd1f5f chore(release): publish 2025-12-13 11:28:37 +05:00
Alex Ant 6033af0f23 fix 2025-12-13 11:28:10 +05:00
Alex Ant 4809786b16 chore(release): publish 2025-12-13 11:19:11 +05:00
Alex Ant a37f40ff6e weasyprint #2 2025-12-13 11:18:25 +05:00
Alex Ant 44f5cc9de0 chore(release): publish 2025-12-13 10:51:12 +05:00
Alex Ant e059d0cbd9 Dockerfile 2025-12-13 10:50:25 +05:00
Alex Ant c51040b0de chore(release): publish 2025-12-13 10:42:39 +05:00
Alex Ant eba2abc108 downgrade weasyprint and upgrade base image 2025-12-13 10:41:46 +05:00
Alex Ant ed7626009a chore(release): publish 2025-12-13 10:11:33 +05:00
Alex Ant 442413ec6b up weasyprint version 2025-12-13 10:09:25 +05:00
Alex Ant 90986df641 onboarding 2025-12-13 09:31:42 +05:00
Alex Ant 8b505dbce3 cooperative onboarding process & chat with union 2025-12-11 20:47:16 +05:00
Alex Ant 2b26e9643b chore(release): publish 2025-12-10 15:31:43 +05:00
Alex Ant eb16f01839 docs and ledger refactoring 2025-12-10 13:50:59 +05:00
Alex Ant 872cf33d1c update 2025-12-02 18:39:54 +05:00
Alex Ant afbce19028 chore(release): publish 2025-12-02 18:13:18 +05:00
Alex Ant e27228e735 chore(release): publish 2025-12-02 18:09:56 +05:00
Alex Ant 3125232114 change CNAME in docs and fix publish CI 2025-12-02 18:09:07 +05:00
Alex Ant e552840cc4 chore(release): publish 2025-12-02 14:39:34 +05:00
Alex Ant 569142845a chore(release): publish 2025-12-02 14:37:38 +05:00
Alex Ant e62d837858 1c-integration and deploy bug fix 2025-12-02 14:35:58 +05:00
Alex Ant 27badfb88a chore(release): publish 2025-11-29 00:50:18 +05:00
Alex Ant e4d0781eb8 chore(release): publish 2025-11-29 00:44:02 +05:00
Alex Ant 93c8373335 disable setInterval on server side 2025-11-29 00:42:51 +05:00
Alex Ant 42bad6b4ea chore(release): publish 2025-11-28 13:08:14 +05:00
Alex Ant 83d4bd88cf chore(release): publish 2025-11-28 13:06:11 +05:00
Alex Ant 4141267104 disable unused routes 2025-11-28 13:05:04 +05:00
Alex Ant d5778f165b remove notifications on update 2025-11-28 12:16:54 +05:00
Alex Ant 7128e26a27 chore(release): publish 2025-11-28 12:15:17 +05:00
Alex Ant 78247e9969 bug fixes and logging improve 2025-11-28 12:14:13 +05:00
Alex Ant 66d36e034e chore(release): publish 2025-11-27 19:52:17 +05:00
Alex Ant 4100ee842c update 2025-11-27 19:49:59 +05:00
Alex Ant d4857c8e6e some fixes 2025-11-27 14:26:34 +05:00
Alex Ant 0f52781489 cmdK z-top & latest tag on docker production container 2025-11-26 17:27:08 +05:00
Alex Ant 332f19cd22 chore(release): publish 2025-11-26 13:13:59 +05:00
Alex Ant 53a1c103d6 chore(release): publish 2025-11-26 12:55:34 +05:00
Alex Ant addf7a3e6b fix 2025-11-26 12:54:19 +05:00
Alex Ant c76078ea06 chore(release): publish 2025-11-26 12:51:24 +05:00
Alex Ant 9299288e8a bug fix: chat loading and console.logs for debug router problem 2025-11-26 12:49:42 +05:00
Alex Ant dc34f24ee2 chore(release): publish 2025-11-26 11:23:40 +05:00
Alex Ant fe83bf1483 fix latest tag on testnet 2025-11-26 11:22:41 +05:00
Alex Ant c184f9281d chore(release): publish 2025-11-26 00:14:11 +05:00
Alex Ant 3127a69b67 chore(release): publish 2025-11-26 00:10:29 +05:00
Alex Ant d7326c4f82 chore(release): publish 2025-11-25 16:12:22 +05:00
Alex Ant f8478284b6 backward capability 2025-11-25 16:11:26 +05:00
Alex Ant 1c910de4b1 chore(release): publish 2025-11-25 10:56:15 +05:00
Alex Ant 2c3cb955ef fix 2025-11-25 10:55:36 +05:00
Alex Ant 1d7746d839 chore(release): publish 2025-11-25 10:10:36 +05:00
Alex Ant e16d2ba238 chore(release): publish 2025-11-24 22:26:05 +05:00
Alex Ant 60f8b448ec coopgram -> chatcoop & mobile client instruction 2025-11-24 22:24:33 +05:00
Alex Ant c0d61f29a8 empty settings fix & matrix room creation fix 2025-11-24 20:10:57 +05:00
Alex Ant c47b99f9ee chore(release): publish 2025-11-24 19:02:09 +05:00
Alex Ant 653429a6d6 docs generation fix & docker compose env missing params fix 2025-11-24 19:01:23 +05:00
Alex Ant c2250e1817 chore(release): publish 2025-11-24 17:35:56 +05:00
Alex Ant 92ca48d65f make common group room & fix deploy workflow 2025-11-24 17:33:17 +05:00
Alex Ant 2f53d9a79d chatcoop prerelease 2025-11-24 11:44:59 +05:00
Alex Ant d025bfe2da Merge branch 'element' into dev 2025-11-22 15:28:41 +05:00
Alex Ant fbc2460ac4 typeorm fix expiration and add complete & fail date 2025-11-22 15:23:49 +05:00
Alex Ant 76c2800275 extensions migration system, extensions logs, powerup log 2025-11-22 10:59:08 +05:00
Alex Ant 536bf984ee powerup page 2025-11-21 19:33:09 +05:00
Alex Ant 23f8af43ee chore(release): publish 2025-11-20 20:26:39 +05:00
Alex Ant eba54bf2e7 fix amount precision in document 2025-11-20 20:25:29 +05:00
Alex Ant 50270e6541 rub to axon convertation 2025-11-20 20:18:36 +05:00
Alex Ant 5f7d9e80ee chore(release): publish 2025-11-20 12:56:57 +05:00
Alex Ant a71ef35d5c delegate fees fund and convert_to_axon action 2025-11-20 12:55:59 +05:00
Alex Ant 2cf5291831 chore(release): publish 2025-11-19 19:40:44 +05:00
Alex Ant 75e3e9dd7b fix convertation system 2025-11-19 19:40:08 +05:00
Alex Ant 7a75365d2f chore(release): publish 2025-11-19 15:38:27 +05:00
Alex Ant 7a23d59aa1 injection AXON to any coop 2025-11-19 15:37:20 +05:00
Alex Ant 2583423600 chore(release): publish 2025-11-19 12:28:35 +05:00
Alex Ant 1d748a0424 generate and process AXON - GOVERN convertation 2025-11-19 12:26:05 +05:00
Alex Ant 86172790d1 chore(release): publish 2025-11-18 11:51:59 +05:00
Alex Ant 92c9103059 currentUser -> session stores refactoring 2025-11-18 11:50:07 +05:00
Alex Ant 16393dbc9d chore(release): publish 2025-11-17 22:10:07 +05:00
Alex Ant e841b0ef03 fix login bug 2025-11-17 22:09:06 +05:00
Alex Ant 27c7ae9d76 chore(release): publish 2025-11-17 14:04:59 +05:00
Alex Ant dc3a40675d fix health check 2025-11-17 14:04:07 +05:00
Alex Ant 31e65ac2e3 chore(release): publish 2025-11-17 13:32:08 +05:00
Alex Ant c2444d1b09 health-check route 2025-11-17 13:31:37 +05:00
Alex Ant 2faadae4cf chore(release): publish 2025-11-17 12:29:25 +05:00
Alex Ant d24e51c355 some fixes 2025-11-17 12:28:37 +05:00
Alex Ant 8ed8af1b9c chore(release): publish 2025-11-16 14:10:06 +05:00
Alex Ant 5f6b416f26 installer fix, parser init fix, service worker fix 2025-11-16 14:06:43 +05:00
Alex Ant 3671c05573 chore(release): publish 2025-11-15 18:53:03 +05:00
Alex Ant db6eed9292 pre-release 2025-11-15 18:51:37 +05:00
Alex Ant 4c903ba0bb chore(release): publish 2025-11-14 12:26:30 +05:00
Alex Ant 9eee216065 fix parser initializator 2025-11-14 12:25:58 +05:00
Alex Ant 9e1dd02d70 chore(release): publish 2025-11-14 11:58:32 +05:00
Alex Ant b986bf9bcc update 2025-11-14 11:56:51 +05:00
Alex Ant a3e0e62efc chore(release): publish 2025-11-14 11:25:42 +05:00
Alex Ant 8646c5acf6 boolean factory fix 2025-11-14 11:24:49 +05:00
Alex Ant dab9465755 chore(release): publish 2025-11-13 20:32:34 +05:00
Alex Ant 3a076cc4ea fix parser 2025-11-13 20:31:38 +05:00
Alex Ant 1b6d1ea0ef chore(release): publish 2025-11-13 20:19:25 +05:00
Alex Ant c490284c65 initializator for parser 2025-11-13 20:17:19 +05:00
Alex Ant c3ca329138 installation step 2025-11-13 18:33:43 +05:00
Alex Ant d0f73c32bb chore(release): publish 2025-11-13 11:18:18 +05:00
Alex Ant 79e0da7cf2 fix user notifications 2025-11-13 11:16:22 +05:00
Alex Ant b261f505aa chore(release): publish 2025-11-12 18:06:40 +05:00
Alex Ant 65c4c95cbe connection page and regcoop payer 2025-11-12 18:05:34 +05:00
Alex Ant 5b3c9c9055 chore(release): publish 2025-11-11 19:12:42 +05:00
Alex Ant d32d4f845c coop connect page 2025-11-11 19:11:25 +05:00
Alex Ant 637950d37f chore(release): publish 2025-11-10 21:01:05 +05:00
Alex Ant 0e1ede58e5 some installation process fixs and regcoop modify instead of emplace when exists 2025-11-10 20:59:36 +05:00
Alex Ant e914a11768 chore(release): publish 2025-11-10 12:47:47 +05:00
Alex Ant 73d66dab35 command+k menu 2025-11-10 12:46:45 +05:00
Alex Ant 634766a08e chore(release): publish 2025-11-10 09:47:19 +05:00
Alex Ant 5fbc450e96 ConnectionStepper 2025-11-10 09:45:11 +05:00
Alex Ant 5acd7ae2c4 chore(release): publish 2025-11-09 20:48:49 +05:00
Alex Ant 09578ddc5d provider integration 2025-11-09 20:47:16 +05:00
Alex Ant 8270801711 chore(release): publish 2025-11-09 16:26:20 +05:00
Alex Ant de91c3cfc8 sdk public constructor 2025-11-09 16:22:43 +05:00
Alex Ant 06888a2061 chore(release): publish 2025-11-08 20:58:10 +05:00
Alex Ant cde2d33d03 chore(release): publish 2025-11-08 20:56:36 +05:00
Alex Ant 9cf89f95d1 fix 2025-11-08 20:55:28 +05:00
Alex Ant 20486e15ea chore(release): publish 2025-11-08 14:18:32 +05:00
Alex Ant 420c403da1 chore(release): publish 2025-11-08 14:14:44 +05:00
Alex Ant cb8fdf3461 startup notification 2025-11-08 14:14:04 +05:00
Alex Ant 0937d08770 chore(release): publish 2025-11-08 13:59:05 +05:00
Alex Ant 9b1fd8e1aa chore(release): publish 2025-11-08 13:58:14 +05:00
Alex Ant d87861142c chore(release): publish 2025-11-08 12:47:15 +05:00
Alex Ant ff5fc1d0f3 update publish scripts 2025-11-08 12:46:45 +05:00
Alex Ant d5a671cbb8 update config 2025-11-08 12:42:51 +05:00
Alex Ant d0508cbd96 chore(release): publish 2025-11-08 11:58:39 +05:00
Alex Ant 12098cf8ac server provisioned notification workflow 2025-11-08 11:48:35 +05:00
Alex Ant 2557c35bea add dev mode to smart-contracts, make notifications external accessable, make boot works 2025-11-07 20:20:17 +05:00
Alex Ant 11f23bbd77 chore(release): publish 2025-11-07 12:25:13 +05:00
Alex Ant 2b2ec729ac external notification resolver 2025-11-07 12:24:18 +05:00
Alex Ant 9292acec8c chore(release): publish 2025-11-06 21:49:46 +05:00
Alex Ant 3095ed5375 installation process 2025-11-06 20:06:40 +05:00
Alex Ant a333e303c1 improve installation process, make privacy policy public and acceptable 2025-11-05 22:13:10 +05:00
Alex Ant bd8512feab clear install 2025-11-03 19:16:57 +05:00
Alex Ant c235fc4a7a fix save wif 2025-11-03 15:55:55 +05:00
Alex Ant 9c283b9aee invite page & set soviet refactoring 2025-11-03 14:16:37 +05:00
Alex Ant 4e824d739b all notifications going throw novu now 2025-11-03 11:09:34 +05:00
Alex Ant fcbbc792b0 make installation starts 2025-11-02 21:58:44 +05:00
Alex Ant ba7cf8e79e Merge branch 'installation' into refactoring 2025-11-02 09:48:41 +05:00
Alex Ant 4ab49d8e43 delete application, domain and infra module aggregators 2025-11-02 09:45:19 +05:00
Alex Ant 7f864db80d wtf 2025-11-01 21:39:23 +05:00
Alex Ant 772a163693 parser fix 2025-11-01 18:04:11 +05:00
Alex Ant c8e106f272 update 2025-11-01 17:53:16 +05:00
Alex Ant ade4f0a1d4 ssr bug fix 2025-11-01 17:14:23 +05:00
Alex Ant 4fcf933dd1 fix: no SSR for some modules 2025-11-01 14:03:45 +05:00
Alex Ant 1fb60771ca fix: synchronize: true in typeorm.module.ts 2025-10-31 23:32:31 +05:00
Alex Ant 2cd391e106 hide capital app for now 2025-10-31 19:52:24 +05:00
Alex Ant 196f3bcba1 level & energy system 2025-10-27 21:36:02 +05:00
Alex Ant bc73c5ca86 contract gamification 2025-10-27 16:19:56 +05:00
Alex Ant f4badc15d1 редактирование участника, шаблон "о себе" 2025-10-27 12:50:51 +05:00
Alex Ant 76f731d0cd poll requests for update content 2025-10-26 18:27:08 +05:00
Alex Ant 5e22858ab4 some bugs and permissions 2025-10-26 15:45:15 +05:00
Alex Ant bc1c54c43e notifications on novu improve (sanity), make a coop settings for chairman and select default workspace / page 2025-10-25 20:19:10 +05:00
Alex Ant 95ac0d36f8 a lot of changes 2025-10-22 19:53:40 +05:00
Alex Ant 2bee3eb179 unique participants count and workflow notification 2025-10-21 17:13:20 +05:00
Alex Ant 52165f2a0b microWallet & workspaceMenu rework 2025-10-20 21:12:53 +05:00
Alex Ant 9f2da226f4 extensionsStore refactoring and make it with multi-desktops 2025-10-20 17:26:24 +05:00
Alex Ant 46b999317d result submission 2025-10-20 15:35:32 +05:00
Alex Ant 80bce3c9c2 refactoring, optimization and improvement 2025-10-17 19:33:07 +05:00
Alex Ant 90b25b4bd8 fab | visual refactoring 2025-10-16 20:34:56 +05:00
Alex Ant f562f1eafd Merge branch 'dev' into capital 2025-10-16 10:27:23 +05:00
Alex Ant ff520622ff chore(release): publish 2025-10-16 10:25:26 +05:00
Alex Ant 1a6c6a4f31 fix parser 2025-10-16 01:32:23 +05:00
Alex Ant 4a9e0388f4 fix parser start block 2025-10-16 01:30:55 +05:00
Alex Ant e47b2a971e chore(release): publish 2025-10-15 23:12:28 +05:00
Alex Ant 96777ec095 delete mongo transactions 2025-10-15 23:00:23 +05:00
Alex Ant 52019deef5 ComponentPage 2025-10-15 22:51:30 +05:00
Alex Ant c26a534ff6 secondary route menu 2025-10-15 20:36:24 +05:00
Alex Ant e5be5c0b05 syncers refactoring and frontend 2025-10-15 13:49:20 +05:00
Alex Ant 5b07f52641 hard update 2025-10-14 22:06:23 +05:00
Alex Ant e0109cb530 pushResult подготовка и отправка документов с ожиданием решения совета 2025-10-12 19:41:53 +05:00
Alex Ant 1496dd1598 approvals with auth start 2025-10-07 19:41:54 +05:00
Alex Ant efcaeb2f00 set plan and other | going to voting 2025-10-07 11:31:56 +05:00
Alex Ant 1252946fe5 permissions and booter 2025-10-05 10:55:53 +05:00
Alex Ant 2913d5f636 some widgets and filters 2025-10-02 17:35:06 +05:00
Alex Ant c50beb3d43 some updates 2025-09-29 13:34:52 +05:00
Alex Ant 46b559001a some refactoring 2025-09-28 16:38:19 +05:00
Alex Ant 55a2e2e060 agreement repo and mutation 2025-09-28 15:48:56 +05:00
Alex Ant 12364a052f desktop agremeent features 2025-09-28 13:18:38 +05:00
Alex Ant 45ac971035 entity log 2025-09-28 11:19:50 +05:00
Alex Ant 26b818c9a6 update 2025-09-27 17:58:04 +05:00
Alex Ant 007836f52e contributor registration #1 2025-09-25 20:16:58 +05:00
Alex Ant ed0982770b mutations for all document generations 2025-09-25 12:47:17 +05:00
Alex Ant dcc72e90b7 factory capital documents basic templates 2025-09-24 21:52:34 +05:00
Alex Ant b428660944 project voting skeleton 2025-09-23 12:56:22 +05:00
Alex Ant 0008dcc0ee time tracker #3 2025-09-22 22:56:45 +05:00
Alex Ant e3fbf3981d time tracker personal time limit 2025-09-22 17:04:09 +05:00
Alex Ant c5e9244a08 selector fix 2025-09-21 23:55:11 +05:00
Alex Ant e1fd823c13 time tracker 2025-09-21 23:53:21 +05:00
Alex Ant 346f0d636c time tracker sdk queries 2025-09-21 20:42:28 +05:00
Alex Ant 315a259e5b lang refactoring 2025-09-21 16:09:19 +05:00
Alex Ant 3157a03c74 time tracking service prototype 2025-09-21 14:50:18 +05:00
Alex Ant 045807a748 edit project and create commit simple button on tasks 2025-09-20 20:52:53 +05:00
Alex Ant ea0e12ac80 update 2025-09-20 17:33:07 +05:00
Alex Ant 692060af6c update 2025-09-19 20:42:16 +05:00
Alex Ant d8b23a4b5c issues create 2025-09-19 10:41:29 +05:00
Alex Ant 074bf55d6c stories complete 2025-09-18 16:28:51 +05:00
Alex Ant 84e505b817 before make pain 2025-09-17 21:23:52 +05:00
Alex Ant a12e6ff603 taskPage 2025-09-17 16:40:18 +05:00
Alex Ant b01d662a1c components 2025-09-16 23:28:34 +05:00
Alex Ant e631dad499 getState, fix some bugs 2025-09-16 17:28:01 +05:00
Alex Ant 7ab0bfdd6b optional right drawer 2025-09-15 22:52:17 +05:00
Alex Ant 35a4db43fb add all entities and make extension store better 2025-09-13 01:02:21 +05:00
Alex Ant 094028006b make new queries and selectors 2025-09-11 22:21:53 +05:00
Alex Ant c12f42fcac some selectors, queries, features and entities 2025-09-11 19:10:37 +05:00
Alex Ant 3e498b098b refactoring #2 2025-09-11 13:03:21 +05:00
Alex Ant bbf1a1a544 refactoring 2025-09-11 12:15:05 +05:00
Alex Ant 42d57f30bf update names 2025-09-10 19:28:13 +05:00
Alex Ant 19967a9ad0 update 2025-09-10 15:23:33 +05:00
Alex Ant 9d1a8f12d8 main domain model 2025-09-10 14:22:14 +05:00
Alex Ant 57bebebe09 domain entities synced with blockchain 2025-09-08 13:47:40 +05:00
Alex Ant d19303cd62 typeorm repositories 2025-09-07 21:06:54 +05:00
Alex Ant 7c96e46a37 domain entities and infrastructure mappers 2025-09-07 20:44:28 +05:00
Alex Ant f75e68f8ce before refactoring 2025-09-06 16:42:29 +05:00
Alex Ant c8d9ee2457 migration fixs 2025-09-05 23:22:49 +05:00
Alex Ant a29f23086a update migrations 2025-09-05 23:07:27 +05:00
Alex Ant 4a76365df4 migrator fix 2025-09-05 22:56:08 +05:00
Alex Ant 972264844a migrator commands and subcommands 2025-09-05 22:46:39 +05:00
Alex Ant e23e6f3c1a some domain modules for capital 2025-09-05 15:03:48 +05:00
Alex Ant 03c10382d1 save deltas and actions and forks to main repositories 2025-09-04 14:21:20 +05:00
Alex Ant 3abe24aa18 refactoring, double bus refactoring, delete notificator module, desktop fix 2025-09-03 22:30:05 +05:00
3973 changed files with 282989 additions and 42497 deletions
+1
View File
@@ -1,5 +1,6 @@
---
alwaysApply: true
globs: **/desktop/**
---
# DESKTOP
+20
View File
@@ -0,0 +1,20 @@
---
globs: notifications/src/workflows/**/*.ts
---
# Правила валидации воркфлоу
## 🚫 ID воркфлоу
- **Длина ID не должна превышать 32 символа**
- Используйте описательные, но короткие идентификаторы малыми латинскими буквами и тире.
## 🚫 Условия в шаблонах
- **Запрещено использовать JavaScript выражения в шаблонах Novu**
- Нельзя использовать:
- Тернарные операторы: `{{condition ? "text1" : "text2"}}`
- Логические операторы: `{{field && "text"}}`
- Любые другие JS конструкции
## ✅ Рекомендации
- Добавляйте текстовые поля в payload для условной логики
- Вычисляйте значения на стороне сервера перед отправкой уведомления
- Используйте только простые переменные: `{{payload.fieldName}}`
+60 -19
View File
@@ -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
+16 -2
View File
@@ -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 }}
+16 -18
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 120,
"plugins": [
"prettier-plugin-sort-imports"
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"vue.volar",
"vue.vscode-typescript-vue-plugin"
]
}
+23 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
# MONO
# Цифровой Кооператив
Система управления взаимоотношениями в кооперативе. Включает в себя полный комплект программного обеспечения для подключения к платформе "Кооперативная Экономика" и управления взаимоотношениями с пайщиками в кооперативе и самим кооперативом на основе смарт-контрактов и простой электронной подписи.
+5
View File
@@ -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
View File
@@ -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"
+10 -3
View File
@@ -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",
+47
View File
@@ -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 "Перезапуск завершен!"
+34
View File
@@ -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 готов!"
+51
View File
@@ -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 "Перезапуск завершен!"
+47
View File
@@ -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 "Перезапуск завершен!"
+93 -34
View File
@@ -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 - массив действий для транзакции
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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,
+4 -4
View File
@@ -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
View File
@@ -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> {
+109 -5
View File
@@ -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')
+31 -3
View File
@@ -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
}
+43 -11
View File
@@ -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('Кооператив предварительно подготовлен к установке совета.')
}
+265 -32
View File
@@ -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('Дополнительная инициализация для расширенного режима выполнена')
}
+294
View File
@@ -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(
+3 -3
View File
@@ -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)
+2
View File
@@ -16,3 +16,5 @@ export async function sendPostToCoopbackWithSecret(url: string, data: any) {
},
})
}
export * from './randomData'
+180
View File
@@ -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`
}
+115
View File
@@ -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 -1
View File
@@ -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, которые допустимо импортировать.
- Не создаём не используемые переменные!
+61
View File
@@ -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`
+58
View File
@@ -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)"
@@ -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"
@@ -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.*
@@ -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
@@ -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` (0100%)
- При внесении взноса (деньги, труд, идеи) участник получает **прирост энергии (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