feat(sub): subscription logic (pre release)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
# ruff: noqa: N803
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
@@ -6,14 +7,19 @@ import math
|
||||
|
||||
from fastapi import Depends, Form, HTTPException
|
||||
from fastapi.routing import APIRouter
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import cfg
|
||||
from core.deps import get_db
|
||||
from db.models import Subscription, SubscriptionAddon
|
||||
from db.models.orders import OrderStatus
|
||||
from db.models.transactions import BalanceTransaction
|
||||
from db.models.transactions import BalanceTxType
|
||||
from external.pally import BillStatus
|
||||
from repositories.invoices import InvoiceRepository
|
||||
from repositories.users import UserRepository
|
||||
from repositories.orders import OrderRepository
|
||||
from schemas.enums import SubscriptionStatus
|
||||
from schemas.invoices import InvoiceStatus
|
||||
|
||||
router = APIRouter(prefix="/payments/pally")
|
||||
@@ -45,8 +51,8 @@ async def pally_callback( # noqa: PLR0911
|
||||
ErrorMessage: str | None = Form(None),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
users_repo = UserRepository(session)
|
||||
invoice_repo = InvoiceRepository(session)
|
||||
orders_repo = OrderRepository(session)
|
||||
invoice_id_str = InvId
|
||||
|
||||
logger.info(
|
||||
@@ -171,39 +177,97 @@ async def pally_callback( # noqa: PLR0911
|
||||
)
|
||||
return "OK"
|
||||
|
||||
active_orders = await orders_repo.get_active_by_user_id(invoice.creator_id)
|
||||
if not active_orders:
|
||||
logger.critical(
|
||||
"Active order not found for bill %s (TrsId: %s, user_id=%s)",
|
||||
invoice.id,
|
||||
TrsId,
|
||||
invoice.creator_id,
|
||||
)
|
||||
return "OK"
|
||||
|
||||
order = active_orders[0]
|
||||
user = invoice.creator
|
||||
subscription = user.subscription
|
||||
now = datetime.now(UTC)
|
||||
|
||||
logger.info(
|
||||
"Processing payment: bill_id=%s, user_id=%s, amount=%s",
|
||||
"Processing payment: bill_id=%s, order_id=%s, user_id=%s, amount=%s",
|
||||
invoice.id,
|
||||
order.id,
|
||||
invoice.creator_id,
|
||||
amount,
|
||||
)
|
||||
|
||||
# Credit user balance
|
||||
await users_repo.increase_balance(
|
||||
invoice.creator_id,
|
||||
amount=amount,
|
||||
tx_type=BalanceTxType.DEPOSIT,
|
||||
description=f"payment via PALLY (TrsId: {TrsId})",
|
||||
)
|
||||
order.status = OrderStatus.PAID
|
||||
invoice.status = InvoiceStatus.PAID
|
||||
|
||||
if subscription is None:
|
||||
subscription = Subscription(
|
||||
user_id=invoice.creator_id,
|
||||
devices=order.devices,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
expires_at=now + timedelta(days=order.duration_days),
|
||||
)
|
||||
session.add(subscription)
|
||||
await session.flush()
|
||||
else:
|
||||
logger.warning(
|
||||
"Subscription already exists for user_id=%s, updating existing subscription %s",
|
||||
invoice.creator_id,
|
||||
subscription.id,
|
||||
)
|
||||
subscription.devices = order.devices
|
||||
subscription.status = SubscriptionStatus.ACTIVE
|
||||
subscription.expires_at = max(subscription.expires_at, now) + timedelta(
|
||||
days=order.duration_days
|
||||
)
|
||||
await session.execute(
|
||||
delete(SubscriptionAddon).where(SubscriptionAddon.subscription_id == subscription.id)
|
||||
)
|
||||
|
||||
for addon in order.addons:
|
||||
session.add(SubscriptionAddon(subscription_id=subscription.id, addon_id=addon.addon_id))
|
||||
|
||||
# Process referral bonus
|
||||
user = invoice.creator
|
||||
referal = user.referal
|
||||
if referal is not None:
|
||||
referal_id = user.referal_id
|
||||
if referal_id is not None:
|
||||
referal_amount = math.floor(amount * (cfg.referal_bonus / 100))
|
||||
await users_repo.increase_balance(
|
||||
referal,
|
||||
referal_amount,
|
||||
tx_type=BalanceTxType.REFERRAL_BONUS,
|
||||
description=f"referral reward for user {invoice.creator_id} (TrsId: {TrsId})",
|
||||
)
|
||||
logger.info(
|
||||
"Referral bonus processed: referrer_id=%s, amount=%s", referal, referal_amount
|
||||
)
|
||||
if referal_amount > 0:
|
||||
referal_user = await session.get(type(user), referal_id)
|
||||
if referal_user is not None:
|
||||
session.add(
|
||||
BalanceTransaction(
|
||||
user_id=referal_id,
|
||||
amount=referal_amount,
|
||||
tx_type=BalanceTxType.REFERRAL_BONUS,
|
||||
balance_before=referal_user.balance,
|
||||
balance_after=referal_user.balance + referal_amount,
|
||||
description=f"referral reward for user {invoice.creator_id} (TrsId: {TrsId})",
|
||||
)
|
||||
)
|
||||
referal_user.balance += referal_amount
|
||||
|
||||
logger.info(
|
||||
"Referral bonus processed: referrer_id=%s, amount=%s",
|
||||
referal_id,
|
||||
referal_amount,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Referrer %s not found for user_id=%s while processing TrsId %s",
|
||||
referal_id,
|
||||
invoice.creator_id,
|
||||
TrsId,
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
|
||||
logger.info("Payment processing completed successfully for TrsId %s", TrsId)
|
||||
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.exception(
|
||||
"CRITICAL ERROR processing payment for TrsId %s, bill_id %s, user_id %s: %s",
|
||||
TrsId,
|
||||
@@ -211,12 +275,8 @@ async def pally_callback( # noqa: PLR0911
|
||||
invoice.creator_id,
|
||||
str(e),
|
||||
)
|
||||
# Don't return early - still mark as success to prevent retries
|
||||
# The balance operation might have partially succeeded
|
||||
raise HTTPException(500, detail="Payment processing failed.") from e
|
||||
|
||||
# Update bill status to success
|
||||
await invoice_repo.update_status_by_id(int(invoice.id), status=InvoiceStatus.PAID)
|
||||
|
||||
logger.info("Bill %s marked as SUCCESS for TrsId %s", invoice.id, TrsId)
|
||||
logger.info("Bill %s marked as PAID for TrsId %s", invoice.id, TrsId)
|
||||
|
||||
return "OK"
|
||||
|
||||
Reference in New Issue
Block a user