fix(capital): заводить доли только в active-проектах, не в pending
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled

По решению пользователя заход долей Благороста — только в активные проекты.
В pending больше не заводим: при переходе pending→active придёт новая дельта
capital::projects со status=active, на ней листенер и зарегистрирует.

- Листенер ProgramShareRegistrationOnProjectDeltaListener: гейт pending|active → только active.
- ProgramShareRegistrationService.findActiveProjects (путь крона и wallet-листенера): фильтр pending|active → только active.
- Тест: pending теперь НЕ реагирует; active — позитивный кейс.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
coopops
2026-06-16 08:04:08 +00:00
parent 0d51973a7b
commit 3d04f76446
3 changed files with 24 additions and 19 deletions
@@ -7,9 +7,11 @@ import { ProjectStatus } from '../../domain/enums/project-status.enum';
import { ProgramShareRegistrationService } from '../services/program-share-registration.service';
/**
* Слушатель дельт `capital::projects`. Как только в блокчейне появляется проект
* (в т.ч. компонент) в статусе pending|active — СРАЗУ регистрирует доли всех
* активных пайщиков Благороста в нём, не дожидаясь периодического scheduler'а.
* Слушатель дельт `capital::projects`. Как только проект (в т.ч. компонент)
* оказывается в статусе active — СРАЗУ регистрирует доли всех активных пайщиков
* Благороста в нём, не дожидаясь периодического scheduler'а. В pending не
* заводим: заход только в активные проекты (решение пользователя 2026-06-16);
* при переходе pending→active придёт новая дельта со status=active.
*
* Why: окно между созданием проекта и его переводом в `result` может быть
* минутами. Контракт `regshare` принимает только pending|active, а отката
@@ -39,10 +41,11 @@ export class ProgramShareRegistrationOnProjectDeltaListener {
const value = delta.value as CapitalContract.Tables.Projects.IProject | undefined;
if (!value?.project_hash) return;
// regshare разрешён только для pending|active — на прочих статусах окно уже
// закрыто (on-chain-проверка всё равно отклонит), реагировать незачем.
// Заводим доли только в active-проектах: pending ещё не готов к заходу
// (решение пользователя 2026-06-16). При переходе pending→active придёт
// новая дельта со status=active — на ней и зайдём.
const status = String(value.status);
if (status !== ProjectStatus.PENDING && status !== ProjectStatus.ACTIVE) return;
if (status !== ProjectStatus.ACTIVE) return;
const project_hash = String(value.project_hash);
try {
@@ -47,12 +47,12 @@ export class ProgramShareRegistrationService {
) {}
/**
* Обход участников в статусах active/import, проектов pending/active; при изменении user_shares относительно capital_contributor_shares — regshare.
* Обход участников в статусах active/import по active-проектам; при изменении user_shares относительно capital_contributor_shares — regshare.
*/
async syncProgramSharesForCoop(coopname: string): Promise<void> {
const projects = await this.findActiveProjects(coopname);
if (projects.length === 0) {
this.logger.debug(`Синхронизация regshare: нет проектов в статусах pending/active для ${coopname}`);
this.logger.debug(`Синхронизация regshare: нет active-проектов для ${coopname}`);
return;
}
@@ -71,7 +71,7 @@ export class ProgramShareRegistrationService {
/**
* Точечная синхронизация regshare для одного пайщика — вызывается из listener'а
* на дельты `ledger2::userwallets[w.cap.blago]`. Не пишет лог, если у пайщика
* нет ни одного pending/active проекта.
* нет ни одного active-проекта.
*/
async syncProgramSharesForUser(coopname: string, username: string): Promise<void> {
const projects = await this.findActiveProjects(coopname);
@@ -114,11 +114,13 @@ export class ProgramShareRegistrationService {
}
}
/**
* Только active-проекты: заход долей в pending отключён (решение пользователя
* 2026-06-16) — заводим/сверяем доли лишь в активных проектах.
*/
private async findActiveProjects(coopname: string): Promise<ProjectDomainEntity[]> {
return (await this.projectRepository.findAll()).filter(
(p) =>
p.coopname === coopname &&
(p.status === ProjectStatus.PENDING || p.status === ProjectStatus.ACTIVE)
(p) => p.coopname === coopname && p.status === ProjectStatus.ACTIVE
);
}
@@ -2,8 +2,8 @@
* Unit-тесты ProgramShareRegistrationOnProjectDeltaListener.
*
* Фокус — гейтинг: доли регистрируются СРАЗУ при появлении проекта в статусе
* pending|active, и только в своём кооперативе. На present=false, чужой scope и
* статусы вне pending|active реакции нет (regshare там всё равно отклонится).
* active, и только в своём кооперативе. На pending (заводим только в active),
* present=false, чужой scope и прочие статусы реакции нет.
*/
import config from '~/config/config';
@@ -25,22 +25,22 @@ function makeDelta(overrides: Partial<IDelta> = {}, value: Record<string, any> =
return {
present: true,
scope: config.coopname,
value: { project_hash: PROJECT_HASH, status: ProjectStatus.PENDING, ...value },
value: { project_hash: PROJECT_HASH, status: ProjectStatus.ACTIVE, ...value },
...overrides,
} as IDelta;
}
describe('ProgramShareRegistrationOnProjectDeltaListener', () => {
it('pending в своём кооперативе → регистрирует доли по project_hash', async () => {
it('active в своём кооперативе → регистрирует доли по project_hash', async () => {
const service = makeServiceStub();
await makeListener(service).handleProjectDelta(makeDelta());
expect(service.syncProgramSharesForProject).toHaveBeenCalledWith(config.coopname, PROJECT_HASH);
});
it('active → тоже регистрирует', async () => {
it('pending → не реагирует (заводим только в active, при переходе в active придёт новая дельта)', async () => {
const service = makeServiceStub();
await makeListener(service).handleProjectDelta(makeDelta({}, { status: ProjectStatus.ACTIVE }));
expect(service.syncProgramSharesForProject).toHaveBeenCalledTimes(1);
await makeListener(service).handleProjectDelta(makeDelta({}, { status: ProjectStatus.PENDING }));
expect(service.syncProgramSharesForProject).not.toHaveBeenCalled();
});
it('result → не реагирует (окно закрыто, откат статуса контракт не даёт)', async () => {