"""
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)