Class: Edi::Walmart::ReturnsReportProcessor
- Inherits:
-
BaseEdiService
- Object
- BaseService
- BaseEdiService
- Edi::Walmart::ReturnsReportProcessor
- Defined in:
- app/services/edi/walmart/returns_report_processor.rb
Overview
Service object: returns report processor.
Applies the Walmart Returns API payload (category +return_batch+, landed
by ReturnsReportRetriever) to our RMAs:
- The retailer-stated return reason is recorded as ONE deduped note
activity per (RMA, Walmart return order line) on the matching RMA, so
staff see why Walmart says the customer sent it back without opening
Seller Center. - Open RMA items still carrying the placeholder +TBD+ reason are upgraded
to the internal reason code mapped from the Walmart reason via
RmaReasonCode.find_by_alias. A non-TBD reason is never changed, and
RMAs are never created — Walmart-side returns without a matching
order/RMA are counted as unmatched and warn-logged.
Rows are matched by the line's +purchaseOrderId+ (falling back to the
return order's +customerOrderId+) against +orders.edi_po_number+ (Walmart
order ingestion stores the purchaseOrderId there), then to the order's
most recent OPEN RMA (Rma::RETURN_OPEN_STATES), falling back to the
most recent RMA of any state so late-arriving returns still land their
note somewhere sensible.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- FIELD_MAP =
Walmart v3 returns payload (GET /v3/returns) => canonical row keys.
Order-level keys are read from each returnOrder, line-level keys from
each of its returnOrderLines; every (returnOrder, line) pair is one row.
Nested line fields (+item.sku+, the status trio) are dig-resolved in
#parse_rows and documented here:
item.sku => :seller_sku
status => :status
currentDeliveryStatus => :delivery_status
currentRefundStatus => :refund_status { order: { 'returnOrderId' => :return_order_id, 'customerOrderId' => :customer_order_id, 'returnOrderDate' => :return_order_date }, line: { 'purchaseOrderId' => :purchase_order_id, 'returnOrderLineNumber' => :line_number, 'returnReason' => :return_reason, 'returnDescription' => :return_description } }.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 AddressAbbreviator
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 return line: 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 JSON payload and applies every return line.
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 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 return line: locate the order and RMA, write the deduped
reason note, upgrade a placeholder-reasoned item when mappable.
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 |
# File 'app/services/edi/walmart/returns_report_processor.rb', line 127 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 Walmart order id #{row[:order_id].inspect} (return order #{row[:return_order_id]})" return end rma = find_rma(order) unless rma counts[:rows_unmatched] += 1 logger.warn "Returns report row unmatched — order #{order.reference_number} has no RMA (return order #{row[:return_order_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 line_already_noted?(rma, row) 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.
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 |
# File 'app/services/edi/walmart/returns_report_processor.rb', line 73 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::Walmart::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.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 JSON payload and applies every return line. Public for
testing; callers normally go through #process.
112 113 114 115 116 117 118 119 |
# File 'app/services/edi/walmart/returns_report_processor.rb', line 112 def process_rows(data) counts = { rows_processed: 0, rows_matched: 0, rows_unmatched: 0, items_upgraded: 0, notes_written: 0, duplicates_skipped: 0 } parse_rows(data).each do |row| counts[:rows_processed] += 1 apply_row(row, counts) end Result.new(**counts) end |