fleet-crm — бекенд CRM для автопарку/логістики
Огляд
Незалежний demo-проєкт: REST API на Go для диспетчерської CRM автопарку — клієнти, техніка, водії, життєвий цикл нарядів, виставлення рахунків та telematics-webhook для прийому GPS/показань одометра від зовнішнього обладнання в реальному часі.
Побудовано, щоб продемонструвати патерн, який регулярно повторюється у фріланс-запитах — диспетчерській компанії потрібна CRM, що зв’язує клієнтів, техніку та білінг, живиться від зовнішнього telematics/GPS-провайдера — з тією ж інженерною дисципліною, що й у клієнтських проєктах: репозиторії на інтерфейсах, стейт-машина замість довільної колонки статусу, і кожне твердження нижче перевірене на живому бінарнику, а не лише юніт-тестами.
Результат: повний CRUD + auth + перевірена стейт-машина нарядів + webhook, з одним реальним cross-entity багом, знайденим і виправленим ще до першого коміту.
Архітектура

WorkOrder пов’язує Client + Vehicle (+ опційно Driver) і рухає два процеси: Invoice, лише після завершення, та статус самого vehicle — через окремий частковий update, а не заміну всього рядка (чому це важливо — нижче). Telematics-webhook — єдиний неавторизований шлях запису в API — захищений спільним секретом, не JWT, оскільки це machine-to-machine взаємодія.
Ключові інженерні рішення
| Рішення | Чому |
|---|---|
| Переходи наряду перевіряються проти єдиної таблиці легальних ребер | scheduled → in_progress/cancelled, in_progress → completed/cancelled, обидва термінальні стани закриті. Наївна колонка статусу дозволила б перескочити одразу в completed або “роззавершити” вже виконаний наряд — нелегальні переходи повертають 409 з типізованою ErrIllegalTransition. |
Статус vehicle змінюється через UpdateStatus (частковий), ніколи через Update (весь рядок) |
Кожен Repository — інтерфейс, від якого залежить код Service/Handler — замінний на in-memory fake у тестах, БД для go test ./... не потрібна. |
Створення рахунку спершу перевіряє, що наряд у статусі StatusCompleted |
Виставлення рахунку до фактичного завершення роботи — реальний клас багів, не гіпотетичний — це перевіряється на рівні сервісу, а не лишається на совість клієнта API. |
Telematics-webhook використовує crypto/subtle.ConstantTimeCompare, не == |
Звичайне порівняння рядків для секрету в заголовку витікає timing-інформацією, пропорційною довжині співпадаючого префікса — дрібниця, але саме та, що відрізняє “прочитав туторіал” від “реально подумав про це”. |
| Жодного migration-фреймворку для 6 таблиць | Власноруч написаний runner на embedded SQL (internal/db) — таблиця applied_migrations плюс відсортоване виконання через embed.FS. Простіше, ніж тягнути залежність заради схеми такого розміру. |
Баг, який мав значення: рання версія побічного ефекту “наряд → техніка” викликала загальний vehicle.Update(id, vehicle.Vehicle{Status: newStatus}) — передаючи структуру, де заповнено лише Status. Оскільки Update заміняє весь рядок, кожна зміна статусу наряду мовчки занулила б номер, тип та показання одометра техніки. Спіймано регресійним тестом, що перевіряє виживання цих полів при переході, і підтверджено вдруге живим прогоном docker compose перед першим комітом — не просто довірено тому, що тест пройшов.
Результати
- Повний REST API: JWT-авторизація (bcrypt + 8-годинні токени, без публічної реєстрації), CRUD для клієнтів/техніки/водіїв, життєвий цикл нарядів, виставлення рахунків, telematics-webhook
- Один реальний cross-entity баг знайдено і виправлено до коміту — баг втрати даних
Update-проти-UpdateStatusвище - Репозиторії на інтерфейсах всюди — кожен тест сервісу/хендлера працює проти in-memory fake;
go test ./... -raceпроходить чисто, БД не потрібна - Перевірено наскрізно, не лише юніт-тестами:
docker compose up→ міграції застосовуються при старті → login засіяного admin → повний CRUD → нелегальний перехід відхилено (409) → легальний перехід підтверджено, включно з тим, що недоторкані поля техніки виживають → рахунок-до-завершення відхилено (409) → рахунок успішний після завершення → обидва шляхи webhook (невірний секрет відхилено, вірний секрет оновлює техніку) підтверджено на живому бінарнику
Код: github.com/valpere/fleet-crm
Стек
Go · chi (router) · pgx/v5 (без ORM) · golang-jwt · bcrypt · PostgreSQL · Docker Compose · testify