Last active
May 16, 2020 16:30
-
-
Save joegaudet/691764b5f80d0a5e1f164c22d5e34f23 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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