connections-sdk 2.1.0__tar.gz → 3.1.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.1.0}/PKG-INFO +1 -1
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/pyproject.toml +1 -1
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/src/connections_sdk/exceptions.py +1 -4
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/src/connections_sdk/models.py +14 -3
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/src/connections_sdk/providers/adyen.py +21 -20
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/src/connections_sdk/providers/checkout.py +221 -16
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/src/connections_sdk/utils/model_utils.py +34 -6
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/src/connections_sdk/utils/request_client.py +2 -2
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/LICENSE +0 -0
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/README.md +0 -0
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/src/connections_sdk/__init__.py +0 -0
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/src/connections_sdk/client.py +0 -0
- {connections_sdk-2.1.0 → connections_sdk-3.1.0}/src/connections_sdk/config.py +0 -0
- {connections_sdk-2.1.0 → connections_sdk-3.1.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
|
|
|
@@ -121,7 +123,8 @@ class AdyenClient:
|
|
|
121
123
|
"merchantAccount": self.merchant_account,
|
|
122
124
|
"shopperInteraction": "ContAuth" if request.merchant_initiated else "Ecommerce",
|
|
123
125
|
"storePaymentMethod": request.source.store_with_provider,
|
|
124
|
-
"channel": request.customer.channel if request.customer else 'web'
|
|
126
|
+
"channel": request.customer.channel if request.customer else 'web',
|
|
127
|
+
"additionalData": {}
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
if request.metadata:
|
|
@@ -155,7 +158,7 @@ class AdyenClient:
|
|
|
155
158
|
payment_method["holderName"] = request.source.holder_name
|
|
156
159
|
|
|
157
160
|
if request.previous_network_transaction_id:
|
|
158
|
-
payment_method["
|
|
161
|
+
payment_method["additionalData"]["networkTxReference"] = request. previous_network_transaction_id
|
|
159
162
|
|
|
160
163
|
payload["paymentMethod"] = payment_method
|
|
161
164
|
|
|
@@ -240,25 +243,30 @@ class AdyenClient:
|
|
|
240
243
|
|
|
241
244
|
return payload
|
|
242
245
|
|
|
243
|
-
def _transform_adyen_response(self, response_data: Dict[str, Any], request: TransactionRequest) -> TransactionResponse:
|
|
246
|
+
def _transform_adyen_response(self, response_data: Dict[str, Any], request: TransactionRequest, headers: CaseInsensitiveDict) -> TransactionResponse:
|
|
244
247
|
"""Transform Adyen response to our standardized format."""
|
|
245
248
|
transaction_response = TransactionResponse(
|
|
246
249
|
id=str(response_data.get("pspReference")),
|
|
247
250
|
reference=str(response_data.get("merchantReference")),
|
|
248
251
|
amount=Amount(
|
|
249
|
-
value=int(response_data.get("amount", {}).get("value")),
|
|
250
|
-
currency=str(response_data.get("amount", {}).get("currency"))
|
|
252
|
+
value=int(response_data.get("amount", {}).get("value", request.amount.value)),
|
|
253
|
+
currency=str(response_data.get("amount", {}).get("currency", request.amount.currency))
|
|
251
254
|
),
|
|
252
255
|
status=TransactionStatus(
|
|
253
256
|
code=self._get_status_code(response_data.get("resultCode")),
|
|
254
257
|
provider_code=str(response_data.get("resultCode"))
|
|
255
258
|
),
|
|
259
|
+
response_code=ResponseCode(
|
|
260
|
+
category=ERROR_CODE_MAPPING.get(str(response_data.get("refusalReasonCode")), ErrorType.OTHER).category,
|
|
261
|
+
code=ERROR_CODE_MAPPING.get(str(response_data.get("refusalReasonCode")), ErrorType.OTHER).code
|
|
262
|
+
),
|
|
256
263
|
source=TransactionSource(
|
|
257
264
|
type=request.source.type,
|
|
258
265
|
id=request.source.id,
|
|
259
266
|
),
|
|
260
267
|
network_transaction_id=str(response_data.get("additionalData", {}).get("networkTxReference")),
|
|
261
268
|
full_provider_response=response_data,
|
|
269
|
+
basis_theory_extras=_basis_theory_extras(headers),
|
|
262
270
|
created_at=datetime.now(timezone.utc)
|
|
263
271
|
)
|
|
264
272
|
|
|
@@ -272,7 +280,7 @@ class AdyenClient:
|
|
|
272
280
|
|
|
273
281
|
return transaction_response
|
|
274
282
|
|
|
275
|
-
def _transform_error_response(self, response: requests.Response, response_data: Dict[str, Any]) -> ErrorResponse:
|
|
283
|
+
def _transform_error_response(self, response: requests.Response, response_data: Dict[str, Any], headers: CaseInsensitiveDict) -> ErrorResponse:
|
|
276
284
|
"""Transform error responses to our standardized format.
|
|
277
285
|
|
|
278
286
|
Args:
|
|
@@ -287,10 +295,6 @@ class AdyenClient:
|
|
|
287
295
|
error_type = ErrorType.INVALID_API_KEY
|
|
288
296
|
elif response.status_code == 403:
|
|
289
297
|
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
298
|
else:
|
|
295
299
|
error_type = ErrorType.OTHER
|
|
296
300
|
|
|
@@ -302,7 +306,8 @@ class AdyenClient:
|
|
|
302
306
|
)
|
|
303
307
|
],
|
|
304
308
|
provider_errors=[response_data.get("refusalReason") or response_data.get("message", "")],
|
|
305
|
-
full_provider_response=response_data
|
|
309
|
+
full_provider_response=response_data,
|
|
310
|
+
basis_theory_extras=_basis_theory_extras(headers)
|
|
306
311
|
)
|
|
307
312
|
|
|
308
313
|
|
|
@@ -331,12 +336,8 @@ class AdyenClient:
|
|
|
331
336
|
|
|
332
337
|
response_data = response.json()
|
|
333
338
|
|
|
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
339
|
# Transform the successful response to our format
|
|
339
|
-
return self._transform_adyen_response(response_data, request_data)
|
|
340
|
+
return self._transform_adyen_response(response_data, request_data, response.headers)
|
|
340
341
|
|
|
341
342
|
except requests.exceptions.HTTPError as e:
|
|
342
343
|
try:
|
|
@@ -344,7 +345,7 @@ class AdyenClient:
|
|
|
344
345
|
except:
|
|
345
346
|
error_data = None
|
|
346
347
|
|
|
347
|
-
raise TransactionError(self._transform_error_response(e.response, error_data))
|
|
348
|
+
raise TransactionError(self._transform_error_response(e.response, error_data, e.response.headers))
|
|
348
349
|
|
|
349
350
|
|
|
350
351
|
def refund_transaction(self, refund_request: RefundRequest) -> RefundResponse:
|
|
@@ -412,5 +413,5 @@ class AdyenClient:
|
|
|
412
413
|
except:
|
|
413
414
|
error_data = None
|
|
414
415
|
|
|
415
|
-
raise TransactionError(self._transform_error_response(e.response, error_data))
|
|
416
|
+
raise TransactionError(self._transform_error_response(e.response, error_data, e.response.headers))
|
|
416
417
|
|
|
@@ -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
|