File size: 8,840 Bytes
b8e7d9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cfe2de7
b8e7d9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cfe2de7
b8e7d9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cfe2de7
b8e7d9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
"""
Payment Transaction Manager

Centralized payment transaction management similar to CreditTransactionManager.
Handles order creation, verification, and analytics.
"""
import uuid
import logging
from datetime import datetime
from typing import Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func

from core.models import PaymentTransaction, User

logger = logging.getLogger(__name__)


class PaymentTransactionError(Exception):
    """Base exception for payment transaction errors."""
    pass


class TransactionNotFoundError(PaymentTransactionError):
    """Transaction not found."""
    pass


class PaymentTransactionManager:
    """
    Centralized manager for all payment transactions.
    
    Similar to CreditTransactionManager, provides single source of truth
    for payment operations.
    """
    
    @staticmethod
    async def create_order(
        session: AsyncSession,
        user: User,
        package_id: str,
        credits_amount: int,
        amount_paise: int,
        currency: str,
        gateway: str,
        gateway_order_id: str,
        metadata: Optional[Dict[str, Any]] = None
    ) -> PaymentTransaction:
        """
        Create a payment order.
        
        Args:
            session: Database session
            user: User making payment
            package_id: Package identifier (e.g., "basic_50")
            credits_amount: Number of credits
            amount_paise: Amount in paise
            currency: Currency code (INR)
            gateway: Payment gateway (razorpay)
            gateway_order_id: Gateway's order ID
            metadata: Additional metadata
        
        Returns:
            PaymentTransaction instance
        """
        transaction = PaymentTransaction(
            transaction_id=f"txn_{uuid.uuid4().hex[:16]}",
            user_id=user.id,
            gateway=gateway,
            gateway_order_id=gateway_order_id,
            package_id=package_id,
            credits_amount=credits_amount,
            amount_paise=amount_paise,
            currency=currency,
            status="created",
            extra_data=metadata or {},
            created_at=datetime.utcnow()
        )
        
        session.add(transaction)
        
        logger.info(
            f"Created payment order: {transaction.transaction_id} "
            f"for user {user.id}, {credits_amount} credits, "
            f"₹{amount_paise/100:.2f}"
        )
        
        return transaction
    
    @staticmethod
    async def verify_payment(
        session: AsyncSession,
        transaction_id: str,
        gateway_payment_id: str,
        verified_by: str = "signature",
        metadata: Optional[Dict[str, Any]] = None
    ) -> PaymentTransaction:
        """
        Verify and mark payment as successful.
        
        Args:
            session: Database session
            transaction_id: Our transaction ID
            gateway_payment_id: Gateway's payment ID
            verified_by: Verification method (signature/webhook)
            metadata: Additional metadata
        
        Returns:
            Updated PaymentTransaction
        
        Raises:
            TransactionNotFoundError: If transaction not found
        """
        # Get transaction
        query = select(PaymentTransaction).where(
            PaymentTransaction.transaction_id == transaction_id
        )
        result = await session.execute(query)
        transaction = result.scalar_one_or_none()
        
        if not transaction:
            raise TransactionNotFoundError(f"Transaction {transaction_id} not found")
        
        # Update transaction
        transaction.gateway_payment_id = gateway_payment_id
        transaction.status = "paid"
        transaction.verified_by = verified_by
        transaction.paid_at = datetime.utcnow()
        
        if metadata:
            transaction.extra_data.update(metadata)
        
        logger.info(
            f"Verified payment: {transaction_id}, "
            f"payment_id: {gateway_payment_id}, "
            f"method: {verified_by}"
        )
        
        return transaction
    
    @staticmethod
    async def mark_failed(
        session: AsyncSession,
        transaction_id: str,
        error_message: str,
        metadata: Optional[Dict[str, Any]] = None
    ) -> PaymentTransaction:
        """
        Mark payment as failed.
        
        Args:
            session: Database session
            transaction_id: Our transaction ID
            error_message: Failure reason
            metadata: Additional metadata
        
        Returns:
            Updated PaymentTransaction
        """
        query = select(PaymentTransaction).where(
            PaymentTransaction.transaction_id == transaction_id
        )
        result = await session.execute(query)
        transaction = result.scalar_one_or_none()
        
        if not transaction:
            raise TransactionNotFoundError(f"Transaction {transaction_id} not found")
        
        transaction.status = "failed"
        transaction.error_message = error_message[:1000]  # Truncate
        
        if metadata:
            transaction.extra_data.update(metadata)
        
        logger.warning(f"Payment failed: {transaction_id}, reason: {error_message}")
        
        return transaction
    
    @staticmethod
    async def get_transaction_history(
        session: AsyncSession,
        user_id: int,
        limit: int = 50,
        offset: int = 0,
        status: Optional[str] = None
    ) -> List[PaymentTransaction]:
        """
        Get payment transaction history for a user.
        
        Args:
            session: Database session
            user_id: User ID
            limit: Maximum number of transactions
            offset: Offset for pagination
            status: Filter by status (paid/failed/created)
        
        Returns:
            List of PaymentTransaction objects
        """
        query = select(PaymentTransaction).where(
            PaymentTransaction.user_id == user_id
        )
        
        if status:
            query = query.where(PaymentTransaction.status == status)
        
        query = query.order_by(
            PaymentTransaction.created_at.desc()
        ).offset(offset).limit(limit)
        
        result = await session.execute(query)
        return list(result.scalars().all())
    
    @staticmethod
    async def get_analytics(
        session: AsyncSession,
        user_id: Optional[int] = None
    ) -> Dict[str, Any]:
        """
        Get payment analytics.
        
        Args:
            session: Database session
            user_id: Optional user ID (if None, get global stats)
        
        Returns:
            Dict with analytics data
        """
        # Base query
        base_query = select(PaymentTransaction)
        if user_id:
            base_query = base_query.where(PaymentTransaction.user_id == user_id)
        
        # Total transactions
        total_count = await session.scalar(
            select(func.count()).select_from(base_query.subquery())
        )
        
        # Successful transactions
        paid_query = base_query.where(PaymentTransaction.status == "paid")
        paid_count = await session.scalar(
            select(func.count()).select_from(paid_query.subquery())
        )
        
        # Total revenue (in paise)
        revenue_result = await session.scalar(
            select(func.sum(PaymentTransaction.amount_paise)).select_from(
                paid_query.subquery()
            )
        )
        total_revenue_paise = revenue_result or 0
        
        # Total credits purchased
        credits_result = await session.scalar(
            select(func.sum(PaymentTransaction.credits_amount)).select_from(
                paid_query.subquery()
            )
        )
        total_credits = credits_result or 0
        
        # Failed transactions
        failed_count = await session.scalar(
            select(func.count()).select_from(
                base_query.where(PaymentTransaction.status == "failed").subquery()
            )
        )
        
        # Success rate
        success_rate = (paid_count / total_count * 100) if total_count > 0 else 0
        
        return {
            "total_transactions": total_count,
            "successful_payments": paid_count,
            "failed_payments": failed_count,
            "success_rate_percent": round(success_rate, 2),
            "total_revenue_paise": total_revenue_paise,
            "total_revenue_rupees": total_revenue_paise / 100,
            "total_credits_purchased": total_credits,
            "average_transaction_value_paise": (
                total_revenue_paise // paid_count if paid_count > 0 else 0
            )
        }