Skip to content

Commit

Permalink
Marketplace: Refactor to turn Checkout into a Command
Browse files Browse the repository at this point in the history
#831

This "Demotes" `Checkout` from an Active Record and makes it serve more
as a Command object than a Domain Model. It's still ActiveModel'y so it
can be rendered using `render checkout` or routed to using `redirect_to
checkout.location`, but otherwise it's not holding any data or what not.
  • Loading branch information
zspencer committed Jan 21, 2023
1 parent 65f1614 commit 397f8ef
Show file tree
Hide file tree
Showing 26 changed files with 142 additions and 111 deletions.
19 changes: 8 additions & 11 deletions app/furniture/marketplace/cart.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
# frozen_string_literal: true

class Marketplace
class Cart < ApplicationRecord
class Cart < Record
self.table_name = "marketplace_carts"
include WithinLocation
self.location_parent = :marketplace

default_scope { where(status: :pre_checkout) }

belongs_to :marketplace, inverse_of: :carts
belongs_to :shopper, inverse_of: :carts
delegate :space, :room, to: :marketplace

belongs_to :shopper, inverse_of: :carts

has_many :cart_products, dependent: :destroy, inverse_of: :cart
has_many :products, through: :cart_products, inverse_of: :carts
has_one :checkout, inverse_of: :cart
has_one :order, inverse_of: :cart

enum status: {
pre_checkout: "pre_checkout",
checked_out: "check_out"
paid: "paid"
}

def self.model_name
@_model_name ||= ActiveModel::Name.new(self, ::Marketplace)
end

def price_total
cart_products.sum do |cart_product|
cart_products.sum(0) do |cart_product|
cart_product.product.price * cart_product.quantity
end
end
Expand Down
8 changes: 2 additions & 6 deletions app/furniture/marketplace/cart_product.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class Marketplace
class CartProduct < ApplicationRecord
class CartProduct < Record
self.table_name = "marketplace_cart_products"

belongs_to :cart, inverse_of: :cart_products
Expand All @@ -14,14 +14,10 @@ class CartProduct < ApplicationRecord

attribute :quantity, :integer, default: 0

def self.model_name
@_model_name ||= ActiveModel::Name.new(self, ::Marketplace)
end

private

def editable_cart
return unless cart&.checked_out?
return if cart&.pre_checkout?

errors.add(:base, "Can't edit a checked-out cart!")
end
Expand Down
2 changes: 1 addition & 1 deletion app/furniture/marketplace/carts/_footer.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<tfoot id="cart-footer-<%= cart.id%>" class="bg-gray-50">
<tr>
<td></td>
<th> <%= button_to("checkout", [space, room, marketplace, :checkouts], data: { turbo: false }) %> </th>
<th> <%= button_to("checkout", cart.location(child: :checkout), data: { turbo: false }) %> </th>
<th scope="row" class="text-right px-1 py-3.5">Total: </th>
<td class="text-left px-3 py-3.5 font-bold">
<%= render "marketplace/carts/total", cart: cart %>
Expand Down
36 changes: 12 additions & 24 deletions app/furniture/marketplace/checkout.rb
Original file line number Diff line number Diff line change
@@ -1,53 +1,41 @@
class Marketplace
class Checkout < ApplicationRecord
self.table_name = "marketplace_checkouts"
include WithinLocation
self.location_parent = :marketplace

belongs_to :cart, inverse_of: :checkout
delegate :marketplace, to: :cart

has_many :ordered_products, through: :cart, source: :cart_products, class_name: "Marketplace::OrderedProduct"

belongs_to :shopper, inverse_of: :checkouts
class Checkout < Model
self.location_parent = :cart
include ActiveModel::Validations
attr_accessor :cart
delegate :shopper, :marketplace, to: :cart

# It would be nice to validate instead the presence of :ordered_products, but my attempts at this raise:
# ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection:
# Cannot modify association 'Marketplace::Checkout#ordered_products' because the source reflection class 'CartProduct' is associated to 'Cart' via :has_many.
validates :stripe_line_items, presence: true

enum status: {
pre_checkout: "pre_checkout",
paid: "paid"
}

def self.model_name
@_model_name ||= ActiveModel::Name.new(self, ::Marketplace)
end

def create_stripe_session(success_url:, cancel_url:)
Stripe::Checkout::Session.create({
line_items: stripe_line_items,
mode: "payment",
success_url: success_url,
cancel_url: cancel_url,
payment_intent_data: {
transfer_group: id
transfer_group: cart.id
}
}, {
api_key: marketplace.stripe_api_key
})
end

def complete(stripe_session_id:)
update!(status: :paid, stripe_session_id: stripe_session_id)
cart.update!(status: :checked_out)
cart.update!(status: :paid, stripe_session_id: stripe_session_id)
end

def persisted?
true
end

private

def stripe_line_items
return [] unless cart.present?
return [] if cart.blank?

cart.cart_products.map do |cart_product|
{
Expand Down
18 changes: 11 additions & 7 deletions app/furniture/marketplace/checkouts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ def show
checkout.complete(stripe_session_id: params[:stripe_session_id])
flash[:notice] = t(".success")
end
redirect_to checkout.becomes(Order).location
redirect_to order.location
end

def create
authorize(checkout)

if checkout.save
if checkout.valid?
stripe_session = checkout.create_stripe_session(
success_url: "#{polymorphic_url(checkout.location)}?stripe_session_id={CHECKOUT_SESSION_ID}",
cancel_url: polymorphic_url(marketplace.location)
Expand All @@ -28,15 +28,19 @@ def create
end

helper_method def checkout
@checkout ||= if params[:id]
Checkout.find(params[:id])
@checkout ||= Checkout.new(cart: cart)
end

helper_method def cart
@cart ||= if params[:cart_id]
marketplace.carts.find(params[:cart_id])
else
cart.checkout || cart.build_checkout(shopper: shopper)
marketplace.carts.find_or_create_by(shopper: shopper)
end
end

helper_method def cart
@cart ||= marketplace.carts.find_or_create_by(shopper: shopper, status: :pre_checkout)
def order
@order ||= cart.becomes(Order)
end
end
end
2 changes: 1 addition & 1 deletion app/furniture/marketplace/marketplace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class Marketplace < FurniturePlacement

has_many :products, inverse_of: :marketplace, dependent: :destroy
has_many :carts, inverse_of: :marketplace, dependent: :destroy
has_many :orders, through: :carts
has_many :orders, inverse_of: :marketplace

# The Secret Stripe API key belonging to the owner of the Marketplace
def stripe_api_key=(key)
Expand Down
10 changes: 10 additions & 0 deletions app/furniture/marketplace/model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Marketplace
class Model
include ActiveModel::Model
include WithinLocation

def self.model_name
@_model_name ||= ActiveModel::Name.new(self, ::Marketplace)
end
end
end
21 changes: 20 additions & 1 deletion app/furniture/marketplace/order.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
class Marketplace
class Order < Checkout
class Order < Record
self.table_name = "marketplace_carts"
self.location_parent = :marketplace

belongs_to :marketplace, inverse_of: :orders
delegate :space, :room, to: :marketplace

belongs_to :shopper, inverse_of: :orders

has_many :ordered_products, inverse_of: :order, foreign_key: :cart_id
has_many :products, through: :ordered_products, inverse_of: :orders

enum status: {
paid: "paid"
}

def price_total
ordered_products.sum(0) do |ordered_product|
ordered_product.product.price * ordered_product.quantity
end
end
end
end
12 changes: 8 additions & 4 deletions app/furniture/marketplace/ordered_product.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
class Marketplace
class OrderedProduct < CartProduct
include WithinLocation
self.location_parent = :checkout
class OrderedProduct < Record
self.table_name = "marketplace_cart_products"

has_one :checkout, through: :cart
self.location_parent = :order

belongs_to :order, inverse_of: :ordered_products, foreign_key: :cart_id

belongs_to :product, inverse_of: :ordered_products
delegate :name, :description, :price, :price_cents, to: :product
end
end
2 changes: 1 addition & 1 deletion app/furniture/marketplace/orders/_order.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<td></td>
<th scope="row" class="text-right px-1 py-3.5">Total: </th>
<td class="text-left px-3 py-3.5 font-bold">
<%= render "marketplace/carts/total", cart: order.cart %>
<%= humanized_money_with_symbol(order.price_total) %>
</td>
</tr>
</tfoot>
Expand Down
6 changes: 4 additions & 2 deletions app/furniture/marketplace/product.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# frozen_string_literal: true

class Marketplace
class Product < ApplicationRecord
class Product < Record
self.table_name = "marketplace_products"
include WithinLocation
self.location_parent = :marketplace

extend StripsNamespaceFromModelName
Expand All @@ -14,6 +13,9 @@ class Product < ApplicationRecord
has_many :cart_products, inverse_of: :product, dependent: :destroy
has_many :carts, through: :cart_products, inverse_of: :products

has_many :ordered_products, inverse_of: :product, dependent: :destroy
has_many :orders, through: :ordered_products, inverse_of: :products

attribute :name, :string
validates :name, presence: true

Expand Down
9 changes: 9 additions & 0 deletions app/furniture/marketplace/record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Marketplace
class Record < ApplicationRecord
self.abstract_class = true

def self.model_name
@_model_name ||= ActiveModel::Name.new(self, ::Marketplace)
end
end
end
2 changes: 1 addition & 1 deletion app/furniture/marketplace/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ def self.append_routes(router)
router.resources :products
router.resources :carts do
router.resources :cart_products
router.resource :checkout, only: [:show, :create]
end
router.resources :checkouts, only: [:show, :create]
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions app/furniture/marketplace/shopper.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# frozen_string_literal: true

class Marketplace
class Shopper < ApplicationRecord
class Shopper < Record
self.table_name = "marketplace_shoppers"

belongs_to :person, optional: true
has_many :carts, inverse_of: :shopper
has_many :checkouts, inverse_of: :shopper
has_many :orders, inverse_of: :shopper
end
end
1 change: 1 addition & 0 deletions app/models/application_record.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include WithinLocation
end
16 changes: 16 additions & 0 deletions db/migrate/20230120234526_drop_checkouts_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class DropCheckoutsTable < ActiveRecord::Migration[7.0]
def change
drop_table "marketplace_checkouts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "cart_id"
t.uuid "shopper_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "status", default: "pre_checkout", null: false
t.string "stripe_session_id"
t.index ["cart_id"], name: "index_marketplace_checkouts_on_cart_id"
t.index ["shopper_id"], name: "index_marketplace_checkouts_on_shopper_id"
end

add_column "marketplace_carts", :stripe_session_id, :string
end
end
18 changes: 4 additions & 14 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2023_01_19_014621) do
ActiveRecord::Schema[7.0].define(version: 2023_01_20_234526) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
Expand All @@ -22,12 +22,12 @@
"expired",
"ignored",
"revoked",
"sent",
"sent"
], force: :cascade

create_enum :membership_status, [
"active",
"revoked",
"revoked"
], force: :cascade

create_table "active_storage_attachments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
Expand Down Expand Up @@ -140,21 +140,11 @@
t.datetime "updated_at", null: false
t.uuid "shopper_id"
t.string "status", default: "pre_checkout", null: false
t.string "stripe_session_id"
t.index ["marketplace_id"], name: "index_marketplace_carts_on_marketplace_id"
t.index ["shopper_id"], name: "index_marketplace_carts_on_shopper_id"
end

create_table "marketplace_checkouts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "cart_id"
t.uuid "shopper_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "status", default: "pre_checkout", null: false
t.string "stripe_session_id"
t.index ["cart_id"], name: "index_marketplace_checkouts_on_cart_id"
t.index ["shopper_id"], name: "index_marketplace_checkouts_on_shopper_id"
end

create_table "marketplace_products", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "marketplace_id"
t.string "name"
Expand Down
Binary file modified docs/erd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 4 additions & 10 deletions spec/factories/marketplace/checkouts.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
FactoryBot.define do
factory :marketplace_checkout, class: "Marketplace::Checkout" do
trait :with_shopper do
transient do
person { nil }
end

shopper { build(:marketplace_shopper, person: person) }
end

trait :with_cart do
transient do
marketplace { nil }
person { nil }
end
before(:build) do |checkout, evaluator|
build(:marketplace_cart, marketplace: marketplace, checkout: checkout, shopper: checkout.shopper)
after(:build) do |checkout, evaluator|
shopper = build(:marketplace_shopper, person: evaluator.person)
checkout.cart = build(:marketplace_cart, marketplace: evaluator.marketplace, shopper: shopper)
end
end
end
Expand Down
Loading

0 comments on commit 397f8ef

Please sign in to comment.