Skip to content

Instantly share code, notes, and snippets.

@joegaudet
Last active May 16, 2020 16:30
Show Gist options
  • Save joegaudet/691764b5f80d0a5e1f164c22d5e34f23 to your computer and use it in GitHub Desktop.
Save joegaudet/691764b5f80d0a5e1f164c22d5e34f23 to your computer and use it in GitHub Desktop.
module Accounting
module Adapters
class ClientOrder < Adapter
alias_attribute :ledger, :invoice
def initialize(order)
raise ArgumentError, 'ClientOrder must adapt an order ' unless order.is_a? Order
super(order)
end
# This interface doesn't make heaps of sense.
def order
self
end
def invoice_recipient
self.client
end
def fees
[
service_fee,
group_order_members.map(&:fees),
(delivery_fee.paper_trail.version_at(deliver_at || Time.current) rescue nil)
].flatten.compact
end
def discounts
[
client_discounts,
promo_discount
].flatten.compact
end
def waived_fees
[
waived_delivery_fee,
waived_service_fee
].compact
end
def cleared_payments
group_order_members.map(&:out_of_pocket_payment).compact
end
def group_order_members
super.map {|gom| Accounting::Adapters::GroupOrderMemberOrder.new(gom)}
end
# @param [OrderItem] order_item
def accounting_code_for(order_item)
order_item.client_accounting_code
end
# @param [OrderItem] order_item
def net_price_for(order_item)
order_item.client_net_price
end
# @param [OrderItem] order_item
def tax_price_for(order_item)
order_item.client_tax_price
end
def ledger_class
Accounting::Invoice
end
end
end
end
module Accounting
module Adapters
class GroupOrderMemberOrder < Adapter
alias_attribute :ledger, :invoice
def initialize(group_order_member)
raise ArgumentError, 'GroupOrderMemberOrder must adapt a group order member' unless group_order_member.is_a? GroupOrderMember
super(group_order_member)
end
def identifier
"#{order.identifier}-#{self.id}"
end
def invoice_recipient
self.user || self
end
def cleared_payments
ret = []
if order_items.any? && per_person_budget_price > 0
ret.push(BasePayment.build(self.wrapped, ordered_items_net, ordered_items_tax))
end
ret
end
def fees
ret = []
ret.push(order.top_up_fee) if order.top_up_fee.present? && exceeds_budget?
ret.compact
ret
end
def waived_fees
[order.waived_top_up_fee].compact
end
# @param [OrderItem] order_item
# @return [String]
def accounting_code_for(order_item)
order_item.client_accounting_code
end
# @param [OrderItem] order_item
# @return [Money]
def net_price_for(order_item)
order_item.client_net_price
end
# @param [OrderItem] order_item
# @return [Money]
def tax_price_for(order_item)
order_item.client_tax_price
end
def ordered_items_net
ordered_items.sum(&:client_net_price)
end
def ordered_items_tax
ordered_items.sum(&:client_tax_price)
end
def ledger_class
Accounting::Invoice
end
def eager_load!
GroupOrderMember.includes(
order: [:client],
order_item: [
menu_item: [:menu_group, [:menu]],
order_item_menu_option_items: [
menu_option_items: [:menu_option_group]
]
]
)
end
def exceeds_budget?
order.top_up_allowed? && ordered_items_net > per_person_budget_price
end
end
end
end
module Accounting
module Commands
class ProcessLedger < ::Commands::Command
dependency :save, ::Commands::SaveRecord
dependency :emit, ::Events::Emitter
attr_reader :ledger, :order
# @param [Accounting::Interfaces::Order] order
def call(order, due_date: Time.now)
@order = order
# Ensure no two processes can run simultaneously
order.with_lock do
raise ArgumentError, "#{order} does not implement Account::Interfaces::Order." unless (order.is_accounting_order?)
@ledger = order.ledger || order.ledger_class.new(
currency: order.currency,
identifier: order.identifier,
due_date: due_date
)
is_new = !@ledger.persisted?
is_closed = @ledger.closed?
order.ledger = @ledger
ledger.recipient = order.invoice_recipient
unless (@order.is_newer_than?(ledger) || is_new || is_closed)
if block_given?
yield ledger
end
return ledger
end
ledger.reset!
order.eager_load!
process_ordered_items(order)
apply_fees(order)
apply_discounts(order)
process_payments(order)
save.(ledger)
save.(order)
emit.(Accounting::Events::InvoiceProcessed.(order, ledger))
# Say you want to ensure some process (templating an email) runs
# inside of the locked order, then this yield is for you
#
# Example Usage
#
# process_ledgers.(order) do |ledger|
# send_email.(ledger)
# end
#
if block_given?
yield ledger
end
ledger
end
end
private
def process_ordered_items(order)
order.ordered_items.each do |order_item|
ledger.ordered_items.build(
quantity: order_item.quantity,
description: order_item.description,
# Sadly the system speaks in Money, and ledgers speak in dollars
net_amount: order.net_price_for(order_item),
tax_amount: order.tax_price_for(order_item),
legacy_tax_rate: order_item.tax_rate,
accounting_code: order.accounting_code_for(order_item)
)
end
end
def apply_discounts(order)
order.discounts.each do |discount|
# discounts must be split across tax rates
order.ordered_items_by_tax_rate.each do |tax_rate, ordered_items|
# Discounts are always computed against the total amount of ordered items
net_price = ordered_items.map{|oi| order.net_price_for(oi).dollars}.sum
discount_amount = discount.apply(net_price)
# Total dollars discounted so far
discounted_net = -ledger.discounts.sum(&:net_amount)
# We need to ensure that the we cannot discount more than the total
# amount of the order
remainder_net = net_price - discounted_net
amount = [discount_amount, remainder_net].min
ledger.discounts.build(discount.as_line_item(amount, tax_rate))
end
end
end
def apply_fees(order)
order.fees.each do |fee|
next unless fee.applies?(order)
net_price = order.ordered_items_net
net_fee_amount = fee.apply(net_price)
ledger.fees.build(fee.as_line_item(net_fee_amount))
waived_fee = fee.waived_by(order)
unless waived_fee.nil?
amount = waived_fee.apply(net_fee_amount)
ledger.discounts.build(waived_fee.as_line_item(amount, fee.tax_rate))
end
end
end
def process_payments(order)
order.cleared_payments.each { |payment| ledger.cleared_payments.build(payment.as_line_item) }
end
end
end
end
module Accounting
module Commands
class ProcessGroupOrderMember < ::Commands::Command
dependency :process_ledger, Accounting::Commands::ProcessLedger
dependency :find, ::Queries::Id.klass(GroupOrderMember)
def call(group_order_member_id)
group_order_member = find.(group_order_member_id)
process_ledger.(Accounting::Adapters::GroupOrderMemberOrder.new(group_order_member))
group_order_member
end
end
end
end
module Accounting
module Commands
class ProcessOrder < ::Commands::Command
dependency :process_ledger, Accounting::Commands::ProcessLedger
dependency :find, ::Queries::Id.klass(Order)
def call(order_id)
order = find.(order_id)
process_ledger.(Accounting::Adapters::RestaurantOrder.new(order))
process_ledger.(Accounting::Adapters::ClientOrder.new(order))
order
end
end
end
end
module Accounting
module Adapters
class RestaurantOrder < Adapter
alias_attribute :ledger, :restaurant_bill
def initialize(order)
raise ArgumentError, 'ClientOrder must adapt an order ' unless order.is_a? Order
super(order)
end
# This interface doesn't make heaps of sense.
def order
self
end
def invoice_recipient
self.restaurant
end
def fees
[ restaurant_admin_fee ] rescue []
end
def discounts
[
restaurant_discounts
].flatten.compact
end
def waived_fees
[
waived_restaurant_admin_fee
].compact
end
def accounting_code_for(order_item)
order_item.restaurants_accounting_code
end
def net_price_for(order_item)
order_item.restaurant_net_price
end
def tax_price_for(order_item)
order_item.order.restaurant.tax_exempt? ? 0 : order_item.restaurant_tax_price
end
def ledger_class
Accounting::Bill
end
end
end
end
order_id = 1
group_order_member_id = 1
Accounting::Commands::ProcessOrder.new.(order_id)
Accounting::Commands::ProcessGroupOrderMember.new.(group_order_member_id)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment