2026-10-08 18:00:47 +02:00
""" Opt-in integration tests against a disposable restored BMC database only. """
import os
import uuid
from concurrent . futures import ThreadPoolExecutor
from threading import Barrier
import pytest
import psycopg2
from psycopg2 . extensions import parse_dsn
from psycopg2 . extras import RealDictCursor , Json
from app . core import database
from app . workflows import settlement , service , worker
@pytest.fixture
def db ( monkeypatch ) :
dsn = os . environ . get ( ' BMC_WORKFLOW_TEST_DSN ' )
if not dsn :
pytest . skip ( ' BMC_WORKFLOW_TEST_DSN required for disposable database tests ' )
if not parse_dsn ( dsn ) . get ( ' dbname ' , ' ' ) . startswith ( ' bmc_workflow_test ' ) :
pytest . fail ( ' Database must be named bmc_workflow_test* ' )
monkeypatch . setattr ( database , ' get_db_connection ' , lambda : psycopg2 . connect ( dsn ) )
monkeypatch . setattr ( database , ' release_db_connection ' , lambda conn : conn . close ( ) )
monkeypatch . setattr ( service , ' get_db_connection ' , lambda : psycopg2 . connect ( dsn ) )
monkeypatch . setattr ( service , ' release_db_connection ' , lambda conn : conn . close ( ) )
monkeypatch . setattr ( settlement , ' active_for_customer ' , lambda _ : True )
conn = psycopg2 . connect ( dsn )
conn . autocommit = True
with conn . cursor ( cursor_factory = RealDictCursor ) as c :
c . execute ( ' SELECT * FROM bmc_workflow_runtime WHERE id=1 ' )
runtime = dict ( c . fetchone ( ) )
c . execute ( ' UPDATE bmc_workflow_runtime SET enabled=FALSE WHERE id=1 ' )
yield conn
with conn . cursor ( ) as c :
c . execute ( ' UPDATE bmc_workflow_runtime SET enabled= %s ,mode= %s ,customer_ids= %s WHERE id=1 ' , ( runtime [ ' enabled ' ] , runtime [ ' mode ' ] , Json ( runtime [ ' customer_ids ' ] ) ) )
conn . close ( )
@pytest.fixture
def entry ( db ) :
token = str ( uuid . uuid4 ( ) )
with db . cursor ( cursor_factory = RealDictCursor ) as c :
c . execute ( ' SELECT user_id FROM users ORDER BY user_id LIMIT 1 ' )
actor = c . fetchone ( ) [ ' user_id ' ]
# Hub mapping is read-only and cards have no customer FK in BMC.
customer = 2000000000 + uuid . uuid4 ( ) . int % 100000000
c . execute ( ' INSERT INTO tmodule_customers(vtiger_id,name,hub_customer_id) VALUES( %s , %s , %s ) RETURNING id ' , ( ' workflow-test- ' + token , ' Workflow integration test ' , customer ) )
tracking = c . fetchone ( ) [ ' id ' ]
c . execute ( " INSERT INTO tticket_prepaid_cards(card_number,customer_id,purchased_hours,price_per_hour,total_amount,status,rounding_minutes) VALUES( %s , %s ,10,100,1000, ' active ' ,30) RETURNING id " , ( ' WF- ' + token , customer ) )
card = c . fetchone ( ) [ ' id ' ]
c . execute ( " INSERT INTO tmodule_times(customer_id,description,original_hours,worked_date,status,billing_method,faktisk_tid_min) VALUES( %s , %s ,1,CURRENT_DATE, ' pending ' , ' prepaid ' ,60) RETURNING id,workflow_revision " , ( tracking , ' Workflow test ' + token ) )
time = c . fetchone ( )
c . execute ( " INSERT INTO bmc_billing_decisions(time_id,revision,status,method,details) VALUES( %s , %s , ' decided ' , ' prepaid ' , %s ) " , ( time [ ' id ' ] , time [ ' workflow_revision ' ] , Json ( { ' billing ' : { ' card_id ' : card } , ' input ' : { ' tag_ids ' : [ ] } } ) ) )
data = { ' time_id ' : time [ ' id ' ] , ' card_id ' : card , ' actor ' : actor , ' customer_id ' : customer , ' tracking_id ' : tracking }
yield data
with db . cursor ( ) as c :
c . execute ( ' DELETE FROM bmc_workflow_settlements WHERE time_id= %s ' , ( time [ ' id ' ] , ) )
c . execute ( ' DELETE FROM bmc_billing_decisions WHERE time_id= %s ' , ( time [ ' id ' ] , ) )
c . execute ( ' DELETE FROM tticket_prepaid_transactions WHERE workflow_time_entry_id= %s ' , ( time [ ' id ' ] , ) )
c . execute ( ' DELETE FROM bmc_workflow_effects WHERE event_id IN(SELECT id FROM bmc_workflow_events WHERE entity_type IN ( \' time \' , \' time_entry \' ) AND entity_id= %s ) ' , ( str ( time [ ' id ' ] ) , ) )
c . execute ( ' DELETE FROM bmc_workflow_executions WHERE event_id IN(SELECT id FROM bmc_workflow_events WHERE entity_type IN ( \' time \' , \' time_entry \' ) AND entity_id= %s ) ' , ( str ( time [ ' id ' ] ) , ) )
c . execute ( ' DELETE FROM bmc_workflow_events WHERE entity_type IN ( \' time \' , \' time_entry \' ) AND entity_id= %s ' , ( str ( time [ ' id ' ] ) , ) )
c . execute ( ' DELETE FROM tmodule_times WHERE id= %s ' , ( time [ ' id ' ] , ) )
c . execute ( ' DELETE FROM tticket_prepaid_cards WHERE id= %s ' , ( card , ) )
c . execute ( ' DELETE FROM tmodule_customers WHERE id= %s ' , ( tracking , ) )
def test_two_concurrent_settlements_debit_exactly_once ( db , entry ) :
barrier = Barrier ( 2 )
def settle ( ) :
barrier . wait ( timeout = 10 )
return settlement . settle_workflow_entries ( [ entry [ ' time_id ' ] ] , entry [ ' actor ' ] )
with ThreadPoolExecutor ( max_workers = 2 ) as pool :
results = list ( pool . map ( lambda _ : settle ( ) , range ( 2 ) ) )
assert all ( result [ ' success ' ] for result in results )
with db . cursor ( ) as c :
c . execute ( ' SELECT used_hours FROM tticket_prepaid_cards WHERE id= %s ' , ( entry [ ' card_id ' ] , ) )
assert float ( c . fetchone ( ) [ 0 ] ) == 1
c . execute ( ' SELECT count(*) FROM tticket_prepaid_transactions WHERE workflow_time_entry_id= %s ' , ( entry [ ' time_id ' ] , ) )
assert c . fetchone ( ) [ 0 ] == 1
c . execute ( ' SELECT count(*) FROM bmc_workflow_settlements WHERE time_id= %s ' , ( entry [ ' time_id ' ] , ) )
assert c . fetchone ( ) [ 0 ] == 1
def test_failure_after_debit_rolls_back_all_changes ( db , entry , monkeypatch ) :
def fail ( * args , * * kwargs ) :
raise RuntimeError ( ' Injected failure after debit ' )
monkeypatch . setattr ( settlement , ' execute_insert ' , fail )
with pytest . raises ( RuntimeError , match = ' Injected failure ' ) :
settlement . settle_workflow_entries ( [ entry [ ' time_id ' ] ] , entry [ ' actor ' ] )
with db . cursor ( ) as c :
c . execute ( ' SELECT used_hours FROM tticket_prepaid_cards WHERE id= %s ' , ( entry [ ' card_id ' ] , ) )
assert float ( c . fetchone ( ) [ 0 ] ) == 0
c . execute ( ' SELECT count(*) FROM bmc_workflow_settlements WHERE time_id= %s ' , ( entry [ ' time_id ' ] , ) )
assert c . fetchone ( ) [ 0 ] == 0
c . execute ( ' SELECT status FROM tmodule_times WHERE id= %s ' , ( entry [ ' time_id ' ] , ) )
assert c . fetchone ( ) [ 0 ] == ' pending '
def test_disabled_capture_creates_no_events ( db , entry ) :
with db . cursor ( ) as c :
c . execute ( " UPDATE tmodule_times SET description= ' Disabled workflow test ' WHERE id= %s " , ( entry [ ' time_id ' ] , ) )
c . execute ( ' SELECT count(*) FROM bmc_workflow_events WHERE entity_id= %s AND entity_type= \' time_entry \' ' , ( str ( entry [ ' time_id ' ] ) , ) )
assert c . fetchone ( ) [ 0 ] == 0
def test_enabled_capture_rolls_back_with_source_change ( db , entry ) :
with db . cursor ( ) as c :
c . execute ( ' UPDATE bmc_workflow_runtime SET enabled=TRUE WHERE id=1 ' )
db . autocommit = False
try :
with db . cursor ( ) as c :
c . execute ( " UPDATE tmodule_times SET description= ' Transactional workflow test ' WHERE id= %s " , ( entry [ ' time_id ' ] , ) )
c . execute ( ' SELECT count(*) FROM bmc_workflow_events WHERE entity_id= %s AND entity_type= \' time_entry \' ' , ( str ( entry [ ' time_id ' ] ) , ) )
assert c . fetchone ( ) [ 0 ] == 1
db . rollback ( )
finally :
db . autocommit = True
with db . cursor ( ) as c :
c . execute ( ' SELECT count(*) FROM bmc_workflow_events WHERE entity_id= %s AND entity_type= \' time_entry \' ' , ( str ( entry [ ' time_id ' ] ) , ) )
assert c . fetchone ( ) [ 0 ] == 0
def test_version_is_immutable ( db ) :
db . autocommit = False
try :
with db . cursor ( ) as c :
c . execute ( " INSERT INTO bmc_workflows(name) VALUES( ' Workflow immutability test ' ) RETURNING id " )
workflow_id = c . fetchone ( ) [ 0 ]
c . execute ( ' INSERT INTO bmc_workflow_versions(workflow_id,version,definition) VALUES( %s ,1, \' {} \' ) RETURNING id ' , ( workflow_id , ) )
version_id = c . fetchone ( ) [ 0 ]
with pytest . raises ( psycopg2 . Error ) :
c . execute ( " UPDATE bmc_workflow_versions SET definition= ' {} ' WHERE id= %s " , ( version_id , ) )
db . rollback ( )
finally :
db . rollback ( )
db . autocommit = True
@pytest.fixture
def worker_rule ( db , entry ) :
definition = { ' trigger ' : ' time.updated ' , ' branches ' : [ { ' condition ' : { ' field ' : ' description ' , ' operator ' : ' contains ' , ' value ' : ' worker test ' } , ' actions ' : [ { ' type ' : ' set_billing ' , ' method ' : ' invoice ' } ] } ] }
with db . cursor ( ) as c :
c . execute ( ' INSERT INTO bmc_workflows(name,customer_id,priority,enabled) VALUES( %s , %s ,0,FALSE) RETURNING id ' , ( ' Disposable worker integration ' + str ( uuid . uuid4 ( ) ) , str ( entry [ ' customer_id ' ] ) ) )
workflow_id = c . fetchone ( ) [ 0 ]
c . execute ( ' INSERT INTO bmc_workflow_versions(workflow_id,version,definition) VALUES( %s ,1, %s ) RETURNING id ' , ( workflow_id , Json ( definition ) ) )
version_id = c . fetchone ( ) [ 0 ]
c . execute ( ' UPDATE bmc_workflows SET enabled=TRUE,active_version_id= %s WHERE id= %s ' , ( version_id , workflow_id ) )
c . execute ( " UPDATE bmc_workflow_runtime SET enabled=TRUE,mode= ' active ' ,customer_ids= %s WHERE id=1 " , ( Json ( [ entry [ ' customer_id ' ] ] ) , ) )
yield workflow_id
# Immutable version history deliberately remains in this disposable DB.
with db . cursor ( ) as c :
c . execute ( ' UPDATE bmc_workflows SET enabled=FALSE WHERE id= %s ' , ( workflow_id , ) )
def test_worker_shadow_then_active_revision_and_repeat_once ( db , entry , worker_rule ) :
with db . cursor ( ) as c :
c . execute ( ' DELETE FROM bmc_billing_decisions WHERE time_id= %s ' , ( entry [ ' time_id ' ] , ) )
c . execute ( " UPDATE tmodule_times SET description= ' First worker test ' WHERE id= %s " , ( entry [ ' time_id ' ] , ) )
assert worker . process_pending_events ( limit = 100 , mode = ' shadow ' ) > = 1
with db . cursor ( ) as c :
c . execute ( ' SELECT count(*) FROM bmc_billing_decisions WHERE time_id= %s ' , ( entry [ ' time_id ' ] , ) )
assert c . fetchone ( ) [ 0 ] == 0
c . execute ( " SELECT count(*) FROM bmc_workflow_executions x JOIN bmc_workflow_events e ON e.id=x.event_id WHERE e.entity_id= %s AND x.mode= ' shadow ' " , ( str ( entry [ ' time_id ' ] ) , ) )
assert c . fetchone ( ) [ 0 ] == 1
c . execute ( " UPDATE tmodule_times SET description= ' Second worker test ' WHERE id= %s RETURNING workflow_revision " , ( entry [ ' time_id ' ] , ) )
revision = c . fetchone ( ) [ 0 ]
assert worker . process_pending_events ( limit = 100 , mode = ' active ' , customer_ids = [ entry [ ' customer_id ' ] ] ) > = 1
with db . cursor ( ) as c :
c . execute ( ' SELECT revision,method,status FROM bmc_billing_decisions WHERE time_id= %s AND is_current ' , ( entry [ ' time_id ' ] , ) )
assert c . fetchone ( ) == ( revision , ' invoice ' , ' decided ' )
assert worker . process_pending_events ( limit = 100 , mode = ' active ' , customer_ids = [ entry [ ' customer_id ' ] ] ) == 0
with db . cursor ( ) as c :
c . execute ( ' SELECT count(*) FROM bmc_billing_decisions WHERE time_id= %s ' , ( entry [ ' time_id ' ] , ) )
assert c . fetchone ( ) [ 0 ] == 1
def test_worker_preserves_manual_override ( db , entry , worker_rule ) :
with db . cursor ( ) as c :
c . execute ( ' UPDATE bmc_billing_decisions SET manual_override=TRUE WHERE time_id= %s ' , ( entry [ ' time_id ' ] , ) )
c . execute ( " UPDATE tmodule_times SET description= ' Manual worker test ' WHERE id= %s " , ( entry [ ' time_id ' ] , ) )
assert worker . process_pending_events ( limit = 100 , mode = ' active ' , customer_ids = [ entry [ ' customer_id ' ] ] ) > = 1
with db . cursor ( ) as c :
c . execute ( ' SELECT method,manual_override FROM bmc_billing_decisions WHERE time_id= %s AND is_current ' , ( entry [ ' time_id ' ] , ) )
assert c . fetchone ( ) == ( ' prepaid ' , True )
c . execute ( ' SELECT count(*) FROM bmc_billing_decisions WHERE time_id= %s ' , ( entry [ ' time_id ' ] , ) )
assert c . fetchone ( ) [ 0 ] == 1
@pytest.mark.parametrize ( ' line_mode,expected_lines ' , [ ( ' separate ' , 2 ) , ( ' combined ' , 1 ) ] )
def test_invoice_draft_lines_preserve_sources_and_retry_once ( db , entry , line_mode , expected_lines ) :
second = draft = None
try :
with db . cursor ( cursor_factory = RealDictCursor ) as c :
c . execute ( ' SELECT id FROM customers ORDER BY id LIMIT 1 ' )
customer = c . fetchone ( ) [ ' id ' ]
c . execute ( ' UPDATE tmodule_customers SET hub_customer_id= %s WHERE id= %s ' , ( customer , entry [ ' tracking_id ' ] ) )
c . execute ( " UPDATE bmc_billing_decisions SET method= ' invoice ' ,details= %s WHERE time_id= %s " , ( Json ( { ' billing ' : { } } ) , entry [ ' time_id ' ] ) )
c . execute ( " INSERT INTO tmodule_times(customer_id,description,original_hours,worked_date,status,billing_method,faktisk_tid_min) VALUES( %s , ' Second invoice integration source ' ,1,CURRENT_DATE, ' pending ' , ' invoice ' ,60) RETURNING id,workflow_revision " , ( entry [ ' tracking_id ' ] , ) )
row = c . fetchone ( ) ; second = row [ ' id ' ]
c . execute ( " INSERT INTO bmc_billing_decisions(time_id,revision,status,method,details) VALUES( %s , %s , ' decided ' , ' invoice ' , %s ) " , ( second , row [ ' workflow_revision ' ] , Json ( { ' billing ' : { } } ) ) )
result = settlement . settle_workflow_entries ( [ entry [ ' time_id ' ] , second ] , entry [ ' actor ' ] , line_mode = line_mode )
assert len ( result [ ' created_drafts ' ] ) == 1
draft = result [ ' created_drafts ' ] [ 0 ] [ ' draft_id ' ]
with db . cursor ( ) as c :
c . execute ( ' SELECT lines_json FROM ordre_drafts WHERE id= %s ' , ( draft , ) )
lines = [ line for line in c . fetchone ( ) [ 0 ] if line . get ( ' source_type ' ) == ' timequeue ' ]
assert len ( lines ) == expected_lines
assert { time_id for line in lines for time_id in line [ ' time_entry_ids ' ] } == { entry [ ' time_id ' ] , second }
retry = settlement . settle_workflow_entries ( [ entry [ ' time_id ' ] , second ] , entry [ ' actor ' ] , line_mode = line_mode )
assert retry [ ' created_drafts ' ] == [ ]
assert set ( retry [ ' already_settled_ids ' ] ) == { entry [ ' time_id ' ] , second }
finally :
with db . cursor ( ) as c :
if second :
c . execute ( ' DELETE FROM bmc_workflow_settlements WHERE time_id=ANY( %s ) ' , ( [ entry [ ' time_id ' ] , second ] , ) )
c . execute ( ' DELETE FROM bmc_billing_decisions WHERE time_id= %s ' , ( second , ) )
c . execute ( ' DELETE FROM tmodule_times WHERE id= %s ' , ( second , ) )
if draft :
c . execute ( ' UPDATE tmodule_times SET economy_order_draft_id=NULL WHERE id= %s ' , ( entry [ ' time_id ' ] , ) )
c . execute ( ' DELETE FROM ordre_drafts WHERE id= %s ' , ( draft , ) )
def test_actual_api_override_reset_and_existing_simulation ( db , entry , worker_rule , monkeypatch ) :
from fastapi import FastAPI
from fastapi . testclient import TestClient
from app . workflows import decisions , router as workflow_api
monkeypatch . setattr ( decisions , ' active_for_customer ' , lambda _ : True )
app = FastAPI ( )
app . include_router ( decisions . router )
app . include_router ( workflow_api . router )
for dep in ( decisions . edit_access , decisions . view_access , workflow_api . manage_access , workflow_api . view_access ) :
app . dependency_overrides [ dep ] = lambda : { ' id ' : entry [ ' actor ' ] , ' is_superadmin ' : True }
client = TestClient ( app )
with db . cursor ( ) as c :
c . execute ( " UPDATE tmodule_times SET description= ' Actual API worker test ' WHERE id= %s " , ( entry [ ' time_id ' ] , ) )
override = client . post ( f " /timetracking/time/ { entry [ ' time_id ' ] } /workflow-override " , json = { ' method ' : ' prepaid ' , ' card_id ' : entry [ ' card_id ' ] , ' reason ' : ' Verified card choice ' } )
assert override . status_code == 200
assert override . json ( ) [ ' manual_override ' ] is True
preserved = client . post ( f " /timetracking/time/ { entry [ ' time_id ' ] } /workflow-recalculate " , json = { } )
assert preserved . status_code == 200
assert preserved . json ( ) [ ' preserved_override ' ] is True
reset = client . post ( f " /timetracking/time/ { entry [ ' time_id ' ] } /workflow-recalculate " , json = { ' reset_override ' : True , ' reason ' : ' Apply verified invoice rule ' } )
assert reset . status_code == 200
assert reset . json ( ) [ ' decision ' ] [ ' method ' ] == ' invoice '
assert reset . json ( ) [ ' decision ' ] [ ' manual_override ' ] is False
simulation = client . post ( f ' /workflows/ { worker_rule } /simulate ' , json = { ' entity_type ' : ' time_entry ' , ' entity_id ' : entry [ ' time_id ' ] } )
assert simulation . status_code == 200
assert simulation . json ( ) [ ' result ' ] [ ' method ' ] == ' invoice '
assert ' keyword_matches ' in simulation . json ( ) [ ' context ' ]
def test_unknown_invoice_product_rolls_back_without_draft ( db , entry ) :
from fastapi import HTTPException
with db . cursor ( ) as c :
c . execute ( ' SELECT id FROM customers ORDER BY id LIMIT 1 ' )
customer = c . fetchone ( ) [ 0 ]
c . execute ( ' UPDATE tmodule_customers SET hub_customer_id= %s WHERE id= %s ' , ( customer , entry [ ' tracking_id ' ] ) )
c . execute ( " UPDATE bmc_billing_decisions SET method= ' invoice ' ,details= %s WHERE time_id= %s " , ( Json ( { ' billing ' : { ' product_number ' : ' missing- ' + str ( uuid . uuid4 ( ) ) } } ) , entry [ ' time_id ' ] ) )
c . execute ( ' SELECT count(*) FROM ordre_drafts ' )
before = c . fetchone ( ) [ 0 ]
with pytest . raises ( HTTPException ) as exc :
settlement . settle_workflow_entries ( [ entry [ ' time_id ' ] ] , entry [ ' actor ' ] )
assert exc . value . status_code == 409
with db . cursor ( ) as c :
c . execute ( ' SELECT count(*) FROM ordre_drafts ' )
assert c . fetchone ( ) [ 0 ] == before
c . execute ( ' SELECT status,economy_order_draft_id FROM tmodule_times WHERE id= %s ' , ( entry [ ' time_id ' ] , ) )
assert c . fetchone ( ) == ( ' pending ' , None )
2026-10-09 01:21:12 +02:00
def _reviewed_queue_request ( db , entry , monkeypatch , method = ' internal ' ) :
from app . core . config import settings
from app . economy . backend import router as economy
monkeypatch . setattr ( settings , ' WORKFLOWS_ENABLED ' , False )
async def actor ( _request ) :
return { ' id ' : entry [ ' actor ' ] , ' username ' : ' reviewed-db-test ' , ' is_superadmin ' : True }
monkeypatch . setattr ( economy , ' _queue_actor ' , actor )
with db . cursor ( cursor_factory = RealDictCursor ) as cursor :
cursor . execute ( ' UPDATE tmodule_times SET billing_method= %s ,prepaid_card_id= %s ,billable= %s WHERE id= %s ' ,
( method , entry [ ' card_id ' ] if method == ' prepaid ' else None ,
method == ' prepaid ' , entry [ ' time_id ' ] ) )
cursor . execute ( ' SELECT id,updated_at,billing_method,prepaid_card_id,fixed_price_agreement_id,workflow_revision FROM tmodule_times WHERE id= %s ' , ( entry [ ' time_id ' ] , ) )
snapshot = dict ( cursor . fetchone ( ) )
snapshot [ ' updated_at ' ] = snapshot [ ' updated_at ' ] . isoformat ( )
snapshot [ ' entry_fingerprint ' ] = economy . _queue_review_fingerprint (
economy . _selected_time_entries ( [ entry [ ' time_id ' ] ] ) [ 0 ] )
return economy , economy . ReviewedTimeRequest ( ids = [ entry [ ' time_id ' ] ] , snapshots = [ snapshot ] )
def test_reviewed_queue_approval_has_no_financial_effects ( db , entry , monkeypatch ) :
import asyncio
from starlette . requests import Request
economy , payload = _reviewed_queue_request ( db , entry , monkeypatch )
with db . cursor ( ) as cursor :
cursor . execute ( ' SELECT count(*) FROM ordre_drafts ' )
drafts_before = cursor . fetchone ( ) [ 0 ]
result = asyncio . run ( economy . approve_reviewed_time_queue ( payload , Request ( { ' type ' : ' http ' , ' headers ' : [ ] } ) ) )
assert result [ ' approved ' ] == 1
with db . cursor ( ) as cursor :
cursor . execute ( ' SELECT status,approved_by,economy_settled_at,economy_order_draft_id FROM tmodule_times WHERE id= %s ' , ( entry [ ' time_id ' ] , ) )
assert cursor . fetchone ( ) == ( ' approved ' , entry [ ' actor ' ] , None , None )
cursor . execute ( ' SELECT used_hours FROM tticket_prepaid_cards WHERE id= %s ' , ( entry [ ' card_id ' ] , ) )
assert float ( cursor . fetchone ( ) [ 0 ] ) == 0
cursor . execute ( ' SELECT count(*) FROM ordre_drafts ' )
assert cursor . fetchone ( ) [ 0 ] == drafts_before
cursor . execute ( ' SELECT count(*) FROM bmc_workflow_settlements WHERE time_id= %s ' , ( entry [ ' time_id ' ] , ) )
assert cursor . fetchone ( ) [ 0 ] == 0
def test_reviewed_queue_stale_snapshot_does_not_approve ( db , entry , monkeypatch ) :
import asyncio
from fastapi import HTTPException
from starlette . requests import Request
economy , payload = _reviewed_queue_request ( db , entry , monkeypatch )
with db . cursor ( ) as cursor :
cursor . execute ( " UPDATE tmodule_times SET description= ' Changed after review ' ,updated_at=updated_at+interval ' 1 second ' WHERE id= %s " , ( entry [ ' time_id ' ] , ) )
with pytest . raises ( HTTPException ) as error :
asyncio . run ( economy . approve_reviewed_time_queue ( payload , Request ( { ' type ' : ' http ' , ' headers ' : [ ] } ) ) )
assert error . value . status_code == 409
with db . cursor ( ) as cursor :
cursor . execute ( ' SELECT status,approved_at,economy_settled_at FROM tmodule_times WHERE id= %s ' , ( entry [ ' time_id ' ] , ) )
assert cursor . fetchone ( ) == ( ' pending ' , None , None )
def test_reviewed_queue_send_rolls_back_card_debit_on_failure ( db , entry , monkeypatch ) :
import asyncio
from starlette . requests import Request
economy , payload = _reviewed_queue_request ( db , entry , monkeypatch , method = ' prepaid ' )
def fail_after_debit ( * _args , * * _kwargs ) :
raise RuntimeError ( ' Injected reviewed queue failure after card debit ' )
monkeypatch . setattr ( economy , ' execute_insert ' , fail_after_debit )
request = Request ( { ' type ' : ' http ' , ' headers ' : [ ] } )
request . state . user_id = entry [ ' actor ' ]
with pytest . raises ( RuntimeError , match = ' after card debit ' ) :
asyncio . run ( economy . send_reviewed_time_queue ( payload , request ) )
with db . cursor ( ) as cursor :
cursor . execute ( ' SELECT used_hours FROM tticket_prepaid_cards WHERE id= %s ' , ( entry [ ' card_id ' ] , ) )
assert float ( cursor . fetchone ( ) [ 0 ] ) == 0
cursor . execute ( ' SELECT status,economy_settled_at,economy_order_draft_id FROM tmodule_times WHERE id= %s ' , ( entry [ ' time_id ' ] , ) )
assert cursor . fetchone ( ) == ( ' pending ' , None , None )
cursor . execute ( ' SELECT count(*) FROM tticket_prepaid_transactions WHERE card_id= %s ' , ( entry [ ' card_id ' ] , ) )
assert cursor . fetchone ( ) [ 0 ] == 0