Class: CreditMemo::SubmitToTaxjar

Inherits:
BaseService show all
Defined in:
app/services/credit_memo/submit_to_taxjar.rb

Overview

Service that creates / updates / deletes the TaxJar "refund"
transaction matching a CreditMemo. Mirrors Invoicing::SubmitToTaxjar
but on the negative side. Called via the Models::TaxjarSubmittable
state machine when a memo posts or is voided.

Defined Under Namespace

Classes: Result

Constant Summary collapse

MARKETPLACE_PROVIDERS =

Mapping from marketplace customer ids to TaxJar provider strings.
When a memo's customer matches, the transaction is tagged
provider: amazon|walmart and exemption_type: marketplace so
TaxJar treats it as marketplace-facilitated rather than direct.

{
  5_322_434 => 'amazon',
  23_041_827 => 'walmart'
}.freeze

Instance Attribute Summary

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseService

#log_debug, #log_error, #log_info, #log_warning, #logger, #process, #tagged_logger

Constructor Details

#initialize(credit_memo, options = {}) ⇒ SubmitToTaxjar

Returns a new instance of SubmitToTaxjar.



23
24
25
26
27
28
# File 'app/services/credit_memo/submit_to_taxjar.rb', line 23

def initialize(credit_memo, options = {})
  @credit_memo = credit_memo
  @options = options
  @logger = options[:logger] || Rails.logger
  @client = Api::Taxjar.client
end

Instance Method Details

#deleteResult

DELETEs the TaxJar refund transaction for this memo (used when a
memo is voided). Returns a Result with success / message.

Returns:



142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'app/services/credit_memo/submit_to_taxjar.rb', line 142

def delete
  @client.show_refund(taxjar_id)
  @client.delete_refund(taxjar_id)
  Result.new(
    success: true,
    message: "Record on Taxjar for credit memo #{taxjar_id} successfully deleted"
  )
rescue Api::Taxjar::Error::NotFound
  @logger.info "Record not found on Taxjar for credit memo #{taxjar_id}"
  Result.new(
    success: false,
    message: "Record not found on Taxjar for credit memo #{taxjar_id}"
  )
end

#detailsHash{Symbol => Object}

Builds the full TaxJar create_refund / update_refund payload —
negative-quantity / negative-price line items, destination address
parts, marketplace flags, and the inverted sales-tax total. Used
by both #submit and #update.

Returns:

  • (Hash{Symbol => Object})


163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'app/services/credit_memo/submit_to_taxjar.rb', line 163

def details
  line_items = []
  if @credit_memo.line_items.present?
    @credit_memo.line_items.parents_only.non_shipping.each do |li|
      next if li.taxable_total.zero?

      product_identifier = li.item.present? ? li.item.sku : li.cm_category
      unit_price = li.price.zero? ? ((li.taxable_total / li.quantity) * -1) : (li.price * -1)
      # unit_price = li.taxable_total
      description = if li.item.present?
                      (li.item.sku + ' - ' + li.item.name)
                    else
                      li.description.presence || li.cm_category
                    end
      line_items << {
                      quantity: (li.quantity * -1),
                      product_identifier: product_identifier,
                      product_tax_code: li.item&.product_tax_code&.product_tax_code,
                      description: description,
                      unit_price: unit_price,
                      discount: [li.line_discounts.sum(:amount), 0].max,
                      sales_tax: (li.tax_total * -1)
                    }
    end
  end

  # Map customer IDs to their marketplace providers
  marketplace_provider = MARKETPLACE_PROVIDERS[@credit_memo.customer_id]

  # Build base transaction details
  base_details = {
    customer_id: @credit_memo.taxjar_customer_id,
    transaction_id: taxjar_id,
    provider: marketplace_provider.presence || 'api',
    transaction_date: @credit_memo.gl_date.to_s,
    to_country: @credit_memo.shipping_address.country.iso,
    to_zip: @credit_memo.shipping_address.zip,
    to_state: @credit_memo.shipping_address.state_code,
    to_city: @credit_memo.shipping_address.city,
    to_street: @credit_memo.shipping_address.street1,
    amount: @credit_memo.line_items.to_a.sum(&:taxable_total).to_f,
    shipping: @credit_memo.shipping_cost.to_f == @credit_memo.line_items.shipping_only.to_a.sum(&:taxable_total).to_f ? @credit_memo.shipping_cost.to_f : @credit_memo.line_items.shipping_only.to_a.sum(&:taxable_total).to_f,
    sales_tax: (@credit_memo.tax_total * -1).to_f,
    line_items: line_items
  }

  # Add exemption_type for marketplace providers only
  base_details[:exemption_type] = 'marketplace' if marketplace_provider.present?

  base_details
end

#existing_record?Boolean

Whether TaxJar already has a refund transaction matching this
memo's id — first by listing the day's refunds, then by direct
show lookup as a fallback.

Returns:

  • (Boolean)


90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'app/services/credit_memo/submit_to_taxjar.rb', line 90

def existing_record?
  # check it already exists

  existing_records = @client.list_refunds({
    from_transaction_date: @credit_memo.gl_date.to_s,
    to_transaction_date: @credit_memo.gl_date.to_s
  })
  existing_records.include?(taxjar_id)
rescue Api::Taxjar::Error
  begin
    existing_record = @client.show_refund(taxjar_id)
    existing_record.present?
  rescue Api::Taxjar::Error::NotFound
    false
  end
end

#submitResult

POSTs a fresh refund transaction to TaxJar, returning a Result
carrying the original tax amount and TaxJar's calculated tax. If
a record already exists (#existing_record?) returns
success: false to avoid duplicates. TaxJar server errors are
returned in Result#message (not raised).

Returns:



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'app/services/credit_memo/submit_to_taxjar.rb', line 48

def submit
  if existing_record?
    @logger.info "Record already exists on Taxjar for credit memo #{taxjar_id}"
    Result.new(
      success: false,
      message: "Record already exists on Taxjar for credit memo #{taxjar_id}"
    )
  else
    new_record = @client.create_refund(details)
    Result.new(
      success: true,
      original_tax_amount: (@credit_memo.tax_total * -1).to_f,
      calculated_tax: new_record.sales_tax
    )
  end
rescue Api::Taxjar::Error::UnprocessableEntity => e
  # Handle "Provider tranx already imported" — refund exists via marketplace integration
  # (mirrors Invoicing::SubmitToTaxjar#submit, since memos for marketplace
  # customers are tagged provider: amazon|walmart and can collide the same way).
  if e.message.include?('already imported')
    Result.new(
      success: true,
      message: "Record already exists on Taxjar via marketplace provider for credit memo #{taxjar_id}"
    )
  else
    Result.new(
      success: false,
      message: e.message
    )
  end
rescue Api::Taxjar::Error::ServerError => e
  Result.new(
    success: false,
    message: e.message
  )
end

#taxjar_idString

The TaxJar transaction id we'll use for this credit memo —
the reference number, prefixed with the Rails env outside
production so staging/dev don't collide with prod transactions.

Returns:

  • (String)


35
36
37
38
39
# File 'app/services/credit_memo/submit_to_taxjar.rb', line 35

def taxjar_id
  str = @credit_memo.reference_number.clone
  str.prepend("#{Rails.env}-") if !Rails.env.production? && !str.start_with?("#{Rails.env}-")
  str
end

#updateResult

PUTs an updated refund transaction to TaxJar. Looks up the existing
record (passing provider: for marketplace-customer memos) and
then submits the new #details. Returns a Result mirroring
#submit.

Returns:



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'app/services/credit_memo/submit_to_taxjar.rb', line 113

def update
  marketplace_provider = MARKETPLACE_PROVIDERS[@credit_memo.customer_id]

  # Fetch existing record with provider parameter if applicable
  if marketplace_provider.present?
    @client.show_refund(taxjar_id, { provider: marketplace_provider })
  else
    @client.show_refund(taxjar_id)
  end

  updated_record = @client.update_refund(details)

  Result.new(
    success: true,
    original_tax_amount: (@credit_memo.tax_total * -1).to_f,
    calculated_tax: updated_record.sales_tax
  )
rescue Api::Taxjar::Error::NotFound
  @logger.info "Record not found on Taxjar for credit memo #{taxjar_id}"
  Result.new(
    success: false,
    message: "Record not found on Taxjar for credit memo #{taxjar_id}"
  )
end