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: Add transaction id to returnitem, returnitemid to transaction #265

Merged
merged 9 commits into from
Sep 6, 2024
41 changes: 37 additions & 4 deletions commerce_coordinator/apps/commercetools/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import datetime
import decimal
import logging
from typing import Generic, List, Optional, Tuple, TypeVar, Union

Expand All @@ -19,11 +20,12 @@
from commercetools.platform.models import Order as CTOrder
from commercetools.platform.models import (
OrderAddReturnInfoAction,
OrderSetReturnItemCustomTypeAction,
OrderSetReturnPaymentStateAction,
OrderTransitionLineItemStateAction
)
from commercetools.platform.models import Payment as CTPayment
from commercetools.platform.models import PaymentAddTransactionAction
from commercetools.platform.models import PaymentAddTransactionAction, PaymentSetTransactionCustomTypeAction
from commercetools.platform.models import ProductVariant as CTProductVariant
from commercetools.platform.models import (
ReturnItemDraft,
Expand All @@ -43,6 +45,7 @@
from commerce_coordinator.apps.commercetools.catalog_info.constants import DEFAULT_ORDER_EXPANSION, EdXFieldNames
from commerce_coordinator.apps.commercetools.catalog_info.foundational_types import TwoUCustomTypes
from commerce_coordinator.apps.commercetools.utils import (
find_refund_transaction,
handle_commercetools_error,
translate_stripe_refund_status_to_transaction_status
)
Expand Down Expand Up @@ -372,7 +375,9 @@ def create_return_for_order(self, order_id: str, order_version: int, order_line_

def update_return_payment_state_after_successful_refund(self, order_id: str,
order_version: int,
return_line_item_return_id: str) -> Union[CTOrder, None]:
return_line_item_return_id: str,
payment_intent_id: str,
amount_in_cents: decimal) -> Union[CTOrder, None]:
"""
Update paymentState on the LineItemReturnItem attached to the order.
Updated by the Order ID (UUID)
Expand All @@ -388,17 +393,45 @@ def update_return_payment_state_after_successful_refund(self, order_id: str,
try:
logger.info(f"[CommercetoolsAPIClient] - Updating payment state for return "
f"with id {return_line_item_return_id} to '{ReturnPaymentState.REFUNDED}'.")

return_payment_state_action = OrderSetReturnPaymentStateAction(
return_item_id=return_line_item_return_id,
payment_state=ReturnPaymentState.REFUNDED
)
if not payment_intent_id:
nyujacky marked this conversation as resolved.
Show resolved Hide resolved
payment_intent_id = ''
logger.info(f'Creating return for order - payment_intent_id: {payment_intent_id}')
payment = self.get_payment_by_key(payment_intent_id)
logger.info(f"Payment found: {payment}")
transaction_id = find_refund_transaction(payment, amount_in_cents)
update_transaction_id_action = OrderSetReturnItemCustomTypeAction(
return_item_id=return_line_item_return_id,
type=CTTypeResourceIdentifier(
key='returnItemCustomType',
),
fields=CTFieldContainer({
'transactionId': transaction_id
})
)
return_transaction_return_item_action = PaymentSetTransactionCustomTypeAction(
transaction_id=transaction_id,
type=CTTypeResourceIdentifier(key='transactionCustomType'),
fields=CTFieldContainer({
'returnItemId': return_line_item_return_id
})
)
logger.info(f"Update return payment state after successful refund - payment_intent_id: {payment_intent_id}")

updated_order = self.base_client.orders.update_by_id(
id=order_id,
version=order_version,
actions=[return_payment_state_action]
actions=[return_payment_state_action, update_transaction_id_action]
)
self.base_client.payments.update_by_id(
id=payment.id,
version=payment.version,
actions=[return_transaction_return_item_action]
)
logger.info("Updated transaction with return item id")
return updated_order
except CommercetoolsError as err:
handle_commercetools_error(err, f"Unable to update ReturnPaymentState of order {order_id}")
Expand Down
5 changes: 4 additions & 1 deletion commerce_coordinator/apps/commercetools/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ def run_filter(
try:
ct_api_client = CommercetoolsAPIClient()
order = ct_api_client.get_order_by_id(order_id=order_id)

if not is_commercetools_line_item_already_refunded(order, order_line_item_id):
returned_order = ct_api_client.create_return_for_order(
order_id=order.id,
Expand Down Expand Up @@ -273,7 +274,9 @@ def run_filter(
updated_order = ct_api_client.update_return_payment_state_after_successful_refund(
order_id=order.id,
order_version=order.version,
return_line_item_return_id=return_line_item_return_id
return_line_item_return_id=return_line_item_return_id,
payment_intent_id=kwargs['payment_intent_id'],
amount_in_cents=kwargs['amount_in_cents']
)

return {
Expand Down
142 changes: 108 additions & 34 deletions commerce_coordinator/apps/commercetools/tests/test_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
Customer,
CustomerDraft,
CustomerPagedQueryResponse,
MoneyType,
Order,
OrderPagedQueryResponse,
ReturnInfo,
ReturnPaymentState,
ReturnShipmentState,
TransactionState,
TransactionType,
Type,
TypedMoney,
TypeDraft
)
from django.test import TestCase
Expand All @@ -23,17 +26,20 @@

from commerce_coordinator.apps.commercetools.catalog_info.constants import EdXFieldNames, TwoUKeys
from commerce_coordinator.apps.commercetools.catalog_info.foundational_types import TwoUCustomTypes
from commerce_coordinator.apps.commercetools.clients import PaginatedResult
from commerce_coordinator.apps.commercetools.clients import CommercetoolsAPIClient, PaginatedResult
from commerce_coordinator.apps.commercetools.tests.conftest import (
APITestingSet,
MonkeyPatch,
gen_example_customer,
gen_line_item_state,
gen_order,
gen_order_history,
gen_payment,
gen_payment_with_multiple_transactions,
gen_retired_customer,
gen_return_item
)
from commerce_coordinator.apps.commercetools.tests.sub_messages.test_tasks import CommercetoolsAPIClientMock
from commerce_coordinator.apps.core.constants import ORDER_HISTORY_PER_SYSTEM_REQ_LIMIT
from commerce_coordinator.apps.core.tests.utils import uuid4_str

Expand Down Expand Up @@ -483,55 +489,46 @@ def test_successful_order_return_payment_state_update(self):

# Mocked expected order recieved after CT SDK call to update the order
mock_response_order = gen_order("mock_order_id")
mock_payment = gen_payment_with_multiple_transactions(TransactionType.CHARGE, 4900, TransactionType.REFUND,
TypedMoney(cent_amount=4900,
currency_code='USD',
type=MoneyType.CENT_PRECISION,
fraction_digits=2))
mock_response_order.version = "3"
mock_response_return_item = gen_return_item("mock_return_item_id", ReturnPaymentState.REFUNDED)
mock_response_return_info = ReturnInfo(items=[mock_response_return_item])
mock_response_order.return_info.append(mock_response_return_info)

with requests_mock.Mocker(real_http=True, case_sensitive=False) as mocker:
mocker.post(
f"{base_url}orders/{mock_response_order.id}",
f"{base_url}orders/mock_order_id",
json=mock_response_order.serialize(),
status_code=200
)
mocker.post(
f"{base_url}payments/{mock_payment.id}",
json=mock_payment.serialize(),
status_code=200
)
mocker.get(
f"{base_url}payments/key={mock_payment.id}",
json=mock_payment.serialize(),
status_code=200
)
mocker.get(
f"{base_url}orders/mock_order_id",
json=mock_response_order.serialize(),
status_code=200
)

result = self.client_set.client.update_return_payment_state_after_successful_refund(
mock_order.id,
mock_order.version,
mock_response_return_item.line_item_id
mock_response_return_item.line_item_id,
mock_payment.id,
10000
)

self.assertEqual(result.return_info[1].items[0].payment_state, ReturnPaymentState.REFUNDED)

def test_update_return_payment_state_exception(self):
base_url = self.client_set.get_base_url_from_client()
mock_error_response: CommercetoolsError = {
"message": "Could not update ReturnPaymentState",
"errors": [
{
"code": "ConcurrentModification",
"message": "Object [mock_order_id] has a "
"different version than expected. Expected: 3 - Actual: 2."
},
],
"response": {},
"correlation_id": "123456"
}

with requests_mock.Mocker(real_http=True, case_sensitive=False) as mocker:
mocker.post(
f"{base_url}orders/mock_order_id",
json=mock_error_response,
status_code=409
)

with self.assertRaises(OpenEdxFilterException):
self.client_set.client.update_return_payment_state_after_successful_refund(
order_id="mock_order_id",
order_version="2",
return_line_item_return_id="mock_return_item_id"
)

def test_create_refund_transaction(self):
base_url = self.client_set.get_base_url_from_client()

Expand Down Expand Up @@ -823,3 +820,80 @@ def test_data_class_doesnt_have_more(self):

self.assertEqual(paginated.has_more(), False)
self.assertEqual(paginated.next_offset(), 10)


class ClientUpdateReturnTests(TestCase):
"""Tests for the update_return_payment_state_after_successful_refund method"""
client_set: APITestingSet

def setUp(self):
super().setUp()
self.mock = CommercetoolsAPIClientMock()
self.client_set = APITestingSet.new_instance()

MonkeyPatch.monkey(
CommercetoolsAPIClient,
{
'__init__': lambda _: None,
'get_order_by_id': self.mock.get_order_by_id,
# 'get_customer_by_id': self.mock.get_customer_by_id,
'get_payment_by_key': self.mock.get_payment_by_key,
'create_return_for_order': self.mock.create_return_for_order,
'create_return_payment_transaction': self.mock.create_return_payment_transaction
}
)

def tearDown(self):
self.mock.payment_mock.side_effect = None
MonkeyPatch.unmonkey(CommercetoolsAPIClient)
super().tearDown()

def test_update_return_payment_state_exception(self):
mock_error_response: CommercetoolsError = CommercetoolsError(
"Could not update ReturnPaymentState", [
{
"code": "ConcurrentModification",
"detailedErrorMessage": "Object [mock_order_id] has a "
"different version than expected. Expected: 3 - Actual: 2."
},
], {}, "123456"
)

def _throw(_payment_id):
raise mock_error_response

self.mock.payment_mock.side_effect = _throw

with self.assertRaises(OpenEdxFilterException):
self.client_set.client.update_return_payment_state_after_successful_refund(
order_id="mock_order_id",
order_version="2",
return_line_item_return_id="mock_return_item_id",
payment_intent_id="1",
amount_in_cents=10000
)

def test_update_return_payment_state_no_payment(self):
mock_error_response: CommercetoolsError = CommercetoolsError(
"Could not update ReturnPaymentState", [
{
"code": "ConcurrentModification",
"detailedErrorMessage": "Object [mock_order_id] has a "
"different version than expected. Expected: 3 - Actual: 2."
},
], {}, "123456"
)

def _throw(_payment_id):
raise mock_error_response

self.mock.payment_mock.side_effect = _throw

with self.assertRaises(OpenEdxFilterException):
self.client_set.client.update_return_payment_state_after_successful_refund(
order_id="mock_order_id",
order_version="2",
return_line_item_return_id="mock_return_item_id",
payment_intent_id=None,
amount_in_cents=10000
)
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ def test_pipeline(self, mock_order_return_update):

pipe = UpdateCommercetoolsOrderReturnPaymentStatus("test_pipe", None)
mock_order_return_update.return_value = self.update_order_response
ret = pipe.run_filter(order_data=self.update_order_data, returned_order=self.update_order_data)
ret = pipe.run_filter(order_data=self.update_order_data, returned_order=self.update_order_data,
payment_intent_id="mock_payment_intent_id", amount_in_cents=10000)
result_data = ret['returned_order']
self.assertEqual(result_data, self.update_order_response)
self.assertEqual(result_data.return_info[1].items[0].payment_state, ReturnPaymentState.REFUNDED)
Expand Down
30 changes: 29 additions & 1 deletion commerce_coordinator/apps/commercetools/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""
Tests for Commerce tools utils
"""
import decimal
import hashlib
import unittest
from unittest.mock import MagicMock

from braze.client import BrazeClient
from commercetools.platform.models import TransactionState, TransactionType
from commercetools.platform.models import MoneyType, TransactionState, TransactionType, TypedMoney
from django.conf import settings
from django.test import override_settings
from django.urls import reverse
Expand All @@ -22,6 +23,7 @@
create_retired_fields,
extract_ct_order_information_for_braze_canvas,
extract_ct_product_information_for_braze_canvas,
find_refund_transaction,
get_braze_client,
has_full_refund_transaction,
has_refund_transaction,
Expand Down Expand Up @@ -224,6 +226,32 @@ def test_has_no_refund_transaction(self):
self.assertFalse(has_full_refund_transaction(payment))


class TestFindRefundTransaction(unittest.TestCase):
"""
Tests for Find Refund Transaction Utils function
"""

def test_has_no_refund_transaction(self):
payment = gen_payment_with_multiple_transactions(TransactionType.CHARGE, 4900)
self.assertEqual(find_refund_transaction(payment, 4900), {})

def test_has_matching_refund_transaction(self):
payment = gen_payment_with_multiple_transactions(TransactionType.CHARGE, 4900, TransactionType.REFUND,
TypedMoney(cent_amount=4900,
currency_code='USD',
type=MoneyType.CENT_PRECISION,
fraction_digits=2))
self.assertEqual(find_refund_transaction(payment, decimal.Decimal(49.0)), payment.transactions[1].id)

def test_has_no_matching_refund_transaction(self):
payment = gen_payment_with_multiple_transactions(TransactionType.CHARGE, 4900, TransactionType.REFUND,
TypedMoney(cent_amount=4900,
currency_code='USD',
type=MoneyType.CENT_PRECISION,
fraction_digits=2))
self.assertEqual(find_refund_transaction(payment, 4000), {})


class TestTranslateStripeRefundStatus(unittest.TestCase):
"""
Tests for Translating Stripes Refund Status Utils class
Expand Down
Loading
Loading