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