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

@ -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)