fix(membership-exit): терминальный экран после выхода + гард повторного выхода
После completexit строка registrator::exits стирается (терминал=erase) → getMembershipExit возвращал null → оверлей пропадал, кабинет разблокировался, пайщик видел стол и мог подать выход повторно (гарда не было). - enum MembershipExitStatus += COMPLETED - getMembershipExit: терминальная фаза по blocked-аккаунту + персистентному MEMBERSHIP_EXIT-платежу (сумма возврата + статус 'Оплачено') - createMembershipExit: гард — заблокированный аккаунт не может выйти повторно (контракт уже блокирует через get_participant_or_fail; отсекаем раньше) - ExitOverlay: терминальная фаза 'Вы вышли из кооператива' (сумма + 'Оплачено' + ожидайте поступления), она же UX-гард — перекрывает кабинет - SDK Zeus regen под новый enum Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
@@ -11,6 +11,10 @@ export enum MembershipExitStatus {
|
||||
PENDING = 'pending',
|
||||
// Совет одобрил, идёт возврат паевого взноса (зарезервирован, исходящий платёж).
|
||||
AUTHORIZED = 'authorized',
|
||||
// Выход завершён: возврат оплачен, аккаунт заблокирован, пайщик вышел.
|
||||
// exits-строка на цепи уже стёрта (терминал=erase) — статус восстанавливаем
|
||||
// по blocked-аккаунту и завершённому MEMBERSHIP_EXIT-платежу.
|
||||
COMPLETED = 'completed',
|
||||
}
|
||||
|
||||
registerEnumType(MembershipExitStatus, {
|
||||
|
||||
+34
@@ -15,6 +15,7 @@ import { USER_DOMAIN_SERVICE, type UserDomainService } from '~/domain/user/servi
|
||||
import { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import { PAYMENT_METHOD_REPOSITORY, type PaymentMethodRepository } from '~/domain/common/repositories/payment-method.repository';
|
||||
import { PAYMENT_REPOSITORY, type PaymentRepository } from '~/domain/gateway/repositories/payment.repository';
|
||||
import { PaymentTypeEnum } from '~/domain/gateway/enums/payment-type.enum';
|
||||
import { MembershipExitRequestEntity } from '~/infrastructure/database/typeorm/entities/membership-exit-request.entity';
|
||||
import { tokenTypes } from '~/types/token.types';
|
||||
import { CreateMembershipExitInputDTO } from '../dto/create-membership-exit-input.dto';
|
||||
@@ -80,6 +81,15 @@ export class MembershipExitService {
|
||||
throw new ForbiddenException('Подать заявление на выход можно только за себя');
|
||||
}
|
||||
|
||||
// Гард повторного выхода: вышедший пайщик заблокирован on-chain (completexit →
|
||||
// status=blocked + удалён из участников). Контракт exitcoop его уже не пропустит
|
||||
// (get_participant_or_fail), но отсекаем раньше — чтобы не плодить off-chain
|
||||
// черновик и не доводить пайщика до провала на шаге подтверждения по ссылке.
|
||||
const account = await this.accountBlockchainPort.getUserAccount(data.username);
|
||||
if (String(account?.status) === 'blocked') {
|
||||
throw new BadRequestException('Вы уже вышли из кооператива — повторный выход невозможен.');
|
||||
}
|
||||
|
||||
// Гейт реквизитов: выход нельзя запускать, пока у пайщика нет реквизитов для
|
||||
// получения возврата паевого взноса — иначе при одобрении совета исходящий
|
||||
// платёж возврата некуда будет создать (см. MembershipExitAuthorizationListener).
|
||||
@@ -235,6 +245,30 @@ export class MembershipExitService {
|
||||
};
|
||||
}
|
||||
|
||||
// Терминальная фаза: выход завершён on-chain. После completexit строка
|
||||
// registrator::exits СТЁРТА (терминал=erase), аккаунт переведён в blocked.
|
||||
// exits-строки уже нет — источник терминального состояния: blocked-аккаунт +
|
||||
// персистентный MEMBERSHIP_EXIT-платёж в gateway (сумма возврата + «оплачено»).
|
||||
// Так кабинет остаётся заблокированным экраном «вы вышли» и после стирания
|
||||
// exits, и при следующих входах (blocked-статус живёт on-chain).
|
||||
const account = await this.accountBlockchainPort.getUserAccount(username);
|
||||
if (String(account?.status) === 'blocked') {
|
||||
const payment = await this.paymentRepository.findLatestByUsernameAndType(
|
||||
username,
|
||||
PaymentTypeEnum.MEMBERSHIP_EXIT
|
||||
);
|
||||
if (payment) {
|
||||
const precision = config.blockchain.root_govern_precision ?? 4;
|
||||
return {
|
||||
exit_hash: payment.hash,
|
||||
status: MembershipExitStatus.COMPLETED,
|
||||
quantity: `${payment.quantity.toFixed(precision)} ${payment.symbol}`,
|
||||
payment_status: payment.status,
|
||||
created_at: (payment.completed_at ?? payment.created_at).toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -25617,7 +25617,7 @@ export type ModelTypes = {
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateExpenseProposalStatementDocument: ModelTypes["GeneratedDocument"],
|
||||
/** Сгенерировать протокол решения по предложенной повестке
|
||||
/** Сгенерировать протокол решения по предложенно�� повестке
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateFreeDecision: ModelTypes["GeneratedDocument"],
|
||||
@@ -27092,7 +27092,7 @@ export type ModelTypes = {
|
||||
capitalResults: ModelTypes["PaginatedCapitalResultsPaginationResult"],
|
||||
/** Получение одного сегмента кооператива по фильтрам */
|
||||
capitalSegment?: ModelTypes["CapitalSegment"] | undefined | null,
|
||||
/** Получение списка сегментов кооператива с фильтрацией и пагинацией */
|
||||
/** Получение списка се��ментов кооператива с фильтрацией и пагинацией */
|
||||
capitalSegments: ModelTypes["PaginatedCapitalSegmentsPaginationResult"],
|
||||
/** Получение полного состояния CAPITAL контракта кооператива */
|
||||
capitalState?: ModelTypes["CapitalState"] | undefined | null,
|
||||
@@ -35477,7 +35477,7 @@ export type GraphQLTypes = {
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateExpenseProposalStatementDocument: GraphQLTypes["GeneratedDocument"],
|
||||
/** Сгенерировать протокол решения по предложенной повестке
|
||||
/** Сгенерировать протокол решения по предложенно�� повестке
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateFreeDecision: GraphQLTypes["GeneratedDocument"],
|
||||
@@ -37098,7 +37098,7 @@ export type GraphQLTypes = {
|
||||
capitalResults: GraphQLTypes["PaginatedCapitalResultsPaginationResult"],
|
||||
/** Получение одного сегмента кооператива по фильтрам */
|
||||
capitalSegment?: GraphQLTypes["CapitalSegment"] | undefined | null,
|
||||
/** Получение списка сегментов кооператива с фильтрацией и пагинацией */
|
||||
/** Получение списка се��ментов кооператива с фильтрацией и пагинацией */
|
||||
capitalSegments: GraphQLTypes["PaginatedCapitalSegmentsPaginationResult"],
|
||||
/** Получение полного состояния CAPITAL контракта кооператива */
|
||||
capitalState?: GraphQLTypes["CapitalState"] | undefined | null,
|
||||
@@ -39433,6 +39433,7 @@ export enum ManagedRoomKind {
|
||||
export enum MembershipExitStatus {
|
||||
AUTHORIZED = "AUTHORIZED",
|
||||
AWAITING_CONFIRMATION = "AWAITING_CONFIRMATION",
|
||||
COMPLETED = "COMPLETED",
|
||||
PENDING = "PENDING"
|
||||
}
|
||||
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
|
||||
|
||||
@@ -74,6 +74,13 @@ const isAuthorized = computed(
|
||||
() => exitStatus.value?.status === Zeus.MembershipExitStatus.AUTHORIZED,
|
||||
);
|
||||
|
||||
// Терминал: выход завершён, возврат оплачен, аккаунт заблокирован. Этот экран —
|
||||
// и есть защита от повторного выхода: пока он показан, кабинет перекрыт и кнопку
|
||||
// «выход из кооператива» не достать (бэкенд и контракт повтор тоже отклоняют).
|
||||
const isCompleted = computed(
|
||||
() => exitStatus.value?.status === Zeus.MembershipExitStatus.COMPLETED,
|
||||
);
|
||||
|
||||
// Статус исходящего платежа возврата → чип у суммы (виден кассиру и пайщику).
|
||||
const PAYMENT_CHIPS: Record<string, { label: string; variant: BaseChipVariant }> = {
|
||||
[Zeus.PaymentStatus.PENDING]: { label: 'Ожидает оплаты', variant: 'warn' },
|
||||
@@ -91,8 +98,19 @@ const paymentChip = computed(() =>
|
||||
paymentStatus.value ? PAYMENT_CHIPS[paymentStatus.value] ?? null : null,
|
||||
);
|
||||
|
||||
// Содержимое экрана по фазе выхода: ожидание письма → рассмотрение советом → одобрено.
|
||||
// Содержимое экрана по фазе выхода: ожидание письма → рассмотрение → одобрено → завершено.
|
||||
const view = computed(() => {
|
||||
if (isCompleted.value) {
|
||||
return {
|
||||
icon: 'check_circle',
|
||||
tone: 'pos',
|
||||
title: 'Вы вышли из кооператива',
|
||||
body: 'Возврат паевого взноса оплачен — дождитесь поступления средств на указанные реквизиты. Аккаунт заблокирован. Благодарим за участие в кооперативе.',
|
||||
amountLabel: 'Сумма возврата',
|
||||
plannedHint: false,
|
||||
canCancel: false,
|
||||
};
|
||||
}
|
||||
if (isAwaitingConfirmation.value) {
|
||||
return {
|
||||
icon: 'mark_email_unread',
|
||||
|
||||
@@ -25617,7 +25617,7 @@ export type ModelTypes = {
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateExpenseProposalStatementDocument: ModelTypes["GeneratedDocument"],
|
||||
/** Сгенерировать протокол решения по предложенной повестке
|
||||
/** Сгенерировать протокол решения по предложенно�� повестке
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateFreeDecision: ModelTypes["GeneratedDocument"],
|
||||
@@ -27092,7 +27092,7 @@ export type ModelTypes = {
|
||||
capitalResults: ModelTypes["PaginatedCapitalResultsPaginationResult"],
|
||||
/** Получение одного сегмента кооператива по фильтрам */
|
||||
capitalSegment?: ModelTypes["CapitalSegment"] | undefined | null,
|
||||
/** Получение списка сегментов кооператива с фильтрацией и пагинацией */
|
||||
/** Получение списка се��ментов кооператива с фильтрацией и пагинацией */
|
||||
capitalSegments: ModelTypes["PaginatedCapitalSegmentsPaginationResult"],
|
||||
/** Получение полного состояния CAPITAL контракта кооператива */
|
||||
capitalState?: ModelTypes["CapitalState"] | undefined | null,
|
||||
@@ -35477,7 +35477,7 @@ export type GraphQLTypes = {
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateExpenseProposalStatementDocument: GraphQLTypes["GeneratedDocument"],
|
||||
/** Сгенерировать протокол решения по предложенной повестке
|
||||
/** Сгенерировать протокол решения по предложенно�� повестке
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
generateFreeDecision: GraphQLTypes["GeneratedDocument"],
|
||||
@@ -37098,7 +37098,7 @@ export type GraphQLTypes = {
|
||||
capitalResults: GraphQLTypes["PaginatedCapitalResultsPaginationResult"],
|
||||
/** Получение одного сегмента кооператива по фильтрам */
|
||||
capitalSegment?: GraphQLTypes["CapitalSegment"] | undefined | null,
|
||||
/** Получение списка сегментов кооператива с фильтрацией и пагинацией */
|
||||
/** Получение списка се��ментов кооператива с фильтрацией и пагинацией */
|
||||
capitalSegments: GraphQLTypes["PaginatedCapitalSegmentsPaginationResult"],
|
||||
/** Получение полного состояния CAPITAL контракта кооператива */
|
||||
capitalState?: GraphQLTypes["CapitalState"] | undefined | null,
|
||||
@@ -39433,6 +39433,7 @@ export enum ManagedRoomKind {
|
||||
export enum MembershipExitStatus {
|
||||
AUTHORIZED = "AUTHORIZED",
|
||||
AWAITING_CONFIRMATION = "AWAITING_CONFIRMATION",
|
||||
COMPLETED = "COMPLETED",
|
||||
PENDING = "PENDING"
|
||||
}
|
||||
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
|
||||
|
||||
Reference in New Issue
Block a user