Все статьи

Soft Delete в РБ: как удалить аккаунт и не сломать учет

Удаление аккаунта в белорусском проекте — это не кнопка `DELETE FROM users`. Между правом пользователя на удаление, сроком в 15 дней и обязанностью хранить бухгалтерские документы легко сломать и базу, и процесс, и отношения с клиентом. Разбираю, как собрать soft delete так, чтобы не нарушить Закон №99-З и не оставить бухгалтерию без первички.

Удаление аккаунта в белорусском проекте почти никогда не сводится к одной SQL-команде. Пользователь пишет: «удалите мои данные», а у бизнеса в этот момент могут висеть оплаченные заказы, акты, накладные, логи безопасности, переписка с поддержкой и обязательства по хранению первички. Если в такой системе сделать честный hard delete, можно красиво выполнить желание пользователя и одновременно испортить учет, порвать связи в базе и создать себе отдельную проблему на случай проверки. Поэтому в реальных проектах я почти всегда проектирую не «удаление», а управляемый жизненный цикл данных: блокировка, архивирование, анонимизация, удаление там, где оно действительно допустимо.

Где здесь конфликт закона и кода

В Беларуси базовый документ по теме — Закон Республики Беларусь от 7 мая 2021 г. № 99-З «О защите персональных данных». По разъяснениям НЦЗПД о правах субъектов персональных данных оператор обязан в пятнадцатидневный срок после получения заявления прекратить обработку, удалить данные и уведомить человека об этом, если нет иных законных оснований для дальнейшей обработки. Там же прямо сказано: если удаление технически невозможно, нужно принять меры по недопущению дальнейшей обработки, включая блокирование. А в FAQ НЦЗПД отдельно объясняется, что оператор вправе отказать в удалении, если обязан хранить документы по другим актам законодательства.

Вот в этой точке у backend-разработчика и начинается настоящая работа. Потому что «данные пользователя» в продукте — это не только строка в таблице users. Это еще и данные в заказах, платежных событиях, доставке, возвратах, обращениях в саппорт, журналах доступа, резервных копиях, CRM и очередях фоновых задач. Часть из этого может жить на основании согласия, а часть — на основании договора или прямой обязанности хранения. НЦЗПД отдельно подчеркивает, что обработка данных, необходимых для исполнения договора, вообще может вестись без согласия, если без нее услугу или сделку не исполнить. Это принципиальный момент для e-commerce, SaaS с биллингом и любых сервисов с заказами. Правовые основания обработки тут полезнее любого шаблонного cookie-плагина.

Параллельно никто не отменял Закон от 12 июля 2013 г. № 57-З «О бухгалтерском учете и отчетности» и Перечень типовых документов, утвержденный постановлением Минюста № 140 от 24.05.2012. Именно поэтому идея «пользователь попросил удаление — значит стираем всё» в белорусском бизнесе часто незаконна не меньше, чем вечное хранение без оснований. На практике надо разделять: что можно удалить, что нужно обезличить, что нужно заблокировать, а что обязано жить до конца срока хранения.

Почему hard delete почти всегда ломает бухгалтерию

Самая частая ошибка — держать все пользовательские данные в одной сущности и потом пытаться физически снести ее целиком. Это выглядит просто, пока не приходит первая интеграция с платежкой, складом или бухгалтерией.

Допустим, у вас есть:

  • users
  • orders
  • payments
  • refunds
  • invoices
  • support_tickets
  • audit_logs

Если удалить users.id = 42 физически, то у вас есть три плохих варианта. Первый: каскадно удалить всё связанное. Это может уничтожить документы и историю операций, которые бизнес обязан хранить. Второй: оставить связи висячими и получить мусор в учете и аналитике. Третий: разорвать foreign key, но сохранить сырые персональные поля прямо в заказах и платежах, после чего удаление превращается в фикцию.

Поэтому я почти всегда делю данные на три слоя:

  1. Профиль и коммуникации — email, телефон, имя, маркетинговые согласия, адреса доставки, аватар, сессии.
  2. Операционные данные — заказы, статусы платежей, возвраты, обращения, события антифрода.
  3. Учетные и аудиторские данные — реквизиты документов, журнал действий, логи доступа, технические следы исполнения обязательств.

Удаление или анонимизация работают по-разному для каждого слоя. Это не «юридическая магия», а обычная инженерия модели данных.

Как я проектирую soft delete

Мой базовый принцип простой: запрос на удаление не должен менять систему мгновенно и необратимо. Он должен запускать процесс с дедлайном, проверками и понятным итогом.

Минимальный набор полей у пользователя выглядит так:

SQL
alter table users
add column status text not null default 'active',
add column deletion_requested_at timestamptz,
add column deletion_deadline_at timestamptz,
add column deleted_at timestamptz,
add column anonymized_at timestamptz,
add column deletion_reason text;

Статусы я обычно делаю такими:

  • active — обычный аккаунт
  • pending_deletion_review — пришел запрос, идет проверка
  • blocked — обработка для пользовательских целей остановлена
  • archived — доступ закрыт, маркетинг отключен, данные частично сохранены по законным основаниям
  • anonymized — идентифицирующие поля очищены или необратимо заменены
  • deleted — запись физически удалена там, где это допустимо

Сама кнопка «Удалить аккаунт» не удаляет ничего. Она создает заявку:

TypeScript
await db.user.update({
  where: { id: userId },
  data: {
    status: 'pending_deletion_review',
    deletion_requested_at: new Date(),
    deletion_deadline_at: addDays(new Date(), 15)
  }
})

await queue.add('review-user-deletion', { userId })

Дальше worker проверяет зависимости. Не только заказы, но и всё, что реально может оказаться законным основанием для хранения:

TypeScript
const dependencies = await collectUserDependencies(userId)

if (dependencies.hasAccountingDocs || dependencies.hasOpenClaims) {
  await blockUserAccount(userId)
  await disableMarketing(userId)
  await redactOptionalProfileFields(userId)

  await sendReasonedResponse(userId, {
    outcome: 'refused_full_deletion',
    reason: 'legal_retention'
  })
} else {
  await revokeSessions(userId)
  await deleteConsents(userId)
  await purgeCaches(userId)
  await anonymizeOrdersIfAllowed(userId)
  await eraseUserProfile(userId)
}

Ключевая мысль здесь не в библиотеке и не в фреймворке. Хоть FastAPI, хоть NestJS, хоть чистый Node. Важно, что решение должно быть асинхронным, проверяемым и воспроизводимым. Потому что на пятый день после запроса вам может написать не только пользователь, но и бухгалтер, и саппорт, и менеджер, который случайно отправил клиенту письмо по старому сценарию.

Что я удаляю, а что нет

В белорусских проектах нормально работает такая логика.

Обычно можно удалить или обезличить

  • email и телефон в профиле, если они больше не нужны для исполнения обязательств;
  • маркетинговые согласия и подписки;
  • сохраненные адреса, избранное, корзину;
  • refresh tokens, API sessions, устройства;
  • внутренние notes менеджеров, если они не связаны с претензией или обязательством;
  • аналитические идентификаторы, если они не нужны для безопасности и расследования инцидентов.

Обычно нельзя просто стереть по кнопке

  • первичные учетные документы и данные, встроенные в них;
  • историю платежей и возвратов;
  • данные, необходимые для исполнения или подтверждения договора;
  • логи, подтверждающие действия оператора и пользователя;
  • записи, нужные для обработки спора, chargeback, претензии или гарантийного кейса.

Здесь важен не только сам факт хранения, но и минимизация. Если заказ обязан жить, это не значит, что в нем нужно вечно хранить весь профиль клиента в исходном виде. Часто можно вынести персональные поля из операционного документа в отдельный слой и после истечения активной фазы заменить их анонимизированными значениями, сохранив сам факт хозяйственной операции.

Анонимизация лучше, чем притворный delete

Я скептически отношусь к системам, где после «удаления» у пользователя просто ставится флажок is_deleted = true, а весь профиль лежит как лежал. Это не soft delete, это отложенная проблема.

Нормальный вариант — разделить идентификатор субъекта и учетный след. Например, заказ остается, но вместо прямой связки с живым профилем получает технический маркер:

SQL
update orders
set customer_display_name = 'Deleted user',
    customer_email = null,
    customer_phone = null,
    user_id = null
where user_id = $1
  and can_be_anonymized = true;

При этом документы, которые обязаны храниться в неизменном виде, не трогаются лопатой из ORM. Для них я отдельно описываю режим доступа: кто видит, зачем, как логируется, когда запись окончательно уходит в архив и по какому регламенту уничтожается после истечения срока хранения.

Если в системе есть бэкапы, soft delete обязан учитывать и их. Я обычно не обещаю пользователю «мгновенное исчезновение из каждой резервной копии». Я обещаю прекращение активной обработки, блокировку доступа, очистку активной базы и естественное вымывание данных из backup-цепочки по регламенту ротации. Это уже не цитата из закона, а инженерная добросовестность: лучше честно описать процесс, чем продать сказку про волшебную кнопку.

Про 15 дней: это задача не юриста, а очереди и cron

Самый недооцененный кусок — не удаление, а дедлайн ответа. У НЦЗПД эта логика сформулирована очень прямо: либо прекратить обработку и удалить, либо заблокировать при технической невозможности, либо мотивированно отказать при наличии иных оснований. И все это нужно уложить в пятнадцать дней после получения заявления. Права субъектов и разъяснения НЦЗПД по сути заставляют проектировать не кнопку, а SLA внутри продукта.

Поэтому я всегда добавляю:

  • очередь задач на проверку запросов;
  • таблицу deletion_requests;
  • фоновый монитор дедлайнов;
  • шаблоны ответов для двух сценариев: удаление и мотивированный отказ;
  • аудит того, кто и когда принял решение;
  • запрет на любые маркетинговые и продуктовые коммуникации сразу после перевода аккаунта в blocked или archived.

Если этого нет, срок в 15 дней у вас существует только в политике на сайте. В коде его нет.

Договоры, роли и зачем тут нужен audit trail

Если сайт или сервис поддерживает подрядчик, сам подрядчик не всегда становится оператором данных. НЦЗПД отдельно объясняет разницу между оператором и уполномоченным лицом: оператор определяет цели и средства обработки, а уполномоченное лицо действует по его поручению. И именно оператор отвечает перед субъектом персональных данных, даже если часть обработки поручена третьей стороне. Это важный момент для студий, интеграторов и аутсорс-команд. Смотри разъяснение об операторе и уполномоченном лице и рекомендации НЦЗПД о взаимоотношениях операторов и уполномоченных лиц.

Из этого вытекает практический вывод: алгоритм soft delete надо описывать не только в коде, но и в договоре, регламенте и логировании. Кто принимает решение об отказе, кто запускает анонимизацию, кто видит архивные документы, кто отвечает на заявление, где хранится подтверждение отправки ответа — это не второстепенные детали. Это и есть работающая система.

Что это значит на практике

Если ты фрилансер или небольшая студия, не обещай клиенту «полное удаление аккаунта» как универсальную функцию. Обещай корректный сценарий обработки запроса: проверка оснований, блокировка, анонимизация, удаление там, где допустимо, и ответ в срок.

Если ты владелец интернет-магазина или сервиса с оплатой, проверь, не смешаны ли у тебя профиль пользователя и учетные документы в одной модели. Если смешаны — переделывать лучше до первого конфликта, а не после жалобы.

Если ты разработчик на аутсорсе, не оставляй тему на уровне политики конфиденциальности. В коде должны быть статусы, очередь, дедлайны, аудит и отдельные правила для документов, логов и бэкапов. Иначе soft delete у тебя существует только в Figma.

Если у тебя проект в РБ и нужно собрать такую схему без декоративного комплаенса, пиши в Telegram или через velutich.com.

VELUTICH.

© 2026 Велютич Дмитрий Игоревич УНП: BE7887089