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:
parent
3a26d963dc
commit
c2d9b39f29
5 changed files with 520 additions and 11 deletions
140
crud.py
140
crud.py
|
|
@ -557,3 +557,143 @@ async def get_all_user_wallet_settings() -> list[StoredUserWalletSettings]:
|
|||
{},
|
||||
StoredUserWalletSettings,
|
||||
)
|
||||
|
||||
|
||||
# ===== MANUAL PAYMENT REQUESTS =====
|
||||
|
||||
|
||||
async def create_manual_payment_request(
|
||||
user_id: str, amount: int, description: str
|
||||
) -> "ManualPaymentRequest":
|
||||
"""Create a new manual payment request"""
|
||||
from .models import ManualPaymentRequest
|
||||
|
||||
request_id = urlsafe_short_hash()
|
||||
request = ManualPaymentRequest(
|
||||
id=request_id,
|
||||
user_id=user_id,
|
||||
amount=amount,
|
||||
description=description,
|
||||
status="pending",
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO manual_payment_requests (id, user_id, amount, description, status, created_at)
|
||||
VALUES (:id, :user_id, :amount, :description, :status, :created_at)
|
||||
""",
|
||||
{
|
||||
"id": request.id,
|
||||
"user_id": request.user_id,
|
||||
"amount": request.amount,
|
||||
"description": request.description,
|
||||
"status": request.status,
|
||||
"created_at": request.created_at,
|
||||
},
|
||||
)
|
||||
|
||||
return request
|
||||
|
||||
|
||||
async def get_manual_payment_request(request_id: str) -> Optional["ManualPaymentRequest"]:
|
||||
"""Get a manual payment request by ID"""
|
||||
from .models import ManualPaymentRequest
|
||||
|
||||
return await db.fetchone(
|
||||
"SELECT * FROM manual_payment_requests WHERE id = :id",
|
||||
{"id": request_id},
|
||||
ManualPaymentRequest,
|
||||
)
|
||||
|
||||
|
||||
async def get_user_manual_payment_requests(
|
||||
user_id: str, limit: int = 100
|
||||
) -> list["ManualPaymentRequest"]:
|
||||
"""Get all manual payment requests for a specific user"""
|
||||
from .models import ManualPaymentRequest
|
||||
|
||||
return await db.fetchall(
|
||||
"""
|
||||
SELECT * FROM manual_payment_requests
|
||||
WHERE user_id = :user_id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT :limit
|
||||
""",
|
||||
{"user_id": user_id, "limit": limit},
|
||||
ManualPaymentRequest,
|
||||
)
|
||||
|
||||
|
||||
async def get_all_manual_payment_requests(
|
||||
status: Optional[str] = None, limit: int = 100
|
||||
) -> list["ManualPaymentRequest"]:
|
||||
"""Get all manual payment requests, optionally filtered by status"""
|
||||
from .models import ManualPaymentRequest
|
||||
|
||||
if status:
|
||||
return await db.fetchall(
|
||||
"""
|
||||
SELECT * FROM manual_payment_requests
|
||||
WHERE status = :status
|
||||
ORDER BY created_at DESC
|
||||
LIMIT :limit
|
||||
""",
|
||||
{"status": status, "limit": limit},
|
||||
ManualPaymentRequest,
|
||||
)
|
||||
else:
|
||||
return await db.fetchall(
|
||||
"""
|
||||
SELECT * FROM manual_payment_requests
|
||||
ORDER BY created_at DESC
|
||||
LIMIT :limit
|
||||
""",
|
||||
{"limit": limit},
|
||||
ManualPaymentRequest,
|
||||
)
|
||||
|
||||
|
||||
async def approve_manual_payment_request(
|
||||
request_id: str, reviewed_by: str, journal_entry_id: str
|
||||
) -> Optional["ManualPaymentRequest"]:
|
||||
"""Approve a manual payment request"""
|
||||
from .models import ManualPaymentRequest
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE manual_payment_requests
|
||||
SET status = 'approved', reviewed_at = :reviewed_at, reviewed_by = :reviewed_by, journal_entry_id = :journal_entry_id
|
||||
WHERE id = :id
|
||||
""",
|
||||
{
|
||||
"id": request_id,
|
||||
"reviewed_at": datetime.now(),
|
||||
"reviewed_by": reviewed_by,
|
||||
"journal_entry_id": journal_entry_id,
|
||||
},
|
||||
)
|
||||
|
||||
return await get_manual_payment_request(request_id)
|
||||
|
||||
|
||||
async def reject_manual_payment_request(
|
||||
request_id: str, reviewed_by: str
|
||||
) -> Optional["ManualPaymentRequest"]:
|
||||
"""Reject a manual payment request"""
|
||||
from .models import ManualPaymentRequest
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE manual_payment_requests
|
||||
SET status = 'rejected', reviewed_at = :reviewed_at, reviewed_by = :reviewed_by
|
||||
WHERE id = :id
|
||||
""",
|
||||
{
|
||||
"id": request_id,
|
||||
"reviewed_at": datetime.now(),
|
||||
"reviewed_by": reviewed_by,
|
||||
},
|
||||
)
|
||||
|
||||
return await get_manual_payment_request(request_id)
|
||||
|
|
|
|||
|
|
@ -155,3 +155,36 @@ async def m004_user_wallet_settings(db):
|
|||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m005_manual_payment_requests(db):
|
||||
"""
|
||||
Create manual_payment_requests table for user payment requests to Castle.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE manual_payment_requests (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
reviewed_at TIMESTAMP,
|
||||
reviewed_by TEXT,
|
||||
journal_entry_id TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE INDEX idx_manual_payment_requests_user_id ON manual_payment_requests (user_id);
|
||||
"""
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE INDEX idx_manual_payment_requests_status ON manual_payment_requests (status);
|
||||
"""
|
||||
)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,14 @@ window.app = Vue.createApp({
|
|||
reference: '',
|
||||
currency: null,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
manualPaymentDialog: {
|
||||
show: false,
|
||||
amount: null,
|
||||
description: '',
|
||||
loading: false
|
||||
},
|
||||
manualPaymentRequests: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -104,6 +111,9 @@ window.app = Vue.createApp({
|
|||
})
|
||||
})
|
||||
return options
|
||||
},
|
||||
pendingManualPaymentRequests() {
|
||||
return this.manualPaymentRequests.filter(r => r.status === 'pending')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -422,13 +432,101 @@ window.app = Vue.createApp({
|
|||
}, 2000)
|
||||
},
|
||||
showManualPaymentOption() {
|
||||
// TODO: Show manual payment request dialog
|
||||
// This is for when user wants to pay their debt manually
|
||||
// For now, just notify them to contact castle
|
||||
this.$q.notify({
|
||||
type: 'info',
|
||||
message: 'Manual payment feature coming soon!',
|
||||
message: 'Please contact Castle directly to arrange manual payment.',
|
||||
timeout: 3000
|
||||
})
|
||||
},
|
||||
showManualPaymentDialog() {
|
||||
// This is for when Castle owes user and they want to request manual payment
|
||||
this.manualPaymentDialog.amount = Math.abs(this.balance.balance)
|
||||
this.manualPaymentDialog.description = ''
|
||||
this.manualPaymentDialog.show = true
|
||||
},
|
||||
async submitManualPaymentRequest() {
|
||||
this.manualPaymentDialog.loading = true
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
'/castle/api/v1/manual-payment-request',
|
||||
this.g.user.wallets[0].inkey,
|
||||
{
|
||||
amount: this.manualPaymentDialog.amount,
|
||||
description: this.manualPaymentDialog.description
|
||||
}
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Manual payment request submitted successfully!'
|
||||
})
|
||||
this.manualPaymentDialog.show = false
|
||||
this.manualPaymentDialog.amount = null
|
||||
this.manualPaymentDialog.description = ''
|
||||
await this.loadManualPaymentRequests()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.manualPaymentDialog.loading = false
|
||||
}
|
||||
},
|
||||
async loadManualPaymentRequests() {
|
||||
try {
|
||||
// If super user, load all requests; otherwise load user's own requests
|
||||
const endpoint = this.isSuperUser
|
||||
? '/castle/api/v1/manual-payment-requests/all'
|
||||
: '/castle/api/v1/manual-payment-requests'
|
||||
const key = this.isSuperUser
|
||||
? this.g.user.wallets[0].adminkey
|
||||
: this.g.user.wallets[0].inkey
|
||||
|
||||
const response = await LNbits.api.request(
|
||||
'GET',
|
||||
endpoint,
|
||||
key,
|
||||
this.isSuperUser ? {status: 'pending'} : {}
|
||||
)
|
||||
this.manualPaymentRequests = response.data
|
||||
} catch (error) {
|
||||
console.error('Error loading manual payment requests:', error)
|
||||
}
|
||||
},
|
||||
async approveManualPaymentRequest(requestId) {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
`/castle/api/v1/manual-payment-requests/${requestId}/approve`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Manual payment request approved!'
|
||||
})
|
||||
await this.loadManualPaymentRequests()
|
||||
await this.loadBalance()
|
||||
await this.loadTransactions()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
async rejectManualPaymentRequest(requestId) {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
`/castle/api/v1/manual-payment-requests/${requestId}/reject`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Manual payment request rejected'
|
||||
})
|
||||
await this.loadManualPaymentRequests()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text)
|
||||
this.$q.notify({
|
||||
|
|
@ -558,6 +656,7 @@ window.app = Vue.createApp({
|
|||
await this.loadTransactions()
|
||||
await this.loadAccounts()
|
||||
await this.loadCurrencies()
|
||||
await this.loadManualPaymentRequests()
|
||||
// Load users if super user (for receivable dialog)
|
||||
if (this.isSuperUser) {
|
||||
await this.loadUsers()
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
135
views_api.py
135
views_api.py
|
|
@ -11,20 +11,26 @@ from lnbits.decorators import (
|
|||
from lnbits.utils.exchange_rates import allowed_currencies, fiat_amount_as_satoshis
|
||||
|
||||
from .crud import (
|
||||
approve_manual_payment_request,
|
||||
create_account,
|
||||
create_journal_entry,
|
||||
create_manual_payment_request,
|
||||
get_account,
|
||||
get_account_balance,
|
||||
get_account_by_name,
|
||||
get_account_transactions,
|
||||
get_all_accounts,
|
||||
get_all_journal_entries,
|
||||
get_all_manual_payment_requests,
|
||||
get_all_user_balances,
|
||||
get_all_user_wallet_settings,
|
||||
get_journal_entries_by_user,
|
||||
get_journal_entry,
|
||||
get_manual_payment_request,
|
||||
get_or_create_user_account,
|
||||
get_user_balance,
|
||||
get_user_manual_payment_requests,
|
||||
reject_manual_payment_request,
|
||||
)
|
||||
from .models import (
|
||||
Account,
|
||||
|
|
@ -33,9 +39,11 @@ from .models import (
|
|||
CreateAccount,
|
||||
CreateEntryLine,
|
||||
CreateJournalEntry,
|
||||
CreateManualPaymentRequest,
|
||||
ExpenseEntry,
|
||||
GeneratePaymentInvoice,
|
||||
JournalEntry,
|
||||
ManualPaymentRequest,
|
||||
ReceivableEntry,
|
||||
RecordPayment,
|
||||
RevenueEntry,
|
||||
|
|
@ -741,3 +749,130 @@ async def api_update_user_wallet(
|
|||
detail="User wallet ID is required",
|
||||
)
|
||||
return await update_user_wallet(user.id, data)
|
||||
|
||||
|
||||
# ===== MANUAL PAYMENT REQUESTS =====
|
||||
|
||||
|
||||
@castle_api_router.post("/api/v1/manual-payment-request")
|
||||
async def api_create_manual_payment_request(
|
||||
data: CreateManualPaymentRequest,
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
) -> ManualPaymentRequest:
|
||||
"""Create a manual payment request for the Castle to review"""
|
||||
return await create_manual_payment_request(
|
||||
wallet.wallet.user, data.amount, data.description
|
||||
)
|
||||
|
||||
|
||||
@castle_api_router.get("/api/v1/manual-payment-requests")
|
||||
async def api_get_manual_payment_requests(
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
) -> list[ManualPaymentRequest]:
|
||||
"""Get manual payment requests for the current user"""
|
||||
return await get_user_manual_payment_requests(wallet.wallet.user)
|
||||
|
||||
|
||||
@castle_api_router.get("/api/v1/manual-payment-requests/all")
|
||||
async def api_get_all_manual_payment_requests(
|
||||
status: str = None,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> list[ManualPaymentRequest]:
|
||||
"""Get all manual payment requests (Castle admin only)"""
|
||||
await check_super_user(wallet.wallet.user)
|
||||
return await get_all_manual_payment_requests(status)
|
||||
|
||||
|
||||
@castle_api_router.post("/api/v1/manual-payment-requests/{request_id}/approve")
|
||||
async def api_approve_manual_payment_request(
|
||||
request_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> ManualPaymentRequest:
|
||||
"""Approve a manual payment request and create accounting entry (Castle admin only)"""
|
||||
from lnbits.settings import settings as lnbits_settings
|
||||
|
||||
await check_super_user(wallet.wallet.user)
|
||||
|
||||
# Get the request
|
||||
request = await get_manual_payment_request(request_id)
|
||||
if not request:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Manual payment request not found",
|
||||
)
|
||||
|
||||
if request.status != "pending":
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Request already {request.status}",
|
||||
)
|
||||
|
||||
# Get castle wallet from settings
|
||||
castle_wallet_id = await check_castle_wallet_configured()
|
||||
|
||||
# Get or create liability account for user (castle owes the user)
|
||||
liability_account = await get_or_create_user_account(
|
||||
request.user_id, AccountType.LIABILITY, "Accounts Payable"
|
||||
)
|
||||
|
||||
# Get the Lightning asset account
|
||||
lightning_account = await get_account_by_name("Lightning Balance")
|
||||
if not lightning_account:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Lightning Balance account not found",
|
||||
)
|
||||
|
||||
# Create journal entry: Debit Lightning (asset decreased), Credit Accounts Payable (liability increased)
|
||||
# This records that the Castle paid the user, reducing the lightning balance and reducing what castle owes
|
||||
journal_entry = await create_journal_entry(
|
||||
CreateJournalEntry(
|
||||
description=f"Manual payment to user: {request.description}",
|
||||
reference=f"MPR-{request.id}",
|
||||
lines=[
|
||||
CreateEntryLine(
|
||||
account_id=liability_account.id,
|
||||
debit=request.amount, # Decrease liability (castle owes less)
|
||||
credit=0,
|
||||
description="Payment to user",
|
||||
),
|
||||
CreateEntryLine(
|
||||
account_id=lightning_account.id,
|
||||
debit=0,
|
||||
credit=request.amount, # Decrease asset (lightning balance reduced)
|
||||
description="Payment from castle",
|
||||
),
|
||||
],
|
||||
),
|
||||
castle_wallet_id,
|
||||
)
|
||||
|
||||
# Approve the request
|
||||
return await approve_manual_payment_request(
|
||||
request_id, wallet.wallet.user, journal_entry.id
|
||||
)
|
||||
|
||||
|
||||
@castle_api_router.post("/api/v1/manual-payment-requests/{request_id}/reject")
|
||||
async def api_reject_manual_payment_request(
|
||||
request_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> ManualPaymentRequest:
|
||||
"""Reject a manual payment request (Castle admin only)"""
|
||||
await check_super_user(wallet.wallet.user)
|
||||
|
||||
# Get the request
|
||||
request = await get_manual_payment_request(request_id)
|
||||
if not request:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Manual payment request not found",
|
||||
)
|
||||
|
||||
if request.status != "pending":
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Request already {request.status}",
|
||||
)
|
||||
|
||||
return await reject_manual_payment_request(request_id, wallet.wallet.user)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue