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

Инженерные истории · 04

Миграция 66 подписок из Loop в Recharge

  • Recharge
  • Migration

Перенос 66 действующих подписчиков с Loop на Recharge через контролируемый импорт CSV, когда аккуратный путь через API был закрыт, и почему ограниченная миграция заслуживала здравого суждения, а не миграционного движка.

Shopify · Recharge · Shop Pay · Subscription data migration · Operational care

Магазин переводил свои подписки с Loop на Recharge. Оба это нативные для Shopify приложения подписок, биллинг идёт через Shop Pay, а checkout остаётся на Shopify. Recharge уже был запущен и обрабатывал более новые подписки магазина. Оставался ограниченный набор существующих подписчиков Loop, которых нужно было перевести на Recharge без разрыва, двойного списания или неверной цены. Шестьдесят шесть человек.

Это операционная сторона работы с данными подписок. Сторона витрины, собственный buy box, построенный на нативных selling plans Shopify, это отдельная история (Deep dive 01). Здесь задача уже и, в каком-то смысле, ответственнее: перенос реальных, регулярных обязательств между двумя процессорами без случайного касания денег клиента.


Ограничение, которое закрыло путь через API

Первым моим побуждением был чистый путь: выгрузить из Loop, преобразовать данные, импортировать в Recharge через его API. Закрыли его две вещи.

Во-первых, массовая выгрузка в Loop доступна только на тарифе PRO. С тарифа, на котором был магазин, никакого CSV не выходило, так что аккуратного исходного файла для преобразования не было.

Во-вторых, с 2026-01-01 Shopify перестал разрешать созданные в admin собственные приложения. Теперь собственное приложение нужно проводить через Dev Dashboard или CLI, а это более тяжёлая настройка, чем оправдывала задача. Ради разового переноса 66 записей поднимать приложение, подключать OAuth и сопоставлять в коде две модели данных подписок было несоразмерно объёму работы.

Решение: подобрать масштаб под задачу

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

Поэтому я полностью отказался от API. Я собрал CSV для импорта подписок в Recharge из собственных экранов детализации Loop и .json каждого товара для variant ID, а затем проверил его поле за полем. Recharge проверяет этот CSV до того, как что-либо зафиксировать, а это именно та подстраховка, которая нужна ограниченной, аккуратной миграции. Здравое суждение здесь заключалось в понимании того, что проверенный импорт на 66 строк, просмотренный до фиксации, несёт меньше риска, чем сделанное на заказ миграционное приложение, написанное в коде с сопоставлением данных ради переноса разового списка. Решение не переусложнять и было инженерным решением.

Решения по данным, где корректность действительно имела значение

Именно здесь «маленькая» работа отрабатывает свою внимательность. Каждый столбец был решением о чьих-то деньгах.

recurring_price: заложить скидку в цену, без отдельного кода. Я задал recurring_price равным цене, которую каждый подписчик уже платил, вместе со скидкой. Очевидный ход это перенести discount code, но Recharge не применяет заново скидку selling plan к перенесённым строкам. discount_code поверх уже сниженной цены дал бы скидку дважды. Поэтому я заложил существующую скидку в цену, а discount_code оставил пустым. Конкретно: $35.99 подписчика на 1 банку превратились в $28.79, которые он реально платит, а подписка на 2 банки вышла в $57.58. Никакого кода, никакой двойной скидки, та же цена, на которую он подписался.

presentment_currency: намеренно пропущено. Валюта магазина по умолчанию это USD, и в USD этим клиентам выставляются счета. В .json одного товара ненадолго мелькнула иностранная валюта (польский злотый), но это была лишь presentment по локали браузера, а не валюта биллинга. Записать presentment currency, опираясь на это показание, было бы тихой ловушкой. Пустое поле позволяет каждому списанию проходить в валюте магазина по умолчанию.

Доставка: зафиксирована для каждой подписки относительно порога. Recharge позволяет зафиксировать доставку для каждой подписки через original_shipping_title и original_shipping_price, и я задал оба поля вручную относительно порога бесплатной доставки в магазине (около $50). Подписка на 2 банки за $57.58 выше порога, поэтому доставка у неё 0.00. Подписка на 1 банку за $28.79 ниже порога, поэтому у неё остаётся доставка $8.00, которую клиент уже платит. Ошибка в любую сторону либо тихо съедает маржу, либо удивляет клиента списанием, которого у него никогда не было.

Способ оплаты: пропущен, чтобы Recharge сам привязал Shop Pay. Я оставил shopify_payment_method_id пустым, вместо того чтобы угадывать ID. Поскольку биллинг идёт через Shop Pay, Recharge сам находит валидный сохранённый способ оплаты каждого клиента. Дать платформе выполнить привязку безопаснее, чем утверждать её вручную.

Мелкие поля тоже имели значение: значения province сведены к двухбуквенным кодам, status выставлен в active, is_prepaid в no. В этом нет ничего хитрого. Всё это разница между чистым импортом и обращением в поддержку.

Подвох, который находишь только запустив импорт

Recharge отклонял файл, пока я не добавил пустой заголовок last_success_charge. Этот столбец не используется при новой миграции, но его заголовок должен присутствовать, иначе импорт сразу не проходит проверку. Это та деталь, которую ни одна спецификация не показывает внятно. Её находишь, только реально попробовав импорт и прочитав ошибку.

Проверка, затем канарейка

Прежде чем что-либо фиксировать, я запустил проверку Recharge. Она читает CSV и сообщает о проблемах, не создавая записей, и вернула 66 из 66 валидных, 0 ошибок.

Затем часть, которая на самом деле защищает клиента. Реальный риск при миграции между процессорами это двойное списание: Loop списывает по старой подписке, пока Recharge списывает по новой за тот же период. Поэтому я нашёл подписчика с самой ближайшей датой следующего списания и сделал его канарейкой. Я завершил импорт этого клиента и отменил его подписку в Loop до его самого раннего списания в Recharge, которое приходилось на 2026-07-04, так что при передаче не было пересечения. Если что-то и пошло бы не так, это проявилось бы первым на самой ранней дате списания, и именно за ней я следил намеренно.

Итог

Шаг миграции (запущен 2026-06-30) создал 66 клиентов и 66 подписок, все Active, каждая с верной ценой, частотой и датой следующего списания, и со статусом оплаты Valid по всем 66, потому что Shop Pay автоматически привязал каждый способ оплаты. Уже существовавший подписчик Recharge, бывший на платформе до этой партии, остался нетронутым: импорт добавил строки, он никого не перезаписал.

Чтобы быть точным в том, на чём держится это утверждение: оно подтверждено собственной проверкой импорта и экраном статусов Recharge, а не бизнес-метрикой. 66 валидных строк, 66 активных подписок, валидная оплата у каждой. Я сообщаю то, что подтвердила платформа, не более.

Поэтапное переключение, намеренно

Один намеренно оставленный незакрытый хвост. Новые подписки пока по-прежнему создаются в Loop, потому что процесс подключения новых подписчиков на витрине ещё не переключён на Recharge. Вместо того чтобы блокировать всю миграцию на этом более крупном изменении, я сначала перенёс текущий список и запланировал добирать более новых подписчиков Loop поздними партиями, тем же ручным процессом. Ради тонкого ручейка новых подписок повторить проверенный CSV дешевле и безопаснее, чем гнать переключение витрины, лишь бы избежать второго прохода. Поэтапно, а не нерешительно.

О чём это было на самом деле

  • Соотносите усилия с масштабом. Зрелое решение состояло в том, чтобы не строить систему миграции. Ограниченная миграция на 66 строк это работа суждения и проверки, а не инфраструктурная работа, и отношение к ней как к инфраструктуре добавило бы риска, а не убрало его.
  • Осторожность с живыми деньгами клиентов. Каждое поле было решением, которое могло списать дважды, списать меньше или потерять подписку. Внимательность ушла в цену, в порог доставки и в сроки списания, а не в хитрый код.
  • Знайте модель платформы. Поведение с двойной скидкой, ловушка presentment currency, автопривязка Shop Pay, обязательный, но пустой заголовок: ничто из этого не берётся из общего навыка работы с CSV. Всё это идёт из понимания того, как Shopify, Recharge и Shop Pay на самом деле делят работу между собой.

Технические сведения

  • Источник истины: экраны детализации Loop по каждой подписке (цена, интервал, следующее списание, доставка) и .json каждого товара для variant ID. Массовой выгрузки, на которую можно было бы опереться, не существовало.
  • Целевой файл: CSV для импорта подписок в Recharge. Поля, заданные вручную: recurring_price (скидка заложена в цену), discount_code (пусто), presentment_currency (пропущено), original_shipping_title / original_shipping_price (для каждой подписки, относительно порога ~$50), shopify_payment_method_id (пропущено ради автопривязки Shop Pay), province (2 буквы), status (active), is_prepaid (no), плюс обязательный пустой заголовок last_success_charge.
  • Проверка: проверка импорта в Recharge (66/66 валидных, 0 ошибок), затем миграция (66 клиентов / 66 подписок Active, оплата Valid у всех).
  • Связанное: Deep dive 01 описывает buy box подписки на витрине на нативных selling plans. Эта статья это операционная сторона данных той же системы подписок.
Назад к кейсуLumway

Нужен инженер Shopify, который работает по всему коммерческому пути?

Я работаю там, где сходятся UX витрины, продуктовая логика, ограничения платформы, измерения и доставка в продакшн.

Начать разговор