diff --git a/commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py b/commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py index 3987f375b..526d1b5fe 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 @@ -89,3 +90,15 @@ def get_edx_refund_amount(order: CTOrder) -> decimal: if transaction.type == TransactionType.CHARGE: # pragma no cover refund_amount += decimal.Decimal(typed_money_to_string(transaction.amount, money_as_decimal_string=True)) return refund_amount + + +def get_edx_paypal_payment_transaction_id(order: CTOrder) -> Union[str, None]: + psp = get_edx_payment_service_provider(order) + for pr in order.payment_info.payments: + pmt = pr.obj + if psp == EDX_PAYPAL_PAYMENT_INTERFACE_NAME: + print('\n\n\n\n\n pmt.transactions', pmt.transactions) + for transaction in pmt.transactions: + if transaction.type == TransactionType.CHARGE and transaction.state == TransactionState.SUCCESS: + print('\n\n\n\n\n transaction.interaction_id', transaction.interaction_id) + return transaction.interaction_id \ No newline at end of file diff --git a/commerce_coordinator/apps/commercetools/pipeline.py b/commerce_coordinator/apps/commercetools/pipeline.py index 30876f6b2..bb166d15c 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/commercetools/utils.py b/commerce_coordinator/apps/commercetools/utils.py index f7e8b415a..e46015523 100644 --- a/commerce_coordinator/apps/commercetools/utils.py +++ b/commerce_coordinator/apps/commercetools/utils.py @@ -243,3 +243,15 @@ def create_retired_fields(field_value, salt_list, retired_user_field_fmt=RETIRED raise SALT_LIST_EXCEPTION return retired_user_field_fmt.format(_create_retired_hash_withsalt(field_value.lower(), salt_list[-1])) + +def translate_paypal_refund_status_to_transaction_status(paypal_refund_status: str): + """ + Utility to translate stripe's refund object's status attribute to a valid CT transaction state + """ + translations = { + 'completed': TransactionState.SUCCESS, + } + print('\n\n\n\n\n\n translate_paypal_refund_status_to_transaction_status paypal_refund_status ', paypal_refund_status, type(paypal_refund_status)) + print('\n\n\n\n\n\n translate_paypal_refund_status_to_transaction_status return ', translations.get(paypal_refund_status.lower(), paypal_refund_status)) + print('\n\n\n\n\n\n translate_paypal_refund_status_to_transaction_status TransactionState.SUCCESS ', TransactionState.SUCCESS) + return translations.get(paypal_refund_status.lower(), paypal_refund_status) \ No newline at end of file diff --git a/commerce_coordinator/apps/paypal/clients.py b/commerce_coordinator/apps/paypal/clients.py new file mode 100644 index 000000000..0b29e7d18 --- /dev/null +++ b/commerce_coordinator/apps/paypal/clients.py @@ -0,0 +1,52 @@ +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 27d049076..dab9c60f5 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 ebfd74bcf..fda9cff67 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 9a7dc2e2e..ab9bcf0cc 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 = "ASoOt8z1BmLEzJGLV-N_gWP083ghlpWaj9eOj4BxQ9k8rQ-jDoSO5e_5-gRR3uzwp-hOt_YmfzKsnrFV" +PAYPAL_CLIENT_SECRET = "EL_StzjNHS0lUbtVLPOUxC-fa27E4x12WIAN4XRHjYNNYM3kkUNxXoaFd_XextoDC1h3TvF9vuC74J1Z" PAYPAL_BASE_URL = "" PAYPAL_USER_ACTIVITES_URL = "" 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