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

Блог ·

Stripe для хостингу: коли платіж є вашим

«Успішно» не означає «зараховано», спір не є поверненням, а повторні спроби списання є налаштуванням панелі. Що дає регулярна оплата хостингу на Stripe.

Автор: Provibr

Виставляння рахунків за хостинг не є електронною комерцією. Крамниця бере гроші один раз і відправляє коробку; ви берете гроші щомісяця і між тим тримаєте машину в роботі. Ця різниця проявляється в одному місці, і саме в ньому кожна інтеграція помиляється першим ділом: у рішенні про те, коли платіж уже досить справжній, щоб надавати послугу далі.

Ось на що ми натрапили, будуючи регулярні платежі на Stripe. Нічого екзотичного, і більшість цього невидима до того місяця, коли платіж розвертається назад.

«Успішно» не означає «зараховано»

Успішне оформлення оплати не є грошима. Картка та iDEAL достатньо близькі до миттєвих, щоб можна було вдавати інше. Прямий дебет SEPA таким не є, як і банківський переказ.

Платіж SEPA сидить у стані processing після оформлення, без жодних отриманих грошей, днями. Зазвичай він зараховується. Іноді ні. Якщо ваша інтеграція вважає оформлення моментом істини, ви видали сервер на підставі наміру.

Тож модель стану не є булевою. Ми зупинилися на шести фазах, і рівно одна з них означає «надавати»:

ФазаЩо вона означаєНадавати?
pendingПочалося, нічого ще не отриманоНі
settledГроші отримано, і вони нашіТак
failedЦього не станетьсяНі
disputedКлієнт це оскаржуєНі, але воно може повернутися
reversedЧарджбек або поверненняНі, назавжди
unknownМи не змогли визначитиНі

Зробити так, щоб «можна надавати» означало точно і лише settled, не коштує при написанні нічого й дає властивість, що жодна інша частина кодової бази не має права мати про це власну думку. Решта коду ставить одне питання.

Порядок подій не гарантований, тож зробіть незаконні переходи незаконними

Вебхуки приходять не в тому порядку. Не часто, але приходять, і збій мовчазний: застаріла подія перезаписує новіший стан, і послуга клієнта або спиняється без причини, або працює далі після чарджбека.

Дешевий захист це записати, які переходи неможливі, і відхиляти їх. Зарахований не може повернутися до очікування, зарахований не може стати невдалим, очікування не може стрибнути одразу в оскарження, і ніщо ніколи не виходить із розвернутого чи невдалого. Запізнілу подію, що спричинила б один із таких переходів, відкидають, а не застосовують.

Другий захист є архітектурним, і це те рішення, яке ми зберегли б у будь-якій платіжній інтеграції.

Вебхук є прискорювачем, а не джерелом правди. Наш перевіряє підпис, прибирає дублікати й кладе в чергу один ідентифікатор організації. Це все, що він робить. Далі опитувач забирає поточний стан зі Stripe, перш ніж щось зміниться. Наслідок вартий цієї сантехніки: пропущений вебхук ніколи не є пропущеним платежем, він є повільнішим платежем. Надання послуги не залежить від механізму доставки, який вам не підконтрольний.

У перевірці підпису є три дрібниці, у яких легко ледь помітно помилитися:

  • Порівнюйте за сталий час, але спершу перевірте довжини, бо порівняння за сталий час на різних довжинах кидає виняток, а не повертає хибність.
  • Приймайте кілька підписів у заголовку. Саме так працює ротація секрету, і реалізація, що читає лише перший, ламається того дня, коли ви робите ротацію.
  • Відхиляйте нешістнадцятковий підпис до того, як розбирати його як шістнадцятковий, бо такий розбір мовчки обрізає на першому недійсному символі, і підпис із двох символів радо порівняється ні з чим.

Ми також прибираємо дублікати, застовплюючи ідентифікатор події до публікації поштовху. У цього є відомий недолік: застовпити, а потім не змогти опублікувати, і підказку втрачено. Це прийнятно саме тому, що джерелом правди є опитування, і це добра ілюстрація того, як одне архітектурне рішення оплачує компроміс в іншому місці.

Повторні спроби списання є налаштуванням панелі, а не параметром API

Якщо місячний платіж не проходить, щось має повторити спробу, і щось має вирішити, коли послуга спиняється. Ми спершу написали цю логіку, а потім більшу її частину видалили.

Smart Retries у Stripe добирає час повторних спроб зі своїх даних, а те, чим стає підписка після того, як усі спроби провалилися, налаштовують у панелі. Це налаштування на весь обліковий запис, а не щось, що ви передаєте на кожну підписку, і тестовий та бойовий режими налаштовують окремо, а це добрий спосіб здивуватися в продакшні після зеленого тестового прогону.

Вашому боку лишається одна робота: перекласти статус підписки в те, чим клієнтові дозволено користуватися.

Статус підпискиЩо ми надаємоЧому
active, trialingПовний доступ
past_dueПільга: банер, нічого не спиненоStripe усе ще повторює спроби
unpaidПризупиненоСпроби вичерпано
canceledПризупинено
incompleteПовний доступ, без обмеженьПерший платіж так і не пройшов
pausedПовний доступ

Рядок past_due керується статусом, а не годинником, і це те спрощення, яке варто взяти. Щойно розклад повторних спроб належить процесингу, питання «скільки днів це вже не проходить» перестає бути вашим. Ставити його попри це означає крутити другий таймер проти розкладу, якого ви не бачите.

Кусається рядок incomplete. Він означає підписку, чий найперший платіж так і не пройшов, а це не те саме, що платний клієнт, який перестав платити. Ставитися до них однаково означає заднім числом призупинити чинного клієнта на підставі події про підписку, яка ніколи не починалася.

І призупинення є паузою, ніколи не видаленням. Повернення є дією, яку виконує людина. Ніщо в скінченному автоматі платежів не має мати змоги знищити дані клієнта, хоч би яким упевненим воно було.

Вада, що повернула гроші й лишила послугу працювати

Ось збій, який навчив нас найбільшого, і платіжний постачальник тут ні до чого.

Чарджбек слушно виставив стан ліцензії клієнта на заблокований. За пʼять хвилин він знову був активним. Клієнт мав і свої гроші, і свої сервери.

Причина була в тому, що фонове завдання, яке перераховувало зовсім інше, перевитрату ресурсів, кожні пʼять хвилин виконувало UPDATE тієї самої колонки без умови WHERE. Три окремі шматки коду складали те саме поле, кожен правильний сам собою, і переміг той, що виконувався найчастіше.

Виправлення було структурним, а не латкою: колонку складає одна функція, беручи на вхід три нижні межі, одну з платежів, одну з перевитрати, одну з розвернутих транзакцій, і більше в неї не пише ніхто. Загальне правило варто сформулювати абстрактно, бо воно діє далеко за межами платежів. Якщо ту саму колонку обчислює більш ніж один автор запису, ця колонка не є рішенням. Вона є гонитвою, і розвʼяжеться вона на користь того завдання, у якого найкоротший інтервал.

Спір не є поверненням, а часткове повернення не є половиною послуги

Це дві відмінності, потрібні платежам за хостинг, яких загальний посібник із платежів за вас не проведе.

Відкритий спір не є втратою. Його можна виграти. Платіж перестає рахуватися як наданий, але це твердження про платіж, а не про ліцензію: відкритий спір відображається на пільгу, тож клієнт зберігає послугу, а ви отримуєте сповіщення. Призупинити того, хто свій спір виграє, означає розмову з підтримкою, якої ви не хочете. Остаточний чарджбек є втратою, і от він відображається на призупинення.

Незручним є часткове повернення. Спокусливо вважати його частковим наданням послуги, а такого не буває. Місяць підписки або надано, або ні. Тож часткове повернення цілком виводить платіж зі стану зарахованого, і надання послуги спиняється. Це чесне прочитання, хай і здається суворим, бо альтернатива це вигадати частку послуги, якої ваш продукт насправді надати не може.

Якщо ви штовхаєте кредит-ноти в бухгалтерську систему, ще одне попередження. У нашій кредит-нота не є окремим ресурсом: це рахунок із відʼємними сумами, а виклик дублювання копіює поле посилання з оригіналу. Це посилання є рівно тим ключем, за яким шукає перевірка ідемпотентності, тож наївна реалізація знаходить на наступному прогоні оригінал, робить висновок, що нічого не кредитовано, і випускає другу кредит-ноту. Гроші повернуто двічі. Кожен згенерований документ потребує власного якоря, виведеного з події, що його спричинила.

Що таке проформа тут і чому лічильник має два префікси

Проформа це документ, який виробляє сама платформа, з власним номером, і саме його клієнт бачить у панелі. Юридичний рахунок створюють у бухгалтерській системі з власною серією. Без підключеної бухгалтерської системи проформа є єдиним наявним документом, і саме тому її треба нумерувати як належить, а не сприймати як квитанцію.

Дві дрібниці, що вийшли з роботи над цим:

Номер доповнюють нулями до чотирьох цифр як мінімуму, а не до фіксованої ширини. Десятитисячний документ отримує пʼять цифр. Обрізання заради охайності формату дало б двом документам той самий номер, а унікальний індекс дасть вам про це дізнатися в найгіршу можливу мить.

Один лічильник, два префікси. Коли префікс змінився, послідовність продовжилася, а не почалася наново. Два лічильники породили б новий документ, номер якого відрізняється від старого лише дволітерним префіксом, що база даних бачить як унікальне, а людина, яка читає банківську виписку, ні.

А поповнення передплаченого балансу не несе ПДВ у своїй проформі, бо поповнення є передоплатою, а не послугою. ПДВ належить рахунку за те, на що баланс зрештою витрачено. Переплутати це навпаки означає стягнути податок двічі з тих самих грошей.

Що ми змогли виміряти, а чого не змогли

Найкориснішою річчю в усьому цьому є тестовий годинник. Місячний цикл виставляння рахунків можна прогнати від початку до кінця за хвилини: створити платіж SEPA, підтвердити, що processing не активує геть нічого, перевести годинник за вікно зарахування, побачити, як він зараховується, і перевірити, що активація стається рівно один раз і є ідемпотентною на другому прогоні. Без нього місячний цикл є тією єдиною річчю, яку неможливо відрепетирувати, і його перший справжній прогін стається в продакшні.

Дві чесні межі всього вищесказаного. Це виміряно проти пісочниці Stripe і справжнього вебхука, доставленого мережею, а не проти великої бойової клієнтської бази. І урок, що узагальнюється за межі платежів: прапорець функції на воркері й на кнопці не закриває схему бази даних. Одного разу деплой випустив код, що читав колонки, чия міграція не виконалася, і не повʼязана з цим адміністративна сторінка повертала помилку сервера для всіх, бо прапорець був вимкнений, а запит ні.

Це вийшло з побудови платіжного боку Provibr, де хостинг-провайдери керують власною інфраструктурою через одну панель, а агент зʼєднується лише назовні.

Читати далі

Почніть з одного хоста й одного агента

Створіть обліковий запис, встановіть агента однією командою та підключіть свій перший хост Proxmox. Пробний період триває 14 днів і не потребує платіжних даних.