2026-10-08 18:00:47 +02:00
""" Transactional workflow persistence. Never calls external systems. """
from contextlib import contextmanager
from datetime import date , datetime
from decimal import Decimal
import uuid
from psycopg2 . extras import RealDictCursor , Json
from app . core . database import get_db_connection , release_db_connection
def json_safe ( value ) :
if isinstance ( value , dict ) : return { str ( k ) : json_safe ( v ) for k , v in value . items ( ) }
if isinstance ( value , ( list , tuple ) ) : return [ json_safe ( v ) for v in value ]
if isinstance ( value , ( date , datetime ) ) : return value . isoformat ( )
if isinstance ( value , Decimal ) : return float ( value )
return value
def is_incoming_mail ( context ) :
""" Align with mail UI classification and reject authored draft/outbox mail. """
folder = str ( context . get ( ' folder ' ) or ' ' ) . strip ( ) . casefold ( )
status = str ( context . get ( ' status ' ) or ' ' ) . strip ( ) . casefold ( )
direction = str ( context . get ( ' direction ' ) or ' ' ) . strip ( ) . casefold ( )
return not ( folder . startswith ( ( ' sent ' , ' outbox ' , ' draft ' ) ) or status in ( ' sent ' , ' sending ' , ' draft ' ) or direction in ( ' outgoing ' , ' outbound ' ) )
@contextmanager
def transaction ( ) :
conn = get_db_connection ( )
try :
with conn . cursor ( cursor_factory = RealDictCursor ) as cursor :
yield cursor
conn . commit ( )
except BaseException :
conn . rollback ( )
raise
finally :
release_db_connection ( conn )
def load_time_context ( time_id , cursor = None ) :
if cursor is None :
with transaction ( ) as cur : return load_time_context ( time_id , cur )
cursor . execute ( ' SELECT t.*, s.titel AS case_title,s.beskrivelse AS case_description,COALESCE(s.customer_id,c.hub_customer_id) AS hub_customer_id FROM tmodule_times t LEFT JOIN tmodule_customers c ON c.id=t.customer_id LEFT JOIN sag_sager s ON s.id=t.sag_id WHERE t.id= %s ' , ( time_id , ) )
row = cursor . fetchone ( )
if not row : raise ValueError ( ' Time registration not found ' )
row = dict ( row )
row [ ' time_id ' ] = row [ ' id ' ]
row [ ' entity_type ' ] = ' time_entry '
row [ ' case_id ' ] = row . get ( ' sag_id ' )
row [ ' hours ' ] = row . get ( ' original_hours ' )
row [ ' tracking_customer_id ' ] = row . get ( ' customer_id ' )
row [ ' customer_mapping_missing ' ] = row . get ( ' customer_id ' ) is not None and row . get ( ' hub_customer_id ' ) is None
row [ ' customer_id ' ] = str ( row [ ' hub_customer_id ' ] ) if row . get ( ' hub_customer_id ' ) else None
row [ ' tags ' ] = [ ]
row [ ' tag_ids ' ] = [ ]
cursor . execute ( " SELECT DISTINCT et.tag_id FROM entity_tags et WHERE (et.entity_type= ' case ' AND et.entity_id= %s ) OR (et.entity_type= ' time_entry ' AND et.entity_id= %s ) " , ( row . get ( ' sag_id ' ) , row [ ' id ' ] ) )
row [ ' tag_ids ' ] = [ r [ ' tag_id ' ] for r in cursor . fetchall ( ) ]
row [ ' tags ' ] = row [ ' tag_ids ' ]
if row [ ' customer_id ' ] :
cursor . execute ( " SELECT * FROM customer_fixed_price_agreements WHERE customer_id= %s AND status IN ( ' active ' , ' pending_cancellation ' ) AND start_date<=COALESCE( %s ::date,CURRENT_DATE) AND (end_date IS NULL OR end_date>=COALESCE( %s ::date,CURRENT_DATE)) AND (cancellation_effective_date IS NULL OR cancellation_effective_date>COALESCE( %s ::date,CURRENT_DATE)) " , ( int ( row [ ' customer_id ' ] ) , row . get ( ' worked_date ' ) , row . get ( ' worked_date ' ) , row . get ( ' worked_date ' ) ) )
row [ ' agreements ' ] = cursor . fetchall ( )
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
cursor . execute ( " SELECT c.*,COALESCE(p.workflow_coverage,c.workflow_coverage) AS workflow_coverage FROM tticket_prepaid_cards c LEFT JOIN bmc_workflow_customer_card_coverage p ON p.customer_id=c.customer_id WHERE c.customer_id= %s AND c.status= ' active ' AND (c.expires_at IS NULL OR c.expires_at>now()) " , ( int ( row [ ' customer_id ' ] ) , ) )
2026-10-08 18:00:47 +02:00
row [ ' cards ' ] = cursor . fetchall ( )
else :
row [ ' agreements ' ] , row [ ' cards ' ] = [ ] , [ ]
row [ ' has_active_agreement ' ] = bool ( row [ ' agreements ' ] )
row [ ' has_active_card ' ] = bool ( row [ ' cards ' ] )
row [ ' remaining_card_hours ' ] = row [ ' cards ' ] [ 0 ] . get ( ' remaining_hours ' ) if len ( row [ ' cards ' ] ) == 1 else ( 0 if not row [ ' cards ' ] else None )
row [ ' has_available_card ' ] = any ( float ( card . get ( ' remaining_hours ' ) or 0 ) > 0 for card in row [ ' cards ' ] )
return json_safe ( row )
def load_case_context ( case_id , cursor = None ) :
if cursor is None :
with transaction ( ) as cur : return load_case_context ( case_id , cur )
cursor . execute ( ' SELECT * FROM sag_sager WHERE id= %s ' , ( case_id , ) )
row = cursor . fetchone ( )
if not row : raise ValueError ( ' Case not found ' )
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
context = dict ( row , entity_type = ' case ' , case_id = case_id , case_title = row . get ( ' titel ' ) , description = row . get ( ' beskrivelse ' ) , subject = row . get ( ' titel ' ) )
2026-10-08 18:00:47 +02:00
context [ ' customer_id ' ] = str ( row [ ' customer_id ' ] ) if row . get ( ' customer_id ' ) is not None else None
cursor . execute ( " SELECT tag_id FROM entity_tags WHERE entity_type= ' case ' AND entity_id= %s " , ( case_id , ) )
concrete = [ r [ ' tag_id ' ] for r in cursor . fetchall ( ) ]
cursor . execute ( ' SELECT id,parent_id FROM tags ' )
from . classification import ancestor_ids
effective = sorted ( ancestor_ids ( concrete , { r [ ' id ' ] : r [ ' parent_id ' ] for r in cursor . fetchall ( ) } ) )
context . update ( tags = effective , tag_ids = effective , concrete_tag_ids = concrete )
return json_safe ( context )
def load_context ( entity_type , entity_id ) :
if entity_type in ( ' time ' , ' time_entry ' ) :
with transaction ( ) as cursor :
from . classification import enrich_time_context
return enrich_time_context ( cursor , load_time_context ( entity_id , cursor ) )
if entity_type == ' case ' : return load_case_context ( entity_id )
raise ValueError ( ' Context adapter not available for this entity ' )
def emit_time_event_on_connection ( cursor , time_id , revision , trigger = ' time.updated ' , context = None , chain = None ) :
event_id = str ( uuid . uuid4 ( ) )
cursor . execute ( ' INSERT INTO bmc_workflow_events(id,event_key,trigger,entity_type,entity_id,revision,context,chain) VALUES( %s , %s , %s , %s , %s , %s , %s , %s ) ON CONFLICT(event_key) DO NOTHING RETURNING id ' ,
( event_id , f ' time: { time_id } : { revision } ' , trigger , ' time ' , str ( time_id ) , revision , Json ( json_safe ( context or { } ) ) , Json ( chain or [ ] ) ) )
row = cursor . fetchone ( )
return row [ ' id ' ] if row else None
def enqueue_time_event ( time_id , revision , * * kwargs ) :
with transaction ( ) as cursor : return emit_time_event_on_connection ( cursor , time_id , revision , * * kwargs )
def emit_event ( cursor , trigger , entity_type , entity_id , revision , context = None , chain = None , event_key = None ) :
""" Adapter entry point: caller supplies its own transaction and stable revision. """
from . registry import TRIGGERS
if trigger not in TRIGGERS : raise ValueError ( ' Unknown workflow trigger ' )
event_id = str ( uuid . uuid4 ( ) )
cursor . execute ( ' INSERT INTO bmc_workflow_events(id,event_key,trigger,entity_type,entity_id,revision,context,chain) SELECT %s , %s , %s , %s , %s , %s , %s , %s WHERE EXISTS(SELECT 1 FROM bmc_workflow_runtime WHERE id=1 AND enabled) ON CONFLICT(event_key) DO NOTHING RETURNING id ' ,
( event_id , event_key or f ' { trigger } : { entity_type } : { entity_id } : { revision } ' , trigger , entity_type , str ( entity_id ) , revision , Json ( json_safe ( context or { } ) ) , Json ( chain or [ ] ) ) )
row = cursor . fetchone ( )
return row [ ' id ' ] if row else None