Class: Spree::Admin::BankTransfersController

Inherits:
BaseController
  • Object
show all
Includes:
Pagy::Method
Defined in:
app/controllers/spree/admin/bank_transfers_controller.rb

Constant Summary collapse

MANUAL_PROVIDER =

Synthetic provider for transfers an admin typed in by hand. Keeps them distinguishable from anything a real reconciler delivered, and gives the (provider, provider_transaction_id) uniqueness index its own namespace so a hand-entered row can never collide with a provider-issued transaction id.

'manual'.freeze

Instance Method Summary collapse

Instance Method Details

#applyObject



107
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
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'app/controllers/spree/admin/bank_transfers_controller.rb', line 107

def apply
  if @transfer.applied?
    flash[:error] = 'That transfer has already been applied.'
    return redirect_to spree.admin_bank_transfers_path
  end

  payment_session = find_bank_transfer_payment_session(params[:payment_session_id])

  if payment_session.nil?
    flash[:error] = 'That payment session could not be found.'
    return redirect_to spree.admin_bank_transfers_path
  end

  if gateway_mismatch?(payment_session)
    flash[:error] = 'That transfer was received on a different bank-transfer gateway ' \
                     'and cannot be applied to this session.'
    return redirect_to spree.admin_bank_transfers_path
  end

  # I3: the refusal now hands back the pair that needs confirming, and
  # the queue renders a distinct, explicitly-labelled confirm button
  # only for that pair. Confirmation is therefore a genuine second
  # step -- a deliberate act after seeing the numbers -- rather than
  # something the view pre-granted before the admin looked at anything.
  if money_mismatch?(payment_session) && !confirmed_mismatch?
    flash[:error] = "Amount/currency mismatch: the transfer is #{@transfer.money}, " \
                     "the session expects #{payment_session.money}. " \
                     'Confirm to apply anyway.'
    return redirect_to spree.admin_bank_transfers_path(
      confirm_transfer_id: @transfer.id,
      confirm_payment_session_id: payment_session.id
    )
  end

  Spree::BankPayments::ApplyTransfer.call(
    transfer: @transfer,
    payment_session: payment_session,
    applied_by: try_spree_current_user
  )

  flash[:success] = 'Payment applied.'
  redirect_to spree.admin_bank_transfers_path
end

#createObject

Deliberately builds a TransferData and hands it to IngestTransfer rather than writing an IncomingTransfer directly: matching lives in exactly one place, so a hand-recorded transfer behaves identically to a provider-delivered one -- exact match auto-applies, anything else lands in the queue for a human.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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
100
101
102
103
104
105
# File 'app/controllers/spree/admin/bank_transfers_controller.rb', line 53

def create
  @transfer_form = transfer_form_params

  error = transfer_form_error(@transfer_form)
  if error
    flash.now[:error] = error
    return render :new, status: :unprocessable_entity
  end

  payment_method = @payment_methods.detect { |pm| pm.id.to_s == @transfer_form[:payment_method_id].to_s }
  transaction_id = manual_transaction_id(payment_method, @transfer_form)

  # The idempotency guard is IngestTransfer's find_or_create_by! on
  # (provider, provider_transaction_id). A hand-typed transfer has no
  # provider-issued id to key on, so we derive a deterministic one from
  # the submitted facts: an admin who double-submits the form (double
  # click, browser back-and-resubmit) reproduces the same digest, hits
  # the existing row, and applies nothing a second time. The trade is
  # that two genuinely distinct but byte-identical transfers on the
  # same day collapse into one -- rare, and far safer than the
  # alternative of crediting an order twice.
  already_recorded = Spree::BankPayments::IncomingTransfer.exists?(
    provider: MANUAL_PROVIDER, provider_transaction_id: transaction_id
  )

  transfer = Spree::BankPayments::IngestTransfer.new(
    payment_method: payment_method,
    transfer_data: Spree::BankPayments::TransferData.new(
      provider: MANUAL_PROVIDER,
      provider_transaction_id: transaction_id,
      amount: BigDecimal(@transfer_form[:amount].to_s),
      currency: @transfer_form[:currency].to_s.strip.upcase,
      reference: @transfer_form[:reference].to_s.strip,
      payer_name: @transfer_form[:payer_name].to_s.strip.presence,
      occurred_at: parse_occurred_at(@transfer_form[:occurred_at]),
      raw: {
        'source' => 'admin_manual_entry',
        'recorded_by_id' => try_spree_current_user&.id
      }
    )
  ).call

  flash[:success] =
    if already_recorded
      'That transfer was already recorded — nothing was applied a second time.'
    elsif transfer.applied?
      "Transfer recorded and applied to order #{transfer.payment_session&.order&.number}."
    else
      'Transfer recorded. It is waiting in the queue below for a match.'
    end

  redirect_to spree.admin_bank_transfers_path
end

#ignoreObject



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'app/controllers/spree/admin/bank_transfers_controller.rb', line 151

def ignore
  if @transfer.applied?
    flash[:error] = 'That transfer has already been applied and cannot be ignored.'
    return redirect_to spree.admin_bank_transfers_path
  end

  reason = params[:reason].to_s.strip
  if reason.blank?
    flash[:error] = 'A reason is required to ignore a transfer.'
    return redirect_to spree.admin_bank_transfers_path
  end

  @transfer.update!(state: 'ignored', ignored_reason: reason)

  flash[:success] = 'Transfer ignored.'
  redirect_to spree.admin_bank_transfers_path
end

#indexObject



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'app/controllers/spree/admin/bank_transfers_controller.rb', line 16

def index
  # includes(:bank_account) because the view asks every row whether its
  # account is pooled. The association is declared `-> { with_deleted }`,
  # which preloads fine, and it must stay that way: a soft-deleted
  # account would otherwise resolve to nil and silently drop the pooled
  # warning on exactly the rows still quoting its coordinates.
  @pagy, @transfers = pagy(
    Spree::BankPayments::IncomingTransfer.unmatched.
      includes(:bank_account).order(occurred_at: :desc)
  )

  @suggestions = @transfers.each_with_object({}) do |transfer, acc|
    acc[transfer.id] = Spree::BankPayments::SuggestMatches.new(transfer: transfer).call
  end

  # Set only by #apply's mismatch refusal (see I3). Identifies the one
  # transfer/session pair the admin has already been shown the numbers
  # for and may now confirm.
  @confirm_transfer_id = params[:confirm_transfer_id].presence&.to_i
  @confirm_payment_session_id = params[:confirm_payment_session_id].presence&.to_i
end

#newObject

"Record a received transfer". The Manual reconciler -- the default, and the only one this gem ships -- returns [] from #poll and nil from #parse_webhook, so nothing else can ever create an IncomingTransfer. Without this form a store on the shipped configuration takes a customer's money and has no action available to record it, and ExpireSessionsJob cancels the order a few days later.



44
45
46
# File 'app/controllers/spree/admin/bank_transfers_controller.rb', line 44

def new
  @transfer_form = blank_transfer_form
end