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 12, 2024
1 parent bec9cf6 commit 4bc53b0
Show file tree
Hide file tree
Showing 24 changed files with 592 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_item_count.zero? ? 0 : order.adjustments.eligible.credit.total / order.line_item_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
26 changes: 26 additions & 0 deletions app/models/vpago/adjustment_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Vpago
module AdjustmentDecorator
def self.prepended(base)
base.enum run_by: { unspecified: 0, store: 1, vendor: 2 }, _prefix: true

base.scope :promotion_run_by_vendor, -> { promotion.eligible.where(run_by: :vendor) }
base.scope :promotion_run_by_store, -> { promotion.eligible.where(run_by: :store) }

base.before_save :set_run_by

def base.total
sum(:amount)
end
end

private

def set_run_by
self.run_by = source.try(:run_by) if source.is_a?(::Spree::PromotionAction)
end
end
end

unless Spree::Adjustment.included_modules.include?(Vpago::AdjustmentDecorator)
Spree::Adjustment.prepend(Vpago::AdjustmentDecorator)
end
19 changes: 17 additions & 2 deletions app/models/vpago/line_item_decorator.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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
Expand All @@ -19,11 +21,24 @@ def commission_rate
end

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

def pre_commission_amount
pre_tax_amount - commission_amount
subtotal_with_vendor_promotion_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_promotion_total
subtotal + vendor_promotion_total
end

def vendor_promotion_total
@vendor_promotion_total ||= begin
order_adjustment = order.line_item_count.zero? ? 0 : order.adjustments.promotion_run_by_vendor.total / order.line_item_count
adjustments.promotion_run_by_vendor.total + order_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_item_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::PayoutProfileLineItemGenerator.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
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
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
5 changes: 5 additions & 0 deletions db/migrate/20240611085734_add_run_by_to_spree_adjustments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddRunByToSpreeAdjustments < ActiveRecord::Migration[7.0]
def change
add_column :spree_adjustments, :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 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
30 changes: 19 additions & 11 deletions lib/vpago/payway_v2/payouts_constructor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,29 @@ class PayoutsConstructor

def initialize(payment)
@payment = payment
@line_items = @payment.order.line_items.includes(:active_payway_payout_profiles)
@line_items = @payment.order.line_items.includes(payout_profile_line_items: :payout_profile)
end

def call
return [] unless @payment.order.allowed_payout?

payouts = build_payouts_for_line_items
payouts = include_default_payout_for_remaining_amounts(payouts)
payouts = group_payouts(payouts)

payouts
# when invalid, we still allow user to pay. we can deal with those issues in report later.
return [] unless valid_payout_total?(payouts)

format_payouts(payouts)
end

def valid_payout_total?(payouts)
payment.amount == payouts.sum { |payout| payout[:amt] }
end

# construct payouts for line item product that has payout profile.
def build_payouts_for_line_items
line_items.each_with_object([]) do |line_item, payouts|
payout_profile = line_item.active_payway_payout_profiles.first

if payout_profile.present?
payouts << { acc: payout_profile.bank_account_number, amt: line_item.pre_commission_amount }
line_item.payout_profile_line_items.each do |payout_profile_line_item|
payouts << { acc: payout_profile_line_item.payout_profile.bank_account_number, amt: payout_profile_line_item.amount }
end
end
end
Expand All @@ -53,13 +56,18 @@ def group_payouts(payouts)
payouts.group_by { |item| item[:acc] }.map do |acc, items|
{
acc: acc,
amt: format_amount(items.sum { |item| item[:amt] })
amt: items.sum { |item| item[:amt] }
}
end
end

def format_amount(amount)
"%.2f" % amount
def format_payouts(payouts)
payouts.map do |payout|
{
acc: payout[:acc],
amt: "%.2f" % payout[:amt]
}
end
end
end
end
Expand Down
6 changes: 6 additions & 0 deletions spec/factories/payout_profile_line_item_factory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FactoryBot.define do
factory :payout_profile_line_item, class: Spree::PayoutProfileLineItem do
payout_profile {|p| p.association(:payway_payout_profile) }
line_item {|p| p.association(:line_item) }
end
end
6 changes: 6 additions & 0 deletions spec/factories/payout_profile_payment_factory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FactoryBot.define do
factory :payout_profile_payment, class: Spree::PayoutProfilePayment do
payout_profile {|p| p.association(:payway_payout_profile) }
payment {|p| p.association(:payment) }
end
end
Loading

0 comments on commit 4bc53b0

Please sign in to comment.