Skip to content

Commit

Permalink
feat: Implement PaymentProvider Invoice and complete Prepayment (
Browse files Browse the repository at this point in the history
…#45)

And do some more or less related fixes + refactorings
  • Loading branch information
sveneberth authored Nov 29, 2024
1 parent d2cbf4b commit 031d32d
Show file tree
Hide file tree
Showing 16 changed files with 146 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/viur/shop/modules/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def basket_view(
self,
):
"""View the basket (the cart stored in the session) itself"""
return self.shop.cart.view(key=self.shop.cart.current_session_cart_key)
return self.shop.cart.view(key=self.shop.cart.current_session_cart_key, skelType="node")

@exposed
def cart_list(
Expand Down
6 changes: 6 additions & 0 deletions src/viur/shop/modules/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ def order_add(
# use current user as default value
skel["email"] = user["name"]
skel.setBoneValue("customer", user["key"])
# Initialize list for payment attempts / partial payments
if not skel["payment"]:
skel["payment"] = {}
skel["payment"].setdefault("payments", [])
skel = self._order_set_values(
skel,
payment_provider=payment_provider,
Expand Down Expand Up @@ -385,6 +389,8 @@ def can_order(
errors.append(ClientError("cart is missing"))
if not order_skel["cart"] or not order_skel["cart"]["dest"]["shipping_address"]:
errors.append(ClientError("cart.shipping_address is missing"))
if not order_skel["cart"] or not order_skel["cart"]["dest"]["total_quantity"]:
errors.append(ClientError("cart.total_quantity is zero"))
if not order_skel["payment_provider"]:
errors.append(ClientError("missing payment_provider"))
if not order_skel["billing_address"]:
Expand Down
3 changes: 2 additions & 1 deletion src/viur/shop/payment_providers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from .abstract import PaymentProviderAbstract
from .amazon_pay import AmazonPay
from .invoice import Invoice
from .paypal_plus import PayPalPlus
from .prepayment import PrePayment
from .prepayment import PrePayment, Prepayment

try:
import unzer
Expand Down
9 changes: 6 additions & 3 deletions src/viur/shop/payment_providers/abstract.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import abc
import functools
import typing as t

from viur.core import Module, translate
from viur.core.prototypes.instanced_module import InstancedModule
from viur.core.skeleton import SkeletonInstance
from viur.shop.types.response import T

from viur.shop.skeletons.order import OrderSkel

from ..types import *

if t.TYPE_CHECKING:
Expand Down Expand Up @@ -98,6 +95,11 @@ def check_payment_state(
self,
order_skel: SkeletonInstance,
) -> tuple[bool, t.Any]:
"""
Check the payment state from the PaymentProvider API/service
Access :attr:`OrderSkel.is_paid` to get the payment state of an order.
"""
...

@abc.abstractmethod
Expand Down Expand Up @@ -131,4 +133,5 @@ def serialize_for_api(
is_available=self.is_available(order_skel),
)


PaymentProviderAbstract.html = True
3 changes: 2 additions & 1 deletion src/viur/shop/payment_providers/amazon_pay.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

from viur.core import current, errors, exposed
from viur.core.skeleton import SkeletonInstance

from . import PaymentProviderAbstract
from ..globals import SHOP_LOGGER

logger = SHOP_LOGGER.getChild(__name__)


class AmazonPay(PaymentProviderAbstract):
name = "amazonpay"
name: t.Final[str] = "amazonpay"

def __init__(
self,
Expand Down
58 changes: 58 additions & 0 deletions src/viur/shop/payment_providers/invoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import typing as t

from viur.core import errors, exposed, utils
from viur.core.skeleton import SkeletonInstance

from . import PaymentProviderAbstract
from ..globals import SHOP_LOGGER
from ..types.exceptions import IllegalOperationError

logger = SHOP_LOGGER.getChild(__name__)


class Invoice(PaymentProviderAbstract):
"""
Order is directly RTS, but not paid.
The customer pays this order in the next x days, independent of shipping.
But this will not be handled or checked here.
"""

name: t.Final[str] = "invoice"

def checkout(
self,
order_skel: SkeletonInstance,
) -> None:
# TODO: Standardize this, write in txn
order_skel["payment"].setdefault("payments", []).append({
"pp": self.name,
"creationdate": utils.utcNow().isoformat(),
})
order_skel.toDB()
return None

def charge(self) -> None:
# An invoice cannot be charged, The user has to do this on his own
raise IllegalOperationError("An invoice cannot be charged")

def check_payment_state(
self,
order_skel: SkeletonInstance,
) -> tuple[bool, t.Any]:
# An invoice payment state cannot be checked without access to the target bank account
# Use meth:`Order.set_paid` to mark an order by external events as paid.
raise IllegalOperationError("The invoice payment_state cannot be checked by this PaymentProvider")

@exposed
def return_handler(self):
raise errors.NotImplemented # TODO: We need a generalized solution for this

@exposed
def webhook(self):
# Use meth:`Order.set_paid` to mark an order by external events as paid.
raise errors.NotImplemented("An invoice has no webhook") # This NotImplemented is fully intentional

@exposed
def get_debug_information(self):
raise errors.NotImplemented # TODO
3 changes: 2 additions & 1 deletion src/viur/shop/payment_providers/paypal_plus.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

from viur.core import errors, exposed
from viur.core.skeleton import SkeletonInstance

from . import PaymentProviderAbstract
from ..globals import SHOP_LOGGER

logger = SHOP_LOGGER.getChild(__name__)


class PayPalPlus(PaymentProviderAbstract):
name = "paypal_plus"
name: t.Final[str] = "paypal_plus"

def checkout(
self,
Expand Down
54 changes: 43 additions & 11 deletions src/viur/shop/payment_providers/prepayment.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,67 @@
import typing as t
from viur.core import errors, exposed

from deprecated.sphinx import deprecated
from viur.core import errors, exposed, utils
from viur.core.skeleton import SkeletonInstance

from . import PaymentProviderAbstract
from ..globals import SHOP_LOGGER
from ..types.exceptions import IllegalOperationError

logger = SHOP_LOGGER.getChild(__name__)


class PrePayment(PaymentProviderAbstract):
name = "prepayment"
class Prepayment(PaymentProviderAbstract):
"""
Order is RTS as soon as the customer has paid.
The customer pays this order in the next x days, shipping will wait.
But this will not be handled or checked here.
"""
name: t.Final[str] = "prepayment"

def checkout(
self,
order_skel: SkeletonInstance,
) -> t.Any:
raise errors.NotImplemented()
# TODO: Standardize this, write in txn
order_skel["payment"].setdefault("payments", []).append({
"pp": self.name,
"creationdate": utils.utcNow().isoformat(),
})
order_skel.toDB()
return None

def charge(self):
raise errors.NotImplemented()
def charge(self) -> None:
# An invoice cannot be charged, The user has to do this on his own
raise IllegalOperationError("A prepayment cannot be charged")

def check_payment_state(self):
raise errors.NotImplemented()
def check_payment_state(
self,
order_skel: SkeletonInstance,
) -> tuple[bool, t.Any]:
# A prepayment payment state cannot be checked without access to the target bank account
# Use meth:`Order.set_paid` to mark an order by external events as paid.
raise IllegalOperationError("The invoice payment_state cannot be checked by this PaymentProvider")

@exposed
def return_handler(self):
raise errors.NotImplemented()
raise errors.NotImplemented # TODO: We need a generalized solution for this

@exposed
def webhook(self):
raise errors.NotImplemented()
# Use meth:`Order.set_paid` to mark an order by external events as paid.
raise errors.NotImplemented("An invoice has no webhook") # This NotImplemented is fully intentional

@exposed
def get_debug_information(self):
raise errors.NotImplemented()
raise errors.NotImplemented # TODO


@deprecated(
reason="Class has been renamed, Use :class:`Prepayment`",
version="0.1.0.dev24",
action="always",
)
class PrePayment(Prepayment):
...
3 changes: 1 addition & 2 deletions src/viur/shop/payment_providers/unzer_abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,7 @@ def save_type(
order_skel = self.shop.order.editSkel()
if not order_skel.fromDB(order_key):
raise errors.NotFound
if not order_skel["payment"]:
order_skel["payment"] = {}
# TODO: Standardize this, write in txn
order_skel["payment"].setdefault("payments", []).append({
"pp": self.name,
"creationdate": utils.utcNow().isoformat(),
Expand Down
6 changes: 4 additions & 2 deletions src/viur/shop/payment_providers/unzer_bancontact.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import typing as t

import unzer
from unzer.model import PaymentType

from viur.core.skeleton import SkeletonInstance

from .unzer_abstract import UnzerAbstract
from ..globals import SHOP_LOGGER

logger = SHOP_LOGGER.getChild(__name__)


class UnzerBancontact(UnzerAbstract):
name = "unzer-bancontact"
name: t.Final[str] = "unzer-bancontact"

def get_payment_type(
self,
Expand Down
6 changes: 4 additions & 2 deletions src/viur/shop/payment_providers/unzer_card.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import typing as t

import unzer
from unzer.model import PaymentType

from viur.core.skeleton import SkeletonInstance

from .unzer_abstract import UnzerAbstract
from ..globals import SHOP_LOGGER

logger = SHOP_LOGGER.getChild(__name__)


class UnzerCard(UnzerAbstract):
name = "unzer-card"
name: t.Final[str] = "unzer-card"

def get_payment_type(
self,
Expand Down
6 changes: 4 additions & 2 deletions src/viur/shop/payment_providers/unzer_ideal.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import typing as t

import unzer
from unzer.model import PaymentType

from viur.core.skeleton import SkeletonInstance

from .unzer_abstract import UnzerAbstract
from ..globals import SHOP_LOGGER

logger = SHOP_LOGGER.getChild(__name__)


class UnzerIdeal(UnzerAbstract):
name = "unzer-ideal"
name: t.Final[str] = "unzer-ideal"

def get_payment_type(
self,
Expand Down
6 changes: 4 additions & 2 deletions src/viur/shop/payment_providers/unzer_paypal.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import typing as t

import unzer
from unzer.model import PaymentType

from viur.core.skeleton import SkeletonInstance

from .unzer_abstract import UnzerAbstract
from ..globals import SHOP_LOGGER

logger = SHOP_LOGGER.getChild(__name__)


class UnzerPayPal(UnzerAbstract):
name = "unzer-paypal"
name: t.Final[str] = "unzer-paypal"

def get_payment_type(
self,
Expand Down
6 changes: 4 additions & 2 deletions src/viur/shop/payment_providers/unzer_sofort.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import typing as t

import unzer
from unzer.model import PaymentType

from viur.core.skeleton import SkeletonInstance

from .unzer_abstract import UnzerAbstract
from ..globals import SHOP_LOGGER

logger = SHOP_LOGGER.getChild(__name__)


class UnzerSofort(UnzerAbstract):
name = "unzer-sofort"
name: t.Final[str] = "unzer-sofort"

def get_payment_type(
self,
Expand Down
2 changes: 1 addition & 1 deletion src/viur/shop/skeletons/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class OrderSkel(Skeleton): # STATE: Complete (as in model)
kind="{{viur_shop_modulename}}_cart_node",
module="{{viur_shop_modulename}}/cart_node",
consistency=RelationalConsistency.PreventDeletion,
refKeys=["key", "name", "shipping_address"],
refKeys=["name", "shipping_address", "total_quantity"],
)

total = NumericBone(
Expand Down
4 changes: 4 additions & 0 deletions src/viur/shop/types/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class InvalidStateError(ViURShopException):
...


class IllegalOperationError(ViURShopException):
...


class DispatchError(ViURShopException):
def __init__(self, msg: t.Any, hook: Hook, *args: t.Any) -> None:
super().__init__(msg, *args)
Expand Down

0 comments on commit 031d32d

Please sign in to comment.