Обзор архитектуры
GlamB построен по паттерну Modular Monolith на Rails 8.1+.
Домены организованы в /app/ по папкам; в будущем каждый может быть выделен в отдельный Rails Engine.
Слои
┌─────────────────────────────────────────────────────────┐
│ Presentation: REST API v1 · Hotwire · ActionCable │
├─────────────────────────────────────────────────────────┤
│ Application: Controllers · Services · Serializers │
│ Pundit Policies · Query Objects │
├─────────────────────────────────────────────────────────┤
│ Domain (11 engines — Phase 1: папки в /app/): │
│ core · catalog · scheduling · booking · payments │
│ reviews · notifications · analytics · crm · admin │
│ favorites │
├─────────────────────────────────────────────────────────┤
│ Infrastructure: PostgreSQL 16 · Redis 7 · SolidQueue │
│ MinIO (S3) · ActionCable │
├─────────────────────────────────────────────────────────┤
│ External: Stripe · Twilio · SendGrid · Google Maps │
│ Firebase FCM · Cloudflare CDN │
└─────────────────────────────────────────────────────────┘
Структура директорий
app/
controllers/api/v1/
auth/ # sign_in, sign_up, refresh, google_oauth
me/ # profile, favorites, salons
salons/ # salons + все вложенные ресурсы
appointments/ # appointments
catalogs/ # categories
invitations_controller.rb
base_controller.rb
services/
auth/ # AuthenticationService, GoogleOAuthService
me/ # ProfileService, FavoriteService
salons/ # SalonService, SalonServicesService, ...
appointments/ # AppointmentService
catalogs/ # CatalogService
models/ # User, Salon, Service, Appointment, ...
serializers/ # UserSerializer, SalonSerializer, ...
policies/ # SalonPolicy, AppointmentPolicy
lib/
jwt_signature.rb
domain_errors.rb
slot_builder.rb
Zeitwerk коллапсирует поддиректории
services/иserializers/— классы объявляются на верхнем уровне без обёртки модуля.
Паттерн сервисов
Два паттерна в зависимости от сложности:
Pattern A — class methods (простые операции без состояния):
class ProfileService
def self.get_profile(user_id)
profile = Profile.find_by(user_id: user_id)
raise DomainErrors::NotFound.new("Profile not found") unless profile
profile
end
end
Pattern B — instance + #call (сложные операции с приватными методами):
class SalonAvailabilityService
def call
# ...
end
private
def initialize(salon, date:, offering_id: nil)
@salon = salon
@date = Date.parse(date.to_s)
end
end
# Использование:
SalonAvailabilityService.new(salon, date: params[:date]).call
Авторизация
Pundit RBAC. Каждый защищённый эндпоинт вызывает authorize или policy_scope.
| Политика | Ресурс |
|---|---|
SalonPolicy | Salon CRUD и управление услугами |
AppointmentPolicy | Appointments с ролевым доступом |
ApplicationPolicy | Base-class, deny all по умолчанию |
Обработка ошибок
Все ошибки централизованы в BaseController через rescue_from:
| Exception | HTTP | Когда |
|---|---|---|
DomainErrors::NotFound | 404 | Ресурс не найден |
DomainErrors::Unauthorized | 401 | Неверный пароль / токен |
DomainErrors::ValidationError | 422 | Валидация модели/параметров |
DomainErrors::Forbidden | 403 | Нет прав |
ActiveRecord::RecordNotFound | 404 | AR find |
Pundit::NotAuthorizedError | 403 | Pundit |
JwtSignature::TokenExpiredError | 401 | Истёкший токен |
JwtSignature::TokenInvalidError | 401 | Невалидный токен |