Skip to content

Commit

Permalink
close #166 support promotion, adjustments, tax in payouts
Browse files Browse the repository at this point in the history
  • Loading branch information
theachoem committed Jun 13, 2024
1 parent bec9cf6 commit c57f58a
Show file tree
Hide file tree
Showing 33 changed files with 769 additions and 192 deletions.
1 change: 1 addition & 0 deletions app/models/spree/payout_profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class PayoutProfile < Base

has_many :payout_profile_payments, class_name: 'Spree::PayoutProfilePayment', inverse_of: :payout_profile
has_many :payout_profile_products, class_name: 'Spree::PayoutProfileProduct', inverse_of: :payout_profile
has_many :payout_profile_line_items, class_name: 'Spree::PayoutProfileLineItem', inverse_of: :payout_profile
has_many :products, class_name: "Spree::Product", through: :payout_profile_products

belongs_to :vendor, class_name: 'Spree::Vendor', optional: true, inverse_of: :payout_profiles
Expand Down
41 changes: 41 additions & 0 deletions app/models/spree/payout_profile_line_item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# This model is used to associate line items with payout profiles, even though we may not know in advance
# if user select payment method that supports payouts. The benefit is that when a user selects and pays with
# a payment method that does support payouts, we can use this association to tell the bank which payout profile to send to.
# It also benefits financial reporting.
module Spree
class PayoutProfileLineItem < Base
belongs_to :payout_profile, class_name: 'Spree::PayoutProfile', required: true, inverse_of: :payout_profile_line_items
belongs_to :line_item, class_name: 'Spree::LineItem', required: true, inverse_of: :payout_profile_line_items

has_one :order, through: :line_item

validates :payout_profile_id, uniqueness: { scope: :line_item_id }

extend DisplayMoney
money_methods :amount, :commission_amount, :pre_commission_amount, :outstanding_amount

before_save :set_amounts

def available_amount_can_be_payout_to_vendor
order_adjustment = order.line_items_count.zero? ? 0 : order.adjustments.eligible.credit.total / order.line_items_count
available_amount = line_item.subtotal + line_item.adjustments.eligible.credit.total + order_adjustment
expected_amount_by_vendor = line_item.pre_commission_amount

# avoid more than expected.
[available_amount, expected_amount_by_vendor].min
end

private

def set_amounts
self.amount = available_amount_can_be_payout_to_vendor

self.commission_rate = line_item.commission_rate
self.commission_amount = line_item.commission_amount
self.pre_commission_amount = line_item.pre_commission_amount

# amount that should be later sent to vendor
self.outstanding_amount = self.pre_commission_amount - self.amount
end
end
end
1 change: 1 addition & 0 deletions app/models/spree/payout_profile_payment.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# This class only create when payment is completed. So we can trust that if this exist, money has already been sent to payout profile.
module Spree
class PayoutProfilePayment < Base
belongs_to :payout_profile, class_name: 'Spree::PayoutProfile', required: true, inverse_of: :payout_profile_payments
Expand Down
30 changes: 30 additions & 0 deletions app/models/vpago/adjustment_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module Vpago
module AdjustmentDecorator
def self.prepended(base)
base.enum handle_by: { unspecified: 0, store: 1, vendor: 2 }, _prefix: true

base.scope :handle_by_vendor, -> { eligible.where(handle_by: :vendor) }
base.scope :handle_by_store, -> { eligible.where(handle_by: :store) }

base.before_save :set_handle_by

def base.total
sum(:amount) || 0
end
end

private

def set_handle_by
if source.is_a?(::Spree::PromotionAction)
self.handle_by = source.try(:run_by)
elsif source.is_a?(::Spree::TaxRate)
self.handle_by = source.tax_category.try(:collect_by)
end
end
end
end

unless Spree::Adjustment.included_modules.include?(Vpago::AdjustmentDecorator)
Spree::Adjustment.prepend(Vpago::AdjustmentDecorator)
end
11 changes: 11 additions & 0 deletions app/models/vpago/inventory_unit_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Vpago
module InventoryUnitDecorator
def self.prepended(base)
base.has_one :selected_shipping_rate, through: :shipment, class_name: 'Spree::ShippingRate'
end
end
end

unless Spree::InventoryUnit.included_modules.include?(Vpago::InventoryUnitDecorator)
Spree::InventoryUnit.prepend(Vpago::InventoryUnitDecorator)
end
27 changes: 25 additions & 2 deletions app/models/vpago/line_item_decorator.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
module Vpago
module LineItemDecorator
def self.prepended(base)
base.has_many :payout_profile_line_items, class_name: 'Spree::PayoutProfileLineItem', inverse_of: :line_item

base.has_many :payout_profiles, class_name: 'Spree::PayoutProfile', through: :product
base.has_many :active_payout_profiles, class_name: 'Spree::PayoutProfile', through: :product
base.has_many :active_payway_payout_profiles, class_name: 'Spree::PayoutProfile', through: :product

base.has_many :required_active_payout_profiles, class_name: 'Spree::PayoutProfile', through: :product

base.has_many :shipments, class_name: 'Spree::Shipment', through: :inventory_units
base.has_many :selected_shipping_rates, class_name: 'Spree::ShippingRate', through: :inventory_units
end

# considred required when there are any required profiles.
Expand All @@ -19,11 +24,29 @@ def commission_rate
end

def commission_amount
pre_tax_amount * commission_rate / 100.0
subtotal_with_vendor_adjustment_total * commission_rate / 100.0
end

def pre_commission_amount
pre_tax_amount - commission_amount
subtotal_with_vendor_adjustment_total - commission_amount
end

# using subtotal instead of pre_tax_amount since pre_tax_amount already include adjustments amount in it.
# we want raw amount to add only vendor adjustment amount.
def subtotal_with_vendor_adjustment_total
subtotal + vendor_adjustment_total
end

# tax, shipment, promotion, etc. are considered adjustment.
def vendor_adjustment_total
@vendor_adjustment_total ||= begin
order_adjustment = order.line_items_count.zero? ? 0 : order.adjustments.handle_by_vendor.total / order.line_items_count

shipment_cost = selected_shipping_rates.handle_by_vendor.sum(:cost) || 0
shipment_adjustment = shipments.map { |shipment| shipment.adjustments.handle_by_vendor.total }.sum || 0

adjustments.handle_by_vendor.total + order_adjustment + shipment_cost + shipment_adjustment
end
end
end
end
Expand Down
19 changes: 9 additions & 10 deletions app/models/vpago/order_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ module OrderDecorator
extend Spree::DisplayMoney
money_methods :order_adjustment_total, :shipping_discount

def self.prepended(base)
base.has_many :payout_profile_payments, class_name: 'Spree::PayoutProfilePayment', through: :payments
base.has_many :payout_profile_line_items, class_name: 'Spree::PayoutProfileLineItem', through: :line_items
end

# Make sure the order confirmation is delivered when the order has been paid for.
def finalize!
# lock all adjustments (coupon promotions, etc.)
Expand All @@ -29,18 +34,12 @@ def finalize!
consider_risk
end

# currently does not support payout when having adjustment/promotion or having tax.
def allowed_payout?
return false if adjustment_total > 0
return false if included_tax_total > 0
return false if additional_tax_total > 0
return false if promo_total > 0

true
def required_payway_payout?
line_items.any?(&:required_payway_payout?)
end

def required_payway_payout?
allowed_payout? && line_items.any?(&:required_payway_payout?)
def line_items_count
line_items.size
end

# override
Expand Down
2 changes: 2 additions & 0 deletions app/models/vpago/payment_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module Vpago
module PaymentDecorator
def self.prepended(base)
base.has_many :payout_profile_payments, class_name: 'Spree::PayoutProfilePayment', inverse_of: :payment

base.after_create -> { Vpago::PayoutProfileLineItemsGenerator.new(order).call }
end

# On the first call, everything works. The order is transitioned to complete and one Spree::Payment,
Expand Down
29 changes: 29 additions & 0 deletions app/models/vpago/payout_profile_line_item_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Vpago
class PayoutProfileLineItemGenerator
attr_reader :order

def initialize(order)
@order = order
end

def call
return if order.payout_profile_payments.any?

prepare_payout_profile_line_items
end

# Associate line items with payout profiles, even though we may not know in advance
# if user select payment method that supports payouts. The benefit is that when a user selects and pays with
# a payment method that does support payouts, we can use this association to tell the bank which payout profile to send to.
# It also benefits financial reporting.
def prepare_payout_profile_line_items
line_items = order.line_items.includes(:active_payway_payout_profiles)
Spree::PayoutProfileLineItem.where(line_item_id: line_items.pluck(:id)).delete_all

line_items.each do |line_item|
payout_profile = line_item.active_payway_payout_profiles.first || Spree::PayoutProfiles::PaywayV2.default
line_item.payout_profile_line_items.create(payout_profile: payout_profile)
end
end
end
end
29 changes: 29 additions & 0 deletions app/models/vpago/payout_profile_line_items_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Vpago
class PayoutProfileLineItemsGenerator
attr_reader :order

def initialize(order)
@order = order
end

def call
return if order.payout_profile_payments.any?

prepare_payout_profile_line_items
end

# Associate line items with payout profiles, even though we may not know in advance
# if user select payment method that supports payouts. The benefit is that when a user selects and pays with
# a payment method that does support payouts, we can use this association to tell the bank which payout profile to send to.
# It also benefits financial reporting.
def prepare_payout_profile_line_items
line_items = order.line_items.includes(:active_payway_payout_profiles)
Spree::PayoutProfileLineItem.where(line_item_id: line_items.pluck(:id)).delete_all

line_items.each do |line_item|
payout_profile = line_item.active_payway_payout_profiles.first || Spree::PayoutProfiles::PaywayV2.default
line_item.payout_profile_line_items.create(payout_profile: payout_profile)
end
end
end
end
11 changes: 11 additions & 0 deletions app/models/vpago/promotion_action_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Vpago
module PromotionActionDecorator
def self.prepended(base)
base.enum run_by: { unspecified: 0, store: 1, vendor: 2 }, _prefix: true
end
end
end

unless Spree::PromotionAction.included_modules.include?(Vpago::PromotionActionDecorator)
Spree::PromotionAction.prepend(Vpago::PromotionActionDecorator)
end
11 changes: 11 additions & 0 deletions app/models/vpago/shipping_method_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Vpago
module ShippingMethodDecorator
def self.prepended(base)
base.enum handle_by: { unspecified: 0, store: 1, vendor: 2 }, _prefix: true
end
end
end

unless Spree::ShippingMethod.included_modules.include?(Vpago::ShippingMethodDecorator)
Spree::ShippingMethod.prepend(Vpago::ShippingMethodDecorator)
end
22 changes: 22 additions & 0 deletions app/models/vpago/shipping_rate_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module Vpago
module ShippingRateDecorator
def self.prepended(base)
base.enum handle_by: { unspecified: 0, store: 1, vendor: 2 }, _prefix: true

base.scope :handle_by_vendor, -> { where(handle_by: :vendor) }
base.scope :handle_by_store, -> { where(handle_by: :store) }

base.before_save :set_handle_by
end

private

def set_handle_by
self.handle_by = shipping_method.try(:handle_by)
end
end
end

unless Spree::ShippingRate.included_modules.include?(Vpago::ShippingRateDecorator)
Spree::ShippingRate.prepend(Vpago::ShippingRateDecorator)
end
11 changes: 11 additions & 0 deletions app/models/vpago/tax_category_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Vpago
module TaxCategoryDecorator
def self.prepended(base)
base.enum collect_by: { unspecified: 0, store: 1, vendor: 2 }, _prefix: true
end
end
end

unless Spree::TaxCategory.included_modules.include?(Vpago::TaxCategoryDecorator)
Spree::TaxCategory.prepend(Vpago::TaxCategoryDecorator)
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Payway
class PayoutProfileRequestCreator < BasePayoutProfileRequest
# override
def request_path
"api/merchant-portal/merchant-access/whitelist-account/add-whitelist-payout"
"/api/merchant-portal/merchant-access/whitelist-account/add-whitelist-payout"
end

# code:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Payway
class PayoutProfileRequestUpdater < BasePayoutProfileRequest
# override
def request_path
"api/merchant-portal/merchant-access/whitelist-account/update-whitelist-status"
"/api/merchant-portal/merchant-access/whitelist-account/update-whitelist-status"
end

def call
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,9 @@
post :verify_with_bank
end
end

resources :orders do
resources :payout_profile_line_items, only: [:index]
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class CreateSpreePayoutProfileLineItems < ActiveRecord::Migration[7.0]
def change
create_table :spree_payout_profile_line_items, if_not_exists: true do |t|
t.references :payout_profile, foreign_key: { to_table: :spree_payout_profiles }
t.references :line_item, foreign_key: { to_table: :spree_line_items }

t.decimal :amount, precision: 10, scale: 2, default: "0.0", null: false
t.decimal :commission_rate, null: false
t.decimal :commission_amount, precision: 10, scale: 2, default: "0.0", null: false
t.decimal :pre_commission_amount, precision: 10, scale: 2, default: "0.0", null: false
t.decimal :outstanding_amount, precision: 10, scale: 2, default: "0.0", null: false

t.timestamps
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddHandleByToSpreeAdjustments < ActiveRecord::Migration[7.0]
def change
add_column :spree_adjustments, :handle_by, :integer, null: false, default: 0, if_not_exists: true
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddRunByToSpreePromotionActions < ActiveRecord::Migration[7.0]
def change
add_column :spree_promotion_actions, :run_by, :integer, null: false, default: 0, if_not_exists: true
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddCollectByToSpreeTaxCategories < ActiveRecord::Migration[7.0]
def change
add_column :spree_tax_categories, :collect_by, :integer, null: false, default: 0, if_not_exists: true
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddHandleByToSpreeShippingMethod < ActiveRecord::Migration[7.0]
def change
add_column :spree_shipping_methods, :handle_by, :integer, null: false, default: 0, if_not_exists: true
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddHandleByToSpreeShippingRates < ActiveRecord::Migration[7.0]
def change
add_column :spree_shipping_rates, :handle_by, :integer, null: false, default: 0, if_not_exists: true
end
end
Loading

0 comments on commit c57f58a

Please sign in to comment.