From b4b59f1bc57b50a3eafb2761d912b9cd9e6f3371 Mon Sep 17 00:00:00 2001 From: Henrique Salvaro Furtado Date: Tue, 8 Oct 2024 12:05:32 -0700 Subject: [PATCH 1/2] feat: Use Unit timeout value on request's timeout (#2) --- unit/api/base_resource.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/unit/api/base_resource.py b/unit/api/base_resource.py index ff42960b..05815e8f 100644 --- a/unit/api/base_resource.py +++ b/unit/api/base_resource.py @@ -49,7 +49,7 @@ def get(self, resource: str, params: Dict = None, headers: Optional[Dict[str, st max_time=self.configuration.get_timeout, jitter=backoff.random_jitter) def get_with_backoff(path: str, p: Dict, h: Dict[str, str]): - return requests.get(path, params=p, headers=h) + return requests.get(path, params=p, headers=h, timeout=self.configuration.get_timeout()) return get_with_backoff(f"{self.configuration.api_url}/{resource}", params, self.__merge_headers(headers)) @@ -62,7 +62,7 @@ def post(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dic max_time=self.configuration.get_timeout, jitter=backoff.random_jitter) def post_with_backoff(path: str, d: Dict, h: Dict[str, str]): - return requests.post(path, data=d, headers=h) + return requests.post(path, data=d, headers=h, timeout=self.configuration.get_timeout()) return post_with_backoff(f"{self.configuration.api_url}/{resource}", data, self.__merge_headers(headers)) @@ -75,7 +75,7 @@ def post_create(self, resource: str, data: Optional[Dict] = None, headers: Optio max_time=self.configuration.get_timeout, jitter=backoff.random_jitter) def post_create_with_backoff(path: str, d, h): - return requests.post(path, data=d, headers=h) + return requests.post(path, data=d, headers=h, timeout=self.configuration.get_timeout()) return post_create_with_backoff(f"{self.configuration.api_url}/{resource}", data, self.__merge_headers(headers)) @@ -88,7 +88,7 @@ def post_full_path(self, path: str, data: Optional[Dict] = None, headers: Option max_time=self.configuration.get_timeout, jitter=backoff.random_jitter) def post_full_path_with_backoff(p, d, h): - return requests.post(p, data=d, headers=h) + return requests.post(p, data=d, headers=h, timeout=self.configuration.get_timeout()) return post_full_path_with_backoff(path, data, self.__merge_headers(headers)) @@ -101,7 +101,7 @@ def patch(self, resource: str, data: Optional[Dict] = None, headers: Optional[Di max_time=self.configuration.get_timeout, jitter=backoff.random_jitter) def patch_with_backoff(p, d, h): - return requests.patch(p, data=d, headers=h) + return requests.patch(p, data=d, headers=h, timeout=self.configuration.get_timeout()) return patch_with_backoff(f"{self.configuration.api_url}/{resource}", data, self.__merge_headers(headers)) @@ -114,7 +114,7 @@ def delete(self, resource: str, data: Dict = None, headers: Optional[Dict[str, s max_time=self.configuration.get_timeout, jitter=backoff.random_jitter) def delete_with_backoff(p, d, h): - return requests.delete(p, data=d, headers=h) + return requests.delete(p, data=d, headers=h, timeout=self.configuration.get_timeout()) return delete_with_backoff(f"{self.configuration.api_url}/{resource}", data, self.__merge_headers(headers)) @@ -125,7 +125,7 @@ def put(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict max_time=self.configuration.get_timeout, jitter=backoff.random_jitter) def put_with_backoff(p, d, h): - return requests.put(p, data=d, headers=h) + return requests.put(p, data=d, headers=h, timeout=self.configuration.get_timeout()) return put_with_backoff(f"{self.configuration.api_url}/{resource}", data, self.__merge_headers(headers)) From 270e1eaf552b3057db9ba3675ae9dce2c875bb2b Mon Sep 17 00:00:00 2001 From: Henrique Salvaro Furtado Date: Mon, 1 Jun 2026 16:25:45 -0300 Subject: [PATCH 2/2] feat: Add outcome and cardDecisionSource mapping to DTOs and models (#3) --- e2e_tests/authorization_request_test.py | 12 ++++++++++++ e2e_tests/authorization_test.py | 1 + e2e_tests/transaction_test.py | 12 +++++++++--- unit/models/authorization.py | 10 +++++++--- unit/models/authorization_request.py | 2 ++ unit/models/transaction.py | 19 +++++++++++++------ 6 files changed, 44 insertions(+), 12 deletions(-) diff --git a/e2e_tests/authorization_request_test.py b/e2e_tests/authorization_request_test.py index 002c5e3e..9e8f307b 100644 --- a/e2e_tests/authorization_request_test.py +++ b/e2e_tests/authorization_request_test.py @@ -41,6 +41,7 @@ def test_card_transaction_authorization_request_dto(): "createdAt": "2021-06-22T13:39:17.018Z", "amount": 2500, "status": "Pending", + "outcome": "WaitTimeout", "partialApprovalAllowed": False, "merchant": { "name": "Apple Inc.", @@ -84,6 +85,7 @@ def test_card_transaction_authorization_request_dto(): assert authorization_request.id == data["id"] assert authorization_request.type == data["type"] assert authorization_request.attributes.get("cardNetwork") == data["attributes"]["cardNetwork"] + assert authorization_request.attributes.get("outcome") == data["attributes"]["outcome"] def test_atm_authorization_request_dto(): @@ -94,6 +96,7 @@ def test_atm_authorization_request_dto(): "createdAt": "2021-06-22T13:39:17.018Z", "amount": 2500, "status": "Pending", + "outcome": "PostTimeout", "partialApprovalAllowed": False, "direction": "Debit", "atmName": "HOME FED SAV BK", @@ -129,6 +132,7 @@ def test_atm_authorization_request_dto(): assert authorization_request.id == data["id"] assert authorization_request.type == data["type"] assert authorization_request.attributes.get("cardNetwork") == data["attributes"]["cardNetwork"] + assert authorization_request.attributes.get("outcome") == data["attributes"]["outcome"] def test_purchase_authorization_request_dto(): @@ -139,6 +143,7 @@ def test_purchase_authorization_request_dto(): "createdAt": "2021-06-22T13:39:17.018Z", "amount": 2500, "status": "Pending", + "outcome": "Approved", "partialApprovalAllowed": False, "merchant": { "name": "Apple Inc.", @@ -179,6 +184,13 @@ def test_purchase_authorization_request_dto(): } } + authorization_request = DtoDecoder.decode(data) + assert type(authorization_request) is PurchaseAuthorizationRequestDTO + assert authorization_request.id == data["id"] + assert authorization_request.type == data["type"] + assert authorization_request.attributes.get("cardNetwork") == data["attributes"]["cardNetwork"] + assert authorization_request.attributes.get("outcome") == data["attributes"]["outcome"] + # # def test_decline_request(): diff --git a/e2e_tests/authorization_test.py b/e2e_tests/authorization_test.py index bc5bd102..31c839f7 100644 --- a/e2e_tests/authorization_test.py +++ b/e2e_tests/authorization_test.py @@ -79,6 +79,7 @@ def test_authorization_api_response(): "cardNetwork": "Visa", "paymentMethod": "Swipe", "digitalWallet": "Google", + "cardDecisionSource": "Org", "cashWithdrawalAmount": 150 }, "relationships": { diff --git a/e2e_tests/transaction_test.py b/e2e_tests/transaction_test.py index 1866b363..b4986e88 100644 --- a/e2e_tests/transaction_test.py +++ b/e2e_tests/transaction_test.py @@ -204,7 +204,8 @@ def test_card_transaction(): "cardVerificationData": { "verificationMethod": "CVV2" }, - "cardNetwork": "Visa" + "cardNetwork": "Visa", + "cardDecisionSource": "IssuerStandIn" }, "relationships": { "account": { @@ -233,6 +234,7 @@ def test_card_transaction(): assert transaction.attributes["recurring"] is False assert transaction.attributes["paymentMethod"] == "Contactless" assert transaction.attributes["cardNetwork"] == "Visa" + assert transaction.attributes["cardDecisionSource"] == "IssuerStandIn" assert transaction.attributes["digitalWallet"] == "Apple" assert transaction.attributes["cardVerificationData"]["verificationMethod"] == "CVV2" @@ -252,7 +254,8 @@ def test_atm_transaction(): "atmLocation": "Masontown, PA 15461", "surcharge": 10, "interchange": 15.2, - "cardNetwork": "Allpoint" + "cardNetwork": "Allpoint", + "cardDecisionSource": "TimeoutApprove" }, "relationships": { "account": { @@ -287,6 +290,7 @@ def test_atm_transaction(): assert transaction.attributes["surcharge"] == 10 assert transaction.attributes["interchange"] == 15.2 assert transaction.attributes["cardNetwork"] == "Allpoint" + assert transaction.attributes["cardDecisionSource"] == "TimeoutApprove" def test_purchase_transaction(): purchase_transaction_api_response = { @@ -318,7 +322,8 @@ def test_purchase_transaction(): "cardVerificationData": { "verificationMethod": "CVV2" }, - "cardNetwork": "Visa" + "cardNetwork": "Visa", + "cardDecisionSource": "Unit" }, "relationships": { "account": { @@ -361,6 +366,7 @@ def test_purchase_transaction(): assert transaction.attributes["recurring"] is False assert transaction.attributes["cardPresent"] is True assert transaction.attributes["cardNetwork"] == "Visa" + assert transaction.attributes["cardDecisionSource"] == "Unit" assert transaction.attributes["digitalWallet"] == "Apple" assert transaction.attributes["paymentMethod"] == "Contactless" diff --git a/unit/models/authorization.py b/unit/models/authorization.py index a2726d9f..f3b122a9 100644 --- a/unit/models/authorization.py +++ b/unit/models/authorization.py @@ -4,6 +4,8 @@ from unit.utils import date_utils AuthorizationStatus = Literal["Authorized", "Completed", "Canceled", "Declined"] +CardDecisionSource = Literal["Org", "Unit", "Network", "TimeoutApprove", "TimeoutDecline", "DefaultApprove", + "InternalError", "IssuerStandIn"] class AuthorizationDTO(object): @@ -14,7 +16,8 @@ def __init__(self, id: str, created_at: datetime, amount: int, card_last_4_digit card_network: Optional[str], tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]], merchant_id: Optional[str], decline_reason: Optional[str], cash_withdrawal_amount: Optional[int], summary: Optional[str], - currency_conversion: Optional[CurrencyConversion], rich_merchant_data: Optional[RichMerchantData] + currency_conversion: Optional[CurrencyConversion], rich_merchant_data: Optional[RichMerchantData], + card_decision_source: Optional[CardDecisionSource] = None ): self.id = id self.type = "authorization" @@ -26,7 +29,7 @@ def __init__(self, id: str, created_at: datetime, amount: int, card_last_4_digit "cardVerificationData": card_verification_data, "cardNetwork": card_network, "tags": tags, "declineReason": decline_reason, "cashWithdrawalAmount": cash_withdrawal_amount, "summary": summary, "currencyConversion": currency_conversion, - "richMerchantData": rich_merchant_data} + "richMerchantData": rich_merchant_data, "cardDecisionSource": card_decision_source} self.relationships = relationships @staticmethod @@ -41,7 +44,8 @@ def from_json_api(_id, _type, attributes, relationships): attributes.get("declineReason"), attributes.get("cashWithdrawalAmount"), attributes.get("summary"), CurrencyConversion.from_json_api(attributes.get("currencyConversion")), - RichMerchantData.from_json_api(attributes.get("richMerchantData")) + RichMerchantData.from_json_api(attributes.get("richMerchantData")), + attributes.get("cardDecisionSource") ) diff --git a/unit/models/authorization_request.py b/unit/models/authorization_request.py index 1ca6a087..092cf4f4 100644 --- a/unit/models/authorization_request.py +++ b/unit/models/authorization_request.py @@ -8,6 +8,7 @@ from unit.utils import date_utils PurchaseAuthorizationRequestStatus = Literal["Pending", "Approved", "Declined"] +AuthorizationRequestOutcome = Literal["Approved", "Declined", "PostTimeout", "PostError", "WaitTimeout"] DeclineReason = Literal["AccountClosed", "CardExceedsAmountLimit", "DoNotHonor", "InsufficientFunds", "InvalidMerchant", "ReferToCardIssuer", "RestrictedCard", "Timeout", "TransactionNotPermittedToCardholder"] @@ -19,6 +20,7 @@ def __init__(self, id: str, type: str, attributes: Dict[str, object], self.type = type self.attributes = {"createdAt": date_utils.to_datetime(attributes["createdAt"]), "amount": attributes["amount"], "status": attributes["status"], + "outcome": attributes.get("outcome"), "partialApprovalAllowed": attributes.get("partialApprovalAllowed"), "approvedAmount": attributes.get("approvedAmount"), "declineReason": attributes.get("declineReason"), diff --git a/unit/models/transaction.py b/unit/models/transaction.py index 8977e373..6cfe704d 100644 --- a/unit/models/transaction.py +++ b/unit/models/transaction.py @@ -135,7 +135,7 @@ def __init__(self, id: str, created_at: datetime, direction: str, amount: int, b card_network: Optional[str], tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]], gross_interchange: Optional[str], cash_withdrawal_amount: Optional[int], currency_conversion: Optional[CurrencyConversion], - rich_merchant_data: Optional[RichMerchantData]): + rich_merchant_data: Optional[RichMerchantData], card_decision_source: Optional[str] = None): BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) self.type = 'purchaseTransaction' self.attributes["cardLast4Digits"] = card_last_4_digits @@ -153,6 +153,7 @@ def __init__(self, id: str, created_at: datetime, direction: str, amount: int, b self.attributes["cashWithdrawalAmount"] = cash_withdrawal_amount self.attributes["currencyConversion"] = currency_conversion self.attributes["richMerchantData"] = rich_merchant_data + self.attributes["cardDecisionSource"] = card_decision_source @staticmethod def from_json_api(_id, _type, attributes, relationships): @@ -165,7 +166,7 @@ def from_json_api(_id, _type, attributes, relationships): attributes.get("cardVerificationData"), attributes.get("cardNetwork"), attributes.get("tags"), relationships, attributes.get("grossInterchange"), attributes.get("cashWithdrawalAmount"), CurrencyConversion.from_json_api(attributes.get("currencyConversion")), - RichMerchantData.from_json_api(attributes.get("richMerchantData"))) + RichMerchantData.from_json_api(attributes.get("richMerchantData")), attributes.get("cardDecisionSource")) class AtmTransactionDTO(BaseTransactionDTO): @@ -173,7 +174,8 @@ def __init__(self, id: str, created_at: datetime, direction: str, amount: int, b summary: str, card_last_4_digits: str, atm_name: str, atm_location: Optional[str], surcharge: int, interchange: Optional[int], card_network: Optional[str], tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]], - gross_interchange: Optional[str], currency_conversion: Optional[CurrencyConversion]): + gross_interchange: Optional[str], currency_conversion: Optional[CurrencyConversion], + card_decision_source: Optional[str] = None): BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) self.type = 'atmTransaction' self.attributes["cardLast4Digits"] = card_last_4_digits @@ -184,6 +186,7 @@ def __init__(self, id: str, created_at: datetime, direction: str, amount: int, b self.attributes["cardNetwork"] = card_network self.attributes["grossInterchange"] = gross_interchange self.attributes["currencyConversion"] = currency_conversion + self.attributes["cardDecisionSource"] = card_decision_source @staticmethod def from_json_api(_id, _type, attributes, relationships): @@ -192,7 +195,8 @@ def from_json_api(_id, _type, attributes, relationships): attributes["cardLast4Digits"], attributes["atmName"], attributes.get("atmLocation"), attributes["surcharge"], attributes.get("interchange"), attributes.get("cardNetwork"), attributes.get("tags"), relationships, attributes.get("grossInterchange"), - CurrencyConversion.from_json_api(attributes.get("currencyConversion"))) + CurrencyConversion.from_json_api(attributes.get("currencyConversion")), + attributes.get("cardDecisionSource")) class FeeTransactionDTO(BaseTransactionDTO): @@ -214,7 +218,8 @@ def __init__(self, id: str, created_at: datetime, direction: str, amount: int, b interchange: Optional[int], payment_method: Optional[str], digital_wallet: Optional[str], card_verification_data: Optional[Dict], card_network: Optional[str], tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]], gross_interchange: Optional[str], - currency_conversion: Optional[CurrencyConversion], rich_merchant_data: Optional[RichMerchantData]): + currency_conversion: Optional[CurrencyConversion], rich_merchant_data: Optional[RichMerchantData], + card_decision_source: Optional[str] = None): BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) self.type = 'cardTransaction' self.attributes["cardLast4Digits"] = card_last_4_digits @@ -228,6 +233,7 @@ def __init__(self, id: str, created_at: datetime, direction: str, amount: int, b self.attributes["grossInterchange"] = gross_interchange self.attributes["currencyConversion"] = currency_conversion self.attributes["richMerchantData"] = rich_merchant_data + self.attributes["cardDecisionSource"] = card_decision_source @staticmethod def from_json_api(_id, _type, attributes, relationships): @@ -239,7 +245,8 @@ def from_json_api(_id, _type, attributes, relationships): attributes.get("cardVerificationData"), attributes.get("cardNetwork"), attributes.get("tags"), relationships, attributes.get("grossInterchange"), CurrencyConversion.from_json_api(attributes.get("currencyConversion")), - RichMerchantData.from_json_api(attributes.get("richMerchantData")) + RichMerchantData.from_json_api(attributes.get("richMerchantData")), + attributes.get("cardDecisionSource") )