feat: integrated pally webhook (draft, needs rewrite)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -180,3 +180,4 @@ plans
|
|||||||
dev.sh
|
dev.sh
|
||||||
test_dummy.py
|
test_dummy.py
|
||||||
*.pem
|
*.pem
|
||||||
|
tests/
|
||||||
45
alembic/versions/3d767875ec7d_transactions.py
Normal file
45
alembic/versions/3d767875ec7d_transactions.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""+transactions
|
||||||
|
|
||||||
|
Revision ID: 3d767875ec7d
|
||||||
|
Revises: 8686021972e9
|
||||||
|
Create Date: 2026-08-01 23:40:43.069199
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '3d767875ec7d'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '8686021972e9'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('balance_transactions',
|
||||||
|
sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('user_id', sa.BIGINT(), nullable=False),
|
||||||
|
sa.Column('amount', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('tx_type', sa.Enum('deposit', 'purchase', 'refund', 'referral', 'manual', 'autorenew', 'manual_renew', name='balancetxtype'), nullable=False),
|
||||||
|
sa.Column('balance_before', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('balance_after', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('description', sa.TEXT(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_balance_transactions_user_id'), 'balance_transactions', ['user_id'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_balance_transactions_user_id'), table_name='balance_transactions')
|
||||||
|
op.drop_table('balance_transactions')
|
||||||
|
# ### end Alembic commands ###
|
||||||
38
alembic/versions/8101e7820b1e_balance_float_2.py
Normal file
38
alembic/versions/8101e7820b1e_balance_float_2.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""balance->float(2)
|
||||||
|
|
||||||
|
Revision ID: 8101e7820b1e
|
||||||
|
Revises: f1bb7c46f6f6
|
||||||
|
Create Date: 2026-08-01 23:16:59.561628
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '8101e7820b1e'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = 'f1bb7c46f6f6'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('users', 'balance',
|
||||||
|
existing_type=sa.REAL(),
|
||||||
|
type_=sa.FLOAT(precision=2),
|
||||||
|
existing_nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('users', 'balance',
|
||||||
|
existing_type=sa.FLOAT(precision=2),
|
||||||
|
type_=sa.REAL(),
|
||||||
|
existing_nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
32
alembic/versions/8686021972e9_balance_real_psql_tweaking.py
Normal file
32
alembic/versions/8686021972e9_balance_real_psql_tweaking.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""balance->REAL -- psql tweaking
|
||||||
|
|
||||||
|
Revision ID: 8686021972e9
|
||||||
|
Revises: 8101e7820b1e
|
||||||
|
Create Date: 2026-08-01 23:17:44.635690
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '8686021972e9'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '8101e7820b1e'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
34
alembic/versions/c58e44d15cca_user_balance.py
Normal file
34
alembic/versions/c58e44d15cca_user_balance.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""+user.balance
|
||||||
|
|
||||||
|
Revision ID: c58e44d15cca
|
||||||
|
Revises: e237f7c5cb9a
|
||||||
|
Create Date: 2026-08-01 22:59:28.814554
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'c58e44d15cca'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = 'e237f7c5cb9a'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_unique_constraint(None, 'invoices', ['id'])
|
||||||
|
op.add_column('users', sa.Column('balance', sa.FLOAT(precision=2), nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('users', 'balance')
|
||||||
|
op.drop_constraint(None, 'invoices', type_='unique')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""user.id -> INTEGER, +user.referal_id
|
||||||
|
|
||||||
|
Revision ID: f1bb7c46f6f6
|
||||||
|
Revises: c58e44d15cca
|
||||||
|
Create Date: 2026-08-01 23:14:21.719693
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'f1bb7c46f6f6'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = 'c58e44d15cca'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('invoices', 'creator_id',
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.INTEGER(),
|
||||||
|
existing_nullable=False)
|
||||||
|
op.alter_column('sessions', 'user_id',
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.INTEGER(),
|
||||||
|
existing_nullable=False)
|
||||||
|
op.add_column('users', sa.Column('referal_id', sa.INTEGER(), nullable=True))
|
||||||
|
op.alter_column('users', 'id',
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.INTEGER(),
|
||||||
|
existing_nullable=False,
|
||||||
|
autoincrement=True)
|
||||||
|
op.alter_column('users', 'balance',
|
||||||
|
existing_type=sa.REAL(),
|
||||||
|
type_=sa.FLOAT(precision=2),
|
||||||
|
existing_nullable=False)
|
||||||
|
op.create_foreign_key(None, 'users', 'users', ['referal_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'users', type_='foreignkey')
|
||||||
|
op.alter_column('users', 'balance',
|
||||||
|
existing_type=sa.FLOAT(precision=2),
|
||||||
|
type_=sa.REAL(),
|
||||||
|
existing_nullable=False)
|
||||||
|
op.alter_column('users', 'id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=False,
|
||||||
|
autoincrement=True)
|
||||||
|
op.drop_column('users', 'referal_id')
|
||||||
|
op.alter_column('sessions', 'user_id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=False)
|
||||||
|
op.alter_column('invoices', 'creator_id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -2,6 +2,7 @@ from .addons import Addon
|
|||||||
from .invoice import Invoice
|
from .invoice import Invoice
|
||||||
from .pricing import PricingConfig
|
from .pricing import PricingConfig
|
||||||
from .sessions import Session
|
from .sessions import Session
|
||||||
|
from .transactions import BalanceTransaction
|
||||||
from .users import User
|
from .users import User
|
||||||
|
|
||||||
__all__ = ["Addon", "Invoice", "PricingConfig", "Session", "User"]
|
__all__ = ["Addon", "BalanceTransaction", "Invoice", "PricingConfig", "Session", "User"]
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ class Invoice(Base):
|
|||||||
Enum(InvoiceStatus, name="invoicestatus"), nullable=False, default=InvoiceStatus.ACTIVE
|
Enum(InvoiceStatus, name="invoicestatus"), nullable=False, default=InvoiceStatus.ACTIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
user: Mapped["User"] = relationship("User")
|
creator: Mapped["User"] = relationship("User", lazy="selectin")
|
||||||
|
|||||||
43
db/models/transactions.py
Normal file
43
db/models/transactions.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from datetime import UTC, datetime
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import BIGINT, INTEGER, TEXT, DateTime, Enum, ForeignKey
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from db.base import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BalanceTxType(StrEnum):
|
||||||
|
DEPOSIT = "deposit"
|
||||||
|
PURCHASE = "purchase"
|
||||||
|
REFUND = "refund"
|
||||||
|
REFERRAL_BONUS = "referral"
|
||||||
|
MANUAL = "manual"
|
||||||
|
AUTORENEW = "autorenew"
|
||||||
|
MANUAL_RENEW = "manual_renew"
|
||||||
|
|
||||||
|
|
||||||
|
class BalanceTransaction(Base):
|
||||||
|
__tablename__ = "balance_transactions"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(BIGINT, autoincrement=True, primary_key=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(BIGINT, ForeignKey("users.id"), nullable=False, index=True)
|
||||||
|
amount: Mapped[int] = mapped_column(INTEGER, nullable=False)
|
||||||
|
|
||||||
|
tx_type: Mapped[BalanceTxType] = mapped_column(
|
||||||
|
Enum(BalanceTxType, name="balancetxtype", values_callable=lambda x: [e.value for e in x]),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
balance_before: Mapped[int] = mapped_column(INTEGER, nullable=False)
|
||||||
|
balance_after: Mapped[int] = mapped_column(INTEGER, nullable=False)
|
||||||
|
description: Mapped[str] = mapped_column(TEXT, nullable=True)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
default=lambda: datetime.now(UTC),
|
||||||
|
)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import BIGINT, TEXT, VARCHAR
|
from sqlalchemy import BIGINT, INTEGER, REAL, TEXT, VARCHAR, ForeignKey
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from db.base import Base
|
from db.base import Base
|
||||||
@@ -13,10 +13,17 @@ class User(Base):
|
|||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(
|
id: Mapped[int] = mapped_column(
|
||||||
BIGINT, unique=True, autoincrement=True, nullable=False, primary_key=True
|
INTEGER, unique=True, autoincrement=True, nullable=False, primary_key=True
|
||||||
)
|
)
|
||||||
username: Mapped[str] = mapped_column(TEXT, unique=True, nullable=True)
|
username: Mapped[str] = mapped_column(TEXT, unique=True, nullable=True)
|
||||||
hashed_password: Mapped[str] = mapped_column(VARCHAR(255), nullable=True)
|
hashed_password: Mapped[str] = mapped_column(VARCHAR(255), nullable=True)
|
||||||
telegram_id: Mapped[int] = mapped_column(BIGINT, unique=True, nullable=True)
|
telegram_id: Mapped[int] = mapped_column(BIGINT, unique=True, nullable=True)
|
||||||
|
referal_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||||
|
|
||||||
|
balance: Mapped[float] = mapped_column(REAL, nullable=False, default=0)
|
||||||
|
|
||||||
sessions: Mapped[list["Session"]] = relationship(back_populates="user", lazy="selectin")
|
sessions: Mapped[list["Session"]] = relationship(back_populates="user", lazy="selectin")
|
||||||
|
referal: Mapped["User | None"] = relationship(
|
||||||
|
"User",
|
||||||
|
remote_side=[id],
|
||||||
|
)
|
||||||
|
|||||||
@@ -32,3 +32,10 @@ class InvoiceRepository:
|
|||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
async def update_status_by_id(self, invoice_id: int, status: InvoiceStatus) -> Invoice | None:
|
||||||
|
invoice = await self.get_by_id(invoice_id)
|
||||||
|
invoice.status = status
|
||||||
|
await self.session.commit()
|
||||||
|
|
||||||
|
return invoice
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from db.models import User
|
from db.models import User
|
||||||
|
from db.models.transactions import BalanceTransaction, BalanceTxType
|
||||||
|
|
||||||
|
|
||||||
class UserRepository:
|
class UserRepository:
|
||||||
@@ -39,3 +40,25 @@ class UserRepository:
|
|||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
async def increase_balance(
|
||||||
|
self, user_id: int, amount: float, tx_type: BalanceTxType, description: str
|
||||||
|
):
|
||||||
|
user = await self.get_user_by_id(user_id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return
|
||||||
|
|
||||||
|
obj = BalanceTransaction(
|
||||||
|
user_id=user_id,
|
||||||
|
amount=amount,
|
||||||
|
tx_type=tx_type,
|
||||||
|
balance_before=user.balance,
|
||||||
|
balance_after=user.balance + amount,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
self.session.add(obj)
|
||||||
|
|
||||||
|
user.balance += amount
|
||||||
|
await self.session.commit()
|
||||||
|
return user
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ pydantic-settings>=2.14.0
|
|||||||
asyncpg>=0.31.0
|
asyncpg>=0.31.0
|
||||||
alembic>=1.18.0
|
alembic>=1.18.0
|
||||||
aiohttp>=3.14.0
|
aiohttp>=3.14.0
|
||||||
|
python-multipart==0.0.32
|
||||||
@@ -2,6 +2,7 @@ from fastapi import APIRouter
|
|||||||
|
|
||||||
from .auth import router as auth_router
|
from .auth import router as auth_router
|
||||||
from .orders import router as orders_router
|
from .orders import router as orders_router
|
||||||
|
from .payments import payment_routers
|
||||||
from .plans import router as plans_router
|
from .plans import router as plans_router
|
||||||
|
|
||||||
routers: list[APIRouter] = [auth_router, plans_router, orders_router]
|
routers: list[APIRouter] = [auth_router, plans_router, orders_router, *payment_routers]
|
||||||
|
|||||||
3
routes/payments/__init__.py
Normal file
3
routes/payments/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .pally import router as pally_router
|
||||||
|
|
||||||
|
payment_routers = [pally_router]
|
||||||
222
routes/payments/pally.py
Normal file
222
routes/payments/pally.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
# ruff: noqa: N803
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
|
||||||
|
from fastapi import Depends, Form, HTTPException
|
||||||
|
from fastapi.routing import APIRouter
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from config import cfg
|
||||||
|
from core.deps import get_db
|
||||||
|
from db.models.transactions import BalanceTxType
|
||||||
|
from external.pally import BillStatus
|
||||||
|
from repositories.invoices import InvoiceRepository
|
||||||
|
from repositories.users import UserRepository
|
||||||
|
from schemas.invoices import InvoiceStatus
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/payments/pally")
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/result")
|
||||||
|
async def pally_callback( # noqa: PLR0911
|
||||||
|
*,
|
||||||
|
InvId: str = Form(...),
|
||||||
|
OutSum: str = Form(...),
|
||||||
|
Commission: str = Form(...),
|
||||||
|
TrsId: str = Form(...),
|
||||||
|
Status: str = Form(...),
|
||||||
|
CurrencyIn: str = Form(...),
|
||||||
|
custom: str | None = Form(None),
|
||||||
|
SignatureValue: str = Form(...),
|
||||||
|
# Optional fields for additional information
|
||||||
|
AccountType: str | None = Form(None),
|
||||||
|
AccountNumber: str | None = Form(None),
|
||||||
|
BalanceAmount: str | None = Form(None),
|
||||||
|
BalanceCurrency: str | None = Form(None),
|
||||||
|
PayerPhone: str | None = Form(None),
|
||||||
|
PayerEmail: str | None = Form(None),
|
||||||
|
PayerName: str | None = Form(None),
|
||||||
|
PayerComment: str | None = Form(None),
|
||||||
|
ErrorCode: int | None = Form(None),
|
||||||
|
ErrorMessage: str | None = Form(None),
|
||||||
|
session: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
users_repo = UserRepository(session)
|
||||||
|
invoice_repo = InvoiceRepository(session)
|
||||||
|
invoice_id_str = InvId
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Pally webhook received - InvId: %s, OutSum: %s, Commission: %s, TrsId: %s, Status: %s, "
|
||||||
|
"CurrencyIn: %s, custom: %s, BalanceAmount: %s, SignatureValue: %s",
|
||||||
|
InvId,
|
||||||
|
OutSum,
|
||||||
|
Commission,
|
||||||
|
TrsId,
|
||||||
|
Status,
|
||||||
|
CurrencyIn,
|
||||||
|
custom,
|
||||||
|
BalanceAmount,
|
||||||
|
SignatureValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
if Status != BillStatus.SUCCESS:
|
||||||
|
logger.warning(
|
||||||
|
"Non-success payment received - Status: %s, ErrorCode: %s, ErrorMessage: %s, TrsId: %s",
|
||||||
|
Status,
|
||||||
|
ErrorCode,
|
||||||
|
ErrorMessage,
|
||||||
|
TrsId,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate signature
|
||||||
|
raw_string = f"{OutSum}:{InvId}:{cfg.pally_token}"
|
||||||
|
expected_signature = hashlib.md5(raw_string.encode("utf-8")).hexdigest().upper()
|
||||||
|
|
||||||
|
logger.debug("Signature validation for TrsId %s", TrsId)
|
||||||
|
|
||||||
|
if not hmac.compare_digest(SignatureValue, expected_signature):
|
||||||
|
logger.critical(
|
||||||
|
"SECURITY ALERT: Invalid signature for TrsId %s - Expected: %s, Received: %s",
|
||||||
|
TrsId,
|
||||||
|
expected_signature,
|
||||||
|
SignatureValue,
|
||||||
|
)
|
||||||
|
raise HTTPException(403, detail="Invalid signature.")
|
||||||
|
|
||||||
|
# Only process successful payments
|
||||||
|
if Status != BillStatus.SUCCESS:
|
||||||
|
logger.info("Bill %s skipped: status=%s", TrsId, Status)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
logger.info("Processing successfully paid bill %s", TrsId)
|
||||||
|
|
||||||
|
# Validate bill ID (from InvId field, which contains the order_id from bill creation)
|
||||||
|
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'",
|
||||||
|
TrsId,
|
||||||
|
invoice_id_str,
|
||||||
|
)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
# Find bill in database
|
||||||
|
invoice = await invoice_repo.get_by_id(int(invoice_id_str))
|
||||||
|
|
||||||
|
if not invoice:
|
||||||
|
logger.critical("Bill %s not found in database for TrsId %s", invoice_id_str, TrsId)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
# Check if already processed
|
||||||
|
if invoice.status != InvoiceStatus.ACTIVE:
|
||||||
|
logger.warning(
|
||||||
|
"Bill %s (TrsId: %s) is already processed with status: %s",
|
||||||
|
invoice.id,
|
||||||
|
TrsId,
|
||||||
|
invoice.status,
|
||||||
|
)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Handle fee scenarios: use BalanceAmount if available (net amount after fees),
|
||||||
|
# otherwise use OutSum (gross amount paid by customer)
|
||||||
|
if BalanceAmount is not None:
|
||||||
|
# Customer pays fees - BalanceAmount is the net amount credited to merchant
|
||||||
|
credited_amount = int(float(BalanceAmount))
|
||||||
|
gross_amount = int(float(OutSum))
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Customer-pays-fees payment: bill_id=%s, expected=%s, gross_paid=%s, net_credited=%s",
|
||||||
|
invoice.id,
|
||||||
|
invoice.amount,
|
||||||
|
gross_amount,
|
||||||
|
credited_amount,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate that the net credited amount matches our bill amount
|
||||||
|
if invoice.amount != credited_amount:
|
||||||
|
logger.error(
|
||||||
|
"Net amount mismatch for bill %s (TrsId: %s) - Expected: %s, Net credited: %s, Gross paid: %s",
|
||||||
|
invoice.id,
|
||||||
|
TrsId,
|
||||||
|
invoice.amount,
|
||||||
|
credited_amount,
|
||||||
|
gross_amount,
|
||||||
|
)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
amount = credited_amount # Credit the net amount (without fees)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Standard payment - OutSum should match bill amount exactly
|
||||||
|
amount = int(float(OutSum))
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Standard payment: bill_id=%s, expected=%s, received=%s",
|
||||||
|
invoice.id,
|
||||||
|
invoice.amount,
|
||||||
|
amount,
|
||||||
|
)
|
||||||
|
|
||||||
|
if invoice.amount != amount:
|
||||||
|
logger.error(
|
||||||
|
"Amount mismatch for bill %s (TrsId: %s) - Expected: %s, Received: %s",
|
||||||
|
invoice.id,
|
||||||
|
TrsId,
|
||||||
|
invoice.amount,
|
||||||
|
amount,
|
||||||
|
)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Processing payment: bill_id=%s, user_id=%s, amount=%s",
|
||||||
|
invoice.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})",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process referral bonus
|
||||||
|
user = invoice.creator
|
||||||
|
referal = user.referal
|
||||||
|
if referal 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
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Payment processing completed successfully for TrsId %s", TrsId)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
"CRITICAL ERROR processing payment for TrsId %s, bill_id %s, user_id %s: %s",
|
||||||
|
TrsId,
|
||||||
|
invoice.id,
|
||||||
|
invoice.creator_id,
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
# Don't return early - still mark as success to prevent retries
|
||||||
|
# The balance operation might have partially succeeded
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
return "OK"
|
||||||
@@ -4,9 +4,9 @@ from schemas.providers import ProvidersType
|
|||||||
|
|
||||||
|
|
||||||
class UserRegistration(BaseModel):
|
class UserRegistration(BaseModel):
|
||||||
telegram_id: str | None = Field()
|
telegram_id: str | None = Field(None)
|
||||||
|
|
||||||
username: str | None = Field()
|
username: str | None = Field(None)
|
||||||
password: str | None = Field()
|
password: str | None = Field(None)
|
||||||
|
|
||||||
provider: ProvidersType
|
provider: ProvidersType
|
||||||
|
|||||||
Reference in New Issue
Block a user