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 f651293
Show file tree
Hide file tree
Showing 7 changed files with 122 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
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
53 changes: 53 additions & 0 deletions commerce_coordinator/apps/paypal/clients.py
Original file line number Diff line number Diff line change
@@ -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)

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 = ""
PAYPAL_CLIENT_SECRET = ""
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 f651293

Please sign in to comment.