Онлайн магазинът bodyspa.bg съществуваше преди нашето участие и не сме участвали в създаването му. Нашата работа е самостоятелната система за партньори, която се свързва с него.
Контекст
BodySpa продава професионална козметика онлайн. Магазинът работи на CloudCart, българска SaaS платформа за онлайн търговия. Компанията искаше да привлече салони за красота като партньори. Салонът препоръчва магазина на своите клиенти, клиентите пазаруват с промо код на салона, а салонът получава комисионна от продажбите.
Преди системата всичко това можеше да се управлява само ръчно: създаване на кодове, проверка кои поръчки са с кой код, изчисляване на комисионни в Excel и изпращане на справки до доставчиците. С повече партньори ръчният процес ставаше неуправляем и податлив на грешки.
Цели
- Салоните да кандидатстват онлайн, а администраторът да ги одобрява с едно действие.
- Одобреният партньор автоматично да получава работещ промо код в магазина.
- Всяка поръчка с промо код да се свързва с правилния партньор, а комисионната да се изчислява според правилата в договора.
- Партньорите да виждат поръчките и комисионните си в реално време.
- Администраторите да имат отчети по месеци и по доставчици, а доставчиците автоматично да получават справки за продажбите си.
- Всичко това да работи без промени в самия магазин.
Ограничения
Проектът имаше няколко сериозни ограничения, които определиха голяма част от техническите решения:
- Затворена SaaS платформа. Нямахме достъп до кода или базата на магазина. Единственият канал беше REST API-то на CloudCart и неговите webhooks, а документацията им е непълна.
- Лимит на отстъпките. Планът на магазина позволява само 5 отделни отстъпки, а системата трябваше да поддържа неограничен брой партньори.
- Споделен хостинг. Сървърът работи на cPanel (Passenger) с ограничени ресурси (CloudLinux LVE). Там не може да се пусне headless браузър, WebAssembly изчерпва паметта, а потребителят на базата данни няма право на DELETE.
- Финансови данни. Грешка в комисионните означава реални пари, дължими на реални хора. Системата трябваше да бъде точна и проверима.
Подход
Започнахме с технически спайк, за да отстраним основните рискове: свързахме се с API-то на CloudCart и проверихме върху реалния магазин дали можем да създаваме кодове, да четем поръчки и да разберем кой код е използван.
След това изградихме целия frontend върху мок API. Всеки екран беше показан и одобрен от клиента, преди да съществува backend. Цялата комуникация минава през един модул (api/index.ts). Когато backend-ът беше готов, променихме само него, без да пипаме нито един екран.
Накрая изградихме backend-а, интеграцията и автоматизациите. Всяка функционалност беше проверена върху живия магазин, а не само локално.
Технически предизвикателства и решения
1. Реверс-инженеринг на недокументирано API
Документацията на CloudCart не описва надеждно ключови детайли, затова проверихме и документирахме реалното поведение на API-то. Установихме, че сумите са в цели центове с включено ДДС, че някои полета (като групата на клиента) са само за четене и се променят през отделен relationship endpoint, и че датите изискват точен формат.
Най-важната находка беше как поръчката се свързва с използвания код. Полето за отстъпки в поръчката идва празно, дори когато е приложена отстъпка. Реалният код се намира в отделен ресурс order-discount, в масива codes[], и трябва да се групира по собственото му order_id. Без тази находка разпознаването на партньора в поръчката нямаше да работи.
2. Лимитът от 5 отстъпки
Първоначалният подход (отделна отстъпка за всеки партньор) удари лимита на плана. Решението беше контейнер-отстъпка: една отстъпка в CloudCart, която съдържа неограничен брой кодове. Проверихме го, като създадохме над лимита кодове без проблем.
Това създаде нов проблем: всички кодове споделят един и същ ID на отстъпката, така че поръчката не може да се свърже с партньора по ID. Затова партньорът се определя по текста на кода. Всеки партньор получава уникален брандиран код (напр. SITEDEN10: име на салона + процент отстъпка).
3. Автоматично създаване на кодове, което се поправя само
При одобрение на партньор системата създава кода в CloudCart и прехвърля клиентския акаунт на партньора в група „Partners“ за цени на едро. Логиката е идемпотентна и самовъзстановяваща се:
- ако кодът вече съществува в CloudCart, използва се той;
- ако кодът е изчезнал от магазина, създава се отново;
- кодът се записва в базата само след като CloudCart потвърди създаването.
Последното правило не беше теоретично. В продукция открихме, че липсваща конфигурационна променлива е карала системата да записва кодове в базата, без да ги създава в магазина. Грешката не даваше никакъв сигнал. Освен поправката добавихме health endpoint, който показва дали ключовите настройки са активни, за да не може подобен проблем да остане незабелязан.
Ако партньорът още няма акаунт в магазина, прехвърлянето в групата се случва по-късно, автоматично, чрез webhook при регистрацията му.
4. Двупосочна синхронизация без безкраен цикъл
Процентът комисионна и отстъпката за клиента на партньора са едно и също число. Промяна в админ панела обновява стойността на кода в CloudCart, а при одобрение стойността се чете от магазина.
Съзнателно не използвахме webhooks за промени в отстъпките. Панелът е източникът на истина: ако запис от панела предизвика събитие, а събитието отново запис, системата влиза в цикъл запис → събитие → запис.
5. Надеждна обработка на webhooks
CloudCart има две особености, които правят webhooks ненадеждни като единствен източник на данни:
- деактивира webhook-а при първия неуспешен отговор (всичко различно от 2xx);
- няма събитие за изтрита поръчка.
Решението има няколко нива:
- Записване преди обработка. Всяко входящо събитие се записва в базата в суров вид, след което сървърът винаги отговаря с 200. Грешките при обработката се логват, но не водят до деактивиране на webhook-а.
- Регистрация чрез скрипт. Седемте webhooks (поръчки, продукти, клиенти) се регистрират със скрипт, който може да се пуска многократно. Той създава липсващите и коригира URL адреса, ключа и версията на съществуващите. Има и режим, в който новите webhooks се създават неактивни, за да се подготви магазинът преди внедряването на съответния код.
- Синхронизация. Отделен процес изтегля поръчките отново от API-то. Записът се обновява по ID-то на поръчката, а комисионната се изчислява наново, така че процесът може да се пуска безопасно колкото пъти е нужно. Той покрива изтритите поръчки, прекъсванията и данните отпреди системата.
При проверка установихме, че в продукция webhooks endpoint-ите са били отворени за всеки: секретният ключ е съществувал в конфигурацията, но е бил празен, и проверката е пропускала заявките. Генерирахме ключ и го регистрирахме като HTTP header във всички webhooks. Header, а не параметър в URL-а, за да не попада в логовете на сървъра.
6. Изчисляване на комисионни
Правилата за комисионните идват директно от договора с клиента:
| Статус на поръчката | Комисионна |
|---|---|
| Обработва се / изпратена | очаквана |
| Завършена | потвърдена, дължима |
| Отказана / върната | отпада |
| Частично върната | преизчислена върху остатъка |
Комисионната се изчислява върху нетната сума, без ДДС и без доставка. Частичните връщания се четат от отделен ресурс в API-то. Когато върнатата сума не може да се определи със сигурност, системата не гадае: поръчката получава нулева комисионна и се маркира за ръчна проверка.
7. Исторически данни и промени във времето
Себестойността и доставчикът на всеки продукт се запазват към момента на поръчката. Така по-късна промяна на цената не пренаписва миналите отчети.
Това създаде обратен проблем. Когато продукт бъде свързан с доставчик за първи път, старите поръчки продължават да се водят „без доставчик“. Затова всяка промяна в свързването автоматично обновява вече записаните редове и показва колко реда са засегнати.
Доставчиците се свързват по марка, защото така BodySpa реално работи: около 700 продукта от 8 марки. С един клик се създава доставчик за всяка марка. Себестойностите се импортират от Excel файл, а парсерът приема и двата варианта на заглавията на колоните, за да не спре да работи вече попълнен от клиента файл след преименуване.
8. Отчети, които си съвпадат
При проверка открихме, че месечният отчет и файлът, изпращан на доставчиците, използват различна база: единият бруто, другият нето. Двата документа никога не можеха да съвпаднат, а разбивката по доставчици не даваше общата сума.
След решение на клиента двата документа вече използват една и съща формула и си съвпадат до стотинка. Комисионната остава нетна според договора и отчетът го пише изрично. Поръчките без продуктови редове, които не могат да се разпределят по доставчици, се показват отделно, а не се губят тихо в общата сума.
9. PDF с кирилица без браузър
Хостингът не позволява headless браузър, затова PDF отчетите се генерират директно с pdfkit. Всички вградени PDF шрифтове са WinAnsi и показват кирилицата като безсмислени символи, затова вградихме Montserrat (шрифта на панела) и проверихме покритието на кирилица и знака €. Ако шрифтовете липсват, endpoint-ът връща ясна грешка вместо счупен файл.
Проверихме резултата визуално в три сценария (нормален отчет, 22 доставчици на 3 страници, празен отчет) и открихме и поправихме два дефекта: празна последна страница и дълги имена, които се пренасяха върху съседния ред. Отчетът е стилизиран с дизайн токените на панела и има номерация „стр. X от Y“.
10. Автоматични справки до доставчиците
Всеки доставчик получава по имейл CSV със собствените си продажби за периода, а администраторът получава обобщено копие. Графикът (ежедневно или седмично, в избран ден) се променя от панела. В cron има само една ежедневна задача: скриптът чете настройките и излиза, ако днес не е ден за изпращане, така че промяна в графика не изисква промяна в сървъра.
Периодът винаги приключва с вчерашния ден, за да не се изпраща справка за ден, в който още постъпват поръчки. Всеки опит за изпращане се записва, а панелът показва последните изпращания и има бутон „Изпрати сега“.
11. От QR код до приложена отстъпка
Всеки партньор има QR код за печат в салона. Когато клиентът го сканира, отваря магазина с кода в адреса. Скрипт, добавен в магазина, запомня кода и го прилага автоматично в количката.
Скриптът не вика API директно, а попълва полето за код и натиска бутона „Приложи“, тоест минава през собствената форма на магазина. Така CSRF защитата, сесията и валидацията на CloudCart работят както при ръчно въвеждане.
12. Работа в рамките на хостинга
- Вграденият
fetchв Node използва WebAssembly парсер, който изчерпва лимита на паметта и срива процеса. Всички външни HTTP заявки минават през собствен клиент наnode:https. - Потребителят на базата няма право на DELETE, което в продукция тихо чупеше записването на продуктовите редове. Обработката на поръчки беше пренаписана така, че изобщо да не изтрива.
- Процесът по внедряване е документиран стъпка по стъпка за cPanel/Passenger.
Функционалности
Партньорски панел
- Табло с KPI, собствени SVG графики (area, bar, donut) с интерактивни tooltips и тенденция на комисионната
- Промо код и QR код за изтегляне и печат
- История на поръчките с филтри и детайлен изглед: времева линия на статуса и изчисление на комисионната
- Каталог с цени на едро, видим само за партньори
- Профил, документи, наръчник „Как работи“, въртящ се банер с изображения
Админ панел
- Табло с общ преглед, опашка за одобрение и топ партньори
- Управление на партньори: одобряване, отказ и спиране, включително групово
- Задължения към всеки партньор (потвърдени и очаквани)
- Всички поръчки, каталог, свързване на марки с доставчици, импорт на себестойности
- Месечни отчети с PDF, управление на доставчици, автоматични справки
- Редактор на шаблоните за автоматичните имейли (с променливи като
{name}и{code}) - Журнал на всички действия: кой какво е одобрил или променил и кога
- Форматиран Excel експорт (валута, проценти, дати, ред с общи суми, фиксирани заглавия, филтри), зареждан само при нужда
Сигурност
- Собствена имплементация на TOTP двуфакторна автентикация: задължителна за администраторите, по избор за партньорите, с резервен код по имейл
- bcrypt, JWT сесии, временно заключване след неуспешни опити за вход
- Смяна на паролата чрез еднократен токен, съхраняван хеширан
- Cloudflare Turnstile срещу ботове, валидация на всички входни данни със zod, webhooks защитени с ключ
Интерфейс
- Изцяло на български, с централизирани текстове и правилни форми за множествено число
- Собствена дизайн система (злато, мастило, топло бяло): модални прозорци, странични панели, известия, skeleton екрани
- Адаптивен дизайн за мобилни устройства
Резултат
Платформата работи в продукция и е проверена с реални поръчки. Поръчките се свързват с правилния партньор, а комисионните се изчисляват правилно. Процесът, който преди изискваше ръчно създаване на кодове, проследяване на поръчки в таблици и изпращане на справки, вече е автоматизиран от кандидатстването на партньора до имейла до доставчика.


