Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply coupon to order #402

Merged
merged 1 commit into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ecommerce/pricing/lib/pricing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ def call(event_store, command_bus)
RemoveFreeProductFromOrder,
RemoveFreeProductFromOrderHandler.new(event_store)
)
command_bus.register(
UseCoupon,
UseCouponHandler.new(event_store)
)
event_store.subscribe(CalculateOrderTotalValue, to: [
PriceItemAdded,
PriceItemRemoved,
Expand Down
8 changes: 8 additions & 0 deletions ecommerce/pricing/lib/pricing/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,12 @@ class RemoveFreeProductFromOrder < Infra::Command

alias aggregate_id order_id
end

class UseCoupon < Infra::Command
attribute :order_id, Infra::Types::UUID
attribute :coupon_id, Infra::Types::UUID
attribute :discount, Infra::Types::CouponDiscount

alias aggregate_id order_id
end
end
6 changes: 6 additions & 0 deletions ecommerce/pricing/lib/pricing/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,10 @@ class FreeProductRemovedFromOrder < Infra::Event
attribute :order_id, Infra::Types::UUID
attribute :product_id, Infra::Types::UUID
end

class CouponUsed < Infra::Event
attribute :order_id, Infra::Types::UUID
attribute :coupon_id, Infra::Types::UUID
attribute :discount, Infra::Types::CouponDiscount
end
end
13 changes: 13 additions & 0 deletions ecommerce/pricing/lib/pricing/offer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ def calculate_sub_amounts(pricing_catalog, time_promotions_discount)
end
end

def use_coupon(coupon_id, discount)
apply CouponUsed.new(
data: {
order_id: @id,
coupon_id: coupon_id,
discount: discount
}
)
end

private

on PriceItemAdded do |event|
Expand Down Expand Up @@ -151,6 +161,9 @@ def calculate_total_sub_discounts(pricing_catalog, time_promotions_discount)
@list.sub_discounts(pricing_catalog, time_promotions_discount, @discount)
end

on CouponUsed do |event|
end

class List

def initialize
Expand Down
12 changes: 12 additions & 0 deletions ecommerce/pricing/lib/pricing/services.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,16 @@ def call(command)
end
end
end

class UseCouponHandler
def initialize(event_store)
@repository = Infra::AggregateRootRepository.new(event_store)
end

def call(command)
@repository.with_aggregate(Offer, command.aggregate_id) do |order|
order.use_coupon(command.coupon_id, command.discount)
end
end
end
end
2 changes: 1 addition & 1 deletion ecommerce/pricing/test/coupons_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def test_100_is_ok
end

def test_0_01_is_ok
register_coupon(@uid, fake_name, @code, 0.01)
register_coupon(@uid, fake_name, @code, "0.01")
end
end
end
38 changes: 38 additions & 0 deletions ecommerce/pricing/test/use_coupon_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require_relative "test_helper"

module Pricing
class UseCouponTest < Test
cover "Pricing*"

def test_coupon_is_used
product_1_id = SecureRandom.uuid
set_price(product_1_id, 20)
coupon_id = SecureRandom.uuid
register_coupon(coupon_id, "Coupon", "coupon10", 10)
order_id = SecureRandom.uuid
add_item(order_id, product_1_id)

assert_events_contain(
stream_name(order_id),
CouponUsed.new(
data: {
order_id: order_id,
coupon_id: coupon_id,
discount: 10
}
)
) do
run_command(
Pricing::UseCoupon.new(order_id: order_id, coupon_id: coupon_id, discount: 10)
)
end
end

private

def stream_name(id)
"Pricing::Offer$#{id}"
end

end
end
7 changes: 7 additions & 0 deletions ecommerce/processes/lib/processes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class << self
def call(event_store, command_bus)
self.class.event_store = event_store
self.class.command_bus = command_bus
enable_coupon_discount_process(event_store, command_bus)
notify_payments_about_order_total_value(event_store, command_bus)
enable_shipment_sync(event_store, command_bus)
determine_vat_rates_on_order_placed(event_store, command_bus)
Expand Down Expand Up @@ -150,5 +151,11 @@ def register_order_on_order_placed(event_store, command_bus)
to: [Ordering::OrderPlaced]
)
end

def enable_coupon_discount_process(event_store, command_bus)
Infra::Process.new(event_store, command_bus)
.call(Pricing::CouponUsed, [:order_id, :discount],
Pricing::SetPercentageDiscount, [:order_id, :amount])
end
end
end
2 changes: 1 addition & 1 deletion infra/lib/infra/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module Types
Price = Types::Coercible::Decimal.constrained(gt: 0)
Value = Types::Coercible::Decimal
PercentageDiscount = Types::Coercible::Decimal.constrained(gt: 0, lteq: 100)
CouponDiscount = Types::Coercible::Float.constrained(gt: 0, lteq: 100)
CouponDiscount = Types::Coercible::Decimal.constrained(gt: 0, lteq: 100)
UUIDQuantityHash = Types::Hash.map(UUID, Quantity)

class VatRate < Dry::Struct
Expand Down
20 changes: 20 additions & 0 deletions rails_application/app/controllers/client/orders_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,25 @@ def remove_item
)
end

def use_coupon
coupon = Coupons::Coupon.find_by!("lower(code) = ?", params[:coupon_code].downcase)
ActiveRecord::Base.transaction do
command_bus.(use_coupon_cmd(params[:id], coupon.uid, coupon.discount))
end
flash[:notice] = "Coupon applied!"
redirect_to edit_client_order_path(params[:id])
rescue ActiveRecord::RecordNotFound
flash[:alert] = "Coupon not found!"
redirect_to edit_client_order_path(params[:id])
rescue Pricing::NotPossibleToAssignDiscountTwice
flash[:alert] = "Coupon already used!"
redirect_to edit_client_order_path(params[:id])
end

private

def use_coupon_cmd(order_id, coupon_id, discount)
Pricing::UseCoupon.new(order_id: order_id, coupon_id: coupon_id, discount: discount)
end
end
end
14 changes: 14 additions & 0 deletions rails_application/app/read_models/client_orders/edit_order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def build(order_id, order_lines, products, attributes = {})
super(attributes)
div do
products_table(order_id, products, order_lines)
coupon_form(order_id)
submit_form(order_id)
end
end
Expand Down Expand Up @@ -67,6 +68,19 @@ def remove_item_button(order_id, product_id)
button_to "Remove", remove_item_client_order_path(id: order_id, product_id: product_id), class: "hover:underline text-blue-500"
end

def coupon_form(order_id)
form(action: use_coupon_client_order_path(id: order_id), method: :post, class: "inline-flex gap-4 mt-8") do
input(
id: "coupon_code",
type: :text,
name: :coupon_code,
class: "focus:ring-blue-500 focus:border-blue-500 block shadow-sm sm:text-sm border-gray-300 rounded-md",
"data-turbo-permanent": true
)
input(type: :submit, value: "Use Coupon", class: "px-4 py-2 border rounded-md shadow-sm text-sm font-medium border-gray-300 text-gray-700 bg-white hover:bg-gray-50")
end
end

def submit_form(order_id)
form(id: "form", action: client_orders_path, method: :post) do
input(type: :hidden, name: :order_id, value: order_id)
Expand Down
3 changes: 1 addition & 2 deletions rails_application/app/read_models/orders/update_discount.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Orders
class UpdateDiscount
def call(event)
order = Order.find_by_uid(event.data.fetch(:order_id))
order = Order.find_or_create_by(uid: event.data.fetch(:order_id))
if is_newest_value?(event, order)
order.percentage_discount = event.data.fetch(:amount)
order.discount_updated_at = event.metadata.fetch(:timestamp)
Expand All @@ -28,4 +28,3 @@ def broadcaster
end
end
end

1 change: 1 addition & 0 deletions rails_application/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
member do
post :add_item
post :remove_item
post :use_coupon
end
end

Expand Down
55 changes: 55 additions & 0 deletions rails_application/test/integration/client_orders_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,56 @@ def test_current_time_promotion_is_applied
assert_select "tr td", "$2.00"
end

def test_using_coupon_applies_discount
customer_id = register_customer("Customer Shop")
product_id = register_product("Fearless Refactoring", 4, 10)
register_coupon("Coupon", "coupon10", 10)

login(customer_id)
visit_client_orders

order_id = SecureRandom.uuid
as_client_add_item_to_basket_for_order(product_id, order_id)
as_client_use_coupon(order_id, "COUPON10")

assert_select "#notice", "Coupon applied!"

as_client_submit_order_for_customer(order_id)

assert_select "tr td", "$3.60"
end

def test_using_coupon_with_wrong_code
customer_id = register_customer("Customer Shop")
product_id = register_product("Fearless Refactoring", 4, 10)
register_coupon("Coupon", "coupon10", 10)

login(customer_id)
visit_client_orders

order_id = SecureRandom.uuid
as_client_add_item_to_basket_for_order(product_id, order_id)
as_client_use_coupon(order_id, "WRONGCODE")

assert_select "#alert", "Coupon not found!"
end

def test_using_coupon_twice
customer_id = register_customer("Customer Shop")
product_id = register_product("Fearless Refactoring", 4, 10)
register_coupon("Coupon", "coupon10", 10)

login(customer_id)
visit_client_orders

order_id = SecureRandom.uuid
as_client_add_item_to_basket_for_order(product_id, order_id)
as_client_use_coupon(order_id, "COUPON10")
as_client_use_coupon(order_id, "COUPON10")

assert_select "#alert", "Coupon already used!"
end

private

def submit_order_for_customer(customer_id, order_id)
Expand All @@ -223,6 +273,11 @@ def as_client_add_item_to_basket_for_order(async_remote_id, order_id)
post "/client_orders/#{order_id}/add_item?product_id=#{async_remote_id}"
end

def as_client_use_coupon(order_id, code)
post "/client_orders/#{order_id}/use_coupon", params: { coupon_code: code }
follow_redirect!
end

def cancel_order(order_id)
post "/orders/#{order_id}/cancel"
end
Expand Down
4 changes: 4 additions & 0 deletions rails_application/test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ def register_product(name, price, vat_rate_code)
product_id
end

def register_coupon(name, code, discount)
post "/coupons", params: { coupon_id: SecureRandom.uuid, name: name, code: code, discount: discount }
end

def add_available_vat_rate(rate, code = rate.to_s)
post "/available_vat_rates", params: { code: code, rate: rate }
end
Expand Down
Loading