Skip to content

Commit

Permalink
feat (tax-integrations): add flow for syncing void/dispute lost invoi…
Browse files Browse the repository at this point in the history
…ces (#2414)

## Context

Currently Lago is implementing integration with tax provider Anrok

## Description

This PR adds logic for syncing voided invoice with tax provider
  • Loading branch information
lovrocolic authored Aug 19, 2024
1 parent d2a7250 commit 8a5b5c7
Show file tree
Hide file tree
Showing 17 changed files with 607 additions and 1 deletion.
25 changes: 25 additions & 0 deletions app/graphql/mutations/invoices/retry_tax_provider_voiding.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Mutations
module Invoices
class RetryTaxProviderVoiding < BaseMutation
include AuthenticableApiUser
include RequiredOrganization

REQUIRED_PERMISSION = 'invoices:update'

description 'Retry voided invoice sync'

argument :id, ID, required: true

type Types::Invoices::Object

def resolve(**args)
invoice = current_organization.invoices.visible.find_by(id: args[:id])
result = ::Invoices::ProviderTaxes::VoidService.call(invoice:)

result.success? ? result.invoice : result_error(result)
end
end
end
end
7 changes: 7 additions & 0 deletions app/graphql/types/invoices/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Object < Types::BaseObject

field :external_integration_id, String, null: true
field :integration_syncable, GraphQL::Types::Boolean, null: false
field :tax_provider_voidable, GraphQL::Types::Boolean, null: false

delegate :error_details, to: :object

Expand All @@ -68,6 +69,12 @@ def integration_syncable
object.integration_resources.where(resource_type: 'invoice', syncable_type: 'Invoice').none?
end

def tax_provider_voidable
return false unless object.voided?

object.error_details.tax_voiding_error.any?
end

def external_integration_id
integration_customer = object.customer&.integration_customers&.accounting_kind&.first

Expand Down
1 change: 1 addition & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class MutationType < Types::BaseObject
field :retry_all_invoices, mutation: Mutations::Invoices::RetryAll
field :retry_invoice, mutation: Mutations::Invoices::Retry
field :retry_invoice_payment, mutation: Mutations::Invoices::RetryPayment
field :retry_tax_provider_voiding, mutation: Mutations::Invoices::RetryTaxProviderVoiding
field :update_invoice, mutation: Mutations::Invoices::Update
field :void_invoice, mutation: Mutations::Invoices::Void

Expand Down
17 changes: 17 additions & 0 deletions app/jobs/invoices/provider_taxes/void_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Invoices
module ProviderTaxes
class VoidJob < ApplicationJob
queue_as 'integrations'

def perform(invoice:)
return unless invoice.customer.anrok_customer

# NOTE: We don't want to raise error here.
# If sync fails, invoice would be marked and retry option would be available in the UI
Invoices::ProviderTaxes::VoidService.call(invoice:)
end
end
end
end
2 changes: 1 addition & 1 deletion app/models/error_detail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ class ErrorDetail < ApplicationRecord
belongs_to :owner, polymorphic: true
belongs_to :organization

ERROR_CODES = %w[not_provided tax_error]
ERROR_CODES = %w[not_provided tax_error tax_voiding_error]
enum error_code: ERROR_CODES
end
1 change: 1 addition & 0 deletions app/services/invoices/lose_dispute_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def call
invoice.mark_as_dispute_lost!(payment_dispute_lost_at)

SendWebhookJob.perform_later('invoice.payment_dispute_lost', result.invoice, provider_error: reason)
Invoices::ProviderTaxes::VoidJob.perform_later(invoice:)

result
rescue ActiveRecord::RecordInvalid => _e
Expand Down
71 changes: 71 additions & 0 deletions app/services/invoices/provider_taxes/void_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

module Invoices
module ProviderTaxes
class VoidService < BaseService
def initialize(invoice:)
@invoice = invoice

super
end

def call
return result.not_found_failure!(resource: 'invoice') if invoice.blank?

invoice.error_details.tax_voiding_error.discard_all

tax_result = Integrations::Aggregator::Taxes::Invoices::VoidService.new(invoice:).call

if frozen_transaction?(tax_result)
negate_result = perform_invoice_negate

unless negate_result.success?
return result.validation_failure!(errors: {tax_error: [negate_result.error.code]})
end
elsif !tax_result.success?
create_error_detail(tax_result.error.code)

return result.validation_failure!(errors: {tax_error: [tax_result.error.code]})
end

result.invoice = invoice

result
end

private

attr_reader :invoice

delegate :customer, to: :invoice

def perform_invoice_negate
negate_result = Integrations::Aggregator::Taxes::Invoices::NegateService.new(invoice:).call

create_error_detail(negate_result.error.code) unless negate_result.success?

negate_result
end

def create_error_detail(code)
error_result = ErrorDetails::CreateService.call(
owner: invoice,
organization: invoice.organization,
params: {
error_code: :tax_voiding_error,
details: {
tax_voiding_error: code
}
}
)
error_result.raise_if_error!
end

# transactionFrozenForFiling error means that tax is already reported to the tax authority
# We should call negate action instead
def frozen_transaction?(tax_result)
!tax_result.success? && tax_result.error.code == 'transactionFrozenForFiling'
end
end
end
end
1 change: 1 addition & 0 deletions app/services/invoices/void_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def call
end

SendWebhookJob.perform_later('invoice.voided', result.invoice)
Invoices::ProviderTaxes::VoidJob.perform_later(invoice:)

result
rescue AASM::InvalidTransition => _e
Expand Down
23 changes: 23 additions & 0 deletions schema.graphql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 92 additions & 0 deletions schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions spec/factories/invoices.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
end
end

trait :with_tax_voiding_error do
after :create do |i|
create(:error_detail, owner: i, error_code: 'tax_voiding_error')
end
end

trait :failed do
status { :failed }
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"succeededInvoices": [],
"failedInvoices": [
{
"id": "invoice_id",
"validation_errors": {
"type": "transactionFrozenForFiling"
}
}
]
}
Loading

0 comments on commit 8a5b5c7

Please sign in to comment.