Skip to content

Commit bc2cfb9

Browse files
feat: update line items state in single api call
1 parent 59ab5f8 commit bc2cfb9

File tree

5 files changed

+166
-10
lines changed

5 files changed

+166
-10
lines changed

commerce_coordinator/apps/commercetools/clients.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from commercetools.platform.models import CustomerSetCustomTypeAction as CTCustomerSetCustomTypeAction
1717
from commercetools.platform.models import CustomerSetFirstNameAction, CustomerSetLastNameAction
1818
from commercetools.platform.models import FieldContainer as CTFieldContainer
19+
from commercetools.platform.models import LineItem as CTLineItem
1920
from commercetools.platform.models import Money as CTMoney
2021
from commercetools.platform.models import Order as CTOrder
2122
from commercetools.platform.models import (
@@ -619,6 +620,64 @@ def update_line_item_transition_state_on_fulfillment(
619620
handle_commercetools_error(err, f"Unable to update LineItemState of order {order_id}", True)
620621
return None
621622

623+
def update_all_line_items_transition_state_on_fulfillment(
624+
self,
625+
order_id: str,
626+
order_version: int,
627+
line_items: List[CTLineItem],
628+
from_state_id: str,
629+
new_state_key: str,
630+
) -> CTOrder:
631+
"""
632+
Update Commercetools order line item state for all items in one call.
633+
Args:
634+
order_id (str): Order ID (UUID)
635+
order_version (int): Current version of order
636+
line_items (List[object]): List of line item objects
637+
from_state_id (str): ID of LineItemState to transition from
638+
new_state_key (str): Key of LineItemState to transition to
639+
Returns (CTOrder): Updated order object or
640+
Returns (CTOrder): Current un-updated order
641+
Raises Exception: Error if update was unsuccessful.
642+
"""
643+
644+
from_state_key = self.get_state_by_id(from_state_id).key
645+
646+
logger.info(
647+
f"[CommercetoolsAPIClient] - Transitioning all line item states for order with ID {order_id} "
648+
f"from {from_state_key} to {new_state_key}"
649+
)
650+
651+
try:
652+
if new_state_key != from_state_key:
653+
actions = [
654+
OrderTransitionLineItemStateAction(
655+
line_item_id=item.id,
656+
quantity=item.quantity,
657+
from_state=StateResourceIdentifier(key=from_state_key),
658+
to_state=StateResourceIdentifier(key=new_state_key),
659+
)
660+
for item in line_items
661+
]
662+
663+
updated_fulfillment_line_item_order = self.base_client.orders.update_by_id(
664+
id=order_id,
665+
version=order_version,
666+
actions=actions,
667+
)
668+
669+
return updated_fulfillment_line_item_order
670+
else:
671+
logger.info(
672+
f"All line items already have the correct state {new_state_key}. "
673+
"Not attempting to transition LineItemState"
674+
)
675+
return self.get_order_by_id(order_id)
676+
except CommercetoolsError as err:
677+
# Logs & ignores version conflict errors due to duplicate Commercetools messages
678+
handle_commercetools_error(err, f"Unable to update all LineItemStates of order {order_id}", True)
679+
return None
680+
622681
def retire_customer_anonymize_fields(
623682
self,
624683
customer_id: str,

commerce_coordinator/apps/commercetools/sub_messages/tasks.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,22 @@ def fulfill_order_placed_message_signal_task(
9898
canvas_entry_properties = {"products": []}
9999
canvas_entry_properties.update(extract_ct_order_information_for_braze_canvas(customer, order))
100100

101+
logger.info(
102+
f"[CT-{tag}] Transitioning all line items for order {order.id} to {TwoUKeys.PROCESSING_FULFILMENT_STATE}"
103+
)
104+
updated_order = client.update_all_line_items_transition_state_on_fulfillment(
105+
order_id=order.id,
106+
order_version=order.version,
107+
line_items=get_edx_items(order),
108+
from_state_id=line_item_state_id,
109+
new_state_key=TwoUKeys.PROCESSING_FULFILMENT_STATE
110+
)
111+
if not updated_order:
112+
return True
113+
101114
for item in get_edx_items(order):
102115
logger.debug(f'[CT-{tag}] processing edX order {order_id}, line item {item.variant.sku}, '
103116
f'message id: {message_id}')
104-
updated_order = client.update_line_item_transition_state_on_fulfillment(
105-
order.id,
106-
order.version,
107-
item.id,
108-
item.quantity,
109-
line_item_state_id,
110-
TwoUKeys.PROCESSING_FULFILMENT_STATE
111-
)
112-
if not updated_order:
113-
return True
114117

115118
# from here we will always be transitioning from a 'Fulfillment Processing' state
116119
line_item_state_id = client.get_state_by_key(TwoUKeys.PROCESSING_FULFILMENT_STATE).id

commerce_coordinator/apps/commercetools/tests/constants.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@
8181
'to_state_key': TwoUKeys.SUCCESS_FULFILMENT_STATE
8282
}
8383

84+
EXAMPLE_UPDATE_ALL_LINE_ITEMS_SIGNAL_PAYLOAD = {
85+
'order_id': '61ec1afa-1b0e-4234-ae28-f997728054fa',
86+
'order_version': 2,
87+
'line_items': [
88+
{
89+
'line_item_id': '822d77c4-00a6-4fb9-909b-094ef0b8c4b9',
90+
'item_quantity': 1,
91+
}
92+
],
93+
'from_state_id': '8f2e888e-9777-4557-9a7f-c649153770c2',
94+
'to_state_key': TwoUKeys.SUCCESS_FULFILMENT_STATE
95+
}
96+
8497
EXAMPLE_RETURNED_ORDER_STRIPE_SIGNAL_PAYLOAD = {
8598
'payment_intent_id': 'pi_3PNWMsH4caH7G0X109NekCG5',
8699
'stripe_refund': {

commerce_coordinator/apps/commercetools/tests/sub_messages/test_tasks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def __init__(self, *args, **kwargs):
8787
self.get_state_by_key = self.state_by_key_mock
8888
self.get_payment_by_key = self.payment_mock
8989
self.update_line_item_transition_state_on_fulfillment = self.updated_line_item_mock
90+
self.update_all_line_items_transition_state_on_fulfillment = self.updated_line_item_mock
9091
self.create_return_for_order = self.create_return_item_mock
9192
self.create_return_payment_transaction = self.payment_mock
9293
self.update_return_payment_state_after_successful_refund = self.order_mock

commerce_coordinator/apps/commercetools/tests/test_clients.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,86 @@ def test_update_line_item_state_exception(self, mock_state_by_id):
691691

692692
log_mock.assert_called_with(expected_message)
693693

694+
@patch('commerce_coordinator.apps.commercetools.clients.CommercetoolsAPIClient.get_state_by_id')
695+
def test_successful_order_all_line_items_state_update(self, mock_state_by_id):
696+
base_url = self.client_set.get_base_url_from_client()
697+
698+
mock_order = gen_order("mock_order_id")
699+
mock_order.version = "2"
700+
mock_line_item_state = gen_line_item_state()
701+
mock_line_item_state.key = TwoUKeys.PROCESSING_FULFILMENT_STATE
702+
mock_order.line_items[0].state[0].state = mock_line_item_state
703+
704+
mock_state_by_id().return_value = mock_line_item_state
705+
706+
mock_response_order = gen_order("mock_order_id")
707+
mock_response_order.version = 3
708+
mock_response_line_item_state = gen_line_item_state()
709+
mock_response_line_item_state.id = "mock_success_id"
710+
mock_response_line_item_state.key = TwoUKeys.SUCCESS_FULFILMENT_STATE
711+
mock_response_order.line_items[0].state[0].state = mock_response_line_item_state
712+
713+
with requests_mock.Mocker(real_http=True, case_sensitive=False) as mocker:
714+
mocker.post(
715+
f"{base_url}orders/{mock_response_order.id}",
716+
json=mock_response_order.serialize(),
717+
status_code=200
718+
)
719+
720+
result = self.client_set.client.update_all_line_items_transition_state_on_fulfillment(
721+
mock_order.id,
722+
mock_order.version,
723+
mock_order.line_items,
724+
TwoUKeys.PENDING_FULFILMENT_STATE,
725+
TwoUKeys.SUCCESS_FULFILMENT_STATE
726+
)
727+
728+
self.assertEqual(result.line_items[0].state[0].state.id, mock_response_line_item_state.id)
729+
730+
@patch('commerce_coordinator.apps.commercetools.clients.CommercetoolsAPIClient.get_state_by_id')
731+
def test_update_all_line_items_state_exception(self, mock_state_by_id):
732+
mock_order = gen_order("mock_order_id")
733+
mock_order.version = "1"
734+
base_url = self.client_set.get_base_url_from_client()
735+
mock_state_by_id().return_value = gen_line_item_state()
736+
mock_error_response: CommercetoolsError = {
737+
"message": "Could not create return for order mock_order_id",
738+
"errors": [
739+
{
740+
"code": "ConcurrentModification",
741+
"message": "Object [mock_order_id] has a "
742+
"different version than expected. Expected: 2 - Actual: 1."
743+
},
744+
],
745+
"response": {},
746+
"correlation_id": "None"
747+
}
748+
749+
with requests_mock.Mocker(real_http=True, case_sensitive=False) as mocker:
750+
mocker.post(
751+
f"{base_url}orders/mock_order_id",
752+
json=mock_error_response,
753+
status_code=409
754+
)
755+
756+
with patch('commerce_coordinator.apps.commercetools.clients.logging.Logger.info') as log_mock:
757+
self.client_set.client.update_all_line_items_transition_state_on_fulfillment(
758+
mock_order.id,
759+
mock_order.version,
760+
mock_order.line_items,
761+
TwoUKeys.PENDING_FULFILMENT_STATE,
762+
TwoUKeys.SUCCESS_FULFILMENT_STATE
763+
)
764+
765+
expected_message = (
766+
f"[CommercetoolsError] Unable to update all LineItemStates "
767+
f"of order mock_order_id "
768+
f"- Correlation ID: {mock_error_response['correlation_id']}, "
769+
f"Details: {mock_error_response['errors']}"
770+
)
771+
772+
log_mock.assert_called_with(expected_message)
773+
694774
@patch('commerce_coordinator.apps.commercetools.clients.CommercetoolsAPIClient.get_state_by_id')
695775
@patch('commerce_coordinator.apps.commercetools.clients.CommercetoolsAPIClient.get_order_by_id')
696776
def test_order_line_item_in_correct_state(self, mock_order_by_id, mock_state_by_id):

0 commit comments

Comments
 (0)