Adds manual payment request functionality

Enables users to request manual payments from the Castle and provides admin functions to approve or reject these requests.

Introduces the `manual_payment_requests` table and related CRUD operations.
Adds API endpoints for creating, retrieving, approving, and rejecting manual payment requests.
Updates the UI to allow users to request payments and for admins to review pending requests.
This commit is contained in:
padreug 2025-10-22 18:02:07 +02:00
parent 3a26d963dc
commit c2d9b39f29
5 changed files with 520 additions and 11 deletions

View file

@ -90,14 +90,22 @@
<div class="text-subtitle2" v-else>
{% raw %}{{ balance.balance >= 0 ? 'Castle owes you' : 'You owe Castle' }}{% endraw %}
</div>
<q-btn
v-if="balance.balance < 0 && !isSuperUser"
color="primary"
class="q-mt-md"
@click="showPayBalanceDialog"
>
Pay Balance
</q-btn>
<div class="q-mt-md q-gutter-sm">
<q-btn
v-if="balance.balance < 0 && !isSuperUser"
color="primary"
@click="showPayBalanceDialog"
>
Pay Balance
</q-btn>
<q-btn
v-if="balance.balance > 0 && !isSuperUser"
color="secondary"
@click="showManualPaymentDialog"
>
Request Manual Payment
</q-btn>
</div>
</div>
<div v-else>
<q-spinner color="primary" size="md"></q-spinner>
@ -145,6 +153,49 @@
</q-card-section>
</q-card>
<!-- Pending Manual Payment Requests (Super User Only) -->
<q-card v-if="isSuperUser && pendingManualPaymentRequests.length > 0">
<q-card-section>
<h6 class="q-my-none q-mb-md">Pending Manual Payment Requests</h6>
<q-list separator>
<q-item v-for="request in pendingManualPaymentRequests" :key="request.id">
<q-item-section>
<q-item-label>{% raw %}{{ request.description }}{% endraw %}</q-item-label>
<q-item-label caption>
User: {% raw %}{{ request.user_id.substring(0, 16) }}...{% endraw %}
</q-item-label>
<q-item-label caption>
Requested: {% raw %}{{ formatDate(request.created_at) }}{% endraw %}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label>{% raw %}{{ formatSats(request.amount) }} sats{% endraw %}</q-item-label>
</q-item-section>
<q-item-section side>
<div class="q-gutter-xs">
<q-btn
size="sm"
color="positive"
@click="approveManualPaymentRequest(request.id)"
:loading="request.approving"
>
Approve
</q-btn>
<q-btn
size="sm"
color="negative"
@click="rejectManualPaymentRequest(request.id)"
:loading="request.rejecting"
>
Reject
</q-btn>
</div>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
</q-card>
<!-- Quick Actions -->
<q-card>
<q-card-section>
@ -411,6 +462,57 @@
</q-card>
</q-dialog>
<!-- Manual Payment Request Dialog -->
<q-dialog v-model="manualPaymentDialog.show" position="top">
<q-card v-if="manualPaymentDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card" style="min-width: 400px">
<q-form @submit="submitManualPaymentRequest" class="q-gutter-md">
<div class="text-h6 q-mb-md">Request Manual Payment</div>
<div class="text-caption text-grey q-mb-md">
Request the Castle to pay you manually (cash, bank transfer, etc.) to settle your balance.
</div>
<div v-if="balance" class="q-mb-md">
<div>
Current balance: <strong>{% raw %}{{ formatSats(Math.abs(balance.balance)) }}{% endraw %} sats</strong>
</div>
<div v-if="balance.fiat_balances && Object.keys(balance.fiat_balances).length > 0" class="text-body2 q-mt-xs">
<span v-for="(amount, currency) in balance.fiat_balances" :key="currency" class="q-mr-md">
<strong>{% raw %}{{ formatFiat(Math.abs(amount), currency) }}{% endraw %}</strong>
</span>
</div>
</div>
<q-input
filled
dense
v-model.number="manualPaymentDialog.amount"
type="number"
label="Amount to request (sats) *"
min="1"
:max="balance ? Math.abs(balance.balance) : 0"
:rules="[val => !!val || 'Amount is required', val => val > 0 || 'Amount must be positive']"
></q-input>
<q-input
filled
dense
v-model="manualPaymentDialog.description"
type="text"
label="Description *"
:rules="[val => !!val || 'Description is required']"
></q-input>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit" :loading="manualPaymentDialog.loading">
Submit Request
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-sm">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<!-- Settings Dialog -->
<q-dialog v-model="settingsDialog.show" position="top">
<q-card v-if="settingsDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">