Add service layer for approving and declining ProjectRequest with transactional logic

This commit is contained in:
Roman 2025-09-29 00:27:09 +02:00
parent 14717c8318
commit cc55b17cff
1 changed files with 75 additions and 0 deletions

75
input/services.py Normal file
View File

@ -0,0 +1,75 @@
from datetime import date
from django.db import transaction
from .models import ProjectRequest, ProjectsDeclined, Project, Account
def approve_project_request(request_id: int, decided_by: str, account_code: str) -> Project:
# Use a DB transaction so either all changes commit, or none (atomic workflow).
with transaction.atomic():
# SELECT ... FOR UPDATE: lock the row to avoid concurrent approvals/declines.
req = ProjectRequest.objects.select_for_update().get(pk=request_id)
# Mark the request as approved and persist the decision metadata.
req.decision = 'APPROVED'
req.decision_date = date.today() # For DateField a date is fine; prefer timezone.localdate() if TZ-sensitive.
req.decided_by = decided_by
req.save()
# The Account (Kostenstelle) must be assigned by WMDE in the admin workflow.
# .get() will raise DoesNotExist/MultipleObjectsReturned if data integrity is broken.
account = Account.objects.get(code=account_code)
# Create the actual Project from the request data.
# Project.save() will generate pid/finance_id/end_quartal according to your model logic.
proj = Project.objects.create(
realname=req.realname,
email=req.email,
end_mail_send=False,
name=req.name,
description=req.description,
start=req.start,
end=req.end,
page=req.page,
group=req.group,
location=req.location,
participants_estimated=req.participants_estimated,
insurance=req.insurance,
cost=req.cost,
account=account,
notes=req.notes,
granted=True,
granted_date=date.today(), # Consider timezone.localdate() if you care about time zones.
granted_from=decided_by,
)
# After successful creation we remove the original request (it has been fulfilled).
# Because we're inside an atomic block, both operations succeed/fail together.
req.delete()
# Return the created project for further processing in the caller if needed.
return proj
def decline_project_request(request_id: int, reason: str | None = None):
# Same transactional guarantees for declines.
with transaction.atomic():
# Lock the row to prevent concurrent decisions.
req = ProjectRequest.objects.select_for_update().get(pk=request_id)
# Mark as declined and persist decision date.
req.decision = 'DECLINED'
req.decision_date = date.today()
req.save()
# Archive minimal relevant information in a dedicated table.
# No pid/finance_id should be created for declined items.
ProjectsDeclined.objects.create(
original_request_id=req.id,
name=req.name,
realname=req.realname,
email=req.email,
decision_date=req.decision_date,
reason=reason or '',
)
# Remove the original request after archiving.
req.delete()