diff --git a/inventory_management_system_api/repositories/catalogue_item.py b/inventory_management_system_api/repositories/catalogue_item.py index 2fa32b51..7c8c13e6 100644 --- a/inventory_management_system_api/repositories/catalogue_item.py +++ b/inventory_management_system_api/repositories/catalogue_item.py @@ -137,7 +137,9 @@ def has_child_elements(self, catalogue_item_id: CustomObjectId, session: ClientS item = self._items_collection.find_one({"catalogue_item_id": catalogue_item_id}, session=session) return item is not None - def list_ids(self, catalogue_category_id: str, session: ClientSession = None) -> List[ObjectId]: + def list_ids(self, catalogue_category_id: Optional[str] = None, session: ClientSession = None) -> List[ObjectId]: + # TODO: Update description and tests + # TODO: Make sure where it is used elsewhere specifies catalogue_category_id = """ Retrieve a list of all catalogue item ids with a specific catalogue_category_id from a MongoDB database. Performs a projection to only include _id. (Required for mass updates of properties @@ -150,6 +152,7 @@ def list_ids(self, catalogue_category_id: str, session: ClientSession = None) -> """ logger.info( "Finding the id's of all catalogue items within the catalogue category with ID '%s' in the database", + # TODO: Update this when catalogue_category_id is None? catalogue_category_id, ) @@ -157,7 +160,9 @@ def list_ids(self, catalogue_category_id: str, session: ClientSession = None) -> # https://stackoverflow.com/questions/29771192/how-do-i-get-a-list-of-just-the-objectids-using-pymongo # For 100000 documents, using list comprehension takes about 0.85 seconds vs 0.50 seconds for distinct return self._catalogue_items_collection.find( - {"catalogue_category_id": CustomObjectId(catalogue_category_id)}, {"_id": 1}, session=session + {"catalogue_category_id": CustomObjectId(catalogue_category_id)} if catalogue_category_id else {}, + {"_id": 1}, + session=session, ).distinct("_id") def insert_property_to_all_matching( @@ -211,3 +216,16 @@ def update_names_of_all_properties_with_id( array_filters=[{"elem._id": CustomObjectId(property_id)}], session=session, ) + + def update_number_of_spares( + self, + catalogue_item_id: ObjectId, + number_of_spares: Optional[int], + session: Optional[ClientSession] = None, + ): + + self._catalogue_items_collection.update_many( + {"_id": catalogue_item_id}, + {"$set": {"number_of_spares": number_of_spares}}, + session=session, + ) diff --git a/inventory_management_system_api/repositories/item.py b/inventory_management_system_api/repositories/item.py index c61ef2e0..b576656c 100644 --- a/inventory_management_system_api/repositories/item.py +++ b/inventory_management_system_api/repositories/item.py @@ -182,3 +182,16 @@ def update_names_of_all_properties_with_id( ) # pylint:enable=duplicate-code + + def count_with_usage_statuses_ids_in( + self, + catalogue_item_id: ObjectId, + usage_status_ids: List[CustomObjectId], + session: Optional[ClientSession] = None, + ): + # TODO: Comment/log + # TODO: Look at https://stackoverflow.com/questions/60237515/mongodb-returning-wrong-count + + return self._items_collection.count_documents( + {"catalogue_item_id": catalogue_item_id, "usage_status_id": {"$in": usage_status_ids}}, session=session + ) diff --git a/inventory_management_system_api/repositories/setting.py b/inventory_management_system_api/repositories/setting.py index bb53d604..8f6d2d1b 100644 --- a/inventory_management_system_api/repositories/setting.py +++ b/inventory_management_system_api/repositories/setting.py @@ -97,7 +97,9 @@ def get(self, out_model_type: Type[SettingOutBaseT], session: ClientSession = No # The spares definition contains a list of usage statuses - use an aggregate query here to obtain # the actual usage status entities instead of just their stored ID - result = list(self._settings_collection.aggregate(SPARES_DEFINITION_GET_AGGREGATION_PIPELINE)) + result = list( + self._settings_collection.aggregate(SPARES_DEFINITION_GET_AGGREGATION_PIPELINE, session=session) + ) setting = result[0] if len(result) > 0 else None else: setting = self._settings_collection.find_one({"_id": out_model_type.SETTING_ID}, session=session) diff --git a/inventory_management_system_api/services/setting.py b/inventory_management_system_api/services/setting.py index bba91a2f..93c7f7c1 100644 --- a/inventory_management_system_api/services/setting.py +++ b/inventory_management_system_api/services/setting.py @@ -7,8 +7,12 @@ from fastapi import Depends +from inventory_management_system_api.core.custom_object_id import CustomObjectId +from inventory_management_system_api.core.database import start_session_transaction from inventory_management_system_api.core.exceptions import MissingRecordError from inventory_management_system_api.models.setting import SparesDefinitionIn, SparesDefinitionOut +from inventory_management_system_api.repositories.catalogue_item import CatalogueItemRepo +from inventory_management_system_api.repositories.item import ItemRepo from inventory_management_system_api.repositories.setting import SettingRepo from inventory_management_system_api.repositories.usage_status import UsageStatusRepo from inventory_management_system_api.schemas.setting import SparesDefinitionPutSchema @@ -24,15 +28,21 @@ class SettingService: def __init__( self, setting_repository: Annotated[SettingRepo, Depends(SettingRepo)], + catalogue_item_repository: Annotated[CatalogueItemRepo, Depends(CatalogueItemRepo)], + item_repository: Annotated[ItemRepo, Depends(ItemRepo)], usage_status_repository: Annotated[UsageStatusRepo, Depends(UsageStatusRepo)], ) -> None: """ Initialise the `SettingService` with a `SettingRepo` repository. :param setting_repository: `SettingRepo` repository to use. - :param usage_status_repository: `UsageStatusRepo` repository to use + :param catalogue_item_repository: `CatalogueItemRepo` repository to use. + :param item_repository: `ItemRepo` repository to use. + :param usage_status_repository: `UsageStatusRepo` repository to use. """ self._setting_repository = setting_repository + self._catalogue_item_repository = catalogue_item_repository + self._item_repository = item_repository self._usage_status_repository = usage_status_repository def set_spares_definition(self, spares_definition: SparesDefinitionPutSchema) -> SparesDefinitionOut: @@ -49,6 +59,34 @@ def set_spares_definition(self, spares_definition: SparesDefinitionPutSchema) -> if not self._usage_status_repository.get(usage_status.id): raise MissingRecordError(f"No usage status found with ID: {usage_status.id}") - return self._setting_repository.upsert( - SparesDefinitionIn(**spares_definition.model_dump()), SparesDefinitionOut - ) + # Need all updates to the number of spares to succeed or fail together with assigning the new definition + # Also need to be able to write lock documents in the process + with start_session_transaction("updating spares definition") as session: + # Update spares definition first to ensure write locked to prevent further updates while calculating below + new_spares_definition_out = self._setting_repository.upsert( + SparesDefinitionIn(**spares_definition.model_dump()), SparesDefinitionOut, session=session + ) + + # Obtain a list of all catalogue item ids that will need to be recalculated + catalogue_item_ids = self._catalogue_item_repository.list_ids() + + # Usage status id that constitute a spare in the new definition (obtain it now to save processing + # repeatedly) + usage_status_ids = [CustomObjectId(usage_status.id) for usage_status in spares_definition.usage_statuses] + + for catalogue_item_id in catalogue_item_ids: + # Write lock the catalogue item to prevent any further item updates for it until the transaction + # completes + self._catalogue_item_repository.update_number_of_spares(catalogue_item_id, None, session=session) + + # Now calculate the new number of spares + new_number_of_spares = self._item_repository.count_with_usage_statuses_ids_in( + catalogue_item_id, usage_status_ids, session=session + ) + + # Finally update + self._catalogue_item_repository.update_number_of_spares( + catalogue_item_id, new_number_of_spares, session=session + ) + + return new_spares_definition_out