-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat (tax-integrations): Add anrok fetch services (#2247)
## 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
1 parent
e79d638
commit eb7e4cd
Showing
17 changed files
with
1,244 additions
and
1 deletion.
There are no files selected for viewing
60 changes: 60 additions & 0 deletions
60
app/graphql/mutations/integrations/anrok/fetch_draft_invoice_taxes.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
20
app/graphql/types/integrations/anrok_objects/breakdown_object.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
18
app/graphql/types/integrations/anrok_objects/fee_object.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
app/services/integrations/aggregator/taxes/invoices/base_service.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
44 changes: 44 additions & 0 deletions
44
app/services/integrations/aggregator/taxes/invoices/create_draft_service.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
49 changes: 49 additions & 0 deletions
49
app/services/integrations/aggregator/taxes/invoices/create_service.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
54
app/services/integrations/aggregator/taxes/invoices/payload.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.