Skip to content

Commit

Permalink
Fix incorrect payment captured amount for Adyen (#11711)
Browse files Browse the repository at this point in the history
  • Loading branch information
korycins committed Jan 12, 2023
1 parent cd943b7 commit 24d6a6a
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ......plugins.manager import get_plugins_manager
from ......warehouse.models import Stock
from ..... import ChargeStatus, TransactionKind
from .....models import Transaction
from .....utils import price_to_minor_unit, update_payment_charge_status
from ...webhooks import (
confirm_payment_and_set_back_to_confirm,
Expand Down Expand Up @@ -424,6 +425,93 @@ def test_handle_authorization_for_checkout_that_cannot_be_finalized(
assert payment.transactions.count() == 2


@patch("saleor.payment.gateway.refund")
def test_handle_authorization_calls_refund_for_inactive_payment(
mock_refund,
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 = False
payment.order = None
payment.total = total.gross.amount
payment.currency = total.gross.currency
payment.to_confirm = True
payment.save()

Transaction.objects.bulk_create(
[
Transaction(
payment_id=payment.id,
token="reference",
kind=TransactionKind.CAPTURE,
is_success=True,
action_required=False,
currency=payment.currency,
amount=payment.total,
gateway_response={},
already_processed=True,
),
Transaction(
payment_id=payment.id,
token="refund-reference",
is_success=True,
kind=TransactionKind.REFUND_ONGOING,
action_required=False,
currency=payment.currency,
amount=payment.total,
gateway_response={},
already_processed=True,
),
Transaction(
payment_id=payment.id,
token="refund-reference",
is_success=True,
kind=TransactionKind.REFUND,
action_required=False,
currency=payment.currency,
amount=payment.total,
gateway_response={},
already_processed=True,
),
]
)

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.captured_amount == Decimal("0")
assert payment.transactions.count() == 3


@patch("saleor.payment.gateway.void")
def test_handle_authorization_for_checkout_one_of_variants_deleted(
void_mock,
Expand Down Expand Up @@ -1242,7 +1330,10 @@ def test_handle_refund_already_refunded(
merchant_reference=payment_id,
value=price_to_minor_unit(payment.total, payment.currency),
)
create_new_transaction(notification, payment, TransactionKind.REFUND)
transaction = create_new_transaction(notification, payment, TransactionKind.REFUND)
transaction.already_processed = True
transaction.save()

config = adyen_plugin().config

handle_refund(notification, config)
Expand Down
75 changes: 25 additions & 50 deletions saleor/payment/gateways/adyen/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logging
from decimal import Decimal
from json.decoder import JSONDecodeError
from typing import Any, Callable, Dict, Iterable, List, Optional
from typing import Any, Callable, Dict, Iterable, List, Optional, cast
from urllib.parse import urlencode, urlparse

import Adyen
Expand Down Expand Up @@ -43,7 +43,6 @@
from ....order.events import external_notification_event
from ....order.fetch import fetch_order_info
from ....payment.models import Payment, Transaction
from ....payment.utils import update_payment_charge_status
from ....plugins.manager import get_plugins_manager
from ... import ChargeStatus, PaymentError, TransactionKind, gateway
from ...gateway import payment_refund_or_void
Expand Down Expand Up @@ -126,25 +125,6 @@ def get_transaction(
return transaction


def _get_or_create_transaction(
payment: "Payment",
transaction_id: Optional[str],
allowed_kinds: Iterable[str],
kind: str,
notification,
):
transaction = payment.transactions.filter(
token=transaction_id,
action_required=False,
is_success=True,
kind__in=allowed_kinds,
).last()
if not transaction:
transaction = create_new_transaction(notification, payment, kind)
update_payment_charge_status(payment, transaction)
return transaction


def create_new_transaction(notification, payment, kind):
transaction_id = notification.get("pspReference")
currency = notification.get("amount", {}).get("currency")
Expand Down Expand Up @@ -310,20 +290,15 @@ def handle_authorization(notification: Dict[str, Any], gateway_config: GatewayCo
)
return

transaction = get_transaction(payment, transaction_id, kind)

if not payment.is_active:
# the `CAPTURE`` transaction should be created only when `AUTH` or `CAPTURE`
# transaction does not exist yet
transaction = _get_or_create_transaction(
payment,
transaction_id,
[TransactionKind.AUTH, TransactionKind.CAPTURE],
kind,
notification,
)
if not transaction:
transaction = create_new_transaction(notification, payment, kind)
transaction = cast(Transaction, transaction)
try_void_or_refund_inactive_payment(payment, transaction, manager)
return

transaction = get_transaction(payment, transaction_id, kind)
if not transaction:
if not payment.order:
handle_not_created_order(notification, payment, checkout, kind, manager)
Expand Down Expand Up @@ -420,20 +395,17 @@ def handle_capture(notification: Dict[str, Any], _gateway_config: GatewayConfig)

manager = get_plugins_manager()

transaction = get_transaction(payment, transaction_id, TransactionKind.CAPTURE)

if not payment.is_active:
# the `CAPTURE`` transaction should be created only when `CAPTURE`
# transaction does not exist yet
transaction = _get_or_create_transaction(
payment,
transaction_id,
[TransactionKind.CAPTURE],
TransactionKind.CAPTURE,
notification,
)
if not transaction:
transaction = create_new_transaction(
notification, payment, TransactionKind.CAPTURE
)
transaction = cast(Transaction, transaction)
try_void_or_refund_inactive_payment(payment, transaction, manager)
return

transaction = get_transaction(payment, transaction_id, TransactionKind.CAPTURE)
if not transaction:
if not payment.order:
handle_not_created_order(
Expand Down Expand Up @@ -518,27 +490,30 @@ def handle_refund(notification: Dict[str, Any], _gateway_config: GatewayConfig):
)
if not payment:
return

transaction = get_transaction(payment, transaction_id, TransactionKind.REFUND)
if transaction and transaction.is_success:
if not transaction or not transaction.already_processed:
if not transaction:
transaction = create_new_transaction(
notification, payment, TransactionKind.REFUND
)
gateway_postprocess(transaction, payment)
else:
# it is already refunded
return
new_transaction = create_new_transaction(
notification, payment, TransactionKind.REFUND
)
gateway_postprocess(new_transaction, payment)

transaction = cast(Transaction, transaction)
reason = notification.get("reason", "-")
success_msg = f"Adyen: The refund {transaction_id} request was successful."
failed_msg = f"Adyen: The refund {transaction_id} request failed. Reason: {reason}"
create_payment_notification_for_order(
payment, success_msg, failed_msg, new_transaction.is_success
payment, success_msg, failed_msg, transaction.is_success
)
if payment.order and new_transaction.is_success:
if payment.order and transaction.is_success:
order_refunded(
payment.order,
None,
None,
new_transaction.amount,
transaction.amount,
payment,
get_plugins_manager(),
)
Expand Down
29 changes: 14 additions & 15 deletions saleor/payment/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,21 +616,20 @@ def try_void_or_refund_inactive_payment(
active. Some payment methods don't required confirmation so we can receive delayed
webhook when we have order already paid.
"""
if transaction.is_success:
channel_slug = get_channel_slug_from_payment(payment)
try:
gateway.payment_refund_or_void(
payment,
manager,
channel_slug=channel_slug,
transaction_id=transaction.token,
)
except PaymentError:
logger.exception(
"Unable to void/refund an inactive payment %s, %s.",
payment.id,
payment.psp_reference,
)
if not transaction.is_success:
return

if not transaction.already_processed:
update_payment_charge_status(payment, transaction)
channel_slug = get_channel_slug_from_payment(payment)
try:
gateway.payment_refund_or_void(payment, manager, channel_slug=channel_slug)
except PaymentError:
logger.exception(
"Unable to void/refund an inactive payment %s, %s.",
payment.id,
payment.psp_reference,
)


def payment_owned_by_user(payment_pk: int, user) -> bool:
Expand Down

0 comments on commit 24d6a6a

Please sign in to comment.