Перейти до вмісту

Інженерні історії · 04

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

  • Recharge
  • Migration

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

Shopify · Recharge · Shop Pay · Міграція даних підписок · Операційна ретельність

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

Це операційний бік роботи з даними підписок. Бік вітрини, власний buy box, побудований на нативних Shopify selling plans, це окрема історія (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 (дволітерне), 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 вітрини, логіка товару, обмеження платформи, вимірювання та релізи в продакшн.

Почати розмову