70 lines
4.4 KiB
Python
70 lines
4.4 KiB
Python
|
|
"""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)
|