Skip to content

Commit

Permalink
feat: add auto refund feature for paypal
Browse files Browse the repository at this point in the history
  • Loading branch information
mubbsharanwar committed Dec 13, 2024
1 parent 220d511 commit 30d5172
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
14 changes: 9 additions & 5 deletions commerce_coordinator/apps/commercetools/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions commerce_coordinator/apps/commercetools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
52 changes: 52 additions & 0 deletions commerce_coordinator/apps/paypal/clients.py
Original file line number Diff line number Diff line change
@@ -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)
51 changes: 51 additions & 0 deletions commerce_coordinator/apps/paypal/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


3 changes: 2 additions & 1 deletion commerce_coordinator/apps/stripe/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ def run_filter(
payment_intent_id,
amount_in_cents,
has_been_refunded,
psp,
**kwargs
): # pylint: disable=arguments-differ
"""
Expand All @@ -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

Expand Down
3 changes: 3 additions & 0 deletions commerce_coordinator/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]
Expand Down Expand Up @@ -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 = ""
2 changes: 2 additions & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 30d5172

Please sign in to comment.