Class: Edi::Amazon::FbaOrderMessageProcessor
- Inherits:
-
BaseEdiService
- Object
- BaseService
- BaseEdiService
- Edi::Amazon::FbaOrderMessageProcessor
- Defined in:
- app/services/edi/amazon/fba_order_message_processor.rb
Overview
Service object: fba order message processor.
Defined Under Namespace
Classes: BatchProcessResult, LineCreationResult, Result
Constant Summary
Constants included from Edi::AddressAbbreviator
Edi::AddressAbbreviator::MAX_LENGTH
Instance Attribute Summary
Attributes inherited from BaseEdiService
Attributes inherited from BaseService
Instance Method Summary collapse
-
#create_invoice_shipping_address(order_hash) ⇒ Address
Builds the shipping
Addressfor an FBA invoice from an Amazon SP-API order payload. - #customer(segment = nil) ⇒ Object
- #is_vine_promotion_order?(order_hash) ⇒ Boolean
-
#process(edi_logs = nil, edi_transaction_id: nil) ⇒ Object
Picks up the edi communication log in the queue ready to process, generate acknowledgements.
-
#process_order(order_hash = nil) ⇒ Object
Process a single FBA order into a consignment invoice.
-
#process_orders(orders_data, edi_transaction_id: nil, edi_log_id: nil) ⇒ Object
Iterates through the batch order data.
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 Edi::AddressAbbreviator
#abbreviate_street, #collect_street_originals, #record_address_abbreviation_notes
Methods inherited from BaseService
#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #tagged_logger
Constructor Details
This class inherits a constructor from Edi::BaseEdiService
Instance Method Details
#create_invoice_shipping_address(order_hash) ⇒ Address
PII fallback is per-field, not all-or-nothing — Amazon sometimes returns the
PII block with only Name populated and the rest in ShippingAddress, or omits
ShippingAddressPii entirely. Each field falls through PII → non-PII independently.
AddressLine2 (suite/unit) falls back the same way as AddressLine1; without
the fallback, suite/unit data is silently dropped when PII is missing.
Quebec province arrives as the literal 'QU' (not 'QC'); other provinces
arrive spelled out. The mapping below normalizes 'QU' → 'Quebec' so
State.code_for_string can resolve it.
Postal code is upcased and stripped of spaces (Canadian postal codes only).
disable_address_correction and override_all_address_validation are set so
USPS/UPS validation does not reject Amazon-supplied addresses we don't fully control.
Sample PII payload (US Commercial):
{ StateOrRegion: 'MN', AddressLine1: '1805 HAVEN RD', PostalCode: '56345-2286', City: 'LITTLE FALLS', CountryCode: 'US', AddressType: 'Commercial', Name: 'XXX XXX' }
Sample PII payload (Canada): { StateOrRegion: 'Manitoba', AddressLine1: 'PO Box 412', PostalCode: 'R0L0C0', City: 'Benito', CountryCode: 'CA', Name: 'David Pleli' }
Builds the shipping Address for an FBA invoice from an Amazon SP-API order payload.
Reads from ShippingAddressPii (restricted-data-token, name + street fields) when
available and falls back to ShippingAddress (non-PII, no name/street) per field.
When both blocks are absent, returns the customer store's warehouse address.
Logic Details
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 |
# File 'app/services/edi/amazon/fba_order_message_processor.rb', line 177 def create_invoice_shipping_address(order_hash) if order_hash.dig(:ShippingAddressPii).nil? && order_hash.dig(:ShippingAddress).nil? # || is_vine_promotion_order?(order_hash) # per @orders team use warehouse address fpr Vine promo FBAs # later overidden by Prabhakar, who said to use address if we get it, it is not available via the Seller Central portal address = customer.store.warehouse_address else raw_street1 = order_hash.dig(:ShippingAddressPii, :AddressLine1) || order_hash.dig(:ShippingAddress, :AddressLine1) || order_hash.dig(:ShippingAddress, :City) raw_street2 = order_hash.dig(:ShippingAddressPii, :AddressLine2) || order_hash.dig(:ShippingAddress, :AddressLine2) # PII fields come from a restricted data token and are sometimes missing # for orders Amazon classifies as non-PII-eligible — fall back to the # plain ShippingAddress for every field so we don't fail Address validations. address = Address.new address.person_name_override = order_hash.dig(:ShippingAddressPii, :Name)&.to_s&.titleize&.strip || order_hash.dig(:BuyerInfo, :BuyerEmail) || 'Customer' address.street1 = abbreviate_street(raw_street1) address.street2 = abbreviate_street(raw_street2) address.city = order_hash.dig(:ShippingAddressPii, :City) || order_hash.dig(:ShippingAddress, :City) address.country_iso = order_hash.dig(:ShippingAddressPii, :CountryCode) || order_hash.dig(:ShippingAddress, :CountryCode) state_or_province_string_or_code = order_hash.dig(:ShippingAddressPii, :StateOrRegion) || order_hash.dig(:ShippingAddress, :StateOrRegion) state_or_province_string_or_code = 'Quebec' if state_or_province_string_or_code == 'QU' # don't ask me why they send this code instead of spelled-out 'Quebec', like the other provinces address.state_code = State.code_for_string(state_or_province_string_or_code, countries_iso3: [Country.iso3_for_string(address.country_iso)]) # we get state code for US but Province name for Canada! address.zip = (order_hash.dig(:ShippingAddressPii, :PostalCode) || order_hash.dig(:ShippingAddress, :PostalCode)).to_s.upcase.delete(' ').strip address.disable_address_correction = true address.override_all_address_validation = true if address.city.blank? || address.country_iso3.blank? address = customer.store.warehouse_address else address.save! end end address end |
#customer(segment = nil) ⇒ Object
19 20 21 |
# File 'app/services/edi/amazon/fba_order_message_processor.rb', line 19 def customer(segment = nil) orchestrator.customer(segment) end |
#is_vine_promotion_order?(order_hash) ⇒ Boolean
208 209 210 |
# File 'app/services/edi/amazon/fba_order_message_processor.rb', line 208 def is_vine_promotion_order?(order_hash) order_hash.dig(:OrderItems)&.any? { |line_hash| line_hash.dig(:PromotionIds)&.any? { |pid| pid&.downcase&.include?('vine.') } } end |
#process(edi_logs = nil, edi_transaction_id: nil) ⇒ Object
Picks up the edi communication log in the queue ready to process, generate acknowledgements
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 51 52 |
# File 'app/services/edi/amazon/fba_order_message_processor.rb', line 24 def process(edi_logs = nil, edi_transaction_id: nil) edi_logs ||= EdiCommunicationLog.requiring_processing.where(partner: orchestrator.partner, category: 'fba_order_batch') 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| log_info "Starting processing edi communication log #{edi_log.id}" EdiCommunicationLog.transaction do # Process the orders in the batch batch_process_result = process_orders(edi_log.data, edi_transaction_id: edi_transaction_id, edi_log_id: edi_log.id) # Record some meta data in file info edi_log.file_info ||= {} edi_log.file_info[:orders_in_batch] = batch_process_result.orders_in_batch batch_process_results << batch_process_result edi_log.complete! # Mark our edi log as processed, we need some error handling at one point # Associate the created invoices batch_process_result.invoices_created.each do |invoice| edi_log.edi_documents.create(invoice: invoice) end rescue StandardError => e edi_log.notes = "#{e} at #{e&.backtrace_locations}" edi_log.error ErrorReporting.error(e, edi_communication_log_id: edi_log.id) # End begin rescue block end end Result.new(batch_process_results: batch_process_results) end |
#process_order(order_hash = nil) ⇒ Object
Process a single FBA order into a consignment invoice
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 |
# File 'app/services/edi/amazon/fba_order_message_processor.rb', line 92 def process_order(order_hash = nil) cust = customer fba_store = Store.find(orchestrator.fba_store_id) purchase_date = Time.zone.parse(order_hash.dig(:PurchaseDate)) invoice = Invoice.new( company: fba_store.company, customer: cust, store: fba_store, invoice_type: Invoice::CI, total: BigDecimal(order_hash.dig(:OrderTotal, :Amount).to_s), currency: order_hash.dig(:OrderTotal, :CurrencyCode), document_date: Time.zone.now.to_date, gl_date: purchase_date.to_date, tax_date: purchase_date.to_date, due_date: Time.zone.now.to_date, shipped_date: purchase_date.to_date, terms: 'Due', po_number: order_hash[:AmazonOrderId], report_grouping: 'E-commerce - Amazon', state: 'draft', transmission_state: 'awaiting_transmission', tax_exempt: false # discount: 0.0, # shipping_cost: 0.0, # shipping_coupon: 0.0, # profit: 0.0, # profit_consolidated: 0.0 ) Invoice.transaction do # Set addresses invoice.shipping_address = create_invoice_shipping_address(order_hash) invoice.billing_address_id = cust.billing_address_id invoice.billing_customer_id = cust.id # Set the consignment offset account invoice.gl_offset_account = LedgerCompanyAccount.for_company_and_account(invoice.company_id, TRADE_ACCOUNTS_RECEIVABLE_ACCOUNT) # Create line items and calculate totals line_creation_result = create_line_items(invoice, fba_store, order_hash) raise line_creation_result. unless line_creation_result.all_skus_valid # Set consolidated amounts invoice.revenue_consolidated = line_creation_result.line_total invoice.consolidated_exchange_rate = invoice.currency == CONSOLIDATED_CURRENCY ? 1.0 : ExchangeRate.get_exchange_rate(invoice.currency, CONSOLIDATED_CURRENCY, invoice.gl_date) invoice.save! # Add notes invoice.quick_note('Amazon FBA Order') invoice.quick_note('Replacement Order') if order_hash[:IsReplacementOrder].to_b end invoice end |
#process_orders(orders_data, edi_transaction_id: nil, edi_log_id: nil) ⇒ Object
Iterates through the batch order data
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 84 85 86 87 88 89 |
# File 'app/services/edi/amazon/fba_order_message_processor.rb', line 55 def process_orders(orders_data, edi_transaction_id: nil, edi_log_id: nil) # rubocop:disable Lint/UnusedMethodArgument json_hash = JSON.parse(orders_data).with_indifferent_access invoices_created = [] orders_in_batch = 0 (json_hash.dig(:payload, :Orders) || []).each do |order_hash| log_info "Processing order #{order_hash.inspect}" orders_in_batch += 1 po_number = order_hash[:AmazonOrderId] begin # Check for existing invoices with this PO number existing_invoices = Invoice.where(po_number: po_number).where('created_at > ?', 12.months.ago) is_replacement_order = order_hash[:IsReplacementOrder].to_b is_shipped_order = order_hash[:OrderStatus] == 'Shipped' if existing_invoices.none? && is_shipped_order && !is_replacement_order # skip orders not in Shipped status and also replacement orders, so accounting team can deal with replacement orders manually for now invoices_created << process_order(order_hash) elsif is_replacement_order || !is_shipped_order reason_blurb = "we do not process replacement FBA orders" if is_replacement_order reason_blurb = "we do not process FBA orders unless they are in Shipped status" unless is_shipped_order subj = "EDI FBA invoice message for orchestrator: #{orchestrator.partner}, SKIPPED, order message PO: #{po_number}, reason: #{reason_blurb}." msg = "We skipped processing amazon FBA order #{po_number} with orchestrator #{orchestrator.partner}, and edi log id #{edi_log_id}, reason: #{reason_blurb}." EdiMailer.notify_edi_admin_of_warning(subj, msg).deliver_later end rescue StandardError => e subj = "EDI FBA invoice message for orchestrator: #{orchestrator.partner}, ERROR, SKIPPED, order message PO: #{po_number}, #{e.}" msg = "Exception #{e.} while processing amazon FBA order #{po_number} with orchestrator #{orchestrator.partner}, and edi log id #{edi_log_id}" ErrorReporting.error e, msg EdiMailer.notify_edi_admin_of_warning(subj, msg).deliver_later end end # As a temporary measure, notify ap, orders team and EDI Admin of new Amazon Seller Central orders OrdersMailer.amazon_seller_central_fba_ci_invoices_notification(invoices_created).deliver_later if invoices_created.any? BatchProcessResult.new(batch_number: nil, invoices_created: invoices_created, orders_in_batch: orders_in_batch) end |