119 lines
7.0 KiB
Python
119 lines
7.0 KiB
Python
|
|
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)
|