connections-sdk 0.1.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.
- connections_sdk/__init__.py +4 -0
- connections_sdk/client.py +106 -0
- connections_sdk/config.py +18 -0
- connections_sdk/exceptions.py +24 -0
- connections_sdk/models.py +210 -0
- connections_sdk/providers/adyen.py +405 -0
- connections_sdk/providers/checkout.py +440 -0
- connections_sdk/utils/__init__.py +4 -0
- connections_sdk/utils/model_utils.py +94 -0
- connections_sdk/utils/request_client.py +89 -0
- connections_sdk-0.1.0.dist-info/LICENSE +202 -0
- connections_sdk-0.1.0.dist-info/METADATA +91 -0
- connections_sdk-0.1.0.dist-info/RECORD +14 -0
- connections_sdk-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
from typing import Dict, Any, Tuple, Optional, Union, cast
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
import requests
|
|
4
|
+
from deepmerge import always_merger
|
|
5
|
+
from ..models import (
|
|
6
|
+
TransactionRequest,
|
|
7
|
+
Amount,
|
|
8
|
+
Source,
|
|
9
|
+
SourceType,
|
|
10
|
+
Customer,
|
|
11
|
+
Address,
|
|
12
|
+
StatementDescription,
|
|
13
|
+
ThreeDS,
|
|
14
|
+
RecurringType,
|
|
15
|
+
TransactionStatusCode,
|
|
16
|
+
ErrorType,
|
|
17
|
+
ErrorCategory,
|
|
18
|
+
RefundRequest,
|
|
19
|
+
RefundResponse,
|
|
20
|
+
TransactionStatus,
|
|
21
|
+
ErrorResponse,
|
|
22
|
+
ErrorCode,
|
|
23
|
+
TransactionResponse,
|
|
24
|
+
TransactionSource,
|
|
25
|
+
ProvisionedSource
|
|
26
|
+
)
|
|
27
|
+
from ..utils.model_utils import create_transaction_request, validate_required_fields
|
|
28
|
+
from ..utils.request_client import RequestClient
|
|
29
|
+
from ..exceptions import TransactionError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
RECURRING_TYPE_MAPPING = {
|
|
33
|
+
RecurringType.ONE_TIME: None,
|
|
34
|
+
RecurringType.CARD_ON_FILE: "CardOnFile",
|
|
35
|
+
RecurringType.SUBSCRIPTION: "Subscription",
|
|
36
|
+
RecurringType.UNSCHEDULED: "UnscheduledCardOnFile"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Map Adyen resultCode to our status codes
|
|
41
|
+
STATUS_CODE_MAPPING = {
|
|
42
|
+
"Authorised": TransactionStatusCode.AUTHORIZED, # Adyen: Authorised - Payment was successfully authorized
|
|
43
|
+
"Pending": TransactionStatusCode.PENDING, # Adyen: Pending - Payment is pending, waiting for completion
|
|
44
|
+
"Error": TransactionStatusCode.DECLINED, # Adyen: Error - Technical error occurred
|
|
45
|
+
"Refused": TransactionStatusCode.DECLINED, # Adyen: Refused - Payment was refused
|
|
46
|
+
"Cancelled": TransactionStatusCode.CANCELLED, # Adyen: Cancelled - Payment was cancelled
|
|
47
|
+
"ChallengeShopper": TransactionStatusCode.CHALLENGE_SHOPPER, # Adyen: ChallengeShopper - 3DS2 challenge required
|
|
48
|
+
"Received": TransactionStatusCode.RECEIVED, # Adyen: Received - Payment was received
|
|
49
|
+
"PartiallyAuthorised": TransactionStatusCode.PARTIALLY_AUTHORIZED # Adyen: PartiallyAuthorised - Only part of the amount was authorized
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Mapping of Adyen refusal reason codes to our error types
|
|
54
|
+
ERROR_CODE_MAPPING = {
|
|
55
|
+
"2": ErrorType.REFUSED, # Refused
|
|
56
|
+
"3": ErrorType.REFERRAL, # Referral
|
|
57
|
+
"4": ErrorType.ACQUIRER_ERROR, # Acquirer Error
|
|
58
|
+
"5": ErrorType.BLOCKED_CARD, # Blocked Card
|
|
59
|
+
"6": ErrorType.EXPIRED_CARD, # Expired Card
|
|
60
|
+
"7": ErrorType.INVALID_AMOUNT, # Invalid Amount
|
|
61
|
+
"8": ErrorType.INVALID_CARD, # Invalid Card Number
|
|
62
|
+
"9": ErrorType.OTHER, # Issuer Unavailable
|
|
63
|
+
"10": ErrorType.NOT_SUPPORTED, # Not supported
|
|
64
|
+
"11": ErrorType.AUTHENTICATION_FAILURE, # 3D Not Authenticated
|
|
65
|
+
"12": ErrorType.INSUFFICENT_FUNDS, # Not enough balance
|
|
66
|
+
"14": ErrorType.FRAUD, # Acquirer Fraud
|
|
67
|
+
"15": ErrorType.PAYMENT_CANCELLED, # Cancelled
|
|
68
|
+
"16": ErrorType.PAYMENT_CANCELLED_BY_CONSUMER, # Shopper Cancelled
|
|
69
|
+
"17": ErrorType.INVALID_PIN, # Invalid Pin
|
|
70
|
+
"18": ErrorType.PIN_TRIES_EXCEEDED, # Pin tries exceeded
|
|
71
|
+
"19": ErrorType.OTHER, # Pin validation not possible
|
|
72
|
+
"20": ErrorType.FRAUD, # FRAUD
|
|
73
|
+
"21": ErrorType.OTHER, # Not Submitted
|
|
74
|
+
"22": ErrorType.FRAUD, # FRAUD-CANCELLED
|
|
75
|
+
"23": ErrorType.NOT_SUPPORTED, # Transaction Not Permitted
|
|
76
|
+
"24": ErrorType.CVC_INVALID, # CVC Declined
|
|
77
|
+
"25": ErrorType.RESTRICTED_CARD, # Restricted Card
|
|
78
|
+
"26": ErrorType.STOP_PAYMENT, # Revocation Of Auth
|
|
79
|
+
"27": ErrorType.REFUSED, # Declined Non Generic
|
|
80
|
+
"28": ErrorType.INSUFFICENT_FUNDS, # Withdrawal amount exceeded
|
|
81
|
+
"29": ErrorType.INSUFFICENT_FUNDS, # Withdrawal count exceeded
|
|
82
|
+
"31": ErrorType.FRAUD, # Issuer Suspected Fraud
|
|
83
|
+
"32": ErrorType.AVS_DECLINE, # AVS Declined
|
|
84
|
+
"33": ErrorType.PIN_REQUIRED, # Card requires online pin
|
|
85
|
+
"34": ErrorType.BANK_ERROR, # No checking account available on Card
|
|
86
|
+
"35": ErrorType.BANK_ERROR, # No savings account available on Card
|
|
87
|
+
"36": ErrorType.PIN_REQUIRED, # Mobile pin required
|
|
88
|
+
"37": ErrorType.CONTACTLESS_FALLBACK, # Contactless fallback
|
|
89
|
+
"38": ErrorType.AUTHENTICATION_REQUIRED, # Authentication required
|
|
90
|
+
"39": ErrorType.AUTHENTICATION_FAILURE, # RReq not received from DS
|
|
91
|
+
"40": ErrorType.OTHER, # Current AID is in Penalty Box
|
|
92
|
+
"41": ErrorType.PIN_REQUIRED, # CVM Required Restart Payment
|
|
93
|
+
"42": ErrorType.AUTHENTICATION_FAILURE, # 3DS Authentication Error
|
|
94
|
+
"43": ErrorType.PIN_REQUIRED, # Online PIN required
|
|
95
|
+
"44": ErrorType.OTHER, # Try another interface
|
|
96
|
+
"45": ErrorType.OTHER, # Chip downgrade mode
|
|
97
|
+
"46": ErrorType.PROCESSOR_BLOCKED, # Transaction blocked by Adyen to prevent excessive retry fees
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AdyenClient:
|
|
102
|
+
def __init__(self, api_key: str, merchant_account: str, is_test: bool, bt_api_key: str, production_prefix: str):
|
|
103
|
+
self.api_key = api_key
|
|
104
|
+
self.merchant_account = merchant_account
|
|
105
|
+
self.base_url = "https://checkout-test.adyen.com/v71" if is_test else f"https://{production_prefix}-checkout-live.adyenpayments.com/checkout/v71"
|
|
106
|
+
self.request_client = RequestClient(bt_api_key)
|
|
107
|
+
|
|
108
|
+
def _get_status_code(self, adyen_result_code: Optional[str]) -> TransactionStatusCode:
|
|
109
|
+
"""Map Adyen result code to our status code."""
|
|
110
|
+
if not adyen_result_code:
|
|
111
|
+
return TransactionStatusCode.DECLINED
|
|
112
|
+
return STATUS_CODE_MAPPING.get(adyen_result_code, TransactionStatusCode.DECLINED)
|
|
113
|
+
|
|
114
|
+
def _transform_to_adyen_payload(self, request: TransactionRequest) -> Dict[str, Any]:
|
|
115
|
+
"""Transform SDK request to Adyen payload format."""
|
|
116
|
+
payload: Dict[str, Any] = {
|
|
117
|
+
"amount": {
|
|
118
|
+
"value": request.amount.value,
|
|
119
|
+
"currency": request.amount.currency
|
|
120
|
+
},
|
|
121
|
+
"merchantAccount": self.merchant_account,
|
|
122
|
+
"shopperInteraction": "ContAuth" if request.merchant_initiated else "Ecommerce",
|
|
123
|
+
"storePaymentMethod": request.source.store_with_provider,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if request.metadata:
|
|
127
|
+
payload["metadata"] = request.metadata
|
|
128
|
+
|
|
129
|
+
# Add reference if provided
|
|
130
|
+
if request.reference:
|
|
131
|
+
payload["reference"] = request.reference
|
|
132
|
+
|
|
133
|
+
# Add recurring type if provided
|
|
134
|
+
if request.type:
|
|
135
|
+
recurring_type = RECURRING_TYPE_MAPPING.get(request.type)
|
|
136
|
+
if recurring_type:
|
|
137
|
+
payload["recurringProcessingModel"] = recurring_type
|
|
138
|
+
|
|
139
|
+
# Process source based on type
|
|
140
|
+
payment_method: Dict[str, Any] = {"type": "scheme"}
|
|
141
|
+
|
|
142
|
+
if request.source.type == SourceType.PROCESSOR_TOKEN:
|
|
143
|
+
payment_method["storedPaymentMethodId"] = request.source.id
|
|
144
|
+
elif request.source.type in [SourceType.BASIS_THEORY_TOKEN, SourceType.BASIS_THEORY_TOKEN_INTENT]:
|
|
145
|
+
# Add card data with Basis Theory expressions
|
|
146
|
+
token_prefix = "token_intent" if request.source.type == SourceType.BASIS_THEORY_TOKEN_INTENT else "token"
|
|
147
|
+
payment_method.update({
|
|
148
|
+
"number": f"{{{{ {token_prefix}: {request.source.id} | json: '$.data.number'}}}}",
|
|
149
|
+
"expiryMonth": f"{{{{ {token_prefix}: {request.source.id} | json: '$.data.expiration_month'}}}}",
|
|
150
|
+
"expiryYear": f"{{{{ {token_prefix}: {request.source.id} | json: '$.data.expiration_year'}}}}",
|
|
151
|
+
"cvc": f"{{{{ {token_prefix}: {request.source.id} | json: '$.data.cvc'}}}}"
|
|
152
|
+
})
|
|
153
|
+
if request.source.holder_name:
|
|
154
|
+
payment_method["holderName"] = request.source.holder_name
|
|
155
|
+
|
|
156
|
+
if request.previous_network_transaction_id:
|
|
157
|
+
payment_method["networkPaymentReference"] = request. previous_network_transaction_id
|
|
158
|
+
|
|
159
|
+
payload["paymentMethod"] = payment_method
|
|
160
|
+
|
|
161
|
+
# Add customer information
|
|
162
|
+
if request.customer:
|
|
163
|
+
if request.customer.reference:
|
|
164
|
+
payload["shopperReference"] = request.customer.reference
|
|
165
|
+
|
|
166
|
+
# Map name fields
|
|
167
|
+
if request.customer.first_name or request.customer.last_name:
|
|
168
|
+
shopper_name: Dict[str, str] = {}
|
|
169
|
+
if request.customer.first_name:
|
|
170
|
+
shopper_name["firstName"] = request.customer.first_name
|
|
171
|
+
if request.customer.last_name:
|
|
172
|
+
shopper_name["lastName"] = request.customer.last_name
|
|
173
|
+
payload["shopperName"] = shopper_name
|
|
174
|
+
|
|
175
|
+
# Map email directly
|
|
176
|
+
if request.customer.email:
|
|
177
|
+
payload["shopperEmail"] = request.customer.email
|
|
178
|
+
|
|
179
|
+
# Map address fields
|
|
180
|
+
if request.customer.address:
|
|
181
|
+
address = request.customer.address
|
|
182
|
+
if any([address.address_line1, address.city, address.state, address.zip, address.country]):
|
|
183
|
+
billing_address: Dict[str, str] = {}
|
|
184
|
+
|
|
185
|
+
# Map address_line1 to street
|
|
186
|
+
if address.address_line1:
|
|
187
|
+
billing_address["street"] = address.address_line1
|
|
188
|
+
|
|
189
|
+
if address.city:
|
|
190
|
+
billing_address["city"] = address.city
|
|
191
|
+
|
|
192
|
+
if address.state:
|
|
193
|
+
billing_address["stateOrProvince"] = address.state
|
|
194
|
+
|
|
195
|
+
if address.zip:
|
|
196
|
+
billing_address["postalCode"] = address.zip
|
|
197
|
+
|
|
198
|
+
if address.country:
|
|
199
|
+
billing_address["country"] = address.country
|
|
200
|
+
|
|
201
|
+
payload["billingAddress"] = billing_address
|
|
202
|
+
|
|
203
|
+
# Map statement description (only name, city is not mapped as per CSV)
|
|
204
|
+
if request.statement_description and request.statement_description.name:
|
|
205
|
+
payload["shopperStatement"] = request.statement_description.name
|
|
206
|
+
|
|
207
|
+
# Map 3DS information
|
|
208
|
+
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
|
|
213
|
+
|
|
214
|
+
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
|
|
219
|
+
|
|
220
|
+
if request.three_ds.version:
|
|
221
|
+
three_ds_data["threeDSVersion"] = request.three_ds.version
|
|
222
|
+
|
|
223
|
+
if three_ds_data:
|
|
224
|
+
payload["additionalData"] = {"threeDSecure": three_ds_data}
|
|
225
|
+
|
|
226
|
+
# Override/merge any provider properties if specified
|
|
227
|
+
if request.override_provider_properties:
|
|
228
|
+
payload = always_merger.merge(payload, request.override_provider_properties)
|
|
229
|
+
|
|
230
|
+
return payload
|
|
231
|
+
|
|
232
|
+
def _transform_adyen_response(self, response_data: Dict[str, Any], request: TransactionRequest) -> TransactionResponse:
|
|
233
|
+
"""Transform Adyen response to our standardized format."""
|
|
234
|
+
transaction_response = TransactionResponse(
|
|
235
|
+
id=str(response_data.get("pspReference")),
|
|
236
|
+
reference=str(response_data.get("merchantReference")),
|
|
237
|
+
amount=Amount(
|
|
238
|
+
value=int(response_data.get("amount", {}).get("value")),
|
|
239
|
+
currency=str(response_data.get("amount", {}).get("currency"))
|
|
240
|
+
),
|
|
241
|
+
status=TransactionStatus(
|
|
242
|
+
code=self._get_status_code(response_data.get("resultCode")),
|
|
243
|
+
provider_code=str(response_data.get("resultCode"))
|
|
244
|
+
),
|
|
245
|
+
source=TransactionSource(
|
|
246
|
+
type=request.source.type,
|
|
247
|
+
id=request.source.id,
|
|
248
|
+
),
|
|
249
|
+
network_transaction_id=str(response_data.get("additionalData", {}).get("networkTxReference")),
|
|
250
|
+
full_provider_response=response_data,
|
|
251
|
+
created_at=datetime.now(timezone.utc)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# checking both as recurringDetailReference is deprecated, although it still appears without storedPaymentMethodId
|
|
255
|
+
stored_payment_id = response_data.get("paymentMethod", {}).get("storedPaymentMethodId")
|
|
256
|
+
recurring_ref = response_data.get("additionalData", {}).get("recurring.recurringDetailReference")
|
|
257
|
+
|
|
258
|
+
if stored_payment_id or recurring_ref:
|
|
259
|
+
transaction_response.source.provisioned = ProvisionedSource(id=stored_payment_id or recurring_ref)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
return transaction_response
|
|
263
|
+
|
|
264
|
+
def _transform_error_response(self, response: requests.Response, response_data: Dict[str, Any]) -> ErrorResponse:
|
|
265
|
+
"""Transform error responses to our standardized format.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
response: The HTTP response object
|
|
269
|
+
response_data: The parsed JSON response data
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Dict[str, Any]: Standardized error response
|
|
273
|
+
"""
|
|
274
|
+
# Map HTTP status codes to error types
|
|
275
|
+
if response.status_code == 401:
|
|
276
|
+
error_type = ErrorType.INVALID_API_KEY
|
|
277
|
+
elif response.status_code == 403:
|
|
278
|
+
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
|
+
else:
|
|
284
|
+
error_type = ErrorType.OTHER
|
|
285
|
+
|
|
286
|
+
return ErrorResponse(
|
|
287
|
+
error_codes=[
|
|
288
|
+
ErrorCode(
|
|
289
|
+
category=error_type.category,
|
|
290
|
+
code=error_type.code
|
|
291
|
+
)
|
|
292
|
+
],
|
|
293
|
+
provider_errors=[response_data.get("refusalReason") or response_data.get("message", "")],
|
|
294
|
+
full_provider_response=response_data
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
async def create_transaction(self, request_data: TransactionRequest) -> TransactionResponse:
|
|
299
|
+
"""Process a payment transaction through Adyen's API directly or via Basis Theory's proxy."""
|
|
300
|
+
validate_required_fields(request_data)
|
|
301
|
+
|
|
302
|
+
# Transform to Adyen's format
|
|
303
|
+
payload = self._transform_to_adyen_payload(request_data)
|
|
304
|
+
|
|
305
|
+
# Set up common headers
|
|
306
|
+
headers = {
|
|
307
|
+
"X-API-Key": self.api_key,
|
|
308
|
+
"Content-Type": "application/json"
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# Make the request (using proxy for BT tokens, direct for processor tokens)
|
|
312
|
+
try:
|
|
313
|
+
response = self.request_client.request(
|
|
314
|
+
url=f"{self.base_url}/payments",
|
|
315
|
+
method="POST",
|
|
316
|
+
headers=headers,
|
|
317
|
+
data=payload,
|
|
318
|
+
use_bt_proxy=request_data.source.type != SourceType.PROCESSOR_TOKEN
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
response_data = response.json()
|
|
322
|
+
|
|
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
|
+
# Transform the successful response to our format
|
|
328
|
+
return self._transform_adyen_response(response_data, request_data)
|
|
329
|
+
|
|
330
|
+
except requests.exceptions.HTTPError as e:
|
|
331
|
+
try:
|
|
332
|
+
error_data = e.response.json()
|
|
333
|
+
except:
|
|
334
|
+
error_data = None
|
|
335
|
+
|
|
336
|
+
raise TransactionError(self._transform_error_response(e.response, error_data))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
async def refund_transaction(self, refund_request: RefundRequest) -> RefundResponse:
|
|
340
|
+
"""
|
|
341
|
+
Refund a payment transaction through Adyen's API.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
refund_request (RefundRequest): The refund request details
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
RefundResponse: The refund response
|
|
348
|
+
"""
|
|
349
|
+
# Set up headers
|
|
350
|
+
headers = {
|
|
351
|
+
"X-API-Key": self.api_key,
|
|
352
|
+
"Content-Type": "application/json"
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# Prepare the refund payload
|
|
356
|
+
payload = {
|
|
357
|
+
"merchantAccount": self.merchant_account,
|
|
358
|
+
"reference": refund_request.reference,
|
|
359
|
+
"amount": {
|
|
360
|
+
"value": refund_request.amount.value,
|
|
361
|
+
"currency": refund_request.amount.currency
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# Add refund reason if provided
|
|
366
|
+
if refund_request.reason:
|
|
367
|
+
payload["merchantRefundReason"] = refund_request.reason
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
# Make request to Adyen
|
|
371
|
+
response = self.request_client.request(
|
|
372
|
+
url=f"{self.base_url}/payments/{refund_request.original_transaction_id}/refunds",
|
|
373
|
+
method="POST",
|
|
374
|
+
headers=headers,
|
|
375
|
+
data=payload,
|
|
376
|
+
use_bt_proxy=False # Refunds don't need BT proxy
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
response_data = response.json()
|
|
380
|
+
|
|
381
|
+
# Transform the response to a standardized format
|
|
382
|
+
return RefundResponse(
|
|
383
|
+
id=response_data.get('pspReference'),
|
|
384
|
+
reference=response_data.get('reference'),
|
|
385
|
+
amount=Amount(
|
|
386
|
+
value=response_data.get('amount', {}).get('value'),
|
|
387
|
+
currency=response_data.get('amount', {}).get('currency')
|
|
388
|
+
),
|
|
389
|
+
status=TransactionStatus(
|
|
390
|
+
code=TransactionStatusCode.RECEIVED,
|
|
391
|
+
provider_code=response_data.get('status')
|
|
392
|
+
),
|
|
393
|
+
refunded_transaction_id=response_data.get('paymentPspReference'),
|
|
394
|
+
full_provider_response=response_data,
|
|
395
|
+
created_at=datetime.now(timezone.utc)
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
except requests.exceptions.HTTPError as e:
|
|
399
|
+
try:
|
|
400
|
+
error_data = e.response.json()
|
|
401
|
+
except:
|
|
402
|
+
error_data = None
|
|
403
|
+
|
|
404
|
+
raise TransactionError(self._transform_error_response(e.response, error_data))
|
|
405
|
+
|