Skip to content

Commit

Permalink
Fix failing adyen confirm (#11693)
Browse files Browse the repository at this point in the history
* Improve Adyen authorize notification handler

* Add test for out of stock after the payment
  • Loading branch information
korycins committed Jan 11, 2023
1 parent e56dd1b commit 112b3e1
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import logging
from decimal import Decimal
from unittest import mock
from unittest.mock import patch
from unittest.mock import MagicMock, patch

import before_after
import graphene
import pytest
from django.core.exceptions import ValidationError
Expand All @@ -12,6 +13,7 @@
from ......checkout.fetch import fetch_checkout_info, fetch_checkout_lines
from ......order import OrderEvents, OrderStatus
from ......plugins.manager import get_plugins_manager
from ......warehouse.models import Stock
from ..... import ChargeStatus, TransactionKind
from .....utils import price_to_minor_unit, update_payment_charge_status
from ...webhooks import (
Expand Down Expand Up @@ -286,6 +288,142 @@ def test_handle_authorization_for_checkout_partial_payment(
assert not payment.order


@mock.patch("saleor.payment.gateways.adyen.plugin.call_refund")
def test_handle_authorization_for_checkout_out_of_stock_after_payment(
mock_refund,
notification,
adyen_plugin,
payment_adyen_for_checkout,
address,
shipping_method,
):

refund_response = {"pspReference": "refund-psp"}
mock_refund_response = MagicMock()
mock_refund.return_value = mock_refund_response
mock_refund_response.message = refund_response

checkout = payment_adyen_for_checkout.checkout
checkout.shipping_address = address
checkout.shipping_method = shipping_method
checkout.billing_address = address
checkout.save()

payment = payment_adyen_for_checkout
manager = get_plugins_manager()
lines, _ = fetch_checkout_lines(checkout)
checkout_info = fetch_checkout_info(checkout, lines, [], manager)
total = calculations.calculate_checkout_total_with_gift_cards(
manager, checkout_info, lines, address
)
payment.is_active = True
payment.order = None
payment.total = total.gross.amount
payment.currency = total.gross.currency
payment.to_confirm = True
payment.save()

payment_id = graphene.Node.to_global_id("Payment", payment.pk)
notification = notification(
psp_reference="reference",
merchant_reference=payment_id,
value=price_to_minor_unit(payment.total, payment.currency),
)
config = adyen_plugin(adyen_auto_capture=True).config

# when
def call_after_finalizing_payment(*args, **kwargs):
Stock.objects.all().update(quantity=0)

with before_after.before(
"saleor.checkout.complete_checkout._create_order",
call_after_finalizing_payment,
):
handle_authorization(notification, config)

# then
payment.refresh_from_db()
assert not payment.order
assert payment.checkout
assert (
payment.transactions.filter(
kind__in=[
TransactionKind.ACTION_TO_CONFIRM,
TransactionKind.CAPTURE,
TransactionKind.REFUND_ONGOING,
]
).count()
== 3
)


def test_handle_authorization_for_checkout_that_cannot_be_finalized(
notification,
adyen_plugin,
payment_adyen_for_checkout,
address,
shipping_method,
):
# given
checkout = payment_adyen_for_checkout.checkout
checkout.shipping_address = address
checkout.shipping_method = shipping_method
checkout.billing_address = address
checkout.save()

payment = payment_adyen_for_checkout
manager = get_plugins_manager()
lines, _ = fetch_checkout_lines(checkout)
checkout_info = fetch_checkout_info(checkout, lines, [], manager)
total = calculations.calculate_checkout_total_with_gift_cards(
manager, checkout_info, lines, address
)
payment.is_active = True
payment.order = None
payment.total = total.gross.amount
payment.currency = total.gross.currency
payment.to_confirm = True
payment.save()

payment.transactions.create(
token="reference",
kind=TransactionKind.CAPTURE,
is_success=True,
action_required=False,
currency=payment.currency,
amount=payment.total,
gateway_response={},
)
payment.transactions.create(
token="refund-reference",
is_success=True,
kind=TransactionKind.REFUND_ONGOING,
action_required=False,
currency=payment.currency,
amount=payment.total,
gateway_response={},
)

checkout.lines.first().delete()

payment_id = graphene.Node.to_global_id("Payment", payment.pk)
notification = notification(
psp_reference="reference",
merchant_reference=payment_id,
value=price_to_minor_unit(payment.total, payment.currency),
)
config = adyen_plugin(adyen_auto_capture=True).config

# when
handle_authorization(notification, config)

# then
payment.refresh_from_db()
assert not payment.order
assert payment.checkout
assert payment.transactions.count() == 2


@patch("saleor.payment.gateway.void")
def test_handle_authorization_for_checkout_one_of_variants_deleted(
void_mock,
Expand Down
77 changes: 31 additions & 46 deletions saleor/payment/gateways/adyen/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def create_new_transaction(notification, payment, kind):
amount=amount,
currency=currency,
error="",
raw_response={},
raw_response=notification,
psp_reference=transaction_id,
)
return create_transaction(
Expand Down Expand Up @@ -245,15 +245,10 @@ def handle_not_created_order(notification, payment, checkout, kind, manager):
ChargeStatus.FULLY_CHARGED,
}:
return
transaction = payment.transactions.filter(
action_required=False, is_success=True, kind=kind
).last()
if not transaction:
# If the payment is not Auth/Capture, it means that user didn't return to the
# storefront and we need to finalize the checkout asynchronously.
transaction = create_new_transaction(
notification, payment, TransactionKind.ACTION_TO_CONFIRM
)

transaction = create_new_transaction(
notification, payment, TransactionKind.ACTION_TO_CONFIRM
)

# Only when we confirm that notification is success we will create the order
if transaction.is_success and checkout: # type: ignore
Expand Down Expand Up @@ -328,20 +323,11 @@ def handle_authorization(notification: Dict[str, Any], gateway_config: GatewayCo
try_void_or_refund_inactive_payment(payment, transaction, manager)
return

if not payment.order:
handle_not_created_order(notification, payment, checkout, kind, manager)
else:
adyen_auto_capture = gateway_config.connection_params["adyen_auto_capture"]
kind = TransactionKind.AUTH
if adyen_auto_capture:
kind = TransactionKind.CAPTURE
transaction = payment.transactions.filter(
token=transaction_id,
action_required=False,
is_success=True,
kind__in=[TransactionKind.AUTH, TransactionKind.CAPTURE],
).last()
if not transaction:
transaction = get_transaction(payment, transaction_id, kind)
if not transaction:
if not payment.order:
handle_not_created_order(notification, payment, checkout, kind, manager)
else:
new_transaction = create_new_transaction(notification, payment, kind)
if new_transaction.is_success:
gateway_postprocess(new_transaction, payment)
Expand All @@ -364,6 +350,7 @@ def handle_authorization(notification: Dict[str, Any], gateway_config: GatewayCo
payment,
manager,
)

reason = notification.get("reason", "-")
is_success = True if notification.get("success") == "true" else False
success_msg = f"Adyen: The payment {transaction_id} request was successful."
Expand Down Expand Up @@ -446,25 +433,22 @@ def handle_capture(notification: Dict[str, Any], _gateway_config: GatewayConfig)
try_void_or_refund_inactive_payment(payment, transaction, manager)
return

if not payment.order:
handle_not_created_order(
notification, payment, checkout, TransactionKind.CAPTURE, manager
)
else:
capture_transaction = payment.transactions.filter(
action_required=False,
is_success=True,
kind=TransactionKind.CAPTURE,
).last()
new_transaction = create_new_transaction(
notification, payment, TransactionKind.CAPTURE
)
if new_transaction.is_success and not capture_transaction:
gateway_postprocess(new_transaction, payment)
order_info = fetch_order_info(payment.order)
order_captured(
order_info, None, None, new_transaction.amount, payment, manager
transaction = get_transaction(payment, transaction_id, TransactionKind.CAPTURE)
if not transaction:
if not payment.order:
handle_not_created_order(
notification, payment, checkout, TransactionKind.CAPTURE, manager
)
else:
new_transaction = create_new_transaction(
notification, payment, TransactionKind.CAPTURE
)
if new_transaction.is_success:
gateway_postprocess(new_transaction, payment)
order_info = fetch_order_info(payment.order)
order_captured(
order_info, None, None, new_transaction.amount, payment, manager
)

reason = notification.get("reason", "-")
is_success = True if notification.get("success") == "true" else False
Expand Down Expand Up @@ -864,10 +848,15 @@ def handle_order_closed(notification: Dict[str, Any], gateway_config: GatewayCon
adyen_auto_capture = gateway_config.connection_params["adyen_auto_capture"]
kind = TransactionKind.CAPTURE if adyen_auto_capture else TransactionKind.AUTH

order = None
try:
order = handle_not_created_order(
notification, payment, checkout, kind, get_plugins_manager()
)
except Exception as e:
logger.exception("Exception during order creation", extra={"error": e})
return
finally:
if not order and adyen_partial_payments:
refund_partial_payments(adyen_partial_payments, config=gateway_config)
# There is a possibility that user will try once again to pay with partial
Expand All @@ -879,10 +868,6 @@ def handle_order_closed(notification: Dict[str, Any], gateway_config: GatewayCon
id__in=[p.id for p in adyen_partial_payments]
).update(partial=False)

except Exception as e:
logger.exception("Exception during order creation", extra={"error": e})
return

if adyen_partial_payments:
create_order_event_about_adyen_partial_payments(adyen_partial_payments, payment)
else:
Expand Down

0 comments on commit 112b3e1

Please sign in to comment.