Чому ми викинули каталог продуктів з інвойсів
Як next10 CRM перейшла з обовʼязкового product_config до ad-hoc інвойсів — і чому це різко спростило життя менеджерам. Архітектурний розбір з реальним кодом.
Перші місяці нашої CRM працювали так: щоб виставити рахунок клієнту — менеджер мав спершу зайти в Settings → Products → створити продукт із slug, ціною, описом, FOP-реквізитами. Потім — відкрити карточку клієнта → Інвойси → обрати щойно створений продукт → надіслати.
Для разової консультації на 500 ₴ це сім кліків і дві сторінки. Команда саботувала — просто скидали реквізити в Telegram руками.
Розбираємо, як ми це переписали.
Стара модель
class Payment(Base):
__tablename__ = "payment"
id: Mapped[int] = mapped_column(primary_key=True)
client_id: Mapped[int] = mapped_column(ForeignKey("client.id"))
product_config_id: Mapped[int] = mapped_column( # NOT NULL
ForeignKey("product_config.id", ondelete="RESTRICT")
)
amount: Mapped[Decimal]
# ...
Кожен платіж жорстко прив’язаний до product_config. Назва, ціна, картинка, реквізити — все там. Це чисто, поки катáлог стабільний. Як тільки бізнес виходить за межі типових послуг — починаються «продукти-сирітки» в Settings: «Консультація для Олега», «Доплата за хостинг», «Чорновий етап лендингу».
Що зробили
Один маленький рефакторинг рятує день:
class Payment(Base):
product_config_id: Mapped[int | None] = mapped_column(
ForeignKey("product_config.id"), nullable=True # ← було NOT NULL
)
title: Mapped[str | None] # ← нове
description: Mapped[str | None] # ← нове
# ...
@property
def display_title(self) -> str:
if self.title:
return self.title
if self.product_config:
return self.product_config.name
return f"Інвойс #{self.id}"
Тепер є дві стратегії:
- Готовий продукт — старий шлях, для типових послуг (MVP-пакети, тарифи).
- Ad-hoc — менеджер вводить назву + суму вручну. Ніяких Settings.
Snapshot-міграція
Найболючіший момент — старі платежі. Раніше їхня назва бралась з JOIN payment.product_config_id = product_config.id. Якщо завтра вимкнути продукт — історія для бухгалтерії «зникне».
Один разовий backfill знімає ризик:
UPDATE payment
SET title = pc.name
FROM product_config pc
WHERE payment.product_config_id = pc.id
AND payment.title IS NULL;
Після цього payment.title — самодостатнє поле. Незалежно від того, що відбувається з каталогом продуктів далі.
Що це дало в UI
Форма створення інвойсу зараз має toggle:
[ Готовий продукт ] [ Свій інвойс ]
назва [...]
сума [500] валюта [UAH ▾]
опис (опціонально) [...]
метод оплати: [Mono] [ФОП]
☑ Надіслати клієнту в Telegram
Менеджер створює інвойс не виходячи з карточки клієнта. Один екран. Чотири кліки.
Lessons learned
- Жорсткі FK у фінансових таблицях — це борг. Snapshot-копія важливіша за нормалізацію.
- Product catalog добре працює для product-led бізнесу. Service-led — потребує гнучкості.
- Toggle «product / ad-hoc» — м’який міграційний шлях. Стара логіка не зникає, нова просто паралельна.
Якщо ваша CRM змушує менеджерів робити кліків більше ніж 4 на типову операцію — десь у моделі є помилка. Подивіться, де саме.