connections-sdk 2.1.0__tar.gz → 3.0.0__tar.gz
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.
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/PKG-INFO +1 -1
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/pyproject.toml +1 -1
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/src/connections_sdk/exceptions.py +1 -4
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/src/connections_sdk/models.py +14 -3
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/src/connections_sdk/providers/adyen.py +18 -18
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/src/connections_sdk/providers/checkout.py +221 -16
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/src/connections_sdk/utils/model_utils.py +34 -6
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/src/connections_sdk/utils/request_client.py +2 -2
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/LICENSE +0 -0
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/README.md +0 -0
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/src/connections_sdk/__init__.py +0 -0
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/src/connections_sdk/client.py +0 -0
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/src/connections_sdk/config.py +0 -0
- {connections_sdk-2.1.0 → connections_sdk-3.0.0}/src/connections_sdk/utils/__init__.py +0 -0
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
from connections_sdk.models import ErrorResponse
|
|
2
2
|
|
|
3
3
|
class TransactionError(Exception):
|
|
4
|
+
"""Raised when request validation fails."""
|
|
4
5
|
error_response: ErrorResponse
|
|
5
6
|
def __init__(self, error_response: 'ErrorResponse'):
|
|
6
7
|
self.error_response = error_response
|
|
7
8
|
super().__init__(str(error_response.error_codes))
|
|
8
9
|
|
|
9
|
-
class ValidationError(Exception):
|
|
10
|
-
"""Raised when request validation fails."""
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
10
|
class ConfigurationError(Exception):
|
|
14
11
|
"""Raised when SDK configuration is invalid."""
|
|
15
12
|
pass
|
|
@@ -89,7 +89,6 @@ class ErrorType(Enum):
|
|
|
89
89
|
self.code = code
|
|
90
90
|
self.category = category
|
|
91
91
|
|
|
92
|
-
|
|
93
92
|
@dataclass
|
|
94
93
|
class Amount:
|
|
95
94
|
value: int
|
|
@@ -185,6 +184,15 @@ class TransactionSource:
|
|
|
185
184
|
id: str
|
|
186
185
|
provisioned: Optional[ProvisionedSource] = None
|
|
187
186
|
|
|
187
|
+
@dataclass
|
|
188
|
+
class ResponseCode:
|
|
189
|
+
category: str
|
|
190
|
+
code: str
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class BasisTheoryExtras:
|
|
194
|
+
trace_id: str
|
|
195
|
+
|
|
188
196
|
|
|
189
197
|
@dataclass
|
|
190
198
|
class TransactionResponse:
|
|
@@ -192,11 +200,12 @@ class TransactionResponse:
|
|
|
192
200
|
reference: str
|
|
193
201
|
amount: Amount
|
|
194
202
|
status: TransactionStatus
|
|
203
|
+
response_code: ResponseCode
|
|
195
204
|
source: TransactionSource
|
|
196
205
|
full_provider_response: Dict[str, Any]
|
|
197
206
|
created_at: datetime
|
|
198
207
|
network_transaction_id: Optional[str] = None
|
|
199
|
-
|
|
208
|
+
basis_theory_extras: Optional[BasisTheoryExtras] = None
|
|
200
209
|
|
|
201
210
|
@dataclass
|
|
202
211
|
class RefundResponse:
|
|
@@ -217,4 +226,6 @@ class ErrorCode:
|
|
|
217
226
|
class ErrorResponse:
|
|
218
227
|
error_codes: List[ErrorCode]
|
|
219
228
|
provider_errors: List[str]
|
|
220
|
-
full_provider_response: Dict[str, Any]
|
|
229
|
+
full_provider_response: Dict[str, Any]
|
|
230
|
+
basis_theory_extras: Optional[BasisTheoryExtras] = None
|
|
231
|
+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Dict, Any, Tuple, Optional, Union, cast
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
3
|
import requests
|
|
4
|
+
from requests.structures import CaseInsensitiveDict
|
|
4
5
|
from deepmerge import always_merger
|
|
5
6
|
from ..models import (
|
|
6
7
|
TransactionRequest,
|
|
@@ -22,9 +23,10 @@ from ..models import (
|
|
|
22
23
|
ErrorCode,
|
|
23
24
|
TransactionResponse,
|
|
24
25
|
TransactionSource,
|
|
25
|
-
ProvisionedSource
|
|
26
|
+
ProvisionedSource,
|
|
27
|
+
ResponseCode
|
|
26
28
|
)
|
|
27
|
-
from ..utils.model_utils import create_transaction_request, validate_required_fields
|
|
29
|
+
from ..utils.model_utils import create_transaction_request, validate_required_fields, _basis_theory_extras
|
|
28
30
|
from ..utils.request_client import RequestClient
|
|
29
31
|
from ..exceptions import TransactionError
|
|
30
32
|
|
|
@@ -240,25 +242,30 @@ class AdyenClient:
|
|
|
240
242
|
|
|
241
243
|
return payload
|
|
242
244
|
|
|
243
|
-
def _transform_adyen_response(self, response_data: Dict[str, Any], request: TransactionRequest) -> TransactionResponse:
|
|
245
|
+
def _transform_adyen_response(self, response_data: Dict[str, Any], request: TransactionRequest, headers: CaseInsensitiveDict) -> TransactionResponse:
|
|
244
246
|
"""Transform Adyen response to our standardized format."""
|
|
245
247
|
transaction_response = TransactionResponse(
|
|
246
248
|
id=str(response_data.get("pspReference")),
|
|
247
249
|
reference=str(response_data.get("merchantReference")),
|
|
248
250
|
amount=Amount(
|
|
249
|
-
value=int(response_data.get("amount", {}).get("value")),
|
|
250
|
-
currency=str(response_data.get("amount", {}).get("currency"))
|
|
251
|
+
value=int(response_data.get("amount", {}).get("value", request.amount.value)),
|
|
252
|
+
currency=str(response_data.get("amount", {}).get("currency", request.amount.currency))
|
|
251
253
|
),
|
|
252
254
|
status=TransactionStatus(
|
|
253
255
|
code=self._get_status_code(response_data.get("resultCode")),
|
|
254
256
|
provider_code=str(response_data.get("resultCode"))
|
|
255
257
|
),
|
|
258
|
+
response_code=ResponseCode(
|
|
259
|
+
category=ERROR_CODE_MAPPING.get(str(response_data.get("refusalReasonCode")), ErrorType.OTHER).category,
|
|
260
|
+
code=ERROR_CODE_MAPPING.get(str(response_data.get("refusalReasonCode")), ErrorType.OTHER).code
|
|
261
|
+
),
|
|
256
262
|
source=TransactionSource(
|
|
257
263
|
type=request.source.type,
|
|
258
264
|
id=request.source.id,
|
|
259
265
|
),
|
|
260
266
|
network_transaction_id=str(response_data.get("additionalData", {}).get("networkTxReference")),
|
|
261
267
|
full_provider_response=response_data,
|
|
268
|
+
basis_theory_extras=_basis_theory_extras(headers),
|
|
262
269
|
created_at=datetime.now(timezone.utc)
|
|
263
270
|
)
|
|
264
271
|
|
|
@@ -272,7 +279,7 @@ class AdyenClient:
|
|
|
272
279
|
|
|
273
280
|
return transaction_response
|
|
274
281
|
|
|
275
|
-
def _transform_error_response(self, response: requests.Response, response_data: Dict[str, Any]) -> ErrorResponse:
|
|
282
|
+
def _transform_error_response(self, response: requests.Response, response_data: Dict[str, Any], headers: CaseInsensitiveDict) -> ErrorResponse:
|
|
276
283
|
"""Transform error responses to our standardized format.
|
|
277
284
|
|
|
278
285
|
Args:
|
|
@@ -287,10 +294,6 @@ class AdyenClient:
|
|
|
287
294
|
error_type = ErrorType.INVALID_API_KEY
|
|
288
295
|
elif response.status_code == 403:
|
|
289
296
|
error_type = ErrorType.UNAUTHORIZED
|
|
290
|
-
# Handle Adyen-specific error codes for declined transactions
|
|
291
|
-
elif response_data.get("resultCode") in ["Refused", "Error", "Cancelled"]:
|
|
292
|
-
refusal_code = response_data.get("refusalReasonCode", "")
|
|
293
|
-
error_type = ERROR_CODE_MAPPING.get(refusal_code, ErrorType.OTHER)
|
|
294
297
|
else:
|
|
295
298
|
error_type = ErrorType.OTHER
|
|
296
299
|
|
|
@@ -302,7 +305,8 @@ class AdyenClient:
|
|
|
302
305
|
)
|
|
303
306
|
],
|
|
304
307
|
provider_errors=[response_data.get("refusalReason") or response_data.get("message", "")],
|
|
305
|
-
full_provider_response=response_data
|
|
308
|
+
full_provider_response=response_data,
|
|
309
|
+
basis_theory_extras=_basis_theory_extras(headers)
|
|
306
310
|
)
|
|
307
311
|
|
|
308
312
|
|
|
@@ -331,12 +335,8 @@ class AdyenClient:
|
|
|
331
335
|
|
|
332
336
|
response_data = response.json()
|
|
333
337
|
|
|
334
|
-
# Check if it's an error response (non-200 status code or Adyen error)
|
|
335
|
-
if not response.ok or response_data.get("resultCode") in ["Refused", "Error", "Cancelled"]:
|
|
336
|
-
raise TransactionError(self._transform_error_response(response, response_data))
|
|
337
|
-
|
|
338
338
|
# Transform the successful response to our format
|
|
339
|
-
return self._transform_adyen_response(response_data, request_data)
|
|
339
|
+
return self._transform_adyen_response(response_data, request_data, response.headers)
|
|
340
340
|
|
|
341
341
|
except requests.exceptions.HTTPError as e:
|
|
342
342
|
try:
|
|
@@ -344,7 +344,7 @@ class AdyenClient:
|
|
|
344
344
|
except:
|
|
345
345
|
error_data = None
|
|
346
346
|
|
|
347
|
-
raise TransactionError(self._transform_error_response(e.response, error_data))
|
|
347
|
+
raise TransactionError(self._transform_error_response(e.response, error_data, e.response.headers))
|
|
348
348
|
|
|
349
349
|
|
|
350
350
|
def refund_transaction(self, refund_request: RefundRequest) -> RefundResponse:
|
|
@@ -412,5 +412,5 @@ class AdyenClient:
|
|
|
412
412
|
except:
|
|
413
413
|
error_data = None
|
|
414
414
|
|
|
415
|
-
raise TransactionError(self._transform_error_response(e.response, error_data))
|
|
415
|
+
raise TransactionError(self._transform_error_response(e.response, error_data, e.response.headers))
|
|
416
416
|
|
|
@@ -5,7 +5,7 @@ import requests
|
|
|
5
5
|
import os
|
|
6
6
|
import json
|
|
7
7
|
from json.decoder import JSONDecodeError
|
|
8
|
-
|
|
8
|
+
from requests.structures import CaseInsensitiveDict
|
|
9
9
|
from ..models import (
|
|
10
10
|
TransactionRequest,
|
|
11
11
|
Amount,
|
|
@@ -26,10 +26,11 @@ from ..models import (
|
|
|
26
26
|
ErrorCode,
|
|
27
27
|
TransactionResponse,
|
|
28
28
|
TransactionSource,
|
|
29
|
-
ProvisionedSource
|
|
29
|
+
ProvisionedSource,
|
|
30
|
+
ResponseCode
|
|
30
31
|
)
|
|
31
32
|
from connections_sdk.exceptions import TransactionError
|
|
32
|
-
from ..utils.model_utils import create_transaction_request, validate_required_fields
|
|
33
|
+
from ..utils.model_utils import create_transaction_request, validate_required_fields, _basis_theory_extras
|
|
33
34
|
from ..utils.request_client import RequestClient
|
|
34
35
|
|
|
35
36
|
|
|
@@ -169,6 +170,183 @@ ERROR_CODE_MAPPING = {
|
|
|
169
170
|
"refund_authorization_declined": ErrorType.REFUND_DECLINED
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
# Mapping of Checkout.com numerical response codes to our error types
|
|
174
|
+
CHECKOUT_NUMERICAL_CODE_MAPPING = {
|
|
175
|
+
# 20xxx Series - Generally Soft Declines / Informational
|
|
176
|
+
"20001": ErrorType.REFERRAL, # Refer to card issuer
|
|
177
|
+
"20002": ErrorType.REFERRAL, # Refer to card issuer - Special conditions
|
|
178
|
+
"20003": ErrorType.CONFIGURATION_ERROR, # Invalid merchant or service provider
|
|
179
|
+
"20004": ErrorType.BLOCKED_CARD, # Card should be captured
|
|
180
|
+
"20005": ErrorType.REFUSED, # Declined - Do not honour
|
|
181
|
+
"20006": ErrorType.OTHER, # Error / Invalid request parameters
|
|
182
|
+
"20009": ErrorType.OTHER, # Request in progress (treating as a final decline state if listed as error)
|
|
183
|
+
"20012": ErrorType.OTHER, # Invalid transaction
|
|
184
|
+
"20013": ErrorType.INVALID_AMOUNT, # Invalid value/amount
|
|
185
|
+
"20014": ErrorType.INVALID_CARD, # Invalid account number (no such number)
|
|
186
|
+
"20015": ErrorType.NOT_SUPPORTED, # Transaction cannot be processed through debit network
|
|
187
|
+
"20016": ErrorType.RESTRICTED_CARD, # Card not initialised
|
|
188
|
+
"20017": ErrorType.PAYMENT_CANCELLED_BY_CONSUMER, # Customer cancellation
|
|
189
|
+
"20018": ErrorType.OTHER, # Customer dispute (more of a post-transaction event, but if it's an error code at payment time)
|
|
190
|
+
"20019": ErrorType.PAYMENT_CANCELLED, # Re-enter transaction Transaction has expired
|
|
191
|
+
"20020": ErrorType.OTHER, # Invalid response
|
|
192
|
+
"20021": ErrorType.OTHER, # No action taken (unable to back out prior transaction)
|
|
193
|
+
"20022": ErrorType.ACQUIRER_ERROR, # Suspected malfunction
|
|
194
|
+
"20023": ErrorType.OTHER, # Unacceptable transaction fee
|
|
195
|
+
"20024": ErrorType.NOT_SUPPORTED, # File update not supported by the receiver
|
|
196
|
+
"20025": ErrorType.OTHER, # Unable to locate record on file Account number is missing from the inquiry
|
|
197
|
+
"20026": ErrorType.OTHER, # Duplicate file update record
|
|
198
|
+
"20027": ErrorType.OTHER, # File update field edit error
|
|
199
|
+
"20028": ErrorType.OTHER, # File is temporarily unavailable
|
|
200
|
+
"20029": ErrorType.OTHER, # File update not successful
|
|
201
|
+
"20030": ErrorType.OTHER, # Format error
|
|
202
|
+
"20031": ErrorType.NOT_SUPPORTED, # Bank not supported by Switch
|
|
203
|
+
"20032": ErrorType.OTHER, # Completed partially (typically a success state, but if listed as error)
|
|
204
|
+
"20033": ErrorType.OTHER, # Previous scheme transaction ID invalid
|
|
205
|
+
"20038": ErrorType.PIN_TRIES_EXCEEDED, # Allowable PIN tries exceeded
|
|
206
|
+
"20039": ErrorType.INVALID_CARD, # No credit account
|
|
207
|
+
"20040": ErrorType.NOT_SUPPORTED, # Requested function not supported
|
|
208
|
+
"20042": ErrorType.INVALID_AMOUNT, # No universal value/amount
|
|
209
|
+
"20044": ErrorType.INVALID_CARD, # No investment account
|
|
210
|
+
"20045": ErrorType.NOT_SUPPORTED, # The Issuer does not support fallback transactions of hybrid-card
|
|
211
|
+
"20046": ErrorType.BANK_ERROR, # Bank decline
|
|
212
|
+
"20051": ErrorType.INSUFFICENT_FUNDS, # Insufficient funds
|
|
213
|
+
"20052": ErrorType.INVALID_CARD, # No current (checking) account
|
|
214
|
+
"20053": ErrorType.INVALID_CARD, # No savings account
|
|
215
|
+
"20054": ErrorType.EXPIRED_CARD, # Expired card
|
|
216
|
+
"20055": ErrorType.INVALID_PIN, # Incorrect PIN PIN validation not possible
|
|
217
|
+
"20056": ErrorType.INVALID_CARD, # No card record
|
|
218
|
+
"20057": ErrorType.NOT_SUPPORTED, # Transaction not permitted to cardholder
|
|
219
|
+
"20058": ErrorType.NOT_SUPPORTED, # Transaction not permitted to terminal
|
|
220
|
+
"20059": ErrorType.FRAUD, # Suspected fraud
|
|
221
|
+
"20060": ErrorType.REFERRAL, # Card acceptor contact acquirer
|
|
222
|
+
"20061": ErrorType.INSUFFICENT_FUNDS, # Activity amount limit exceeded
|
|
223
|
+
"20062": ErrorType.RESTRICTED_CARD, # Restricted card
|
|
224
|
+
"20063": ErrorType.FRAUD, # Security violation
|
|
225
|
+
"20064": ErrorType.OTHER, # Transaction does not fulfil AML requirement
|
|
226
|
+
"20065": ErrorType.INSUFFICENT_FUNDS, # Exceeds Withdrawal Frequency Limit
|
|
227
|
+
"20066": ErrorType.REFERRAL, # Card acceptor call acquirer security
|
|
228
|
+
"20067": ErrorType.BLOCKED_CARD, # Hard capture - Pick up card at ATM
|
|
229
|
+
"20068": ErrorType.ACQUIRER_ERROR, # Response received too late / Timeout
|
|
230
|
+
"20072": ErrorType.RESTRICTED_CARD, # Account not yet activated
|
|
231
|
+
"20075": ErrorType.PIN_TRIES_EXCEEDED, # Allowable PIN-entry tries exceeded
|
|
232
|
+
"20078": ErrorType.BLOCKED_CARD, # Blocked at first use
|
|
233
|
+
"20081": ErrorType.NOT_SUPPORTED, # Card is local use only
|
|
234
|
+
"20082": ErrorType.CVC_INVALID, # No security model / Negative CAM, dCVV, iCVV, or CVV results
|
|
235
|
+
"20083": ErrorType.INVALID_CARD, # No accounts
|
|
236
|
+
"20084": ErrorType.OTHER, # No PBF
|
|
237
|
+
"20085": ErrorType.OTHER, # PBF update error
|
|
238
|
+
"20086": ErrorType.ACQUIRER_ERROR, # ATM malfunction Invalid authorization type
|
|
239
|
+
"20087": ErrorType.INVALID_CARD, # Bad track data (invalid CVV and/or expiry date)
|
|
240
|
+
"20088": ErrorType.OTHER, # Unable to dispense/process
|
|
241
|
+
"20089": ErrorType.OTHER, # Administration error
|
|
242
|
+
"20090": ErrorType.ACQUIRER_ERROR, # Cut-off in progress
|
|
243
|
+
"20091": ErrorType.BANK_ERROR, # Issuer unavailable or switch is inoperative
|
|
244
|
+
"20092": ErrorType.ACQUIRER_ERROR, # Destination cannot be found for routing
|
|
245
|
+
"20093": ErrorType.NOT_SUPPORTED, # Transaction cannot be completed; violation of law
|
|
246
|
+
"20094": ErrorType.OTHER, # Duplicate transmission / invoice
|
|
247
|
+
"20095": ErrorType.OTHER, # Reconcile error
|
|
248
|
+
"20096": ErrorType.ACQUIRER_ERROR, # System malfunction
|
|
249
|
+
"20097": ErrorType.OTHER, # Reconciliation totals reset
|
|
250
|
+
"20098": ErrorType.OTHER, # MAC error
|
|
251
|
+
"20099": ErrorType.OTHER, # Other / Unidentified responses
|
|
252
|
+
"20197": ErrorType.OTHER, # Catch-all for many sub-errors like CVV2 failure, transaction not supported. Mapping to OTHER due to its composite nature.
|
|
253
|
+
"20100": ErrorType.INVALID_CARD, # Invalid expiry date format
|
|
254
|
+
"20101": ErrorType.INVALID_SOURCE_TOKEN, # No Account / No Customer (Token is incorrect or invalid)
|
|
255
|
+
"20102": ErrorType.CONFIGURATION_ERROR, # Invalid merchant / wallet ID
|
|
256
|
+
"20103": ErrorType.NOT_SUPPORTED, # Card type / payment method not supported
|
|
257
|
+
"20104": ErrorType.OTHER, # Gateway reject - Invalid transaction
|
|
258
|
+
"20105": ErrorType.OTHER, # Gateway reject - Violation
|
|
259
|
+
"20106": ErrorType.NOT_SUPPORTED, # Unsupported currency
|
|
260
|
+
"20107": ErrorType.OTHER, # Billing address is missing (Could be AVS_DECLINE if validation fails, but often a data validation before submission)
|
|
261
|
+
"20108": ErrorType.REFUSED, # Declined - Updated cardholder available
|
|
262
|
+
"20109": ErrorType.OTHER, # Transaction already reversed (voided) Capture is larger than initial authorized value
|
|
263
|
+
"20110": ErrorType.OTHER, # Authorization completed (Not an error, but if returned in error context)
|
|
264
|
+
"20111": ErrorType.OTHER, # Transaction already reversed
|
|
265
|
+
"20112": ErrorType.CONFIGURATION_ERROR, # Merchant not Mastercard SecureCode enabled
|
|
266
|
+
"20113": ErrorType.OTHER, # Invalid property
|
|
267
|
+
"20114": ErrorType.INVALID_SOURCE_TOKEN, # Token is incorrect
|
|
268
|
+
"20115": ErrorType.OTHER, # Missing / Invalid lifetime
|
|
269
|
+
"20116": ErrorType.OTHER, # Invalid encoding
|
|
270
|
+
"20117": ErrorType.CONFIGURATION_ERROR, # Invalid API version
|
|
271
|
+
"20118": ErrorType.OTHER, # Transaction pending
|
|
272
|
+
"20119": ErrorType.OTHER, # Invalid batch data and/or batch data is missing
|
|
273
|
+
"20120": ErrorType.OTHER, # Invalid customer/user
|
|
274
|
+
"20121": ErrorType.OTHER, # Transaction limit for merchant/terminal exceeded
|
|
275
|
+
"20122": ErrorType.NOT_SUPPORTED, # Mastercard installments not supported
|
|
276
|
+
"20123": ErrorType.OTHER, # Missing basic data: zip, addr, member
|
|
277
|
+
"20124": ErrorType.CVC_INVALID, # Missing CVV value, required for ecommerce transaction
|
|
278
|
+
"20150": ErrorType.AUTHENTICATION_FAILURE, # Card not 3D Secure (3DS) enabled
|
|
279
|
+
"20151": ErrorType.AUTHENTICATION_FAILURE, # Cardholder failed 3DS authentication
|
|
280
|
+
"20152": ErrorType.AUTHENTICATION_FAILURE, # Initial 3DS transaction not completed within 15 minutes
|
|
281
|
+
"20153": ErrorType.AUTHENTICATION_FAILURE, # 3DS system malfunction
|
|
282
|
+
"20154": ErrorType.AUTHENTICATION_REQUIRED, # 3DS authentication required
|
|
283
|
+
"20155": ErrorType.AUTHENTICATION_FAILURE, # 3DS authentication service provided invalid authentication result
|
|
284
|
+
"20156": ErrorType.NOT_SUPPORTED, # Requested function not supported by the acquirer
|
|
285
|
+
"20157": ErrorType.CONFIGURATION_ERROR, # Invalid merchant configurations - Contact Support
|
|
286
|
+
"20158": ErrorType.OTHER, # Refund validity period has expired
|
|
287
|
+
"20159": ErrorType.AUTHENTICATION_FAILURE, # ACS Malfunction
|
|
288
|
+
"20179": ErrorType.INVALID_CARD, # Lifecycle (Occurs when transaction has invalid card data)
|
|
289
|
+
"20182": ErrorType.NOT_SUPPORTED, # Policy (Occurs when a transaction does not comply with card policy)
|
|
290
|
+
"20183": ErrorType.FRAUD, # Security (Occurs when a transaction is suspected to be fraudulent)
|
|
291
|
+
"20193": ErrorType.OTHER, # Invalid country code
|
|
292
|
+
|
|
293
|
+
# 30xxx Series - Hard Declines
|
|
294
|
+
"30004": ErrorType.BLOCKED_CARD, # Pick up card (No fraud)
|
|
295
|
+
"30007": ErrorType.BLOCKED_CARD, # Pick up card - Special conditions
|
|
296
|
+
"30015": ErrorType.INVALID_CARD, # No such issuer
|
|
297
|
+
"30016": ErrorType.NOT_SUPPORTED, # Issuer does not allow online gambling payout
|
|
298
|
+
"30017": ErrorType.NOT_SUPPORTED, # Issuer does not allow original credit transaction
|
|
299
|
+
"30018": ErrorType.NOT_SUPPORTED, # Issuer does not allow money transfer payout
|
|
300
|
+
"30019": ErrorType.NOT_SUPPORTED, # Issuer does not allow non-money transfer payout
|
|
301
|
+
"30020": ErrorType.INVALID_AMOUNT, # Invalid amount
|
|
302
|
+
"30021": ErrorType.INSUFFICENT_FUNDS, # Total amount limit reached
|
|
303
|
+
"30022": ErrorType.OTHER, # Total transaction count limit reached
|
|
304
|
+
"30033": ErrorType.EXPIRED_CARD, # Expired card - Pick up
|
|
305
|
+
"30034": ErrorType.FRAUD, # Suspected fraud - Pick up
|
|
306
|
+
"30035": ErrorType.REFERRAL, # Contact acquirer - Pick up
|
|
307
|
+
"30036": ErrorType.RESTRICTED_CARD, # Restricted card - Pick up
|
|
308
|
+
"30037": ErrorType.REFERRAL, # Call acquirer security - Pick up
|
|
309
|
+
"30038": ErrorType.PIN_TRIES_EXCEEDED, # Allowable PIN tries exceeded - Pick up
|
|
310
|
+
"30041": ErrorType.BLOCKED_CARD, # Lost card - Pick up
|
|
311
|
+
"30043": ErrorType.FRAUD, # Stolen card - Pick up
|
|
312
|
+
"30044": ErrorType.NOT_SUPPORTED, # Transaction rejected - AMLD5
|
|
313
|
+
"30045": ErrorType.NOT_SUPPORTED, # Invalid payout fund transfer type
|
|
314
|
+
"30046": ErrorType.INVALID_CARD, # Closed account
|
|
315
|
+
|
|
316
|
+
# 4xxxx Series - Risk Responses
|
|
317
|
+
"40101": ErrorType.FRAUD, # Risk blocked transaction
|
|
318
|
+
"40201": ErrorType.FRAUD, # Gateway reject - card number blocklist
|
|
319
|
+
"40202": ErrorType.FRAUD, # Gateway reject - IP address blocklist
|
|
320
|
+
"40203": ErrorType.FRAUD, # Gateway reject - email blocklist
|
|
321
|
+
"40204": ErrorType.FRAUD, # Gateway reject - phone number blocklist
|
|
322
|
+
"40205": ErrorType.FRAUD, # Gateway Reject - BIN number blocklist
|
|
323
|
+
"41101": ErrorType.FRAUD, # Risk Blocked Transaction (Client-level rule)
|
|
324
|
+
"41201": ErrorType.FRAUD, # Decline list - Card number (Client-level)
|
|
325
|
+
"41202": ErrorType.FRAUD, # Decline list - BIN (Client-level)
|
|
326
|
+
"41203": ErrorType.FRAUD, # Decline list - Email address (Client-level)
|
|
327
|
+
"41204": ErrorType.FRAUD, # Decline list - Phone (Client-level)
|
|
328
|
+
"41205": ErrorType.FRAUD, # Decline list - Payment IP (Client-level) - using client as first seen
|
|
329
|
+
"41206": ErrorType.FRAUD, # Decline list - Email domain (Client-level)
|
|
330
|
+
"41301": ErrorType.FRAUD, # Fraud score exceeds threshold (Client-level)
|
|
331
|
+
"42101": ErrorType.FRAUD, # Risk Blocked Transaction (Entity-level rule)
|
|
332
|
+
"42201": ErrorType.FRAUD, # Decline list - Card number (Client-level)
|
|
333
|
+
"42202": ErrorType.FRAUD, # Decline list - BIN (Client-level)
|
|
334
|
+
"42203": ErrorType.FRAUD, # Decline list - Email address (Client-level)
|
|
335
|
+
"42204": ErrorType.FRAUD, # Decline list - Phone (Client-level)
|
|
336
|
+
"42206": ErrorType.FRAUD, # Decline list - Email domain (Client-level)
|
|
337
|
+
"42301": ErrorType.FRAUD, # Fraud score exceeds threshold
|
|
338
|
+
"43101": ErrorType.FRAUD, # Potential fraud risk
|
|
339
|
+
"43102": ErrorType.FRAUD, # Risk blocked transaction – {Rule group name} (Checkout.com-level)
|
|
340
|
+
"43201": ErrorType.FRAUD, # Decline list - Card number (Checkout.com-level)
|
|
341
|
+
"43202": ErrorType.FRAUD, # Decline list - BIN (Checkout.com-level)
|
|
342
|
+
"43203": ErrorType.FRAUD, # Decline list - Email address (Checkout.com-level)
|
|
343
|
+
"43204": ErrorType.FRAUD, # Decline list - Phone (Checkout.com-level)
|
|
344
|
+
"43205": ErrorType.FRAUD, # Decline list - Payment IP (Checkout.com-level)
|
|
345
|
+
"43206": ErrorType.FRAUD, # Decline list - Email domain (Checkout.com-level)
|
|
346
|
+
"43301": ErrorType.FRAUD, # Fraud score exceeds threshold (Checkout.com-level)
|
|
347
|
+
"44301": ErrorType.AUTHENTICATION_REQUIRED, # 3DS authentication required
|
|
348
|
+
}
|
|
349
|
+
|
|
172
350
|
|
|
173
351
|
class CheckoutClient:
|
|
174
352
|
def __init__(self, private_key: str, processing_channel: str, is_test: bool, bt_api_key: str):
|
|
@@ -304,19 +482,34 @@ class CheckoutClient:
|
|
|
304
482
|
|
|
305
483
|
return payload
|
|
306
484
|
|
|
307
|
-
def _transform_checkout_response(self, response_data: Dict[str, Any], request: TransactionRequest) -> TransactionResponse:
|
|
485
|
+
def _transform_checkout_response(self, response_data: Dict[str, Any], request: TransactionRequest, headers: CaseInsensitiveDict, error_data: Optional[Dict[str, Any]] = None) -> TransactionResponse:
|
|
308
486
|
"""Transform Checkout.com response to our standardized format."""
|
|
487
|
+
response_code = ResponseCode(
|
|
488
|
+
category=CHECKOUT_NUMERICAL_CODE_MAPPING.get(str(response_data.get("response_code")), ErrorType.OTHER).category,
|
|
489
|
+
code=CHECKOUT_NUMERICAL_CODE_MAPPING.get(str(response_data.get("response_code")), ErrorType.OTHER).code
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
if error_data and isinstance(error_data, dict):
|
|
493
|
+
error_codes = error_data.get("error_codes", [])
|
|
494
|
+
if error_codes and len(error_codes) > 0:
|
|
495
|
+
first_error = str(error_codes[0])
|
|
496
|
+
response_code = ResponseCode(
|
|
497
|
+
category=ERROR_CODE_MAPPING.get(first_error, ErrorType.OTHER).category,
|
|
498
|
+
code=ERROR_CODE_MAPPING.get(first_error, ErrorType.OTHER).code
|
|
499
|
+
)
|
|
500
|
+
|
|
309
501
|
return TransactionResponse(
|
|
310
502
|
id=str(response_data.get("id")),
|
|
311
503
|
reference=str(response_data.get("reference")),
|
|
312
504
|
amount=Amount(
|
|
313
|
-
value=int(str(response_data.get("amount"))),
|
|
314
|
-
currency=str(response_data.get("currency"))
|
|
505
|
+
value=int(str(response_data.get("amount", request.amount.value))),
|
|
506
|
+
currency=str(response_data.get("currency", request.amount.currency))
|
|
315
507
|
),
|
|
316
508
|
status=TransactionStatus(
|
|
317
|
-
code=self._get_status_code(response_data.get("status")),
|
|
318
|
-
provider_code=str(response_data.get("status"))
|
|
509
|
+
code=self._get_status_code(response_data.get("status", TransactionStatusCode.DECLINED)),
|
|
510
|
+
provider_code=str(response_data.get("status", ""))
|
|
319
511
|
),
|
|
512
|
+
response_code=response_code,
|
|
320
513
|
source=TransactionSource(
|
|
321
514
|
type=request.source.type,
|
|
322
515
|
id=request.source.id,
|
|
@@ -325,8 +518,9 @@ class CheckoutClient:
|
|
|
325
518
|
) if response_data.get("source", {}).get("id") else None
|
|
326
519
|
),
|
|
327
520
|
full_provider_response=response_data,
|
|
521
|
+
basis_theory_extras=_basis_theory_extras(headers),
|
|
328
522
|
created_at=datetime.fromisoformat(response_data["processed_on"].split(".")[0] + "+00:00") if response_data.get("processed_on") else datetime.now(timezone.utc),
|
|
329
|
-
network_transaction_id=
|
|
523
|
+
network_transaction_id=response_data.get("scheme_id")
|
|
330
524
|
)
|
|
331
525
|
|
|
332
526
|
def _get_error_code(self, error: ErrorType) -> Dict[str, Any]:
|
|
@@ -341,14 +535,17 @@ class CheckoutClient:
|
|
|
341
535
|
code=error.code
|
|
342
536
|
)
|
|
343
537
|
|
|
344
|
-
def _transform_error_response_object(self, response, error_data=None) -> ErrorResponse:
|
|
538
|
+
def _transform_error_response_object(self, response, error_data=None, headers=None) -> ErrorResponse:
|
|
345
539
|
"""Transform error response from Checkout.com to SDK format."""
|
|
346
540
|
error_codes = []
|
|
541
|
+
provider_errors = error_data.get('error_codes', []) if error_data else []
|
|
347
542
|
|
|
348
543
|
if response.status_code == 401:
|
|
349
544
|
error_codes.append(self._get_error_code_object(ErrorType.INVALID_API_KEY))
|
|
350
545
|
elif response.status_code == 403:
|
|
351
546
|
error_codes.append(self._get_error_code_object(ErrorType.UNAUTHORIZED))
|
|
547
|
+
elif response.status_code == 404:
|
|
548
|
+
error_codes.append(self._get_error_code_object(ErrorType.REFUND_FAILED))
|
|
352
549
|
elif error_data is not None:
|
|
353
550
|
for error_code in error_data.get('error_codes', []):
|
|
354
551
|
mapped_error = ERROR_CODE_MAPPING.get(error_code, ErrorType.OTHER)
|
|
@@ -361,7 +558,8 @@ class CheckoutClient:
|
|
|
361
558
|
|
|
362
559
|
return ErrorResponse(
|
|
363
560
|
error_codes=error_codes,
|
|
364
|
-
provider_errors=
|
|
561
|
+
provider_errors=provider_errors,
|
|
562
|
+
basis_theory_extras=_basis_theory_extras(headers),
|
|
365
563
|
full_provider_response=error_data
|
|
366
564
|
)
|
|
367
565
|
|
|
@@ -387,6 +585,11 @@ class CheckoutClient:
|
|
|
387
585
|
data=payload,
|
|
388
586
|
use_bt_proxy=request_data.source.type != SourceType.PROCESSOR_TOKEN
|
|
389
587
|
)
|
|
588
|
+
|
|
589
|
+
response_data = response.json()
|
|
590
|
+
|
|
591
|
+
print(f"Response data: {response_data}")
|
|
592
|
+
|
|
390
593
|
except requests.exceptions.HTTPError as e:
|
|
391
594
|
# Check if this is a BT error
|
|
392
595
|
if hasattr(e, 'bt_error_response'):
|
|
@@ -394,13 +597,16 @@ class CheckoutClient:
|
|
|
394
597
|
|
|
395
598
|
try:
|
|
396
599
|
error_data = e.response.json()
|
|
600
|
+
|
|
601
|
+
if "card_expired" in error_data.get("error_codes", []) or "card_disabled" in error_data.get("error_codes", []):
|
|
602
|
+
return self._transform_checkout_response(error_data, request_data, e.response.headers, error_data)
|
|
397
603
|
except:
|
|
398
604
|
error_data = None
|
|
399
605
|
|
|
400
|
-
raise TransactionError(self._transform_error_response_object(e.response, error_data))
|
|
606
|
+
raise TransactionError(self._transform_error_response_object(e.response, error_data, e.response.headers))
|
|
401
607
|
|
|
402
608
|
# Transform response to SDK format
|
|
403
|
-
return self._transform_checkout_response(response.json(), request_data)
|
|
609
|
+
return self._transform_checkout_response(response.json(), request_data, response.headers)
|
|
404
610
|
|
|
405
611
|
def refund_transaction(self, refund_request: RefundRequest) -> RefundResponse:
|
|
406
612
|
"""
|
|
@@ -435,7 +641,7 @@ class CheckoutClient:
|
|
|
435
641
|
)
|
|
436
642
|
|
|
437
643
|
response_data = response.json()
|
|
438
|
-
|
|
644
|
+
|
|
439
645
|
# Transform the response to a standardized format
|
|
440
646
|
return RefundResponse(
|
|
441
647
|
id=response_data.get('action_id'),
|
|
@@ -453,5 +659,4 @@ class CheckoutClient:
|
|
|
453
659
|
except:
|
|
454
660
|
error_data = None
|
|
455
661
|
|
|
456
|
-
raise TransactionError(self._transform_error_response_object(e.response, error_data))
|
|
457
|
-
|
|
662
|
+
raise TransactionError(self._transform_error_response_object(e.response, error_data, e.response.headers))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import Dict, Any, Optional
|
|
2
|
+
from requests.structures import CaseInsensitiveDict
|
|
2
3
|
from ..models import (
|
|
3
4
|
TransactionRequest,
|
|
4
5
|
Amount,
|
|
@@ -8,10 +9,29 @@ from ..models import (
|
|
|
8
9
|
Address,
|
|
9
10
|
StatementDescription,
|
|
10
11
|
ThreeDS,
|
|
11
|
-
RecurringType
|
|
12
|
+
RecurringType,
|
|
13
|
+
ErrorType,
|
|
14
|
+
ErrorResponse,
|
|
15
|
+
ErrorCode,
|
|
16
|
+
BasisTheoryExtras
|
|
12
17
|
)
|
|
13
|
-
from ..exceptions import
|
|
18
|
+
from ..exceptions import TransactionError
|
|
14
19
|
|
|
20
|
+
def _error_code(error_type: ErrorType) -> ErrorCode:
|
|
21
|
+
"""
|
|
22
|
+
Validate the amount in a transaction request.
|
|
23
|
+
"""
|
|
24
|
+
return ErrorCode(
|
|
25
|
+
category=error_type.category,
|
|
26
|
+
code=error_type.code
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def _basis_theory_extras(headers: Optional[CaseInsensitiveDict]) -> Optional[BasisTheoryExtras]:
|
|
30
|
+
if headers and "bt-trace-id" in headers:
|
|
31
|
+
return BasisTheoryExtras(
|
|
32
|
+
trace_id=headers.get("bt-trace-id", "")
|
|
33
|
+
)
|
|
34
|
+
return None
|
|
15
35
|
|
|
16
36
|
def validate_required_fields(data: TransactionRequest) -> None:
|
|
17
37
|
"""
|
|
@@ -21,12 +41,20 @@ def validate_required_fields(data: TransactionRequest) -> None:
|
|
|
21
41
|
data: TransactionRequest containing transaction request data
|
|
22
42
|
|
|
23
43
|
Raises:
|
|
24
|
-
|
|
44
|
+
TransactionError: If required fields are missing
|
|
25
45
|
"""
|
|
26
46
|
if data.amount is None or data.amount.value is None:
|
|
27
|
-
raise
|
|
47
|
+
raise TransactionError(ErrorResponse(
|
|
48
|
+
error_codes=[_error_code(ErrorType.INVALID_AMOUNT)],
|
|
49
|
+
provider_errors=[],
|
|
50
|
+
full_provider_response={}
|
|
51
|
+
))
|
|
28
52
|
if not data.source or not data.source.type or not data.source.id:
|
|
29
|
-
raise
|
|
53
|
+
raise TransactionError(ErrorResponse(
|
|
54
|
+
error_codes=[_error_code(ErrorType.INVALID_SOURCE_TOKEN)],
|
|
55
|
+
provider_errors=[],
|
|
56
|
+
full_provider_response={}
|
|
57
|
+
))
|
|
30
58
|
|
|
31
59
|
|
|
32
60
|
def create_transaction_request(data: Dict[str, Any]) -> TransactionRequest:
|
|
@@ -40,7 +68,7 @@ def create_transaction_request(data: Dict[str, Any]) -> TransactionRequest:
|
|
|
40
68
|
TransactionRequest: A fully populated TransactionRequest object
|
|
41
69
|
|
|
42
70
|
Raises:
|
|
43
|
-
|
|
71
|
+
TransactionError: If required fields are missing
|
|
44
72
|
"""
|
|
45
73
|
return TransactionRequest(
|
|
46
74
|
amount=Amount(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, Any, Optional,
|
|
1
|
+
from typing import Dict, Any, Optional, List
|
|
2
2
|
import requests
|
|
3
3
|
from requests.models import Response
|
|
4
4
|
from ..models import ErrorType, ErrorCode, ErrorResponse
|
|
@@ -41,7 +41,7 @@ class RequestClient:
|
|
|
41
41
|
ErrorCode(
|
|
42
42
|
category=error_type.category,
|
|
43
43
|
code=error_type.code
|
|
44
|
-
)
|
|
44
|
+
)
|
|
45
45
|
],
|
|
46
46
|
provider_errors=provider_errors,
|
|
47
47
|
full_provider_response=response_data
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|