asxpio/views/admin/invoices/payout_addresses.erb
Sergei Poljanski 33ba7650c3
invoices: self-contained payout addresses
Crypto options were limited to a hard-coded asset list, with one
address per coin in a side table. A payout address now carries
everything an invoice needs: ticker, name, QR URI scheme, amount
parameter, CoinGecko id, address and an active flag. Any coin can be
added, several addresses per ticker are fine, and known tickers fill the
QR and rate fields from the built-in templates.

The new-invoice form picks a payout address. The invoice snapshots the
ticker, address, name, scheme and amount parameter, and the PDF and QR
render from that copy, so editing, retiring or deleting a payout entry
never changes an issued invoice. "Fetch live" looks up the rate by the
payout address's CoinGecko id.

Migration 012 keeps the existing rows and backfills both the payout
metadata and the snapshots on existing crypto invoices.

Assisted-by: Claude:opus-5
2026-10-10 09:17:09 +04:00

72 lines
5.3 KiB
Text

<% writable = can?('invoices', 'write') %>
<%== erb :'admin/_nav', layout: false %>
<div class="header">
<h1>Payout addresses</h1>
<p class="tagline"><a href="/admin/invoices">← invoices</a> · crypto payment options for new invoices</p>
</div>
<% if @flash %><div class="form-error-base"><div><%= @flash %></div></div><% end %>
<datalist id="schemes"><% CryptoAsset::KNOWN_SCHEMES.each do |sch| %><option value="<%= sch %>"></option><% end %></datalist>
<datalist id="tickers"><% CryptoAsset::DEFAULTS.each do |code, a| %><option value="<%= code %>"><%= a[:name] %></option><% end %></datalist>
<section class="section">
<form class="invoice-form" method="post" action="/admin/invoices/payout-addresses" autocomplete="off">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<fieldset class="plain" <%= 'disabled' unless writable %>>
<% rows = @rows.map { |r| [r.id.to_s, r] } %>
<% rows << ['new', nil] if writable %>
<% rows.each do |key, r| %>
<div class="payout-card<%= ' inactive' if r && !r.active %><%= ' new' unless r %>">
<div class="payout-head">
<b><%= r ? "#{r.name} · #{r.coin}" : 'Add a payout address' %></b>
<% if r %>
<label><input type="checkbox" name="payout[<%= key %>][active]" value="1" <%= 'checked' if r.active %> /> active</label>
<% else %>
<span class="optional">known tickers fill the rest automatically</span>
<% end %>
</div>
<div class="payout-grid">
<div class="field"><label>Name</label><input type="text" name="payout[<%= key %>][name]" value="<%= r&.name %>" placeholder="Litecoin (Ledger)" maxlength="60" /></div>
<div class="field"><label>Ticker <span class="optional">(on the PDF)</span></label><input type="text" name="payout[<%= key %>][coin]" value="<%= r&.coin %>" list="tickers" placeholder="LTC" class="mono" maxlength="20" <%= 'required' if r %> /></div>
<div class="field"><label>QR scheme <span class="optional">(empty = bare address)</span></label><input type="text" name="payout[<%= key %>][scheme]" value="<%= r&.scheme %>" list="schemes" placeholder="<%= r ? 'none (bare address)' : 'litecoin' %>" class="mono" /></div>
<div class="field">
<label>Amount in QR</label>
<select name="payout[<%= key %>][amount_param]">
<option value="" <%= 'selected' unless r&.amount_param %>>no</option>
<% CryptoAsset::AMOUNT_PARAMS.each do |ap| %><option value="<%= ap %>" <%= 'selected' if r&.amount_param == ap %>><%= ap %></option><% end %>
</select>
</div>
<div class="field"><label>CoinGecko id <span class="optional">(empty = type rate)</span></label><input type="text" name="payout[<%= key %>][coingecko]" value="<%= r&.coingecko %>" placeholder="<%= r ? 'none (type the rate)' : 'litecoin' %>" class="mono" /></div>
<div class="field payout-address"><label>Address</label><input type="text" name="payout[<%= key %>][address]" value="<%= r&.address %>" placeholder="ltc1q…" class="mono" maxlength="128" spellcheck="false" <%= 'required' if r %> /></div>
</div>
<% if r %>
<p class="mono optional qr-line">QR → <%= CryptoQr.uri(scheme: r.scheme, amount_param: r.amount_param, address: r.address, amount: BigDecimal('0.5')) %>
<span class="payout-meta">· updated <%= r.updated_at.strftime('%Y-%m-%d') %> by <%= r.updated_by %></span></p>
<% end %>
</div>
<% end %>
<ul class="optional">
<li><b>QR scheme</b> is the full coin name wallets recognise, without the colon: <span class="mono">litecoin</span>, not <span class="mono">ltc</span>. Leave it empty for tokens (USDT, USDC…): the QR then holds the bare address and the PDF tells the payer to check the network.</li>
<li><b>Amount in QR</b>: <span class="mono">amount</span> for Bitcoin-style coins (BTC, LTC, SOL…), <span class="mono">tx_amount</span> for Monero; needs a scheme.</li>
<li><b>CoinGecko id</b> enables "Fetch live" on the invoice form; it's the slug in the coin's coingecko.com URL.</li>
<li>Invoices copy these fields when issued, so editing, retiring or removing an entry never changes existing invoices.</li>
</ul>
<% if writable %><button type="submit" class="download-button">Save</button><% end %>
</fieldset>
</form>
</section>
<% if can?('invoices', 'delete') && @rows.any? %>
<section class="section">
<h2>Remove</h2>
<div class="invoice-actions">
<% @rows.each do |r| %>
<form method="post" action="/admin/invoices/payout-addresses/<%= r.id %>/delete" onsubmit="return confirm('Remove this payout address? Existing invoices keep their copy.')">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<button type="submit" class="link-button danger"><%= r.coin %> · <%= r.name %></button>
</form>
<% end %>
</div>
</section>
<% end %>