class Cart < ApplicationRecord
belongs_to :user, optional: true
has_many :cart_items, dependent: :destroy
scope :for_session, ->(token) { where(session_token: token) }
scope :active, -> { where(merged_at: nil) }
def line_item_for(variant_id)
cart_items.find_by(variant_id: variant_id)
end
def merged?
merged_at.present?
end
def merged!
update!(merged_at: Time.current)
end
def total_quantity
cart_items.sum(:quantity)
end
end
class CartMerger
def initialize(user_cart:, guest_cart:)
@user_cart = user_cart
@guest_cart = guest_cart
end
def call
return @user_cart if @guest_cart.nil? || @guest_cart.merged?
return @user_cart if @guest_cart == @user_cart
ActiveRecord::Base.transaction do
@guest_cart.cart_items.each do |guest_item|
merge_item(guest_item)
end
@guest_cart.merged!
@guest_cart.reload.destroy!
end
@user_cart
end
private
def merge_item(guest_item)
existing = @user_cart.line_item_for(guest_item.variant_id)
if existing
existing.increment!(:quantity, guest_item.quantity)
guest_item.destroy!
else
guest_item.update!(cart_id: @user_cart.id)
end
end
end
class SessionsController < Devise::SessionsController
protected
def after_sign_in_path_for(resource)
merge_guest_cart(resource)
super
end
private
def merge_guest_cart(user)
guest_cart = current_guest_cart
return if guest_cart.nil?
CartMerger.new(
user_cart: user.current_cart,
guest_cart: guest_cart
).call
ensure
session.delete(:guest_cart_id)
end
def current_guest_cart
token = session[:guest_cart_id]
return if token.blank?
Cart.for_session(token).first
end
end
This snippet shows the common e-commerce problem of reconciling an anonymous shopping cart with a returning customer's saved cart at the moment they authenticate. Before login a visitor accumulates items against a cart keyed by session; after login those items must be folded into the persistent cart tied to their account without dropping quantities or creating duplicate line items.
The CartMerger service in the first tab is a plain-old Ruby object that takes the two carts and does the reconciliation in a single database transaction. Wrapping the work in ActiveRecord::Base.transaction guarantees the merge is atomic: if merging any line item fails, the whole operation rolls back and the customer is never left with a half-merged cart. For each guest line item it looks for a matching CartItem on the user cart by variant_id; when found it increments quantity, otherwise it re-parents the row by updating cart_id. The merged? check on the guest cart makes the operation idempotent so a retried callback cannot double-count. Finally the emptied guest cart is destroyed.
The SessionsController tab overrides Devise's controller and hooks into after_sign_in_path_for, the canonical place to run post-authentication side effects. It pulls the guest cart out of the session via current_guest_cart, hands both carts to CartMerger, and clears the stale :guest_cart_id from the session so the reference cannot leak into the next visit. Guarding on the presence of a guest cart keeps logins fast for users who arrive with an empty session.
The Cart model tab supplies the domain methods the other files lean on: line_item_for for the lookup-or-nil pattern, merged! to flip the idempotency flag, and a scope for locating carts by session token. Keeping this behavior on the model keeps the service thin and the controller ignorant of persistence details.
The trade-off is that merging by variant_id assumes line items are uniquely identified by variant; carts that allow per-line customization (gift notes, engraving) would need a richer match key. Running the merge inside the sign-in path also couples cart logic to authentication, so high-traffic stores often move the same service into a background job triggered by an event instead.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.