Class: Payment::StrategyResolver

Inherits:
BaseService show all
Defined in:
app/services/payment/strategy_resolver.rb

Overview

Decides capture/reauthorization strategy for a Payment based on
upstream gateway capabilities (Stripe multicapture, incremental and
extended auth) and PayPal's 29-day window. Encapsulates the deadline
math the model needs without bloating it.

Defined Under Namespace

Classes: CaptureOptions, Deadline, ReauthResult, Result

Constant Summary collapse

REAUTH_BUFFER_DAYS =
2
LEGACY_AUTH_WINDOW_DAYS =
6
EXTENDED_AUTH_WINDOW_DAYS =
28
PAYPAL_AUTH_WINDOW_DAYS =
3

Instance Attribute Summary

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseService

#log_debug, #log_error, #log_info, #log_warning, #logger, #process, #tagged_logger

Constructor Details

#initialize(payment) ⇒ StrategyResolver

Returns a new instance of StrategyResolver.



28
29
30
# File 'app/services/payment/strategy_resolver.rb', line 28

def initialize(payment)
  @payment = payment
end

Instance Method Details

#authorization_deadlinePayment::StrategyResolver::Deadline

When the payment must be captured by. Prefers the gateway-reported
capture_before; otherwise uses a heuristic window based on the
gateway and capability flags (PayPal 3d, Stripe extended-auth 28d,
legacy Stripe 6d).



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'app/services/payment/strategy_resolver.rb', line 110

def authorization_deadline
  if @payment.capture_before.present?
    remaining = ((@payment.capture_before - Time.current) / 1.day).ceil
    source = paypal? ? :paypal_api : :stripe_api
    Deadline.new(capture_before: @payment.capture_before, days_remaining: remaining, source: source)
  else
    fallback_days = if paypal?
                      PAYPAL_AUTH_WINDOW_DAYS
                    elsif supports_extended_authorization?
                      EXTENDED_AUTH_WINDOW_DAYS
                    else
                      LEGACY_AUTH_WINDOW_DAYS
                    end
    fallback = @payment.created_at + fallback_days.days
    remaining = ((fallback - Time.current) / 1.day).ceil
    Deadline.new(capture_before: fallback, days_remaining: remaining, source: :heuristic)
  end
end

#capture_options(order:) ⇒ Payment::StrategyResolver::CaptureOptions

Decide whether the next capture against order should be marked
final_capture (releasing remaining auth) or kept open. Returns a
final_capture: true option for non-CC payments, single deliveries,
or gateways without multicapture; otherwise leaves the auth open.

Parameters:

Returns:



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'app/services/payment/strategy_resolver.rb', line 51

def capture_options(order:)
  unless credit_card? || paypal?
    return CaptureOptions.new(final_capture: true, reason: 'non-gateway payment')
  end

  if credit_card? && !supports_multicapture?
    return CaptureOptions.new(final_capture: true, reason: 'multicapture not available')
  end

  if cross_order_siblings_pending?
    return CaptureOptions.new(final_capture: false, reason: 'cross-order siblings still authorized')
  end

  unshipped = order.deliveries.select { |d| !d.shipped? && !d.invoiced? }
  if unshipped.size > 1
    CaptureOptions.new(final_capture: false, reason: "#{unshipped.size} deliveries still pending")
  else
    CaptureOptions.new(final_capture: true, reason: 'last or only delivery')
  end
end

#increase_strategy(new_amount) ⇒ Payment::StrategyResolver::Result

Pick the cheapest path to increase a Payment's authorized total
to new_amount: an incremental authorization on Stripe when the PI
supports it, a void-and-reauth against the stored vault otherwise,
or a brand-new payment if no vault is on file.

Parameters:

  • new_amount (BigDecimal, Numeric)

Returns:



79
80
81
82
83
84
85
86
87
88
89
90
# File 'app/services/payment/strategy_resolver.rb', line 79

def increase_strategy(new_amount)
  return Result.new(strategy: :new_payment, reason: 'not a credit card') unless credit_card?

  if supports_incremental_authorization? && @payment.authorized? && @payment.stripe_payment_intent_id.present?
    Result.new(strategy: :increment, payment: @payment, reason: 'incremental authorization available')
  elsif @payment.credit_card_vault.present?
    Result.new(strategy: :void_and_reauth, payment: @payment, vault: @payment.credit_card_vault,
               reason: 'no incremental auth; vault available for void+reauth')
  else
    Result.new(strategy: :new_payment, reason: 'no incremental auth and no vault')
  end
end

#needs_reauthorization?Boolean

Returns:

  • (Boolean)


92
93
94
95
96
97
98
99
100
101
102
# File 'app/services/payment/strategy_resolver.rb', line 92

def needs_reauthorization?
  return ReauthResult.new(reauth_needed: false, reason: 'not authorized') unless @payment.authorized?
  return ReauthResult.new(reauth_needed: false, reason: 'not a credit card') unless credit_card?

  deadline = authorization_deadline
  if deadline.capture_before.present? && deadline.days_remaining <= REAUTH_BUFFER_DAYS
    ReauthResult.new(reauth_needed: true, reason: "capture_before in #{deadline.days_remaining} days (#{deadline.source})")
  else
    ReauthResult.new(reauth_needed: false, reason: "#{deadline.days_remaining} days remaining (#{deadline.source})")
  end
end

#supports_extended_authorization?Boolean

Returns:

  • (Boolean)


40
41
42
# File 'app/services/payment/strategy_resolver.rb', line 40

def supports_extended_authorization?
  credit_card? && capability_value(:extended_authorization) == 'enabled'
end

#supports_incremental_authorization?Boolean

Returns:

  • (Boolean)


36
37
38
# File 'app/services/payment/strategy_resolver.rb', line 36

def supports_incremental_authorization?
  credit_card? && capability_value(:incremental_authorization) == 'available'
end

#supports_multicapture?Boolean

Returns:

  • (Boolean)


32
33
34
# File 'app/services/payment/strategy_resolver.rb', line 32

def supports_multicapture?
  credit_card? && capability_value(:multicapture) == 'available'
end