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: update line items state in single api call #314

Merged
merged 2 commits into from
Jan 2, 2025
Merged
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
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 @@
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(
aht007 marked this conversation as resolved.
Show resolved Hide resolved
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)

Check failure on line 675 in commerce_coordinator/apps/commercetools/clients.py

View workflow job for this annotation

GitHub Actions / tests (ubuntu-20.04, 3.12, django42)

Missing coverage

Missing coverage on lines 671-675
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 @@
canvas_entry_properties = {"products": []}
canvas_entry_properties.update(extract_ct_order_information_for_braze_canvas(customer, order))

logger.info(
aht007 marked this conversation as resolved.
Show resolved Hide resolved
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

Check failure on line 112 in commerce_coordinator/apps/commercetools/sub_messages/tasks.py

View workflow job for this annotation

GitHub Actions / tests (ubuntu-20.04, 3.12, django42)

Missing coverage

Missing coverage on line 112

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
Loading