feat(sub): handling upgrade/downgrade
rw integration and /me endpoints are left
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
from datetime import UTC, datetime
|
||||
import math
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import cfg
|
||||
from core.deps import get_auth_context, get_pally_client
|
||||
from db.models.orders import OrderStatus
|
||||
from db.session import get_db
|
||||
from external.pally import PallyClient
|
||||
from repositories import AddonsRepository, PricingRepository
|
||||
@@ -13,6 +17,12 @@ from schemas.dto import AuthContext
|
||||
from schemas.invoices import InvoiceStatus
|
||||
from schemas.plans import OrderDetails
|
||||
from services.plans import calculate_price, get_pricing_model
|
||||
from services.subscriptions import (
|
||||
apply_order_now,
|
||||
deduct_order_balance,
|
||||
queue_order_for_later,
|
||||
should_apply_immediately,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/orders")
|
||||
|
||||
@@ -29,32 +39,68 @@ async def checkout(
|
||||
invoices_repo = InvoiceRepository(session)
|
||||
orders_repo = OrderRepository(session)
|
||||
|
||||
pricing = await get_pricing_model(addons_repo, pricing_repo)
|
||||
price = math.ceil(await calculate_price(addons_repo=addons_repo, order=order, pricing=pricing))
|
||||
bonus_covered = min(ctx.user.balance, price)
|
||||
amount_to_pay = round(max(price - bonus_covered, 0), 2)
|
||||
now = datetime.now(UTC)
|
||||
|
||||
order_entry = await orders_repo.create(
|
||||
ctx.user.id,
|
||||
order.devices,
|
||||
order.duration_days,
|
||||
user_id=ctx.user.id,
|
||||
devices=order.devices,
|
||||
duration_days=order.duration_days,
|
||||
total_amount=price,
|
||||
balance_amount=bonus_covered,
|
||||
addons=order.addons,
|
||||
)
|
||||
|
||||
pricing = await get_pricing_model(addons_repo, pricing_repo)
|
||||
price = await calculate_price(addons_repo=addons_repo, order=order, pricing=pricing)
|
||||
bonus_covered = ctx.user.balance
|
||||
amount_to_pay = max(price - bonus_covered, cfg.minimal_deposit) if bonus_covered < price else 0
|
||||
|
||||
if amount_to_pay > 0:
|
||||
invoice = await invoices_repo.create(
|
||||
creator_id=ctx.user.id, amount=amount_to_pay, status=InvoiceStatus.ACTIVE
|
||||
await invoices_repo.create(
|
||||
creator_id=ctx.user.id,
|
||||
order_id=order_entry.id,
|
||||
amount=amount_to_pay,
|
||||
status=InvoiceStatus.ACTIVE,
|
||||
)
|
||||
with open("log.txt", "w") as f:
|
||||
f.write(
|
||||
" ".join((str(price), str(amount_to_pay), cfg.pally_shop_id, str(order_entry.id)))
|
||||
)
|
||||
bill = await pally.bills.create(
|
||||
float(amount_to_pay), cfg.pally_shop_id, order_id=str(order_entry.id)
|
||||
)
|
||||
|
||||
bill = await pally.bills.create(amount_to_pay, cfg.pally_shop_id, order_id=invoice.id)
|
||||
payment_link = bill.link_page_url
|
||||
if not payment_link:
|
||||
raise HTTPException(500, detail="failed to create invoice")
|
||||
else:
|
||||
await deduct_order_balance(
|
||||
session,
|
||||
user=ctx.user,
|
||||
order=order_entry,
|
||||
description=f"order {order_entry.id} paid from balance",
|
||||
)
|
||||
order_entry.status = OrderStatus.PAID
|
||||
if should_apply_immediately(
|
||||
subscription=ctx.user.subscription,
|
||||
order=order_entry,
|
||||
pricing=pricing,
|
||||
now=now,
|
||||
):
|
||||
await apply_order_now(
|
||||
session,
|
||||
user=ctx.user,
|
||||
order=order_entry,
|
||||
pricing=pricing,
|
||||
now=now,
|
||||
)
|
||||
else:
|
||||
await queue_order_for_later(
|
||||
order=order_entry, subscription=ctx.user.subscription, now=now
|
||||
)
|
||||
await session.commit()
|
||||
payment_link = None
|
||||
|
||||
return CheckoutResponse(
|
||||
order_id=order_entry.id,
|
||||
order_id=str(order_entry.id),
|
||||
total_amount=price,
|
||||
bonus_paid=bonus_covered,
|
||||
amount_to_pay=amount_to_pay,
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
# ruff: noqa: N803
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import math
|
||||
from datetime import UTC, datetime
|
||||
|
||||
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 db.models.transactions import BalanceTransaction, BalanceTxType
|
||||
from external.pally import BillStatus
|
||||
from repositories import AddonsRepository
|
||||
from repositories.invoices import InvoiceRepository
|
||||
from repositories.orders import OrderRepository
|
||||
from schemas.enums import SubscriptionStatus
|
||||
from repositories.pricing import PricingRepository
|
||||
from repositories.users import UserRepository
|
||||
from schemas.invoices import InvoiceStatus
|
||||
from services.plans import get_pricing_model
|
||||
from services.subscriptions import (
|
||||
apply_order_now,
|
||||
deduct_order_balance,
|
||||
queue_order_for_later,
|
||||
should_apply_immediately,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/payments/pally")
|
||||
|
||||
@@ -28,7 +34,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/result")
|
||||
async def pally_callback( # noqa: PLR0911
|
||||
async def pally_callback( # noqa: PLR0911, PLR0912, PLR0915
|
||||
*,
|
||||
InvId: str = Form(...),
|
||||
OutSum: str = Form(...),
|
||||
@@ -53,7 +59,11 @@ async def pally_callback( # noqa: PLR0911
|
||||
):
|
||||
invoice_repo = InvoiceRepository(session)
|
||||
orders_repo = OrderRepository(session)
|
||||
users_repo = UserRepository(session)
|
||||
pricing_repo = PricingRepository(session)
|
||||
invoice_id_str = InvId
|
||||
invoice_id: int | None = None
|
||||
invoice_creator_id: int | None = None
|
||||
|
||||
logger.info(
|
||||
"Pally webhook received - InvId: %s, OutSum: %s, Commission: %s, TrsId: %s, Status: %s, "
|
||||
@@ -100,7 +110,7 @@ async def pally_callback( # noqa: PLR0911
|
||||
|
||||
logger.info("Processing successfully paid bill %s", TrsId)
|
||||
|
||||
# Validate bill ID (from InvId field, which contains the order_id from bill creation)
|
||||
# Validate invoice ID
|
||||
if not invoice_id_str or not invoice_id_str.isdigit():
|
||||
logger.critical(
|
||||
"Invalid or non-numeric bill ID in InvId field for TrsId %s: '%s'",
|
||||
@@ -116,6 +126,9 @@ async def pally_callback( # noqa: PLR0911
|
||||
logger.critical("Bill %s not found in database for TrsId %s", invoice_id_str, TrsId)
|
||||
return "OK"
|
||||
|
||||
invoice_id = invoice.id
|
||||
invoice_creator_id = invoice.creator_id
|
||||
|
||||
# Check if already processed
|
||||
if invoice.status != InvoiceStatus.ACTIVE:
|
||||
logger.warning(
|
||||
@@ -177,20 +190,38 @@ 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:
|
||||
if invoice.order_id is None:
|
||||
logger.critical("Invoice %s has no linked order for TrsId %s", invoice.id, TrsId)
|
||||
return "OK"
|
||||
|
||||
order = await orders_repo.get_by_id(invoice.order_id)
|
||||
if order is None:
|
||||
logger.critical(
|
||||
"Active order not found for bill %s (TrsId: %s, user_id=%s)",
|
||||
"Order not found for bill %s (TrsId: %s, user_id=%s, order_id=%s)",
|
||||
invoice.id,
|
||||
TrsId,
|
||||
invoice.creator_id,
|
||||
invoice.order_id,
|
||||
)
|
||||
return "OK"
|
||||
|
||||
order = active_orders[0]
|
||||
user = invoice.creator
|
||||
if order.user_id != invoice.creator_id:
|
||||
logger.critical(
|
||||
"Order %s does not belong to invoice creator %s for TrsId %s",
|
||||
order.id,
|
||||
invoice.creator_id,
|
||||
TrsId,
|
||||
)
|
||||
return "OK"
|
||||
|
||||
user = await users_repo.get_user_by_id(invoice.creator_id)
|
||||
if user is None:
|
||||
logger.critical("User %s not found for TrsId %s", invoice.creator_id, TrsId)
|
||||
return "OK"
|
||||
|
||||
subscription = user.subscription
|
||||
now = datetime.now(UTC)
|
||||
pricing = await get_pricing_model(AddonsRepository(session), pricing_repo)
|
||||
|
||||
logger.info(
|
||||
"Processing payment: bill_id=%s, order_id=%s, user_id=%s, amount=%s",
|
||||
@@ -203,32 +234,28 @@ async def pally_callback( # noqa: PLR0911
|
||||
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)
|
||||
if order.balance_amount > 0:
|
||||
await deduct_order_balance(
|
||||
session,
|
||||
user=user,
|
||||
order=order,
|
||||
description=f"order {order.id} partial payment from balance",
|
||||
)
|
||||
|
||||
for addon in order.addons:
|
||||
session.add(SubscriptionAddon(subscription_id=subscription.id, addon_id=addon.addon_id))
|
||||
if should_apply_immediately(
|
||||
subscription=subscription,
|
||||
order=order,
|
||||
pricing=pricing,
|
||||
now=now,
|
||||
):
|
||||
await apply_order_now(session, user=user, order=order, pricing=pricing, now=now)
|
||||
else:
|
||||
if subscription is None:
|
||||
logger.critical(
|
||||
"Cannot queue order %s without subscription for user %s", order.id, user.id
|
||||
)
|
||||
return "OK"
|
||||
await queue_order_for_later(order=order, subscription=subscription, now=now)
|
||||
|
||||
# Process referral bonus
|
||||
referal_id = user.referal_id
|
||||
@@ -271,12 +298,12 @@ async def pally_callback( # noqa: PLR0911
|
||||
logger.exception(
|
||||
"CRITICAL ERROR processing payment for TrsId %s, bill_id %s, user_id %s: %s",
|
||||
TrsId,
|
||||
invoice.id,
|
||||
invoice.creator_id,
|
||||
invoice_id,
|
||||
invoice_creator_id,
|
||||
str(e),
|
||||
)
|
||||
raise HTTPException(500, detail="Payment processing failed.") from e
|
||||
|
||||
logger.info("Bill %s marked as PAID 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