Перейти к основному содержимому

Обзор архитектуры

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.

ПолитикаРесурс
SalonPolicySalon CRUD и управление услугами
AppointmentPolicyAppointments с ролевым доступом
ApplicationPolicyBase-class, deny all по умолчанию

Обработка ошибок

Все ошибки централизованы в BaseController через rescue_from:

ExceptionHTTPКогда
DomainErrors::NotFound404Ресурс не найден
DomainErrors::Unauthorized401Неверный пароль / токен
DomainErrors::ValidationError422Валидация модели/параметров
DomainErrors::Forbidden403Нет прав
ActiveRecord::RecordNotFound404AR find
Pundit::NotAuthorizedError403Pundit
JwtSignature::TokenExpiredError401Истёкший токен
JwtSignature::TokenInvalidError401Невалидный токен