Adds functionality to pay users (Castle pays)

Implements the ability for the super user (Castle) to pay other users for expenses or liabilities.

Introduces a new `PayUser` model to represent these payments, along with API endpoints to process and record them.

Integrates a "Pay User" button into the user list, allowing the super user to initiate payments through either lightning or manual methods (cash, bank transfer, check).

Adds UI elements and logic for handling both lightning payments (generating invoices and paying them) and manual payment recording.

This functionality allows Castle to manage and settle debts with its users directly through the application.
This commit is contained in:
padreug 2025-10-23 10:01:33 +02:00
parent f0257e7c7f
commit 60aba90e00
4 changed files with 560 additions and 1 deletions

View file

@ -192,6 +192,7 @@
</template>
<template v-slot:body-cell-actions="props">
<q-td :props="props">
<!-- User owes Castle (negative balance) - Castle receives payment -->
<q-btn
v-if="props.row.balance < 0"
flat
@ -201,7 +202,19 @@
icon="payments"
@click="showSettleReceivableDialog(props.row)"
>
<q-tooltip>Settle receivable (user pays)</q-tooltip>
<q-tooltip>Settle receivable (user pays castle)</q-tooltip>
</q-btn>
<!-- Castle owes User (positive balance) - Castle pays user -->
<q-btn
v-if="props.row.balance > 0"
flat
dense
size="sm"
color="positive"
icon="send"
@click="showPayUserDialog(props.row)"
>
<q-tooltip>Pay user (castle pays user)</q-tooltip>
</q-btn>
</q-td>
</template>
@ -1254,4 +1267,126 @@
</q-card>
</q-dialog>
<!-- Pay User Dialog (Castle pays user - Super User Only) -->
<q-dialog v-model="payUserDialog.show" position="top">
<q-card class="q-pa-md" style="min-width: 400px">
<q-form @submit="submitPayUser">
<h6 class="q-my-none q-mb-md">Pay User</h6>
<div class="q-mb-md">
<div class="text-subtitle2">User</div>
<div>{% raw %}{{ payUserDialog.username }}{% endraw %}</div>
<div class="text-caption text-grey">{% raw %}{{ payUserDialog.user_id }}{% endraw %}</div>
</div>
<div class="q-mb-md">
<div class="text-subtitle2">Amount Castle Owes</div>
<div class="text-positive text-h6">
{% raw %}{{ formatSats(payUserDialog.maxAmount) }}{% endraw %} sats
</div>
<div v-if="payUserDialog.fiatCurrency && payUserDialog.maxAmountFiat" class="text-caption">
{% raw %}{{ formatFiat(payUserDialog.maxAmountFiat, payUserDialog.fiatCurrency) }}{% endraw %}
</div>
</div>
<q-input
filled
dense
v-model.number="payUserDialog.amount"
type="number"
:label="paymentAmountLabel"
hint="Amount castle is paying (max: owed amount)"
:max="paymentMaxAmount"
:step="paymentAmountStep"
:rules="[
val => val !== null && val !== undefined && val !== '' || 'Amount is required',
val => val > 0 || 'Amount must be positive',
val => val <= paymentMaxAmount || 'Cannot exceed owed amount'
]"
></q-input>
<q-select
filled
dense
v-model="payUserDialog.payment_method"
:options="[
{label: 'Lightning Payment', value: 'lightning'},
{label: 'Cash', value: 'cash'},
{label: 'Bank Transfer', value: 'bank_transfer'},
{label: 'Check', value: 'check'},
{label: 'Other', value: 'other'}
]"
option-label="label"
option-value="value"
emit-value
map-options
label="Payment Method *"
:rules="[val => !!val || 'Payment method is required']"
></q-select>
<q-input
v-if="payUserDialog.payment_method !== 'lightning'"
filled
dense
v-model="payUserDialog.description"
type="text"
label="Description (optional)"
:placeholder="payUserDialog.payment_method === 'cash' ?
`Cash payment to ${payUserDialog.username}` :
payUserDialog.payment_method === 'bank_transfer' ?
`Bank transfer to ${payUserDialog.username}` :
payUserDialog.payment_method === 'check' ?
`Check payment to ${payUserDialog.username}` :
`Payment to ${payUserDialog.username}`"
hint="Auto-generated if left empty"
></q-input>
<q-input
v-if="payUserDialog.payment_method !== 'lightning'"
filled
dense
v-model="payUserDialog.reference"
type="text"
label="Reference (optional)"
hint="Receipt number, transaction ID, etc."
></q-input>
<!-- Show success message if lightning payment was made -->
<div v-if="payUserDialog.paymentSuccess" class="q-mt-md q-pa-md bg-positive text-white rounded-borders">
<q-icon name="check_circle" size="md" class="q-mr-sm"></q-icon>
Payment sent successfully!
</div>
<div class="row q-mt-md q-gutter-sm">
<!-- For lightning: send payment button -->
<q-btn
v-if="payUserDialog.payment_method === 'lightning'"
unelevated
color="positive"
@click="sendLightningPayment"
:loading="payUserDialog.loading"
:disable="payUserDialog.paymentSuccess"
>
Send Lightning Payment
</q-btn>
<!-- For non-lightning: record manual payment button -->
<q-btn
v-if="payUserDialog.payment_method !== 'lightning'"
unelevated
color="positive"
type="submit"
:loading="payUserDialog.loading"
>
Record Payment
</q-btn>
<q-btn v-close-popup flat color="grey">
{% raw %}{{ payUserDialog.paymentSuccess ? 'Close' : 'Cancel' }}{% endraw %}
</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
{% endblock %}