pounce/backend/app/services/yield_payouts.py
Yves Gugger bb7ce97330
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Deploy: referral rewards antifraud + legal contact updates
2025-12-15 13:56:43 +01:00

133 lines
4.1 KiB
Python

"""
Yield payout generation helpers (ledger).
Used by:
- Admin endpoints (manual ops)
- Scheduler (automatic monthly preparation)
"""
from __future__ import annotations
from datetime import datetime, timedelta
from decimal import Decimal
from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.yield_domain import YieldDomain, YieldPayout, YieldTransaction
async def generate_payouts_for_period(
db: AsyncSession,
*,
period_start: datetime,
period_end: datetime,
) -> tuple[int, int]:
"""
Create payouts for confirmed, unpaid transactions and assign payout_id.
Returns: (created_count, skipped_existing_count)
"""
if period_end <= period_start:
raise ValueError("period_end must be after period_start")
aggregates = (
await db.execute(
select(
YieldDomain.user_id.label("user_id"),
YieldTransaction.currency.label("currency"),
func.count(YieldTransaction.id).label("tx_count"),
func.coalesce(func.sum(YieldTransaction.net_amount), 0).label("amount"),
)
.join(YieldDomain, YieldDomain.id == YieldTransaction.yield_domain_id)
.where(
and_(
YieldTransaction.status == "confirmed",
YieldTransaction.payout_id.is_(None),
YieldTransaction.created_at >= period_start,
YieldTransaction.created_at < period_end,
)
)
.group_by(YieldDomain.user_id, YieldTransaction.currency)
)
).all()
created = 0
skipped = 0
for row in aggregates:
user_id = int(row.user_id)
currency = (row.currency or "CHF").upper()
tx_count = int(row.tx_count or 0)
amount = Decimal(str(row.amount or 0))
if tx_count <= 0 or amount <= 0:
continue
existing = (
await db.execute(
select(YieldPayout).where(
and_(
YieldPayout.user_id == user_id,
YieldPayout.currency == currency,
YieldPayout.period_start == period_start,
YieldPayout.period_end == period_end,
)
)
)
).scalar_one_or_none()
if existing:
skipped += 1
continue
payout = YieldPayout(
user_id=user_id,
amount=amount,
currency=currency,
period_start=period_start,
period_end=period_end,
transaction_count=tx_count,
status="pending",
payment_method=None,
payment_reference=None,
)
db.add(payout)
await db.flush()
tx_ids = (
await db.execute(
select(YieldTransaction.id)
.join(YieldDomain, YieldDomain.id == YieldTransaction.yield_domain_id)
.where(
and_(
YieldDomain.user_id == user_id,
YieldTransaction.currency == currency,
YieldTransaction.status == "confirmed",
YieldTransaction.payout_id.is_(None),
YieldTransaction.created_at >= period_start,
YieldTransaction.created_at < period_end,
)
)
)
).scalars().all()
for tx_id in tx_ids:
tx = (
await db.execute(select(YieldTransaction).where(YieldTransaction.id == tx_id))
).scalar_one()
tx.payout_id = payout.id
created += 1
await db.commit()
return created, skipped
async def generate_payouts_for_previous_month(db: AsyncSession) -> tuple[int, int]:
now = datetime.utcnow()
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
prev_month_end = month_start
prev_month_start = (month_start - timedelta(days=1)).replace(day=1)
return await generate_payouts_for_period(db, period_start=prev_month_start, period_end=prev_month_end)