Source code for yenepay.models.checkout

"""
YenePay models
"""
import json
import typing
import uuid
from abc import ABCMeta, abstractmethod
from pprint import pformat

from requests import codes

from yenepay.api import ApiRequest
from yenepay.constants import CART, EXPRESS
from yenepay.exceptions import CheckoutError
from yenepay.models.pdt import PDT


[docs]class Item: """ Represent a single item to be purchased. Item will be used when Express or Cart checkout is created. .. note:: Item ID is not required, but if not given the class will generate new UUID for an item. * Sample usage >>> from yenepay import Item >>> >>> # Creating a single item >>> item = Item( name="PC-1", unit_price=42_000.00, quantity=1, ) >>> # Or using positional arguments >>> item = Item("PC-1", 42_000.00, 1) >>> # Creating multiple items >>> items = [ Item("PC-2", 40_000.99, 1), Item("PC-3", 40_000.99, 1), Item("PC-4", 40_000.99, 1), Item("PC-5", 40_000.99, 1), ] >>> # Or using Cart >>> from yenepay import Cart, Item >>> cart = Cart( Item("PC-6", 50_700.99, 1), Item("PC-7", 43_001.99, 1), ) """ def __init__( self, name: str, unit_price: float, quantity: int, item_id: typing.Optional[str] = None, ) -> None: """ :param name: A unique identifier of the item (SKU, UUID,…) that is used to identify the item on the merchant’s platform. :type name: :func:`str` :param unit_price: Amount in ETB currency. :type unit_price: :func:`float` :param quantity: Quantity of the item. :type quantity: :func:`int` :param item_id: Optional item id. :type item_id: Optional :func:`str` :rtype: :obj:`None` """ self.itemId = item_id or str(uuid.uuid4()) self.itemName = name self.unitPrice = unit_price self.quantity = quantity @property def id(self) -> typing.Union[str, uuid.UUID]: """ :return: item id :rtype: :func:`str` or :class:`uuid.UUID` """ return self.itemId @id.setter def id(self, value: str): """set item id""" self.itemId = value @property def name(self) -> str: """ :return: item name :rtype: :func:`str` """ return self.itemName @name.setter def name(self, value: str): """set item name""" self.itemName = value @property def unit_price(self) -> float: """ :return: item unit price :rtype: :func:`float` """ return self.unitPrice @unit_price.setter def unit_price(self, value: float): """set item unit price""" self.unitPrice = value
[docs] def to_dict(self) -> dict: """ Convert item properties into dictionary object. :return: dictionary of item properties. :rtype: :func:`dict` """ return { attr: getattr(self, attr, None) for attr in ["itemId", "itemName", "unitPrice", "quantity"] if getattr(self, attr, None) is not None }
[docs] def to_json(self) -> bytes: """ Convert item properties into json format. usefull while creating requests. :return: Json representation of item properties. :rtype: :func:`bytes` """ return json.dumps(self.to_dict())
def __repr__(self) -> str: """Item representation.""" return "<Item '{}'>".format(self.name) def __str__(self) -> str: """return item string represenation.""" return self.__repr__()
[docs]class Cart: """ Represent a collection of multiple items to be purchased. Add addtional functionalityies for items. """ def __init__(self, *items: typing.List[Item]) -> None: """ :param items: Collection of :class:`yenepay.models.checkout.Item` objects :type items: List of :class:`yenepay.models.checkout.Item` :rtype: :obj:`None` """ self._items = list(items) self._total_price = 0 self._total_quantity = 0 self._validate() def _validate(self) -> None: """ Validate cart configurations, and initialize cart properties. """ for idx, item in enumerate(self._items): self.__validate_item(item, idx) self._total_price += item.unitPrice self._total_quantity += item.quantity def __validate_item( self, item: Item, idx: typing.Optional[int] = 0 ) -> None: """Validate a single item is valid or not.""" if not isinstance(item, Item): raise TypeError( "Items parameter must be type of yenepay.Item," "got {} at index {}".format(type(item).__name__, idx) )
[docs] def create_item( self, name: str, unit_price: float, quantity: int, item_id: typing.Optional[str] = None, ) -> Item: """ Create a new Item instance and add into a cart. :param name: A unique identifier of the item (SKU, UUID,…) that is used to identify the item on the merchant’s platform. :type name: :func:`str` :param unit_price: Amount in ETB currency. Required for Express type checkout. :type unit_price: :func:`float` :param quantity: Quantity of the item. Required for Express type checkout. :type quantity: :func:`int` :param item_id: Optional item id. Required for Express type checkout. :type item_id: Optional :func:`str` :return: Created item :rtype: :class:`yenepay.models.checkout.Item` """ itemId = item_id or str(uuid.uuid4()) item = Item(name, unit_price, quantity, itemId) self._items.append(item) return item
def __iter__(self) -> typing.Iterator: """return iterator of a given cart.""" return iter(self._items) def __contains__(self, item: Item) -> bool: """Check a given item is in the cart.""" return item in self._items
[docs] def add_item(self, item: Item) -> None: """ Add a single item into a cart. :param item: Item to be added into a cart. :type item: :class:`yenepay.models.checkout.Item` :rtype: :obj:`None` """ self.__validate_item(item) self._items.append(item) self._total_price += item.unitPrice self._total_quantity += item.quantity
def __iadd__(self, item: Item) -> None: """add item into a cart.""" self.__validate_item(item) self._items.append(item) def __imul__(self, value: int) -> None: """multiply number of items.""" self._items *= value self._total_price *= value self._total_quantity *= value def __len__(self) -> None: """return number of items.""" return len(self._items) def __getitem__(self, postion: int) -> Item: """return item at a given postion.""" return self._items[postion] def __repr__(self) -> typing.List: """return cart representation.""" return str(self._items) @property def total_price(self) -> float: """ :return: cart total price :rtype: :func:`float` """ return self._total_price @property def total_quantity(self) -> int: """ :return: cart total quantity. :rtype: :func:`int` """ return self._total_quantity
[docs]class Checkout(metaclass=ABCMeta): """ An abstract class to creates a new payment order on YenePay for a given items and generate redirect link to checkout application to complete the payment. """ @abstractmethod def __init__( self, client: str, process: str, items: typing.Union[typing.List[Item], Cart], merchant_order_id: typing.Optional[str] = None, success_url: typing.Optional[str] = None, cancel_url: typing.Optional[str] = None, ipn_url: typing.Optional[str] = None, failure_url: typing.Optional[str] = None, expires_after: typing.Optional[int] = None, expires_in_days: typing.Optional[int] = 1, total_items_handling_fee: typing.Optional[float] = None, total_items_delivery_fee: typing.Optional[float] = None, total_items_discount: typing.Optional[float] = None, total_items_tax1: typing.Optional[float] = None, total_items_tax2: typing.Optional[float] = None, ) -> None: """ :param client: yenepay.Client instance. :type client: :class:`yenepay.models.client.Client` :param process: Checkout type for this payment. Should have a value of either Express or Cart. Use Express checkout type for single item payment and Cart if this payment includes more than one item. :type process: :func:`str` :param items: Items to be purchased. :type items: List :class:`yenepay.models.checkout.Item` :param merchant_order_id: A unique identifier for this payment order on the merchant’s platform. Will be used to track payment status for this order. :type merchant_order_id: Optional :func:`str` :param success_url: A fully qualified URL endpoint on the merchant’s platform that will be used to redirect the paying customer after the payment has successfully been completed. :type success_url: Optional :func:`str` :param cancel_url: A fully qualified URL endpoint on the merchant’s platform that will be used to redirect the paying customer if this payment is cancelled by the customer. :type cancel_url: Optional :func:`str` :param ipn_url: A fully qualified URL endpoint on the merchant’s platform that will be used to send Instant Payment Notification to the merchant’s platform when a payment is successfully completed. :type ipn_url: Optional :func:`str` :param failure_url: A fully qualified URL endpoint on the merchant’s platform that will be used to redirect the paying customer if this payment fails. :type failure_url: Optional :func:`str` :param expires_after: Expiration period for this payment in minutes. This payment order will expire after the specified number of minutes, if specified. :type expires_after: Optional :func:`int` :param expires_in_days: Expiration period for this payment in days. This payment order will expire after the specified number of days. The default value is 1 day. :type expires_in_days: Optional :func:`int` :param total_items_handling_fee: Handling fee in ETB currency for this payment order, if applicable. Set this value for Cart type checkout. When calculating total payment amount, this will be added to the cart items total amount. :type total_items_handling_fee: Optional :func:`float` :param total_items_delivery_fee: Delivery or shipping fee in ETB currency for this payment order, if applicable. Set this value for Cart type checkout. When calculating total payment amount, this will be added to the cart items total amount. :type total_items_devlivery_fee: Optional :func:`float` :param total_items_discount: Discount amount in ETB currency for this payment order, if applicable. Set this value for Cart type checkout. When calculating total payment amount, this will be deducted from the cart items total amount. :type total_items_discount: Optional :func:`float` :param total_items_tax1: Tax amount in ETB currency for this payment order, if applicable. Set this value for Cart type checkout. When calculating total payment amount, this will be added to the cart items total amount. :type total_items_tax1: Optional :func:`float` :param total_items_tax2: Tax amount in ETB currency for this payment order, if applicable. Set this value for Cart type checkout. When calculating total payment amount, this will be added to the cart items total amount. :type total_items_tax2: Optional :func:`float` :rtype: :obj:`None` """ self._client = client self._process = process self.items = items self.merchantOrderId = merchant_order_id self.successUrl = success_url self.cancelUrl = cancel_url self.ipnUrl = ipn_url self.failureUrl = failure_url self.expiresAfter = expires_after self.expiresInDays = expires_in_days self.totalItemsHandlingFee = total_items_handling_fee self.totalItemsDeliveryFee = total_items_delivery_fee self.totalItemsDiscount = total_items_discount self.totalItemsTax1 = total_items_tax1 self.totalItemsTax2 = total_items_tax2 self._validate() def _validate(self) -> None: """ validate configuration. """ from yenepay.models.client import Client if not isinstance(self._client, Client): raise TypeError( "client must be instance of yenepay.Client, got {}".format( type(self._client).__name__ ) ) if self.process is None: raise ValueError("Checkout process cannot be None.") if self.process not in (EXPRESS, CART): raise ValueError( "Process must be {} or {}, got {}".format( EXPRESS, CART, self.process ) ) if not isinstance(self.items, (tuple, set, list, Cart)): raise TypeError( "Items must be tuple, set, list or yenepay.Cart," " got {}".format(type(self.items).__name__) ) if not self.items: raise ValueError("Items cannot be empty") if not isinstance(self.items, Cart): cart = Cart(*(item for item in self.items)) self.items = cart if self.process == EXPRESS and len(self.items) > 1: raise ValueError( "'{}' process is for a single item. if you want to " "purchase multiple item use '{}' for process" " parameter.".format(EXPRESS, CART) ) @property def process(self) -> str: """ :return: checkout process type. :rtype: :func:`str` """ return getattr(self, "_process", None) @property def merchant_id(self) -> str: """ :return: checkout merchant id. :rtype: :func:`str` """ return self._client.merchantId @property def merchantId(self) -> str: """ :return: checkout merchant id. :rtype: :func:`str` """ return self._client.merchantId @property def token(self) -> str: """ :return: client pdt token. :rtype: :func:`str` """ return self._client.pdtToken @property def merchant_order_id(self) -> typing.Optional[str]: """ :return: checkout merchant order id. :rtype: :func:`str` """ return self.merchantOrderId @merchant_order_id.setter def merchant_order_id(self, value: typing.Optional[str]) -> None: """set merchant order id""" self.merchantOrderId = value @property def success_url(self) -> typing.Optional[str]: """ :return: checkout success url. :rtype: :func:`str` """ return self.successUrl @success_url.setter def success_url(self, value: typing.Optional[str]) -> None: """set checkout success url""" self.successUrl = value @property def cancel_url(self): """ :return: chechout cancel url. :rtype: :func:`str` """ return self.cancelUrl @cancel_url.setter def cancel_url(self, value: typing.Optional[str]) -> None: """set checkout cancel url""" self.cancelUrl = value @property def ipn_url(self) -> typing.Optional[str]: """ :return: chckout ipn url. :rtype: :func:`str` """ return self.ipnUrl @ipn_url.setter def ipn_url(self, value: typing.Optional[str]) -> None: """set ipn url""" self.ipnUrl = value @property def failure_url(self) -> typing.Optional[str]: """ :return: checkout failure url. :rtype: :func:`str` """ return self.failureUrl @failure_url.setter def failure_url(self, value: typing.Optional[str]) -> None: """set failure url""" self.failureUrl = value @property def expires_after(self) -> int: """ :return: checkout expires after. :rtype: :func:`int` """ return self.expiresAfter @expires_after.setter def expires_after(self, value: int) -> None: """set expires after""" self.expiresAfter = value @property def expires_in_days(self) -> int: """ :return: checkout expires in days. :rtype: :func:`int` """ return self.expiresInDays @expires_in_days.setter def expires_in_days(self, value: int) -> None: """set expires in days""" self.expiresInDays = value @property def total_items_handling_fee(self) -> typing.Optional[float]: """ :return: checkout total items handling fee. :rtype: :func:`int` """ return self.totalItemsHandlingFee @total_items_handling_fee.setter def total_items_handling_fee(self, value: typing.Optional[float]) -> None: """set total items handling fee""" self.totalItemsHandlingFee = value @property def total_items_delivery_fee(self) -> typing.Optional[float]: """ :return: checkout total items delivery fee. :rtype: :func:`float` """ return self.totalItemsDeliveryFee @total_items_delivery_fee.setter def total_items_delivery_fee(self, value: typing.Optional[float]) -> None: """set total items delivery fee""" self.totalItemsDeliveryFee = value @property def total_items_discount(self) -> typing.Optional[float]: """ :return: checkout total items discount :rtype: :func:`float` """ return self.totalItemsDiscount @total_items_discount.setter def total_items_discount(self, value: typing.Optional[float]) -> None: """set total items discount""" self.totalItemsDiscount = value @property def total_items_tax1(self) -> typing.Optional[float]: """ :return: checkout total items tax1. :rtype: :func:`float` """ return self.totalItemsTax1 @total_items_tax1.setter def total_items_tax1(self, value: typing.Optional[float]) -> None: """set total items tax1""" self.totalItemsTax1 = value @property def total_items_tax2(self) -> typing.Optional[float]: """ :return: checkout total items tax2. :rtype: :func:`float` """ return self.totalItemsTax2 @total_items_tax2.setter def total_items_tax2(self, value: typing.Optional[float]) -> None: """set tis_saotal items tax2""" self.totalItemsTax2 = value @property def is_sandbox(self) -> bool: """ :return: check if sandbox is enabled or not. :rtype: :func:`bool` """ return self._client.use_sandbox @property def total_price(self) -> float: """ :return: checkout total price. :rtype: :func:`float` """ return self.items.total_price @property def total_quantity(self) -> int: """ :return: checkout total quantity. :rtype: :func:`int` """ return self.items.total_quantity def __setattr__(self, attr, value) -> None: """set attribute value.""" if attr == "_process" and getattr(self, attr, None) is not None: raise AttributeError("process type attribute is immutable.") super().__setattr__(attr, value)
[docs] def to_dict(self) -> dict: """ Convert checkout properties into dictionary object. :return: dictionary of checkout properties :rtype: :func:`dict` """ data = {} attrs = [ "process", "merchantOrderId", "merchantId", "successUrl", "cancelUrl", "ipnUrl", "failureUrl", "expiresAfter", "expiresInDays", "totalItemsHandlingFee", "totalItemsDeliveryFee", "totalItemsDiscount", "totalItemsTax1", "totalItemsTax2", ] for attr in attrs: if getattr(self, attr, None) is not None: data[attr] = getattr(self, attr) data.update({"items": [item.to_dict() for item in self.items]}) return data
[docs] def to_json(self) -> bytes: """ Convert checkout properties into json format. usefull while creating requests. :return: json representaion of checkout properties :rtype: :func:`bytes` """ return json.dumps(self.to_dict())
def __repr__(self): """representation of checkout object.""" return "<{}Checkout: {} - {}>".format( self.process, self.merchant_order_id, self.merchant_id ) def __str__(self): """string representation of checkout object.""" return self.__repr__()
[docs] def get_url(self) -> str: """ :return: checkout url for payment order :rtype: :func:`str` :raise yenepay.exceptions.CheckoutError: if paramenters are incorrect. """ status_code, response = ApiRequest.checkout( self.to_dict(), self.is_sandbox ) if status_code == codes.ok: return response["result"] else: raise CheckoutError(pformat(response))
[docs] def check_pdt_status(self, transaction_id: str): """ Check pdt status of checkout. :param transaction_id: a unique identifier id of the payment transaction that is set on YenePay’s platform. This id can be obtained from your checkout success_url or ipn_url endpoints. :type transaction_id: :func:`str` :return: Return pdt respose of a server :rtype: :class:`yenepay.models.pdt.PDTRespose` :raise yenepay.exceptions.CheckoutError: if paramenters are incorrect. """ pdt = PDT( self._client, self.merchant_order_id, transaction_id, use_sandbox=self.is_sandbox, ) return pdt.check_status()
[docs]class ExpressCheckout(Checkout): """A Checkout class that process express""" def __init__(self, client, *args, **kwargs): kwargs.pop("process", None) items = kwargs.pop("items", None) if isinstance(items, Item): kwargs.update({"items": [items]}) super().__init__(client, EXPRESS, *args, **kwargs) @property def item(self) -> Item: """ :return: an item of express checkout. :rtype: :class:`yenepay.models.checkout.Item` """ return self.items[0]
[docs]class CartCheckout(Checkout): """A Checkout class that process cart""" def __init__(self, client, *args, **kwargs): kwargs.pop("process", None) super().__init__(client, CART, *args, **kwargs)
[docs] def add_item(self, item): """ Add item into a cart. :param item: an item, that need to be added into a cart. :type item: :class:`yenepay.models.checkout.Item` :rtype: :obj:`None` """ self.items.add_item(item)
[docs] def add_items(self, *items): """ Add multiple items into a cart. :param items: list of itmems, that need to be added into a cart. :type items: List of :class:`yenepay.models.checkout.Item` :rtype: :obj:`None` """ for item in items: self.add_item(item)