Skip to content

Білінг

Як команда платить і що саме білінг повідомляє решті продукту. П'ять слайсів у групі billing/, і між ними та всім іншим — одне-єдине поле: Team.planId.

Ця вузькість і є задум. Ніщо поза billing/ не питає, хто провайдер, який вигляд має підписка на дроті й чи було захоплено платіж, — воно читає тариф у команди й живе далі.

Екран оплати в кабінеті: команда на безкоштовному тарифі раннього доступу та споживання за період

Те, що показує кабінет сьогодні, вужче за те, що вміє api. На екрані вище не пропонується нічого купити: він називає команду, пише Free (early access) і повідомляє, що підписки, ліміти й оплата за споживанням з'являться ближче до v0.9. Усе описане нижче зібрано й доступне через api — просто шлях купівлі ще не виведено в інтерфейс.

П'ять слайсів

слайсщо тримає
billing/productкаталог: одна продавана річ — ім'я, ціна, planId, який отримує передплатник
billing/subscriptionнаш запис про підписку команди й запис у Team.planId
billing/paymentProviderпровайдер за шлюзом — сьогодні PayPal
billing/webhookприймання подій провайдера
billing/usageщо команда справді спожила і проти якої квоти

Провайдер захований за шлюзом

IPaymentProviderGateway живе в domain/, PayPal — у data/. Споживачі ніколи не бачать ані SDK, ані формату дроту, ані словника провайдера — та сама форма, що в infra/storage та infra/vector, і з тієї самої причини.

У 1.x було навпаки: слайс звався paypal, його називав на ім'я кожен споживач, а написання провайдера (APPROVAL_PENDING, BILLING.SUBSCRIPTION.ACTIVATED) дотягувалися до сервісів, яким не було потреби їх знати.

Хід підписки

  1. POST /billing/subscriptionsспершу пишеться наш рядок, бо його id і є те посилання, яке провайдер носить із собою й повертає в кожній події. Потім у провайдера запитується підписка, а платникові видається посилання на підтвердження.
  2. Платник підтверджує в провайдера. До цього не списується нічого.
  3. Провайдер доставляє ACTIVATED. Слайс вебхуків перевіряє подію, застовплює її в журналі повторів і застосовує — підписка стає активною, а в Team.planId з'являється тариф.
  4. POST /billing/subscriptions/:id/cancel зупиняє майбутні списання; приходить CANCELLED, і тариф знімається.

Одна відкрита підписка на команду. pending, active і suspended вважаються відкритими, тож платник, який перезавантажив недооформлену оплату, отримує те саме посилання на підтвердження, а не другу підписку, за яку треба платити.

Два правила, що існують через те, що 1.x їх порушувала

Тут ніщо не обчислює дату. currentPeriodEnd читається в провайдера. У 1.x на кожній події захоплення додавався місяць — через що повторно доставлена подія коштувала безкоштовного місяця.

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

Чому журнал повторів вартий окремої таблиці

Спокуслива заперечність: застосувати «активовано» двічі нічого не змінює. Правда — і грошей коштує не цей випадок. Грошей коштує порушений порядок: активовано, потім скасовано, потім повторна доставка «активовано». Кожен запис окремо ідемпотентний, а результат — команда на платному тарифі, яка перестала платити.

Порядок і є задум:

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

Заявка — це INSERT за унікальним ключем, а не читання з подальшим записом: дві одночасні доставки обидві нічого не прочитають і обидві підуть далі, а вирішить база, один раз.

Споживання

GET /usage/summary   що витрачено, з розбивкою
GET /usage/total     одне число
GET /usage/quota     скільки лишилося за тарифом
GET /usage/agents    те саме за агентами

Відомі діри, названі, а не сховані

Покинуту оплату можуть підтвердити пізніше. Провайдер не має операції скасування для непідтвердженої підписки — заміряно наживо, /cancel відповідає 404 RESOURCE_NOT_FOUND, — тож скасування підписки у стані pending закриває наш рядок і лишає посилання спливати за розкладом провайдера. Той, хто покинув оплату, підписався на інше, а потім повернувся й підтвердив перше посилання до його спливання, опиниться з двома живими підписками.

Справжньої стелі витрат не існує. Споживання вимірюється й показується; ніщо не зупиняє команду на межі.

Де код

api/src/slices/billing/ — по каталогу на кожен слайс вище. Див. також Розбір слайсів та Ресурси.