lib/invoice_test/api.rb in killbill-invoice-test-0.0.1 vs lib/invoice_test/api.rb in killbill-invoice-test-0.1.0
- old
+ new
@@ -1,14 +1,103 @@
module InvoiceTest
class InvoicePlugin < Killbill::Plugin::Invoice
- def get_additional_invoice_items(invoice, properties, context)
- additional_items = []
- invoice.invoice_items.each do |original_item|
- additional_items << build_item(original_item, original_item.amount * 7 / 100, 'Tax item', :TAX) unless original_item.amount == 0
+ # This implementation is idempotent, will apply tax on old invoices if needed
+ # and automatically handle adjustments
+ def get_additional_invoice_items(new_invoice, properties, context)
+ all_invoices = @kb_apis.invoice_user_api.get_invoices_by_account(new_invoice.account_id, context)
+ # Workaround for https://github.com/killbill/killbill/issues/265
+ all_invoices << new_invoice unless all_invoices.find { |inv| inv.id == new_invoice.id }
+
+ existing_taxable_items = []
+ existing_tax_items = {}
+ existing_adjustment_items = {}
+ all_invoices.each do |invoice|
+ invoice.invoice_items.each do |invoice_item|
+ existing_taxable_items << invoice_item if is_taxable_item(invoice_item)
+ (existing_tax_items[invoice_item.linked_item_id] ||= []) << invoice_item if is_tax_item(invoice_item)
+ (existing_adjustment_items[invoice_item.linked_item_id] ||= []) << invoice_item if is_adjustment_item(invoice_item)
+ end
end
+ compute_tax_for_items(new_invoice, existing_taxable_items, existing_tax_items, existing_adjustment_items)
+ end
+
+ private
+
+ def compute_tax_for_items(current_invoice, taxable_items, tax_items, adjustment_items)
+ additional_items = taxable_items.map { |taxable_item| compute_tax_for_item(current_invoice, taxable_item, tax_items[taxable_item.id] || [], adjustment_items) }
+ additional_items.compact!
+
+ # Add all new items on the latest invoice
+ additional_items.map { |ii| ii.invoice_id = current_invoice.id }
+
additional_items
+ end
+
+ def compute_tax_for_item(current_invoice, taxable_item, tax_items, adjustment_items)
+ current_tax_amount = tax_items.inject(0) { |sum, tax_item| sum + net_amount(tax_item, adjustment_items) }
+ expected_tax_amount = compute_tax_amount(net_amount(taxable_item, adjustment_items))
+
+ build_missing_item(current_invoice, current_tax_amount, expected_tax_amount, taxable_item, tax_items, adjustment_items)
+ end
+
+ def build_missing_item(current_invoice, current_tax_amount, expected_tax_amount, taxable_item, tax_items, adjustment_items)
+ if current_tax_amount < expected_tax_amount
+ # Add missing TAX item
+ build_tax_item(taxable_item, expected_tax_amount - current_tax_amount)
+ elsif current_tax_amount > expected_tax_amount
+ # Item adjust the TAX item
+ adjustment_amount = current_tax_amount - expected_tax_amount
+ tax_item_to_adjust = find_tax_item_to_adjust(tax_items, adjustment_items, adjustment_amount)
+ build_adjustment_item(current_invoice, tax_item_to_adjust, adjustment_amount)
+ else
+ # Nothing to do
+ nil
+ end
+ end
+
+ def build_tax_item(original_item, amount)
+ rounded_amount = amount.round(2)
+ return nil if rounded_amount == 0
+ build_item(original_item, rounded_amount, 'Tax item', :TAX)
+ end
+
+ def build_adjustment_item(current_invoice, item_to_adjust, amount)
+ rounded_amount = amount.round(2)
+ return nil if rounded_amount == 0
+ item = build_item(item_to_adjust, -rounded_amount, 'Tax item', :ITEM_ADJ)
+ item.start_date = current_invoice.invoice_date
+ item.end_date = nil
+ item
+ end
+
+ def compute_tax_amount(net_taxable_amount)
+ net_taxable_amount * 7.0 / 100
+ end
+
+ def find_tax_item_to_adjust(tax_items, adjustment_items, amount_to_adjust)
+ tax_items.find { |tax_item| net_amount(tax_item, adjustment_items) >= amount_to_adjust }
+ end
+
+ def net_amount(invoice_item, adjustment_items)
+ invoice_item.amount + sum(adjustment_items[invoice_item.id])
+ end
+
+ def sum(invoice_items)
+ (invoice_items || []).inject(0) { |sum, ii| sum + ii.amount }
+ end
+
+ def is_taxable_item(invoice_item)
+ invoice_item.amount > 0 and [:EXTERNAL_CHARGE, :FIXED, :RECURRING, :USAGE].include?(invoice_item.invoice_item_type)
+ end
+
+ def is_tax_item(invoice_item)
+ invoice_item.invoice_item_type == :TAX
+ end
+
+ def is_adjustment_item(invoice_item)
+ [:ITEM_ADJ, :REPAIR_ADJ].include?(invoice_item.invoice_item_type)
end
end
end