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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: connections_sdk
3
- Version: 2.1.0
3
+ Version: 3.1.0
4
4
  Summary: A Python SDK for payment processing
5
5
  Author: Basis Theory
6
6
  Author-email: support@basistheory.com
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "connections_sdk"
7
- version = "2.1.0"
7
+ version = "3.1.0"
8
8
  description = "A Python SDK for payment processing"
9
9
  authors = ["Basis Theory <support@basistheory.com>"]
10
10
  readme = "README.md"
@@ -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["networkPaymentReference"] = request. previous_network_transaction_id
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=str(response_data.get("processing", {}).get("acquirer_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=error_data.get('error_codes', []) if error_data else [],
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 ValidationError
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
- ValidationError: If required fields are missing
44
+ TransactionError: If required fields are missing
25
45
  """
26
46
  if data.amount is None or data.amount.value is None:
27
- raise ValidationError("amount.value is required")
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 ValidationError("source.type and source.id are required")
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
- ValidationError: If required fields are missing
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, Union, List, cast
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