Prezentare generală a arhitecturii
GlamB este construit folosind modelul Modular Monolith pe Rails 8.1+.
Domeniile sunt organizate în /app/ pe foldere; în viitor fiecare poate fi extras într-un Rails Engine separat.
Straturi
┌─────────────────────────────────────────────────────────┐
│ Presentation: REST API v1 · Hotwire · ActionCable │
├─────────────────────────────────────────────────────────┤
│ Application: Controllers · Services · Serializers │
│ Pundit Policies · Query Objects │
├─────────────────────────────────────────────────────────┤
│ Domain (11 engines — Phase 1: foldere în /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 │
└─────────────────────────────────────────────────────────┘
Structura directoarelor
app/
controllers/api/v1/
auth/ # sign_in, sign_up, refresh, google_oauth
me/ # profile, favorites, salons
salons/ # salons + toate resursele imbricate
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 colapsează subdirectoarele
services/șiserializers/— clasele sunt declarate la nivel superior fără înveliș de modul.
Modelul serviciilor
Două modele în funcție de complexitate:
Pattern A — metode de clasă (operații simple fără stare):
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 — instanță + #call (operații complexe cu metode private):
class SalonAvailabilityService
def call
# ...
end
private
def initialize(salon, date:, offering_id: nil)
@salon = salon
@date = Date.parse(date.to_s)
end
end
# Utilizare:
SalonAvailabilityService.new(salon, date: params[:date]).call
Autorizare
Pundit RBAC. Fiecare endpoint protejat apelează authorize sau policy_scope.
| Politică | Resursă |
|---|---|
SalonPolicy | Salon CRUD și gestionarea serviciilor |
AppointmentPolicy | Programări cu acces bazat pe roluri |
ApplicationPolicy | Clasă de bază, refuză tot implicit |
Gestionarea erorilor
Toate erorile sunt centralizate în BaseController prin rescue_from:
| Excepție | HTTP | Când |
|---|---|---|
DomainErrors::NotFound | 404 | Resursa nu a fost găsită |
DomainErrors::Unauthorized | 401 | Parolă / token invalid |
DomainErrors::ValidationError | 422 | Validare model/parametri eșuată |
DomainErrors::Forbidden | 403 | Permisiuni insuficiente |
ActiveRecord::RecordNotFound | 404 | AR find |
Pundit::NotAuthorizedError | 403 | Pundit |
JwtSignature::TokenExpiredError | 401 | Token expirat |
JwtSignature::TokenInvalidError | 401 | Token invalid |