forked from beba/foerderbarometer
Add service layer for approving and declining ProjectRequest with transactional logic
This commit is contained in:
parent
14717c8318
commit
cc55b17cff
|
|
@ -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()
|
||||
Loading…
Reference in New Issue