from __future__ import annotations
from typing import TYPE_CHECKING, List, Optional, Union
from django.core.exceptions import ValidationError
from django.http import HttpRequest
from django.urls import include, path
from django.utils.translation import gettext_lazy as _
from salesman.conf import app_settings
from salesman.core.utils import get_salesman_model
if TYPE_CHECKING: # pragma: no cover
from salesman.basket.models import BaseBasket
from salesman.orders.models import BaseOrder, BaseOrderPayment
Basket = get_salesman_model('Basket')
Order = get_salesman_model('Order')
OrderPayment = get_salesman_model('OrderPayment')
[docs]class PaymentError(Exception):
"""
Payment error for raising payment related exceptions.
"""
[docs]class PaymentMethod:
"""
Base payment method, all payment methods defined
in ``SALESMAN_PAYMENT_METHODS`` must extend this class.
"""
identifier: str
label: str
[docs] def get_urls(self) -> list:
"""
Hook for adding extra url patterns for payment method.
Urls will be included as child patterns under the defined
identifier namespace => ``/payment/{identifier}/{urls}``.
"""
return []
[docs] def validate_basket(self, basket: BaseBasket, request: HttpRequest) -> None:
"""
Method used to validate that payment method is valid for the given basket.
Args:
basket (Basket): Basket instance
request (HttpRequest): Django request
Raises:
ValidationError: If payment is not valid for basket
"""
if not basket.count:
raise ValidationError(_("Your basket is empty."))
[docs] def validate_order(self, order: BaseOrder, request: HttpRequest) -> None:
"""
Method used to validate that payment method is valid for the given order.
Args:
order (Order): Order instance
request (HttpRequest): Django request
Raises:
ValidationError: If payment is not valid for order
"""
if order.is_paid:
raise ValidationError(_("This order has already been paid for."))
if order.status not in order.Status.get_payable():
msg = _("Payment for order with status '{status}' is not allowed.")
raise ValidationError(msg.format(status=order.status_display))
[docs] def basket_payment(
self,
basket: BaseBasket,
request: HttpRequest,
) -> Union[str, dict]:
"""
This method gets called when new checkout is submitted and
is responsible for creating a new order from given basket.
Args:
basket (Basket): Basket instance
request (HttpRequest): Django request
Raises:
PaymentError: If error with payment occurs
Returns:
Union[str, dict]: Redirect URL string or JSON serializable data dictionary
"""
raise NotImplementedError("Method `basket_payment()` is not implemented.")
[docs] def order_payment(
self,
order: BaseOrder,
request: HttpRequest,
) -> Union[str, dict]:
"""
This method gets called when payment for an existing order is requested.
Args:
order (Order): Order instance
request (HttpRequest): Django request
Raises:
PaymentError: If error with payment occurs
Returns:
Union[str, dict]: Redirect URL string or JSON serializable data dictionary
"""
raise NotImplementedError("Method `order_payment()` is not implemented.")
[docs] def refund_payment(self, payment: BaseOrderPayment) -> bool:
"""
This method gets called when orders payment refund is requested.
Should return True if refund was completed.
Args:
payment (OrderPayment): Order payment instance
Returns:
bool: True if refund was completed
"""
return False
[docs]class PaymentMethodsPool:
"""
Pool for storing payment method instances.
"""
def __init__(self) -> None:
self._payments: Optional[list[PaymentMethod]] = None
[docs] def get_payments(self, kind: Optional[str] = None) -> List[PaymentMethod]:
"""
Returns payment method instances.
Args:
kind (Optional[str], optional): Either basket or order. Defaults to None.
Returns:
List[PaymentMethod]: Payment method instances
"""
if not self._payments:
self._payments = [P() for P in app_settings.SALESMAN_PAYMENT_METHODS]
if kind in ['basket', 'order']:
method = f'{kind}_payment'
return [p for p in self._payments if method in p.__class__.__dict__]
return self._payments
[docs] def get_urls(self) -> list:
"""
Returns a list of url patterns for payments to be included.
"""
urlpatterns = []
for payment in self.get_payments():
urls = payment.get_urls()
if urls:
base_url = f'payment/{payment.identifier}/'
urlpatterns.append(path(base_url, include(urls)))
return urlpatterns
[docs] def get_choices(self, kind: Optional[str] = None) -> list:
"""
Return payments formated as choices list of tuples.
Args:
kind (Optional[str], optional): Either basket or order. Defaults to None.
Returns:
list: List of choices
"""
return [(p.identifier, p.label) for p in self.get_payments(kind=kind)]
[docs] def get_payment(
self,
identifier: str,
kind: Optional[str] = None,
) -> Optional[PaymentMethod]:
"""
Returns payment with given identifier.
Args:
identifier (str): Payment identifier
kind (Optional[str], optional): Either basket or order. Defaults to None.
Returns:
PaymentMethod: Payment method instance
"""
for payment in self.get_payments(kind=kind):
if payment.identifier == identifier:
return payment
return None
payment_methods_pool = PaymentMethodsPool()