jebin2 commited on
Commit
b8e7d9e
·
1 Parent(s): 43df312

Phase 4: Implement Payment Transaction Manager

Browse files

Core implementation:
- Created services/payment_service/ package
- PaymentTransactionManager with full payment lifecycle
- Added /payments/analytics endpoint

Features:
- create_order(): Centralized order creation
- verify_payment(): Payment verification and marking as paid
- mark_failed(): Failure handling with error logging
- get_transaction_history(): User payment history
- get_analytics(): Revenue, success rate, credits purchased stats

Analytics endpoint returns:
- Total transactions
- Successful/failed counts
- Success rate percentage
- Total revenue (paise & rupees)
- Total credits purchased
- Average transaction value

Benefits:
- Single source of truth for payment operations
- Better analytics and reporting
- Consistent error handling
- Easier to add features (refunds, disputes, etc.)

All 4 phases complete! 🎉

routers/payments.py CHANGED
@@ -636,3 +636,27 @@ async def get_payment_history(
636
  page=page,
637
  limit=limit
638
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  page=page,
637
  limit=limit
638
  )
639
+
640
+
641
+ @router.get("/analytics")
642
+ async def get_payment_analytics(
643
+ request: Request,
644
+ db: AsyncSession = Depends(get_db)
645
+ ):
646
+ """
647
+ Get payment analytics for the current user.
648
+
649
+ Returns:
650
+ Analytics including total revenue, success rate, etc.
651
+ """
652
+ user = request.state.user
653
+
654
+ from services.payment_service import PaymentTransactionManager
655
+
656
+ analytics = await PaymentTransactionManager.get_analytics(
657
+ session=db,
658
+ user_id=user.id
659
+ )
660
+
661
+ return analytics
662
+
services/payment_service/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Payment Service Package
3
+
4
+ Centralized payment transaction management.
5
+ """
6
+ from services.payment_service.transaction_manager import (
7
+ PaymentTransactionManager,
8
+ PaymentTransactionError,
9
+ TransactionNotFoundError
10
+ )
11
+
12
+ __all__ = [
13
+ 'PaymentTransactionManager',
14
+ 'PaymentTransactionError',
15
+ 'TransactionNotFoundError',
16
+ ]
services/payment_service/transaction_manager.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Payment Transaction Manager
3
+
4
+ Centralized payment transaction management similar to CreditTransactionManager.
5
+ Handles order creation, verification, and analytics.
6
+ """
7
+ import uuid
8
+ import logging
9
+ from datetime import datetime
10
+ from typing import Optional, List, Dict, Any
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+ from sqlalchemy import select, func
13
+
14
+ from core.models import PaymentTransaction, User
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class PaymentTransactionError(Exception):
20
+ """Base exception for payment transaction errors."""
21
+ pass
22
+
23
+
24
+ class TransactionNotFoundError(PaymentTransactionError):
25
+ """Transaction not found."""
26
+ pass
27
+
28
+
29
+ class PaymentTransactionManager:
30
+ """
31
+ Centralized manager for all payment transactions.
32
+
33
+ Similar to CreditTransactionManager, provides single source of truth
34
+ for payment operations.
35
+ """
36
+
37
+ @staticmethod
38
+ async def create_order(
39
+ session: AsyncSession,
40
+ user: User,
41
+ package_id: str,
42
+ credits_amount: int,
43
+ amount_paise: int,
44
+ currency: str,
45
+ gateway: str,
46
+ gateway_order_id: str,
47
+ metadata: Optional[Dict[str, Any]] = None
48
+ ) -> PaymentTransaction:
49
+ """
50
+ Create a payment order.
51
+
52
+ Args:
53
+ session: Database session
54
+ user: User making payment
55
+ package_id: Package identifier (e.g., "basic_50")
56
+ credits_amount: Number of credits
57
+ amount_paise: Amount in paise
58
+ currency: Currency code (INR)
59
+ gateway: Payment gateway (razorpay)
60
+ gateway_order_id: Gateway's order ID
61
+ metadata: Additional metadata
62
+
63
+ Returns:
64
+ PaymentTransaction instance
65
+ """
66
+ transaction = PaymentTransaction(
67
+ transaction_id=f"txn_{uuid.uuid4().hex[:16]}",
68
+ user_id=user.id,
69
+ gateway=gateway,
70
+ gateway_order_id=gateway_order_id,
71
+ package_id=package_id,
72
+ credits_amount=credits_amount,
73
+ amount_paise=amount_paise,
74
+ currency=currency,
75
+ status="created",
76
+ metadata=metadata or {},
77
+ created_at=datetime.utcnow()
78
+ )
79
+
80
+ session.add(transaction)
81
+
82
+ logger.info(
83
+ f"Created payment order: {transaction.transaction_id} "
84
+ f"for user {user.id}, {credits_amount} credits, "
85
+ f"₹{amount_paise/100:.2f}"
86
+ )
87
+
88
+ return transaction
89
+
90
+ @staticmethod
91
+ async def verify_payment(
92
+ session: AsyncSession,
93
+ transaction_id: str,
94
+ gateway_payment_id: str,
95
+ verified_by: str = "signature",
96
+ metadata: Optional[Dict[str, Any]] = None
97
+ ) -> PaymentTransaction:
98
+ """
99
+ Verify and mark payment as successful.
100
+
101
+ Args:
102
+ session: Database session
103
+ transaction_id: Our transaction ID
104
+ gateway_payment_id: Gateway's payment ID
105
+ verified_by: Verification method (signature/webhook)
106
+ metadata: Additional metadata
107
+
108
+ Returns:
109
+ Updated PaymentTransaction
110
+
111
+ Raises:
112
+ TransactionNotFoundError: If transaction not found
113
+ """
114
+ # Get transaction
115
+ query = select(PaymentTransaction).where(
116
+ PaymentTransaction.transaction_id == transaction_id
117
+ )
118
+ result = await session.execute(query)
119
+ transaction = result.scalar_one_or_none()
120
+
121
+ if not transaction:
122
+ raise TransactionNotFoundError(f"Transaction {transaction_id} not found")
123
+
124
+ # Update transaction
125
+ transaction.gateway_payment_id = gateway_payment_id
126
+ transaction.status = "paid"
127
+ transaction.verified_by = verified_by
128
+ transaction.paid_at = datetime.utcnow()
129
+
130
+ if metadata:
131
+ transaction.metadata.update(metadata)
132
+
133
+ logger.info(
134
+ f"Verified payment: {transaction_id}, "
135
+ f"payment_id: {gateway_payment_id}, "
136
+ f"method: {verified_by}"
137
+ )
138
+
139
+ return transaction
140
+
141
+ @staticmethod
142
+ async def mark_failed(
143
+ session: AsyncSession,
144
+ transaction_id: str,
145
+ error_message: str,
146
+ metadata: Optional[Dict[str, Any]] = None
147
+ ) -> PaymentTransaction:
148
+ """
149
+ Mark payment as failed.
150
+
151
+ Args:
152
+ session: Database session
153
+ transaction_id: Our transaction ID
154
+ error_message: Failure reason
155
+ metadata: Additional metadata
156
+
157
+ Returns:
158
+ Updated PaymentTransaction
159
+ """
160
+ query = select(PaymentTransaction).where(
161
+ PaymentTransaction.transaction_id == transaction_id
162
+ )
163
+ result = await session.execute(query)
164
+ transaction = result.scalar_one_or_none()
165
+
166
+ if not transaction:
167
+ raise TransactionNotFoundError(f"Transaction {transaction_id} not found")
168
+
169
+ transaction.status = "failed"
170
+ transaction.error_message = error_message[:1000] # Truncate
171
+
172
+ if metadata:
173
+ transaction.metadata.update(metadata)
174
+
175
+ logger.warning(f"Payment failed: {transaction_id}, reason: {error_message}")
176
+
177
+ return transaction
178
+
179
+ @staticmethod
180
+ async def get_transaction_history(
181
+ session: AsyncSession,
182
+ user_id: int,
183
+ limit: int = 50,
184
+ offset: int = 0,
185
+ status: Optional[str] = None
186
+ ) -> List[PaymentTransaction]:
187
+ """
188
+ Get payment transaction history for a user.
189
+
190
+ Args:
191
+ session: Database session
192
+ user_id: User ID
193
+ limit: Maximum number of transactions
194
+ offset: Offset for pagination
195
+ status: Filter by status (paid/failed/created)
196
+
197
+ Returns:
198
+ List of PaymentTransaction objects
199
+ """
200
+ query = select(PaymentTransaction).where(
201
+ PaymentTransaction.user_id == user_id
202
+ )
203
+
204
+ if status:
205
+ query = query.where(PaymentTransaction.status == status)
206
+
207
+ query = query.order_by(
208
+ PaymentTransaction.created_at.desc()
209
+ ).offset(offset).limit(limit)
210
+
211
+ result = await session.execute(query)
212
+ return list(result.scalars().all())
213
+
214
+ @staticmethod
215
+ async def get_analytics(
216
+ session: AsyncSession,
217
+ user_id: Optional[int] = None
218
+ ) -> Dict[str, Any]:
219
+ """
220
+ Get payment analytics.
221
+
222
+ Args:
223
+ session: Database session
224
+ user_id: Optional user ID (if None, get global stats)
225
+
226
+ Returns:
227
+ Dict with analytics data
228
+ """
229
+ # Base query
230
+ base_query = select(PaymentTransaction)
231
+ if user_id:
232
+ base_query = base_query.where(PaymentTransaction.user_id == user_id)
233
+
234
+ # Total transactions
235
+ total_count = await session.scalar(
236
+ select(func.count()).select_from(base_query.subquery())
237
+ )
238
+
239
+ # Successful transactions
240
+ paid_query = base_query.where(PaymentTransaction.status == "paid")
241
+ paid_count = await session.scalar(
242
+ select(func.count()).select_from(paid_query.subquery())
243
+ )
244
+
245
+ # Total revenue (in paise)
246
+ revenue_result = await session.scalar(
247
+ select(func.sum(PaymentTransaction.amount_paise)).select_from(
248
+ paid_query.subquery()
249
+ )
250
+ )
251
+ total_revenue_paise = revenue_result or 0
252
+
253
+ # Total credits purchased
254
+ credits_result = await session.scalar(
255
+ select(func.sum(PaymentTransaction.credits_amount)).select_from(
256
+ paid_query.subquery()
257
+ )
258
+ )
259
+ total_credits = credits_result or 0
260
+
261
+ # Failed transactions
262
+ failed_count = await session.scalar(
263
+ select(func.count()).select_from(
264
+ base_query.where(PaymentTransaction.status == "failed").subquery()
265
+ )
266
+ )
267
+
268
+ # Success rate
269
+ success_rate = (paid_count / total_count * 100) if total_count > 0 else 0
270
+
271
+ return {
272
+ "total_transactions": total_count,
273
+ "successful_payments": paid_count,
274
+ "failed_payments": failed_count,
275
+ "success_rate_percent": round(success_rate, 2),
276
+ "total_revenue_paise": total_revenue_paise,
277
+ "total_revenue_rupees": total_revenue_paise / 100,
278
+ "total_credits_purchased": total_credits,
279
+ "average_transaction_value_paise": (
280
+ total_revenue_paise // paid_count if paid_count > 0 else 0
281
+ )
282
+ }