110 lines
9.7 KiB
Python
110 lines
9.7 KiB
Python
"""Per-category live QA semantics, resource permissions and internal workflow. No visual claim."""
|
|
import importlib.util
|
|
import json
|
|
import uuid
|
|
import copy
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
spec=importlib.util.spec_from_file_location('opportunity_workflow_http_qa',Path(__file__).with_name('opportunity-workflow-http-qa.py'))
|
|
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
|
|
m.path=m.PRIVATE/'opportunity-semantic-http-qa.json'
|
|
proof=m.proof
|
|
authority=json.loads((m.PRIVATE/'opportunity-semantic-fixtures-qa.json').read_text())['authority_id']
|
|
|
|
def enrich(p,doc):
|
|
p['currency']='MZN';p['review_due_date']='2027-01-01'
|
|
if p['opportunity_category']=='investment_project':
|
|
p['markets'][0]['country_code']='FR'
|
|
for i,row in enumerate(p['milestones']):
|
|
row.update({'start_date':row['due_date'],'end_date':row['due_date'],'responsible_name':'Responsável declarado sintético QA','dependencies':[] if i==0 else [p['milestones'][0]['title']]})
|
|
if 'real_estate' in p:
|
|
p['real_estate'].update({'title_document_id':doc,'title_holder_name':'Titular sintético da organização QA','issuing_authority_id':authority,
|
|
'utilities':[{'type':t,'status':'existing'} for t in ['electricity','water','sanitation','telecommunications','access']]})
|
|
if 'land_rights' in p:
|
|
p['land_rights'].update({'holder_document_id':doc,'communities_affected':False,'exploitation_plan_required':False})
|
|
if 'business_transaction' in p:
|
|
bt=p['business_transaction'];bt.update({'available_financial_years':3,'entity_registration':{'tax_identifier':'QA-SYNTHETIC','registration_number':'QA-ONLY','legal_document_id':doc,'beneficial_owner_document_id':doc},
|
|
'financial_periods':[{'year':y,'currency':'MZN','revenue':500,'profit':50,'assets':300,'liabilities':100,'cash_flow':50,'document_id':doc} for y in [2023,2024,2025]],
|
|
'assets_liabilities':{'assets':'Ativos sintéticos QA','excluded_assets':'Nenhum no ensaio','liabilities':'Passivos sintéticos QA','guarantees':'Nenhuma no ensaio','reconciliation_document_id':doc},
|
|
'employees':{'total':2,'contracts':'Vínculos sintéticos','obligations':'Obrigações sintéticas','labor_liabilities':'Nenhum passivo no cenário sintético'},
|
|
'contracts_licenses':[{'name':'Licença fictícia QA','transferability':'transferable','expiry_date':'2027-12-31','document_id':doc}],
|
|
'litigation_contingencies':[{'type':'none'}]})
|
|
bt['valuation'].update({'appraiser':'Avaliador sintético QA','document_id':doc})
|
|
if 'governance_rights' in bt:bt['governance_rights'].update({'board':'Conselho sintético','reserved_matters':'Matérias reservadas sintéticas','dividends':'Dividendos sintéticos','exit':'Saída sintética','deadlock':'Mecanismo sintético de impasse'})
|
|
if 'partner_contribution' in bt:bt['partner_contribution'].update({'assets':'Ativos sintéticos','technology':'Tecnologia sintética','market':'Mercado sintético','management':'Gestão sintética'})
|
|
if 'franchise_terms' in bt:bt['franchise_terms'].update({'initial_fee':1000,'royalties':'Encargos sintéticos descritos','support':'Suporte sintético','exclusivity':'Sem exclusividade no cenário','renewal':'Renovação sintética','termination':'Rescisão sintética','brand_license_document_id':doc})
|
|
return p
|
|
|
|
def negative(id,p,path,change):
|
|
bad=copy.deepcopy(p);change(bad);m.save(m.promoter,id,bad)
|
|
response=m.transition(m.promoter,id,'submit',status=422)
|
|
assert path in response['errors'],(path,list(response.get('errors',{})))
|
|
recovered=m.page(m.Session('promoter').login(),id)
|
|
assert recovered['values']['canonical']==m.normalized(bad)
|
|
proof.setdefault('negative_fields',[]).append({'id':id,'field':path,'status':422,'incomplete_draft_recovered':True})
|
|
|
|
try:
|
|
# Normal administrator assignment; synthetic reviewer, no real institutional responsibility inferred.
|
|
assignments=m.superuser.call('GET','/perfis/gestao',inertia=True)['props']['assignments']
|
|
if not any(g['user_id']==m.actors['bootstrap']['id'] and g['role']=='validator' and g['revoked_at'] is None and 'technical_reviewer' in json.loads(g['responsibilities']) and set(m.types).issubset(json.loads(g['opportunity_types'])) for g in assignments):
|
|
m.superuser.call('POST','/perfis/atribuicoes',{'user_id':m.actors['bootstrap']['id'],'role':'validator','opportunity_types':m.types,
|
|
'responsibilities':['technical_reviewer','legal_reviewer','appeal_reviewer'],'reason':'Responsabilidades sintéticas QA para pareceres; nenhuma competência real atribuída'},status=201)
|
|
reviewer=m.Session('bootstrap').login()
|
|
for category in m.types:
|
|
id,p,doc=m.create(category)
|
|
case=proof['cases'][-1]
|
|
if category in ['concession','financing','supply','other']:
|
|
m.transition(m.promoter,id,'submit',status=422);case.update({'editing':'saved/recovered','submission':'blocked-unapproved-module-profile','review':'not-eligible','publication':'blocked'})
|
|
m.persist();continue
|
|
p=enrich(p,doc)
|
|
if category=='investment_project':
|
|
negative(id,p,'canonical.milestones.0.end_date',lambda q:q['milestones'][0].update({'end_date':'2026-01-01'}))
|
|
negative(id,p,'canonical.milestones',lambda q:q['milestones'][0].update({'dependencies':[q['milestones'][1]['title']]}))
|
|
negative(id,p,'canonical.financial_indicators',lambda q:q.update({'maturity_stage':'feasibility'}))
|
|
p.update({'maturity_stage':'operational','financial_indicators':{'npv':-50,'irr':5,'payback_years':5,'scenario':'Cenário sintético negativo; sem score de sucesso','horizon_years':10,'currency':'MZN','discount_rate':6,'model_document_id':doc}})
|
|
p['evidences'].append({'title':'Estudo sintético sem validade real','evidence_type':'feasibility_study','document_id':doc})
|
|
elif category=='real_estate':
|
|
negative(id,p,'canonical.real_estate.utilities',lambda q:q['real_estate'].update({'utilities':[{'type':'water','status':'existing'}]}))
|
|
negative(id,p,'canonical.real_estate.title_document_id',lambda q:q['real_estate'].update({'title_document_id':str(uuid.uuid4())}))
|
|
elif category=='land_rights':
|
|
negative(id,p,'canonical.land_rights.community_consultation',lambda q:q['land_rights'].update({'communities_affected':True}))
|
|
else:
|
|
negative(id,p,'canonical.business_transaction.financial_periods',lambda q:q['business_transaction'].update({'financial_periods':q['business_transaction']['financial_periods'][:1]}))
|
|
negative(id,p,'canonical.business_transaction.litigation_contingencies',lambda q:q['business_transaction'].update({'litigation_contingencies':[]}))
|
|
m.save(m.promoter,id,p)
|
|
recovered=m.page(m.Session('promoter').login(),id)
|
|
assert recovered['values']['canonical']==m.normalized(p)
|
|
location=recovered['canonicalOptions']['resources']['locations'][0]['payload']
|
|
geo=m.promoter.call('GET','/minhas-oportunidades/territory-geometries?territory_id='+location['district_id'])
|
|
case['territorial_consumer']={'country':location['country_code'],'district_id':location['district_id'],'selected_geometry_available':geo['selected_geometry_available'],'areas_returned':len(geo['areas']),'rendered':False}
|
|
for s in [m.promoter,m.validator,m.publisher]:
|
|
raw=s.call('GET',f'/minhas-oportunidades/{id}/resources/documents/{doc}/download',binary=True)
|
|
assert isinstance(raw,bytes) and raw.startswith(b'\x89PNG')
|
|
m.sessions['outsider'].call('GET',f'/minhas-oportunidades/{id}/resources/documents/{doc}/download',status=403)
|
|
m.transition(m.promoter,id,'submit');m.transition(m.validator,id,'assign_review',assignee_user_id=m.actors['validator']['id'])
|
|
if category=='investment_project':
|
|
m.transition(m.validator,id,'pending_opinion',gate='technical_review',recipient_user_id=m.actors['bootstrap']['id'])
|
|
context=m.page(m.validator,id)['profileContext'];opinion=next(iter(context['opinions']))
|
|
assigned=reviewer.call('GET','/minhas-oportunidades?queue=assigned',inertia=True)['props']['records']['data']
|
|
assert any(r['id']==id for r in assigned)
|
|
m.transition(m.validator,id,'approve',status=422)
|
|
m.transition(m.validator,id,'respond_opinion',status=403,opinion_id=opinion,gate_decision='approved',evidence_document_ids=[doc])
|
|
m.transition(reviewer,id,'respond_opinion',opinion_id=opinion,gate_decision='approved',evidence_document_ids=[doc])
|
|
assert 'reviews' not in m.page(m.validator,id)['profileContext']
|
|
case['opinion']={'recipient_id':m.actors['bootstrap']['id'],'response_saved':True,'approval_while_pending':422,'wrong_recipient':403,'gate_not_automatically_approved':True}
|
|
m.review(id,doc)
|
|
if category=='land_rights':
|
|
m.transition(m.publisher,id,'publish',status=422);case['publication']='blocked-pending-external-authorization'
|
|
else:
|
|
m.transition(m.publisher,id,'publish');case['publication']='published-internal'
|
|
final=m.page(m.Session('promoter').login(),id)
|
|
case.update({'editing':'saved/recovered','submission':'passed','review':'gates-approved','working_state':final['workflowState'],'document_recovered_allowed_profiles':[21,24,25],'foreign_organization_document_denied':403})
|
|
m.persist()
|
|
proof['result']='passed';proof['completed_at']=datetime.now(timezone.utc).isoformat()
|
|
print(json.dumps({'result':'passed','target':m.EXPECTED,'categories':len(proof['cases']),'negative_fields':len(proof['negative_fields']),'visual_pending':True,'active_writes':0}))
|
|
except Exception:
|
|
proof['result']='failed';raise
|
|
finally:
|
|
m.persist()
|