Skip to content

Commit

Permalink
Merge pull request #4669 from mirumee/extract-payment-plugins
Browse files Browse the repository at this point in the history
Extract payment plugins
  • Loading branch information
korycins committed Sep 20, 2019
2 parents d369a4b + 24fa2cc commit 60d0a8d
Show file tree
Hide file tree
Showing 39 changed files with 1,649 additions and 1,161 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add translations to countries in shop query - #4732 by @fowczarek
- Added validations for minimum password length in settings - #4735 by @fowczarek
- Add error codes to mutations responses - #4676 by @Kwaidan00
- Payment gateways are now saleor plugins with dynamic configuration - #4669 by @salwator

## 2.8.0

Expand Down
76 changes: 76 additions & 0 deletions saleor/core/payments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import TYPE_CHECKING, List

if TYPE_CHECKING:
from saleor.payment.interface import PaymentData, GatewayResponse, TokenConfig


class Gateway(Enum):
"""Possible gateway values.
TODO: Create this in runtime based on available plugins
"""

DUMMY = "dummy"
BRAINTREE = "braintree"
RAZORPAY = "razorpay"
STRIPE = "stripe"


class PaymentInterface(ABC):
@abstractmethod
def list_payment_gateways(self) -> List[Gateway]:
pass

@abstractmethod
def authorize_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
pass

@abstractmethod
def capture_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
pass

@abstractmethod
def refund_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
pass

@abstractmethod
def void_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
pass

@abstractmethod
def confirm_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
pass

@abstractmethod
def process_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
pass

@abstractmethod
def create_payment_form(
self, data, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
pass

@abstractmethod
def get_client_token(self, gateway: Gateway, token_config: "TokenConfig") -> str:
pass

@abstractmethod
def list_payment_sources(
self, gateway: Gateway, customer_id: str
) -> List["CustomerSource"]:
pass
18 changes: 6 additions & 12 deletions saleor/core/utils/random_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,8 @@
from ...order.models import Fulfillment, Order, OrderLine
from ...order.utils import update_order_status
from ...page.models import Page
from ...payment.utils import (
create_payment,
gateway_authorize,
gateway_capture,
gateway_refund,
gateway_void,
)
from ...payment import gateway
from ...payment.utils import create_payment
from ...product.models import (
AssignedProductAttribute,
AssignedVariantAttribute,
Expand All @@ -62,7 +57,6 @@
from ...shipping.models import ShippingMethod, ShippingMethodType, ShippingZone

fake = Factory.create()

PRODUCTS_LIST_DIR = "products-list/"

IMAGES_MAPPING = {
Expand Down Expand Up @@ -390,19 +384,19 @@ def create_fake_payment(mock_email_confirmation, order):
)

# Create authorization transaction
gateway_authorize(payment, payment.token)
gateway.authorize(payment, payment.token)
# 20% chance to void the transaction at this stage
if random.choice([0, 0, 0, 0, 1]):
gateway_void(payment)
gateway.void(payment)
return payment
# 25% to end the payment at the authorization stage
if not random.choice([1, 1, 1, 0]):
return payment
# Create capture transaction
gateway_capture(payment)
gateway.capture(payment)
# 25% to refund the payment
if random.choice([0, 0, 0, 1]):
gateway_refund(payment)
gateway.refund(payment)
return payment


Expand Down
16 changes: 5 additions & 11 deletions saleor/dashboard/order/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,8 @@
fulfill_order_line,
recalculate_order,
)
from ...payment import ChargeStatus, CustomPaymentChoices, PaymentError
from ...payment.utils import (
clean_mark_order_as_paid,
gateway_capture,
gateway_refund,
gateway_void,
mark_order_as_paid,
)
from ...payment import ChargeStatus, CustomPaymentChoices, PaymentError, gateway
from ...payment.utils import clean_mark_order_as_paid, mark_order_as_paid
from ...product.models import Product, ProductVariant
from ...product.utils import allocate_stock, deallocate_stock
from ...shipping.models import ShippingMethod
Expand Down Expand Up @@ -343,7 +337,7 @@ def clean(self):

def capture(self, user):
return self.try_payment_action(
user, gateway_capture, self.cleaned_data["amount"]
user, gateway.capture, self.cleaned_data["amount"]
)


Expand All @@ -366,7 +360,7 @@ def clean(self):

def refund(self, user):
return self.try_payment_action(
user, gateway_refund, self.cleaned_data["amount"]
user, gateway.refund, self.cleaned_data["amount"]
)


Expand All @@ -388,7 +382,7 @@ def clean(self):
raise forms.ValidationError(self.clean_error)

def void(self, user):
return self.try_payment_action(user, gateway_void)
return self.try_payment_action(user, gateway.void)


class OrderMarkAsPaidForm(forms.Form):
Expand Down
37 changes: 37 additions & 0 deletions saleor/extensions/base_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ..product.models import Product
from ..account.models import Address
from ..order.models import OrderLine, Order
from ..payment.interface import GatewayResponse, PaymentData


class BasePlugin:
Expand Down Expand Up @@ -136,6 +137,42 @@ def get_tax_rate_percentage_value(
) -> Decimal:
return NotImplemented

def authorize_payment(
self, payment_information: "PaymentData", previous_value
) -> "GatewayResponse":
return NotImplemented

def capture_payment(
self, payment_information: "PaymentData", previous_value
) -> "GatewayResponse":
return NotImplemented

def refund_payment(
self, payment_information: "PaymentData", previous_value
) -> "GatewayResponse":
return NotImplemented

def confirm_payment(
self, payment_information: "PaymentData", previous_value
) -> "GatewayResponse":
return NotImplemented

def process_payment(
self, payment_information: "PaymentData", previous_value
) -> "GatewayResponse":
return NotImplemented

def list_payment_sources(
self, customer_id: str, previous_value
) -> List["CustomerSource"]:
return NotImplemented

def create_form(self, data, payment_information, previous_value):
return NotImplemented

def get_client_token(self, token_config, previous_value):
return NotImplemented

@classmethod
def _update_config_items(
cls, configuration_to_update: List[dict], current_config: List[dict]
Expand Down
116 changes: 111 additions & 5 deletions saleor/extensions/manager.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from decimal import Decimal
from typing import TYPE_CHECKING, Any, List, Union
from typing import TYPE_CHECKING, Any, List, Optional, Union

from django.conf import settings
from django.utils.module_loading import import_string
from django_countries.fields import Country
from prices import Money, MoneyRange, TaxedMoney, TaxedMoneyRange

from ..core.payments import Gateway, PaymentInterface
from ..core.taxes import TaxType, quantize_price
from .models import PluginConfiguration

Expand All @@ -15,9 +16,10 @@
from ..product.models import Product
from ..account.models import Address
from ..order.models import OrderLine, Order
from ..payment.interface import PaymentData, GatewayResponse, TokenConfig


class ExtensionsManager:
class ExtensionsManager(PaymentInterface):
"""Base manager for handling plugins logic."""

plugins = None
Expand Down Expand Up @@ -178,7 +180,106 @@ def postprocess_order_creation(self, order: "Order"):
"postprocess_order_creation", default_value, order
)

def authorize_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
method_name = "authorize_payment"
return self.__run_payment_method(gateway, method_name, payment_information)

def capture_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
method_name = "capture_payment"
return self.__run_payment_method(gateway, method_name, payment_information)

def refund_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
method_name = "refund_payment"
return self.__run_payment_method(gateway, method_name, payment_information)

def void_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
method_name = "void_payment"
return self.__run_payment_method(gateway, method_name, payment_information)

def confirm_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
method_name = "confirm_payment"
return self.__run_payment_method(gateway, method_name, payment_information)

def process_payment(
self, gateway: Gateway, payment_information: "PaymentData"
) -> "GatewayResponse":
method_name = "process_payment"
return self.__run_payment_method(gateway, method_name, payment_information)

def create_payment_form(self, data, gateway, payment_information):
method_name = "create_form"
return self.__run_payment_method(
gateway, method_name, payment_information, data=data
)

def get_client_token(self, gateway, token_config: "TokenConfig") -> str:
method_name = "get_client_token"
default_value = None
gateway_name = gateway.value
gtw = self.get_plugin(gateway_name)
return self.__run_method_on_single_plugin(
gtw, method_name, default_value, token_config=token_config
)

def list_payment_sources(
self, gateway: Gateway, customer_id: str
) -> List["CustomerSource"]:
default_value = []
gateway_name = gateway.value
gtw = self.get_plugin(gateway_name)
if gtw is not None:
return self.__run_method_on_single_plugin(
gtw, "list_payment_sources", default_value, customer_id=customer_id
)
raise Exception(f"Payment plugin {gateway_name} is inaccessible!")

def list_payment_gateways(self) -> List[Gateway]:
payment_method = "process_payment"
return [
Gateway(plugin.PLUGIN_NAME)
for plugin in self.plugins
if payment_method in type(plugin).__dict__
and self.get_plugin_configuration(plugin.PLUGIN_NAME).active
]

def __run_payment_method(
self,
gateway: Gateway,
method_name: str,
payment_information: "PaymentData",
**kwargs,
) -> Optional["GatewayResposne"]:
default_value = None
gateway_name = gateway.value
gtw = self.get_plugin(gateway_name)
if gtw is not None:
resp = self.__run_method_on_single_plugin(
gtw,
method_name,
previous_value=default_value,
payment_information=payment_information,
**kwargs,
)
if resp is not None:
return resp

raise Exception(
f"Payment plugin {gateway_name} for {method_name}"
" payment method is inaccessible!"
)

# FIXME these methods should be more generic

def assign_tax_code_to_object_meta(
self, obj: Union["Product", "ProductType"], tax_code: str
):
Expand Down Expand Up @@ -211,11 +312,16 @@ def save_plugin_configuration(self, plugin_name, cleaned_data: dict):
plugin_configuration, cleaned_data
)

def get_plugin_configuration(self, plugin_name) -> "PluginConfiguration":
plugin_configurations_qs = PluginConfiguration.objects.all()
def get_plugin(self, plugin_name: str) -> Optional["BasePlugin"]:
for plugin in self.plugins:
if plugin.PLUGIN_NAME == plugin_name:
return plugin.get_plugin_configuration(plugin_configurations_qs)
return plugin

def get_plugin_configuration(self, plugin_name) -> Optional["PluginConfiguration"]:
plugin = self.get_plugin(plugin_name)
if plugin is not None:
plugin_configurations_qs = PluginConfiguration.objects.all()
return plugin.get_plugin_configuration(plugin_configurations_qs)

def get_plugin_configurations(self) -> List["PluginConfiguration"]:
plugin_configuration_ids = []
Expand Down

0 comments on commit 60d0a8d

Please sign in to comment.