paytechuz 0.2.20__py3-none-any.whl → 0.2.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of paytechuz might be problematic. Click here for more details.
- paytechuz/integrations/fastapi/routes.py +32 -27
- {paytechuz-0.2.20.dist-info → paytechuz-0.2.22.dist-info}/METADATA +6 -33
- paytechuz-0.2.22.dist-info/RECORD +36 -0
- {paytechuz-0.2.20.dist-info → paytechuz-0.2.22.dist-info}/WHEEL +1 -1
- paytechuz-0.2.22.dist-info/top_level.txt +1 -0
- core/__init__.py +0 -0
- core/base.py +0 -97
- core/constants.py +0 -68
- core/exceptions.py +0 -190
- core/http.py +0 -268
- core/payme/errors.py +0 -25
- core/utils.py +0 -192
- gateways/__init__.py +0 -0
- gateways/click/__init__.py +0 -0
- gateways/click/client.py +0 -199
- gateways/click/merchant.py +0 -265
- gateways/click/webhook.py +0 -227
- gateways/payme/__init__.py +0 -0
- gateways/payme/cards.py +0 -222
- gateways/payme/client.py +0 -262
- gateways/payme/receipts.py +0 -336
- gateways/payme/webhook.py +0 -379
- integrations/__init__.py +0 -0
- integrations/django/__init__.py +0 -4
- integrations/django/admin.py +0 -78
- integrations/django/apps.py +0 -21
- integrations/django/migrations/0001_initial.py +0 -51
- integrations/django/migrations/__init__.py +0 -3
- integrations/django/models.py +0 -174
- integrations/django/signals.py +0 -46
- integrations/django/views.py +0 -102
- integrations/django/webhooks.py +0 -884
- integrations/fastapi/__init__.py +0 -21
- integrations/fastapi/models.py +0 -183
- integrations/fastapi/routes.py +0 -1038
- integrations/fastapi/schemas.py +0 -116
- paytechuz-0.2.20.dist-info/RECORD +0 -67
- paytechuz-0.2.20.dist-info/top_level.txt +0 -4
integrations/fastapi/routes.py
DELETED
|
@@ -1,1038 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
FastAPI routes for PayTechUZ.
|
|
3
|
-
"""
|
|
4
|
-
import base64
|
|
5
|
-
import hashlib
|
|
6
|
-
import json
|
|
7
|
-
import logging
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from decimal import Decimal
|
|
10
|
-
from typing import Dict, Any, Optional
|
|
11
|
-
|
|
12
|
-
from fastapi import (
|
|
13
|
-
APIRouter,
|
|
14
|
-
HTTPException,
|
|
15
|
-
Request,
|
|
16
|
-
Response,
|
|
17
|
-
status
|
|
18
|
-
)
|
|
19
|
-
from sqlalchemy.orm import Session
|
|
20
|
-
|
|
21
|
-
from paytechuz.core.exceptions import (
|
|
22
|
-
PermissionDenied,
|
|
23
|
-
InvalidAmount,
|
|
24
|
-
TransactionNotFound,
|
|
25
|
-
AccountNotFound,
|
|
26
|
-
MethodNotFound,
|
|
27
|
-
UnsupportedMethod,
|
|
28
|
-
InvalidAccount
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
from .models import PaymentTransaction
|
|
32
|
-
|
|
33
|
-
logger = logging.getLogger(__name__)
|
|
34
|
-
|
|
35
|
-
router = APIRouter()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class PaymeWebhookHandler:
|
|
39
|
-
"""
|
|
40
|
-
Base Payme webhook handler for FastAPI.
|
|
41
|
-
|
|
42
|
-
This class handles webhook requests from the Payme payment system.
|
|
43
|
-
You can extend this class and override the event methods to customize
|
|
44
|
-
the behavior.
|
|
45
|
-
|
|
46
|
-
Example:
|
|
47
|
-
```python
|
|
48
|
-
from paytechuz.integrations.fastapi.routes import PaymeWebhookHandler
|
|
49
|
-
|
|
50
|
-
class CustomPaymeWebhookHandler(PaymeWebhookHandler):
|
|
51
|
-
def successfully_payment(self, params, transaction):
|
|
52
|
-
# Your custom logic here
|
|
53
|
-
print(f"Payment successful: {transaction.transaction_id}")
|
|
54
|
-
|
|
55
|
-
# Update your order status
|
|
56
|
-
order = db.query(Order).filter(
|
|
57
|
-
Order.id == transaction.account_id
|
|
58
|
-
).first()
|
|
59
|
-
order.status = 'paid'
|
|
60
|
-
db.commit()
|
|
61
|
-
```
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
def __init__(
|
|
65
|
-
self,
|
|
66
|
-
db: Session,
|
|
67
|
-
payme_id: str,
|
|
68
|
-
payme_key: str,
|
|
69
|
-
account_model: Any,
|
|
70
|
-
account_field: str = 'id',
|
|
71
|
-
amount_field: str = 'amount',
|
|
72
|
-
one_time_payment: bool = True
|
|
73
|
-
):
|
|
74
|
-
"""
|
|
75
|
-
Initialize the Payme webhook handler.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
db: Database session
|
|
79
|
-
payme_id: Payme merchant ID
|
|
80
|
-
payme_key: Payme merchant key
|
|
81
|
-
account_model: Account model class
|
|
82
|
-
account_field: Account field name
|
|
83
|
-
amount_field: Amount field name
|
|
84
|
-
one_time_payment: Whether to validate amount
|
|
85
|
-
"""
|
|
86
|
-
self.db = db
|
|
87
|
-
self.payme_id = payme_id
|
|
88
|
-
self.payme_key = payme_key
|
|
89
|
-
self.account_model = account_model
|
|
90
|
-
self.account_field = account_field
|
|
91
|
-
self.amount_field = amount_field
|
|
92
|
-
self.one_time_payment = one_time_payment
|
|
93
|
-
|
|
94
|
-
async def handle_webhook(self, request: Request) -> Response:
|
|
95
|
-
"""
|
|
96
|
-
Handle webhook request from Payme.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
request: FastAPI request object
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
Response object with JSON data
|
|
103
|
-
"""
|
|
104
|
-
try:
|
|
105
|
-
# Check authorization
|
|
106
|
-
auth_header = request.headers.get('Authorization')
|
|
107
|
-
self._check_auth(auth_header)
|
|
108
|
-
|
|
109
|
-
# Parse request data
|
|
110
|
-
data = await request.json()
|
|
111
|
-
method = data.get('method')
|
|
112
|
-
params = data.get('params', {})
|
|
113
|
-
request_id = data.get('id', 0)
|
|
114
|
-
|
|
115
|
-
# Process the request based on the method
|
|
116
|
-
if method == 'CheckPerformTransaction':
|
|
117
|
-
result = self._check_perform_transaction(params)
|
|
118
|
-
elif method == 'CreateTransaction':
|
|
119
|
-
result = self._create_transaction(params)
|
|
120
|
-
elif method == 'PerformTransaction':
|
|
121
|
-
result = self._perform_transaction(params)
|
|
122
|
-
elif method == 'CheckTransaction':
|
|
123
|
-
result = self._check_transaction(params)
|
|
124
|
-
elif method == 'CancelTransaction':
|
|
125
|
-
result = self._cancel_transaction(params)
|
|
126
|
-
elif method == 'GetStatement':
|
|
127
|
-
result = self._get_statement(params)
|
|
128
|
-
else:
|
|
129
|
-
return Response(
|
|
130
|
-
content=json.dumps({
|
|
131
|
-
'jsonrpc': '2.0',
|
|
132
|
-
'id': request_id,
|
|
133
|
-
'error': {
|
|
134
|
-
'code': -32601,
|
|
135
|
-
'message': f"Method not supported: {method}"
|
|
136
|
-
}
|
|
137
|
-
}),
|
|
138
|
-
media_type="application/json",
|
|
139
|
-
status_code=200 # Always return 200 for RPC-style errors
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
# Return the result
|
|
143
|
-
return Response(
|
|
144
|
-
content=json.dumps({
|
|
145
|
-
'jsonrpc': '2.0',
|
|
146
|
-
'id': request_id,
|
|
147
|
-
'result': result
|
|
148
|
-
}),
|
|
149
|
-
media_type="application/json",
|
|
150
|
-
status_code=200
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
except PermissionDenied:
|
|
154
|
-
return Response(
|
|
155
|
-
content=json.dumps({
|
|
156
|
-
'jsonrpc': '2.0',
|
|
157
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
158
|
-
'error': {
|
|
159
|
-
'code': -32504,
|
|
160
|
-
'message': "permission denied"
|
|
161
|
-
}
|
|
162
|
-
}),
|
|
163
|
-
media_type="application/json",
|
|
164
|
-
status_code=200 # Return 200 status code for all errors
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
except (MethodNotFound, UnsupportedMethod) as e:
|
|
168
|
-
return Response(
|
|
169
|
-
content=json.dumps({
|
|
170
|
-
'jsonrpc': '2.0',
|
|
171
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
172
|
-
'error': {
|
|
173
|
-
'code': -32601,
|
|
174
|
-
'message': str(e)
|
|
175
|
-
}
|
|
176
|
-
}),
|
|
177
|
-
media_type="application/json",
|
|
178
|
-
status_code=200 # Return 200 status code for all errors
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
except AccountNotFound as e:
|
|
182
|
-
return Response(
|
|
183
|
-
content=json.dumps({
|
|
184
|
-
'jsonrpc': '2.0',
|
|
185
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
186
|
-
'error': {
|
|
187
|
-
# Code for account not found, in the range -31099 to -31050
|
|
188
|
-
'code': -31050,
|
|
189
|
-
'message': str(e)
|
|
190
|
-
}
|
|
191
|
-
}),
|
|
192
|
-
media_type="application/json",
|
|
193
|
-
status_code=200 # Return 200 status code for all errors
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
except InvalidAmount as e:
|
|
197
|
-
return Response(
|
|
198
|
-
content=json.dumps({
|
|
199
|
-
'jsonrpc': '2.0',
|
|
200
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
201
|
-
'error': {
|
|
202
|
-
'code': -31001, # Code for invalid amount
|
|
203
|
-
'message': str(e)
|
|
204
|
-
}
|
|
205
|
-
}),
|
|
206
|
-
media_type="application/json",
|
|
207
|
-
status_code=200 # Return 200 status code for all errors
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
except InvalidAccount as e:
|
|
211
|
-
return Response(
|
|
212
|
-
content=json.dumps({
|
|
213
|
-
'jsonrpc': '2.0',
|
|
214
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
215
|
-
'error': {
|
|
216
|
-
# Code for invalid account, in the range -31099 to -31050
|
|
217
|
-
'code': -31050,
|
|
218
|
-
'message': str(e)
|
|
219
|
-
}
|
|
220
|
-
}),
|
|
221
|
-
media_type="application/json",
|
|
222
|
-
status_code=200 # Return 200 status code for all errors
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
except TransactionNotFound as e:
|
|
226
|
-
return Response(
|
|
227
|
-
content=json.dumps({
|
|
228
|
-
'jsonrpc': '2.0',
|
|
229
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
230
|
-
'error': {
|
|
231
|
-
'code': -31001,
|
|
232
|
-
'message': str(e)
|
|
233
|
-
}
|
|
234
|
-
}),
|
|
235
|
-
media_type="application/json",
|
|
236
|
-
status_code=200 # Return 200 status code for all errors
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
except Exception as e:
|
|
240
|
-
logger.exception(f"Unexpected error in Payme webhook: {e}")
|
|
241
|
-
return Response(
|
|
242
|
-
content=json.dumps({
|
|
243
|
-
'jsonrpc': '2.0',
|
|
244
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
245
|
-
'error': {
|
|
246
|
-
'code': -32400,
|
|
247
|
-
'message': 'Internal error'
|
|
248
|
-
}
|
|
249
|
-
}),
|
|
250
|
-
media_type="application/json",
|
|
251
|
-
status_code=200 # Return 200 status code for all errors
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
def _check_auth(self, auth_header: Optional[str]) -> None:
|
|
255
|
-
"""
|
|
256
|
-
Check authorization header.
|
|
257
|
-
"""
|
|
258
|
-
if not auth_header:
|
|
259
|
-
raise PermissionDenied("Missing authentication credentials")
|
|
260
|
-
|
|
261
|
-
try:
|
|
262
|
-
auth_parts = auth_header.split()
|
|
263
|
-
if len(auth_parts) != 2 or auth_parts[0].lower() != 'basic':
|
|
264
|
-
raise PermissionDenied("Invalid authentication format")
|
|
265
|
-
|
|
266
|
-
auth_decoded = base64.b64decode(auth_parts[1]).decode('utf-8')
|
|
267
|
-
_, password = auth_decoded.split(':') # We only need the password
|
|
268
|
-
|
|
269
|
-
if password != self.payme_key:
|
|
270
|
-
raise PermissionDenied("Invalid merchant key")
|
|
271
|
-
except PermissionDenied:
|
|
272
|
-
# Re-raise permission denied exceptions
|
|
273
|
-
raise
|
|
274
|
-
except Exception as e:
|
|
275
|
-
logger.error(f"Authentication error: {e}")
|
|
276
|
-
raise PermissionDenied("Authentication error")
|
|
277
|
-
|
|
278
|
-
def _find_account(self, params: Dict[str, Any]) -> Any:
|
|
279
|
-
"""
|
|
280
|
-
Find account by parameters.
|
|
281
|
-
"""
|
|
282
|
-
account_value = params.get('account', {}).get(self.account_field)
|
|
283
|
-
if not account_value:
|
|
284
|
-
raise AccountNotFound("Account not found in parameters")
|
|
285
|
-
|
|
286
|
-
# Handle special case for 'order_id' field
|
|
287
|
-
lookup_field = 'id' if self.account_field == 'order_id' else (
|
|
288
|
-
self.account_field
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
# Try to convert account_value to int if it's a string and lookup_field is 'id'
|
|
292
|
-
if (lookup_field == 'id' and isinstance(account_value, str) and
|
|
293
|
-
account_value.isdigit()):
|
|
294
|
-
account_value = int(account_value)
|
|
295
|
-
|
|
296
|
-
account = self.db.query(self.account_model).filter_by(
|
|
297
|
-
**{lookup_field: account_value}
|
|
298
|
-
).first()
|
|
299
|
-
if not account:
|
|
300
|
-
raise AccountNotFound(
|
|
301
|
-
f"Account with {self.account_field}={account_value} not found"
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
return account
|
|
305
|
-
|
|
306
|
-
def _validate_amount(self, account: Any, amount: int) -> bool:
|
|
307
|
-
"""
|
|
308
|
-
Validate payment amount.
|
|
309
|
-
"""
|
|
310
|
-
# If one_time_payment is disabled, we still validate the amount
|
|
311
|
-
# but we don't require it to match exactly
|
|
312
|
-
|
|
313
|
-
expected_amount = Decimal(getattr(account, self.amount_field)) * 100
|
|
314
|
-
received_amount = Decimal(amount)
|
|
315
|
-
|
|
316
|
-
# If one_time_payment is enabled, amount must match exactly
|
|
317
|
-
if self.one_time_payment and expected_amount != received_amount:
|
|
318
|
-
raise InvalidAmount(
|
|
319
|
-
(f"Invalid amount. Expected: {expected_amount}, "
|
|
320
|
-
f"received: {received_amount}")
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
# If one_time_payment is disabled, amount must be positive
|
|
324
|
-
if not self.one_time_payment and received_amount <= 0:
|
|
325
|
-
raise InvalidAmount(
|
|
326
|
-
f"Invalid amount. Amount must be positive, received: {received_amount}"
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
return True
|
|
330
|
-
|
|
331
|
-
def _check_perform_transaction(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
332
|
-
"""
|
|
333
|
-
Handle CheckPerformTransaction method.
|
|
334
|
-
"""
|
|
335
|
-
account = self._find_account(params)
|
|
336
|
-
self._validate_amount(account, params.get('amount'))
|
|
337
|
-
|
|
338
|
-
# Call the event method
|
|
339
|
-
self.before_check_perform_transaction(params, account)
|
|
340
|
-
|
|
341
|
-
return {'allow': True}
|
|
342
|
-
|
|
343
|
-
def _create_transaction(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
344
|
-
"""
|
|
345
|
-
Handle CreateTransaction method.
|
|
346
|
-
"""
|
|
347
|
-
transaction_id = params.get('id')
|
|
348
|
-
account = self._find_account(params)
|
|
349
|
-
amount = params.get('amount')
|
|
350
|
-
|
|
351
|
-
self._validate_amount(account, amount)
|
|
352
|
-
|
|
353
|
-
# Check if there's already a transaction for this account with a different transaction_id
|
|
354
|
-
# Only check if one_time_payment is enabled
|
|
355
|
-
if self.one_time_payment:
|
|
356
|
-
# Check for existing transactions in non-final states
|
|
357
|
-
existing_transactions = self.db.query(PaymentTransaction).filter(
|
|
358
|
-
PaymentTransaction.gateway == PaymentTransaction.PAYME,
|
|
359
|
-
PaymentTransaction.account_id == str(account.id)
|
|
360
|
-
).filter(
|
|
361
|
-
PaymentTransaction.transaction_id != transaction_id
|
|
362
|
-
).all()
|
|
363
|
-
|
|
364
|
-
# Filter out transactions in final states (SUCCESSFULLY, CANCELLED)
|
|
365
|
-
non_final_transactions = [
|
|
366
|
-
t for t in existing_transactions
|
|
367
|
-
if t.state not in [PaymentTransaction.SUCCESSFULLY, PaymentTransaction.CANCELLED]
|
|
368
|
-
]
|
|
369
|
-
|
|
370
|
-
if non_final_transactions:
|
|
371
|
-
# If there's already a transaction for this account with a different transaction_id in a non-final state, raise an error
|
|
372
|
-
raise InvalidAccount(
|
|
373
|
-
f"Account with {self.account_field}={account.id} already has a pending transaction"
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
# Check for existing transaction with the same transaction_id
|
|
377
|
-
transaction = self.db.query(PaymentTransaction).filter(
|
|
378
|
-
PaymentTransaction.gateway == PaymentTransaction.PAYME,
|
|
379
|
-
PaymentTransaction.transaction_id == transaction_id
|
|
380
|
-
).first()
|
|
381
|
-
|
|
382
|
-
if transaction:
|
|
383
|
-
# Call the event method
|
|
384
|
-
self.transaction_already_exists(params, transaction)
|
|
385
|
-
|
|
386
|
-
# For existing transactions, use the original time from extra_data
|
|
387
|
-
# This ensures the same response is returned for repeated calls
|
|
388
|
-
create_time = transaction.extra_data.get('create_time', params.get('time'))
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
'transaction': transaction.transaction_id,
|
|
392
|
-
'state': transaction.state,
|
|
393
|
-
'create_time': create_time,
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
# Create new transaction
|
|
397
|
-
transaction = PaymentTransaction(
|
|
398
|
-
gateway=PaymentTransaction.PAYME,
|
|
399
|
-
transaction_id=transaction_id,
|
|
400
|
-
account_id=account.id,
|
|
401
|
-
amount=Decimal(amount) / 100, # Convert from tiyin to som
|
|
402
|
-
state=PaymentTransaction.INITIATING,
|
|
403
|
-
extra_data={
|
|
404
|
-
'account_field': self.account_field,
|
|
405
|
-
'account_value': params.get('account', {}).get(self.account_field),
|
|
406
|
-
'create_time': params.get('time'),
|
|
407
|
-
'raw_params': params
|
|
408
|
-
}
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
self.db.add(transaction)
|
|
412
|
-
self.db.commit()
|
|
413
|
-
self.db.refresh(transaction)
|
|
414
|
-
|
|
415
|
-
# Call the event method
|
|
416
|
-
self.transaction_created(params, transaction, account)
|
|
417
|
-
|
|
418
|
-
# Use the time from the request params instead of transaction.created_at
|
|
419
|
-
create_time = params.get('time')
|
|
420
|
-
|
|
421
|
-
return {
|
|
422
|
-
'transaction': transaction.transaction_id,
|
|
423
|
-
'state': transaction.state,
|
|
424
|
-
'create_time': create_time,
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
def _perform_transaction(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
428
|
-
"""
|
|
429
|
-
Handle PerformTransaction method.
|
|
430
|
-
"""
|
|
431
|
-
transaction_id = params.get('id')
|
|
432
|
-
|
|
433
|
-
transaction = self.db.query(PaymentTransaction).filter(
|
|
434
|
-
PaymentTransaction.gateway == PaymentTransaction.PAYME,
|
|
435
|
-
PaymentTransaction.transaction_id == transaction_id
|
|
436
|
-
).first()
|
|
437
|
-
|
|
438
|
-
if not transaction:
|
|
439
|
-
raise HTTPException(
|
|
440
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
441
|
-
detail=f"Transaction {transaction_id} not found"
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
# Mark transaction as paid
|
|
445
|
-
transaction.mark_as_paid(self.db)
|
|
446
|
-
|
|
447
|
-
# Call the event method
|
|
448
|
-
self.successfully_payment(params, transaction)
|
|
449
|
-
|
|
450
|
-
return {
|
|
451
|
-
'transaction': transaction.transaction_id,
|
|
452
|
-
'state': transaction.state,
|
|
453
|
-
'perform_time': int(transaction.performed_at.timestamp() * 1000) if transaction.performed_at else 0,
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
def _check_transaction(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
457
|
-
"""
|
|
458
|
-
Handle CheckTransaction method.
|
|
459
|
-
"""
|
|
460
|
-
transaction_id = params.get('id')
|
|
461
|
-
|
|
462
|
-
transaction = self.db.query(PaymentTransaction).filter(
|
|
463
|
-
PaymentTransaction.gateway == PaymentTransaction.PAYME,
|
|
464
|
-
PaymentTransaction.transaction_id == transaction_id
|
|
465
|
-
).first()
|
|
466
|
-
|
|
467
|
-
if not transaction:
|
|
468
|
-
raise HTTPException(
|
|
469
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
470
|
-
detail=f"Transaction {transaction_id} not found"
|
|
471
|
-
)
|
|
472
|
-
|
|
473
|
-
# Call the event method
|
|
474
|
-
self.check_transaction(params, transaction)
|
|
475
|
-
|
|
476
|
-
# Use the original time from extra_data for consistency
|
|
477
|
-
create_time = transaction.extra_data.get('create_time', int(transaction.created_at.timestamp() * 1000))
|
|
478
|
-
|
|
479
|
-
return {
|
|
480
|
-
'transaction': transaction.transaction_id,
|
|
481
|
-
'state': transaction.state,
|
|
482
|
-
'create_time': create_time,
|
|
483
|
-
'perform_time': int(transaction.performed_at.timestamp() * 1000) if transaction.performed_at else 0,
|
|
484
|
-
'cancel_time': int(transaction.cancelled_at.timestamp() * 1000) if transaction.cancelled_at else 0,
|
|
485
|
-
'reason': transaction.reason,
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
def _cancel_response(self, transaction: PaymentTransaction) -> Dict[str, Any]:
|
|
489
|
-
"""
|
|
490
|
-
Helper method to generate cancel transaction response.
|
|
491
|
-
|
|
492
|
-
Args:
|
|
493
|
-
transaction: Transaction object
|
|
494
|
-
|
|
495
|
-
Returns:
|
|
496
|
-
Dict containing the response
|
|
497
|
-
"""
|
|
498
|
-
# Get reason from the transaction model
|
|
499
|
-
reason = transaction.reason
|
|
500
|
-
|
|
501
|
-
# If reason is None, use default reason
|
|
502
|
-
if reason is None:
|
|
503
|
-
from paytechuz.core.constants import PaymeCancelReason
|
|
504
|
-
reason = PaymeCancelReason.REASON_FUND_RETURNED # Default reason 5
|
|
505
|
-
# Update the transaction with the default reason
|
|
506
|
-
transaction.reason = reason
|
|
507
|
-
self.db.commit()
|
|
508
|
-
self.db.refresh(transaction)
|
|
509
|
-
|
|
510
|
-
return {
|
|
511
|
-
'transaction': transaction.transaction_id,
|
|
512
|
-
'state': transaction.state,
|
|
513
|
-
'cancel_time': int(transaction.cancelled_at.timestamp() * 1000) if transaction.cancelled_at else 0,
|
|
514
|
-
'reason': reason,
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
def _cancel_transaction(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
518
|
-
"""
|
|
519
|
-
Handle CancelTransaction method.
|
|
520
|
-
"""
|
|
521
|
-
transaction_id = params.get('id')
|
|
522
|
-
reason = params.get('reason')
|
|
523
|
-
|
|
524
|
-
transaction = self.db.query(PaymentTransaction).filter(
|
|
525
|
-
PaymentTransaction.gateway == PaymentTransaction.PAYME,
|
|
526
|
-
PaymentTransaction.transaction_id == transaction_id
|
|
527
|
-
).first()
|
|
528
|
-
|
|
529
|
-
if not transaction:
|
|
530
|
-
raise HTTPException(
|
|
531
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
532
|
-
detail=f"Transaction {transaction_id} not found"
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
# Check if transaction is already cancelled
|
|
536
|
-
if transaction.state == PaymentTransaction.CANCELLED:
|
|
537
|
-
# If transaction is already cancelled, update the reason if provided
|
|
538
|
-
if 'reason' in params:
|
|
539
|
-
reason = params.get('reason')
|
|
540
|
-
|
|
541
|
-
# If reason is not provided, use default reason from PaymeCancelReason
|
|
542
|
-
if reason is None:
|
|
543
|
-
from paytechuz.core.constants import PaymeCancelReason
|
|
544
|
-
reason = PaymeCancelReason.REASON_FUND_RETURNED # Default reason 5
|
|
545
|
-
|
|
546
|
-
# Convert reason to int if it's a string
|
|
547
|
-
if isinstance(reason, str) and reason.isdigit():
|
|
548
|
-
reason = int(reason)
|
|
549
|
-
|
|
550
|
-
# Store the reason directly in the reason column
|
|
551
|
-
transaction.reason = reason
|
|
552
|
-
|
|
553
|
-
# For backward compatibility, also store in extra_data
|
|
554
|
-
extra_data = transaction.extra_data or {}
|
|
555
|
-
extra_data['cancel_reason'] = reason
|
|
556
|
-
transaction.extra_data = extra_data
|
|
557
|
-
|
|
558
|
-
self.db.commit()
|
|
559
|
-
self.db.refresh(transaction)
|
|
560
|
-
|
|
561
|
-
# Return the existing data
|
|
562
|
-
return self._cancel_response(transaction)
|
|
563
|
-
|
|
564
|
-
# Use mark_as_cancelled method to ensure consistent behavior
|
|
565
|
-
reason = params.get('reason')
|
|
566
|
-
transaction.mark_as_cancelled(self.db, reason=reason)
|
|
567
|
-
|
|
568
|
-
# Ensure the reason is stored in extra_data
|
|
569
|
-
extra_data = transaction.extra_data or {}
|
|
570
|
-
if 'cancel_reason' not in extra_data:
|
|
571
|
-
extra_data['cancel_reason'] = reason if reason is not None else 5 # Default reason 5
|
|
572
|
-
transaction.extra_data = extra_data
|
|
573
|
-
self.db.commit()
|
|
574
|
-
self.db.refresh(transaction)
|
|
575
|
-
|
|
576
|
-
# Call the event method
|
|
577
|
-
self.cancelled_payment(params, transaction)
|
|
578
|
-
|
|
579
|
-
# Return cancel response
|
|
580
|
-
return self._cancel_response(transaction)
|
|
581
|
-
|
|
582
|
-
def _get_statement(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
583
|
-
"""
|
|
584
|
-
Handle GetStatement method.
|
|
585
|
-
"""
|
|
586
|
-
from_date = params.get('from')
|
|
587
|
-
to_date = params.get('to')
|
|
588
|
-
|
|
589
|
-
# Convert milliseconds to datetime objects
|
|
590
|
-
if from_date:
|
|
591
|
-
from_datetime = datetime.fromtimestamp(from_date / 1000)
|
|
592
|
-
else:
|
|
593
|
-
from_datetime = datetime.fromtimestamp(0) # Unix epoch start
|
|
594
|
-
|
|
595
|
-
if to_date:
|
|
596
|
-
to_datetime = datetime.fromtimestamp(to_date / 1000)
|
|
597
|
-
else:
|
|
598
|
-
to_datetime = datetime.now() # Current time
|
|
599
|
-
|
|
600
|
-
# Get transactions in the date range
|
|
601
|
-
transactions = self.db.query(PaymentTransaction).filter(
|
|
602
|
-
PaymentTransaction.gateway == PaymentTransaction.PAYME,
|
|
603
|
-
PaymentTransaction.created_at >= from_datetime,
|
|
604
|
-
PaymentTransaction.created_at <= to_datetime
|
|
605
|
-
).all()
|
|
606
|
-
|
|
607
|
-
# Format transactions for response
|
|
608
|
-
result = []
|
|
609
|
-
for transaction in transactions:
|
|
610
|
-
result.append({
|
|
611
|
-
'id': transaction.transaction_id,
|
|
612
|
-
'time': int(transaction.created_at.timestamp() * 1000),
|
|
613
|
-
'amount': int(transaction.amount * 100), # Convert to tiyin
|
|
614
|
-
'account': {
|
|
615
|
-
self.account_field: transaction.account_id
|
|
616
|
-
},
|
|
617
|
-
'state': transaction.state,
|
|
618
|
-
'create_time': transaction.extra_data.get('create_time', int(transaction.created_at.timestamp() * 1000)),
|
|
619
|
-
'perform_time': int(transaction.performed_at.timestamp() * 1000) if transaction.performed_at else 0,
|
|
620
|
-
'cancel_time': int(transaction.cancelled_at.timestamp() * 1000) if transaction.cancelled_at else 0,
|
|
621
|
-
'reason': transaction.reason,
|
|
622
|
-
})
|
|
623
|
-
|
|
624
|
-
# Call the event method
|
|
625
|
-
self.get_statement(params, result)
|
|
626
|
-
|
|
627
|
-
return {'transactions': result}
|
|
628
|
-
|
|
629
|
-
# Event methods that can be overridden by subclasses
|
|
630
|
-
|
|
631
|
-
def before_check_perform_transaction(self, params: Dict[str, Any], account: Any) -> None:
|
|
632
|
-
"""
|
|
633
|
-
Called before checking if a transaction can be performed.
|
|
634
|
-
|
|
635
|
-
Args:
|
|
636
|
-
params: Request parameters
|
|
637
|
-
account: Account object
|
|
638
|
-
"""
|
|
639
|
-
pass
|
|
640
|
-
|
|
641
|
-
def transaction_already_exists(self, params: Dict[str, Any], transaction: PaymentTransaction) -> None:
|
|
642
|
-
"""
|
|
643
|
-
Called when a transaction already exists.
|
|
644
|
-
|
|
645
|
-
Args:
|
|
646
|
-
params: Request parameters
|
|
647
|
-
transaction: Transaction object
|
|
648
|
-
"""
|
|
649
|
-
pass
|
|
650
|
-
|
|
651
|
-
def transaction_created(self, params: Dict[str, Any], transaction: PaymentTransaction, account: Any) -> None:
|
|
652
|
-
"""
|
|
653
|
-
Called when a transaction is created.
|
|
654
|
-
|
|
655
|
-
Args:
|
|
656
|
-
params: Request parameters
|
|
657
|
-
transaction: Transaction object
|
|
658
|
-
account: Account object
|
|
659
|
-
"""
|
|
660
|
-
pass
|
|
661
|
-
|
|
662
|
-
def successfully_payment(self, params: Dict[str, Any], transaction: PaymentTransaction) -> None:
|
|
663
|
-
"""
|
|
664
|
-
Called when a payment is successful.
|
|
665
|
-
|
|
666
|
-
Args:
|
|
667
|
-
params: Request parameters
|
|
668
|
-
transaction: Transaction object
|
|
669
|
-
"""
|
|
670
|
-
pass
|
|
671
|
-
|
|
672
|
-
def check_transaction(self, params: Dict[str, Any], transaction: PaymentTransaction) -> None:
|
|
673
|
-
"""
|
|
674
|
-
Called when checking a transaction.
|
|
675
|
-
|
|
676
|
-
Args:
|
|
677
|
-
params: Request parameters
|
|
678
|
-
transaction: Transaction object
|
|
679
|
-
"""
|
|
680
|
-
pass
|
|
681
|
-
|
|
682
|
-
def cancelled_payment(self, params: Dict[str, Any], transaction: PaymentTransaction) -> None:
|
|
683
|
-
"""
|
|
684
|
-
Called when a payment is cancelled.
|
|
685
|
-
|
|
686
|
-
Args:
|
|
687
|
-
params: Request parameters
|
|
688
|
-
transaction: Transaction object
|
|
689
|
-
"""
|
|
690
|
-
pass
|
|
691
|
-
|
|
692
|
-
def get_statement(self, params: Dict[str, Any], transactions: list) -> None:
|
|
693
|
-
"""
|
|
694
|
-
Called when getting a statement.
|
|
695
|
-
|
|
696
|
-
Args:
|
|
697
|
-
params: Request parameters
|
|
698
|
-
transactions: List of transactions
|
|
699
|
-
"""
|
|
700
|
-
pass
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
class ClickWebhookHandler:
|
|
704
|
-
"""
|
|
705
|
-
Base Click webhook handler for FastAPI.
|
|
706
|
-
|
|
707
|
-
This class handles webhook requests from the Click payment system.
|
|
708
|
-
You can extend this class and override the event methods to customize
|
|
709
|
-
the behavior.
|
|
710
|
-
|
|
711
|
-
Example:
|
|
712
|
-
```python
|
|
713
|
-
from paytechuz.integrations.fastapi.routes import ClickWebhookHandler
|
|
714
|
-
|
|
715
|
-
class CustomClickWebhookHandler(ClickWebhookHandler):
|
|
716
|
-
def successfully_payment(self, params, transaction):
|
|
717
|
-
# Your custom logic here
|
|
718
|
-
print(f"Payment successful: {transaction.transaction_id}")
|
|
719
|
-
|
|
720
|
-
# Update your order status
|
|
721
|
-
order = db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
722
|
-
order.status = 'paid'
|
|
723
|
-
db.commit()
|
|
724
|
-
```
|
|
725
|
-
"""
|
|
726
|
-
|
|
727
|
-
def __init__(
|
|
728
|
-
self,
|
|
729
|
-
db: Session,
|
|
730
|
-
service_id: str,
|
|
731
|
-
secret_key: str,
|
|
732
|
-
account_model: Any,
|
|
733
|
-
commission_percent: float = 0.0
|
|
734
|
-
):
|
|
735
|
-
"""
|
|
736
|
-
Initialize the Click webhook handler.
|
|
737
|
-
|
|
738
|
-
Args:
|
|
739
|
-
db: Database session
|
|
740
|
-
service_id: Click service ID
|
|
741
|
-
secret_key: Click secret key
|
|
742
|
-
account_model: Account model class
|
|
743
|
-
commission_percent: Commission percentage
|
|
744
|
-
"""
|
|
745
|
-
self.db = db
|
|
746
|
-
self.service_id = service_id
|
|
747
|
-
self.secret_key = secret_key
|
|
748
|
-
self.account_model = account_model
|
|
749
|
-
self.commission_percent = commission_percent
|
|
750
|
-
|
|
751
|
-
async def handle_webhook(self, request: Request) -> Dict[str, Any]:
|
|
752
|
-
"""
|
|
753
|
-
Handle webhook request from Click.
|
|
754
|
-
|
|
755
|
-
Args:
|
|
756
|
-
request: FastAPI request object
|
|
757
|
-
|
|
758
|
-
Returns:
|
|
759
|
-
Response data
|
|
760
|
-
"""
|
|
761
|
-
try:
|
|
762
|
-
# Get parameters from request
|
|
763
|
-
form_data = await request.form()
|
|
764
|
-
params = {key: form_data.get(key) for key in form_data}
|
|
765
|
-
|
|
766
|
-
# Check authorization
|
|
767
|
-
self._check_auth(params)
|
|
768
|
-
|
|
769
|
-
# Extract parameters
|
|
770
|
-
click_trans_id = params.get('click_trans_id')
|
|
771
|
-
merchant_trans_id = params.get('merchant_trans_id')
|
|
772
|
-
amount = float(params.get('amount', 0))
|
|
773
|
-
action = int(params.get('action', -1))
|
|
774
|
-
error = int(params.get('error', 0))
|
|
775
|
-
|
|
776
|
-
# Find account
|
|
777
|
-
try:
|
|
778
|
-
account = self._find_account(merchant_trans_id)
|
|
779
|
-
except Exception:
|
|
780
|
-
logger.error(f"Account not found: {merchant_trans_id}")
|
|
781
|
-
return {
|
|
782
|
-
'click_trans_id': click_trans_id,
|
|
783
|
-
'merchant_trans_id': merchant_trans_id,
|
|
784
|
-
'error': -5,
|
|
785
|
-
'error_note': "User not found"
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
# Validate amount
|
|
789
|
-
try:
|
|
790
|
-
self._validate_amount(amount, float(getattr(account, 'amount', 0)))
|
|
791
|
-
except Exception as e:
|
|
792
|
-
logger.error(f"Invalid amount: {e}")
|
|
793
|
-
return {
|
|
794
|
-
'click_trans_id': click_trans_id,
|
|
795
|
-
'merchant_trans_id': merchant_trans_id,
|
|
796
|
-
'error': -2,
|
|
797
|
-
'error_note': str(e)
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
# Check if transaction already exists
|
|
801
|
-
transaction = self.db.query(PaymentTransaction).filter(
|
|
802
|
-
PaymentTransaction.gateway == PaymentTransaction.CLICK,
|
|
803
|
-
PaymentTransaction.transaction_id == click_trans_id
|
|
804
|
-
).first()
|
|
805
|
-
|
|
806
|
-
if transaction:
|
|
807
|
-
# If transaction is already completed, return success
|
|
808
|
-
if transaction.state == PaymentTransaction.SUCCESSFULLY:
|
|
809
|
-
# Call the event method
|
|
810
|
-
self.transaction_already_exists(params, transaction)
|
|
811
|
-
|
|
812
|
-
return {
|
|
813
|
-
'click_trans_id': click_trans_id,
|
|
814
|
-
'merchant_trans_id': merchant_trans_id,
|
|
815
|
-
'merchant_prepare_id': transaction.id,
|
|
816
|
-
'error': 0,
|
|
817
|
-
'error_note': "Success"
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
# If transaction is cancelled, return error
|
|
821
|
-
if transaction.state == PaymentTransaction.CANCELLED:
|
|
822
|
-
return {
|
|
823
|
-
'click_trans_id': click_trans_id,
|
|
824
|
-
'merchant_trans_id': merchant_trans_id,
|
|
825
|
-
'merchant_prepare_id': transaction.id,
|
|
826
|
-
'error': -9,
|
|
827
|
-
'error_note': "Transaction cancelled"
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
# Handle different actions
|
|
831
|
-
if action == 0: # Prepare
|
|
832
|
-
# Create transaction
|
|
833
|
-
transaction = PaymentTransaction(
|
|
834
|
-
gateway=PaymentTransaction.CLICK,
|
|
835
|
-
transaction_id=click_trans_id,
|
|
836
|
-
account_id=merchant_trans_id,
|
|
837
|
-
amount=amount,
|
|
838
|
-
state=PaymentTransaction.INITIATING,
|
|
839
|
-
extra_data={
|
|
840
|
-
'raw_params': params
|
|
841
|
-
}
|
|
842
|
-
)
|
|
843
|
-
|
|
844
|
-
self.db.add(transaction)
|
|
845
|
-
self.db.commit()
|
|
846
|
-
self.db.refresh(transaction)
|
|
847
|
-
|
|
848
|
-
# Call the event method
|
|
849
|
-
self.transaction_created(params, transaction, account)
|
|
850
|
-
|
|
851
|
-
return {
|
|
852
|
-
'click_trans_id': click_trans_id,
|
|
853
|
-
'merchant_trans_id': merchant_trans_id,
|
|
854
|
-
'merchant_prepare_id': transaction.id,
|
|
855
|
-
'error': 0,
|
|
856
|
-
'error_note': "Success"
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
elif action == 1: # Complete
|
|
860
|
-
# Check if error is negative (payment failed)
|
|
861
|
-
is_successful = error >= 0
|
|
862
|
-
|
|
863
|
-
if not transaction:
|
|
864
|
-
# Create transaction if it doesn't exist
|
|
865
|
-
transaction = PaymentTransaction(
|
|
866
|
-
gateway=PaymentTransaction.CLICK,
|
|
867
|
-
transaction_id=click_trans_id,
|
|
868
|
-
account_id=merchant_trans_id,
|
|
869
|
-
amount=amount,
|
|
870
|
-
state=PaymentTransaction.INITIATING,
|
|
871
|
-
extra_data={
|
|
872
|
-
'raw_params': params
|
|
873
|
-
}
|
|
874
|
-
)
|
|
875
|
-
|
|
876
|
-
self.db.add(transaction)
|
|
877
|
-
self.db.commit()
|
|
878
|
-
self.db.refresh(transaction)
|
|
879
|
-
|
|
880
|
-
if is_successful:
|
|
881
|
-
# Mark transaction as paid
|
|
882
|
-
transaction.mark_as_paid(self.db)
|
|
883
|
-
|
|
884
|
-
# Call the event method
|
|
885
|
-
self.successfully_payment(params, transaction)
|
|
886
|
-
else:
|
|
887
|
-
# Mark transaction as cancelled
|
|
888
|
-
transaction.mark_as_cancelled(self.db, reason=f"Error code: {error}")
|
|
889
|
-
|
|
890
|
-
# Call the event method
|
|
891
|
-
self.cancelled_payment(params, transaction)
|
|
892
|
-
|
|
893
|
-
return {
|
|
894
|
-
'click_trans_id': click_trans_id,
|
|
895
|
-
'merchant_trans_id': merchant_trans_id,
|
|
896
|
-
'merchant_prepare_id': transaction.id,
|
|
897
|
-
'error': 0,
|
|
898
|
-
'error_note': "Success"
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
else:
|
|
902
|
-
logger.error(f"Unsupported action: {action}")
|
|
903
|
-
return {
|
|
904
|
-
'click_trans_id': click_trans_id,
|
|
905
|
-
'merchant_trans_id': merchant_trans_id,
|
|
906
|
-
'error': -3,
|
|
907
|
-
'error_note': "Action not found"
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
except Exception as e:
|
|
911
|
-
logger.exception(f"Unexpected error in Click webhook: {e}")
|
|
912
|
-
return {
|
|
913
|
-
'error': -7,
|
|
914
|
-
'error_note': "Internal error"
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
def _check_auth(self, params: Dict[str, Any]) -> None:
|
|
918
|
-
"""
|
|
919
|
-
Check authentication using signature.
|
|
920
|
-
"""
|
|
921
|
-
if not all([self.service_id, self.secret_key]):
|
|
922
|
-
raise HTTPException(
|
|
923
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
924
|
-
detail="Missing required settings: service_id or secret_key"
|
|
925
|
-
)
|
|
926
|
-
|
|
927
|
-
if str(params.get("service_id")) != self.service_id:
|
|
928
|
-
raise HTTPException(
|
|
929
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
930
|
-
detail="Invalid service ID"
|
|
931
|
-
)
|
|
932
|
-
|
|
933
|
-
sign_string = params.get("sign_string")
|
|
934
|
-
sign_time = params.get("sign_time")
|
|
935
|
-
|
|
936
|
-
if not sign_string or not sign_time:
|
|
937
|
-
raise HTTPException(
|
|
938
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
939
|
-
detail="Missing signature parameters"
|
|
940
|
-
)
|
|
941
|
-
|
|
942
|
-
# Prepare signature components
|
|
943
|
-
text_parts = [
|
|
944
|
-
str(params.get("click_trans_id") or ""),
|
|
945
|
-
str(params.get("service_id") or ""),
|
|
946
|
-
str(self.secret_key or ""),
|
|
947
|
-
str(params.get("merchant_trans_id") or ""),
|
|
948
|
-
str(params.get("merchant_prepare_id") or ""),
|
|
949
|
-
str(params.get("amount") or ""),
|
|
950
|
-
str(params.get("action") or ""),
|
|
951
|
-
str(sign_time)
|
|
952
|
-
]
|
|
953
|
-
|
|
954
|
-
# Calculate hash
|
|
955
|
-
calculated_hash = hashlib.md5("".join(text_parts).encode("utf-8")).hexdigest()
|
|
956
|
-
|
|
957
|
-
if calculated_hash != sign_string:
|
|
958
|
-
raise HTTPException(
|
|
959
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
960
|
-
detail="Invalid signature"
|
|
961
|
-
)
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
def _find_account(self, merchant_trans_id: str) -> Any:
|
|
965
|
-
"""
|
|
966
|
-
Find account by merchant_trans_id.
|
|
967
|
-
"""
|
|
968
|
-
# Try to convert merchant_trans_id to int if it's a string and a digit
|
|
969
|
-
if isinstance(merchant_trans_id, str) and merchant_trans_id.isdigit():
|
|
970
|
-
merchant_trans_id = int(merchant_trans_id)
|
|
971
|
-
|
|
972
|
-
account = self.db.query(self.account_model).filter_by(id=merchant_trans_id).first()
|
|
973
|
-
if not account:
|
|
974
|
-
raise HTTPException(
|
|
975
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
976
|
-
detail=f"Account with id={merchant_trans_id} not found"
|
|
977
|
-
)
|
|
978
|
-
|
|
979
|
-
return account
|
|
980
|
-
|
|
981
|
-
def _validate_amount(self, received_amount: float, expected_amount: float) -> None:
|
|
982
|
-
"""
|
|
983
|
-
Validate payment amount.
|
|
984
|
-
"""
|
|
985
|
-
# Add commission if needed
|
|
986
|
-
if self.commission_percent > 0:
|
|
987
|
-
expected_amount = expected_amount * (1 + self.commission_percent / 100)
|
|
988
|
-
expected_amount = round(expected_amount, 2)
|
|
989
|
-
|
|
990
|
-
# Allow small difference due to floating point precision
|
|
991
|
-
if abs(received_amount - expected_amount) > 0.01:
|
|
992
|
-
raise HTTPException(
|
|
993
|
-
status_code=status.HTTP_400_BAD_REQUEST,
|
|
994
|
-
detail=f"Incorrect amount. Expected: {expected_amount}, received: {received_amount}"
|
|
995
|
-
)
|
|
996
|
-
|
|
997
|
-
# Event methods that can be overridden by subclasses
|
|
998
|
-
|
|
999
|
-
def transaction_already_exists(self, params: Dict[str, Any], transaction: PaymentTransaction) -> None:
|
|
1000
|
-
"""
|
|
1001
|
-
Called when a transaction already exists.
|
|
1002
|
-
|
|
1003
|
-
Args:
|
|
1004
|
-
params: Request parameters
|
|
1005
|
-
transaction: Transaction object
|
|
1006
|
-
"""
|
|
1007
|
-
pass
|
|
1008
|
-
|
|
1009
|
-
def transaction_created(self, params: Dict[str, Any], transaction: PaymentTransaction, account: Any) -> None:
|
|
1010
|
-
"""
|
|
1011
|
-
Called when a transaction is created.
|
|
1012
|
-
|
|
1013
|
-
Args:
|
|
1014
|
-
params: Request parameters
|
|
1015
|
-
transaction: Transaction object
|
|
1016
|
-
account: Account object
|
|
1017
|
-
"""
|
|
1018
|
-
pass
|
|
1019
|
-
|
|
1020
|
-
def successfully_payment(self, params: Dict[str, Any], transaction: PaymentTransaction) -> None:
|
|
1021
|
-
"""
|
|
1022
|
-
Called when a payment is successful.
|
|
1023
|
-
|
|
1024
|
-
Args:
|
|
1025
|
-
params: Request parameters
|
|
1026
|
-
transaction: Transaction object
|
|
1027
|
-
"""
|
|
1028
|
-
pass
|
|
1029
|
-
|
|
1030
|
-
def cancelled_payment(self, params: Dict[str, Any], transaction: PaymentTransaction) -> None:
|
|
1031
|
-
"""
|
|
1032
|
-
Called when a payment is cancelled.
|
|
1033
|
-
|
|
1034
|
-
Args:
|
|
1035
|
-
params: Request parameters
|
|
1036
|
-
transaction: Transaction object
|
|
1037
|
-
"""
|
|
1038
|
-
pass
|