, Š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

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

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

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.