"""Order lifecycle service — ties reservations, weighing, dynamic step selection, pricing and confirmation together. All financial values are computed by the backend and stored as immutable snapshots.""" from decimal import Decimal from django.db import transaction from django.utils import timezone from apps.core.errors import ApiError from apps.orders.models import ProcessingOrder from apps.pricing.engine import PricingEngine from apps.workflows.models import Workflow class OrderService: def _active_workflow(self) -> Workflow: workflow = Workflow.objects.filter(status="active", active_version__isnull=False).first() if workflow is None: raise ApiError("NO_ACTIVE_WORKFLOW", "No active workflow is configured.") return workflow @transaction.atomic def create_from_reservation(self, reservation, workflow=None) -> ProcessingOrder: if ProcessingOrder.objects.filter(reservation=reservation).exists(): raise ApiError("ORDER_EXISTS", "An order already exists for this reservation.") if workflow is None: workflow = self._active_workflow() return ProcessingOrder.objects.create( customer=reservation.customer, reservation=reservation, workflow_version=workflow.active_version, ) @transaction.atomic def record_weighing(self, order, gross, tare) -> ProcessingOrder: order = ProcessingOrder.objects.select_for_update().get(pk=order.pk) if order.net_weight_kg is not None: raise ApiError("WEIGHING_IMMUTABLE", "Net weight is already recorded and immutable.") gross = Decimal(str(gross)) tare = Decimal(str(tare)) if gross < tare: raise ApiError("INVALID_WEIGHT", "Gross weight cannot be less than tare weight.") order.gross_weight_kg = gross order.tare_weight_kg = tare order.net_weight_kg = gross - tare order.save(update_fields=["gross_weight_kg", "tare_weight_kg", "net_weight_kg"]) return order @transaction.atomic def select_steps(self, order, step_keys) -> ProcessingOrder: order = ProcessingOrder.objects.select_for_update().get(pk=order.pk) if order.net_weight_kg is None: raise ApiError("WEIGHING_REQUIRED", "Record weighing before selecting processing.") version = order.workflow_version steps = list(version.steps.filter(key__in=step_keys, enabled=True)) keys = {s.key for s in steps} for required in version.steps.filter(required=True): if required.key not in keys: raise ApiError( "REQUIRED_STEP_MISSING", f"Required step '{required.key}' must be selected.", {"step": required.key}, ) order.selected_step_keys = sorted(keys) order.price_snapshot = PricingEngine().calculate(order.net_weight_kg, steps, order.currency) order.status = ProcessingOrder.Status.AWAITING_CONFIRMATION order.save(update_fields=["selected_step_keys", "price_snapshot", "status"]) return order @transaction.atomic def confirm(self, order, terms_accepted=True) -> ProcessingOrder: order = ProcessingOrder.objects.select_for_update().get(pk=order.pk) if order.status != ProcessingOrder.Status.AWAITING_CONFIRMATION: raise ApiError("ORDER_NOT_CONFIRMABLE", "Order is not awaiting confirmation.") if not terms_accepted: raise ApiError("TERMS_REQUIRED", "Terms must be accepted.") order.status = ProcessingOrder.Status.CONFIRMED order.confirmed_at = timezone.now() order.terms_accepted = True order.save(update_fields=["status", "confirmed_at", "terms_accepted"]) return order