From fb93771aa7747d23ff1bd73a6dad2ae67d7fd1e8 Mon Sep 17 00:00:00 2001 From: Denis Date: Thu, 6 Jul 2023 16:20:03 +0400 Subject: [PATCH 1/6] added json type alias --- xpayments_cloud.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/xpayments_cloud.py b/xpayments_cloud.py index d6ecd9d..a0b1dd9 100644 --- a/xpayments_cloud.py +++ b/xpayments_cloud.py @@ -7,7 +7,9 @@ from exceptions import IllegalArgumentError, JSONProcessingError, UnicodeProcessingError from request_params import PaymentRequestParams from dotenv import dotenv_values +from typing import TypeAlias +JSON: TypeAlias = dict[str, 'JSON'] | list['JSON'] | str | int | float | bool | None config = dotenv_values(".env") @@ -32,7 +34,7 @@ def __init__(self, account: str, api_key: str, secret_key: str) -> None: self.secret_key = str(secret_key) self.TEST_SERVER_HOST = config.get('TEST_SERVER_HOST', '') - def send(self, controller: str, action: str, request_data: dict) -> str: + def send(self, controller: str, action: str, request_data: dict) -> JSON: """ Send API request to X-Payments Cloud """ @@ -49,7 +51,6 @@ def send(self, controller: str, action: str, request_data: dict) -> str: raise HTTPError response.raise_for_status() try: - # print(response.json()) return response.json() except JSONDecodeError as ex: raise JSONProcessingError(ex.msg) @@ -106,19 +107,19 @@ def __init__(self, account: str, api_key: str, secret_key: str) -> None: self.secret_key = str(secret_key) self.request = Request(account=self.account, api_key=self.api_key, secret_key=self.secret_key) - def do_pay(self, params: PaymentRequestParams) -> str: + def do_pay(self, params: PaymentRequestParams) -> JSON: """ Process payment """ return self.request.send(controller='payment', action='pay', request_data=params.to_dict()) - def do_tokenize_card(self, params: PaymentRequestParams) -> str: + def do_tokenize_card(self, params: PaymentRequestParams) -> JSON: """ Tokenize card """ return self.request.send(controller='payment', action='tokenize_card', request_data=params.to_dict()) - def do_rebill(self, xpid: str, ref_id: str, customer_id: str, cart: list, callback_url: str) -> str: + def do_rebill(self, xpid: str, ref_id: str, customer_id: str, cart: list, callback_url: str) -> JSON: """ Rebill payment (process payment using the saved card) """ @@ -131,7 +132,7 @@ def do_rebill(self, xpid: str, ref_id: str, customer_id: str, cart: list, callba } return self.request.send(controller='payment', action='rebill', request_data=params) - def do_action(self, action: str, xpid: str, amount: int | None = None) -> str: + def do_action(self, action: str, xpid: str, amount: int | None = None) -> JSON: """ Execute secondary payment action """ @@ -140,49 +141,49 @@ def do_action(self, action: str, xpid: str, amount: int | None = None) -> str: params.amount = amount return self.request.send(controller='payment', action=action, request_data=params) - def do_capture(self, xpid: str, amount: int) -> str: + def do_capture(self, xpid: str, amount: int) -> JSON: """ Capture payment """ return self.do_action(action='capture', xpid=xpid, amount=amount) - def do_void(self, xpid: str, amount: int) -> str: + def do_void(self, xpid: str, amount: int) -> JSON: """ Void payment """ return self.do_action(action='void', xpid=xpid, amount=amount) - def do_refund(self, xpid: str, amount: int) -> str: + def do_refund(self, xpid: str, amount: int) -> JSON: """ Refund payment """ return self.do_action(action='refund', xpid=xpid, amount=amount) - def do_continue(self, xpid: str) -> str: + def do_continue(self, xpid: str) -> JSON: """ Continue payment """ return self.do_action(action='continue', xpid=xpid) - def do_accept(self, xpid: str) -> str: + def do_accept(self, xpid: str) -> JSON: """ Accept payment """ return self.do_action(action='accept', xpid=xpid) - def do_decline(self, xpid: str) -> str: + def do_decline(self, xpid: str) -> JSON: """ Decline payment """ return self.do_action(action='decline', xpid=xpid) - def do_get_info(self, xpid: str) -> str: + def do_get_info(self, xpid: str) -> JSON: """ Get detailed payment information """ return self.do_action(action='get_info', xpid=xpid) - def do_get_customer_cards(self, customer_id: str, status: str = 'any') -> str: + def do_get_customer_cards(self, customer_id: str, status: str = 'any') -> JSON: """ Get customer's cards """ @@ -192,7 +193,7 @@ def do_get_customer_cards(self, customer_id: str, status: str = 'any') -> str: } return self.request.send(controller='customer', action='get_cards', request_data=params) - def do_add_bulk_operation(self, operation: str, xpids: list[str]) -> str: + def do_add_bulk_operation(self, operation: str, xpids: list[str]) -> JSON: """ Add bulk operation """ @@ -202,28 +203,28 @@ def do_add_bulk_operation(self, operation: str, xpids: list[str]) -> str: } return self.request.send(controller='bulk_operation', action='add', request_data=params) - def do_start_bulk_operation(self, batch_id: str) -> str: + def do_start_bulk_operation(self, batch_id: str) -> JSON: """ Start bulk operation """ params = {"batch_id": batch_id} return self.request.send(controller='bulk_operation', action='start', request_data=params) - def do_stop_bulk_operation(self, batch_id: str) -> str: + def do_stop_bulk_operation(self, batch_id: str) -> JSON: """ Stop bulk operation """ params = {"batch_id": batch_id} return self.request.send(controller='bulk_operation', action='stop', request_data=params) - def do_get_bulk_operation(self, batch_id: str) -> str: + def do_get_bulk_operation(self, batch_id: str) -> JSON: """ Get bulk operation """ params = {"batch_id": batch_id} return self.request.send(controller='bulk_operation', action='get', request_data=params) - def do_delete_bulk_operation(self, batch_id: str) -> str: + def do_delete_bulk_operation(self, batch_id: str) -> JSON: """ Delete bulk operation """ From 261492d876cc5d4cd354824c8664714868a2f26f Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 10 Jul 2023 22:35:40 +0400 Subject: [PATCH 2/6] added dataclasses for request params --- request_params.py | 133 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 111 insertions(+), 22 deletions(-) diff --git a/request_params.py b/request_params.py index d0bf614..b4451a7 100644 --- a/request_params.py +++ b/request_params.py @@ -2,38 +2,127 @@ from typing import Any +@dataclass(frozen=True) +class TransactionType: + """ + TransactionType.Auth - Authorize only + TransactionType.Sale - Auth & Capture + """ + Auth = "A" + Sale = "S" + + +@dataclass(frozen=True) +class DurationUnit: + Day = "D" + Week = "W" + Month = "M" + Year = "Y" + + +@dataclass(frozen=True) +class ScheduleType: + """ + ScheduleType.Each - Each specific day + ScheduleType.Every - Every X days + """ + Each = "E" + Every = "D" + + +@dataclass +class BaseParamsClass: + def _get(self, field: str) -> Any: + return getattr(self, field) + + def to_dict(self) -> dict: + """ + Converts this dataclass to a dict. + Optional 'None' params are excluded. + """ + return dict((field.name, self._get(field.name)) + for field in fields(self) if getattr(self, field.name) is not None) + + +@dataclass +class Address(BaseParamsClass): + firstname: str + address: str + city: str + state: str + zipcode: str + country: str + lastname: str | None = None + zip4: str | None = None + company: str | None = None + phone: str | None = None + fax: str | None = None + + +@dataclass +class SubscriptionSchedule(BaseParamsClass): + type: ScheduleType | None = None + number: int | None = None + reverse: bool | None = None + period: DurationUnit | None = None + periods: int | None = None + + +@dataclass +class SubscriptionPlan(BaseParamsClass): + subscriptionSchedule: SubscriptionSchedule | None = None + callbackUrl: str | None = None + recurringAmount: float | None = None + description: str | None = None + uniqueOrderItemId: str | None = None + trialDuration: int | None = None + trialDurationUnit: DurationUnit | None = None + + +@dataclass +class CartItem(BaseParamsClass): + sku: str + name: str + price: float + quantity: int | None = None + isSubscription: bool | None = None + subscriptionPlan: SubscriptionPlan | None = None + + +@dataclass +class Cart(BaseParamsClass): + billingAddress: Address + merchantEmail: str + currency: str + items: list[CartItem] + totalCost: float + shippingAddress: Address | None = None + login: str | None = None + shippingCost: float | None = None + taxCost: float | None = None + discount: float | None = None + description: str | None = None + + @dataclass -class PaymentRequestParams: +class PaymentRequestParams(BaseParamsClass): token: str refId: str - customerId: str returnUrl: str - callbackUrl: str - cart: list + cart: Cart + callbackUrl: str | None = None + customerId: str | None = None forceSaveCard: bool | None = None - forceTransactionType: str | None = None + forceTransactionType: TransactionType | None = None confId: int | None = None - def __get(self, field: str) -> Any: + def _get(self, field: str) -> Any: + """ + Boolean values are replaced: True->Y / False->N + """ val = getattr(self, field) if type(val) is bool: return "Y" if val is True else "N" return val - def to_dict(self) -> dict: - """ - Converts this dataclass to a dict. - 'None' params are excluded. Boolean values are replaced: True->Y / False->N - """ - return dict((field.name, self.__get(field.name)) - for field in fields(self) if getattr(self, field.name) is not None) - -@dataclass(frozen=True) -class TransactionType: - """ - TransactionType.A - Authorize only - TransactionType.S - Sale (Auth & Capture) - """ - A = "A" - S = "S" From 6808d3048e55310dd20b803b43bc5850aeda1c83 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 10 Jul 2023 23:35:24 +0400 Subject: [PATCH 3/6] added recursive to_dict() --- request_params.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/request_params.py b/request_params.py index b4451a7..cfb2d1e 100644 --- a/request_params.py +++ b/request_params.py @@ -32,6 +32,11 @@ class ScheduleType: @dataclass class BaseParamsClass: + def __to_dict_nested(self, field: Any) -> Any: + if isinstance(field, BaseParamsClass): + return field.to_dict() + return field + def _get(self, field: str) -> Any: return getattr(self, field) @@ -40,7 +45,7 @@ def to_dict(self) -> dict: Converts this dataclass to a dict. Optional 'None' params are excluded. """ - return dict((field.name, self._get(field.name)) + return dict((field.name, self.__to_dict_nested(self._get(field.name))) for field in fields(self) if getattr(self, field.name) is not None) From dfcee5b83557fabb6e597aab3a458254515216a0 Mon Sep 17 00:00:00 2001 From: Denis Date: Tue, 11 Jul 2023 00:01:06 +0400 Subject: [PATCH 4/6] fix for nested lists --- request_params.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/request_params.py b/request_params.py index cfb2d1e..84217b7 100644 --- a/request_params.py +++ b/request_params.py @@ -35,6 +35,8 @@ class BaseParamsClass: def __to_dict_nested(self, field: Any) -> Any: if isinstance(field, BaseParamsClass): return field.to_dict() + if isinstance(field, list): + return [self.__to_dict_nested(i) for i in field] return field def _get(self, field: str) -> Any: From d97b6e54581331a65c645b8c11778f86bd480e77 Mon Sep 17 00:00:00 2001 From: Denis Date: Tue, 11 Jul 2023 15:54:26 +0400 Subject: [PATCH 5/6] check if list elements are not None, just in case --- request_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/request_params.py b/request_params.py index 84217b7..f03989c 100644 --- a/request_params.py +++ b/request_params.py @@ -36,7 +36,7 @@ def __to_dict_nested(self, field: Any) -> Any: if isinstance(field, BaseParamsClass): return field.to_dict() if isinstance(field, list): - return [self.__to_dict_nested(i) for i in field] + return [self.__to_dict_nested(i) for i in field if i is not None] return field def _get(self, field: str) -> Any: From 53a97611264c6a6979d272d1292bf39561c80f4b Mon Sep 17 00:00:00 2001 From: Denis Date: Sat, 15 Jul 2023 17:28:00 +0400 Subject: [PATCH 6/6] better readability --- request_params.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/request_params.py b/request_params.py index f03989c..8c7ba95 100644 --- a/request_params.py +++ b/request_params.py @@ -33,11 +33,10 @@ class ScheduleType: @dataclass class BaseParamsClass: def __to_dict_nested(self, field: Any) -> Any: - if isinstance(field, BaseParamsClass): - return field.to_dict() - if isinstance(field, list): - return [self.__to_dict_nested(i) for i in field if i is not None] - return field + match field: + case BaseParamsClass(): return field.to_dict() + case list(): return [self.__to_dict_nested(i) for i in field if i is not None] + case _: return field def _get(self, field: str) -> Any: return getattr(self, field)