"""Application use cases for creating and editing procurement requests.""" from collections.abc import Mapping, Sequence from typing import Any from django.db import transaction from communications import comments_helpers from procurement.constants import ProcurementStatus from procurement.models import ProcurementItem, ProcurementRequest class ProcurementRequestUpdateError(Exception): """Expected conflict while applying a procurement request update.""" class ProcurementRequestItemNotFound(ProcurementRequestUpdateError): """An update referenced an item that does not belong to the request.""" def _create_initial_comment(*, item, author, content: str) -> None: if content and author and author.is_authenticated: comments_helpers.create_comment( obj=item, author=author, content=content, ) @transaction.atomic def create_procurement_request( *, requestor, attributes: Mapping[str, Any], items: Sequence[Mapping[str, Any]], ) -> ProcurementRequest: """Create a request and all initial items as one atomic operation.""" request_attributes = dict(attributes) if request_attributes.get("processing_department"): request_attributes["status"] = ProcurementStatus.WAITING procurement_request = ProcurementRequest( requestor=requestor, **request_attributes, ) procurement_request._notification_actor = requestor procurement_request.save() for raw_item in items: item_attributes = dict(raw_item) initial_comment = item_attributes.pop("initial_comment", "").strip() item = ProcurementItem.objects.create( request=procurement_request, **item_attributes, ) _create_initial_comment( item=item, author=requestor, content=initial_comment, ) return procurement_request @transaction.atomic def update_procurement_request_with_items( *, request_id: int, attributes: Mapping[str, Any], items: Sequence[Mapping[str, Any]], author, ) -> ProcurementRequest: """Update request fields and synchronize its item collection atomically.""" procurement_request = ProcurementRequest.objects.select_for_update().get( pk=request_id, ) existing_items = { item.id: item for item in procurement_request.items.select_for_update().all() } for field, value in attributes.items(): setattr(procurement_request, field, value) procurement_request.save() kept_item_ids: set[int] = set() for raw_item in items: item_attributes = dict(raw_item) item_id = item_attributes.pop("id", None) initial_comment = item_attributes.pop("initial_comment", "").strip() if item_id is None: item = ProcurementItem.objects.create( request=procurement_request, **item_attributes, ) else: item = existing_items.get(item_id) if item is None: raise ProcurementRequestItemNotFound(item_id) for field, value in item_attributes.items(): setattr(item, field, value) item.save() kept_item_ids.add(item.id) _create_initial_comment( item=item, author=author, content=initial_comment, ) obsolete_item_ids = set(existing_items) - kept_item_ids if obsolete_item_ids: ProcurementItem.objects.filter( request=procurement_request, id__in=obsolete_item_ids, ).delete() procurement_request.recalculate_fulfillment_status(save=True) return procurement_request