Архитектура и данные

Как перенести данные из старой CRM или Excel и не потерять историю бизнеса

Разработка новой CRM часто начинается с фразы:

Старую систему больше невозможно нормально поддерживать. Давайте сделаем новую.

Через некоторое время появляется интерфейс новой CRM, роли сотрудников, карточки клиентов, сделки, документы, статусы и отчёты.

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

что делать со всеми данными, накопленными за последние несколько лет?

У компании уже могут существовать десятки Excel-файлов, старая CRM, таблицы менеджеров, экспорт бухгалтерии, папки с договорами и отдельные списки клиентов.

И просто сказать:

Импортируем всё в новую базу,

недостаточно.

Потому что данные — это не только строки вроде:

Иван Петров
+7...
ivan@example.com

В старой системе может находиться история:

когда появился клиент;
кто с ним работал;
какие сделки создавались;
как менялись статусы;
какой была стоимость;
какие комментарии оставляли сотрудники;
какие документы прикладывались;
почему сделка была закрыта;
кто изменил данные.

Если после миграции сохранить только текущее состояние, новая CRM технически будет работать.

Но несколько лет истории бизнеса исчезнут.

Поэтому хорошая миграция — это не:

Excel
  ↓
INSERT INTO clients

Это отдельный инженерный проект со своими правилами, проверками и критериями приёмки.

Разберём, как мы бы проектировали такую миграцию.


Главное правило: сначала понять данные, потом писать импорт

Представим файл:

clients_final_2026.xlsx

В нём 37 колонок.

Кажется, что задача очевидна:

Excel column
        ↓
CRM field

Например:

ФИО
↓
client.name
Телефон
↓
client.phone
Email
↓
client.email

Но уже через несколько строк обнаруживается:

Телефон:
+7 900 123-45-67

Телефон:
89001234567

Телефон:
8 (900) 1234567 / рабочий

Телефон:
нет

Телефон:
+7 900 123 45 67, Мария

В колонке:

Дата

оказываются:

12.03.2024
2024-03-12
Март
весна 24
—

А поле:

Менеджер

содержит:

Иван
Иван П.
Иван Петров
И.П.
Петров Иван

Хотя это один сотрудник.

Именно поэтому импорт начинается не с написания SQL.

Он начинается с профилирования исходных данных.


Сначала нужно провести инвентаризацию источников

В реальном бизнесе данные редко находятся в одном месте.

Например:

Старая CRM
      +
clients.xlsx
      +
sales-2024.xlsx
      +
Google Sheets
      +
папка Documents
      +
экспорт бухгалтерии

При этом источники могут противоречить друг другу.

В старой CRM телефон:

+7 900 111-22-33

В Excel:

+7 900 444-55-66

Какой правильный?

Нельзя решить это универсальным:

берём последнее значение.

Потому что нужно сначала определить источник истины для каждого типа данных.

Например:

ДанныеПриоритетный источник
Клиентыстарая CRM
Финансовые суммыбухгалтерская система
Дополнительные телефоныExcel менеджеров
Документыфайловый архив
Текущий ответственныйCRM
Исторические комментарииCRM

Это уже часть спецификации миграции.


Миграция — это преобразование модели, а не копирование таблиц

Старая CRM может иметь структуру:

customers
orders
comments

Новая:

organizations
contacts
projects
deals
messages
activities
documents

Поэтому операция редко выглядит:

old.customers
↓
new.customers

Чаще:

Old customer row
      ↓
Organization
      +
Contact
      +
Membership

или наоборот.

Например старая система хранит:

Компания:
ООО Альфа

Контакт:
Иван Иванов

Телефон:
...

Последний заказ:
...

в одной строке.

Новая CRM разделяет это:

Organization
   │
   ├── Contact Ivan
   ├── Contact Maria
   │
   └── Deals

Теперь миграция должна понять семантику данных и создать несколько связанных сущностей.


Полезно создать отдельную карту миграции

До написания importer можно составить таблицу:

Старое полеНовая сущностьНовое полеПравило
companyOrganizationnametrim
fioContactfullNamenormalize
phoneContactphonenormalize E.164
managerUser relationownerIdmapping
statusDealstatusstatus mapping
createdDealcreatedAtpreserve
commentActivitytextpreserve original

Такая таблица превращает миграцию из:

Разработчик примерно понял Excel

в явный технический контракт.


Старые ID лучше не выбрасывать

Представим старую CRM:

client_id = 1842

В новой системе создаётся:

id = 9d7f...

На первый взгляд старый ID больше не нужен.

Но через месяц бухгалтер пишет:

Посмотрите клиента 1842 из старой CRM.

Если связь потеряна, начинается ручной поиск.

Поэтому полезно сохранять:

legacy_source = old_crm
legacy_id = 1842

или отдельную таблицу:

migration_id_map

source
entity
legacy_id
new_id

Например:

old_crm
client
1842
→
9d7f...

Эта таблица невероятно полезна во время проверки миграции.


ID mapping нужен и для восстановления связей

Допустим старая система имеет:

Client #1842
   ↓
Deal #773
   ↓
Comment #9921

В новой появляются другие IDs:

Client A
   ↓
Deal B
   ↓
Activity C

Чтобы правильно перенести:

deal.client_id

нужно знать:

old Client 1842
→
new Client A

Именно поэтому сущности часто мигрируются по этапам.

Сначала:

Users
Organizations
Contacts

потом:

Deals
Projects

после:

Comments
Documents
Activities

Связи восстанавливаются через mapping.


Самая серьёзная ошибка — потерять даты

Очень часто importer создаёт новые записи обычным способом.

Получается:

created_at = NOW()

У клиента, который появился:

18.04.2019

после миграции:

created_at = 28.09.2026

Теперь новая CRM считает, что все клиенты появились сегодня.

История бизнеса уничтожена.


Исторические поля должны сохраняться отдельно от технических дат миграции

Например:

created_at
→ оригинальная дата создания

updated_at
→ исходная дата изменения,
если она достоверна

migrated_at
→ момент импорта

Теперь можно отличить:

Когда сделка появилась в бизнесе?

от:

Когда мы перенесли её в новую CRM?

Особенно важно сохранить историю статусов

Представим сделку:

Сейчас:
Завершена

Если перенести только:

status = COMPLETED

мы знаем только результат.

Но старая CRM содержала:

12 марта
Новая

13 марта
Уточнение

15 марта
Оценка

18 марта
В работе

2 апреля
Завершена

Эта история позволяет рассчитывать:

скорость обработки;
время сделки;
длительность этапов;
работу менеджера.

После миграции она должна выглядеть примерно так:

DealStatusHistory

deal_id
from_status
to_status
changed_at
actor_id
source

И важно сохранить реальные:

changed_at

а не время выполнения importer.


Иногда старой истории нет

Это тоже нормальная ситуация.

Excel может содержать только:

Клиент
Текущий статус

Нельзя из этого придумать:

когда он переходил
через предыдущие статусы.

Хорошая миграция не должна создавать фиктивную историю для красоты.

Правильнее сохранить:

Current status:
IN_PROGRESS

Historical transitions:
unknown

и metadata:

imported_from = excel

Не нужно «восстанавливать» факты, которых источник не знает

Это очень важный принцип.

Представим Excel:

Статус:
Завершено

Дата:
2022

Нельзя автоматически решить:

Значит завершено 1 января 2022 года.

Такой timestamp выглядит точным, но является выдуманным.

Лучше сохранить:

legacy_year = 2022

или пометить дату как неполную, если новая модель это поддерживает.

Миграция должна сохранять уровень достоверности данных, а не искусственно повышать его.


Отдельная проблема — пользователи и бывшие сотрудники

Старая история содержит:

Менеджер:
Алексей Смирнов

Но Алексей уволился три года назад.

В новой CRM активного аккаунта для него нет.

Если просто назначить все старые сделки текущему администратору, потеряется авторство.

Практичнее различать:

active user

и:

historical actor

Например, можно создать:

User:
Алексей Смирнов

status:
INACTIVE

login:
disabled

История сохранит:

createdBy = Alexey

но войти в систему он не сможет.


Авторство комментариев тоже является историей

Например:

14.05.2021
Алексей:
Клиент попросил перенести запуск.

После миграции плохо получить:

28.09.2026
System:
Клиент попросил перенести запуск.

Содержание сохранилось.

Контекст исчез.

Поэтому activity importer должен сохранять:

original author;
original timestamp;
source.

Иногда можно сохранить оригинальное значение рядом с нормализованным

Например телефон:

original:
8 (900) 123-45-67 доб. 104

после parsing:

phone:
+79001234567

extension:
104

На этапе миграции полезно временно или постоянно иметь:

legacy_raw_value

особенно для неоднозначных полей.

Если что-то распарсилось неправильно, можно увидеть исходник.


Excel особенно опасен из-за неявной структуры

В одном столбце могут находиться:

Иван Иванов
ООО Альфа
+7...

или:

Телефон 1 / Телефон 2

или:

email; второй email

Иногда цвет ячейки имеет смысл:

красный
→ проблемный клиент

а жирный шрифт:

→ VIP

Для человека таблица выглядит структурированной.

Для программы значимая часть структуры вообще отсутствует.


Поэтому перед миграцией Excel нужно анализировать как пользовательский интерфейс

Важно выяснить:

Используются ли цвета?
Есть ли скрытые листы?
Что означает пустая строка?
Существуют ли формулы?
Объединены ли ячейки?
Есть ли комментарии Excel?
Как сотрудники обозначают удалённых клиентов?

Иногда оказывается, что бизнес-правила существуют только в привычках сотрудников.

Например:

Если строка серая — сделка закрыта.

В базе данных такого правила никогда не было.


Формулы нельзя импортировать как значения бездумно

Например Excel содержит:

Итого:
=SUM(...)

На момент импорта значение:

580 000

В новой CRM нужно решить:

580 000 — исторический факт?

или:

Это вычисляемое поле, которое новая система должна рассчитывать сама?

Это принципиально разные вещи.


В бизнес-системе лучше различать snapshot и derived value

Например:

current_project_total

можно рассчитывать.

Но:

invoice_amount_at_issue

должна быть исторически зафиксирована.

Иначе изменение цен задним числом перепишет прошлое.

Миграция — хороший момент, чтобы впервые формально определить это различие.


Дедупликация — отдельный проект внутри миграции

Представим Excel:

ООО Альфа
+7 900...
info@alpha.ru

Другая строка:

Альфа ООО
+7 900...

Ещё одна:

ALFA
info@alpha.ru

Это одна организация?

Вероятно.

Но автоматическое:

похожее название
→ объединить

опасно.

Могут существовать реальные компании:

Строй-Сервис
Строй Сервис
СтройСервис

которые вообще не связаны.


Поэтому полезна многоуровневая deduplication

Самые надёжные признаки:

уникальный legacy ID;
ИНН;
официальный внешний идентификатор.

Далее:

точный email;
нормализованный телефон.

А:

похожее название;
похожее ФИО.

лучше использовать для создания списка:

Возможные дубли.

Но не для безусловного автоматического объединения.


Confidence может помогать, но решение должно быть контролируемым

Например:

ИНН совпадает
→ 100%
→ merge автоматически
email совпадает
→ высокий уровень
название похоже на 87%
→ manual review

Так миграция не уничтожает две разные записи из-за слишком агрессивного fuzzy matching.


Лучше оставить два дубля, чем неправильно объединить двух клиентов

Дубликат можно устранить позднее.

После неправильного merge разделить данные назад намного сложнее.

Особенно если уже объединились:

сделки;
комментарии;
документы;
платежи.

Поэтому автоматическая очистка данных должна быть консервативной.


Email тоже нельзя считать идеальным идентификатором

В B2B один адрес:

info@company.ru

может относиться к нескольким людям.

А один человек может иметь:

ivan@gmail.com
ivan@company.ru

То же касается телефона.

Именно поэтому deduplication — бизнес-задача, а не просто SQL:

GROUP BY email

Статусы почти всегда требуют mapping

В старой CRM:

Новый
В процессе
Думает
Закрыто
Не интересно

В новой:

NEW
NEEDS_CLARIFICATION
ESTIMATION
OFFER_SENT
IN_PROGRESS
COMPLETED
CANCELLED

Нельзя просто перенести текст.

Нужна таблица:

Старый статусНовый
НовыйNEW
В процессеIN_PROGRESS
ЗакрытоCOMPLETED
Не интересноCANCELLED
Думаеттребует уточнения

Последний случай нельзя автоматически решить без знания бизнеса.


Иногда одного старого статуса недостаточно для нового

Например:

Закрыто

в старой системе означает одновременно:

успешно завершено

и:

клиент отказался

Новая CRM различает:

COMPLETED

и:

CANCELLED

Значит, одного status mapping недостаточно.

Нужно искать дополнительные признаки:

причина;
сумма;
комментарий;
результат.

А записи, которые невозможно определить автоматически, отправлять в:

manual review.

Manual review — нормальная часть миграции

Нередко хочется добиться:

100% полностью автоматический импорт.

Но если 2% исходных данных неоднозначны, автоматическое угадывание может быть хуже ручной проверки.

Практичная схема:

100 000 записей
      ↓
98 400 импортируются автоматически

1 600
      ↓
migration review queue

Администратор решает только спорные случаи.

Это намного эффективнее ручной обработки всех 100 000 строк.


Не нужно импортировать прямо в production-таблицы с первого прохода

Очень полезен промежуточный слой:

Source
  ↓
Staging
  ↓
Validation
  ↓
Transformation
  ↓
Production

Например:

staging_clients
staging_deals
staging_comments

Туда можно загрузить исходные данные почти без изменений.

После этого запускать анализ.


Зачем нужен staging

Он позволяет сохранить:

исходную строку;
номер файла;
номер листа;
номер строки;
raw values;
результат validation;
migration errors.

Например:

source_file:
clients.xlsx

sheet:
Clients

row:
1843

error:
UNKNOWN_MANAGER

Теперь ошибку можно расследовать.


Без staging debugging превращается в кошмар

Importer сообщает:

Ошибка foreign key.

Где?

В какой строке Excel?

Что было исходным значением?

Если importer уже преобразовал всё в runtime objects и выбросил исходный контекст, ответ получить сложно.

Staging превращает migration pipeline в наблюдаемую систему.


Хорошая миграция похожа на ETL

Классическая схема:

Extract
   ↓
Transform
   ↓
Load

Extract

Получаем данные из:

старой CRM;
CSV;
Excel;
API;
SQL dump.

Transform

Нормализуем:

телефоны;
даты;
статусы;
ссылки;
пользователей;
связи.

Load

Записываем в новую модель.

Но в реальной миграции между Transform и Load обычно нужна ещё серьёзная Validation.


Поэтому практический pipeline скорее такой

Extract
   ↓
Stage
   ↓
Profile
   ↓
Normalize
   ↓
Validate
   ↓
Map
   ↓
Load
   ↓
Reconcile

Последний этап — один из самых важных.


Что такое reconciliation после миграции

Импорт завершился без ошибок.

Это ещё не значит, что данные перенесены правильно.

Нужно сравнить старую и новую системы.

Например:

Старая CRM:
42 841 клиента

Новая CRM:
42 841 клиента

Хороший первый сигнал.

Но недостаточный.


Нужно проверять не только количество строк

Например:

Старые сделки:
184 220

Новые:
184 220

Количество совпадает.

Но все суммы оказались:

× 100

из-за ошибки единиц.

Формально ни одна строка не потерялась.

Миграция провалилась.


Полезны контрольные агрегаты

Например сравнить:

количество клиентов;
количество сделок;
сумму продаж;
количество завершённых;
количество отменённых;
количество комментариев;
число документов.

По периодам:

2019
2020
2021
...

и по подразделениям.

Если старая система показывает:

Выручка 2024:
48 921 400 ₽

а новая:

48 921 400 ₽

это хороший признак.


Checksums полезны там, где данные можно нормализовать однозначно

Например, создаём canonical представление:

legacy_id
normalized_name
normalized_phone
created_at

и считаем hash.

После миграции пересчитываем.

При несовпадении запись попадает в отчёт.

Это помогает проверять большие объёмы автоматически.


Но контрольные суммы не заменяют выборочную ручную проверку

Полезно выбрать:

нового клиента;
старого клиента;
клиента с несколькими телефонами;
сделку с 20 статусами;
закрытую сделку;
бывшего сотрудника;
запись с документами;
спорный дубль.

И пройти историю вручную.

Автоматические тесты находят массовые ошибки.

Человеческая проверка — смысловые.


Особенно важно мигрировать файлы отдельно

Допустим старая CRM содержит:

Document record

и файлы лежат:

/uploads/...

Недостаточно перенести metadata.

Нужно убедиться, что:

Document #184
↓
реальный PDF

существует и открывается.

При большом архиве useful pipeline:

copy object
↓
verify size
↓
checksum
↓
create metadata

или другая схема в зависимости от storage.


И checksum файлов действительно полезен

Например старый файл:

SHA-256:
abc...

после копирования:

SHA-256:
abc...

Мы знаем, что содержимое не изменилось при переносе.

Размер файла сам по себе даёт более слабую гарантию.


Не забывайте про attachments внутри истории

В старой CRM комментарий может выглядеть:

12.04.2022
Иван:
Согласован новый договор.

[contract-v3.pdf]

Если мигрировать:

comment

и отдельно:

file

но потерять связь:

comment ↔ file

история уже не эквивалентна исходной.

Поэтому мигрируются не только сущности.

Мигрируются relations.


Именно связи чаще всего оказываются сложнее строк

Данные:

100 000 contacts

можно загрузить быстро.

Но потом нужно восстановить:

contacts → organizations
deals → contacts
deals → owners
messages → deals
files → messages
activities → users

Одна неправильная mapping-table способна испортить тысячи связей.


Constraints полезно включать во время миграции, а не только после

Есть соблазн:

отключить все foreign keys

и быстро вставить данные.

Иногда это действительно используется как технический приём.

Но тогда обязательна последующая строгая валидация.

Иначе новая БД может содержать:

deal.client_id

ссылающийся на несуществующего клиента.

Мы бы предпочитали по возможности загружать сущности в правильном порядке и позволять ограничениям БД ловить ошибки.


Новый ID не должен зависеть от порядка строк Excel

Например:

row 1 → client 1
row 2 → client 2

После сортировки файла IDs меняются.

Намного лучше использовать стабильный:

legacy_id

или создаваемую migration mapping.

Так повторный импорт остаётся предсказуемым.


Идемпотентность importer очень полезна

Первый импорт дошёл до:

72%

и упал.

Что делать?

Плохой ответ:

Очистим всё и начнём заново.

Для небольшой базы это возможно.

Для сотен гигабайт и миллионов строк — неудобно.

Лучше, если importer умеет безопасно повторяться.


Например, через legacy key

Уникальность:

(source, legacy_id)

позволяет понять:

эта запись уже импортирована.

Importer может:

skip

или:

update

в зависимости от режима.

Получаем resumable migration.


Но режим update должен быть определён заранее

Что если запись уже была импортирована, а пользователь успел изменить её в новой CRM?

Например:

Migration #1:
phone = A

После запуска новой CRM сотрудник исправил:

phone = B

Затем migration rerun снова импортирует:

phone = A

и уничтожает новую правку.

Поэтому повторяемость миграции требует понимания:

До какого момента legacy-source имеет право переписывать новые данные?

Именно поэтому миграция должна иметь чёткий cutover

До определённого момента:

Old CRM
→ source of truth

После:

New CRM
→ source of truth

Нельзя бесконечно разрешать обеим системам независимо изменять одни данные.

Иначе появляется задача постоянной двусторонней синхронизации, которая намного сложнее одноразовой миграции.


Самая простая миграция — с остановкой изменений

Например:

Пятница 20:00
↓
Старую CRM переводим
в read-only

↓
Final export

↓
Migration

↓
Validation

↓
Понедельник
Новая CRM

Это называется cutover window.

Для небольшого бизнеса такой подход часто самый надёжный.


Но не каждый бизнес может остановиться

Интернет-магазин или крупная CRM могут работать:

24/7.

Тогда понадобится более сложная схема.

Например:

T0
полная миграция

↓
старая система продолжает работать

↓
delta changes

↓
финальная синхронизация

↓
cutover

Delta migration переносит только новые изменения

Допустим первая копия сделана:

1 сентября.

Запуск новой CRM:

7 сентября.

За шесть дней в старой системе появились новые:

клиенты;
сделки;
комментарии.

Финальная миграция должна перенести только:

changed_since = 1 сентября

Но это работает лишь тогда, когда старая система действительно умеет надёжно определять изменения.


updated_at не всегда достаточно

Например старая система при добавлении комментария не обновляет:

deal.updated_at

Тогда delta query:

WHERE updated_at > ...

пропустит изменение.

Для критичной миграции полезнее иметь:

change log;
CDC;
audit table;
monotonic revision.

Если старая система этого не предоставляет, стратегию придётся адаптировать.


Иногда migration downtime дешевле сложной синхронизации

Бизнес может сказать:

Мы не можем отключить CRM даже на минуту.

Технически почти всё возможно.

Но continuous dual-write migration может потребовать значительной разработки.

Если реальный ущерб от:

2 часов read-only

невелик, это может быть значительно безопаснее и дешевле.

Миграционный план должен учитывать экономику, а не только максимальную техническую сложность.


Очень опасен dual write

Идея:

пишем одновременно
в старую CRM
и новую CRM

звучит удобно.

Но теперь нужно отвечать:

Что если старая записалась, а новая нет?
Что если новая записалась, а старая нет?
Как повторить?
Кто source of truth?

Мы превращаем migration в distributed consistency problem.

Без необходимости такой путь лучше не выбирать.


Для крупных миграций полезен режим shadow

Например новая система получает копию production-данных и работает параллельно, но пользователи пока не используют её для реальных изменений.

Можно сравнивать:

отчёты;
поиск;
карточки клиентов;
агрегаты.

до переключения.

Это позволяет обнаружить смысловые расхождения заранее.


Перед запуском новой CRM полезно сделать migration rehearsal

То есть выполнить полный перенос на тестовой инфраструктуре.

Например:

Production dump old CRM
        ↓
Staging migration
        ↓
New CRM staging

Замерить:

сколько занимает extract;
transform;
load;
file copy;
validation.

Теперь cutover планируется по реальным цифрам.


Первый полный прогон почти никогда не должен происходить в день переключения

Потому что именно тогда обнаруживается:

неизвестный статус;
битая дата;
неожиданный encoding;
пустой ID;
невалидный email;
потерянный документ.

Гораздо лучше обнаружить это за неделю до production migration.


Migration report должен быть полноценным артефактом

После тестового прогона полезно получить примерно:

Source clients:
48 920

Imported:
48 712

Skipped:
71

Needs review:
137

И далее причины:

UNKNOWN_MANAGER
INVALID_DATE
DUPLICATE_EXTERNAL_ID
MISSING_ORGANIZATION

Так заказчик и разработчик обсуждают конкретные проблемы.


Нельзя тихо игнорировать плохую строку

Очень опасный importer:

try {
  import(row)
} catch {
  continue
}

В конце:

Import complete.

А 2 000 клиентов исчезли.

Каждая пропущенная запись должна иметь:

причину;
source;
row;
status.

И общий отчёт.


Хороший критерий: каждая исходная запись должна получить результат

Например:

IMPORTED
SKIPPED_BY_RULE
MERGED
MANUAL_REVIEW
REJECTED

Но не:

неизвестно, куда делась.

Это делает миграцию аудируемой.


Ошибки исходных данных не всегда нужно исправлять автоматически

Например:

email:
ivan@@mail.ru

Можно:

исправить?

Но откуда мы знаем правильный?

Лучше:

import original value
+
mark invalid

или отправить на review.

Автоматически «угадывать» критичные пользовательские данные опасно.


Нормализация и исправление — разные вещи

Нормализация:

8 (900) 123-45-67
↓
+79001234567

при достаточной информации.

Исправление:

ivan@@mail.ru
↓
ivan@mail.ru

уже является предположением.

Миграция должна различать эти операции.


Особенно осторожно нужно обращаться с деньгами

Excel может содержать:

100000

Что это?

100 000 рублей?
100 000 копеек?
$1000?

Название колонки:

Сумма

не отвечает.

До импорта денежных данных нужно зафиксировать:

валюту;
единицу;
округление;
налоговую семантику.

Ошибка ×100 в финансовой истории намного хуже пары неправильно нормализованных телефонов.


Timezone — ещё один скрытый источник ошибок

Старая CRM хранит:

2024-05-01 10:00

В какой timezone?

Москва?
UTC?
локальный сервер?

Если importer решит неправильно, все события сдвинутся.

Особенно опасно для:

платежей;
встреч;
SLA;
журналов действий.

Поэтому temporal semantics нужно выяснить до миграции.


То же касается Excel serial dates

Excel может хранить дату не как:

2024-05-01

а как числовое значение внутреннего формата.

Importer должен использовать корректный parser для конкретного типа файла.

Чтение:

число → timestamp

по собственному предположению создаёт ошибки.


История переписки имеет отдельную сложность

Старая CRM может хранить:

Incoming
Outgoing
System note

Новая различает:

client message
manager message
internal note
system event.

Если всё перенести в:

comment

важное различие исчезнет.

Менеджер может случайно увидеть внутреннюю заметку как сообщение клиента.

При миграции нужно сохранить тип коммуникации.


Не каждый старый пользователь должен получить новый login

История может содержать сотни людей:

бывшие сотрудники;
тестовые аккаунты;
удалённые пользователи.

Создавать активную учётную запись каждому опасно.

Полезнее:

historical account
active = false

или отдельное представление historical actor.


Пароли обычно нельзя «перенести» как обычное поле

Если старая CRM использует безопасный password hash, иногда технически возможна миграция hashes при совместимом authentication layer.

Но далеко не всегда это разумно или возможно.

Часто при переходе пользователям требуется:

reset password.

Нельзя экспортировать plaintext passwords, если их вообще нет и быть не должно.


Персональные данные требуют отдельного внимания

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

Например:

паспорт;
домашний адрес;
старый телефон;
лишние комментарии.

Миграция — хороший момент спросить:

Нужно ли вообще переносить это в новую систему?

Принцип:

раз есть в старой БД
→ обязательно переносим

не всегда правильный.


Миграция может одновременно быть data cleanup

Но cleanup должен быть формализован.

Например:

пустые тестовые клиенты
→ не переносим

дубли
→ объединяем только
по подтверждённым правилам

удалённые temporary records
→ исключаем

Каждое правило фиксируется до production migration.

Иначе разработчик незаметно начинает решать:

Какие данные бизнеса важны, а какие нет.

Старую систему нельзя удалять сразу после запуска новой

Даже если migration validation прошла идеально.

Практично оставить legacy CRM:

read-only

на определённый период.

Например:

30 дней

или другой срок по требованиям бизнеса.

Сотрудники могут проверить спорные исторические случаи.


Но read-only legacy не должна жить вечно

Иначе через пять лет компания продолжает оплачивать и поддерживать:

новую CRM
+
старую CRM

только потому что:

Вдруг понадобится.

Нужно заранее определить:

validation period
↓
final archive
↓
decommission

Финальный архив старой системы тоже является частью миграции

Можно сохранить:

SQL dump;
экспорты;
файлы;
описание схемы;
migration mapping;
migration report.

В будущем это намного полезнее, чем единственный:

oldcrm.zip

без инструкции, что внутри.


Rollback нужно продумать до переключения

Представим новую CRM запустили утром.

Через час обнаружили критичный дефект.

Что делать?

Если сотрудники уже начали создавать новые данные, просто:

вернуться в старую

не так просто.

Нужно определить cutover policy.

Например, первые часы новая система работает под усиленным контролем.

Если rollback действительно допускается, нужно понимать, как обрабатывать данные, созданные после переключения.


Иногда правильнее не rollback, а forward fix

Если новая система уже получила:

50 новых клиентов;
20 сделок;
100 сообщений,

откатываться к старой CRM может быть опаснее, чем оперативно исправить дефект.

Поэтому migration rollback и application rollback — не одно и то же.


Data migration должна иметь собственные тесты

Например:

legacy client #1842

после import должен иметь:

имя;
телефон;
организацию;
5 сделок;
18 комментариев;
3 файла.

Тест может автоматически сравнить expected mapping.


Очень полезны fixture с «грязными» данными

Не только идеальная строка:

Иван
+7900...
ivan@example.com

Но:

двойной телефон;
пустой email;
неизвестный менеджер;
сломанная дата;
duplicate company;
deleted user;
несуществующая relation.

Importer должен пройти реальные странности legacy-системы.


Миграция должна быть версионируемой

Скрипт:

migration-final.js

который постоянно редактируют вручную, плохо воспроизводим.

Лучше иметь:

migration-v1
migration-v2

или Git history.

И конфигурацию mapping.

Тогда можно доказать:

Какой именно код перенёс production-данные?

Исходный экспорт тоже нужно фиксировать

Например:

crm-export-2026-09-28.csv
SHA-256: ...

Или database dump checksum.

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


Хороший migration run должен быть воспроизводим

Имея:

тот же source snapshot
+
ту же migration version
+
ту же configuration

мы должны получать:

тот же результат.

Это сильно упрощает QA.


Секрет миграции — отделить техническое преобразование от бизнес-решений

Техническое:

trim spaces;
parse date;
convert encoding.

Бизнес-решение:

статус "Думает"
→ какой новый статус?

или:

два похожих клиента
→ один или два?

Разработчик не должен самостоятельно угадывать вторую категорию.


Для спорных правил нужен migration specification

Например:

Rule M-017

Legacy:
status = "Закрыт"

If:
payment exists

Then:
COMPLETED

Else:
MANUAL_REVIEW

Теперь решение документировано.

Его можно протестировать.


Это особенно полезно для приёмки

Без specification заказчик после migration говорит:

Мне кажется, некоторые статусы неправильные.

Разработчик:

Мы сделали как поняли.

С specification можно проверить:

ожидаемое правило
vs
фактический результат.

Как мы бы организовали migration project

Общая схема выглядела бы так:

            LEGACY SYSTEMS
                  │
      ┌───────────┼───────────┐
      │           │           │
      ▼           ▼           ▼
   Old CRM     Excel      Documents
      │           │           │
      └───────────┼───────────┘
                  ▼
               EXTRACT
                  │
                  ▼
              STAGING
                  │
         ┌────────┴────────┐
         ▼                 ▼
   Validation          Profiling
         │                 │
         └────────┬────────┘
                  ▼
            NORMALIZATION
                  │
                  ▼
               MAPPING
                  │
                  ▼
             NEW CRM DB
                  │
           ┌──────┴──────┐
           ▼             ▼
      Reconciliation   QA
           │             │
           └──────┬──────┘
                  ▼
              CUTOVER

Обратите внимание: INSERT занимает в этой схеме совсем небольшое место.

Основная работа находится вокруг него.


Пример миграции клиента

Исходная Excel-строка:

ID:
1842

Компания:
ООО "Альфа "

Контакт:
Иван Петров

Телефон:
8 (900) 123-45-67

Менеджер:
Иван

Статус:
В работе

Создан:
12.03.2021

После нормализации:

legacy_id:
1842

organization_name:
ООО «Альфа»

contact_name:
Иван Петров

phone:
+79001234567

legacy_manager:
Иван

legacy_status:
В работе

created_at:
2021-03-12

Mapping:

Manager Иван
↓
User #42
В работе
↓
IN_PROGRESS

Load:

Organization #A
Contact #B
Deal #C

и:

legacy map:
1842 → C

Но импорт считается успешным только после проверки результата

Например:

Deal #C
status = IN_PROGRESS

createdAt = 2021-03-12

owner = User #42

contact = Ivan Petrov

organization = Alpha

И контрольной выборки.


Пример сложнее: историческая сделка

Legacy:

Deal #773
Client #1842
Manager #12
Created 2021-03-14
Closed 2021-04-02
Amount 480000

History:

14.03 NEW
15.03 QUALIFICATION
20.03 PROPOSAL
23.03 IN_PROGRESS
02.04 WON

После migration мы хотим получить не только:

status = COMPLETED

Но и:

Deal
createdAt = 2021-03-14
completedAt = 2021-04-02
amount = 480000

плюс:

StatusHistory

со всеми исходными датами.

Тогда новая аналитика сможет работать и с прошлым периодом.


Иначе отчёты после перехода начнут «жизнь с нуля»

Представим новую CRM запустили:

1 октября.

Если история статусов не перенесена, dashboard может показать:

Среднее время сделки:
0 дней

или учитывать только новые сделки.

Руководство сравнивает данные с прошлым годом и видит бессмысленные показатели.


Миграция должна учитывать будущую аналитику

До переноса полезно спросить:

Какие отчёты бизнес хочет строить после запуска?

Если нужен:

sales by manager 2020–2026

нужно сохранить historical owner.

Если:

conversion by stage

нужна status history.

Если:

revenue by month

нужны достоверные timestamps и суммы.

Бизнес-требования к аналитике определяют глубину миграции.


Иногда полную историю переносить невыгодно

Представим старой CRM:

15 лет.

Первые десять лет данные крайне плохого качества.

Бизнес реально использует:

последние 3 года.

Можно принять осознанное решение:

2023–2026
→ полноценная миграция

до 2023
→ read-only archive

Это не потеря данных, если архив остаётся доступным и решение согласовано.

Это управление стоимостью migration.


Не обязательно превращать всё legacy в новую domain model

Например, старые записи содержат поле:

Параметр X

которого больше нет в бизнес-процессе.

Можно сохранить его:

legacy_metadata

или в read-only archive, вместо добавления ненужного поля в современную CRM только ради истории.


Новая система не должна навсегда наследовать ошибки старой

Это одна из главных возможностей миграции.

Если legacy CRM имела:

17 почти одинаковых статусов;
одну гигантскую таблицу;
дублированные клиенты;
текстовые ID менеджеров,

не нужно копировать эту архитектуру в новую.

Сохраняем информацию.

Не обязательно сохранять плохую модель.


История и старая структура — не одно и то же

Можно сохранить:

кто;
что;
когда;
с кем;
на какую сумму;
какой был результат.

Но представить это уже через нормальную новую domain model.

Именно это делает migration полезнее простого переноса БД.


Что должен получить заказчик после миграции

Не просто сообщение:

Импорт завершён.

А понятный результат:

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

Это делает миграцию проверяемой.


Когда можно считать миграцию завершённой

Не когда script закончил работу с:

exit code 0.

А когда выполнены несколько условий.

Новая CRM содержит согласованный объём данных.

Связи восстановлены.

Исторические даты сохранены.

Файлы доступны.

Ключевые агрегаты совпадают.

Спорные записи разобраны или зафиксированы.

Сотрудники провели выборочную проверку.

Есть backup исходных данных.

Есть migration report.

И только после этого legacy-система перестаёт быть operational source of truth.


Почему «просто импортировать Excel» часто недооценивают

Потому что технически прочитать XLSX действительно несложно.

Самый трудный вопрос не:

Как открыть Excel из Node.js?

А:

Что именно означает каждая строка и как сохранить её смысл в новой системе?

Например:

Красная строка

может означать:

должник.
Пустой менеджер

может означать:

общий клиент отдела.
Закрыто

может означать два противоположных исхода.

Эти знания находятся не в API Excel.

Они находятся внутри бизнеса.


Поэтому миграция требует участия сотрудников компании

Разработчик понимает:

database;
ETL;
transactions;
constraints.

Но менеджер понимает:

Что означало поле «Тип 2» в таблице 2021 года.

Без этого знания автоматическая миграция может быть технически безупречной и бизнесово неправильной.


Самый полезный вопрос на старте миграции

Мы бы спросили не:

Сколько строк в Excel?

А:

Какие факты из старой системы должны оставаться доказуемыми после запуска новой?

Например:

когда появился клиент;
кто вёл сделку;
какая цена была согласована;
какой статус был в конкретную дату;
какой документ был приложен;
кто оставил комментарий.

От ответа зависит архитектура миграции.


Практический чек качества

Есть простой способ мысленно проверить результат.

Откройте случайную старую сделку в legacy CRM.

Затем ту же сделку в новой.

И спросите:

Могу ли я понять ту же историю?

Не обязательно интерфейсы должны совпадать.

Но факты должны.

Например:

клиент;
ответственный;
дата;
сумма;
статусы;
комментарии;
документы;
результат.

Если новая система показывает только последний snapshot — данные перенесены, но история нет.


И это главное отличие миграции от импорта

Импорт отвечает:

Как записать данные в новую систему?

Миграция:

Как перевести бизнес на новую систему, сохранив смысл, историю и возможность доверять данным?

Это намного более широкая задача.


Вместо вывода

Переход со старой CRM или Excel часто воспринимают как заключительный технический этап:

новая CRM готова
↓
осталось загрузить данные

На практике миграцию лучше проектировать ещё до завершения разработки.

Потому что именно старые данные показывают:

какие сущности существуют;
какие связи важны;
какую историю нужно хранить;
какие статусы реально использует бизнес;
какие отчёты должны работать после запуска.

Надёжный процесс выглядит примерно так:

Инвентаризация источников
        ↓
Профилирование данных
        ↓
Migration specification
        ↓
Staging
        ↓
Нормализация
        ↓
Mapping
        ↓
Дедупликация
        ↓
Импорт
        ↓
Reconciliation
        ↓
Тестовый прогон
        ↓
Final migration
        ↓
Cutover
        ↓
Проверка
        ↓
Legacy read-only

При этом ключевая цель — не добиться красивого:

Imported: 100%

Гораздо важнее другое:

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

Клиент, созданный пять лет назад, не должен внезапно стать сегодняшним.

Бывший менеджер не должен исчезнуть из старых сделок.

Завершённая сделка не должна потерять путь, которым она дошла до результата.

Документ не должен отделиться от комментария, к которому был приложен.

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

Хорошая миграция — это не максимальная скорость копирования строк.

Это способность после переключения новой CRM ответить на вопрос:

Можем ли мы доверять своей истории?

Если ответ уверенно «да», тогда переход действительно завершён.

Есть похожая задача?

Опишите продукт, интеграции и ограничения. До разработки зафиксируем объём, риски и критерии приёмки.

Обсудить проект →