bmc_hub/app/workflows/resources.py

70 lines
4.4 KiB
Python
Raw Normal View History

"""Additive resource configuration APIs; existing agreement/card APIs stay unchanged."""
from decimal import Decimal
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from psycopg2.extras import Json
from app.core.auth_dependencies import require_permission
from .service import transaction, json_safe, emit_event
router=APIRouter(prefix='/workflows/resources')
access=require_permission('workflows.manage')
class Coverage(BaseModel):
tag_ids:list[int]=Field(default_factory=list)
excluded_tag_ids:list[int]=Field(default_factory=list)
include_descendants:bool=False
class ResourceUpdate(BaseModel):
workflow_coverage:Coverage
monthly_price:Optional[Decimal]=Field(default=None,ge=0,max_digits=12,decimal_places=2)
@router.get('')
def list_resources(customer_id:Optional[int]=None,user=Depends(access)):
with transaction() as cursor:
where=' WHERE customer_id=%s' if customer_id is not None else ''
params=(customer_id,) if customer_id is not None else None
cursor.execute('SELECT id,agreement_number,customer_id,status,workflow_coverage,monthly_price FROM customer_fixed_price_agreements'+where+' ORDER BY customer_id,id',params)
agreements=cursor.fetchall()
cursor.execute('SELECT id,card_number,customer_id,status,remaining_hours,workflow_coverage FROM tticket_prepaid_cards'+where+' ORDER BY customer_id,id',params)
return json_safe({'agreements':agreements,'cards':cursor.fetchall()})
def update_resource(kind,resource_id,payload,user):
table='customer_fixed_price_agreements' if kind=='agreement' else 'tticket_prepaid_cards'
if kind=='card' and 'monthly_price' in payload.model_fields_set:
raise HTTPException(422,'Månedspris gælder kun fastprisaftaler')
coverage=payload.workflow_coverage.model_dump()
tag_ids=sorted(set(coverage['tag_ids']+coverage['excluded_tag_ids']))
if any(tag_id<=0 for tag_id in tag_ids): raise HTTPException(422,'Tag-ID skal være positivt')
with transaction() as cursor:
cursor.execute(f'SELECT * FROM {table} WHERE id=%s',(resource_id,))
resource=cursor.fetchone()
if not resource: raise HTTPException(404,'Aftale eller kort findes ikke')
if tag_ids:
cursor.execute('SELECT id FROM tags WHERE id=ANY(%s) AND is_active=TRUE',(tag_ids,))
if {r['id'] for r in cursor.fetchall()}!=set(tag_ids): raise HTTPException(422,'Et tag findes ikke eller er inaktivt')
# Lock pending time rows before the resource, matching settlement lock order.
cursor.execute("SELECT t.id FROM tmodule_times t LEFT JOIN sag_sager s ON s.id=t.sag_id LEFT JOIN tmodule_customers c ON c.id=t.customer_id WHERE COALESCE(s.customer_id,c.hub_customer_id)=%s AND t.status NOT IN ('approved','billed') AND t.economy_order_draft_id IS NULL ORDER BY t.id FOR UPDATE OF t",(resource['customer_id'],))
time_ids=[r['id'] for r in cursor.fetchall()]
cursor.execute(f'SELECT * FROM {table} WHERE id=%s FOR UPDATE',(resource_id,))
resource=cursor.fetchone()
price_sql=',monthly_price=%s' if kind=='agreement' and 'monthly_price' in payload.model_fields_set else ''
values=[Json(coverage)]
if price_sql: values.append(payload.monthly_price)
values.append(resource_id)
cursor.execute(f'UPDATE {table} SET workflow_coverage=%s{price_sql},updated_at=now() WHERE id=%s RETURNING *',tuple(values))
updated=cursor.fetchone()
cursor.execute('INSERT INTO bmc_workflow_resource_history(resource_type,resource_id,created_by,before_value,after_value) VALUES(%s,%s,%s,%s,%s)',(kind,resource_id,str(user['id']),Json(json_safe(dict(resource))),Json(json_safe(dict(updated)))))
if time_ids:
cursor.execute('UPDATE tmodule_times SET workflow_revision=workflow_revision+1 WHERE id=ANY(%s) RETURNING id,workflow_revision',(time_ids,))
for time in cursor.fetchall(): emit_event(cursor,'time.updated','time_entry',time['id'],time['workflow_revision'])
return json_safe(dict(updated))
@router.put('/agreements/{resource_id}/coverage')
def update_agreement(resource_id:int,payload:ResourceUpdate,user=Depends(access)):
return update_resource('agreement',resource_id,payload,user)
@router.put('/cards/{resource_id}/coverage')
def update_card(resource_id:int,payload:ResourceUpdate,user=Depends(access)):
return update_resource('card',resource_id,payload,user)