371 lines
12 KiB
Python
371 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Dict, Any
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, Depends, Request, Body, Path
|
|
from sqlalchemy.orm import Session
|
|
|
|
from adapters.db.session import get_db
|
|
from app.core.auth_dependency import get_current_user, AuthContext
|
|
from app.core.permissions import require_business_access_dep
|
|
from app.core.responses import success_response, ApiError
|
|
from app.services.wallet_service import (
|
|
get_wallet_overview,
|
|
list_wallet_transactions,
|
|
create_payout_request,
|
|
approve_payout_request,
|
|
cancel_payout_request,
|
|
create_top_up_request,
|
|
get_wallet_metrics,
|
|
get_business_wallet_settings,
|
|
update_business_wallet_settings,
|
|
run_auto_settlement,
|
|
)
|
|
from adapters.db.models.wallet import WalletPayout
|
|
from fastapi import Query
|
|
|
|
|
|
router = APIRouter(prefix="/businesses/{business_id}/wallet", tags=["wallet"])
|
|
|
|
|
|
@router.get(
|
|
"",
|
|
summary="خلاصه کیفپول کسبوکار",
|
|
description="نمایش ماندهها و ارز پایه",
|
|
)
|
|
def get_wallet_overview_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
data = get_wallet_overview(db, business_id)
|
|
return success_response(data, request)
|
|
|
|
@router.post(
|
|
"/top-up",
|
|
summary="ایجاد درخواست افزایش اعتبار",
|
|
description="ایجاد top-up و بازگشت شناسه تراکنش برای هدایت به درگاه",
|
|
)
|
|
def create_top_up_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
payload: Dict[str, Any] = Body(...),
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
data = create_top_up_request(db, business_id, ctx.get_user_id(), payload)
|
|
return success_response(data, request, message="TOPUP_REQUESTED")
|
|
|
|
|
|
@router.get(
|
|
"/transactions",
|
|
summary="لیست تراکنشهای کیفپول",
|
|
description="نمایش تراکنشها به ترتیب نزولی",
|
|
)
|
|
def list_wallet_transactions_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
skip: int = 0,
|
|
limit: int = 50,
|
|
from_date: str | None = None,
|
|
to_date: str | None = None,
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
from_dt = None
|
|
to_dt = None
|
|
try:
|
|
if from_date:
|
|
from_dt = datetime.fromisoformat(from_date)
|
|
if to_date:
|
|
to_dt = datetime.fromisoformat(to_date)
|
|
except Exception:
|
|
from_dt = None
|
|
to_dt = None
|
|
data = list_wallet_transactions(db, business_id, limit=limit, skip=skip, from_date=from_dt, to_date=to_dt)
|
|
return success_response(data, request)
|
|
|
|
|
|
@router.post(
|
|
"/transactions/table",
|
|
summary="لیست تراکنشها برای جدول عمومی (پگینیشن استاندارد)",
|
|
description="سازگار با DataTableWidget: ورودی QueryInfo و خروجی items/total/page/limit",
|
|
)
|
|
def list_wallet_transactions_table_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
payload: Dict[str, Any] = Body(default_factory=dict),
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
# Extract pagination params
|
|
take = int(payload.get("take") or 20)
|
|
skip = int(payload.get("skip") or 0)
|
|
# Optional date range via additional params or filters (best-effort)
|
|
from_dt = None
|
|
to_dt = None
|
|
try:
|
|
filters = payload.get("filters") or []
|
|
# Try to detect date range filters by common keys
|
|
for f in filters:
|
|
prop = str(f.get("property") or "").lower()
|
|
op = str(f.get("operator") or "")
|
|
val = f.get("value")
|
|
if prop in ("created_at", "date", "transaction_date"):
|
|
if op == ">=" and val:
|
|
from_dt = datetime.fromisoformat(str(val))
|
|
elif op == "<=" and val:
|
|
to_dt = datetime.fromisoformat(str(val))
|
|
except Exception:
|
|
from_dt = None
|
|
to_dt = None
|
|
items = list_wallet_transactions(db, business_id, limit=take, skip=skip, from_date=from_dt, to_date=to_dt)
|
|
# Compute total (simple count for business)
|
|
try:
|
|
from adapters.db.models.wallet import WalletTransaction
|
|
q = db.query(WalletTransaction).filter(WalletTransaction.business_id == int(business_id))
|
|
if from_dt is not None:
|
|
from adapters.db.models.wallet import WalletTransaction as WT
|
|
q = q.filter(WT.created_at >= from_dt)
|
|
if to_dt is not None:
|
|
from adapters.db.models.wallet import WalletTransaction as WT2
|
|
q = q.filter(WT2.created_at <= to_dt)
|
|
total = q.count()
|
|
except Exception:
|
|
total = len(items)
|
|
page = (skip // take) + 1 if take > 0 else 1
|
|
total_pages = (total + take - 1) // take if take > 0 else 1
|
|
resp = {
|
|
"items": items,
|
|
"total": total,
|
|
"page": page,
|
|
"limit": take,
|
|
"total_pages": total_pages,
|
|
}
|
|
return success_response(resp, request)
|
|
|
|
|
|
@router.get(
|
|
"/transactions/export",
|
|
summary="خروجی CSV تراکنشهای کیفپول",
|
|
)
|
|
def export_wallet_transactions_csv_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
from_date: str | None = None,
|
|
to_date: str | None = None,
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
):
|
|
from_dt = None
|
|
to_dt = None
|
|
try:
|
|
if from_date:
|
|
from_dt = datetime.fromisoformat(from_date)
|
|
if to_date:
|
|
to_dt = datetime.fromisoformat(to_date)
|
|
except Exception:
|
|
from_dt = None
|
|
to_dt = None
|
|
items = list_wallet_transactions(db, business_id, limit=10000, skip=0, from_date=from_dt, to_date=to_dt)
|
|
# CSV ساده
|
|
import csv
|
|
from io import StringIO
|
|
buf = StringIO()
|
|
writer = csv.writer(buf)
|
|
writer.writerow(["id", "type", "status", "amount", "fee_amount", "description", "document_id", "created_at"])
|
|
for it in items:
|
|
writer.writerow([it.get("id"), it.get("type"), it.get("status"), it.get("amount"), it.get("fee_amount"), (it.get("description") or "").replace("\n", " "), it.get("document_id"), it.get("created_at")])
|
|
csv_data = buf.getvalue().encode("utf-8")
|
|
from fastapi.responses import Response
|
|
return Response(content=csv_data, media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f'attachment; filename="wallet_transactions_{business_id}.csv"'})
|
|
|
|
|
|
@router.get(
|
|
"/metrics/export",
|
|
summary="خروجی CSV خلاصه کیفپول",
|
|
)
|
|
def export_wallet_metrics_csv_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
from_date: str | None = None,
|
|
to_date: str | None = None,
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
):
|
|
from_dt = None
|
|
to_dt = None
|
|
try:
|
|
if from_date:
|
|
from_dt = datetime.fromisoformat(from_date)
|
|
if to_date:
|
|
to_dt = datetime.fromisoformat(to_date)
|
|
except Exception:
|
|
from_dt = None
|
|
to_dt = None
|
|
m = get_wallet_metrics(db, business_id, from_date=from_dt, to_date=to_dt)
|
|
import csv
|
|
from io import StringIO
|
|
buf = StringIO()
|
|
writer = csv.writer(buf)
|
|
writer.writerow(["metric", "value"])
|
|
t = m.get("totals") or {}
|
|
writer.writerow(["gross_in", t.get("gross_in", 0)])
|
|
writer.writerow(["fees_in", t.get("fees_in", 0)])
|
|
writer.writerow(["net_in", t.get("net_in", 0)])
|
|
writer.writerow(["gross_out", t.get("gross_out", 0)])
|
|
writer.writerow(["fees_out", t.get("fees_out", 0)])
|
|
writer.writerow(["net_out", t.get("net_out", 0)])
|
|
b = m.get("balances") or {}
|
|
writer.writerow(["available", b.get("available", 0)])
|
|
writer.writerow(["pending", b.get("pending", 0)])
|
|
csv_data = buf.getvalue().encode("utf-8")
|
|
from fastapi.responses import Response
|
|
return Response(content=csv_data, media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f'attachment; filename="wallet_metrics_{business_id}.csv"'})
|
|
|
|
|
|
@router.post(
|
|
"/payouts",
|
|
summary="ایجاد درخواست تسویه",
|
|
description="ایجاد درخواست تسویه به حساب بانکی مشخص",
|
|
)
|
|
def create_payout_request_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
payload: Dict[str, Any] = Body(...),
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
data = create_payout_request(db, business_id, ctx.get_user_id(), payload)
|
|
return success_response(data, request, message="PAYOUT_REQUESTED")
|
|
|
|
|
|
@router.get(
|
|
"/metrics",
|
|
summary="گزارش خلاصه کیفپول (metrics)",
|
|
description="مبالغ ورودی/خروجی/کارمزد و ماندهها در بازه زمانی",
|
|
)
|
|
def get_wallet_metrics_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
from_date: str | None = None,
|
|
to_date: str | None = None,
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
from_dt = None
|
|
to_dt = None
|
|
try:
|
|
if from_date:
|
|
from_dt = datetime.fromisoformat(from_date)
|
|
if to_date:
|
|
to_dt = datetime.fromisoformat(to_date)
|
|
except Exception:
|
|
from_dt = None
|
|
to_dt = None
|
|
data = get_wallet_metrics(db, business_id, from_date=from_dt, to_date=to_dt)
|
|
return success_response(data, request)
|
|
|
|
|
|
@router.get(
|
|
"/settings",
|
|
summary="تنظیمات کیفپول کسبوکار",
|
|
)
|
|
def get_wallet_settings_business_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
data = get_business_wallet_settings(db, business_id)
|
|
return success_response(data, request)
|
|
|
|
|
|
@router.put(
|
|
"/settings",
|
|
summary="ویرایش تنظیمات کیفپول کسبوکار",
|
|
)
|
|
def update_wallet_settings_business_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
payload: Dict[str, Any] = Body(...),
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
data = update_business_wallet_settings(db, business_id, payload)
|
|
return success_response(data, request, message="WALLET_SETTINGS_UPDATED")
|
|
|
|
|
|
@router.post(
|
|
"/auto-settle/run",
|
|
summary="اجرای تسویه خودکار (برای cron/job)",
|
|
)
|
|
def run_auto_settle_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
data = run_auto_settlement(db, business_id, ctx.get_user_id())
|
|
return success_response(data, request, message="AUTO_SETTLE_EXECUTED" if data.get("executed") else "AUTO_SETTLE_SKIPPED")
|
|
|
|
@router.put(
|
|
"/payouts/{payout_id}/approve",
|
|
summary="تایید درخواست تسویه",
|
|
description="تایید توسط کاربر مجاز",
|
|
)
|
|
def approve_payout_request_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
payout_id: int = Path(...),
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
# Ensure payout belongs to the same business
|
|
payout = db.query(WalletPayout).filter(WalletPayout.id == int(payout_id)).first()
|
|
if not payout:
|
|
raise ApiError("PAYOUT_NOT_FOUND", "درخواست تسویه یافت نشد", http_status=404)
|
|
if int(payout.business_id) != int(business_id):
|
|
raise ApiError("FORBIDDEN", "دسترسی به این درخواست تسویه مجاز نیست", http_status=403)
|
|
data = approve_payout_request(db, payout_id, ctx.get_user_id())
|
|
return success_response(data, request, message="PAYOUT_APPROVED")
|
|
|
|
|
|
@router.put(
|
|
"/payouts/{payout_id}/cancel",
|
|
summary="لغو درخواست تسویه",
|
|
description="لغو و بازگردانی مبلغ به مانده قابل برداشت",
|
|
)
|
|
def cancel_payout_request_endpoint(
|
|
request: Request,
|
|
business_id: int,
|
|
payout_id: int = Path(...),
|
|
_: None = Depends(require_business_access_dep),
|
|
db: Session = Depends(get_db),
|
|
ctx: AuthContext = Depends(get_current_user),
|
|
) -> dict:
|
|
# Ensure payout belongs to the same business
|
|
payout = db.query(WalletPayout).filter(WalletPayout.id == int(payout_id)).first()
|
|
if not payout:
|
|
raise ApiError("PAYOUT_NOT_FOUND", "درخواست تسویه یافت نشد", http_status=404)
|
|
if int(payout.business_id) != int(business_id):
|
|
raise ApiError("FORBIDDEN", "دسترسی به این درخواست تسویه مجاز نیست", http_status=403)
|
|
data = cancel_payout_request(db, payout_id, ctx.get_user_id())
|
|
return success_response(data, request, message="PAYOUT_CANCELED")
|
|
|
|
|