Skip to content

Commit

Permalink
feat (tax-integrations): Add anrok fetch services (#2247)
Browse files Browse the repository at this point in the history
## Context

Currently, integration with tax provider Anrok is being added to the
Lago app

## Description

This PR adds logic for feching tax data from Anrok through aggregator
called Nango.

There are two types of fetching data for invoices: tax data for
finalized invoices and tax data for draft invoices.

Those two types will be used for creating one-off invoices
  • Loading branch information
lovrocolic authored Jul 8, 2024
1 parent e79d638 commit eb7e4cd
Show file tree
Hide file tree
Showing 17 changed files with 1,244 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

module Mutations
module Integrations
module Anrok
class FetchDraftInvoiceTaxes < BaseMutation
include AuthenticableApiUser
include RequiredOrganization

REQUIRED_PERMISSION = 'invoices:create'

description 'Fetches taxes for one-off invoice'

input_object_class Types::Invoices::CreateInvoiceInput

type Types::Integrations::AnrokObjects::FeeObject.collection_type

def resolve(**args)
customer = Customer.find_by(
id: args[:customer_id],
organization_id: current_organization.id
)

result = ::Integrations::Aggregator::Taxes::Invoices::CreateDraftService.new(
invoice: invoice(customer, args),
fees: fees(args)
).call

result.success? ? result.fees : result_error(result)
end

private

# Note: We need to pass invoice object to the service that return taxes. This service should
# work with real invoice objects. In this case, it should also work with invoice that is not stored yet,
# because we need to fetch taxes for one-off invoice UI form.
def invoice(customer, args)
OpenStruct.new(
issuing_date: Time.current.in_time_zone(customer.applicable_timezone).to_date,
currency: args[:currency],
customer:
)
end

def fees(args)
args[:fees].map do |fee|
unit_amount_cents = fee[:unit_amount_cents]
units = fee[:units]&.to_f || 1

OpenStruct.new(
add_on_id: fee[:add_on_id],
item_id: fee[:add_on_id],
amount_cents: (unit_amount_cents * units).round
)
end
end
end
end
end
end
20 changes: 20 additions & 0 deletions app/graphql/types/integrations/anrok_objects/breakdown_object.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Types
module Integrations
module AnrokObjects
class BreakdownObject < Types::BaseObject
graphql_name 'AnrokBreakdownObject'

field :name, String, null: true
field :rate, GraphQL::Types::Float, null: true
field :tax_amount, GraphQL::Types::BigInt, null: true
field :type, String, null: true

def rate
BigDecimal(object.rate)
end
end
end
end
end
18 changes: 18 additions & 0 deletions app/graphql/types/integrations/anrok_objects/fee_object.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Types
module Integrations
module AnrokObjects
class FeeObject < Types::BaseObject
graphql_name 'AnrokFeeObject'

field :amount_cents, GraphQL::Types::BigInt, null: true
field :item_code, String, null: true
field :item_id, String, null: true
field :tax_amount_cents, GraphQL::Types::BigInt, null: true

field :tax_breakdown, [Types::Integrations::AnrokObjects::BreakdownObject]
end
end
end
end
2 changes: 2 additions & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ class MutationType < Types::BaseObject
field :create_anrok_integration, mutation: Mutations::Integrations::Anrok::Create
field :update_anrok_integration, mutation: Mutations::Integrations::Anrok::Update

field :fetch_draft_invoice_taxes, mutation: Mutations::Integrations::Anrok::FetchDraftInvoiceTaxes

field :create_xero_integration, mutation: Mutations::Integrations::Xero::Create
field :update_xero_integration, mutation: Mutations::Integrations::Xero::Update

Expand Down
2 changes: 1 addition & 1 deletion app/services/integrations/aggregator/base_payload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def billable_metric_item(fee)
def add_on_item(fee)
integration
.integration_mappings
.find_by(mappable_type: 'AddOn', mappable_id: fee.add_on.id) || fallback_item
.find_by(mappable_type: 'AddOn', mappable_id: fee.add_on_id) || fallback_item
end

def tax_item
Expand Down
4 changes: 4 additions & 0 deletions app/services/integrations/aggregator/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def provider
'netsuite'
when 'Integrations::XeroIntegration'
'xero'
when 'Integrations::AnrokIntegration'
'anrok'
end
end

Expand All @@ -38,6 +40,8 @@ def provider_key
'netsuite-tba'
when 'Integrations::XeroIntegration'
'xero'
when 'Integrations::AnrokIntegration'
'anrok'
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

module Integrations
module Aggregator
module Taxes
module Invoices
class BaseService < Integrations::Aggregator::BaseService
def initialize(invoice:, fees: nil)
@invoice = invoice
@fees = fees || invoice.fees

super(integration:)
end

private

attr_reader :invoice, :fees

delegate :customer, to: :invoice, allow_nil: true

def integration
return nil unless integration_customer

integration_customer&.integration
end

def integration_customer
@integration_customer ||=
customer
.integration_customers
.where(type: 'IntegrationCustomers::AnrokCustomer')
.first
end

def headers
{
'Connection-Id' => integration.connection_id,
'Authorization' => "Bearer #{secret_key}",
'Provider-Config-Key' => provider_key
}
end

def process_response(body)
fees = body['succeededInvoices']&.first.try(:[], 'fees')

if fees
result.fees = fees.map do |fee|
OpenStruct.new(
item_id: fee['item_id'],
item_code: fee['item_code'],
amount_cents: fee['amount_cents'],
tax_amount_cents: fee['tax_amount_cents'],
tax_breakdown: tax_breakdown(fee['tax_breakdown'])
)
end
else
code = body['failedInvoices'].first['validation_errors']['type']
message = 'Service failure'

result.service_failure!(code:, message:)
end
end

def tax_breakdown(breakdown)
breakdown.map do |b|
OpenStruct.new(
name: b['name'],
rate: b['rate'],
tax_amount: b['tax_amount'],
type: b['type']
)
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Integrations
module Aggregator
module Taxes
module Invoices
class CreateDraftService < BaseService
def action_path
"v1/#{provider}/draft_invoices"
end

def call
return result unless integration
return result unless integration.type == 'Integrations::AnrokIntegration'

response = http_client.post_with_response(payload, headers)
body = JSON.parse(response.body)

process_response(body)

result
rescue LagoHttpClient::HttpError => e
error = e.json_message
code = error['type']
message = error.dig('payload', 'message')

result.service_failure!(code:, message:)
end

private

def payload
Integrations::Aggregator::Taxes::Invoices::Payload.new(
integration:,
invoice:,
customer:,
fees:
).body
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module Integrations
module Aggregator
module Taxes
module Invoices
class CreateService < BaseService
def action_path
"v1/#{provider}/finalized_invoices"
end

def call
return result unless integration
return result unless integration.type == 'Integrations::AnrokIntegration'

response = http_client.post_with_response(payload, headers)
body = JSON.parse(response.body)

process_response(body)

result
rescue LagoHttpClient::HttpError => e
error = e.json_message
code = error['type']
message = error.dig('payload', 'message')

result.service_failure!(code:, message:)
end

private

def payload
payload_body = Integrations::Aggregator::Taxes::Invoices::Payload.new(
integration:,
invoice:,
customer:,
fees:
).body

invoice_data = payload_body.first
invoice_data['id'] = invoice.id

[invoice_data]
end
end
end
end
end
end
54 changes: 54 additions & 0 deletions app/services/integrations/aggregator/taxes/invoices/payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

module Integrations
module Aggregator
module Taxes
module Invoices
class Payload < BasePayload
def initialize(integration:, customer:, invoice:, fees: [])
super(integration:)

@customer = customer
@invoice = invoice
@fees = fees.is_a?(Array) ? fees : fees.order(created_at: :asc)
end

def body
[
{
'issuing_date' => invoice.issuing_date,
'currency' => invoice.currency,
'contact' => {
'external_id' => customer.external_id,
'name' => customer.name,
'address_line_1' => customer.shipping_address_line1,
'city' => customer.shipping_city,
'zip' => customer.shipping_zipcode,
'country' => customer.shipping_country,
'taxable' => customer.tax_identification_number.present?,
'tax_number' => customer.tax_identification_number
},
'fees' => fees.map { |fee| fee_item(fee) }
}
]
end

def fee_item(fee)
# TODO: Update later with other fee types
mapped_item = add_on_item(fee)

{
'item_id' => fee.item_id,
'item_code' => mapped_item.external_id,
'amount_cents' => fee.amount_cents
}
end

private

attr_reader :customer, :invoice, :fees
end
end
end
end
end
Loading

0 comments on commit eb7e4cd

Please sign in to comment.