Class: Returns::ReminderLadder
- Inherits:
-
Object
- Object
- Returns::ReminderLadder
- Defined in:
- app/services/returns/reminder_ladder.rb
Overview
Shared query + send mechanics for the advance-replacement reminder ladder:
RMA_REMINDER_1 (10 days) → RMA_REMINDER_2 (20 days) → RMA_CHARGE (30 days).
Each stage used to match invoices.document_date = N.days.ago.to_date
exactly, so a single missed daily run dropped that day's cohort permanently.
The match is now "at least N days old AND not yet notified", with the mark
kept per-RMA on rmas.<stage>_sent_at — the same shape as
quotes.expiration_notice_sent.
Stages are ordered AND day-separated: stage N requires stage N-1 to have been
sent on an EARLIER day. RmaReminderWorker runs all three back to back, so a
non-null check alone would let an invoice that first becomes eligible while
already 20+ days old collect two or three notices in a single pass.
Defined Under Namespace
Classes: Result
Instance Attribute Summary collapse
-
#days ⇒ Object
readonly
Returns the value of attribute days.
-
#requires ⇒ Object
readonly
Returns the value of attribute requires.
-
#sent_column ⇒ Object
readonly
Returns the value of attribute sent_column.
-
#system_code ⇒ Object
readonly
Returns the value of attribute system_code.
Instance Method Summary collapse
-
#deliver_all(logger: Rails.logger) {|invoice, rma| ... } ⇒ Result
Sends this stage for every pending invoice, claiming each RMA first so overlapping runs can't double-send.
-
#initialize(days:, sent_column:, system_code:, requires: nil) ⇒ ReminderLadder
constructor
A new instance of ReminderLadder.
-
#pending_invoices ⇒ ActiveRecord::Relation<Invoice>
Unpaid advance-replacement invoices whose RMA is still awaiting a return, old enough for this stage, and not yet notified at this stage.
Constructor Details
#initialize(days:, sent_column:, system_code:, requires: nil) ⇒ ReminderLadder
Returns a new instance of ReminderLadder.
36 37 38 39 40 41 |
# File 'app/services/returns/reminder_ladder.rb', line 36 def initialize(days:, sent_column:, system_code:, requires: nil) @days = days @sent_column = sent_column @requires = requires @system_code = system_code end |
Instance Attribute Details
#days ⇒ Object (readonly)
Returns the value of attribute days.
43 44 45 |
# File 'app/services/returns/reminder_ladder.rb', line 43 def days @days end |
#requires ⇒ Object (readonly)
Returns the value of attribute requires.
43 44 45 |
# File 'app/services/returns/reminder_ladder.rb', line 43 def requires @requires end |
#sent_column ⇒ Object (readonly)
Returns the value of attribute sent_column.
43 44 45 |
# File 'app/services/returns/reminder_ladder.rb', line 43 def sent_column @sent_column end |
#system_code ⇒ Object (readonly)
Returns the value of attribute system_code.
43 44 45 |
# File 'app/services/returns/reminder_ladder.rb', line 43 def system_code @system_code end |
Instance Method Details
#deliver_all(logger: Rails.logger) {|invoice, rma| ... } ⇒ Result
Sends this stage for every pending invoice, claiming each RMA first so
overlapping runs can't double-send.
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 |
# File 'app/services/returns/reminder_ladder.rb', line 76 def deliver_all(logger: Rails.logger) delivered = [] skipped = [] failed = [] pending_invoices.each do |invoice| rma = invoice.order.rma if rma.nil? || rma.skip_reminders skipped << rma&.rma_number next end # Atomic claim: whoever flips NULL → now() owns this stage for this RMA. # A concurrent run updates 0 rows and moves on rather than sending twice. unless claim(rma) skipped << rma.rma_number next end begin deliver(invoice, rma) yield(invoice, rma) if block_given? delivered << rma.rma_number rescue StandardError => e # Release the claim so a later run retries — a stage that failed to # send must not stay marked as done. release(rma) failed << rma.rma_number # One bad RMA must not strand the rest of the cohort; that's the whole # point of this rework. ErrorReporting.error(e, { rma_id: rma.id, stage: system_code }) logger.error "[#{system_code}] RMA #{rma.rma_number} failed: #{e.}" end end Result.new(delivered:, skipped: skipped.compact, failed:) end |
#pending_invoices ⇒ ActiveRecord::Relation<Invoice>
Unpaid advance-replacement invoices whose RMA is still awaiting a return,
old enough for this stage, and not yet notified at this stage.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'app/services/returns/reminder_ladder.rb', line 49 def pending_invoices scope = Invoice .joins(order: [:rma, { deliveries: [:payments] }]) .where(invoices: { state: 'unpaid' }) .where(payments: { category: Payment::ADV_REPL }) .where(rmas: { state: 'awaiting_return' }) .where(Invoice.arel_table[:document_date].lteq(days.days.ago.to_date)) .where(rmas: { sent_column => nil }) # Strictly BEFORE today, not merely present: the worker runs all three # stages in one pass, so `IS NOT NULL` would let a stage fire moments after # its predecessor stamped the same RMA. scope = scope.where(Rma.arel_table[requires].lt(Time.current.beginning_of_day)) if requires # `joins` only builds the filtering INNER JOIN — it does not populate the # association cache, so `invoice.order.rma` in the delivery loop would fire # two queries per row. `preload` fetches both in one extra query each, # without touching the SQL this relation's callers assert on. scope.distinct.preload(order: :rma) end |