import asyncio import pytest from fastapi import HTTPException import importlib r=importlib.import_module("app.timetracking.backend.router") @pytest.mark.parametrize('choice,method,card,agreement,status',[ ('draft','draft',None,None,'kladde'),('operations','operations',None,None,'kladde'),('development','development',None,None,'kladde'),('invoice','invoice',None,None,'afventer'), ('card_7','prepaid',7,None,'afventer'),('fpa_9','subscription',None,9,'afventer'), ]) def test_billing_selection_validates_and_keeps_selected_link(monkeypatch,choice,method,card,agreement,status): monkeypatch.setattr(r,'_settlement_options_for_case_entry',lambda entry:{'prepaid_cards':[{'id':7}],'agreements':[{'id':9}]}) result=r._case_billing_choice(186,choice) assert result=={'method':method,'status':status,'card_id':card,'agreement_id':agreement} @pytest.mark.parametrize('choice',['card_8','fpa_10','card_bad','unknown']) def test_invalid_or_foreign_billing_option_is_rejected(monkeypatch,choice): monkeypatch.setattr(r,'_settlement_options_for_case_entry',lambda entry:{'prepaid_cards':[],'agreements':[]}) with pytest.raises(HTTPException):r._case_billing_choice(186,choice) def test_draft_cannot_be_self_approved(monkeypatch): monkeypatch.setattr(r,'execute_query_single',lambda *args:{'id':1,'medarbejder_id':12,'billing_method':'draft'}) monkeypatch.setattr(r,'_can_view_all_team_timers',lambda user:False) with pytest.raises(HTTPException) as error: asyncio.run(r.approve_time_entry_v1(1,{}, {'id':12,'username':'test'})) assert error.value.status_code==403 def test_manual_selection_is_stored_pending_without_consuming_credit(monkeypatch): monkeypatch.setattr(r,'_resolve_case_customer_id',lambda *args:77) monkeypatch.setattr(r,'_settlement_options_for_case_entry',lambda entry:{'prepaid_cards':[{'id':7}],'agreements':[]}) captured={} def insert(sql,params): assert sql.count('%s')==len(params) assert 'prepaid_card_id, fixed_price_agreement_id' in sql captured['params']=params return [{'id':3}] monkeypatch.setattr(r,'execute_query',insert) result=asyncio.run(r.create_manual_time_v1({'sag_id':186,'faktisk_tid_min':15,'settlement_choice':'card_7'},{'id':12,'username':'test'})) assert result['id']==3 assert captured['params'][9]=='prepaid' assert captured['params'][15]=='sag_v5' assert captured['params'][16]=='afventer' assert captured['params'][-4:-2]==(7,None) def test_options_show_explicit_zero_customer_rate(monkeypatch): monkeypatch.setattr(r,'execute_query_single',lambda sql,params: {'customer_id':5} if 'sag_sager' in sql else {'standard_hourly_rate':0}) monkeypatch.setattr(r,'_settlement_options_for_case_entry',lambda entry:{'prepaid_cards':[],'agreements':[]}) assert asyncio.run(r.get_case_billing_options(186))['hourly_rate']==0 def test_live_timer_keeps_draft_choice_and_does_not_consume_credit(monkeypatch): monkeypatch.setattr(r,'_resolve_case_customer_id',lambda *args:77) monkeypatch.setattr(r,'_resolve_target_user_id',lambda *args:12) monkeypatch.setattr(r,'execute_query_single',lambda *args:None) monkeypatch.setattr(r,'_settlement_options_for_case_entry',lambda entry:{'prepaid_cards':[],'agreements':[]}) def insert(sql,params): assert sql.count('%s')==len(params) assert params[13]=='sag_v5' assert params[14]=='kladde' assert params[-4:-1]==('draft',None,None) return [{'id':3}] monkeypatch.setattr(r,'execute_query',insert) asyncio.run(r.start_live_timer_v1({'sag_id':186,'settlement_choice':'draft'},{'id':12,'username':'test'})) def test_approval_preserves_explicit_invoice_instead_of_recommending_prepaid(monkeypatch): entry={'id':3,'medarbejder_id':12,'billing_method':'invoice','kilde':'sag_v5','entry_type':'manuel','work_type':'support','fakturerbar_tid_min':30} monkeypatch.setattr(r,'execute_query_single',lambda *args:entry) monkeypatch.setattr(r,'_settlement_options_for_case_entry',lambda entry:{'recommended':{'method':'prepaid','prepaid_card_id':7},'prepaid_cards':[{'id':7}],'agreements':[]}) def update(sql,params): assert params[4]=='invoice' assert params[5] is None return [{'id':3,'status':'approved'}] monkeypatch.setattr(r,'execute_query',update) assert asyncio.run(r.approve_time_entry_v1(3,{}, {'id':12,'username':'test'}))['status']=='approved' def test_manager_can_review_draft_and_select_invoice(monkeypatch): entry={'id':3,'medarbejder_id':12,'billing_method':'draft','kilde':'sag_v5','entry_type':'manuel','work_type':'support','fakturerbar_tid_min':30} monkeypatch.setattr(r,'execute_query_single',lambda *args:entry) monkeypatch.setattr(r,'_can_view_all_team_timers',lambda user:True) monkeypatch.setattr(r,'_settlement_options_for_case_entry',lambda entry:{'recommended':{'method':'invoice'},'prepaid_cards':[],'agreements':[]}) monkeypatch.setattr(r,'execute_query',lambda *args:[{'id':3,'status':'approved'}]) assert asyncio.run(r.approve_time_entry_v1(3,{'billing_method':'invoice'}, {'id':20,'username':'chef'}))['status']=='approved' @pytest.mark.parametrize('method',['operations','development']) def test_review_categories_require_manager_and_final_choice(monkeypatch,method): entry={'id':3,'medarbejder_id':12,'billing_method':method,'kilde':'sag_v5','entry_type':'manuel','work_type':'support'} monkeypatch.setattr(r,'execute_query_single',lambda *args:entry) monkeypatch.setattr(r,'_can_view_all_team_timers',lambda user:False) with pytest.raises(HTTPException) as error: asyncio.run(r.approve_time_entry_v1(3,{'billing_method':'invoice'},{'id':12})) assert error.value.status_code==403 monkeypatch.setattr(r,'_can_view_all_team_timers',lambda user:True) with pytest.raises(HTTPException) as error: asyncio.run(r.approve_time_entry_v1(3,{}, {'id':20})) assert error.value.status_code==400 @pytest.mark.parametrize('field,value',[('prepaid_card_id',999),('fixed_price_agreement_id',999)]) def test_legacy_case_registration_rejects_other_company_before_writes(monkeypatch,field,value): monkeypatch.setattr(r,'_settlement_options_for_case_entry',lambda entry:{'prepaid_cards':[],'agreements':[]}) writes=[] monkeypatch.setattr(r,'execute_query',lambda *args:writes.append(args)) with pytest.raises(HTTPException) as error: asyncio.run(r.create_internal_time_entry({'sag_id':186,'original_hours':1,'customer_id':999,field:value},{'id':12,'username':'test'})) assert error.value.status_code in (400,409) assert writes==[] def test_case_billing_queries_use_case_company_not_entry_customer(monkeypatch): monkeypatch.setattr(r,'execute_query_single',lambda *args:{'customer_id':8}) queries=[] def read(sql,params):queries.append((sql,params));return [] monkeypatch.setattr(r,'execute_query',read) r._settlement_options_for_case_entry({'sag_id':186,'customer_id':999}) assert len(queries)==2 assert all(params==(8,) and 'customer_id = %s' in sql for sql,params in queries)