connections-sdk 2.0.0__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
connections_sdk/models.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import Enum
3
- from typing import Optional, Any, Dict, List
3
+ from typing import Optional, Any, Dict, List, Literal
4
4
  from datetime import datetime
5
5
 
6
6
 
@@ -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
@@ -121,6 +120,7 @@ class Customer:
121
120
  last_name: Optional[str] = None
122
121
  email: Optional[str] = None
123
122
  address: Optional[Address] = None
123
+ channel: Optional[Literal['ios', 'android', 'web']] = 'web'
124
124
 
125
125
 
126
126
  @dataclass
@@ -133,8 +133,17 @@ class StatementDescription:
133
133
  class ThreeDS:
134
134
  eci: Optional[str] = None
135
135
  authentication_value: Optional[str] = None
136
- xid: Optional[str] = None
137
136
  version: Optional[str] = None
137
+ ds_transaction_id: Optional[str] = None
138
+ directory_status_code: Optional[str] = None
139
+ authentication_status_code: Optional[str] = None
140
+ challenge_cancel_reason_code: Optional[str] = None
141
+ challenge_preference_code: Optional[str] = None
142
+ authentication_status_reason_code: Optional[str] = None
143
+
144
+ # API aligned fields (preferred)
145
+ threeds_version: Optional[str] = None
146
+ authentication_status_reason: Optional[str] = None
138
147
 
139
148
 
140
149
  @dataclass
@@ -175,6 +184,15 @@ class TransactionSource:
175
184
  id: str
176
185
  provisioned: Optional[ProvisionedSource] = None
177
186
 
187
+ @dataclass
188
+ class ResponseCode:
189
+ category: str
190
+ code: str
191
+
192
+ @dataclass
193
+ class BasisTheoryExtras:
194
+ trace_id: str
195
+
178
196
 
179
197
  @dataclass
180
198
  class TransactionResponse:
@@ -182,11 +200,12 @@ class TransactionResponse:
182
200
  reference: str
183
201
  amount: Amount
184
202
  status: TransactionStatus
203
+ response_code: ResponseCode
185
204
  source: TransactionSource
186
205
  full_provider_response: Dict[str, Any]
187
206
  created_at: datetime
188
207
  network_transaction_id: Optional[str] = None
189
-
208
+ basis_theory_extras: Optional[BasisTheoryExtras] = None
190
209
 
191
210
  @dataclass
192
211
  class RefundResponse:
@@ -207,4 +226,6 @@ class ErrorCode:
207
226
  class ErrorResponse:
208
227
  error_codes: List[ErrorCode]
209
228
  provider_errors: List[str]
210
- 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,6 +123,7 @@ 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,
126
+ "channel": request.customer.channel if request.customer else 'web'
124
127
  }
125
128
 
126
129
  if request.metadata:
@@ -206,22 +209,32 @@ class AdyenClient:
206
209
 
207
210
  # Map 3DS information
208
211
  if request.three_ds:
209
- three_ds_data: Dict[str, str] = {}
210
-
211
- if request.three_ds.eci:
212
- three_ds_data["eci"] = request.three_ds.eci
212
+ mpi_data: Dict[str, Any] = {}
213
+ three_ds_2_request_data: Dict[str, Any] = {}
213
214
 
214
215
  if request.three_ds.authentication_value:
215
- three_ds_data["authenticationValue"] = request.three_ds.authentication_value
216
-
217
- if request.three_ds.xid:
218
- three_ds_data["xid"] = request.three_ds.xid
216
+ mpi_data["cavv"] = request.three_ds.authentication_value
217
+ if request.three_ds.eci:
218
+ mpi_data["eci"] = request.three_ds.eci
219
+ if request.three_ds.ds_transaction_id:
220
+ mpi_data["dsTransID"] = request.three_ds.ds_transaction_id
221
+ if request.three_ds.directory_status_code:
222
+ mpi_data["directoryResponse"] = request.three_ds.directory_status_code
223
+ if request.three_ds.authentication_status_code:
224
+ mpi_data["authenticationResponse"] = request.three_ds.authentication_status_code
225
+ if request.three_ds.threeds_version or request.three_ds.version: # threeds_version from API, fallback to version
226
+ mpi_data["threeDSVersion"] = request.three_ds.threeds_version or request.three_ds.version
227
+ if request.three_ds.challenge_cancel_reason_code:
228
+ mpi_data["challengeCancel"] = request.three_ds.challenge_cancel_reason_code
229
+
230
+ if mpi_data:
231
+ payload["mpiData"] = mpi_data
219
232
 
220
- if request.three_ds.version:
221
- three_ds_data["threeDSVersion"] = request.three_ds.version
233
+ if request.three_ds.challenge_preference_code:
234
+ three_ds_2_request_data["threeDSRequestorChallengeInd"] = request.three_ds.challenge_preference_code
222
235
 
223
- if three_ds_data:
224
- payload["additionalData"] = {"threeDSecure": three_ds_data}
236
+ if three_ds_2_request_data:
237
+ payload["threeDS2RequestData"] = three_ds_2_request_data
225
238
 
226
239
  # Override/merge any provider properties if specified
227
240
  if request.override_provider_properties:
@@ -229,25 +242,30 @@ class AdyenClient:
229
242
 
230
243
  return payload
231
244
 
232
- 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:
233
246
  """Transform Adyen response to our standardized format."""
234
247
  transaction_response = TransactionResponse(
235
248
  id=str(response_data.get("pspReference")),
236
249
  reference=str(response_data.get("merchantReference")),
237
250
  amount=Amount(
238
- value=int(response_data.get("amount", {}).get("value")),
239
- 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))
240
253
  ),
241
254
  status=TransactionStatus(
242
255
  code=self._get_status_code(response_data.get("resultCode")),
243
256
  provider_code=str(response_data.get("resultCode"))
244
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
+ ),
245
262
  source=TransactionSource(
246
263
  type=request.source.type,
247
264
  id=request.source.id,
248
265
  ),
249
266
  network_transaction_id=str(response_data.get("additionalData", {}).get("networkTxReference")),
250
267
  full_provider_response=response_data,
268
+ basis_theory_extras=_basis_theory_extras(headers),
251
269
  created_at=datetime.now(timezone.utc)
252
270
  )
253
271
 
@@ -261,7 +279,7 @@ class AdyenClient:
261
279
 
262
280
  return transaction_response
263
281
 
264
- 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:
265
283
  """Transform error responses to our standardized format.
266
284
 
267
285
  Args:
@@ -276,10 +294,6 @@ class AdyenClient:
276
294
  error_type = ErrorType.INVALID_API_KEY
277
295
  elif response.status_code == 403:
278
296
  error_type = ErrorType.UNAUTHORIZED
279
- # Handle Adyen-specific error codes for declined transactions
280
- elif response_data.get("resultCode") in ["Refused", "Error", "Cancelled"]:
281
- refusal_code = response_data.get("refusalReasonCode", "")
282
- error_type = ERROR_CODE_MAPPING.get(refusal_code, ErrorType.OTHER)
283
297
  else:
284
298
  error_type = ErrorType.OTHER
285
299
 
@@ -291,7 +305,8 @@ class AdyenClient:
291
305
  )
292
306
  ],
293
307
  provider_errors=[response_data.get("refusalReason") or response_data.get("message", "")],
294
- full_provider_response=response_data
308
+ full_provider_response=response_data,
309
+ basis_theory_extras=_basis_theory_extras(headers)
295
310
  )
296
311
 
297
312
 
@@ -320,12 +335,8 @@ class AdyenClient:
320
335
 
321
336
  response_data = response.json()
322
337
 
323
- # Check if it's an error response (non-200 status code or Adyen error)
324
- if not response.ok or response_data.get("resultCode") in ["Refused", "Error", "Cancelled"]:
325
- raise TransactionError(self._transform_error_response(response, response_data))
326
-
327
338
  # Transform the successful response to our format
328
- return self._transform_adyen_response(response_data, request_data)
339
+ return self._transform_adyen_response(response_data, request_data, response.headers)
329
340
 
330
341
  except requests.exceptions.HTTPError as e:
331
342
  try:
@@ -333,7 +344,7 @@ class AdyenClient:
333
344
  except:
334
345
  error_data = None
335
346
 
336
- raise TransactionError(self._transform_error_response(e.response, error_data))
347
+ raise TransactionError(self._transform_error_response(e.response, error_data, e.response.headers))
337
348
 
338
349
 
339
350
  def refund_transaction(self, refund_request: RefundRequest) -> RefundResponse:
@@ -401,5 +412,5 @@ class AdyenClient:
401
412
  except:
402
413
  error_data = None
403
414
 
404
- raise TransactionError(self._transform_error_response(e.response, error_data))
415
+ raise TransactionError(self._transform_error_response(e.response, error_data, e.response.headers))
405
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):
@@ -268,15 +446,34 @@ class CheckoutClient:
268
446
 
269
447
  # Add 3DS information if provided
270
448
  if request.three_ds:
271
- three_ds_data: Dict[str, str] = {}
272
- if request.three_ds.eci:
273
- three_ds_data["eci"] = request.three_ds.eci
449
+ three_ds_data: Dict[str, Any] = {
450
+ "enabled": True
451
+ }
452
+
274
453
  if request.three_ds.authentication_value:
275
454
  three_ds_data["cryptogram"] = request.three_ds.authentication_value
276
- if request.three_ds.xid:
277
- three_ds_data["xid"] = request.three_ds.xid
278
- if request.three_ds.version:
279
- three_ds_data["version"] = request.three_ds.version
455
+ if request.three_ds.eci:
456
+ three_ds_data["eci"] = request.three_ds.eci
457
+ if request.three_ds.threeds_version or request.three_ds.version: # threeds_version from API, fallback to version
458
+ three_ds_data["version"] = request.three_ds.threeds_version or request.three_ds.version
459
+ if request.three_ds.ds_transaction_id: # ds_transaction_id in BT, xid in Checkout
460
+ three_ds_data["xid"] = request.three_ds.ds_transaction_id
461
+ if request.three_ds.authentication_status_code:
462
+ three_ds_data["status"] = request.three_ds.authentication_status_code
463
+ if request.three_ds.authentication_status_reason_code:
464
+ three_ds_data["status_reason_code"] = request.three_ds.authentication_status_reason_code
465
+
466
+ if request.three_ds.challenge_preference_code:
467
+ challenge_indicator_mapping = {
468
+ "no-preference": "no_preference",
469
+ "no-challenge": "no_challenge_requested",
470
+ "challenge-requested": "challenge_requested",
471
+ "challenge-mandated": "challenge_requested_mandate"
472
+ }
473
+ checkout_challenge_indicator = challenge_indicator_mapping.get(request.three_ds.challenge_preference_code)
474
+ if checkout_challenge_indicator: # Only add if a valid mapping exists
475
+ three_ds_data["challenge_indicator"] = checkout_challenge_indicator
476
+
280
477
  payload["3ds"] = three_ds_data
281
478
 
282
479
  # Override/merge any provider properties if specified
@@ -285,19 +482,34 @@ class CheckoutClient:
285
482
 
286
483
  return payload
287
484
 
288
- 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:
289
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
+
290
501
  return TransactionResponse(
291
502
  id=str(response_data.get("id")),
292
503
  reference=str(response_data.get("reference")),
293
504
  amount=Amount(
294
- value=int(str(response_data.get("amount"))),
295
- 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))
296
507
  ),
297
508
  status=TransactionStatus(
298
- code=self._get_status_code(response_data.get("status")),
299
- 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", ""))
300
511
  ),
512
+ response_code=response_code,
301
513
  source=TransactionSource(
302
514
  type=request.source.type,
303
515
  id=request.source.id,
@@ -306,8 +518,9 @@ class CheckoutClient:
306
518
  ) if response_data.get("source", {}).get("id") else None
307
519
  ),
308
520
  full_provider_response=response_data,
521
+ basis_theory_extras=_basis_theory_extras(headers),
309
522
  created_at=datetime.fromisoformat(response_data["processed_on"].split(".")[0] + "+00:00") if response_data.get("processed_on") else datetime.now(timezone.utc),
310
- network_transaction_id=str(response_data.get("processing", {}).get("acquirer_transaction_id"))
523
+ network_transaction_id=response_data.get("scheme_id")
311
524
  )
312
525
 
313
526
  def _get_error_code(self, error: ErrorType) -> Dict[str, Any]:
@@ -322,14 +535,17 @@ class CheckoutClient:
322
535
  code=error.code
323
536
  )
324
537
 
325
- 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:
326
539
  """Transform error response from Checkout.com to SDK format."""
327
540
  error_codes = []
541
+ provider_errors = error_data.get('error_codes', []) if error_data else []
328
542
 
329
543
  if response.status_code == 401:
330
544
  error_codes.append(self._get_error_code_object(ErrorType.INVALID_API_KEY))
331
545
  elif response.status_code == 403:
332
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))
333
549
  elif error_data is not None:
334
550
  for error_code in error_data.get('error_codes', []):
335
551
  mapped_error = ERROR_CODE_MAPPING.get(error_code, ErrorType.OTHER)
@@ -342,7 +558,8 @@ class CheckoutClient:
342
558
 
343
559
  return ErrorResponse(
344
560
  error_codes=error_codes,
345
- provider_errors=error_data.get('error_codes', []) if error_data else [],
561
+ provider_errors=provider_errors,
562
+ basis_theory_extras=_basis_theory_extras(headers),
346
563
  full_provider_response=error_data
347
564
  )
348
565
 
@@ -352,7 +569,7 @@ class CheckoutClient:
352
569
  validate_required_fields(request_data)
353
570
  # Transform request to Checkout.com format
354
571
  payload = self._transform_to_checkout_payload(request_data)
355
-
572
+
356
573
  # Set up common headers
357
574
  headers = {
358
575
  "Authorization": f"Bearer {self.api_key}",
@@ -368,6 +585,11 @@ class CheckoutClient:
368
585
  data=payload,
369
586
  use_bt_proxy=request_data.source.type != SourceType.PROCESSOR_TOKEN
370
587
  )
588
+
589
+ response_data = response.json()
590
+
591
+ print(f"Response data: {response_data}")
592
+
371
593
  except requests.exceptions.HTTPError as e:
372
594
  # Check if this is a BT error
373
595
  if hasattr(e, 'bt_error_response'):
@@ -375,13 +597,16 @@ class CheckoutClient:
375
597
 
376
598
  try:
377
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)
378
603
  except:
379
604
  error_data = None
380
605
 
381
- 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))
382
607
 
383
608
  # Transform response to SDK format
384
- return self._transform_checkout_response(response.json(), request_data)
609
+ return self._transform_checkout_response(response.json(), request_data, response.headers)
385
610
 
386
611
  def refund_transaction(self, refund_request: RefundRequest) -> RefundResponse:
387
612
  """
@@ -416,7 +641,7 @@ class CheckoutClient:
416
641
  )
417
642
 
418
643
  response_data = response.json()
419
-
644
+
420
645
  # Transform the response to a standardized format
421
646
  return RefundResponse(
422
647
  id=response_data.get('action_id'),
@@ -434,5 +659,4 @@ class CheckoutClient:
434
659
  except:
435
660
  error_data = None
436
661
 
437
- raise TransactionError(self._transform_error_response_object(e.response, error_data))
438
-
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: connections_sdk
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: A Python SDK for payment processing
5
5
  Author: Basis Theory
6
6
  Author-email: support@basistheory.com
@@ -0,0 +1,14 @@
1
+ connections_sdk/__init__.py,sha256=IYYzlDWAgIeJs_1Ly2_SNsUYtMjGexysfY3Q9-L1Pn8,174
2
+ connections_sdk/client.py,sha256=FkZq68S_syvaJTVRSVPtpA4FwsybRdC9FivRi2SOkoI,3165
3
+ connections_sdk/config.py,sha256=OPH-J11ecQYP17ubtIXuB_VVuso3t-Ym9V8f5j-G1Xk,398
4
+ connections_sdk/exceptions.py,sha256=AuLet9gpV6OmQrKLO9ozgyrWw1KCPuUxV8Own7gTEmk,761
5
+ connections_sdk/models.py,sha256=L_AntoEFsD9I3TeAi8j-h7HGQK1mNXsHnwZOT5vn7iI,7414
6
+ connections_sdk/providers/adyen.py,sha256=j2-FRLsqGJFNYMxq9-8hy1R5zLzmYJ_h-uHwPtcAk9Y,18509
7
+ connections_sdk/providers/checkout.py,sha256=j3YwmpFWt66Zbb9j-pO8vyxYxV_cupbrw0aixvBdKy4,35153
8
+ connections_sdk/utils/__init__.py,sha256=iKwqW2egmbc_LO0oQoNLiP5-RBXUd9OpMW6sdEVB_Fs,153
9
+ connections_sdk/utils/model_utils.py,sha256=FB6Cf-dMFr52gi-F6yMEAPJU413Sjv_8zx3IUNBI-Q8,4015
10
+ connections_sdk/utils/request_client.py,sha256=M9eqEpRVoIFUMS51jGJ-C2R1L9_NWw3v3saAq31hVoI,3251
11
+ connections_sdk-3.0.0.dist-info/LICENSE,sha256=OJSDpWNs9gHwRBdHonZIkQ2-rUpFMxn0V8xxjfz4UQQ,11342
12
+ connections_sdk-3.0.0.dist-info/METADATA,sha256=CwqdNMn8KCXHaSRk-10MFWcqMxtnE2dT1MyGIbkyQZI,2821
13
+ connections_sdk-3.0.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
14
+ connections_sdk-3.0.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- connections_sdk/__init__.py,sha256=IYYzlDWAgIeJs_1Ly2_SNsUYtMjGexysfY3Q9-L1Pn8,174
2
- connections_sdk/client.py,sha256=FkZq68S_syvaJTVRSVPtpA4FwsybRdC9FivRi2SOkoI,3165
3
- connections_sdk/config.py,sha256=OPH-J11ecQYP17ubtIXuB_VVuso3t-Ym9V8f5j-G1Xk,398
4
- connections_sdk/exceptions.py,sha256=3L3X1OCp7EIPg5l4NPfcEUrPPahfEft-qnZ5QfxXR0k,805
5
- connections_sdk/models.py,sha256=slAgi-v9qZjWNGvHrJc8myXcN-v1X2lP5gXFjj6fUCQ,6650
6
- connections_sdk/providers/adyen.py,sha256=6iOyy_7Sh1cZTaV9Lu4MHaazDcL0aUtgYnYluIAMIk4,17458
7
- connections_sdk/providers/checkout.py,sha256=2hTWkZwnMazlQn4MpICoQ5GZDfGOEhrjYp5QknbuCG4,19543
8
- connections_sdk/utils/__init__.py,sha256=iKwqW2egmbc_LO0oQoNLiP5-RBXUd9OpMW6sdEVB_Fs,153
9
- connections_sdk/utils/model_utils.py,sha256=B1lIam4Ctajnhi1H9eB7hpM4IVbemHgpHzBVruoPKcU,3159
10
- connections_sdk/utils/request_client.py,sha256=zbCGoJajAQ9RUNUb5x5edSaqdxJxPhoedAwUBT0S3ek,3261
11
- connections_sdk-2.0.0.dist-info/LICENSE,sha256=OJSDpWNs9gHwRBdHonZIkQ2-rUpFMxn0V8xxjfz4UQQ,11342
12
- connections_sdk-2.0.0.dist-info/METADATA,sha256=bcoDDC_r3AhbyIKend4Zylkw7Pup1RvALske8rGKIN4,2821
13
- connections_sdk-2.0.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
14
- connections_sdk-2.0.0.dist-info/RECORD,,