Class: Order::AuthorizationSplitter

Inherits:
Object
  • Object
show all
Defined in:
app/services/order/authorization_splitter.rb

Overview

Allocates a parent order's existing authorization across a split-off child
order, once the child has committed delivery(ies) to attach payment to.

This is the payment half of Splitter, extracted so it can run in
two places:

  1. Synchronously inside Splitter when the child already has a
    delivery at split time (the in-stock case).
  2. Deferred, via AuthorizationSplitWorker, when the child's
    delivery is committed after the split finished (back-order / shipping
    quoted later) — the case where the authorization previously never
    followed the order.

@order is the parent (which holds the un-split authorization) and
@new_order is the child. The credit-card / PayPal / PO logic is unchanged
from the original splitter.

Defined Under Namespace

Classes: Result

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parent, new_order, logger: Rails.logger) ⇒ AuthorizationSplitter

Returns a new instance of AuthorizationSplitter.

Parameters:

  • parent (Order)

    the order holding the original authorization

  • new_order (Order)

    the split-off child order to fund

  • logger (Logger) (defaults to: Rails.logger)


42
43
44
45
46
# File 'app/services/order/authorization_splitter.rb', line 42

def initialize(parent, new_order, logger: Rails.logger)
  @order = parent
  @new_order = new_order
  @logger = logger
end

Class Method Details

.topup(order, logger: Rails.logger) ⇒ Result

Reconcile a single order whose shared-PI authorization no longer covers
its total after the split's coupon reallocation / shipping re-quote
settled. Unlike #call, this runs for the PARENT too — the deferred
child-only worker never re-funds the parent.

Parameters:

  • order (Order)

    the under-authorized order to top up

  • logger (Logger) (defaults to: Rails.logger)

Returns:



56
57
58
# File 'app/services/order/authorization_splitter.rb', line 56

def self.topup(order, logger: Rails.logger)
  new(order, order, logger:).topup_shared_pi_authorization(order)
end

Instance Method Details

#callBoolean, Result

Splits the parent's authorization onto the child's deliveries.

Returns:

  • (Boolean)

    true if an allocation was attempted, false when there was
    nothing to do (no child deliveries, or no authorized parent payment).

  • (Result)

    :split when the child ends up funded, :failed when a
    payment path ran but it's still underfunded, :noop when there was nothing
    to allocate. Callers must not advance the order's state on :noop/:failed.



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
# File 'app/services/order/authorization_splitter.rb', line 108

def call
  return Result.noop if @new_order.deliveries.blank?

  po_payment = @order.payments.all_authorized.where(category: [Payment::PO, Payment::VPO, Payment::CHECK, Payment::CASH]).first
  cc_payment = @order.payments.all_authorized.where(category: Payment::CREDIT_CARD).first
  paypal_payment = @order.payments.all_authorized.where(category: Payment::PAYPAL).first

  if po_payment
    attrs = {
      currency: po_payment.currency,
      category: po_payment.category,
      po_number: po_payment.po_number,
      amount: @new_order.total
    }
    Payment::OrderProcessor.new(@new_order, attrs).process
  elsif cc_payment && cc_payment.credit_card_vault.present?
    split_cc_payment(cc_payment)
  elsif paypal_payment
    split_paypal_payment(paypal_payment)
  else
    return Result.noop
  end

  # A path ran; report whether the child is actually covered now. (The
  # individual gateway paths swallow their own failures, so balance is the
  # reliable post-condition.)
  @new_order.reload
  @new_order.balance <= 0 ? Result.split : Result.failed
end

#topup_shared_pi_authorization(order) ⇒ Result

Create supplemental shared-PI payment(s) covering order's uncovered
balance and raise the PI to match (incremental auth, falling back to a
fresh authorization when the PI can't be incremented). Reuses the same
allocation + shortfall primitives as the initial split, so it is safe to
call repeatedly — a fully-funded order is a :noop.

Parameters:

Returns:

  • (Result)

    :split when the order ends up funded, :failed when a
    path ran but it's still short, :noop when there was nothing to do.



69
70
71
72
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
# File 'app/services/order/authorization_splitter.rb', line 69

def topup_shared_pi_authorization(order)
  order.reload
  return Result.noop unless order.balance.positive?

  source = order.payments.credit_cards.all_authorized
                .where.not(stripe_payment_intent_id: nil)
                .detect(&:supports_multicapture?)
  unless source
    @logger.warn("#{Time.current}: Auth top-up skipped for order #{order.id} — no multicapture-capable shared-PI CC payment to top up")
    return Result.noop
  end

  vault = source.credit_card_vault
  unless vault
    @logger.warn("#{Time.current}: Auth top-up skipped for order #{order.id} — source payment #{source.id} has no vault on file")
    return Result.noop
  end

  begin
    pi = Payment::Apis::Stripe.retrieve_payment_intent(source.stripe_payment_intent_id, currency: source.currency)
  rescue ::Stripe::StripeError => e
    @logger.error("#{Time.current}: Auth top-up could not retrieve PI #{source.stripe_payment_intent_id} for order #{order.id}: #{e.message}")
    return Result.failed
  end

  create_shared_pi_payments_for_order(order, source, vault, pi, source.stripe_capabilities, source.capture_before)
  resolve_shared_pi_shortfall(pi.id, pi.amount_capturable.to_i, vault, source)

  order.reload
  order.balance <= 0 ? Result.split : Result.failed
end