Class: Edi::Amazon::ReturnsReportProcessor
- Inherits:
-
BaseEdiService
- Object
- BaseService
- BaseEdiService
- Edi::Amazon::ReturnsReportProcessor
- Defined in:
- app/services/edi/amazon/returns_report_processor.rb
Overview
Service object: returns report processor.
Applies the Seller Central returns flat file (category +return_batch+,
landed by ReturnsReportRetriever) to our RMAs:
- The retailer-stated return reason is recorded as ONE deduped note
activity per (RMA, Amazon RMA ID) on the matching RMA, so staff see why
Amazon says the customer sent it back without opening Seller Central. - Open RMA items still carrying the placeholder +TBD+ reason are upgraded
to the internal reason code mapped from the Amazon reason via
RmaReasonCode.find_by_alias (aliases seeded from Amazon's
return-reason vocabulary). A non-TBD reason is never changed, and RMAs
are never created — Amazon-side returns without a matching order/RMA
are counted as unmatched and warn-logged.
Rows are matched by the report's "Order ID" against +orders.edi_po_number+
(Amazon order ingestion stores the AmazonOrderId there). When Amazon
supplies "Merchant RMA ID", that internal RMA number is authoritative only
after verifying it belongs to the reported order or an RMA-generated
replacement-order descendant. Missing or unrelated Merchant RMA IDs are
quarantined without side effects. Older report shapes without that field
retain the most-recent-open-RMA fallback.
Defined Under Namespace
Constant Summary collapse
- HEADER_MAP =
Normalized report header => canonical row key. Normalization strips
everything non-alphanumeric and downcases, so header variants (extra
spaces, different casing) all land on the same key. { 'returnrequestdate' => :return_request_date, 'orderid' => :order_id, 'sellersku' => :seller_sku, 'merchantsku' => :seller_sku, # live reports use "Merchant SKU" 'asin' => :asin, 'returnreason' => :return_reason, 'amazonrmaid' => :amazon_rma_id, 'merchantrmaid' => :merchant_rma_id, 'returnquantity' => :return_quantity, 'returnrequeststatus' => :status, 'returncarrier' => :carrier, 'trackingid' => :tracking_id, 'returndeliverydate' => :return_delivery_date }.freeze
- PLACEHOLDER_REASON =
The placeholder reason code assigned when a return is keyed in before
the retailer's reason is known (rma.rb keys new items with 'TBD'). 'TBD'
Constants included from RequestIdentifiable
RequestIdentifiable::REQUEST_ID_HEADERS
Constants included from Edi::AddressAbbreviator
Edi::AddressAbbreviator::MAX_LENGTH
Instance Attribute Summary
Attributes inherited from BaseEdiService
Attributes inherited from BaseService
Instance Method Summary collapse
-
#apply_row(row, counts) ⇒ void
Applies one report row: locate the order and RMA, write the deduped reason note, upgrade a placeholder-reasoned item when mappable.
-
#process(edi_logs = nil) ⇒ Array<Result>
Picks up the +return_batch+ logs in the queue ready to process and applies each to our RMAs.
-
#process_rows(data) ⇒ Result
Parses the TSV header-first and applies every row.
Methods inherited from BaseEdiService
#amazon_feed_product_type, #duplicate_po_already_notified?, #initialize, #mark_duplicate_po_as_notified, #onboard_ordered_catalog_items, #report_order_creation_issues, #safe_process_edi_communication_log
Methods included from RequestIdentifiable
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
#apply_row(row, counts) ⇒ void
This method returns an undefined value.
Applies one report row: locate the order and RMA, write the deduped
reason note, upgrade a placeholder-reasoned item when mappable.
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 |
# File 'app/services/edi/amazon/returns_report_processor.rb', line 151 def apply_row(row, counts) order = find_order(row[:order_id]) unless order counts[:rows_unmatched] += 1 logger.warn "Returns report row unmatched — no order for Amazon order id #{row[:order_id].inspect} (Amazon RMA #{row[:amazon_rma_id]})" return end rma_match = resolve_rma(order, row) if rma_match.quarantine counts[:rows_quarantined] += 1 counts[:quarantined_rows] << rma_match.quarantine logger.warn "Returns report row quarantined — #{rma_match.quarantine.inspect}" return end rma = rma_match.rma unless rma counts[:rows_unmatched] += 1 logger.warn "Returns report row unmatched — order #{order.reference_number} has no RMA (Amazon RMA #{row[:amazon_rma_id]})" return end counts[:rows_matched] += 1 item = match_item(rma, row[:seller_sku]) upgrade = upgrade_for(item, row[:return_reason]) note = build_note(row, upgrade) if note_already_recorded?(rma, note) counts[:duplicates_skipped] += 1 return end if upgrade # update_column, not update!: legacy RmaItem rows can be invalid by # current rules (e.g. blank returned_item_location, which predates the # presence validation at rma_item.rb:159) and a full save would raise # on fields we are not touching. This upgrade only ever corrects the # reason code; the activity note below is the audit trail. (AppSignal # #1358 — a blank-location legacy row aborted a whole return_batch log.) item.update_column(:returned_reason, upgrade[:code]) counts[:items_upgraded] += 1 end # create!, not create: a note that fails validation must raise so the # per-log rescue marks the log exception and retries — a silently lost # note with notes_written += 1 and a completed log is unrecoverable. rma.activities.create!(notes: note) counts[:notes_written] += 1 end |
#process(edi_logs = nil) ⇒ Array<Result>
Picks up the +return_batch+ logs in the queue ready to process and
applies each to our RMAs. Follows the canonical processor pattern: one
transaction per log, +complete!+ on success, +error+ + AppSignal report
on exception.
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 |
# File 'app/services/edi/amazon/returns_report_processor.rb', line 87 def process(edi_logs = nil) # Pick up queued logs AND prior exceptions: the daily retriever run # re-invokes this processor, so a transient failure self-heals on the # next day instead of dropping out of the queue (requiring_processing # only selects ready/retry). Bounded to once/day by the flow gate. edi_logs ||= EdiCommunicationLog.where(partner: orchestrator.partner, category: 'return_batch', state: %w[ready retry exception]).order(:created_at) edi_logs = [edi_logs].flatten results = [] # System-actor whodunnit for the audit trail: reason upgrades and note # writes made by this ingest must name the processor, not NULL. PaperTrail.request(whodunnit: 'Edi::Amazon::ReturnsReportProcessor') do edi_logs.each do |edi_log| log_info "Starting processing edi communication log #{edi_log.id}" ErrorReporting.scoped({ edi_log_id: edi_log.id }) do EdiCommunicationLog.transaction do result = process_rows(edi_log.data) edi_log.file_info ||= {} edi_log.file_info[:returns_report] = result.to_h edi_log.complete! results << result log_info "Returns report edi log #{edi_log.id}: #{result.rows_processed} rows, #{result.rows_matched} matched, " \ "#{result.rows_unmatched} unmatched, #{result.rows_quarantined} quarantined, #{result.items_upgraded} items upgraded, " \ "#{result.notes_written} notes written, " \ "#{result.duplicates_skipped} duplicates skipped" end end rescue StandardError => e edi_log.notes = "#{e} at #{e&.backtrace_locations&.first} for edi log #{edi_log.id}" edi_log.error ErrorReporting.error(e, edi_communication_log_id: edi_log.id) end end results end |
#process_rows(data) ⇒ Result
Parses the TSV header-first and applies every row. Public for testing;
callers normally go through #process.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'app/services/edi/amazon/returns_report_processor.rb', line 127 def process_rows(data) counts = { rows_processed: 0, rows_matched: 0, rows_unmatched: 0, rows_quarantined: 0, items_upgraded: 0, notes_written: 0, duplicates_skipped: 0, quarantined_rows: [] } parse_rows(data).each do |row| counts[:rows_processed] += 1 apply_row(row, counts) end Result.new(**counts) end |