diff --git a/commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py b/commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py index 3987f375..e98162e1 100644 --- a/commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py +++ b/commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py @@ -7,10 +7,11 @@ 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, + EDX_PAYPAL_PAYMENT_INTERFACE_NAME, STRIPE_PAYMENT_STATUS_INTERFACE_CODE_SUCCEEDED, EdXFieldNames, TwoUKeys diff --git a/commerce_coordinator/apps/commercetools/pipeline.py b/commerce_coordinator/apps/commercetools/pipeline.py index 30876f6b..bb166d15 100644 --- a/commerce_coordinator/apps/commercetools/pipeline.py +++ b/commerce_coordinator/apps/commercetools/pipeline.py @@ -169,21 +169,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 00000000..26768b6f --- /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 27d04907..dab9c60f 100644 --- a/commerce_coordinator/apps/paypal/pipeline.py +++ b/commerce_coordinator/apps/paypal/pipeline.py @@ -6,6 +6,8 @@ from urllib.parse import urlencode, urljoin 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 @@ -29,3 +31,52 @@ def run_filter(self, psp=None, **params): } return None + + +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 ebfd74bc..fda9cff6 100644 --- a/commerce_coordinator/apps/stripe/pipeline.py +++ b/commerce_coordinator/apps/stripe/pipeline.py @@ -251,6 +251,7 @@ def run_filter( payment_intent_id, amount_in_cents, has_been_refunded, + psp, **kwargs ): # pylint: disable=arguments-differ """ @@ -265,7 +266,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 9a7dc2e2..3dd1355e 100644 --- a/commerce_coordinator/settings/base.py +++ b/commerce_coordinator/settings/base.py @@ -392,6 +392,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', ] @@ -478,5 +479,7 @@ def root(*path_fragments): FAVICON_URL = "https://edx-cdn.org/v3/prod/favicon.ico" # PAYPAL SETTINIS +PAYPAL_CLIENT_ID = "" +PAYPAL_CLIENT_SECRET = "" PAYPAL_BASE_URL = "" PAYPAL_USER_ACTIVITES_URL = "" diff --git a/requirements/base.txt b/requirements/base.txt index f1484a6b..62ca0518 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