feat(membership-exit): email-подтверждение выхода — backend + письмо
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled

Двойное подтверждение необратимого выхода (закрытия аккаунта): заявление
подписывается и принимается сразу, но в блокчейн уходит только после перехода
по ссылке из письма (по аналогии с verify-email/reset-key токенами).

- notifications: воркфлоу MembershipExitConfirmation (письмо со ссылкой confirmationUrl).
- token: тип CONFIRM_EXIT + generateConfirmExitToken (домен + application).
- entity membership_exit_requests (off-chain черновик подписанного заявления; uniq coopname+username; удаляется при confirm/cancel).
- MembershipExitStatus += AWAITING_CONFIRMATION; MembershipExitResult += status.
- service: createMembershipExit теперь сохраняет черновик + шлёт письмо (НЕ цепь);
  confirmMembershipExit(token) проверяет токен и шлёт exitcoop в цепь;
  cancelMembershipExit удаляет черновик+токен; getMembershipExit отдаёт off-chain фазу.
- resolver: + confirmMembershipExit (публичная, по токену) + cancelMembershipExit (auth+владелец).

Проверено вживую: мутации в схеме, enum/Result обновлены, таблица создана (synchronize).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
coopops
2026-06-15 15:08:03 +00:00
parent 0c995b1bca
commit 9ee57d1e05
11 changed files with 294 additions and 22 deletions
@@ -20,6 +20,7 @@ import { workflow as newDepositPaymentRequestWorkflow } from './new-deposit-paym
import { workflow as resetKeyWorkflow } from './reset-key';
import { workflow as inviteWorkflow } from './invite';
import { workflow as emailVerificationWorkflow } from './email-verification';
import { workflow as membershipExitConfirmationWorkflow } from './membership-exit-confirmation';
import { workflow as serverProvisionedWorkflow } from './server-provisioned';
import { workflow as decisionExpiredWorkflow } from './decision-expired';
import { workflow as chatcoopCalendarEventCreatedWorkflow } from './chatcoop-calendar-event-created';
@@ -46,6 +47,7 @@ export * as NewDepositPaymentRequest from './new-deposit-payment-request';
export * as ResetKey from './reset-key';
export * as Invite from './invite';
export * as EmailVerification from './email-verification';
export * as MembershipExitConfirmation from './membership-exit-confirmation';
export * as ServerProvisioned from './server-provisioned';
export * as DecisionExpired from './decision-expired';
export * as ChatCoopCalendarEventCreated from './chatcoop-calendar-event-created';
@@ -73,6 +75,7 @@ export const allWorkflows: WorkflowDefinition[] = [
resetKeyWorkflow,
inviteWorkflow,
emailVerificationWorkflow,
membershipExitConfirmationWorkflow,
serverProvisionedWorkflow,
decisionExpiredWorkflow,
chatcoopCalendarEventCreatedWorkflow,
@@ -0,0 +1,38 @@
import { WorkflowDefinition, type BaseWorkflowPayload } from '../../types';
import { WorkflowBuilder } from '../../base/workflow-builder';
import { createEmailStep } from '../../base/defaults';
import { z } from 'zod';
import { slugify } from '../../utils';
// Схема payload для письма-подтверждения выхода из кооператива
export const membershipExitConfirmationPayloadSchema = z.object({
confirmationUrl: z.string(),
});
export type IPayload = z.infer<typeof membershipExitConfirmationPayloadSchema>;
export interface IWorkflow extends BaseWorkflowPayload, IPayload {}
export const name = 'Подтверждение выхода из кооператива';
export const id = slugify(name);
export const workflow: WorkflowDefinition<IWorkflow> = WorkflowBuilder
.create<IWorkflow>()
.name(name)
.workflowId(id)
.description('Подтверждение по email заявления о добровольном выходе пайщика из кооператива')
.payloadSchema(membershipExitConfirmationPayloadSchema)
.tags(['auth'])
.addSteps([
createEmailStep(
'membership-exit-confirmation-email',
'Подтверждение выхода из кооператива',
'Здравствуйте!<br><br>' +
'Вы подали заявление о добровольном выходе из кооператива. Это действие необратимо: ' +
'после подтверждения запускается процедура выхода и возврата паевого взноса, а ваш аккаунт блокируется.<br><br>' +
'Если вы действительно хотите выйти из кооператива, перейдите по ссылке для подтверждения: ' +
'<a href="{{payload.confirmationUrl}}">{{payload.confirmationUrl}}</a><br><br>' +
'Если вы не подавали такое заявление — просто проигнорируйте это письмо, ничего не произойдёт.'
),
])
.build();