From 8f449850fae7dec9930c94d118d5e025f6c8da68 Mon Sep 17 00:00:00 2001 From: mubbsharanwar Date: Fri, 13 Dec 2024 18:37:13 +0500 Subject: [PATCH] feat: add auto refund feature for paypal --- .../commercetools/catalog_info/edx_utils.py | 2 +- .../apps/commercetools/pipeline.py | 14 +++-- commerce_coordinator/apps/paypal/clients.py | 53 +++++++++++++++++++ commerce_coordinator/apps/paypal/pipeline.py | 52 +++++++++++++++++- commerce_coordinator/apps/stripe/pipeline.py | 3 +- commerce_coordinator/settings/base.py | 3 ++ requirements/base.txt | 2 + 7 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 commerce_coordinator/apps/paypal/clients.py diff --git a/commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py b/commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py index ed7a5edc6..9994c64ef 100644 --- a/commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py +++ b/commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py @@ -7,7 +7,7 @@ from commercetools.platform.models import Payment as CTPayment from commercetools.platform.models import Product as CTProduct from commercetools.platform.models import ProductVariant as CTProductVariant -from commercetools.platform.models import TransactionType +from commercetools.platform.models import TransactionType, TransactionState from commerce_coordinator.apps.commercetools.catalog_info.constants import ( EDX_STRIPE_PAYMENT_INTERFACE_NAME, diff --git a/commerce_coordinator/apps/commercetools/pipeline.py b/commerce_coordinator/apps/commercetools/pipeline.py index 5eaafef2b..64b5cab85 100644 --- a/commerce_coordinator/apps/commercetools/pipeline.py +++ b/commerce_coordinator/apps/commercetools/pipeline.py @@ -156,21 +156,25 @@ def run_filter(self, active_order_management_system, order_id, **kwargs): # pyl duration = (datetime.now() - start_time).total_seconds() log.info(f"[Performance Check] get_order_by_id call took {duration} seconds") + psp = get_edx_payment_service_provider(ct_order) + + intent_id = None + if psp == EDX_STRIPE_PAYMENT_INTERFACE_NAME: + intent_id = get_edx_payment_intent_id(ct_order) + ret_val = { "order_data": ct_order, - "order_id": ct_order.id + "order_id": ct_order.id, + "psp": psp, + "payment_intent_id": intent_id } - intent_id = get_edx_payment_intent_id(ct_order) - if intent_id: ct_payment = ct_api_client.get_payment_by_key(intent_id) - ret_val['payment_intent_id'] = intent_id ret_val['amount_in_cents'] = get_edx_refund_amount(ct_order) ret_val['has_been_refunded'] = has_refund_transaction(ct_payment) ret_val['payment_data'] = ct_payment else: - ret_val['payment_intent_id'] = None ret_val['amount_in_cents'] = decimal.Decimal(0.00) ret_val['has_been_refunded'] = False ret_val['payment_data'] = None diff --git a/commerce_coordinator/apps/paypal/clients.py b/commerce_coordinator/apps/paypal/clients.py new file mode 100644 index 000000000..26768b6f0 --- /dev/null +++ b/commerce_coordinator/apps/paypal/clients.py @@ -0,0 +1,53 @@ +import logging + +from django.conf import settings +from paypalserversdk.http.auth.o_auth_2 import ClientCredentialsAuthCredentials +from paypalserversdk.logging.configuration.api_logging_configuration import ( + LoggingConfiguration, + RequestLoggingConfiguration, + ResponseLoggingConfiguration, +) +from paypalserversdk.paypalserversdk_client import PaypalserversdkClient +from paypalserversdk.controllers.orders_controller import OrdersController +from paypalserversdk.controllers.payments_controller import PaymentsController +from paypalserversdk.api_helper import ApiHelper + + +class PayPalClient: + def __init__(self): + self.paypal_client: PaypalserversdkClient = PaypalserversdkClient( + client_credentials_auth_credentials=ClientCredentialsAuthCredentials( + o_auth_client_id=settings.PAYPAL_CLIENT_ID, + o_auth_client_secret=settings.PAYPAL_CLIENT_SECRET, + ), + logging_configuration=LoggingConfiguration( + log_level=logging.INFO, + # Disable masking of sensitive headers for Sandbox testing. + # This should be set to True (the default if unset)in production. + mask_sensitive_headers=False, + request_logging_config=RequestLoggingConfiguration( + log_headers=True, log_body=True + ), + response_logging_config=ResponseLoggingConfiguration( + log_headers=True, log_body=True + ), + ), + ) + + + def refund_order(self, order_id): + paypal_client = self.paypal_client + orders_controller: OrdersController = paypal_client.orders + payments_controller: PaymentsController = paypal_client.payments + # get order + order = orders_controller.orders_get({"id": order_id}) + print("order", order) + # get capture id + capture_id = order.body.purchase_units[0].payments.captures[0].id + # refund the capture + collect = {"capture_id": capture_id, "prefer": "return=minimal"} + result = payments_controller.captures_refund(collect) + + print("result:", result) + return ApiHelper.json_serialize(result.body) + \ No newline at end of file diff --git a/commerce_coordinator/apps/paypal/pipeline.py b/commerce_coordinator/apps/paypal/pipeline.py index 9d4841d9c..3415e22b3 100644 --- a/commerce_coordinator/apps/paypal/pipeline.py +++ b/commerce_coordinator/apps/paypal/pipeline.py @@ -6,10 +6,11 @@ from urllib.parse import urlencode from django.conf import settings +from commerce_coordinator.apps.paypal.clients import PayPalClient +from commerce_coordinator.apps.core.constants import PipelineCommand from openedx_filters import PipelineStep from commerce_coordinator.apps.commercetools.catalog_info.constants import EDX_PAYPAL_PAYMENT_INTERFACE_NAME -from commerce_coordinator.apps.core.constants import PipelineCommand logger = logging.getLogger(__name__) @@ -30,3 +31,52 @@ def run_filter(self, psp=None, payment_intent_id=None, **params): return { 'redirect_url': redirect_url, } + + +class RefundPayPalPayment(PipelineStep): + """ + Refunds a PayPal payment + """ + + def run_filter( + self, + order_id, + amount_in_cents, + has_been_refunded, + psp, + **kwargs + ): # pylint: disable=arguments-differ + """ + Execute a filter with the signature specified. + Arguments: + order_id (str): The identifier of the order. + amount_in_cents (decimal): Total amount to refund + has_been_refunded (bool): Has this payment been refunded + kwargs: arguments passed through from the filter. + """ + + tag = type(self).__name__ + + if psp == EDX_PAYPAL_PAYMENT_INTERFACE_NAME and not amount_in_cents: + logger.info(f'[{tag}] amount_in_cents not set, skipping.') + return PipelineCommand.CONTINUE.value + + if has_been_refunded: + logger.info(f'[{tag}] payment already refunded, skipping.') + return { + 'refund_response': "charge_already_refunded" + } + + paypal_client = PayPalClient() + try: + ret_val = paypal_client.refund_order(order_id=order_id) + return { + 'refund_response': ret_val + } + except Exception as ex: + logger.info(f'[CT-{tag}] Unsuccessful PayPal refund with details:' + f'[order_id: {order_id}' + f'message_id: {kwargs["message_id"]}') + raise Exception from ex + + diff --git a/commerce_coordinator/apps/stripe/pipeline.py b/commerce_coordinator/apps/stripe/pipeline.py index 1840223de..f7a5b04c8 100644 --- a/commerce_coordinator/apps/stripe/pipeline.py +++ b/commerce_coordinator/apps/stripe/pipeline.py @@ -249,6 +249,7 @@ def run_filter( payment_intent_id, amount_in_cents, has_been_refunded, + psp, **kwargs ): # pylint: disable=arguments-differ """ @@ -263,7 +264,7 @@ def run_filter( tag = type(self).__name__ - if not payment_intent_id or not amount_in_cents: # pragma: no cover + if psp != EDX_STRIPE_PAYMENT_INTERFACE_NAME and not payment_intent_id or not amount_in_cents: # pragma: no cover logger.info(f'[{tag}] payment_intent_id or amount_in_cents not set, skipping.') return PipelineCommand.CONTINUE.value diff --git a/commerce_coordinator/settings/base.py b/commerce_coordinator/settings/base.py index 8bcec13aa..3d183c000 100644 --- a/commerce_coordinator/settings/base.py +++ b/commerce_coordinator/settings/base.py @@ -395,6 +395,7 @@ def root(*path_fragments): 'commerce_coordinator.apps.rollout.pipeline.DetermineActiveOrderManagementSystemByOrderID', 'commerce_coordinator.apps.commercetools.pipeline.FetchOrderDetailsByOrderID', 'commerce_coordinator.apps.stripe.pipeline.RefundPaymentIntent', + 'commerce_coordinator.apps.paypal.pipeline.RefundPayPalPayment', 'commerce_coordinator.apps.commercetools.pipeline.CreateReturnPaymentTransaction', 'commerce_coordinator.apps.commercetools.pipeline.UpdateCommercetoolsOrderReturnPaymentStatus', ] @@ -438,6 +439,8 @@ def root(*path_fragments): 'paypal': { 'user_activity_page_url': '', 'paypal_webhook_id': PAYPAL_WEBHOOK_ID, + 'client_id': '', + 'client_secret': '', }, }, } diff --git a/requirements/base.txt b/requirements/base.txt index f1484a6b3..62ca05188 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -147,6 +147,8 @@ openedx-filters==1.11.0 # via -r requirements/base.in packaging==24.1 # via marshmallow +paypal-server-sdk==0.5.1 + # via -r requirements/base.in pbr==6.1.0 # via stevedore pillow==11.0.0