├── storefront └── src │ ├── @next │ ├── components │ │ └── organisms │ │ │ ├── SberbankPaymentGateway │ │ │ ├── index.ts │ │ │ ├── styles.ts │ │ │ ├── stories.tsx │ │ │ └── SberbankPaymentGateway.tsx │ │ │ ├── index.ts │ │ │ └── PaymentGatewaysList │ │ │ └── PaymentGatewaysList.tsx │ └── pages │ │ └── CheckoutPage │ │ ├── subpages │ │ └── CheckoutReviewSubpage.tsx │ │ └── CheckoutPage.tsx │ └── core │ └── config.ts ├── saleor └── payment │ └── gateways │ └── sberbank │ ├── client │ ├── resources │ │ ├── __init__.py │ │ ├── base.py │ │ └── payment.py │ ├── constants │ │ ├── http_status_code.py │ │ ├── error_code.py │ │ ├── __init__.py │ │ └── url.py │ ├── __init__.py │ ├── errors.py │ └── client.py │ ├── forms.py │ ├── errors.py │ ├── tasks.py │ ├── utils.py │ ├── plugin.py │ ├── __init__.py │ └── webhooks.py ├── templates └── order │ └── payment │ └── sberbank.html ├── LICENSE.md └── README.md /storefront/src/@next/components/organisms/SberbankPaymentGateway/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SberbankPaymentGateway"; 2 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/client/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from .payment import Payment 2 | 3 | __all__ = [ 4 | 'Payment', 5 | ] 6 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/client/constants/http_status_code.py: -------------------------------------------------------------------------------- 1 | class HTTP_STATUS_CODE(object): 2 | OK = 200 3 | REDIRECT = 300 4 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/client/constants/error_code.py: -------------------------------------------------------------------------------- 1 | class ERROR_CODE(object): 2 | BAD_REQUEST_ERROR = "BAD_REQUEST_ERROR" 3 | GATEWAY_ERROR = "GATEWAY_ERROR" 4 | SERVER_ERROR = "SERVER_ERROR" 5 | -------------------------------------------------------------------------------- /storefront/src/@next/components/organisms/SberbankPaymentGateway/styles.ts: -------------------------------------------------------------------------------- 1 | import { styled } from "@styles"; 2 | 3 | export const Form = styled.form``; 4 | 5 | export const Status = styled.div` 6 | display: block; 7 | margin: 18px; 8 | `; 9 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/client/constants/__init__.py: -------------------------------------------------------------------------------- 1 | from .http_status_code import HTTP_STATUS_CODE 2 | from .error_code import ERROR_CODE 3 | from .url import URL 4 | 5 | __all__ = [ 6 | 'HTTP_STATUS_CODE', 7 | 'ERROR_CODE', 8 | 'URL', 9 | ] 10 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/client/constants/url.py: -------------------------------------------------------------------------------- 1 | class URL(object): 2 | BASE_URL = 'https://securepayments.sberbank.ru/payment/rest' 3 | SANDBOX_URL = 'https://3dsec.sberbank.ru/payment/rest' 4 | REGISTER_URL = '/register.do' 5 | STATUS_URL = '/getOrderStatusExtended.do' 6 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/client/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .constants import ERROR_CODE 3 | from .constants import HTTP_STATUS_CODE 4 | from . import errors 5 | from . import resources 6 | 7 | __all__ = [ 8 | 'Client', 9 | 'HTTP_STATUS_CODE', 10 | 'ERROR_CODE', 11 | ] 12 | -------------------------------------------------------------------------------- /templates/order/payment/sberbank.html: -------------------------------------------------------------------------------- 1 | {% extends "order/payment/details.html" %} 2 | {% load i18n %} 3 | 4 | {% block forms %} 5 | 6 | {{ form }} 7 | 8 | 9 | 12 | 13 | 14 | {% endblock forms %} 15 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class SberbankPaymentForm(forms.Form): 5 | sberbank_payment_id = forms.CharField(required=True, widget=forms.HiddenInput) 6 | 7 | def __init__(self, payment_information, connection_params, *args, **kwargs): 8 | super().__init__(*args, **kwargs) 9 | 10 | def get_payment_token(self): 11 | return self.cleaned_data["sberbank_payment_id"] 12 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/client/errors.py: -------------------------------------------------------------------------------- 1 | class BadRequestError(Exception): 2 | def __init__(self, message=None, *args, **kwargs): 3 | super(BadRequestError, self).__init__(message) 4 | 5 | 6 | class GatewayError(Exception): 7 | def __init__(self, message=None, *args, **kwargs): 8 | super(GatewayError, self).__init__(message) 9 | 10 | 11 | class ServerError(Exception): 12 | def __init__(self, message=None, *args, **kwargs): 13 | super(ServerError, self).__init__(message) 14 | 15 | 16 | class SignatureVerificationError(Exception): 17 | def __init__(self, message=None, *args, **kwargs): 18 | super(SignatureVerificationError, self).__init__(message) 19 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/errors.py: -------------------------------------------------------------------------------- 1 | ORDER_ERRORS = [ 2 | 'Заказ с таким номером уже обработан', 3 | 'Заказ с таким номером был зарегистрирован, но не был оплачен', 4 | 'Неверный номер заказа', 5 | ] 6 | 7 | CURRENCY_ERRORS = [ 8 | 'Неизвестная (запрещенная) валюта', 9 | ] 10 | 11 | REQUEST_ERRORS = [ 12 | 'Номер заказа не может быть пуст', 13 | 'Имя мерчанта не может быть пустым', 14 | 'Отсутствует сумма', 15 | 'URL возврата не может быть пуст', 16 | 'Пароль не может быть пуст', 17 | ] 18 | 19 | VALUE_ERRORS = [ 20 | 'Неверное значение одного из параметров', 21 | 'Доступ запрещён', 22 | 'Пользователь должен сменить свой пароль', 23 | '[jsonParams] неверен', 24 | ] 25 | 26 | SYSTEM_ERRORS = [ 27 | 'Системная ошибка', 28 | ] 29 | 30 | ERRORS = { 31 | '1': ORDER_ERRORS, 32 | '2': [], 33 | '3': CURRENCY_ERRORS, 34 | '4': REQUEST_ERRORS, 35 | '5': VALUE_ERRORS, 36 | '6': [], 37 | '7': SYSTEM_ERRORS, 38 | } 39 | -------------------------------------------------------------------------------- /storefront/src/@next/components/organisms/SberbankPaymentGateway/stories.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from "@storybook/react"; 2 | import { action } from "@storybook/addon-actions"; 3 | import React from "react"; 4 | import { IntlProvider } from "react-intl"; 5 | 6 | import { SberbankPaymentGateway } from "."; 7 | 8 | const processPayment = action("processPayment"); 9 | const submitPayment = async () => action("submitPayment"); 10 | const submitPaymentSuccess = action("submitPaymentSuccess"); 11 | const onError = action("onError"); 12 | 13 | storiesOf("@components/organisms/SberbankPaymentGateway", module) 14 | .addParameters({ component: SberbankPaymentGateway }) 15 | .addDecorator(story => {story()}) 16 | .add("default", () => ( 17 | 23 | )); 24 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/tasks.py: -------------------------------------------------------------------------------- 1 | from ....celeryconf import app 2 | from . import client as sberbank 3 | from ...models import Payment, Transaction 4 | from ...utils import TransactionKind 5 | 6 | 7 | @app.task(bind=True, default_retry_delay=60, time_limit=1200) 8 | def check_status_sberbank_task(self, order_id, connection_params): 9 | sberbank_client = sberbank.Client(auth=(connection_params['login'], connection_params['password']), 10 | sandbox=connection_params['sandbox_mode']) 11 | 12 | response = sberbank_client.payment.get_status(order_id=order_id) 13 | 14 | txn = Transaction.objects.get(token=order_id) 15 | if response['actionCode'] == 0: 16 | 17 | txn.is_success = True 18 | txn.kind = TransactionKind.CAPTURE 19 | txn.save() 20 | 21 | payment = Payment.objects.get(pk=txn.payment_id) 22 | payment.charge_status = 'fully-charged' 23 | payment.captured_amount = payment.total 24 | payment.save() 25 | 26 | return 'Success pay on Sberbank for ' + str(order_id) 27 | else: 28 | self.retry(countdown=60) 29 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/client/resources/base.py: -------------------------------------------------------------------------------- 1 | class Resource(object): 2 | 3 | def __init__(self, client=None): 4 | self.client = client 5 | 6 | def all(self, data, **kwargs): 7 | return self.get_url(self.base_url, data, **kwargs) 8 | 9 | def fetch(self, id, data, **kwargs): 10 | url = "{}/{}".format(self.base_url, id) 11 | return self.get_url(url, data, **kwargs) 12 | 13 | def get_url(self, url, data, **kwargs): 14 | return self.client.get(url, data, **kwargs) 15 | 16 | def patch_url(self, url, data, **kwargs): 17 | return self.client.patch(url, data, **kwargs) 18 | 19 | def post_url(self, url, data, **kwargs): 20 | return self.client.post(url, data, **kwargs) 21 | 22 | def put_url(self, url, data, **kwargs): 23 | return self.client.put(url, data, **kwargs) 24 | 25 | def delete_url(self, url, data, **kwargs): 26 | return self.client.delete(url, data, **kwargs) 27 | 28 | def delete(self, id, data, **kwargs): 29 | url = "{}/{}/delete".format(self.base_url, id) 30 | return self.delete_url(url, data, **kwargs) 31 | -------------------------------------------------------------------------------- /storefront/src/@next/components/organisms/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CreditCardForm"; 2 | export * from "./CreditCardGrid"; 3 | export * from "./Overlay"; 4 | export * from "./Modal"; 5 | export * from "./AddressGrid"; 6 | export * from "./AddressForm"; 7 | export * from "./TopNavbar"; 8 | export * from "./SideNavbar"; 9 | export * from "./AddressFormModal"; 10 | export * from "./FilterSidebar"; 11 | export * from "./ProductVariantPicker"; 12 | export * from "./SelectSidebar"; 13 | export * from "./DiscountForm"; 14 | export * from "./ProductGallery"; 15 | export * from "./ProductList"; 16 | export * from "./CartSummary"; 17 | export * from "./CartRow"; 18 | export * from "./AddressGridSelector"; 19 | export * from "./StripeCreditCardForm"; 20 | export * from "./BraintreePaymentGateway"; 21 | export * from "./DummyPaymentGateway"; 22 | export * from "./StripePaymentGateway"; 23 | export * from "./PaymentGatewaysList"; 24 | export * from "./CheckoutAddress"; 25 | export * from "./CheckoutShipping"; 26 | export * from "./CheckoutPayment"; 27 | export * from "./CheckoutReview"; 28 | export * from "./ThankYou"; 29 | export * from "./AdyenPaymentGateway"; 30 | export * from "./AddToCartSection"; 31 | export * from "./SberbankPaymentGateway"; 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 Pavel Korolev 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Плагин оплаты через Сбербанк API для Saleor 2 | 3 | [![Join telegram chat](https://img.shields.io/badge/chat-telegram-blue?style=flat&logo=telegram)](https://t.me/django_ecommerce) 4 | 5 | # Важно 6 | * Плагин для релиза Saleor 2.9 - [смотри ветку 2.9](https://github.com/korolevpavel/saleor-gateaway-sberbank/tree/2.9) 7 | * Плагин для релиза Saleor 2.11 - [смотри ветку 2.11](https://github.com/korolevpavel/saleor-gateaway-sberbank/tree/2.11) 8 | 9 | # Установка 10 | * Склонировать файлы репозитория к себе 11 | * Разместить в каталог с проектом (в корне проекта есть папка `/saleor/`) 12 | * Фронтенд разместить в каталог с проектом (в корне проекта есть папка `/storefront/`) 13 | 14 | # Настройка 15 | * Добавить путь к плагину в `setting.py`: 16 | ```python 17 | PLUGINS = [ 18 | #... 19 | "saleor.payment.gateways.sberbank.plugin.SberbankGatewayPlugin", 20 | ] 21 | ``` 22 | * В Дашборде сделать настройки платежного шлюза (ввести данные от API) 23 | 24 | # Как работает 25 | * Клиент выбирает способ оплаты "Сбербанк" 26 | * Происходит редирект на сайт Сбербанка для оплаты заказа 27 | * В случае успешной оплаты, Сбербанк возвращает на страницу с информацией об успешном заказе 28 | 29 | # Что можно улучшить 30 | * Добавить обработку ошибок, в случае не успешной оплаты 31 | * Провести рефакторинг 32 | * ... 33 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/client/resources/payment.py: -------------------------------------------------------------------------------- 1 | from .base import Resource 2 | from ..constants.url import URL 3 | 4 | 5 | class Payment(Resource): 6 | def __init__(self, client): 7 | super(Payment, self).__init__(client) 8 | self.base_url = client.base_url 9 | 10 | def all(self, data={}, **kwargs): 11 | """" 12 | Fetch all Payment entities 13 | 14 | Returns: 15 | Dictionary of Payment data 16 | """ 17 | return super(Payment, self).all(data, **kwargs) 18 | 19 | def register(self, order_id, amount, return_url, data={}, **kwargs): 20 | """" 21 | Запрос регистрации заказа в Сбербанке 22 | 23 | Args: 24 | order_id : Id for which payment object has to be retrieved 25 | amount : Amount for which the payment has to be retrieved 26 | 27 | Returns: 28 | Payment form URL to redirect the client's browser to. 29 | :param data: 30 | """ 31 | data['amount'] = amount 32 | data['orderNumber'] = "mymilavitsacom-" + str(order_id) 33 | data['returnUrl'] = return_url 34 | 35 | return self.post_url(URL.REGISTER_URL, data, **kwargs) 36 | 37 | def get_status(self, order_id, data={}, **kwargs): 38 | """" 39 | Get payment status in Sberbank 40 | 41 | Args: 42 | order_id : ID of the registered order in Sberbank 43 | 44 | Returns: 45 | Order status in the payment system 46 | """ 47 | 48 | data['orderNumber'] = "mymilavitsacom-" + str(order_id) 49 | return self.post_url(URL.STATUS_URL, data, **kwargs) 50 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/utils.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | # from ..sberbank import SBERBANK_EXCEPTIONS, logger 4 | from . import client as sberbank 5 | from ... import PaymentError 6 | from ...models import Order 7 | from .errors import ERRORS as FAILED_STATUSES 8 | 9 | 10 | def get_error_response(amount: Decimal, **additional_kwargs) -> dict: 11 | """Create a placeholder response for invalid or failed requests. 12 | 13 | It is used to generate a failed transaction object. 14 | """ 15 | return {"is_success": False, "amount": amount, **additional_kwargs} 16 | 17 | 18 | def get_amount_for_sberbank(amount): 19 | """В Сбербанк необходимо передавать значение в копейках или центах 20 | Поэтому необходимо получить значение в копейках 21 | https://developer.sberbank.ru/doc/v1/acquiring/rest-requests1pay 22 | """ 23 | 24 | amount *= 100 25 | 26 | return int(amount.to_integral_value()) 27 | 28 | 29 | def get_order_token(order_id): 30 | return Order.objects.get(pk=order_id).token 31 | 32 | 33 | def get_return_url(order_id): 34 | #return build_absolute_uri(reverse("order:payment-success", kwargs={"token": get_order_token(order_id)})) 35 | return ('http://localhost:3000/checkout/review') 36 | 37 | def get_data_for_payment(payment_information): 38 | data = { 39 | 'language': 'ru', 40 | 'currency': 643, 41 | 'email': payment_information.customer_email, 42 | } 43 | return data 44 | 45 | def api_call(request_data: dict, config): 46 | 47 | sberbank_client = sberbank.Client( 48 | auth=(config.connection_params['login'], config.connection_params['password']), 49 | sandbox=config.connection_params['sandbox_mode']) 50 | 51 | response = sberbank_client.payment.get_status(order_id=request_data.get('payment_id')) 52 | result_code = response.get('errorCode') 53 | is_success = result_code not in FAILED_STATUSES 54 | if is_success: 55 | return response 56 | else: 57 | raise PaymentError( 58 | code=response.get('errorCode'), 59 | message=response.get('errorMessage') 60 | ) -------------------------------------------------------------------------------- /storefront/src/@next/components/organisms/SberbankPaymentGateway/SberbankPaymentGateway.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | 3 | import { IFormError } from "@types"; 4 | import { CompleteCheckout_checkoutComplete_order } from "@saleor/sdk/lib/mutations/gqlTypes/CompleteCheckout"; 5 | import { ErrorMessage } from "@components/atoms"; 6 | 7 | export const sberbankNotNegativeConfirmationStatusCodes = ["Успешно"]; 8 | 9 | interface SberbankSubmitState { 10 | data?: any; 11 | isValid?: boolean; 12 | } 13 | 14 | export interface IPropsSber { 15 | formRef?: React.RefObject; 16 | 17 | processPayment: () => void; 18 | 19 | submitPayment: (data: { 20 | confirmationData: any; 21 | confirmationNeeded: boolean; 22 | }) => Promise; 23 | 24 | submitPaymentSuccess: ( 25 | order?: CompleteCheckout_checkoutComplete_order 26 | ) => void; 27 | 28 | errors?: IFormError[]; 29 | 30 | onError: (errors: IFormError[]) => void; 31 | } 32 | 33 | const SberbankPaymentGateway: React.FC = ({ 34 | formRef, 35 | processPayment, 36 | submitPayment, 37 | submitPaymentSuccess, 38 | errors, 39 | onError, 40 | }: IPropsSber) => { 41 | const gatewayRef = useRef(null); 42 | 43 | const handlePaymentAction = (data?: any) => { 44 | if (data?.url) { 45 | window.location.href = data?.url; 46 | } else { 47 | onError([new Error("Invalid payment url. please try again")]); 48 | } 49 | }; 50 | 51 | const onSubmitSberbankForm = async (state?: SberbankSubmitState) => { 52 | const payment = await submitPayment(state?.data); 53 | if (payment.errors?.length) { 54 | onError(payment.errors); 55 | } else { 56 | let paymentActionData; 57 | try { 58 | paymentActionData = JSON.parse(payment.confirmationData); 59 | } catch (parseError) { 60 | onError([ 61 | new Error( 62 | "Payment needs confirmation but data required for confirmation received from the server is malformed." 63 | ), 64 | ]); 65 | } 66 | try { 67 | handlePaymentAction(paymentActionData); 68 | } catch (error) { 69 | onError([new Error(error)]); 70 | } 71 | } 72 | }; 73 | 74 | useEffect(() => { 75 | (formRef?.current as any)?.addEventListener("submitComplete", () => { 76 | onSubmitSberbankForm(); 77 | }); 78 | }, [formRef]); 79 | 80 | const handleSubmit = (event: React.FormEvent) => { 81 | event.preventDefault(); 82 | processPayment(); 83 | }; 84 | 85 | return ( 86 |
87 |
88 | 89 | 90 | ); 91 | }; 92 | 93 | export { SberbankPaymentGateway }; 94 | -------------------------------------------------------------------------------- /storefront/src/core/config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | import { generatePageUrl } from "./utils"; 4 | 5 | export const BASE_URL = "/"; 6 | export const PRODUCTS_PER_PAGE = 18; 7 | export const SUPPORT_EMAIL = "support@example.com"; 8 | export const PROVIDERS = { 9 | BRAINTREE: { 10 | label: "Braintree", 11 | }, 12 | DUMMY: { 13 | label: "Dummy", 14 | }, 15 | STRIPE: { 16 | label: "Stripe", 17 | }, 18 | ADYEN: { 19 | label: "Adyen", 20 | script: { 21 | src: 22 | "https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/3.10.1/adyen.js", 23 | integrity: 24 | "sha384-wG2z9zSQo61EIvyXmiFCo+zB3y0ZB4hsrXVcANmpP8HLthjoQJQPBh7tZKJSV8jA", 25 | crossOrigin: "anonymous", 26 | }, 27 | style: { 28 | src: 29 | "https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/3.10.1/adyen.css", 30 | integrity: 31 | "sha384-8ofgICZZ/k5cC5N7xegqFZOA73H9RQ7H13439JfAZW8Gj3qjuKL2isaTD3GMIhDE", 32 | crossOrigin: "anonymous", 33 | }, 34 | }, 35 | SBERBANK: { 36 | label: "Sberbank", 37 | }, 38 | }; 39 | export const STATIC_PAGES = [ 40 | { 41 | label: "About", 42 | url: generatePageUrl("about"), 43 | }, 44 | ]; 45 | export const SOCIAL_MEDIA = [ 46 | { 47 | ariaLabel: "facebook", 48 | href: "https://www.facebook.com/mirumeelabs/", 49 | path: require("../images/facebook-icon.svg"), 50 | }, 51 | { 52 | ariaLabel: "instagram", 53 | href: "https://www.instagram.com/mirumeelabs/", 54 | path: require("../images/instagram-icon.svg"), 55 | }, 56 | { 57 | ariaLabel: "twitter", 58 | href: "https://twitter.com/getsaleor", 59 | path: require("../images/twitter-icon.svg"), 60 | }, 61 | { 62 | ariaLabel: "youtube", 63 | href: "https://www.youtube.com/channel/UCg_ptb-U75e7BprLCGS4s1g/videos", 64 | path: require("../images/youtube-icon.svg"), 65 | }, 66 | ]; 67 | export const META_DEFAULTS = { 68 | custom: [], 69 | description: 70 | "Open-source PWA storefront built with Saleor's e-commerce GraphQL API. Written with React and TypeScript.", 71 | image: `${window.location.origin}${require("../images/logo.svg")}`, 72 | title: "Интернет-магазин Милавица", 73 | type: "website", 74 | url: window.location.origin, 75 | }; 76 | export enum CheckoutStep { 77 | Address = 1, 78 | Shipping, 79 | Payment, 80 | Review, 81 | PaymentConfirm, 82 | } 83 | export const CHECKOUT_STEPS = [ 84 | { 85 | index: 0, 86 | link: "/checkout/address", 87 | name: "Address", 88 | nextActionName: "Continue to Shipping", 89 | onlyIfShippingRequired: false, 90 | step: CheckoutStep.Address, 91 | }, 92 | { 93 | index: 1, 94 | link: "/checkout/shipping", 95 | name: "Shipping", 96 | nextActionName: "Continue to Payment", 97 | onlyIfShippingRequired: true, 98 | step: CheckoutStep.Shipping, 99 | }, 100 | { 101 | index: 2, 102 | link: "/checkout/payment", 103 | name: "Payment", 104 | nextActionName: "Continue to Review", 105 | onlyIfShippingRequired: false, 106 | step: CheckoutStep.Payment, 107 | }, 108 | { 109 | index: 3, 110 | link: "/checkout/review", 111 | name: "Review", 112 | nextActionName: "Place order", 113 | onlyIfShippingRequired: false, 114 | step: CheckoutStep.Review, 115 | }, 116 | { 117 | index: 4, 118 | link: "/checkout/payment-confirm", 119 | name: "Payment confirm", 120 | onlyIfShippingRequired: false, 121 | step: CheckoutStep.PaymentConfirm, 122 | withoutOwnView: true, 123 | }, 124 | ]; 125 | -------------------------------------------------------------------------------- /storefront/src/@next/pages/CheckoutPage/subpages/CheckoutReviewSubpage.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | forwardRef, 3 | RefForwardingComponent, 4 | useImperativeHandle, 5 | useState, 6 | } from "react"; 7 | import { RouteComponentProps } from "react-router"; 8 | 9 | import { CheckoutReview } from "@components/organisms"; 10 | import { statuses as dummyStatuses } from "@components/organisms/DummyPaymentGateway"; 11 | import { useCheckout } from "@saleor/sdk"; 12 | import { IFormError } from "@types"; 13 | 14 | export interface ISubmitCheckoutData { 15 | id: string; 16 | orderNumber: string; 17 | token: string; 18 | } 19 | 20 | export interface ICheckoutReviewSubpageHandles { 21 | complete: () => void; 22 | } 23 | 24 | interface IProps extends RouteComponentProps { 25 | selectedPaymentGatewayToken?: string; 26 | paymentGatewayFormRef: React.RefObject; 27 | changeSubmitProgress: (submitInProgress: boolean) => void; 28 | onSubmitSuccess: (data: ISubmitCheckoutData) => void; 29 | } 30 | 31 | const CheckoutReviewSubpageWithRef: RefForwardingComponent< 32 | ICheckoutReviewSubpageHandles, 33 | IProps 34 | > = ( 35 | { 36 | selectedPaymentGatewayToken, 37 | paymentGatewayFormRef, 38 | changeSubmitProgress, 39 | onSubmitSuccess, 40 | ...props 41 | }: IProps, 42 | ref 43 | ) => { 44 | const { checkout, payment, completeCheckout } = useCheckout(); 45 | 46 | const [errors, setErrors] = useState([]); 47 | 48 | const checkoutShippingAddress = checkout?.shippingAddress 49 | ? { 50 | ...checkout?.shippingAddress, 51 | phone: checkout?.shippingAddress?.phone || undefined, 52 | } 53 | : undefined; 54 | 55 | const checkoutBillingAddress = checkout?.billingAddress 56 | ? { 57 | ...checkout?.billingAddress, 58 | phone: checkout?.billingAddress?.phone || undefined, 59 | } 60 | : undefined; 61 | 62 | const getPaymentMethodDescription = () => { 63 | if (payment?.gateway === "mirumee.payments.dummy") { 64 | return `Dummy: ${ 65 | dummyStatuses.find( 66 | status => status.token === selectedPaymentGatewayToken 67 | )?.label 68 | }`; 69 | } 70 | if (payment?.gateway === "mirumee.payments.adyen") { 71 | return `Adyen payments`; 72 | } 73 | if (payment?.creditCard) { 74 | return `Ending in ${payment?.creditCard.lastDigits}`; 75 | } 76 | if (payment?.gateway === "korolev.payments.sberbank") { 77 | return `Заказ будет оплачен на сайте Сбербанка`; 78 | } 79 | return ``; 80 | }; 81 | 82 | useImperativeHandle(ref, () => ({ 83 | complete: async () => { 84 | changeSubmitProgress(true); 85 | let data; 86 | let dataError; 87 | if ( 88 | payment?.gateway === "mirumee.payments.adyen" || 89 | payment?.gateway === "korolev.payments.sberbank" 90 | ) { 91 | paymentGatewayFormRef.current?.dispatchEvent( 92 | new Event("submitComplete", { cancelable: true }) 93 | ); 94 | } else { 95 | const response = await completeCheckout(); 96 | data = response.data; 97 | dataError = response.dataError; 98 | changeSubmitProgress(false); 99 | const errors = dataError?.error; 100 | if (errors) { 101 | setErrors(errors); 102 | } else { 103 | setErrors([]); 104 | onSubmitSuccess({ 105 | id: data?.order?.id, 106 | orderNumber: data?.order?.number, 107 | token: data?.order?.token, 108 | }); 109 | } 110 | } 111 | }, 112 | })); 113 | 114 | return ( 115 | 124 | ); 125 | }; 126 | 127 | const CheckoutReviewSubpage = forwardRef(CheckoutReviewSubpageWithRef); 128 | 129 | export { CheckoutReviewSubpage }; 130 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/plugin.py: -------------------------------------------------------------------------------- 1 | from saleor.plugins.base_plugin import BasePlugin, ConfigurationTypeField 2 | from django.utils.translation import pgettext_lazy 3 | 4 | from ..utils import get_supported_currencies 5 | from . import (GatewayConfig, 6 | confirm_payment, 7 | process_payment, 8 | ) 9 | 10 | from django.core.handlers.wsgi import WSGIRequest 11 | from django.http import HttpResponse, HttpResponseNotFound 12 | 13 | from .webhooks import handle_additional_actions 14 | 15 | GATEWAY_NAME = "Sberbank" 16 | ADDITIONAL_ACTION_PATH = "/additional-actions" 17 | 18 | 19 | def require_active_plugin(fn): 20 | def wrapped(self, *args, **kwargs): 21 | previous = kwargs.get("previous_value", None) 22 | if not self.active: 23 | return previous 24 | return fn(self, *args, **kwargs) 25 | 26 | return wrapped 27 | 28 | 29 | class SberbankGatewayPlugin(BasePlugin): 30 | PLUGIN_NAME = GATEWAY_NAME 31 | PLUGIN_ID = "korolev.payments.sberbank" 32 | 33 | DEFAULT_CONFIGURATION = [ 34 | {"name": "Login", "value": None}, 35 | {"name": "Password", "value": None}, 36 | {"name": "Use sandbox", "value": True}, 37 | {"name": "Automatic payment capture", "value": False}, 38 | {"name": "Supported currencies", "value": "RUB"}, 39 | ] 40 | 41 | CONFIG_STRUCTURE = { 42 | "Template path": { 43 | "type": ConfigurationTypeField.STRING, 44 | "help_text": pgettext_lazy( 45 | "Plugin help text", "Location of django payment template for gateway." 46 | ), 47 | "label": pgettext_lazy("Plugin label", "Template path"), 48 | }, 49 | "Login": { 50 | "type": ConfigurationTypeField.STRING, 51 | "help_text": "Provide your login name Sberbank API", 52 | "label": "Username for API", 53 | }, 54 | "Password": { 55 | "type": ConfigurationTypeField.STRING, 56 | "help_text": "Provide your password", 57 | "label": "Password for API", 58 | }, 59 | "Use sandbox": { 60 | "type": ConfigurationTypeField.BOOLEAN, 61 | "help_text": pgettext_lazy( 62 | "Plugin help text", 63 | "Determines if Saleor should use Sberbank sandbox API.", 64 | ), 65 | "label": pgettext_lazy("Plugin label", "Use sandbox"), 66 | }, 67 | "Automatic payment capture": { 68 | "type": ConfigurationTypeField.BOOLEAN, 69 | "help_text": pgettext_lazy( 70 | "Plugin help text", 71 | "Determines if Saleor should automaticaly capture payments.", 72 | ), 73 | "label": pgettext_lazy("Plugin label", "Automatic payment capture"), 74 | }, 75 | "Supported currencies": { 76 | "type": ConfigurationTypeField.STRING, 77 | "help_text": "Determines currencies supported by gateway." 78 | " Please enter currency codes separated by a comma.", 79 | "label": "Supported currencies", 80 | }, 81 | } 82 | 83 | def __init__(self, *args, **kwargs): 84 | super().__init__(*args, **kwargs) 85 | configuration = {item["name"]: item["value"] for item in self.configuration} 86 | self.config = GatewayConfig( 87 | gateway_name=GATEWAY_NAME, 88 | auto_capture=configuration["Automatic payment capture"], 89 | supported_currencies=configuration["Supported currencies"], 90 | connection_params={ 91 | "sandbox_mode": configuration["Use sandbox"], 92 | "login": configuration["Login"], 93 | "password": configuration["Password"] 94 | }, 95 | ) 96 | 97 | def _get_gateway_config(self) -> GatewayConfig: 98 | return self.config 99 | 100 | @require_active_plugin 101 | def get_supported_currencies(self, previous_value): 102 | config = self._get_gateway_config() 103 | return get_supported_currencies(config, GATEWAY_NAME) 104 | 105 | @require_active_plugin 106 | def process_payment( 107 | self, payment_information: "PaymentData", previous_value 108 | ) -> "GatewayResponse": 109 | return process_payment(self, payment_information, self._get_gateway_config()) 110 | 111 | @require_active_plugin 112 | def confirm_payment( 113 | self, payment_information: "PaymentData", previous_value 114 | ) -> "GatewayResponse": 115 | return confirm_payment(self, payment_information, previous_value) 116 | 117 | @require_active_plugin 118 | def token_is_required_as_payment_input(self, previous_value): 119 | return False 120 | 121 | def webhook(self, request: WSGIRequest, path: str, previous_value) -> HttpResponse: 122 | config = self._get_gateway_config() 123 | if path.startswith(ADDITIONAL_ACTION_PATH): 124 | return handle_additional_actions( 125 | request, config 126 | ) 127 | return HttpResponseNotFound() 128 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/client/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from .constants import HTTP_STATUS_CODE, ERROR_CODE, URL 4 | 5 | from .errors import (BadRequestError, 6 | GatewayError, 7 | ServerError) 8 | 9 | from . import resources 10 | from types import ModuleType 11 | 12 | 13 | def capitalize_camel_case(string): 14 | return "".join(map(str.capitalize, string.split('_'))) 15 | 16 | 17 | # Create a dict of resource classes 18 | RESOURCE_CLASSES = {} 19 | for name, module in resources.__dict__.items(): 20 | if isinstance(module, ModuleType) and \ 21 | capitalize_camel_case(name) in module.__dict__: 22 | RESOURCE_CLASSES[name] = module.__dict__[capitalize_camel_case(name)] 23 | 24 | 25 | class Client: 26 | """Sberbank client class""" 27 | 28 | DEFAULTS = { 29 | 'base_url': URL.BASE_URL, 30 | 'sandbox_url': URL.SANDBOX_URL, 31 | 'status_url': URL.STATUS_URL, 32 | 'register_url': URL.REGISTER_URL, 33 | } 34 | 35 | def __init__(self, session=None, auth=None, sandbox=True, **options): 36 | """ 37 | Initialize a Client object with session, 38 | optional auth handler, and options 39 | """ 40 | self.session = session or requests.Session() 41 | self.auth = auth 42 | 43 | if sandbox: 44 | self.base_url = self._set_sandbox_url(**options) 45 | else: 46 | self.base_url = self._set_base_url(**options) 47 | 48 | # intializes each resource 49 | # injecting this client object into the constructor 50 | for name, Klass in RESOURCE_CLASSES.items(): 51 | setattr(self, name, Klass(self)) 52 | 53 | def _set_base_url(self, **options): 54 | base_url = self.DEFAULTS['base_url'] 55 | 56 | if 'base_url' in options: 57 | base_url = options['base_url'] 58 | del (options['base_url']) 59 | 60 | return base_url 61 | 62 | def _set_sandbox_url(self, **options): 63 | sandbox_url = self.DEFAULTS['sandbox_url'] 64 | 65 | if 'sandbox_url' in options: 66 | sandbox_url = options['sandbox_url'] 67 | del (options['sandbox_url']) 68 | 69 | return sandbox_url 70 | 71 | def request(self, method, path, **options): 72 | """ 73 | Dispatches a request to the Sberbank HTTP API 74 | """ 75 | 76 | url = "{}{}".format(self.base_url, path) 77 | 78 | response = getattr(self.session, method)(url, auth=self.auth, 79 | **options) 80 | if ((response.status_code >= HTTP_STATUS_CODE.OK) and 81 | (response.status_code < HTTP_STATUS_CODE.REDIRECT)): 82 | return response.json() 83 | else: 84 | msg = "" 85 | code = "" 86 | json_response = response.json() 87 | if 'error' in json_response: 88 | if 'description' in json_response['error']: 89 | msg = json_response['error']['description'] 90 | if 'code' in json_response['error']: 91 | code = str(json_response['error']['code']) 92 | 93 | if str.upper(code) == ERROR_CODE.BAD_REQUEST_ERROR: 94 | raise BadRequestError(msg) 95 | elif str.upper(code) == ERROR_CODE.GATEWAY_ERROR: 96 | raise GatewayError(msg) 97 | elif str.upper(code) == ERROR_CODE.SERVER_ERROR: 98 | raise ServerError(msg) 99 | else: 100 | raise ServerError(msg) 101 | 102 | def get(self, path, params, **options): 103 | """ 104 | Parses GET request options and dispatches a request 105 | """ 106 | return self.request('get', path, params=params, **options) 107 | 108 | def post(self, path, data, **options): 109 | """ 110 | Parses POST request options and dispatches a request 111 | """ 112 | data, options = self._update_request(data, options) 113 | return self.request('post', path, data=data, **options) 114 | 115 | def patch(self, path, data, **options): 116 | """ 117 | Parses PATCH request options and dispatches a request 118 | """ 119 | data, options = self._update_request(data, options) 120 | return self.request('patch', path, data=data, **options) 121 | 122 | def delete(self, path, data, **options): 123 | """ 124 | Parses DELETE request options and dispatches a request 125 | """ 126 | data, options = self._update_request(data, options) 127 | return self.request('delete', path, data=data, **options) 128 | 129 | def put(self, path, data, **options): 130 | """ 131 | Parses PUT request options and dispatches a request 132 | """ 133 | data, options = self._update_request(data, options) 134 | return self.request('put', path, data=data, **options) 135 | 136 | def _update_request(self, data, options): 137 | """ 138 | Updates The resource data and header options 139 | """ 140 | 141 | data['userName'] = self.auth[0] 142 | data['password'] = self.auth[1] 143 | data['locale'] = 'ru' 144 | 145 | if 'headers' not in options: 146 | options['headers'] = {} 147 | 148 | options['headers'].update({'Content-type': 'application/x-www-form-urlencoded'}) 149 | options['headers'].update({'Accept': 'application/json'}) 150 | 151 | return data, options 152 | -------------------------------------------------------------------------------- /storefront/src/@next/components/organisms/PaymentGatewaysList/PaymentGatewaysList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { ErrorMessage, Radio } from "@components/atoms"; 4 | import { PROVIDERS } from "@temp/core/config"; 5 | 6 | import { 7 | BraintreePaymentGateway, 8 | DummyPaymentGateway, 9 | StripePaymentGateway, 10 | AdyenPaymentGateway, 11 | SberbankPaymentGateway, 12 | } from ".."; 13 | import * as S from "./styles"; 14 | import { IProps } from "./types"; 15 | 16 | /** 17 | * Payment Gateways list 18 | */ 19 | const PaymentGatewaysList: React.FC = ({ 20 | paymentGateways, 21 | selectedPaymentGateway, 22 | selectedPaymentGatewayToken, 23 | selectPaymentGateway, 24 | formRef, 25 | formId, 26 | processPayment, 27 | submitPayment, 28 | submitPaymentSuccess, 29 | errors, 30 | onError, 31 | }: IProps) => { 32 | return ( 33 | 34 | {paymentGateways.map(({ id, name, config }, index) => { 35 | const checked = selectedPaymentGateway === id; 36 | 37 | switch (name) { 38 | case PROVIDERS.BRAINTREE.label: 39 | return ( 40 |
41 | 42 | 48 | selectPaymentGateway && selectPaymentGateway(id) 49 | } 50 | customLabel 51 | > 52 | 53 | {name} 54 | 55 | 56 | 57 | {checked && ( 58 | 63 | processPayment(id, token, cardData) 64 | } 65 | errors={errors} 66 | onError={onError} 67 | /> 68 | )} 69 |
70 | ); 71 | 72 | case PROVIDERS.DUMMY.label: 73 | return ( 74 |
75 | 76 | 82 | selectPaymentGateway && selectPaymentGateway(id) 83 | } 84 | customLabel 85 | > 86 | 87 | {name} 88 | 89 | 90 | 91 | {checked && ( 92 | processPayment(id, token)} 96 | initialStatus={selectedPaymentGatewayToken} 97 | /> 98 | )} 99 |
100 | ); 101 | 102 | case PROVIDERS.STRIPE.label: 103 | return ( 104 |
105 | 106 | 112 | selectPaymentGateway && selectPaymentGateway(id) 113 | } 114 | customLabel 115 | > 116 | 117 | {name} 118 | 119 | 120 | 121 | {checked && ( 122 | 127 | processPayment(id, token, cardData) 128 | } 129 | errors={errors} 130 | onError={onError} 131 | /> 132 | )} 133 |
134 | ); 135 | 136 | case PROVIDERS.ADYEN.label: 137 | return ( 138 |
139 | 140 | 146 | selectPaymentGateway && selectPaymentGateway(id) 147 | } 148 | customLabel 149 | > 150 | 151 | {name} 152 | 153 | 154 | 155 | {checked && ( 156 | processPayment(id)} 162 | submitPayment={submitPayment} 163 | submitPaymentSuccess={submitPaymentSuccess} 164 | errors={errors} 165 | onError={onError} 166 | /> 167 | )} 168 |
169 | ); 170 | case PROVIDERS.SBERBANK.label: 171 | return ( 172 |
173 | 174 | 180 | selectPaymentGateway && selectPaymentGateway(id) 181 | } 182 | > 183 | 184 | Банковская карта 185 | 186 | 187 | 188 | {checked && ( 189 | processPayment(id)} 192 | submitPayment={submitPayment} 193 | submitPaymentSuccess={submitPaymentSuccess} 194 | errors={errors} 195 | onError={onError} 196 | /> 197 | )} 198 |
199 | ); 200 | 201 | default: 202 | return null; 203 | } 204 | })} 205 | {!selectedPaymentGateway && errors && } 206 |
207 | ); 208 | }; 209 | 210 | export { PaymentGatewaysList }; 211 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from urllib.parse import urlencode 3 | 4 | from django.core.exceptions import ObjectDoesNotExist 5 | 6 | from ....core.utils import build_absolute_uri 7 | from ....core.utils.url import prepare_url 8 | from ... import PaymentError 9 | from ...interface import ( 10 | GatewayConfig, 11 | GatewayResponse, 12 | PaymentData, 13 | ) 14 | 15 | from ...utils import create_transaction, TransactionKind 16 | 17 | from ...models import Payment 18 | 19 | from .forms import SberbankPaymentForm 20 | from . import errors 21 | from .utils import ( 22 | get_amount_for_sberbank, 23 | get_error_response, 24 | get_return_url, 25 | get_data_for_payment) 26 | 27 | from . import client as sberbank 28 | 29 | from .tasks import check_status_sberbank_task 30 | 31 | # The list of currencies supported by razorpay 32 | SUPPORTED_CURRENCIES = ("RUB", "USD") 33 | PENDING_STATUSES = [""] 34 | 35 | # Define what are the Sberbank exceptions, 36 | # as the Sberbank provider doesn't define a base exception as of now. 37 | SBERBANK_EXCEPTIONS = ( 38 | sberbank.errors.BadRequestError, 39 | sberbank.errors.GatewayError, 40 | sberbank.errors.ServerError, 41 | ) 42 | 43 | # Get the logger for this file, it will allow us to log 44 | # error responses from Sberbank. 45 | logger = logging.getLogger(__name__) 46 | 47 | 48 | def get_error_message_from_sberbank_error(exc: BaseException): 49 | """Convert a Razorpay error to a user-friendly error message. 50 | 51 | It also logs the exception to stderr. 52 | """ 53 | logger.exception(exc) 54 | if isinstance(exc, sberbank.errors.BadRequestError): 55 | return errors.INVALID_REQUEST 56 | else: 57 | return errors.SERVER_ERROR 58 | 59 | 60 | def check_payment_supported(payment_information: PaymentData): 61 | """Check that a given payment is supported.""" 62 | if payment_information.currency not in SUPPORTED_CURRENCIES: 63 | return errors.UNSUPPORTED_CURRENCY % {"currency": payment_information.currency} 64 | 65 | 66 | def get_client(connection_params): 67 | """Create a Sberbank client from set-up application keys.""" 68 | sberbank_client = sberbank.Client( 69 | auth=(connection_params['login'], connection_params['password']), 70 | sandbox=connection_params['sandbox_mode']) 71 | return sberbank_client 72 | 73 | 74 | def process_payment(self, payment_information: PaymentData, config: GatewayConfig 75 | ) -> GatewayResponse: 76 | # return authorize(payment_information, config) 77 | 78 | try: 79 | payment = Payment.objects.get(pk=payment_information.payment_id) 80 | except ObjectDoesNotExist: 81 | raise PaymentError("Payment cannot be performed. Payment does not exists.") 82 | 83 | checkout = payment.checkout 84 | if checkout is None: 85 | raise PaymentError( 86 | "Payment cannot be performed. Checkout for this payment does not exist." 87 | ) 88 | 89 | params = urlencode( 90 | {"payment": payment_information.graphql_payment_id, "checkout": checkout.pk} 91 | ) 92 | return_url = prepare_url( 93 | params, 94 | build_absolute_uri( 95 | f"/plugins/{self.PLUGIN_ID}/additional-actions" 96 | ), # type: ignore 97 | ) 98 | 99 | error = check_payment_supported(payment_information=payment_information) 100 | 101 | sberbank_client = get_client(config.connection_params) 102 | 103 | try: 104 | 105 | kind = TransactionKind.AUTH 106 | sberbank_auto_capture = self.config.auto_capture 107 | if sberbank_auto_capture: 108 | kind = TransactionKind.CAPTURE 109 | 110 | response = sberbank_client.payment.register( 111 | order_id=payment_information.payment_id, 112 | amount=get_amount_for_sberbank(payment_information.amount), 113 | return_url=return_url, 114 | data=get_data_for_payment(payment_information)) 115 | # response = {"formUrl": "https://3dsec.sberbank.ru/payment/merchants/sbersafe_id/payment_ru.html?mdOrder=389320f5-d423-714b-bae5-ca325e3d5a10", 116 | # "orderId": "389320f5-d423-714b-bae5-ca325e3d5a10"} 117 | 118 | # orderId есть только у успешно зарегистрированных заказов 119 | if 'orderId' in response: 120 | token = response['orderId'] 121 | payment_information.token = token 122 | 123 | # Запустим проверку оплаты с API-сбербанка 124 | # check_status_sberbank_task.delay(order_id=token, 125 | # connection_params=config.connection_params) 126 | 127 | action = { 128 | 'method': 'GET', 129 | 'type': 'redirect', 130 | 'paymentMethodType': 'sberbank', 131 | 'paymentData': token, 132 | 'url': response['formUrl'] 133 | } 134 | 135 | return GatewayResponse( 136 | is_success=True, 137 | action_required=True, 138 | transaction_id=token, 139 | amount=payment_information.amount, 140 | currency=payment_information.currency, 141 | kind=kind, 142 | error='', 143 | raw_response=response, 144 | action_required_data=action, 145 | customer_id=payment_information.customer_id, 146 | searchable_key=token, 147 | ) 148 | 149 | if 'errorCode' in response: 150 | error_code = int(response['errorCode']) 151 | if error_code in errors.ERRORS: 152 | error_msg = response['errorMessage'] 153 | logger.critical('{}:{}'.format(error_code, error_msg)) 154 | 155 | return GatewayResponse( 156 | is_success=False, 157 | action_required=True, 158 | amount=payment_information.amount, 159 | error=error_msg, 160 | transaction_id='', 161 | currency=payment_information.currency, 162 | kind=kind, 163 | raw_response=response, 164 | customer_id=payment_information.customer_id, 165 | ) 166 | # raise Exception(error_msg) 167 | 168 | except SBERBANK_EXCEPTIONS as exc: 169 | error = get_error_message_from_sberbank_error(exc) 170 | 171 | return GatewayResponse( 172 | is_success=False, 173 | action_required=False, 174 | currency=payment_information.currency, 175 | error=error, 176 | customer_id=payment_information.customer_id, 177 | ) 178 | 179 | 180 | def confirm_payment( 181 | self, payment_information: "PaymentData", previous_value 182 | ) -> "GatewayResponse": 183 | config = self._get_gateway_config() 184 | # The additional checks are proceed asynchronously so we try to confirm that 185 | # the payment is already processed 186 | payment = Payment.objects.filter(id=payment_information.payment_id).first() 187 | if not payment: 188 | raise PaymentError("Unable to find the payment.") 189 | 190 | transaction = ( 191 | payment.transactions.filter( 192 | kind=TransactionKind.ACTION_TO_CONFIRM, 193 | is_success=True, 194 | action_required=False, 195 | ) 196 | .exclude(token__isnull=False, token__exact="") 197 | .last() 198 | ) 199 | 200 | kind = TransactionKind.AUTH 201 | if config.auto_capture: 202 | kind = TransactionKind.CAPTURE 203 | 204 | # if not transaction: 205 | # return self._process_additional_action(payment_information, kind) 206 | 207 | result_code = transaction.gateway_response.get("actionCodeDescription", "").strip().lower() 208 | if result_code and result_code in PENDING_STATUSES: 209 | kind = TransactionKind.PENDING 210 | 211 | transaction_already_processed = payment.transactions.filter( 212 | kind=kind, 213 | is_success=True, 214 | action_required=False, 215 | amount=payment_information.amount, 216 | currency=payment_information.currency, 217 | ).first() 218 | is_success = True 219 | 220 | # confirm that we should proceed the capture action 221 | if ( 222 | not transaction_already_processed 223 | and config.auto_capture 224 | and kind == TransactionKind.CAPTURE 225 | ): 226 | is_success = True 227 | 228 | token = transaction.token 229 | if transaction_already_processed: 230 | token = transaction_already_processed.token 231 | 232 | return GatewayResponse( 233 | is_success=is_success, 234 | action_required=False, 235 | kind=kind, 236 | amount=payment_information.amount, # type: ignore 237 | currency=payment_information.currency, # type: ignore 238 | transaction_id=token, # type: ignore 239 | error=None, 240 | raw_response={}, 241 | transaction_already_processed=bool(transaction_already_processed), 242 | ) 243 | -------------------------------------------------------------------------------- /saleor/payment/gateways/sberbank/webhooks.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import binascii 3 | import hashlib 4 | import hmac 5 | import json 6 | import logging 7 | from typing import Any, Callable, Dict, Optional 8 | from urllib.parse import urlencode 9 | 10 | import Adyen 11 | import graphene 12 | from django.contrib.auth.hashers import check_password 13 | from django.contrib.auth.models import AnonymousUser 14 | from django.core.exceptions import ValidationError 15 | from django.core.handlers.wsgi import WSGIRequest 16 | from django.http import ( 17 | HttpResponse, 18 | HttpResponseBadRequest, 19 | HttpResponseNotFound, 20 | QueryDict, 21 | ) 22 | from django.http.request import HttpHeaders 23 | from django.shortcuts import redirect 24 | from graphql_relay import from_global_id 25 | 26 | from ....checkout.complete_checkout import complete_checkout 27 | from ....checkout.models import Checkout 28 | from ....core.transactions import transaction_with_commit_on_errors 29 | from ....core.utils.url import prepare_url 30 | from ....discount.utils import fetch_active_discounts 31 | from ....order.actions import ( 32 | cancel_order, 33 | order_authorized, 34 | order_captured, 35 | order_refunded, 36 | ) 37 | from ....order.events import external_notification_event 38 | from ....payment.models import Payment, Transaction 39 | from ... import ChargeStatus, PaymentError, TransactionKind 40 | from ...gateway import payment_refund_or_void 41 | from ...interface import GatewayConfig, GatewayResponse 42 | from ...utils import create_payment_information, create_transaction, gateway_postprocess 43 | from .utils import api_call 44 | from .errors import ERRORS as FAILED_STATUSES 45 | 46 | logger = logging.getLogger(__name__) 47 | 48 | 49 | def get_payment( 50 | payment_id: Optional[str], transaction_id: Optional[str] = None 51 | ) -> Optional[Payment]: 52 | transaction_id = transaction_id or "" 53 | if not payment_id: 54 | logger.warning("Missing payment ID. Reference %s", transaction_id) 55 | return None 56 | try: 57 | _type, db_payment_id = from_global_id(payment_id) 58 | except UnicodeDecodeError: 59 | logger.warning( 60 | "Unable to decode the payment ID %s. Reference %s", 61 | payment_id, 62 | transaction_id, 63 | ) 64 | return None 65 | payment = ( 66 | Payment.objects.prefetch_related("order", "checkout") 67 | .select_for_update(of=("self",)) 68 | .filter(id=db_payment_id, is_active=True, gateway="korolev.payments.sberbank") 69 | .first() 70 | ) 71 | if not payment: 72 | logger.warning( 73 | "Payment for %s was not found. Reference %s", payment_id, transaction_id 74 | ) 75 | return payment 76 | 77 | 78 | def get_checkout(payment: Payment) -> Optional[Checkout]: 79 | if not payment.checkout: 80 | return None 81 | # Lock checkout in the same way as in checkoutComplete 82 | return ( 83 | Checkout.objects.select_for_update(of=("self",)) 84 | .prefetch_related("gift_cards", "lines__variant__product", ) 85 | .select_related("shipping_method__shipping_zone") 86 | .filter(pk=payment.checkout.pk) 87 | .first() 88 | ) 89 | 90 | 91 | def get_transaction( 92 | payment: "Payment", transaction_id: Optional[str], kind: str, 93 | ) -> Optional[Transaction]: 94 | transaction = payment.transactions.filter(kind=kind, token=transaction_id).last() 95 | return transaction 96 | 97 | 98 | def create_new_transaction(notification, payment, kind): 99 | transaction_id = notification.get("pspReference") 100 | currency = notification.get("amount", {}).get("currency") 101 | # amount = from_sberbank_price(notification.get("amount", {}).get("value")) 102 | amount = notification.get("amount", {}).get("value") 103 | is_success = True if notification.get("success") == "true" else False 104 | 105 | gateway_response = GatewayResponse( 106 | kind=kind, 107 | action_required=False, 108 | transaction_id=transaction_id, 109 | is_success=is_success, 110 | amount=amount, 111 | currency=currency, 112 | error="", 113 | raw_response={}, 114 | searchable_key=transaction_id, 115 | ) 116 | return create_transaction( 117 | payment, 118 | kind=kind, 119 | payment_information=None, 120 | action_required=False, 121 | gateway_response=gateway_response, 122 | ) 123 | 124 | 125 | def create_payment_notification_for_order( 126 | payment: Payment, success_msg: str, failed_msg: Optional[str], is_success: bool 127 | ): 128 | if not payment.order: 129 | # Order is not assigned 130 | return 131 | msg = success_msg if is_success else failed_msg 132 | 133 | external_notification_event( 134 | order=payment.order, 135 | user=None, 136 | message=msg, 137 | parameters={"service": payment.gateway, "id": payment.token}, 138 | ) 139 | 140 | 141 | def create_order(payment, checkout): 142 | try: 143 | discounts = fetch_active_discounts() 144 | order, _, _ = complete_checkout( 145 | checkout=checkout, 146 | payment_data={}, 147 | store_source=False, 148 | discounts=discounts, 149 | user=checkout.user or AnonymousUser(), 150 | ) 151 | except ValidationError: 152 | payment_refund_or_void(payment) 153 | return None 154 | # Refresh the payment to assign the newly created order 155 | payment.refresh_from_db() 156 | return order 157 | 158 | 159 | def handle_not_created_order(notification, payment, checkout): 160 | """Process the notification in case when payment doesn't have assigned order.""" 161 | 162 | # We don't want to create order for payment that is cancelled or refunded 163 | if payment.charge_status not in { 164 | ChargeStatus.NOT_CHARGED, 165 | ChargeStatus.PENDING, 166 | ChargeStatus.PARTIALLY_CHARGED, 167 | ChargeStatus.FULLY_CHARGED, 168 | }: 169 | return 170 | # If the payment is not Auth/Capture, it means that user didn't return to the 171 | # storefront and we need to finalize the checkout asynchronously. 172 | action_transaction = create_new_transaction( 173 | notification, payment, TransactionKind.ACTION_TO_CONFIRM 174 | ) 175 | 176 | # Only when we confirm that notification is success we will create the order 177 | if action_transaction.is_success and checkout: # type: ignore 178 | order = create_order(payment, checkout) 179 | return order 180 | return None 181 | 182 | 183 | def handle_authorization(notification: Dict[str, Any], gateway_config: GatewayConfig): 184 | # TODO: handle_authorization 185 | pass 186 | 187 | 188 | def handle_cancellation(notification: Dict[str, Any], _gateway_config: GatewayConfig): 189 | # TODO: handle_cancellation 190 | pass 191 | 192 | 193 | def handle_cancel_or_refund( 194 | notification: Dict[str, Any], gateway_config: GatewayConfig 195 | ): 196 | # TODO: handle_cancel_or_refund 197 | pass 198 | 199 | 200 | def handle_capture(notification: Dict[str, Any], _gateway_config: GatewayConfig): 201 | # TODO: handle_capture 202 | pass 203 | 204 | 205 | def handle_failed_capture(notification: Dict[str, Any], _gateway_config: GatewayConfig): 206 | # TODO: handle_failed_capture 207 | pass 208 | 209 | 210 | def handle_pending(notification: Dict[str, Any], gateway_config: GatewayConfig): 211 | # TODO: handle_pending 212 | pass 213 | 214 | 215 | def handle_refund(notification: Dict[str, Any], _gateway_config: GatewayConfig): 216 | # TODO: handle_refund 217 | pass 218 | 219 | 220 | def _get_kind(transaction: Optional[Transaction]) -> str: 221 | if transaction: 222 | return transaction.kind 223 | # To proceed the refund we already need to have the capture status so we will use it 224 | return TransactionKind.CAPTURE 225 | 226 | 227 | def handle_failed_refund(notification: Dict[str, Any], gateway_config: GatewayConfig): 228 | # TODO: handle_failed_refund 229 | pass 230 | 231 | 232 | def handle_reversed_refund( 233 | notification: Dict[str, Any], _gateway_config: GatewayConfig 234 | ): 235 | # TODO: handle_reversed_refund 236 | pass 237 | 238 | 239 | def handle_refund_with_data( 240 | notification: Dict[str, Any], gateway_config: GatewayConfig 241 | ): 242 | handle_refund(notification, gateway_config) 243 | 244 | 245 | def webhook_not_implemented( 246 | notification: Dict[str, Any], gateway_config: GatewayConfig 247 | ): 248 | # TODO: handle_refund 249 | pass 250 | 251 | 252 | EVENT_MAP = { 253 | "AUTHORISATION": handle_authorization, 254 | "AUTHORISATION_ADJUSTMENT": webhook_not_implemented, 255 | "CANCELLATION": handle_cancellation, 256 | "CANCEL_OR_REFUND": handle_cancel_or_refund, 257 | "CAPTURE": handle_capture, 258 | "CAPTURE_FAILED": handle_failed_capture, 259 | "HANDLED_EXTERNALLY": webhook_not_implemented, 260 | "ORDER_OPENED": webhook_not_implemented, 261 | "ORDER_CLOSED": webhook_not_implemented, 262 | "PENDING": handle_pending, 263 | "PROCESS_RETRY": webhook_not_implemented, 264 | "REFUND": handle_refund, 265 | "REFUND_FAILED": handle_failed_refund, 266 | "REFUNDED_REVERSED": handle_reversed_refund, 267 | "REFUND_WITH_DATA": handle_refund_with_data, 268 | "REPORT_AVAILABLE": webhook_not_implemented, 269 | "VOID_PENDING_REFUND": webhook_not_implemented, 270 | } 271 | 272 | 273 | @transaction_with_commit_on_errors() 274 | def handle_additional_actions( 275 | request: WSGIRequest, gateway_config: "GatewayConfig" 276 | ): 277 | payment_id = request.GET.get("payment") 278 | checkout_pk = request.GET.get("checkout") 279 | 280 | if not payment_id or not checkout_pk: 281 | return HttpResponseNotFound() 282 | 283 | payment = get_payment(payment_id, transaction_id=None) 284 | if not payment: 285 | return HttpResponseNotFound( 286 | "Cannot perform payment.There is no active sberbank payment." 287 | ) 288 | if not payment.checkout or str(payment.checkout.token) != checkout_pk: 289 | return HttpResponseNotFound( 290 | "Cannot perform payment.There is no checkout with this payment." 291 | ) 292 | 293 | extra_data = json.loads(payment.extra_data) 294 | data = extra_data[-1] if isinstance(extra_data, list) else extra_data 295 | 296 | return_url = payment.return_url 297 | 298 | if not return_url: 299 | return HttpResponseNotFound( 300 | "Cannot perform payment. Lack of data about returnUrl." 301 | ) 302 | 303 | try: 304 | request_data = prepare_api_request_data(request, data, payment.pk, checkout_pk) 305 | except KeyError as e: 306 | 307 | return HttpResponseBadRequest(e.args[0]) 308 | try: 309 | result = api_call(request_data, gateway_config) 310 | except PaymentError as e: 311 | return HttpResponseBadRequest(str(e)) 312 | 313 | handle_api_response(payment, result) 314 | 315 | redirect_url = prepare_redirect_url(payment_id, checkout_pk, result, return_url) 316 | return redirect(redirect_url) 317 | 318 | 319 | def prepare_api_request_data(request: WSGIRequest, data: dict, payment_pk, checkout_pk): 320 | params = request.GET 321 | request_data: "QueryDict" = QueryDict("") 322 | 323 | if all([param in request.GET for param in params]): 324 | request_data = request.GET 325 | elif all([param in request.POST for param in params]): 326 | request_data = request.POST 327 | 328 | if not request_data: 329 | raise KeyError( 330 | "Cannot perform payment. Lack of required parameters in request." 331 | ) 332 | 333 | api_request_data = { 334 | "data": data, 335 | "payment_id": payment_pk, 336 | "checkout_pk": checkout_pk, 337 | "details": {key: request_data[key] for key in params}, 338 | } 339 | return api_request_data 340 | 341 | 342 | def prepare_redirect_url( 343 | payment_id: str, checkout_pk: str, api_response: Adyen.Adyen, return_url: str 344 | ): 345 | checkout_id = graphene.Node.to_global_id( 346 | "Checkout", checkout_pk # type: ignore 347 | ) 348 | 349 | params = { 350 | "checkout": checkout_id, 351 | "payment": payment_id, 352 | "resultCode": api_response.get("errorMessage"), 353 | } 354 | 355 | # Check if further action is needed. 356 | # if "action" in api_response.message: 357 | # params.update(api_response.message["action"]) 358 | 359 | return prepare_url(urlencode(params), return_url) 360 | 361 | 362 | def handle_api_response( 363 | payment: Payment, response: Adyen.Adyen, 364 | ): 365 | checkout = get_checkout(payment) 366 | payment_data = create_payment_information( 367 | payment=payment, payment_token=payment.token, 368 | ) 369 | 370 | error_message = response.get('errorMessage') 371 | 372 | result_code = response.get('errorCode') 373 | is_success = result_code not in FAILED_STATUSES 374 | 375 | token = '' 376 | list_attributes = response.get('attributes') 377 | if list_attributes: 378 | if len(list_attributes) > 0: 379 | token = list_attributes[0].get('value') 380 | 381 | gateway_response = GatewayResponse( 382 | is_success=is_success, 383 | action_required=False, 384 | kind=TransactionKind.ACTION_TO_CONFIRM, 385 | amount=payment_data.amount, 386 | currency=payment_data.currency, 387 | transaction_id=token, 388 | error=error_message, 389 | raw_response=response, 390 | searchable_key=token, 391 | ) 392 | 393 | create_transaction( 394 | payment=payment, 395 | kind=TransactionKind.ACTION_TO_CONFIRM, 396 | action_required=False, 397 | payment_information=payment_data, 398 | gateway_response=gateway_response, 399 | ) 400 | 401 | if is_success: 402 | create_order(payment, checkout) 403 | -------------------------------------------------------------------------------- /storefront/src/@next/pages/CheckoutPage/CheckoutPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { useIntl } from "react-intl"; 3 | import { Redirect, useLocation, useHistory } from "react-router-dom"; 4 | 5 | import { Button, Loader } from "@components/atoms"; 6 | import { CheckoutProgressBar } from "@components/molecules"; 7 | import { 8 | CartSummary, 9 | PaymentGatewaysList, 10 | translateAdyenConfirmationError, 11 | adyenNotNegativeConfirmationStatusCodes, 12 | sberbankNotNegativeConfirmationStatusCodes, 13 | } from "@components/organisms"; 14 | import { Checkout } from "@components/templates"; 15 | import { useCart, useCheckout } from "@saleor/sdk"; 16 | import { IItems } from "@saleor/sdk/lib/api/Cart/types"; 17 | import { CHECKOUT_STEPS, CheckoutStep } from "@temp/core/config"; 18 | import { checkoutMessages } from "@temp/intl"; 19 | import { ITaxedMoney, ICheckoutStep, ICardData, IFormError } from "@types"; 20 | import { parseQueryString } from "@temp/core/utils"; 21 | import { CompleteCheckout_checkoutComplete_order } from "@saleor/sdk/lib/mutations/gqlTypes/CompleteCheckout"; 22 | 23 | import { CheckoutRouter } from "./CheckoutRouter"; 24 | import { 25 | CheckoutAddressSubpage, 26 | CheckoutPaymentSubpage, 27 | CheckoutReviewSubpage, 28 | CheckoutShippingSubpage, 29 | ICheckoutAddressSubpageHandles, 30 | ICheckoutPaymentSubpageHandles, 31 | ICheckoutReviewSubpageHandles, 32 | ICheckoutShippingSubpageHandles, 33 | } from "./subpages"; 34 | import { IProps } from "./types"; 35 | 36 | const prepareCartSummary = ( 37 | totalPrice?: ITaxedMoney | null, 38 | subtotalPrice?: ITaxedMoney | null, 39 | shippingTaxedPrice?: ITaxedMoney | null, 40 | promoTaxedPrice?: ITaxedMoney | null, 41 | items?: IItems 42 | ) => { 43 | const products = items?.map(({ id, variant, totalPrice, quantity }) => ({ 44 | id: id || "", 45 | name: variant.name || "", 46 | price: { 47 | gross: { 48 | amount: totalPrice?.gross.amount || 0, 49 | currency: totalPrice?.gross.currency || "", 50 | }, 51 | net: { 52 | amount: totalPrice?.net.amount || 0, 53 | currency: totalPrice?.net.currency || "", 54 | }, 55 | }, 56 | quantity, 57 | sku: variant.sku || "", 58 | thumbnail: { 59 | alt: variant.product?.thumbnail?.alt || undefined, 60 | url: variant.product?.thumbnail?.url, 61 | url2x: variant.product?.thumbnail2x?.url, 62 | }, 63 | })); 64 | 65 | return ( 66 | 73 | ); 74 | }; 75 | 76 | const getCheckoutProgress = ( 77 | loaded: boolean, 78 | activeStepIndex: number, 79 | steps: ICheckoutStep[] 80 | ) => { 81 | return loaded ? ( 82 | 83 | ) : null; 84 | }; 85 | 86 | const getButton = (text?: string, onClick?: () => void) => { 87 | if (text) { 88 | return ( 89 | 96 | ); 97 | } 98 | return null; 99 | }; 100 | 101 | const CheckoutPage: React.FC = ({}: IProps) => { 102 | const location = useLocation(); 103 | const history = useHistory(); 104 | const querystring = parseQueryString(location); 105 | const { 106 | loaded: cartLoaded, 107 | shippingPrice, 108 | discount, 109 | subtotalPrice, 110 | totalPrice, 111 | items, 112 | } = useCart(); 113 | const { 114 | loaded: checkoutLoaded, 115 | checkout, 116 | payment, 117 | availablePaymentGateways, 118 | createPayment, 119 | completeCheckout, 120 | } = useCheckout(); 121 | const intl = useIntl(); 122 | 123 | if (cartLoaded && (!items || !items?.length)) { 124 | return ; 125 | } 126 | 127 | const [submitInProgress, setSubmitInProgress] = useState(false); 128 | const [paymentConfirmation, setPaymentConfirmation] = useState(false); 129 | 130 | const [selectedPaymentGateway, setSelectedPaymentGateway] = useState< 131 | string | undefined 132 | >(payment?.gateway); 133 | const [ 134 | selectedPaymentGatewayToken, 135 | setSelectedPaymentGatewayToken, 136 | ] = useState(payment?.token); 137 | const [paymentGatewayErrors, setPaymentGatewayErrors] = useState< 138 | IFormError[] 139 | >([]); 140 | 141 | useEffect(() => { 142 | setSelectedPaymentGateway(payment?.gateway); 143 | }, [payment?.gateway]); 144 | useEffect(() => { 145 | setSelectedPaymentGatewayToken(payment?.token); 146 | }, [payment?.token]); 147 | 148 | const isShippingRequiredForProducts = 149 | items && 150 | items.some( 151 | ({ variant }) => variant.product?.productType.isShippingRequired 152 | ); 153 | 154 | const stepsWithViews = CHECKOUT_STEPS.filter( 155 | ({ withoutOwnView }) => !withoutOwnView 156 | ); 157 | const steps = isShippingRequiredForProducts 158 | ? stepsWithViews 159 | : stepsWithViews.filter( 160 | ({ onlyIfShippingRequired }) => !onlyIfShippingRequired 161 | ); 162 | const getActiveStepIndex = () => { 163 | const matchingStepIndex = steps.findIndex( 164 | ({ link }) => link === location.pathname 165 | ); 166 | return matchingStepIndex !== -1 ? matchingStepIndex : steps.length - 1; 167 | }; 168 | const getActiveStep = () => { 169 | return steps[getActiveStepIndex()]; 170 | }; 171 | 172 | const checkoutAddressSubpageRef = useRef( 173 | null 174 | ); 175 | const checkoutShippingSubpageRef = useRef( 176 | null 177 | ); 178 | const checkoutPaymentSubpageRef = useRef( 179 | null 180 | ); 181 | const checkoutReviewSubpageRef = useRef(null); 182 | const checkoutGatewayFormId = "gateway-form"; 183 | const checkoutGatewayFormRef = useRef(null); 184 | 185 | const handleNextStepClick = () => { 186 | // Some magic above and below ensures that the activeStepIndex will always 187 | // be in 0-3 range 188 | /* eslint-disable default-case */ 189 | switch (getActiveStep().index) { 190 | case 0: 191 | if (checkoutAddressSubpageRef.current?.submitAddress) { 192 | checkoutAddressSubpageRef.current?.submitAddress(); 193 | } 194 | break; 195 | case 1: 196 | if (checkoutShippingSubpageRef.current?.submitShipping) { 197 | checkoutShippingSubpageRef.current?.submitShipping(); 198 | } 199 | break; 200 | case 2: 201 | if (checkoutPaymentSubpageRef.current?.submitPayment) { 202 | checkoutPaymentSubpageRef.current?.submitPayment(); 203 | } 204 | break; 205 | case 3: 206 | if (checkoutReviewSubpageRef.current?.complete) { 207 | checkoutReviewSubpageRef.current?.complete(); 208 | } 209 | break; 210 | } 211 | }; 212 | const handleStepSubmitSuccess = ( 213 | currentStep: CheckoutStep, 214 | data?: object 215 | ) => { 216 | const activeStepIndex = getActiveStepIndex(); 217 | if (currentStep === CheckoutStep.Review) { 218 | history.push({ 219 | pathname: "/order-finalized", 220 | state: data, 221 | }); 222 | } else { 223 | history.push(steps[activeStepIndex + 1].link); 224 | } 225 | }; 226 | 227 | const shippingTaxedPrice = 228 | checkout?.shippingMethod?.id && shippingPrice 229 | ? { 230 | gross: shippingPrice, 231 | net: shippingPrice, 232 | } 233 | : null; 234 | const promoTaxedPrice = discount && { 235 | gross: discount, 236 | net: discount, 237 | }; 238 | 239 | const checkoutView = 240 | cartLoaded && checkoutLoaded ? ( 241 | ( 247 | 251 | handleStepSubmitSuccess(CheckoutStep.Address) 252 | } 253 | {...props} 254 | /> 255 | )} 256 | renderShipping={props => ( 257 | 261 | handleStepSubmitSuccess(CheckoutStep.Shipping) 262 | } 263 | {...props} 264 | /> 265 | )} 266 | renderPayment={props => ( 267 | 272 | handleStepSubmitSuccess(CheckoutStep.Payment) 273 | } 274 | onPaymentGatewayError={setPaymentGatewayErrors} 275 | {...props} 276 | /> 277 | )} 278 | renderReview={props => ( 279 | 285 | handleStepSubmitSuccess(CheckoutStep.Review, data) 286 | } 287 | {...props} 288 | /> 289 | )} 290 | /> 291 | ) : ( 292 | 293 | ); 294 | 295 | const handleProcessPayment = async ( 296 | gateway: string, 297 | token?: string, 298 | cardData?: ICardData 299 | ) => { 300 | const paymentConfirmStepLink = CHECKOUT_STEPS.find( 301 | step => step.step === CheckoutStep.PaymentConfirm 302 | )?.link; 303 | const { dataError } = await createPayment({ 304 | gateway, 305 | token, 306 | creditCard: cardData, 307 | returnUrl: `${window.location.origin}${paymentConfirmStepLink}`, 308 | }); 309 | const errors = dataError?.error; 310 | setSubmitInProgress(false); 311 | if (errors) { 312 | setPaymentGatewayErrors(errors); 313 | } else { 314 | setPaymentGatewayErrors([]); 315 | handleStepSubmitSuccess(CheckoutStep.Payment); 316 | } 317 | }; 318 | const handleSubmitPayment = async (paymentData?: object) => { 319 | const response = await completeCheckout({ paymentData }); 320 | return { 321 | confirmationData: response.data?.confirmationData, 322 | confirmationNeeded: response.data?.confirmationNeeded, 323 | order: response.data?.order, 324 | errors: response.dataError?.error, 325 | }; 326 | }; 327 | const handleSubmitPaymentSuccess = ( 328 | order?: CompleteCheckout_checkoutComplete_order 329 | ) => { 330 | setSubmitInProgress(false); 331 | setPaymentGatewayErrors([]); 332 | handleStepSubmitSuccess(CheckoutStep.Review, { 333 | id: order?.id, 334 | orderNumber: order?.number, 335 | token: order?.token, 336 | }); 337 | }; 338 | const handlePaymentGatewayError = (errors: IFormError[]) => { 339 | setSubmitInProgress(false); 340 | setPaymentGatewayErrors(errors); 341 | const paymentStepLink = steps.find( 342 | step => step.step === CheckoutStep.Payment 343 | )?.link; 344 | if (paymentStepLink) { 345 | history.push(paymentStepLink); 346 | } 347 | }; 348 | 349 | const paymentGatewaysView = availablePaymentGateways && ( 350 | 363 | ); 364 | 365 | const activeStep = getActiveStep(); 366 | let buttonText = activeStep.nextActionName; 367 | /* eslint-disable default-case */ 368 | switch (activeStep.nextActionName) { 369 | case "Continue to Shipping": 370 | buttonText = intl.formatMessage(checkoutMessages.addressNextActionName); 371 | break; 372 | case "Continue to Payment": 373 | buttonText = intl.formatMessage(checkoutMessages.shippingNextActionName); 374 | break; 375 | case "Continue to Review": 376 | buttonText = intl.formatMessage(checkoutMessages.paymentNextActionName); 377 | break; 378 | case "Place order": 379 | buttonText = intl.formatMessage(checkoutMessages.reviewNextActionName); 380 | break; 381 | } 382 | 383 | useEffect(() => { 384 | const paymentConfirmStepLink = CHECKOUT_STEPS.find( 385 | step => step.step === CheckoutStep.PaymentConfirm 386 | )?.link; 387 | if ( 388 | !submitInProgress && 389 | checkout && 390 | location.pathname === paymentConfirmStepLink && 391 | !paymentConfirmation 392 | ) { 393 | handlePaymentConfirm(); 394 | } 395 | }, [location.pathname, querystring, submitInProgress, checkout]); 396 | 397 | const handlePaymentConfirm = async () => { 398 | /** 399 | * Prevent proceeding in confirmation flow in case of gateways that don't support it to prevent unknown bugs. 400 | */ 401 | if ( 402 | payment?.gateway !== "mirumee.payments.adyen" && 403 | payment?.gateway !== "korolev.payments.sberbank" 404 | ) { 405 | const paymentStepLink = steps.find( 406 | step => step.step === CheckoutStep.Payment 407 | )?.link; 408 | if (paymentStepLink) { 409 | history.push(paymentStepLink); 410 | } 411 | } 412 | 413 | setSubmitInProgress(true); 414 | setPaymentConfirmation(true); 415 | /** 416 | * Saleor API creates an order for not fully authorised payments, thus we accept all non negative payment result codes, 417 | * assuming the payment is completed, what means we can proceed further. 418 | * https://docs.adyen.com/checkout/drop-in-web?tab=http_get_1#step-6-present-payment-result 419 | */ 420 | if ( 421 | adyenNotNegativeConfirmationStatusCodes.includes( 422 | querystring.resultCode as string 423 | ) || 424 | sberbankNotNegativeConfirmationStatusCodes.includes( 425 | querystring.resultCode as string 426 | ) 427 | ) { 428 | const { data, dataError } = await completeCheckout(); 429 | const errors = dataError?.error; 430 | setSubmitInProgress(false); 431 | if (errors) { 432 | setPaymentGatewayErrors(errors); 433 | const paymentStepLink = steps.find( 434 | step => step.step === CheckoutStep.Payment 435 | )?.link; 436 | if (paymentStepLink) { 437 | history.push(paymentStepLink); 438 | } 439 | } else { 440 | setPaymentGatewayErrors([]); 441 | handleStepSubmitSuccess(CheckoutStep.Review, { 442 | id: data?.order?.id, 443 | orderNumber: data?.order?.number, 444 | token: data?.order?.token, 445 | }); 446 | } 447 | } else { 448 | setPaymentGatewayErrors([ 449 | { 450 | message: translateAdyenConfirmationError( 451 | querystring.resultCode as string, 452 | intl 453 | ), 454 | }, 455 | ]); 456 | const paymentStepLink = steps.find( 457 | step => step.step === CheckoutStep.Payment 458 | )?.link; 459 | if (paymentStepLink) { 460 | history.push(paymentStepLink); 461 | setSubmitInProgress(false); 462 | setPaymentConfirmation(false); 463 | } 464 | } 465 | }; 466 | 467 | const activeStepIndex = getActiveStepIndex(); 468 | 469 | return ( 470 | 489 | ); 490 | }; 491 | 492 | export { CheckoutPage }; 493 | --------------------------------------------------------------------------------