Skip to content
39 changes: 27 additions & 12 deletions app/jobs/super_good/solidus_taxjar/replace_transaction_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,35 @@ class ReplaceTransactionJob < ApplicationJob
queue_as { SuperGood::SolidusTaxjar.job_queue }

def perform(order)
order_transaction = SuperGood::SolidusTaxjar.reporting.refund_and_create_new_transaction(order)
order.shipments.group_by(&:address).each do |address, shipments|
begin
latest_order_transactions = OrderTransaction.latest_for(order)

SuperGood::SolidusTaxjar::TransactionSyncLog.create!(
order: order,
order_transaction: order_transaction,
status: :success
)
latest_order_transactions.each do |transaction|
transaction_response = @api.create_refund_transaction_for(order)
transaction.create_refund_transaction!(
transaction_id: transaction_response.transaction_id,
transaction_date: transaction_response.transaction_date
)
end

rescue Taxjar::Error => exception
SuperGood::SolidusTaxjar::TransactionSyncLog.create!(
order: order,
status: :error,
error_message: exception.message
)
return if order.total.zero?

order_transaction = SuperGood::SolidusTaxjar.reporting.refund_and_create_new_transaction(order, address, shipments)

SuperGood::SolidusTaxjar::TransactionSyncLog.create!(
order: order,
order_transaction: order_transaction,
status: :success
)
rescue Taxjar::Error => exception
SuperGood::SolidusTaxjar::TransactionSyncLog.create!(
order: order,
status: :error,
error_message: exception.message
)
end
end
end
end
end
Expand Down
12 changes: 7 additions & 5 deletions app/jobs/super_good/solidus_taxjar/report_transaction_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ def perform(order, transaction_sync_batch = nil)
transaction_sync_batch: transaction_sync_batch,
order: order
)
begin
order_transaction = SuperGood::SolidusTaxjar.reporting.show_or_create_transaction(order)
transaction_sync_log.update!(order_transaction: order_transaction, status: :success)
rescue StandardError => exception
transaction_sync_log.update!(status: :error, error_message: exception.message)
order.shipments.group_by(&:address).each do |address, shipments|
begin
order_transaction = SuperGood::SolidusTaxjar.reporting.show_or_create_transaction(order, address, shipments)
transaction_sync_log.update!(order_transaction: order_transaction, status: :success)
rescue StandardError => exception
transaction_sync_log.update!(status: :error, error_message: exception.message)
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/models/super_good/solidus_taxjar/order_transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class OrderTransaction < ActiveRecord::Base
validates_presence_of :transaction_date

def self.latest_for(order)
where(order: order).order(transaction_date: :desc, created_at: :desc).limit(1).first
where(order: order).where.missing(:refund_transaction).order(transaction_date: :desc, created_at: :desc)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ class SuperGood::SolidusTaxjar::TransactionSyncLog < ApplicationRecord
belongs_to :order_transaction, optional: true
delegate :refund_transaction, to: :order_transaction, :allow_nil => true

enum status: [:processing, :success, :error]
enum :status, [:processing, :success, :error]
end
5 changes: 3 additions & 2 deletions lib/super_good/solidus_taxjar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
require "super_good/solidus_taxjar/tax_calculator"
require "super_good/solidus_taxjar/tax_rate_calculator"
require "super_good/solidus_taxjar/discount_calculator"
require "super_good/solidus_taxjar/proportional_discount_calculator"
require "super_good/solidus_taxjar/addresses"
require "super_good/solidus_taxjar/reporting"
require "super_good/solidus_taxjar/reportable"
Expand Down Expand Up @@ -79,8 +80,8 @@ def logger

self.reportable_order_check = ->(order) { true }

self.shipping_calculator = ->(order) { order.shipments.sum(&:total_before_tax) }
self.shipping_tax_label_maker = ->(shipment, shipping_tax) { "Sales Tax" }
self.shipping_calculator = ->(shipments) { shipments.sum(&:cost) }
self.shipping_tax_label_maker = ->(taxjar_shipment, shipment) { "Sales Tax" }
self.tax_exemption_mailer_from_address = "admin@example.com"
self.tax_exemption_mailer_to_address = "admin@example.com"
self.taxable_address_check = ->(address) { true }
Expand Down
14 changes: 7 additions & 7 deletions lib/super_good/solidus_taxjar/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ def tax_categories
taxjar_client.categories
end

def tax_for(order)
taxjar_client.tax_for_order(ApiParams.order_params(order))
def tax_for(order, address, shipments)
taxjar_client.tax_for_order(ApiParams.order_params(order, address, shipments))
end

def tax_rate_for(address)
Expand All @@ -33,22 +33,22 @@ def tax_rates_for(address)
taxjar_client.rates_for_location(*ApiParams.address_params(address))
end

def create_transaction_for(order)
def create_transaction_for(order, address, shipments)
latest_transaction_id =
OrderTransaction.latest_for(order)&.transaction_id
OrderTransaction.latest_for(order)&.first&.transaction_id

transaction_id = TransactionIdGenerator.next_transaction_id(
order: order,
current_transaction_id: latest_transaction_id
)

taxjar_client.create_order(
ApiParams.transaction_params(order, transaction_id)
ApiParams.transaction_params(order, address, shipments, transaction_id)
)
end

def update_transaction_for(order)
taxjar_client.update_order ApiParams.transaction_params(order)
def update_transaction_for(order, address, shipments)
taxjar_client.update_order ApiParams.transaction_params(order, address, shipments)
end

def delete_transaction_for(order)
Expand Down
183 changes: 115 additions & 68 deletions lib/super_good/solidus_taxjar/api_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,15 @@ module ApiParams
UNTAXABLE_INVENTORY_UNIT_STATES = ["returned", "canceled"]

class << self
def order_params(order)
def order_params(order, address, shipments)
{}
.merge(customer_id(order))
.merge(order_address_params(order.tax_address))
.merge(line_items_params(order.line_items))
.merge(shipping: shipping(order))
.merge(order_address_params(address))
.merge(line_items_params(shipments.map(&:inventory_units).flatten.compact))
.merge(shipping: shipping(shipments))
.merge(SuperGood::SolidusTaxjar.custom_order_params.call(order))
end

#ADNAN: temp fix until we have a solution
def reorder_params(order)
order_params(order)
end

def address_params(address)
[
address.zipcode,
Expand All @@ -37,19 +32,27 @@ def tax_rate_address_params(address)
}.merge(order_address_params(address))
end

def transaction_params(order, transaction_id = order.number)
def transaction_params(order, address, shipments, transaction_id = order.number)
{}.merge(customer_id(order))
.merge(order_address_params(address))
.merge(line_items_params(shipments.map(&:inventory_units).flatten.compact))
.merge(shipping: shipping(shipments))
.merge(SuperGood::SolidusTaxjar.custom_order_params.call(order))

# Calculate discount adjustments for proper rounding across all shipments
calculator = ProportionalDiscountCalculator.new(order)
discount_adjustments = calculator.calculate

{}
.merge(customer_id(order))
.merge(order_address_params(order.tax_address))
.merge(transaction_line_items_params(order.line_items))
.merge(order_address_params(address))
.merge(transaction_line_items_params(address, shipments.map(&:inventory_units).flatten.compact, discount_adjustments))
.merge(
transaction_id: transaction_id,
transaction_date: order.completed_at.to_formatted_s(:iso8601),
# We use `payment_total` to reflect the total liablity
# transferred.
amount: [order.payments.completed.sum(&:amount) - refund_total_without_tax(order) - order.additional_tax_total, 0].max,
shipping: shipping(order),
sales_tax: sales_tax(order)
amount: order_total_for_shipments(address, shipments, discount_adjustments) - reimbursement_total_without_tax(shipments),
shipping: shipping(shipments),
sales_tax: sales_tax(order, address, shipments)
)
end

Expand Down Expand Up @@ -149,20 +152,24 @@ def order_address_params(address)
# @param line_items [Spree::LineItem::ActiveRecord_Relation] All of the
# order's line items.
# @return [Hash] A TaxJar API-friendly line item collection.
def line_items_params(line_items)
{
line_items: line_items.filter_map { |line_item|
next unless line_item.quantity.positive?
def line_items_params(_inventory_units)
grouped_inventory_units = _inventory_units.group_by(&:line_item)

{
id: line_item.id,
quantity: 1,
unit_price: line_item.total,
discount: discount(line_item),
product_tax_code: line_item.tax_category&.tax_code
}
line_items = grouped_inventory_units.filter_map { |line_item, inventory_units|
quantity = inventory_units.sum(&:quantity)

next unless quantity.positive?

{
id: line_item.id,
quantity:,
unit_price: line_item.total / line_item.quantity,
discount: discount(line_item) * (quantity / line_item.quantity.to_f),
product_tax_code: line_item.tax_category&.tax_code
}
}

{ line_items: }
end

# @private
Expand All @@ -173,79 +180,119 @@ def line_items_params(line_items)
#
# @param line_items [Spree::LineItem::ActiveRecord_Relation] All of the
# order's line items.
# @param discount_adjustments [Hash] Pre-calculated discount amounts per line_item per address
# @return [Hash] A TaxJar API-friendly line item collection.
def transaction_line_items_params(line_items)
{
line_items: line_items.filter_map { |line_item|
quantity = taxable_quantity line_item
next unless quantity.positive?
def transaction_line_items_params(address, _inventory_units, discount_adjustments = {})
grouped_inventory_units = _inventory_units.group_by(&:line_item)

{
id: line_item.id,
quantity: quantity,
product_identifier: line_item.sku,
description: line_item.variant.descriptive_name,
product_tax_code: line_item.tax_category&.tax_code,
unit_price: SuperGood::SolidusTaxjar.line_item_unit_price_calculator.call(line_item),
discount: discount(line_item),
sales_tax: line_item_sales_tax(line_item)
}
line_items = grouped_inventory_units.filter_map { |line_item, inventory_units|
quantity = taxable_quantity(inventory_units)

next unless quantity.positive?

# Use pre-calculated discount adjustment if available, otherwise calculate proportionally
discount_amount = if discount_adjustments.dig(line_item.id, address.id)
discount_adjustments[line_item.id][address.id]
else
proportional_discount_calculator.proportional_discount(line_item, quantity)
end

{
id: line_item.id,
quantity:,
product_identifier: line_item.sku,
unit_price: line_item.total / line_item.quantity,
discount: discount_amount,
product_tax_code: line_item.tax_category&.tax_code,
description: line_item.variant.descriptive_name,
sales_tax: line_item_sales_tax(line_item, address, inventory_units)
}
}

{ line_items: }
end

def discount(line_item)
::SuperGood::SolidusTaxjar.discount_calculator.new(line_item).discount
end

def shipping(order)
SuperGood::SolidusTaxjar.shipping_calculator.call(order)
def proportional_discount_calculator
@proportional_discount_calculator ||= ProportionalDiscountCalculator.new(nil)
end

def sales_tax(order)
def shipping(shipments)
SuperGood::SolidusTaxjar.shipping_calculator.call(shipments)
end

def sales_tax(order, address, shipments)
return 0 if order.total.zero?

order.additional_tax_total - order_reimbursement_tax_total(order)
tax_total = order.all_adjustments.tax.
select { |adjustment| adjustment.label.include?(address.address1) }.sum(&:amount)

round_to_two_places(tax_total - reimbursement_tax_total(shipments))
end

def line_item_sales_tax(line_item)
def line_item_sales_tax(line_item, address, inventory_units)
return 0 if line_item.order.total.zero?

line_item.additional_tax_total - line_item_reimbursement_tax_total(line_item)
tax_total = line_item.adjustments.tax.
select { |adjustment| adjustment.label.include?(address.address1) }.sum(&:amount)

round_to_two_places(tax_total - line_item_reimbursement_tax_total(inventory_units))
end

def taxable_quantity(line_item)
line_item.inventory_units
.where.not(state: UNTAXABLE_INVENTORY_UNIT_STATES)
.count
def round_to_two_places(amount)
BigDecimal(amount.to_s).round(2, BigDecimal::ROUND_HALF_UP)
end

def line_item_reimbursement_tax_total(line_item)
line_item
.inventory_units
def taxable_inventory(inventory_units)
inventory_units.reject {|i| UNTAXABLE_INVENTORY_UNIT_STATES.include?(i.state)}
end

def taxable_quantity(inventory_units)
taxable_inventory(inventory_units).sum(&:quantity)
end

def line_item_reimbursement_tax_total(inventory_units)
inventory_units
.flat_map(&:return_items)
.filter { |return_item| return_item.reimbursement.present? }
.sum(&:additional_tax_total)
end

def order_reimbursement_tax_total(order)
order.reimbursements.sum { |reimbursement| reimbursement_tax_total(reimbursement) }
def reimbursement_tax_total(shipments)
inventory_units = shipments.map(&:inventory_units).flatten.compact
inventory_units.flat_map(&:return_items)
.filter { |return_item| return_item.reimbursement.present? }
.sum(&:additional_tax_total)
end

def reimbursement_tax_total(reimbursement)
reimbursement.return_items.sum(&:additional_tax_total)
def reimbursement_total_without_tax(shipments)
inventory_units = shipments.map(&:inventory_units).flatten.compact
inventory_units.flat_map(&:return_items)
.filter { |return_item| return_item.reimbursement.present? }
.sum(&:amount)
end

def refund_total_without_tax(order)
order.refunds.sum do |refund|
if refund.reimbursement.present?
refund.reimbursement.total - reimbursement_tax_total(refund.reimbursement)
def order_total_for_shipments(address, shipments, discount_adjustments = {})
grouped_inventory_units = shipments.map(&:inventory_units).flatten.compact.group_by(&:line_item)

line_items_total = grouped_inventory_units.filter_map { |line_item, inventory_units|
quantity = inventory_units.sum(&:quantity)
next unless quantity.positive?

# Use pre-calculated discount adjustment if available
if discount_adjustments.dig(line_item.id, address.id)
discount_amount = discount_adjustments[line_item.id][address.id]
line_item.total * (quantity / line_item.quantity.to_f) - discount_amount.abs
else
# This use case represents making a line item level adjustment, and then refunding
# that amount.
refund.amount
# Use the original calculation method to maintain backward compatibility
(line_item.total - discount(line_item)) * (quantity / line_item.quantity.to_f)
end
end
}.sum

line_items_total + shipping(shipments)
end
end
end
Expand Down
Loading