Class: Edi::Commercehub::RemitMessageProcessor

Inherits:
BaseEdiService show all
Defined in:
app/services/edi/commercehub/remit_message_processor.rb

Defined Under Namespace

Classes: BatchProcessResult, Result

Constant Summary collapse

PAYMENT_METHOD_MAP =
{
  ACH: 'ACH'
}

Constants included from AddressAbbreviator

AddressAbbreviator::MAX_LENGTH

Instance Attribute Summary

Attributes inherited from BaseEdiService

#orchestrator

Instance Method Summary collapse

Methods inherited from BaseEdiService

#duplicate_po_already_notified?, #initialize, #mark_duplicate_po_as_notified, #report_order_creation_issues, #safe_process_edi_communication_log

Methods included from AddressAbbreviator

#abbreviate_street, #collect_street_originals, #record_address_abbreviation_notes

Methods inherited from BaseService

#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #options, #tagged_logger

Constructor Details

This class inherits a constructor from Edi::BaseEdiService

Instance Method Details

#customer(segment = nil) ⇒ Object



16
17
18
# File 'app/services/edi/commercehub/remit_message_processor.rb', line 16

def customer(segment = nil)
  orchestrator.customer(segment)
end

#process(edi_logs = nil) ⇒ Object

Picks up the edi communication log in the queue ready to process



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'app/services/edi/commercehub/remit_message_processor.rb', line 21

def process(edi_logs = nil)
  edi_logs ||= EdiCommunicationLog.requiring_processing.where(partner: orchestrator.partner, category: 'remittance_advice')
  edi_logs = [edi_logs].flatten # This way you can pass a single edi communication log
  # Wrap in one big transaction
  batch_process_results = []
  edi_logs.each do |edi_log|
    EdiCommunicationLog.transaction do
      if orchestrator.remit_message_processor_enabled?
        # Process the receipts in the batch
        batch_process_result = process_receipts(edi_log.data)
        # Record some meta data in file info
        edi_log.file_info ||= {}
        edi_log.file_info[:receipts_in_batch] = batch_process_result.receipts_in_batch
        # Mark our edi log as processed, we need some error handling at one point
        # Associate the created receipts
        batch_process_result.receipts_created.each do |receipt|
          edi_log.edi_documents.create(receipt: receipt)
        end
        batch_process_results << batch_process_result
      end
      edi_log.complete!
    end # End transaction
  rescue StandardError => e
    edi_log.notes = "#{e} at #{e&.backtrace_locations&.first}"
    edi_log.error
    ErrorReporting.error(e)
    # End transaction
  end # End edi log iteration
  Result.new(batch_process_results: batch_process_results)
end

#process_receipt(remittance_advice_message_xml, batch_number) ⇒ Object

Process a single receipt



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'app/services/edi/commercehub/remit_message_processor.rb', line 68

def process_receipt(remittance_advice_message_xml, batch_number)
  cust = orchestrator.partner == :commercehub_lowes_ca ? customer('lowes') : customer
  edi_transaction_id = remittance_advice_message_xml.xpath('hubMessageId').text # unique EDI transaction ID for this receipt
  category = PAYMENT_METHOD_MAP[remittance_advice_message_xml.xpath('paymentMethod').text] || 'ACH' # this is always ACH based on looking at all the existing EDI data, without any exceptions
  amount = remittance_advice_message_xml.xpath('balanceDue').text.to_d
  payment_date = remittance_advice_message_xml.xpath('paymentDate').text
  reference = "EDI #{category} #{cust.name} #{payment_date}" # following existing conventions of for e.g. 'WIRE HOME DEPOT 101921' but adding 'EDI' and using category (which will likely be 'ACH' and is more accurate than 'WIRE')
  currency = cust.store.currency
  company_id = cust.store.company_id
   = cust.store.company..id
  remark = "Payment # #{batch_number}" # also following existing conventions of for e.g. 'Payment # 16059629173' where that number is in fact the batch number
  receipt = cust.receipts.build(
    edi_transaction_id: edi_transaction_id,
    category: category,
    amount: amount,
    reference: reference,
    currency: currency,
    company_id: company_id,
    bank_account_id: ,
    remark: remark,
    gl_date: payment_date.to_date,
    receipt_date: payment_date.to_date
  )
  should_hold_for_review = false # this is set to false so the receipy will fully apply if everything is simply invoice remittances, we will set it to true i.e. hold the receipt in 'partially applied' if there are any negative remittances to manually research
  invoice_discounts_to_apply = 0.0
  remittance_advice_message_xml.xpath('//remittanceAdviceItem').each do |remittance_advice_item_xml|
    invoice_id = nil
    detail_category = nil
    invoice_ref = remittance_advice_item_xml.xpath('refInvoiceNumber').text
    invoice_ref = 'IN' + invoice_ref if orchestrator.partner == :commercehub_thd_us && invoice_ref.present? && invoice_ref[0] == ('V')
    po_number = orchestrator.partner == :commercehub_thd_us ? remittance_advice_item_xml.xpath('refOrderNumber').text : nil
    business_unit_id = nil
     = nil
    ledger_detail_project_id = nil
    remark = nil
    if (invoice_ref.present? && invoice = Invoice.find_by(reference_number: invoice_ref)) || (invoice_ref.present? && po_number.present? && invoice = Invoice.find_by(po_number: po_number.to_i))
      if receipt_details = ReceiptDetail.find_by(invoice_id: invoice.id)
        should_hold_for_review = true
        detail_category = 'GL Account'
        if orchestrator.partner == :commercehub_thd_us
          amount = remittance_advice_item_xml.xpath('itemBalanceDue').text.to_d
        else
          invoice_id = invoice.id
          detail_category = 'Invoice'
          amount = remittance_advice_item_xml.xpath('refInvoiceAmount').text.to_d
          invoice_discounts_to_apply += remittance_advice_item_xml.xpath('refInvoiceDiscAmount').text.to_d
        end
        business_unit_id = receipt.company.business_units.where("name ILIKE '%Sales%'").first&.id || receipt.company.business_units.where(number: [10_170, 20_170])&.id
        remark = "Invoice #{remittance_advice_item_xml.xpath('refInvoiceNumber').text} already paid in Receipt #{receipt_details.receipt.id}" if amount > 0.0 || amount < 0.0
         = receipt.company.ledger_company_accounts.where("name ILIKE '%Sales Expenses Other%'").first&.id
        ledger_detail_project_id = LedgerDetailProject.where("description ILIKE '%CN#{cust.id}%'").first&.id
      else
        invoice_id = invoice.id
        detail_category = 'Invoice' # this is how they establish this kind of detail for Invoice payment
        amount = remittance_advice_item_xml.xpath('refInvoiceAmount').text.to_d
        invoice_discounts_to_apply += remittance_advice_item_xml.xpath('refInvoiceDiscAmount').text.to_d
      end
    else # does not correspond to one of our invoices, needs manual research
      should_hold_for_review = true # this is likely a negative remittance to manually research, but could also be a one off invoice not in Heatwave
      detail_category = 'GL Account' # this is how they establish this kind of detail for credits TBD
      amount = if orchestrator.partner == :commercehub_thd_us
                 remittance_advice_item_xml.xpath('itemBalanceDue').text.to_d # this will be negative
               else
                 remittance_advice_item_xml.xpath('refInvoiceAmount').text.to_d
               end
      business_unit_id = receipt.company.business_units.where("name ILIKE '%Sales%'").first&.id || receipt.company.business_units.where(number: [10_170, 20_170])&.id # here we use the business unit associated with these i.e. the Sales & Affiliations (USA or Canada) unit
      if amount > 0.0 || amount < 0.0
        # this is a positive remittance, i.e. not due  to a return see EDI Communication Log ID: 582544 and Receipt ID: 105505 for an example of an unlinked credit, maybe from a dispute in our favor
        remark = if invoice_ref.present?
                   "Unlinked Invoice #{remittance_advice_item_xml.xpath('refInvoiceNumber').text}, date: #{remittance_advice_item_xml.xpath('refInvoiceDate').text}, Order #{remittance_advice_item_xml.xpath('refOrderNumber').text}"
                 else
                   "Adjustment to Invoice #{remittance_advice_item_xml.xpath('refInvoiceAdjNumber').text}, date: #{remittance_advice_item_xml.xpath('refInvoiceAdjDate').text}"
                 end
      end
       = receipt.company.ledger_company_accounts.where("name ILIKE '%Sales Expenses Other%'").first&.id # here we use the ledger company account associated with these i.e. the Sales Expenses Other (USA or Canada) company account
      ledger_detail_project_id = LedgerDetailProject.where("description ILIKE '%CN#{cust.id}%'").first&.id # here we use the ledger detail project associated with the customer i.e. description contains CNXXXXXX where XXXXXX is the customer ID
    end
    receipt.receipt_details.build(
      invoice_id: invoice_id,
      category: detail_category,
      amount: amount,
      business_unit_id: business_unit_id,
      ledger_company_account_id: ,
      ledger_detail_project_id: ledger_detail_project_id,
      remark: remark,
      gl_date: payment_date.to_date
    )
  end
  if invoice_discounts_to_apply > 0.0
    receipt.receipt_details.build(
      category: 'GL Account', # this is how they establish this kind of detail for discounts
      amount: -1.0 * invoice_discounts_to_apply,
      business_unit_id: receipt.company.business_units.where("name ILIKE '%Administration%'").first&.id || receipt.company.business_units.where(number: [10_100, 20_100])&.id, # here we use the business unit associated with these i.e. the Administration (USA or Canada) unit
      ledger_company_account_id: receipt.company.ledger_company_accounts.where("name ILIKE '%Cash Discounts%'").first&.id, # here we use the ledger company account associated with these i.e. the Cash Discounts (USA or Canada) company account
      gl_date: payment_date.to_date
    )
  end

  receipt.hold_for_review = should_hold_for_review
  receipt.save!
  receipt
end

#process_receipts(remittance_advices_data) ⇒ Object

Iterates through the batch receipt/remittance advice data



53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'app/services/edi/commercehub/remit_message_processor.rb', line 53

def process_receipts(remittance_advices_data)
  xml_doc = Nokogiri::XML(remittance_advices_data)
  receipts_created = []
  batch_number = xml_doc.xpath('//RemittanceAdvices/@messagebatch-id').text
  receipts_in_batch = 0
  xml_doc.xpath('//RemittanceAdviceMessage').each do |remittance_advice_message_xml|
    receipts_in_batch += 1
    receipts_created << process_receipt(remittance_advice_message_xml, batch_number) if Receipt.where(edi_transaction_id: remittance_advice_message_xml.xpath('hubMessageId').text).count.zero? # only process these once
  end
  BatchProcessResult.new(batch_number: batch_number,
                         receipts_created: receipts_created,
                         receipts_in_batch: receipts_in_batch)
end