Class: Edi::MftGateway::InvoiceMessageProcessor

Inherits:
BaseEdiService show all
Defined in:
app/services/edi/mft_gateway/invoice_message_processor.rb

Constant Summary

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

#create_credit_memo(credit_memo, options = {}) ⇒ Object



9
10
11
12
# File 'app/services/edi/mft_gateway/invoice_message_processor.rb', line 9

def create_credit_memo(credit_memo, options = {})
  # Here we simply send the amount as negative for credit memo, per Build.com's 810 EDI spec book 4.0
  process(options.merge({ order: credit_memo.invoice.order, invoice: credit_memo.invoice, credit_memo: }))
end

#create_invoice(invoice, options = {}) ⇒ Object



5
6
7
# File 'app/services/edi/mft_gateway/invoice_message_processor.rb', line 5

def create_invoice(invoice, options = {})
  process(options.merge({ order: invoice.order, invoice: invoice }))
end

#process(options = {}) ⇒ Object

{
"Header": {
"InvoiceHeader": {
"TradingPartnerId": "TPIDXXXXWARMLYYOU",
"InvoiceNumber": "INVNUM",
"InvoiceDate": "2023-01-26",
"PurchaseOrderDate": "2023-01-26",
"PurchaseOrderNumber": '123456789',
"TsetPurposeCode": "00",
"BuyersCurrency": "USD",
"Vendor": "Warmly_Yours",
"ShipDate": "2023-01-26"
},
"PaymentTerms": {
"TermsType": "01",
"TermsDiscountPercentage": "0.0",
"TermsDiscountDate": "2023-01-26",
"TermsDiscountAmount": "0.0",
"TermsDescription": "Terms and discount as previously agreed upon."
},
"Address": [
{
"AddressTypeCode": "RI",
"LocationCodeQualifier": "92",
"AddressLocationNumber": "135125",
"AddressName": "Build.com",
"Address1": "402 OTTERSON DR",
"Address2": "",
"Address3": "",
"City": "CHICO",
"State": "CA",
"PostalCode": "95928",
"Country": "US"
},
{
"AddressTypeCode": "BT",
"LocationCodeQualifier": "92",
"AddressLocationNumber": "135125",
"AddressName": "Build.com",
"Address1": "402 OTTERSON DR",
"Address2": "",
"Address3": "",
"City": "CHICO",
"State": "CA",
"PostalCode": "95928",
"Country": "US"
},
{
"AddressTypeCode": "ST",
"LocationCodeQualifier": "12",
"AddressLocationNumber": "1234567890",
"AddressName": "Customer Name",
"AddressAlternateName": "Alternate Name",
"Address1": "XXXXX Street Name Rd",
"City": "City Name",
"State": "WA",
"PostalCode": "12345"
}
]
},
"Structure": {
"LineItem": [
{
"InvoiceLine": {
"LineSequenceNumber": "1",
"BuyerPartNumber": "TW-SR08GS-HP",
"VendorPartNumber": "TW-SR08GS-HP",
"ConsumerPackageCode": "881308069001",
"InvoiceQty": "1",
"InvoiceQtyUOM": "EA",
"PurchasePrice": "769.3",
"PurchasePriceBasis": "PE",
"ShipQty": "1",
"ShipQtyUOM": "EA",
"ExtendedItemTotal": "769.3"
},
"ProductOrItemDescription": {
"ProductCharacteristicCode": "08",
"ProductDescription": "Sierra Towel Warmer Polished Gold Dual Connection 8 Bars"
}
}
]
},
"Summary": {
"TotalAmount": "769.3",
"TotalSalesAmount": "769.3",
"TotalLineItemNumber": "1"
}
}



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
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'app/services/edi/mft_gateway/invoice_message_processor.rb', line 104

def process(options = {})
  order = options[:order]
  invoice = options[:invoice]
  credit_memo = options[:credit_memo]
  invoice.delivery
  total = credit_memo ? credit_memo.total : invoice.total
  transmit_after = options[:transmit_after]
  inv_date, = credit_memo ? (credit_memo.request_date || credit_memo.document_date || Time.current).iso8601.split('T') : Time.current.iso8601.split('T')
  shipped_date = order.shipped_date&.iso8601&.split('T')&.first
  order_hash = JSON.parse(order.edi_original_order_message).with_indifferent_access
  ship_to_address_hash = order_hash.dig(:Header, :Address)&.detect { |add_hash| add_hash[:AddressTypeCode] == 'ST' } || order_hash.dig(:Header, :Address)&.detect { |add_hash| add_hash[:AddressTypeCode] == 'MA' } # MA for Canadian Tire

  # This is the workaround for when we do not get a proper MA segment in the PO from DSCO for Canadian Tire
  # Start workaround  part 1/2 ===========================
  store_number = ship_to_address_hash.dig(:AddressLocationNumber) || ship_to_address_hash.dig(:Address1).to_s.split('#').last.delete(' ')
  ma_address_hash = nil
  if orchestrator.partner == :mft_gateway_canadian_tire && store_number.present? && (ship_to_address_hash.dig(:AddressTypeCode) == 'ST') && !ship_to_address_hash.dig(:LocationCodeQualifier).present?
    # put in the ^&%^& store number for Canadian Tire DSCO in an MA segment, even though they don't send it to us!
    ma_address_hash = {}
    ma_address_hash[:AddressTypeCode] = 'MA' # ^&%^&% Canadian Tire requires this
    ma_address_hash[:LocationCodeQualifier] ||= '92'
    ma_address_hash[:AddressLocationNumber] ||= store_number
    ma_address_hash[:Address1] = order&.shipping_address&.company_name_override
  end
  # End workaround  part 1/2 ===========================

  # Comment out this old SPS Commerce weirdness with Canadian Tire
  # if (ship_to_address_hash[:AddressTypeCode] == 'MA' && ship_to_address_hash[:Address1].blank?)
  #   # must populate Canadian Tire actual address fields
  #   ship_to_address_hash[:AddressTypeCode] = 'ST' # ^&%^&% Canadian Tire requires this
  #   ship_to_address_hash[:AddressName] = order&.shipping_address&.company_name_override
  #   ship_to_address_hash[:Address1] = order&.shipping_address&.street1
  #   ship_to_address_hash[:City] = order&.shipping_address&.city
  #   ship_to_address_hash[:State] = order&.shipping_address&.state_code
  #   ship_to_address_hash[:PostalCode] = order&.shipping_address&.zip
  # end

  bill_to_address_hash = order_hash.dig(:Header, :Address)&.detect { |add_hash| add_hash[:AddressTypeCode] == 'BT' }
  remit_to_address = orchestrator.customer.billing_address
  remit_to_address_hash = nil
  if remit_to_address
    remit_to_address_hash = {
      AddressTypeCode: 'RI', # from: https://developercenter.spscommerce.com/#/rsx/docs/fields-qualifiers/7.7.6/Invoices/AddressTypeCode: ... RI Remit To ...
      LocationCodeQualifier: (orchestrator.partner == :mft_gateway_canadian_tire ? '1' : '92'), # just make it work, everyone has their own codes, Canadian Tire wants "1" here
      # https://developercenter.spscommerce.com/#/rsx/docs/fields-qualifiers/7.7.6/Invoices/LocationCodeQualifier ... 92 Buyer Location Number ...
      AddressLocationNumber: "#{remit_to_address.id}", #  documentation from SPS Commerce requires this so put some&^*&^ thing here
      AddressName: remit_to_address.company_name,
      Address1: remit_to_address.street1,
      Address2: remit_to_address.street2,
      Address3: remit_to_address.street3,
      City: remit_to_address.city,
      State: remit_to_address.state_code,
      PostalCode: remit_to_address.zip_compact,
      Country: remit_to_address.country.iso
    }
    bill_to_address_hash ||= remit_to_address_hash.dup
  elsif bill_to_address_hash.present? # this should never happen but populate the RI address as a dupe of the BT address
    remit_to_address_hash = bill_to_address_hash.dup
    remit_to_address_hash[:AddressTypeCode] = 'RI'
  end

  bill_to_address_hash[:AddressTypeCode] = 'BT' # for Canadian Tire, in case it came from remit_to_address
  bill_to_address_hash[:LocationCodeQualifier] = (orchestrator.partner == :mft_gateway_canadian_tire ? '1' : '92') # just make it work, everyone has their own codes, Canadian Tire wants "1" here
  # https://developercenter.spscommerce.com/#/rsx/docs/fields-qualifiers/7.7.6/Invoices/LocationCodeQualifier ... 92 Buyer Location Number ...
  addresses_arr = [bill_to_address_hash, ship_to_address_hash.merge({ PostalCode: order&.shipping_address&.zip_compact })].compact

  if orchestrator.customer.is_canadian_tire? && ma_address_hash.present?

    # Comment out this old SPS Commerce weirdness with Canadian Tire
    # deal with ^&%$&^% Canadian Tire
    #   # address comes only with AddressTypeCode "MA', and AddressLocationNumber and AddressName populated, i.e. store name/number, nothing else populated, for e.g.
    #   # {
    #   #   "AddressTypeCode": "MA",
    #   #   "LocationCodeQualifier": "92",
    #   #   "AddressLocationNumber": "0087",
    #   #   "AddressName": "CT RETAIL - ONLINE ORDER",
    #   #   "Address1": "",
    #   #   "City": "",
    #   #   "State": "",
    #   #   "PostalCode": ""
    #   # }
    # This is the workaround for when we do not get a proper MA segment in the PO from DSCO for Canadian Tire
    # Start workaround  part 2/2 ===========================
    ship_to_address_hash.delete(:Contacts) # remove Contacts array if any
    addresses_arr = [remit_to_address_hash, ship_to_address_hash.merge({ PostalCode: order&.shipping_address&.zip_compact }), ma_address_hash].compact
  end
  # End workaround part 2/2 ===========================

  invoice_line_items = invoice.line_items.goods.parents_only.non_shipping.where.not(edi_reference: nil).order(:edi_line_number)
  line_items_to_use = credit_memo ? credit_memo.line_items.goods.parents_only.non_shipping : invoice_line_items
  tot_lines = line_items_to_use.size

  line_items_hash_arr = []
  order_line_hash_arr = order_hash.dig(:Structure, :LineItem)
  summary_hash = {
    TotalAmount: ('%.2f' % total),
    TotalSalesAmount: ('%.2f' % total),
    TotalLineItemNumber: tot_lines
  }
  if orchestrator.try(:tax_registration_number).present?
    summary_hash[:Tax] = {
      TaxPercent: invoice_line_items.first.tax_rate * 100.0.round(2),
      TaxIdentification: orchestrator.tax_registration_number,
      TaxAmount: format('%.2f', (line_items_to_use.sum { |li| li.tax_total }))
    }
    if orchestrator.partner == :mft_gateway_canadian_tire
      # per https://support.dsco.io/hc/en-us/articles/13509498740763-Canadian-Tire-Corporation-Limited-Documentation
      # Acceptable tax type code values are: - GS: Goods and Services Tax (GST) - ZZ: Harmonized Sales Tax (HST) - SP: Quebec Sales Tax (QST)
      tax_type = invoice_line_items.first.tax_type
      tax_type_code = tax_type.downcase == 'hst' ? 'ZZ' : 'GS' # we only maintain hst and gst
      summary_hash[:Tax][:TaxTypeCode] = tax_type_code
      summary_hash[:Tax][:Description] = tax_type_code # they require a mirror of tax type code!
    end

  end
  line_items_to_use.each do |li|
    # do this so we can use the credit_memo line item stuff but inherit the invoice line item EDI stuff
    in_li = credit_memo ? invoice_line_items.detect{|l| l.sku == li.sku} : li
    line_number = in_li.edi_line_number.to_s
    order_line_hash = # deal with *&%&* DSCO padding line numbers with leading zeroes
      order_line_hash_arr.detect do |olh|
        olh&.dig(:OrderLine, :LineSequenceNumber) == line_number || in_li.edi_line_number == olh&.dig(:OrderLine, :LineSequenceNumber).to_s.to_i
      end
    vendor_sku = order_line_hash.dig(:OrderLine, :VendorPartNumber).presence || li&.item&.sku
    buyer_part_number = order_line_hash.dig(:OrderLine, :BuyerPartNumber).presence || li&.catalog_item&.third_party_part_number.presence || order_line_hash.dig(:OrderLine, :SKU).presence || vendor_sku
    sku = buyer_part_number
    sku = order_line_hash.dig(:OrderLine, :SKU) || vendor_sku || buyer_part_number if orchestrator.partner == :mft_gateway_canadian_tire
    consumer_package_code = order_line_hash.dig(:OrderLine, :ConsumerPackageCode).presence || li&.item&.upc.presence #  documentation from SPS Commerce requires this
    line_items_hash_arr << {
      InvoiceLine: {
        LineSequenceNumber: line_number,
        BuyerPartNumber: buyer_part_number,
        SKU: sku,
        VendorPartNumber: vendor_sku,
        ConsumerPackageCode: consumer_package_code,
        InvoiceQty: li.quantity.abs, # can be negative for credit memo
        InvoiceQtyUOM: 'EA',
        PurchasePrice: ('%.2f' % in_li.edi_unit_cost), # let's just send this because it will not be accepted unless it matches
        PurchasePriceBasis: 'PE',
        ShipQty: li.quantity.abs,
        ShipQtyUOM: 'EA',
        ExtendedItemTotal: format('%.2f', (in_li.edi_unit_cost * li.quantity.abs))
      },
      ProductOrItemDescription: {
        ProductCharacteristicCode: '08', # from: https://developercenter.spscommerce.com/#/rsx/docs/fields-qualifiers/7.7.6/Invoices/ProductCharacteristicCode: ... 08 Product Description ...
        ProductDescription: li.catalog_item.item.name[0..79].tr('', "'").gsub(/[^0-9a-z\s]/i, '').strip
      }
    }
  end

  inv_hash_data = {
    Header: {
      InvoiceHeader: {
        TradingPartnerId: orchestrator.partner_id,
        InvoiceNumber: credit_memo ? credit_memo.reference_number : invoice.reference_number, # per Build.com we don't have to limit this to the last 6 digits
        InvoiceDate: inv_date, # e.g. 2022-12-24
        PurchaseOrderDate: order.edi_order_date, # e.g. 2022-12-24
        PurchaseOrderNumber: order.edi_po_number,
        TsetPurposeCode: '00', # 00: Original
        BuyersCurrency: order.currency,
        Vendor: orchestrator.vendor_id,
        VendorId: orchestrator.vendor_id,
        ShipDate: shipped_date # e.g. 2022-12-24
      },
      PaymentTerms: {
        TermsType: '01', # 01 Basic,  documentation from SPS Commerce
        TermsDiscountPercentage: 0.0,
        TermsDiscountDate: inv_date,
        TermsDiscountAmount: format('%.2f', 0.0),
        TermsDescription: 'Terms and discount as previously agreed upon.'
      },
      Address: addresses_arr
    },
    Structure: {
      LineItem: line_items_hash_arr
    },
    Summary: summary_hash
  }.compact.to_json

  invoice = options[:invoice]
  EdiCommunicationLog.create_outbound_file_from_data(data: inv_hash_data,
                                                     file_extension: 'invoice',
                                                     partner: orchestrator.partner,
                                                     category: 'invoice',
                                                     data_type: 'json',
                                                     resources: invoice || order,
                                                     transmit_after: transmit_after,
                                                     file_info: { lines_confirmed: tot_lines })
end