Skip to content

Commit

Permalink
feat: update line items state in single api call (#314)
Browse files Browse the repository at this point in the history
* feat: update line items state in single api call

* fix: improve logging
  • Loading branch information
syedsajjadkazmii authored Jan 2, 2025
1 parent 59ab5f8 commit 41c5ca3
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 10 deletions.
64 changes: 64 additions & 0 deletions commerce_coordinator/apps/commercetools/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from commercetools.platform.models import CustomerSetCustomTypeAction as CTCustomerSetCustomTypeAction
from commercetools.platform.models import CustomerSetFirstNameAction, CustomerSetLastNameAction
from commercetools.platform.models import FieldContainer as CTFieldContainer
from commercetools.platform.models import LineItem as CTLineItem
from commercetools.platform.models import Money as CTMoney
from commercetools.platform.models import Order as CTOrder
from commercetools.platform.models import (
Expand Down Expand Up @@ -619,6 +620,69 @@ def update_line_item_transition_state_on_fulfillment(
handle_commercetools_error(err, f"Unable to update LineItemState of order {order_id}", True)
return None

def update_line_items_transition_state(
self,
order_id: str,
order_version: int,
line_items: List[CTLineItem],
from_state_id: str,
new_state_key: str,
) -> CTOrder:
"""
Update Commercetools order line item state for all items in one call.
Args:
order_id (str): Order ID (UUID)
order_version (int): Current version of order
line_items (List[object]): List of line item objects
from_state_id (str): ID of LineItemState to transition from
new_state_key (str): Key of LineItemState to transition to
Returns (CTOrder): Updated order object or
Returns (CTOrder): Current un-updated order
Raises Exception: Error if update was unsuccessful.
"""

from_state_key = self.get_state_by_id(from_state_id).key

logger.info(
f"[CommercetoolsAPIClient] - Transitioning line item states for order ID '{order_id}'. "
f"From State: '{from_state_key}' "
f"To State: '{new_state_key}' "
f"Line Item IDs: {', '.join(item.id for item in line_items)}"
)

try:
if new_state_key != from_state_key:
actions = [
OrderTransitionLineItemStateAction(
line_item_id=item.id,
quantity=item.quantity,
from_state=StateResourceIdentifier(key=from_state_key),
to_state=StateResourceIdentifier(key=new_state_key),
)
for item in line_items
]

return self.base_client.orders.update_by_id(
id=order_id,
version=order_version,
actions=actions,
)
else:
logger.info(
f"All line items already have the correct state {new_state_key}. "
"Not attempting to transition LineItemState"
)
return self.get_order_by_id(order_id)
except CommercetoolsError as err:
# Logs & ignores version conflict errors due to duplicate Commercetools messages
handle_commercetools_error(
err,
f"Failed to update LineItemStates for order ID '{order_id}'. "
f"Line Item IDs: {', '.join(item.id for item in line_items)}",
True
)
return None

def retire_customer_anonymize_fields(
self,
customer_id: str,
Expand Down
23 changes: 13 additions & 10 deletions commerce_coordinator/apps/commercetools/sub_messages/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,22 @@ def fulfill_order_placed_message_signal_task(
canvas_entry_properties = {"products": []}
canvas_entry_properties.update(extract_ct_order_information_for_braze_canvas(customer, order))

logger.info(
f"[CT-{tag}] Transitioning all line items for order {order.id} to {TwoUKeys.PROCESSING_FULFILMENT_STATE}"
)
updated_order = client.update_line_items_transition_state(
order_id=order.id,
order_version=order.version,
line_items=get_edx_items(order),
from_state_id=line_item_state_id,
new_state_key=TwoUKeys.PROCESSING_FULFILMENT_STATE
)
if not updated_order:
return True

for item in get_edx_items(order):
logger.debug(f'[CT-{tag}] processing edX order {order_id}, line item {item.variant.sku}, '
f'message id: {message_id}')
updated_order = client.update_line_item_transition_state_on_fulfillment(
order.id,
order.version,
item.id,
item.quantity,
line_item_state_id,
TwoUKeys.PROCESSING_FULFILMENT_STATE
)
if not updated_order:
return True

# from here we will always be transitioning from a 'Fulfillment Processing' state
line_item_state_id = client.get_state_by_key(TwoUKeys.PROCESSING_FULFILMENT_STATE).id
Expand Down
13 changes: 13 additions & 0 deletions commerce_coordinator/apps/commercetools/tests/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@
'to_state_key': TwoUKeys.SUCCESS_FULFILMENT_STATE
}

EXAMPLE_UPDATE_ALL_LINE_ITEMS_SIGNAL_PAYLOAD = {
'order_id': '61ec1afa-1b0e-4234-ae28-f997728054fa',
'order_version': 2,
'line_items': [
{
'line_item_id': '822d77c4-00a6-4fb9-909b-094ef0b8c4b9',
'item_quantity': 1,
}
],
'from_state_id': '8f2e888e-9777-4557-9a7f-c649153770c2',
'to_state_key': TwoUKeys.SUCCESS_FULFILMENT_STATE
}

EXAMPLE_RETURNED_ORDER_STRIPE_SIGNAL_PAYLOAD = {
'payment_intent_id': 'pi_3PNWMsH4caH7G0X109NekCG5',
'stripe_refund': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def __init__(self, *args, **kwargs):
self.get_state_by_key = self.state_by_key_mock
self.get_payment_by_key = self.payment_mock
self.update_line_item_transition_state_on_fulfillment = self.updated_line_item_mock
self.update_line_items_transition_state = self.updated_line_item_mock
self.create_return_for_order = self.create_return_item_mock
self.create_return_payment_transaction = self.payment_mock
self.update_return_payment_state_after_successful_refund = self.order_mock
Expand Down
80 changes: 80 additions & 0 deletions commerce_coordinator/apps/commercetools/tests/test_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,86 @@ def test_update_line_item_state_exception(self, mock_state_by_id):

log_mock.assert_called_with(expected_message)

@patch('commerce_coordinator.apps.commercetools.clients.CommercetoolsAPIClient.get_state_by_id')
def test_successful_order_all_line_items_state_update(self, mock_state_by_id):
base_url = self.client_set.get_base_url_from_client()

mock_order = gen_order("mock_order_id")
mock_order.version = "2"
mock_line_item_state = gen_line_item_state()
mock_line_item_state.key = TwoUKeys.PROCESSING_FULFILMENT_STATE
mock_order.line_items[0].state[0].state = mock_line_item_state

mock_state_by_id().return_value = mock_line_item_state

mock_response_order = gen_order("mock_order_id")
mock_response_order.version = 3
mock_response_line_item_state = gen_line_item_state()
mock_response_line_item_state.id = "mock_success_id"
mock_response_line_item_state.key = TwoUKeys.SUCCESS_FULFILMENT_STATE
mock_response_order.line_items[0].state[0].state = mock_response_line_item_state

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

result = self.client_set.client.update_line_items_transition_state(
mock_order.id,
mock_order.version,
mock_order.line_items,
TwoUKeys.PENDING_FULFILMENT_STATE,
TwoUKeys.SUCCESS_FULFILMENT_STATE
)

self.assertEqual(result.line_items[0].state[0].state.id, mock_response_line_item_state.id)

@patch('commerce_coordinator.apps.commercetools.clients.CommercetoolsAPIClient.get_state_by_id')
def test_update_all_line_items_state_exception(self, mock_state_by_id):
mock_order = gen_order("mock_order_id")
mock_order.version = "1"
base_url = self.client_set.get_base_url_from_client()
mock_state_by_id().return_value = gen_line_item_state()
mock_error_response: CommercetoolsError = {
"message": "Could not create return for order mock_order_id",
"errors": [
{
"code": "ConcurrentModification",
"message": "Object [mock_order_id] has a "
"different version than expected. Expected: 2 - Actual: 1."
},
],
"response": {},
"correlation_id": "None"
}

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 patch('commerce_coordinator.apps.commercetools.clients.logging.Logger.info') as log_mock:
self.client_set.client.update_line_items_transition_state(
mock_order.id,
mock_order.version,
mock_order.line_items,
TwoUKeys.PENDING_FULFILMENT_STATE,
TwoUKeys.SUCCESS_FULFILMENT_STATE
)

expected_message = (
f"[CommercetoolsError] Failed to update LineItemStates "
f"for order ID 'mock_order_id'. Line Item IDs: {mock_order.line_items[0].id} "
f"- Correlation ID: {mock_error_response['correlation_id']}, "
f"Details: {mock_error_response['errors']}"
)

log_mock.assert_called_with(expected_message)

@patch('commerce_coordinator.apps.commercetools.clients.CommercetoolsAPIClient.get_state_by_id')
@patch('commerce_coordinator.apps.commercetools.clients.CommercetoolsAPIClient.get_order_by_id')
def test_order_line_item_in_correct_state(self, mock_order_by_id, mock_state_by_id):
Expand Down

0 comments on commit 41c5ca3

Please sign in to comment.