, Štefan Húska
Guest checkout v Rails s Devise: objednávka bez registrácie
Objednávkový flow v e-shope je lievik — čím viac krokov, tým viac ľudí odpadne. V našom fotolabe Tlačenica.sk sme tento lievik práve zúžili - odstránili sme povinnú registráciu zavedením guest userov.
Požiadavka “chcem si objednať fotky” a odpoveď “najprv sa zaregistrujte” je spoľahlivý spôsob, ako prísť o zákazníka. Celá aplikácia je postavená na Devise autentifikácii a current_user preniká každým controllerom. Namiesto prepisovania polovice kódu sme zvolili iný prístup: vytvoríme skutočného usera, len ho označíme ako guest.
![FireShot Capture 008 - Tlačenica - Profesionálny fotolab Senec - [tlacenica.sk].png](/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTQsInB1ciI6ImJsb2JfaWQifX0=--a8dc83f173a3a9cb64bf0bf533cef40a719b105c/FireShot%20Capture%20008%20-%20Tla%C4%8Denica%20-%20Profesion%C3%A1lny%20fotolab%20Senec%20-%20%5Btlacenica.sk%5D.png)
Prečo nie session-based košík
Prvý inštinkt bol ukladať košík do session. Ale v našej doméne zákazník nahráva fotky (Active Storage, S3), vytvára albumy, počíta sa mu cena — všetko to vyžaduje belongs_to :user. Guest user, ktorý je plnohodnotný User záznam, znamená nula zmien v existujúcich controlleroch.
Anatómia guest usera
Migrácia pridá jediný boolean stĺpec:
# db/migrate/20260326101958_add_guest_to_users.rb
add_column :users, :guest, :boolean, default: false, null: false
V User modeli pribudne factory metóda, ktorá vytvorí potvrdený účet s náhodným emailom a heslom. Kľúčové je confirmed_at: Time.current — guest nesmie dostať potvrdzovací email:
# app/models/user.rb
def self.create_guest!
password = Devise.friendly_token[0, 20]
create!(
email: "guest_#{SecureRandom.hex(8)}@guests.tlacenica.sk",
password: password,
password_confirmation: password,
guest: true,
confirmed_at: Time.current
)
end
Ešte jeden dôležitý detail — terms of service validácia sa preskočí pre guestov, inak by create! zlyhal:
validates :terms_of_service, acceptance: true, on: :create, unless: :guest?
Transparentné prihlásenie
GuestSessionsController vytvorí guest usera a prihlási ho cez Devise sign_in. Od tohto momentu current_user funguje všade rovnako — v controlleroch, view helpers, Turbo broadcast autorizácii:
# app/controllers/guest_sessions_controller.rb
def create
unless user_signed_in?
user = User.create_guest!
sign_in(user)
end
redirect_to new_dashboard_album_path
end
Na landing pages sa tlačidlo “Začať objednávku” zmení z link_to new_user_registration_path na button_to start_order_path — POST request, ktorý vytvorí guest session a rovno presmeruje do objednávkového flow.
Zachytenie skutočného emailu
Guest má placeholder email guest_xxx@guests.tlacenica.sk, ale pred dokončením objednávky potrebujeme skutočný email na notifikácie. Riešenie: formulár adresy zobrazí extra pole pre guestov. Controller validuje email a pri kolízii s existujúcim účtom ponúkne prihlásenie:
![FireShot Capture 009 - Pridať adresu - [localhost].png](/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTUsInB1ciI6ImJsb2JfaWQifX0=--d3d2662c1f77cbad45e9957e8de909d53cab5094/FireShot%20Capture%20009%20-%20Prida%C5%A5%20adresu%20-%20%5Blocalhost%5D.png)
Uloženie emailu pod aktuálneho používateľa (nahradenie vygenerovaného guest emailu), s kontrolou kolízie s už existujúcim účtom:
# app/controllers/dashboard/user_addresses_controller.rb
def update_guest_email
email = params[:guest_email]&.strip&.downcase
if email.blank?
@address.errors.add(:base, t("addresses.email_required"))
return false
end
existing = User.where(email: email).where.not(id: current_user.id).first
if existing && !existing.guest?
@address.errors.add(:base, t("addresses.email_taken"))
@show_login_link = true
return false
end
current_user.skip_reconfirmation!
current_user.update!(email: email, confirmed_at: Time.current)
true
end
skip_reconfirmation! je kľúčový — bez neho by Devise odoslal potvrdzovací email a zablokoval účet.
Premena guest na registrovaného
V prípade, že zákazník dokončí objednávku ako guest user, po dokončení sa mu zobrazí formulár s možnosťou nastaviť si heslo. To ho povýši na plnohodnotného usera, ktorý sa môže v budúcnosti opakovane prihlásiť a vidieť aj históriu svojich objednávok.
![FireShot Capture 010 - Detaily objednávky - [localhost].png](/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTYsInB1ciI6ImJsb2JfaWQifX0=--8e2bba829e8b8d39ba3270db8315c123c1de14aa/FireShot%20Capture%20010%20-%20Detaily%20objedn%C3%A1vky%20-%20%5Blocalhost%5D.png)
Metóda claim! prepne guest na plnohodnotný účet:
# app/models/user.rb
def claim!(email:, password:)
self.email = email
self.password = password
self.password_confirmation = password
self.guest = false
self.confirmed_at = Time.current
skip_reconfirmation!
save!
end
Merge dát pri prihlásení existujúcim účtom
Čo ak guest zistí, že už má účet? Klikne “Prihláste sa”, čím sa odhlási z guest session. Na login stránke je hidden field merge_guest_id, ktorý cestuje cez Devise sign-in flow. Po úspešnom prihlásení ApplicationController prenesie všetky dáta z guest usera:
# app/models/user.rb
def merge_guest_data!(guest_user)
transaction do
guest_user.albums.update_all(user_id: id)
if guest_user.cart.present?
if cart.present?
# Both have carts — move items and destroy guest cart
guest_user.cart.cart_items.update_all(cart_id: cart.id)
guest_user.cart.destroy
else
guest_user.cart.update!(user_id: id)
end
end
guest_user.user_addresses.update_all(user_id: id)
guest_user.reload
guest_user.destroy
end
end
Transakcia je nevyhnutná — ak by sa niektorý krok nepodaril, nechceme pol-mergnutého stavu. reload pred destroy je tiež dôležitý — bez neho by ActiveRecord držal v pamäti asociácie (cart, albums), ktoré sme práve presunuli, a destroy by kaskádovo zmazal aj prenesené záznamy.
Upratovanie
Guest useri, ktorí nedokončia objednávku, zostanú v databáze. Solid Queue job ich po siedmich dňoch vymaže — ale len tých, ktorí nemajú žiadnu objednávku:
# app/jobs/purge_guest_users_job.rb
User.guests
.where("users.created_at < ?", RETENTION_DAYS.days.ago)
.left_joins(:orders)
.where(orders: { id: nil })
.find_each(&:destroy)
left_joins s where(orders: { id: nil }) je SQL anti-join pattern — nájde userov, ktorí nemajú ani jednu objednávku. Job beží denne o tretej ráno cez Solid Queue recurring schedule.
Čo si z toho odniesť
Guest user pattern je pragmatický kompromis: namiesto refactoru polovice aplikácie vytvoríme plnohodnotný záznam User a označíme ho flagom. current_user funguje bez zmien, Devise session management funguje bez zmien, autorizácia funguje bez zmien. Cena za to je lifecycle management — čistenie starých guestov, merge pri prihlásení, konverzia na registrovaného. Ale tá cena je izolovaná v troch metódach na User modeli a jednom background jobe. Zvyšok aplikácie o guestoch ani nevie.