Skip to content

Commit

Permalink
Marketplace: DeliveryArea fees as a percentage (#2643)
Browse files Browse the repository at this point in the history
- #2641

Todo:
- [x] A `DeliveryArea` may have a delivery fee as a percentage
- [x] `Orders` to a `DeliveryArea` with a Percentage Fee calculate the fee correctly.
- [x] `Shoppers` see a description of the Delivery Fee when building their Order.

April has adjusted her fee schedule, and catering orders are now 12% and regular orders are $10 flat. This simplifies things quite a bit; hooray! But alas, we had already built out stuff to support different fees for every vendor and across regions (i.e. SF it's $20, Oakland it's $10)

I think it's probably for the best to leave that flexibility in for now, and have updated it so we now have another attribute for how to calculate fees.

I've made the Executive Decision to treat these fees as cumulative (i.e. $10 plus 12%) if both the `DeliveryArea#price` and the `DeliveryArea#fee_as_percentage` are set; rather than trying to figure out how to break ties or validate only one is set or whatever. This may be a poor decision.
  • Loading branch information
zspencer authored Oct 10, 2024
1 parent f8bced0 commit c8d1694
Show file tree
Hide file tree
Showing 18 changed files with 171 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<%- if single_delivery_area? %>
<div><small>Orders outside of this location will be subject to cancellation.</small></div>
<% end %>
<div>Delivery Fee: <%= fee_description %></div>
</div>
</div>
<div class="text-right">
Expand Down
18 changes: 18 additions & 0 deletions app/furniture/marketplace/cart/delivery_area_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ def dom_id
super(cart, :delivery_area)
end

def price
helpers.humanized_money_with_symbol(delivery_area.price)
end

def fee_as_percentage
"#{helpers.number_to_percentage(delivery_area.fee_as_percentage, precision: 0)} of subtotal"
end

def fee_description
if delivery_area.charges_fee_as_percentage? && delivery_area.charges_fee_as_price?
"#{price} plus #{fee_as_percentage}"
elsif !delivery_area.charges_fee_as_percentage? && delivery_area.charges_fee_as_price?
price
elsif delivery_area.charges_fee_as_percentage? && !delivery_area.charges_fee_as_price?
fee_as_percentage
end
end

def single_delivery_area?
cart.marketplace.delivery_areas.unarchived.size == 1
end
Expand Down
7 changes: 6 additions & 1 deletion app/furniture/marketplace/delivery.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Delivery < Record
belongs_to :marketplace
belongs_to :shopper
belongs_to :delivery_area
has_many :ordered_products, inverse_of: :order, foreign_key: :cart_id

has_encrypted :delivery_address
has_encrypted :contact_phone_number
Expand All @@ -16,7 +17,11 @@ def delivery_window
alias_method :window, :delivery_window

def fee
delivery_area&.price.presence || Money.new(0)
delivery_area&.delivery_fee(subtotal: product_total) || Money.new(0)
end

def product_total
ordered_products.sum(0, &:price_total)
end

def details_filled_in?
Expand Down
22 changes: 22 additions & 0 deletions app/furniture/marketplace/delivery_area.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,27 @@ class DeliveryArea < Record

attribute :delivery_window
monetize :price_cents

def delivery_fee(subtotal: nil)
if charges_fee_as_price? && !charges_fee_as_percentage?
price
elsif charges_fee_as_price? && charges_fee_as_percentage? && subtotal.present?
price + fee_as_percentage_of(subtotal:)
elsif !charges_fee_as_price? && charges_fee_as_percentage? && subtotal.present?
fee_as_percentage_of(subtotal:)
end
end

def charges_fee_as_percentage?
fee_as_percentage.present? && fee_as_percentage.positive?
end

def charges_fee_as_price?
price.present? && price.positive?
end

def fee_as_percentage_of(subtotal:)
subtotal * (fee_as_percentage.to_f / 100)
end
end
end
2 changes: 1 addition & 1 deletion app/furniture/marketplace/delivery_area_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</div>

<div class="text-right mt-3">
<%= price %>
<%= fee_description %>
</div>
</div>

Expand Down
14 changes: 14 additions & 0 deletions app/furniture/marketplace/delivery_area_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ def price
helpers.humanized_money_with_symbol(delivery_area.price)
end

def fee_as_percentage
"#{helpers.number_to_percentage(delivery_area.fee_as_percentage, precision: 0)} of subtotal"
end

def fee_description
if delivery_area.charges_fee_as_percentage? && delivery_area.charges_fee_as_price?
"#{price} plus #{fee_as_percentage}"
elsif !delivery_area.charges_fee_as_percentage? && delivery_area.charges_fee_as_price?
price
elsif delivery_area.charges_fee_as_percentage? && !delivery_area.charges_fee_as_price?
fee_as_percentage
end
end

def example_cart
delivery_area.marketplace.carts.new(delivery_area: delivery_area)
end
Expand Down
2 changes: 1 addition & 1 deletion app/furniture/marketplace/delivery_area_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def create?
alias_method :update?, :create?

def permitted_attributes(_)
[:label, :price, :order_by, :delivery_window, :restore]
[:label, :price, :fee_as_percentage, :order_by, :delivery_window, :restore]
end

class Scope < ApplicationScope
Expand Down
3 changes: 2 additions & 1 deletion app/furniture/marketplace/delivery_areas/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<%= render CardComponent.new(dom_id: dom_id(delivery_area), classes: "mt-3") do %>
<%= form_with(model: delivery_area.location) do |delivery_area_form| %>
<%= render "text_field", attribute: :label, form: delivery_area_form %>
<%= render "money_field", attribute: :price, form: delivery_area_form, required: true, step: 0.01, min: 0 %>
<%= render "money_field", attribute: :price, form: delivery_area_form, step: 0.01, min: 0 %>
<%= render "percentage_field", attribute: :fee_as_percentage, form: delivery_area_form, step: 1, min: 0, max: 100 %>
<%= render "text_field", attribute: :order_by, form: delivery_area_form %>
<%= render "text_field", attribute: :delivery_window, form: delivery_area_form %>

Expand Down
2 changes: 1 addition & 1 deletion app/furniture/marketplace/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def delivery
end

def delivery_fee
delivery_area&.price
delivery_area&.delivery_fee(subtotal: product_total)
end
delegate :delivery_window, to: :delivery_area, allow_nil: true

Expand Down
13 changes: 13 additions & 0 deletions app/views/application/_percentage_field.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<% required = local_assigns[:required] || false %>
<% min = local_assigns[:min] || 0 %>
<% step = local_assigns[:step] || 0.01 %>
<div>
<%= form.label attribute, class: "block font-medium text-gray-700"%>
<div class="relative mt-1 rounded-md shadow-sm">
<%= form.number_field attribute, class: "block w-full rounded-md border-gray-300 !pl-7 !pr-12 focus:border-indigo-500 focus:ring-indigo-500", step: step, min: min, required: required %>
<div class="pointer-events-none absolute bottom-0 right-0 flex items-center pt-3 pr-3">
<span class="text-gray-500">%</span>
</div>
</div>
<%= render partial: "error", locals: { model: form.object, attribute: attribute } %>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class MarketplaceAddFeeAsPercentageToDeliveryAreas < ActiveRecord::Migration[7.1]
def change
add_column :marketplace_delivery_areas, :fee_as_percentage, :integer
end
end
3 changes: 2 additions & 1 deletion 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.1].define(version: 2024_05_31_200256) do
ActiveRecord::Schema[7.1].define(version: 2024_10_10_004915) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
Expand Down Expand Up @@ -159,6 +159,7 @@
t.string "delivery_window"
t.string "order_by"
t.datetime "discarded_at"
t.integer "fee_as_percentage"
t.index ["discarded_at"], name: "index_marketplace_delivery_areas_on_discarded_at"
t.index ["marketplace_id"], name: "index_marketplace_delivery_areas_on_marketplace_id"
end
Expand Down
1 change: 0 additions & 1 deletion spec/factories/furniture/marketplace/marketplace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@
marketplace

label { Faker::Address.city }
price { Faker::Commerce.price }
trait :archived do
discarded_at { 1.hour.ago }
end
Expand Down
24 changes: 22 additions & 2 deletions spec/furniture/marketplace/delivery_area_component_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,31 @@
let(:delivery_area) { create(:marketplace_delivery_area) }

it { is_expected.to have_content(delivery_area.label) }
it { is_expected.to have_content(vc_test_controller.view_context.humanized_money_with_symbol(delivery_area.price)) }

it { is_expected.to have_css("a[href='#{polymorphic_path(delivery_area.location)}'][data-turbo-method=delete]") }
it { is_expected.to have_link(I18n.t("archive.link_to", href: polymorphic_path(delivery_area.location))) }

context "when the delivery area has a fee as percentage" do
let(:delivery_area) { create(:marketplace_delivery_area, fee_as_percentage: 10) }

it { is_expected.to have_content("10% of subtotal") }
end

context "when the delivery area has a price" do
let(:delivery_area) { create(:marketplace_delivery_area, price: 10) }

it { is_expected.to have_content("$10.00") }
end

context "when the delivery are has a price and a fee as percentage" do
let(:delivery_area) { create(:marketplace_delivery_area, price: 10, fee_as_percentage: 10) }

it { is_expected.to have_content("$10.00 plus 10% of subtotal") }
end

context "when the delivery area has neither a price nor a percentage" do
it { is_expected.to have_no_content("of subtotal") }
end

context "when the delivery area is Discarded" do
let(:delivery_area) { create(:marketplace_delivery_area, :archived) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@

specify do
perform_request
assert_select("form input", 5)
assert_select("form input", 6)
assert_select("#delivery_area_label")
assert_select("#delivery_area_price")
assert_select("#delivery_area_fee_as_percentage")
assert_select("#delivery_area_order_by")
assert_select("#delivery_area_delivery_window")
end
Expand Down
25 changes: 22 additions & 3 deletions spec/furniture/marketplace/delivery_areas_system_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
sign_in(space.members.first, space)
end

describe "Setting a Delivery Fee" do
it "allows Percentage-Based Delivery Fees" do
visit(polymorphic_path(marketplace.location(child: :delivery_areas)))
click_link("Add Delivery Area")
fill_in("Label", with: "Percentage Based Delivery Area")
fill_in("Fee as percentage", with: "10")

expect { click_button("Create") }.to change(marketplace.delivery_areas, :count).by(1)
expect(page).to have_content("Percentage Based Delivery Area")
within(marketplace.delivery_areas.order(created_at: :desc).first) do
expect(page).to have_content("10% of subtotal")
end
end
end

describe "Restoring Delivery Areas" do
let!(:delivery_area) do
create(:marketplace_delivery_area, :archived, marketplace:,
Expand All @@ -18,7 +33,7 @@
it "Makes the DeliveryArea selectable" do
visit(polymorphic_path(marketplace.location(child: :delivery_areas)))
click_link("Archived Delivery Areas")
within("##{dom_id(delivery_area)}") do
within(delivery_area) do
click_link("Edit")
end

Expand All @@ -40,7 +55,7 @@
cart = create(:marketplace_cart, delivery_area:, marketplace:)
visit(polymorphic_path(marketplace.location(child: :delivery_areas)))
click_link("Archived Delivery Areas")
within("##{dom_id(delivery_area)}") do
within(delivery_area) do
accept_confirm { click_link(I18n.t("destroy.link_to")) }
end

Expand All @@ -56,10 +71,14 @@
visit(polymorphic_path(marketplace.location(child: :delivery_areas)))
click_link("Archived Delivery Areas")

within("##{dom_id(delivery_area)}") do
within(delivery_area) do
expect(page).to have_no_content(I18n.t("destroy.link_to"))
end
end
end
end

def within(model, *, **, &block)
page.within("##{dom_id(model)}", *, **, &block)
end
end
22 changes: 21 additions & 1 deletion spec/furniture/marketplace/delivery_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,31 @@
describe "#fee" do
subject(:fee) { delivery.fee }

context "when `price_cents` is nil" do
context "when `delivery_area#price_cents` is nil" do
let(:delivery_area) { build(:marketplace_delivery_area, marketplace: marketplace, price_cents: nil) }

it { is_expected.to be_zero }
end

context "when `delivery_area#fee_as_percentage` is set" do
let(:delivery_area) { build(:marketplace_delivery_area, marketplace: marketplace, fee_as_percentage: 10) }

context "with no products" do
it { is_expected.to be_zero }
end

context "with products" do
let(:product_a) { create(:marketplace_product, marketplace:, price_cents: 10_00) }
let(:product_b) { create(:marketplace_product, marketplace:, price_cents: 5_00) }

before do
delivery.ordered_products.build(product: product_a, quantity: 1)
delivery.ordered_products.build(product: product_b, quantity: 2)
end

it { is_expected.to eql(Money.new(2_00)) }
end
end
end

describe "#window" do
Expand Down
20 changes: 18 additions & 2 deletions spec/furniture/marketplace/order_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
subject(:price_total) { order.price_total }

let(:marketplace) { create(:marketplace) }
let(:delivery_area) { create(:marketplace_delivery_area, marketplace: marketplace, price_cents: 1200) }

let(:order) { create(:marketplace_order, marketplace: marketplace, delivery_area: delivery_area) }
let(:product_a) { create(:marketplace_product, marketplace: order.marketplace) }
let(:product_b) { create(:marketplace_product, marketplace: order.marketplace, tax_rate_ids: [sales_tax.id]) }
Expand All @@ -34,7 +34,23 @@
order.ordered_products.create!(product: product_b, quantity: 2)
end

it { is_expected.to eql(product_a.price + product_b.price * 2 + delivery_area.price + (product_b.price * 2 * 0.05)) }
context "when the delivery area has a fee as a percentage" do
let(:delivery_area) { create(:marketplace_delivery_area, marketplace: marketplace, fee_as_percentage: 10) }

it { is_expected.to eql(product_a.price + product_b.price * 2 + delivery_area.delivery_fee(subtotal: order.product_total) + (product_b.price * 2 * 0.05)) }
end

context "when the delivery area has a flat fee" do
let(:delivery_area) { create(:marketplace_delivery_area, marketplace: marketplace, price_cents: 1200) }

it { is_expected.to eql(product_a.price + product_b.price * 2 + delivery_area.price + (product_b.price * 2 * 0.05)) }
end

context "when the delivery area has a flat fee and a percentage fee" do
let(:delivery_area) { create(:marketplace_delivery_area, marketplace: marketplace, price_cents: 1200, fee_as_percentage: 10) }

it { is_expected.to eql(product_a.price + product_b.price * 2 + delivery_area.price + delivery_area.fee_as_percentage_of(subtotal: order.product_total) + (product_b.price * 2 * 0.05)) }
end
end

describe "#product_total" do
Expand Down

0 comments on commit c8d1694

Please sign in to comment.