Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Paypal refund POC #288

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,7 @@ class Languages:
)

STRIPE_PAYMENT_STATUS_INTERFACE_CODE_SUCCEEDED = "succeeded"
PAYPAL_PAYMENT_STATUS_INTERFACE_CODE_SUCCEEDED = "COMPLETED"

EDX_STRIPE_PAYMENT_INTERFACE_NAME = "stripe_edx"
EDX_PAYPAL_PAYMENT_INTERFACE_NAME = "paypal_edx"
39 changes: 28 additions & 11 deletions commerce_coordinator/apps/commercetools/catalog_info/edx_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
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,
STRIPE_PAYMENT_STATUS_INTERFACE_CODE_SUCCEEDED,
EdXFieldNames,
TwoUKeys
TwoUKeys,
PAYPAL_PAYMENT_STATUS_INTERFACE_CODE_SUCCEEDED,
EDX_PAYPAL_PAYMENT_INTERFACE_NAME
)
from commerce_coordinator.apps.commercetools.catalog_info.utils import typed_money_to_string

Expand Down Expand Up @@ -48,23 +50,38 @@ def get_edx_lms_user_name(customer: CTCustomer):
return customer.custom.fields[EdXFieldNames.LMS_USER_NAME]


def get_edx_successful_stripe_payment(order: CTOrder) -> Union[CTPayment, None]:
def get_edx_successful_payment(order: CTOrder) -> Union[CTPayment, None]:
for pr in order.payment_info.payments:
pmt = pr.obj
print('\n\n\n\n\n get_edx_successful_payment pmt', pmt.payment_status, pmt.payment_method_info.payment_interface)
if pmt.payment_status.interface_code == STRIPE_PAYMENT_STATUS_INTERFACE_CODE_SUCCEEDED \
and pmt.payment_method_info.payment_interface == EDX_STRIPE_PAYMENT_INTERFACE_NAME and \
pmt.interface_id:
return pmt
return None

print('\n\n\n\n\n get_edx_successful_payment returning stripe', pmt, EDX_STRIPE_PAYMENT_INTERFACE_NAME)
return pmt, EDX_STRIPE_PAYMENT_INTERFACE_NAME
elif pmt.payment_status.interface_code == PAYPAL_PAYMENT_STATUS_INTERFACE_CODE_SUCCEEDED \
and pmt.payment_method_info.payment_interface == EDX_PAYPAL_PAYMENT_INTERFACE_NAME and \
pmt.interface_id:
print('\n\n\n\n\n get_edx_successful_payment returning paypal', pmt, EDX_PAYPAL_PAYMENT_INTERFACE_NAME)
return pmt, EDX_PAYPAL_PAYMENT_INTERFACE_NAME
return None, None

def get_edx_payment_intent_id(order: CTOrder) -> Union[str, None]:
pmt = get_edx_successful_stripe_payment(order)
def get_edx_payment_interface_id(order: CTOrder) -> Union[str, None]:
pmt, psp = get_edx_successful_payment(order)
if pmt:
return pmt.interface_id
return pmt.interface_id, psp
return None, None

def get_edx_paypal_payment_transaction_id(order: CTOrder) -> Union[str, None]:
pmt, psp = get_edx_successful_payment(order)
if pmt and 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
return None


def get_edx_order_workflow_state_key(order: CTOrder) -> Optional[str]:
order_workflow_state = None
if order.state and order.state.obj: # it should never be that we have one and not the other. # pragma no cover
Expand All @@ -78,7 +95,7 @@ def get_edx_is_sanctioned(order: CTOrder) -> bool:

def get_edx_refund_amount(order: CTOrder) -> decimal:
refund_amount = decimal.Decimal(0.00)
pmt = get_edx_successful_stripe_payment(order)
pmt, psp = get_edx_successful_payment(order)
for transaction in pmt.transactions:
if transaction.type == TransactionType.CHARGE: # pragma no cover
refund_amount += decimal.Decimal(typed_money_to_string(transaction.amount, money_as_decimal_string=True))
Expand Down
80 changes: 79 additions & 1 deletion commerce_coordinator/apps/commercetools/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
from commerce_coordinator.apps.commercetools.utils import (
find_refund_transaction,
handle_commercetools_error,
translate_stripe_refund_status_to_transaction_status
translate_stripe_refund_status_to_transaction_status, translate_paypal_refund_status_to_transaction_status
)
from commerce_coordinator.apps.core.constants import ORDER_HISTORY_PER_SYSTEM_REQ_LIMIT

Expand Down Expand Up @@ -484,6 +484,48 @@ def create_return_payment_transaction(
handle_commercetools_error(err, context)
raise err

def create_paypal_return_payment_transaction(
self, payment, paypal_refund) -> CTPayment:
try:
logger.info(f"[CommercetoolsAPIClient] - Creating refund transaction for payment with ID {payment.id} "
f"following successful Paypal refund {paypal_refund.get('id')}")

amount_as_money = CTMoney(
cent_amount=payment.amount_planned.cent_amount,
currency_code=payment.amount_planned.currency_code.upper()
)
print('\n\n\n\n amount_as_money', amount_as_money)
transaction_draft = TransactionDraft(
type=TransactionType.REFUND,
amount=amount_as_money,
timestamp=datetime.datetime.utcfromtimestamp(
getattr(paypal_refund, 'create_time', datetime.datetime.now().timestamp())),
state=translate_paypal_refund_status_to_transaction_status(paypal_refund.get('status')),
interaction_id=paypal_refund.get('id')
)
print('\n\n\n\n transaction_draft', transaction_draft)
print('\n\n\n\n datetime', datetime.datetime.utcfromtimestamp(
getattr(paypal_refund, 'create_time', datetime.datetime.now().timestamp())))

add_transaction_action = PaymentAddTransactionAction(
transaction=transaction_draft
)

returned_payment = self.base_client.payments.update_by_id(
id=payment.id,
version=payment.version,
actions=[add_transaction_action]
)
print('\n\n\n\n returned_payment', returned_payment)


return returned_payment
except CommercetoolsError as err:
context = f"Unable to create refund payment transaction for "\
f"payment {payment.id} and paypal refund {paypal_refund.get('id')}"
handle_commercetools_error(err, context)
raise err

def update_line_item_transition_state_on_fulfillment(self, order_id: str, order_version: int,
line_item_id: str, item_quantity: int,
from_state_id: str, new_state_key: str) -> CTOrder:
Expand Down Expand Up @@ -584,3 +626,39 @@ def retire_customer_anonymize_fields(self, customer_id: str, customer_version: i
f"with ID: {customer_id}, after LMS retirement with "
f"error correlation id {err.correlation_id} and error/s: {err.errors}")
raise err


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.models.amount_with_breakdown import AmountWithBreakdown
from paypalserversdk.models.checkout_payment_intent import CheckoutPaymentIntent
from paypalserversdk.models.order_request import OrderRequest
from paypalserversdk.models.purchase_unit_request import PurchaseUnitRequest
from paypalserversdk.api_helper import ApiHelper

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
),
),
)
payments_controller: PaymentsController = paypal_client.payments
49 changes: 34 additions & 15 deletions commerce_coordinator/apps/commercetools/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
from openedx_filters.exceptions import OpenEdxFilterException
from requests import HTTPError

from commerce_coordinator.apps.commercetools.catalog_info.constants import EDX_STRIPE_PAYMENT_INTERFACE_NAME, \
EDX_PAYPAL_PAYMENT_INTERFACE_NAME
from commerce_coordinator.apps.commercetools.catalog_info.edx_utils import (
get_edx_payment_intent_id,
get_edx_refund_amount
get_edx_payment_interface_id,
get_edx_refund_amount,
)
from commerce_coordinator.apps.commercetools.clients import CommercetoolsAPIClient
from commerce_coordinator.apps.commercetools.constants import COMMERCETOOLS_ORDER_MANAGEMENT_SYSTEM
Expand Down Expand Up @@ -106,19 +108,22 @@ def run_filter(self, active_order_management_system, order_number, **kwargs): #
"order_data": ct_order,
}

intent_id = get_edx_payment_intent_id(ct_order)
interface_id, payment_service_provider = get_edx_payment_interface_id(ct_order)
print('\n\n\n\n\n FetchOrderDetailsByOrderNumber interface_id, payment_service_provider', interface_id, payment_service_provider)

if intent_id:
ct_payment = ct_api_client.get_payment_by_key(intent_id)
ret_val['payment_intent_id'] = intent_id
if interface_id:
ct_payment = ct_api_client.get_payment_by_key(interface_id)
ret_val['payment_intent_id'] = interface_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
ret_val['payment_service_provider'] = payment_service_provider
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
ret_val['payment_service_provider'] = None

return ret_val
except CommercetoolsError as err: # pragma no cover
Expand Down Expand Up @@ -160,19 +165,21 @@ def run_filter(self, active_order_management_system, order_id, **kwargs): # pyl
"order_id": ct_order.id
}

intent_id = get_edx_payment_intent_id(ct_order)
interface_id, payment_service_provider = get_edx_payment_interface_id(ct_order)

if intent_id:
ct_payment = ct_api_client.get_payment_by_key(intent_id)
ret_val['payment_intent_id'] = intent_id
if interface_id and payment_service_provider:
ct_payment = ct_api_client.get_payment_by_key(interface_id)
ret_val['payment_intent_id'] = interface_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
ret_val['payment_service_provider'] = payment_service_provider
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
ret_val['payment_service_provider'] = None

return ret_val
except CommercetoolsError as err: # pragma no cover
Expand Down Expand Up @@ -296,8 +303,14 @@ def run_filter(
active_order_management_system,
payment_data,
has_been_refunded,
payment_service_provider,
**kwargs
): # pylint: disable=arguments-differ
print('\n\n\n\n CreateReturnPaymentTransaction refund_response', refund_response)
print('\n\n\n\n CreateReturnPaymentTransaction payment_data', payment_data)
print('\n\n\n\n CreateReturnPaymentTransaction has_been_refunded', has_been_refunded)
print('\n\n\n\n CreateReturnPaymentTransaction payment_service_provider', payment_service_provider)
print('\n\n\n\n CreateReturnPaymentTransaction **kwargs', kwargs)
"""
Execute a filter with the signature specified.
Arguments:
Expand Down Expand Up @@ -328,11 +341,17 @@ def run_filter(
payment_key = refund_response['payment_intent']
payment_on_order = ct_api_client.get_payment_by_key(payment_key)

updated_payment = ct_api_client.create_return_payment_transaction(
payment_id=payment_on_order.id,
payment_version=payment_on_order.version,
stripe_refund=refund_response
)
if payment_service_provider == EDX_STRIPE_PAYMENT_INTERFACE_NAME:
updated_payment = ct_api_client.create_return_payment_transaction(
payment_id=payment_on_order.id,
payment_version=payment_on_order.version,
stripe_refund=refund_response
)
elif payment_service_provider == EDX_PAYPAL_PAYMENT_INTERFACE_NAME:
updated_payment = ct_api_client.create_paypal_return_payment_transaction(
payment=payment_on_order,
paypal_refund=refund_response
)

return {
'returned_payment': updated_payment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
@log_receiver(logger)
def fulfill_order_placed_message_signal(**kwargs):
""" CoordinatorSignal receiver to invoke Celery Task fulfill_order_placed_message_signal_task"""
print('\n\n\n\nfulfill_order_placed_message_signal starting celery')

async_result = fulfill_order_placed_message_signal_task.delay(
order_id=kwargs['order_id'],
line_item_state_id=kwargs['line_item_state_id'],
Expand Down
17 changes: 14 additions & 3 deletions commerce_coordinator/apps/commercetools/sub_messages/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
get_edx_lms_user_id,
get_edx_lms_user_name,
get_edx_order_workflow_state_key,
get_edx_payment_intent_id,
get_edx_product_course_run_key,
is_edx_lms_order
is_edx_lms_order, get_edx_payment_interface_id
)
from commerce_coordinator.apps.commercetools.clients import CommercetoolsAPIClient
from commerce_coordinator.apps.commercetools.constants import EMAIL_NOTIFICATION_CACHE_TTL_SECS
Expand Down Expand Up @@ -49,6 +48,9 @@ def fulfill_order_placed_message_signal_task(

tag = "fulfill_order_placed_message_signal_task"

print('\n\n\n\ninside fulfill_order_placed_message_signal_task = ', f'[CT-{tag}] Processing order {order_id}, '
f'line item {line_item_state_id}, source system {source_system}, message id: {message_id}')

logger.info(f'[CT-{tag}] Processing order {order_id}, '
f'line item {line_item_state_id}, source system {source_system}, message id: {message_id}')

Expand All @@ -61,13 +63,17 @@ def fulfill_order_placed_message_signal_task(
f'message id: {message_id}')
return False

print('\n\n\n\ninside fulfill_order_placed_message_signal_task order = ', order)

try:
customer = client.get_customer_by_id(order.customer_id)
except CommercetoolsError as err: # pragma no cover
logger.error(f'[CT-{tag}] Customer not found: {order.customer_id} for order {order_id} with '
f'CT error {err}, {err.errors}, message id: {message_id}')
return False

print('\n\n\n\ninside fulfill_order_placed_message_signal_task customer = ', customer)

if not (customer and order and is_edx_lms_order(order)):
logger.debug(f'[CT-{tag}] order {order_id} is not an edX order, message id: {message_id}')

Expand Down Expand Up @@ -121,6 +127,8 @@ def fulfill_order_placed_message_signal_task(
serializer.is_valid(raise_exception=True) # pragma no cover

payload = serializer.validated_data
print('\n\n\n\nsending fulfill_order_placed_signal payload = ', payload)

fulfill_order_placed_signal.send_robust(
sender=fulfill_order_placed_message_signal_task,
**payload
Expand Down Expand Up @@ -265,7 +273,10 @@ def _prepare_segment_event_properties(in_order, return_line_item_return_id):
logger.debug(f'[CT-{tag}] order {order_id} is not an edX order, message id: {message_id}')
return True

payment_intent_id = get_edx_payment_intent_id(order)
print('\n\n\n\ninside fulfill_order_returned_signal_task order = ', order)
payment_intent_id, psp = get_edx_payment_interface_id(order)
print('\n\n\n\ninside fulfill_order_returned_signal_task payment_intent_id = ', payment_intent_id, psp)

lms_user_name = get_edx_lms_user_name(customer)
lms_user_id = get_edx_lms_user_id(customer)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def test_correct_arguments_passed_valid_stripe_refund(
mock_values.customer_mock.assert_called_once_with(mock_values.customer_id)
_stripe_api_mock.return_value.refund_payment_intent.assert_called_once()

@patch('commerce_coordinator.apps.commercetools.sub_messages.tasks.get_edx_payment_intent_id')
@patch('commerce_coordinator.apps.commercetools.sub_messages.tasks.get_edx_payment_interface_id')
@patch('commerce_coordinator.apps.commercetools.sub_messages.tasks.OrderRefundRequested.run_filter')
def test_refund_already_charged(
self,
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 @@ -184,6 +184,18 @@ def translate_stripe_refund_status_to_transaction_status(stripe_refund_status: s
}
return translations.get(stripe_refund_status.lower(), stripe_refund_status)

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)


def _create_retired_hash_withsalt(value_to_retire, salt):
"""
Expand Down
3 changes: 3 additions & 0 deletions commerce_coordinator/apps/commercetools/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def post(self, request):
message_details = OrderLineItemMessageInputSerializer(data=input_data)
message_details.is_valid(raise_exception=True)

print('\n\n\n\nmessage_details.data', message_details.data)
order_id = message_details.data['order_id']
line_item_state_id = message_details.data['to_state']['id']
message_id = message_details.data['message_id']
Expand All @@ -56,6 +57,8 @@ def post(self, request):
else:
self.mark_running(tag, order_id)

print('\n\n\n\nOrderFulfilview sending signal', order_id, line_item_state_id, message_id)

fulfill_order_placed_message_signal.send_robust(
sender=self,
order_id=order_id,
Expand Down
Loading
Loading