Files
Investidor-V2/tools/opportunity-workflow-http-qa.py
T
nala 01405cbf8a
tests / ci (push) Canceled after 0s
oportunidades 5
2026-10-10 14:15:28 +02:00

180 lines
16 KiB
Python

"""Canonical contracts/workflow through QA HTTP with real Fortify MFA and ClamD. Sanitized output."""
from portal_profiles_http_session import Session, actors, PRIVATE, BASE, EXPECTED, REASON, events
import json
import uuid
import hashlib
import zlib
import struct
from datetime import datetime, timezone
fixtures = json.loads((PRIVATE / 'opportunity-workflow-fixtures-qa.json').read_text())
old = json.loads((PRIVATE / 'profiles-v1-http-receipts-qa.json').read_text())
path = PRIVATE / 'opportunity-workflow-http-qa.json'
proof = {'target': EXPECTED, 'url': BASE, 'started_at': datetime.now(timezone.utc).isoformat(), 'interface_demonstrated': False, 'active_writes': 0, 'cases': []}
sessions = {role: Session(role).login() for role in ['promoter', 'colleague', 'validator', 'publisher', 'outsider', 'multiple', 'super_first']}
promoter = sessions['promoter']; validator = sessions['validator']; publisher = sessions['publisher']; superuser = sessions['super_first']
types = ['investment_project', 'real_estate', 'land_rights', 'business_sale', 'business_partnership', 'franchise', 'concession', 'financing', 'supply', 'other']
def persist():
proof['events'] = events
path.write_text(json.dumps(proof, indent=2), encoding='utf-8')
def prepare_roles():
assignments=superuser.call('GET','/perfis/gestao',inertia=True)['props']['assignments']
def available(role,access,responsibilities=()):
return any(g['user_id']==actors[role]['id'] and g['role']==access and g['revoked_at'] is None
and set(types).issubset(json.loads(g['opportunity_types'])) and set(responsibilities).issubset(json.loads(g['responsibilities'])) for g in assignments)
if available('promoter','promoter') and available('validator','validator',['validation_coordinator','technical_reviewer','legal_reviewer']) and available('publisher','publisher') and available('multiple','publisher') and available('colleague','validator',['appeal_reviewer']):
return
# Additional scope uses the normal request and independent approval, preserving earlier grants.
request = promoter.call('POST', '/perfil/promotor', {'creation_key': str(uuid.uuid4()), 'representation_type': 'company',
'organization_id': fixtures['organization_id'], 'opportunity_types': types, 'document_ids': [old['requests']['promoter']['document_id']], 'reason': REASON}, status=201)
superuser.call('POST', f"/perfis/pedidos/{request['id']}/decisao", {'version': 1, 'decision': 'approved', 'approved_types': types,
'verified_identity': True, 'verified_representation': True, 'verified_documents': True, 'reason': 'Aprovação de dados estritamente sintéticos QA; não verifica identidade real'})
for role, access, responsibilities in [('validator','validator',['validation_coordinator','technical_reviewer','legal_reviewer']), ('publisher','publisher',[]), ('multiple','publisher',[]), ('colleague','validator',['appeal_reviewer'])]:
superuser.call('POST', '/perfis/atribuicoes', {'user_id': actors[role]['id'], 'role': access, 'opportunity_types': types, 'responsibilities': responsibilities, 'reason': REASON}, status=201)
def page(s, id):
return s.call('GET', f'/minhas-oportunidades/{id}/view', inertia=True)['props']
def transition(s, id, action, status=200, **data):
p = page(s, id)
if action=='publish':
data={'confirm_publication':True,'preview_hash':p['profileContext'].get('approval',{}).get('hash','0'*64),**data}
return s.call('POST', f'/minhas-oportunidades/{id}/workflow', {'etag': p['etag'], 'action': action, 'reason': REASON, **data}, status=status)
def save(s, id, canonical, status=200):
p = page(s, id)
return s.call('PUT', f'/minhas-oportunidades/{id}/draft', {'etag': p['etag'], 'canonical': canonical, 'reason': REASON}, status=status)
def normalized(value):
if isinstance(value,str): return value.strip()
if isinstance(value,list): return [normalized(v) for v in value]
if isinstance(value,dict): return {k:normalized(v) for k,v in value.items()}
return value
def png(seed):
def chunk(t, raw):
return struct.pack('>I', len(raw)) + t + raw + struct.pack('>I', zlib.crc32(t + raw) & 0xffffffff)
pixels = b''.join(b'\x00' + bytes([20 + seed % 200, 90, 140]) * 512 for _ in range(512))
return b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', 512, 512, 8, 2, 0, 0, 0)) + chunk(b'IDAT', zlib.compress(pixels)) + chunk(b'IEND', b'')
def create(category):
draft = promoter.call('POST', '/minhas-oportunidades/drafts', {'creation_key': str(uuid.uuid4()), 'organization_id': fixtures['organization_id'],
'opportunity_category': category, 'title': 'Oportunidade sintética QA ' + category, 'canonical': {'opportunity_category': category}, 'reason': REASON}, status=201)
id = draft['id']; transition(promoter, id, 'submit', status=422)
first = page(promoter, id)
assert first['canonicalOptions']['version'] == 'opportunity-canonical-adapter-20261010-semantic1'
provinces = promoter.call('GET', '/minhas-oportunidades/territory-options?type=province')['data']
province = provinces[0]
districts = promoter.call('GET', '/minhas-oportunidades/territory-options?type=district&parent_id=' + province['id'])['data']
district = districts[0]
location = promoter.call('POST', f'/minhas-oportunidades/{id}/resources/locations', {'etag': page(promoter,id)['etag'], 'creation_key': str(uuid.uuid4()), 'reason': REASON,
'payload': {'country_code':'MZ','province_id':province['id'],'district_id':district['id'],'role':'headquarters','is_primary':True}}, status=201)
raw = png(id)
document = promoter.call('POST', f'/minhas-oportunidades/{id}/resources/documents', {'etag':page(promoter,id)['etag'], 'creation_key':str(uuid.uuid4()), 'reason':REASON,
'payload':json.dumps({'title':'Documento de ensaio sintético — sem validade real','category':'qa-synthetic','classification':'restricted','visibility':'internal'})}, multipart=raw, status=201)
resources = promoter.call('GET',f'/minhas-oportunidades/{id}/resources',inertia=True)['props']['resources']
doc = next(d for d in resources['documents'] if d['id']==document['id'])
assert doc['file']['malware_scan_status']=='clean' and doc['file']['sha256'].strip()==hashlib.sha256(raw).hexdigest()
payload = {'opportunity_category':category,'official_title':'Oportunidade sintética QA '+category, 'promoter_id':fixtures['organization_id'], 'promoter_type':'private',
'internal_contact_id':fixtures['contact_id'], 'sector_id':fixtures['sector_id'], 'subsector_id':fixtures['subsector_id'], 'economic_activity_id':fixtures['activity_id'],
'location_mode':'single','geographic_scope':'district','locations':[location['id']], 'executive_summary':('Cenário sintético exclusivamente QA, sem oferta ou decisão real. ' * 7).strip(),
'demand_statement':('Necessidade fictícia para verificar recuperação e decisões independentes. '*3).strip(),'documents':[document['id']],
'evidences':[{'title':'Comprovativo sintético','evidence_type':'qa-synthetic','document_id':document['id']}],
'publication_languages':[{'language_code':'pt-MZ','title':'Oportunidade sintética QA','summary':'Texto de demonstração QA'}], 'visibility':'internal','truth_declaration':True}
details = {'investment_project': {'opportunity_type':'greenfield','maturity_stage':'idea','investment_modalities':['equity'],'investor_profiles':['institutional'],
'products':[{'name':'Produto QA','category':'qa'}], 'assets':[{'name':'Ativo QA','asset_type':'qa'}], 'markets':[{'name':'Mercado QA','market_type':'domestic','country_code':'MZ'}],
'competitive_advantage':'Diferenciação fictícia exclusivamente para ensaio QA. '*4,'employment':{'employment_type':'permanent','direct_jobs':2},
'impact_indicators':[{'impact_type':'qa','description':'Impacto fictício QA'}],'total_investment':1000,'currency':'MZN',
'investment_breakdown':[{'label':'Rubrica QA','amount':1000,'currency':'MZN'}], 'secured_financing':[], 'funding_sought':1000,
'milestones':[{'title':'Preparação QA','due_date':'2027-01-01','status':'planned'},{'title':'Execução QA','due_date':'2027-02-01','status':'planned'}],
'licenses':[{'name':'Declaração QA de aplicabilidade','authority':'Entidade fictícia QA','status':'not_applicable'}],
'environmental_social':{'assessment_status':'qa','environmental_category':'qa'},'risks':[{'title':'Risco sintético QA','category':'qa','likelihood':1,'severity':1,'mitigation':'Medida de ensaio QA'}]},
'real_estate':{'real_estate':{'transaction_type':'sale','property_type':'warehouse','construction_status':'completed','property_condition':'good','land_area':1000,'built_area':500,'area_unit':'sqm',
'utilities':[{'type':'water','status':'existing'}], 'occupancy_status':'vacant','title_type':'qa-synthetic','registration_number':'QA-'+str(id),'encumbrances':[{'type':'none'}],
'valuation':{'value':1000,'date':'2026-10-10','method':'qa-only','appraiser':'Synthetic QA','document_id':document['id']},'price':1000,'use_license_document_id':document['id']}},
'land_rights':{'land_rights':{'operation_type':'investment_with_duat','duat_status':'application','holder_name':'Titular sintético QA','authorized_purpose':'Preparação sintética QA',
'parcels':[{'location_id':location['id'],'area':1000,'geometry':{'type':'MultiPolygon','coordinates':[[[[40,-13],[40.001,-13],[40.001,-12.999],[40,-12.999],[40,-13]]]]},'overlap_status':'qa-pending'}],
'overlaps_conflicts':[{'type':'none','status':'qa-only'}],'required_authorizations':[{'name':'Decisão externa ainda necessária','status':'pending'}]}},
}
if category in ['business_sale','business_partnership','franchise']:
transaction = {'business_sale':'full_sale','business_partnership':'partnership','franchise':'franchise'}[category]
details[category] = {'business_transaction':{'transaction_type':transaction,'legal_entity_id':fixtures['organization_id'],'legal_form':'Synthetic QA','asking_price':1000,
'valuation':{'value':1000,'date':'2026-10-10','method':'QA'},'financial_periods':[{'year':2025,'document_id':document['id']}], 'assets_liabilities':{'assets':'QA','liabilities':'QA'},
'employees':{'total':2}, 'contracts_licenses':[{'name':'QA','document_id':document['id']}],'litigation_contingencies':[], 'reason_for_transaction':'Operação fictícia para validar contratos, versões e permissões em QA. ',
'confidentiality_level':'restricted','due_diligence_status':'data_room_ready', **({'percentage_offered':100 if category=='business_sale' else 40,'governance_rights':{'voting':'QA'}} if category!='franchise' else {'franchise_terms':{'brand':'QA','territory':'QA','term':'QA'}}),
**({'partner_contribution':{'capital':1000,'calendar':'QA'}} if category=='business_partnership' else {})}}
payload.update(details.get(category,{'module_profile_version':'qa-unapproved'}))
payload=normalized(payload)
save(promoter,id,payload)
new = Session('promoter').login(); recovered = page(new,id)
assert recovered['values']['canonical']==payload and recovered['canonicalOptions']['resources']['locations'][0]['payload']['country_code']=='MZ'
proof['cases'].append({'category':category,'id':id,'draft_saved_incomplete':True,'recovered_in_new_mfa_session':True,'document_id':document['id'],'location_id':location['id'],'clamd_real_clean':True})
persist()
return id, payload, document['id']
def review(id, document):
gates = page(validator,id)['canonicalOptions']['gates']
for gate in gates:
transition(validator,id,'review_gate',gate=gate,gate_decision='approved',evidence_document_ids=[document])
transition(validator,id,'approve')
if __name__ == '__main__':
try:
prepare_roles()
# MFA session revisions may have changed when the new scope was granted.
sessions.update({r:Session(r).login() for r in ['promoter','validator','publisher','colleague','multiple']})
promoter=sessions['promoter'];validator=sessions['validator'];publisher=sessions['publisher']
nav=promoter.call('GET','/dashboard',inertia=True)['props']['profileAccess']
promoter.call('GET',nav['mine'],inertia=True);promoter.call('GET',nav['create'],inertia=True)
id,payload,doc=create('investment_project')
transition(promoter,id,'submit')
transition(validator,id,'assign_review',assignee_user_id=actors['validator']['id'])
transition(validator,id,'approve',status=422)
transition(validator,id,'request_correction',correction_items=['canonical.demand_statement'])
payload['demand_statement']+=' Informação sintética corrigida pelo Promotor QA.'
save(promoter,id,payload);transition(promoter,id,'submit');transition(validator,id,'start_review');review(id,doc)
transition(sessions['multiple'],id,'approve',status=403)
transition(publisher,id,'publish')
published=page(promoter,id)
assert published['workflowState']=='published' and published['profileContext']['publication']['visibility']=='internal'
transition(promoter,id,'request_revision');payload['official_title']+=' — alteração material';save(promoter,id,payload)
revised=page(promoter,id)
assert revised['record']['title']==published['record']['title'] and 'approval' not in revised['profileContext'] and revised['workflowState']=='needs_information'
transition(publisher,id,'publish',status=409);transition(publisher,id,'suspend')
assert page(promoter,id)['record']['status']=='suspended'
proof['positive_cycle']={'id':id,'promoter':actors['promoter']['id'],'validator':actors['validator']['id'],'publisher':actors['publisher']['id'],
'path':['create','save','new_session','recover','submit','assign_review','request_correction','correct','resubmit','review_gates','approve','publish','request_revision','material_edit','suspend'],
'publication_visibility':'internal','published_columns_preserved_during_revision':True,'approval_invalidated':True,'sla_unconfigured':True}
for category in ['real_estate','land_rights','business_sale','business_partnership','franchise','concession','financing','supply','other']:
other,p,document=create(category)
if category in ['concession','financing','supply','other']:
transition(promoter,other,'submit',status=422)
proof['cases'][-1]['submission']='blocked-unapproved-module-profile'
else:
transition(promoter,other,'submit');proof['cases'][-1]['submission']='passed'
if category=='real_estate':
transition(validator,other,'reject',status=422,reason='curto')
transition(validator,other,'reject');transition(promoter,other,'appeal')
transition(validator,other,'appeal_reopen',status=403)
# Same-organization colleague with another role is still conflicted, not an appeal instance.
transition(sessions['colleague'],other,'appeal_reopen',status=403)
proof['reasoned_rejection_and_appeal']={'id':other,'reason_persisted':True,'same_rejector_denied':True,'organization_conflict_denied':True,'independent_appeal_decision':'pending-authorized-independent-actor'}
sessions['outsider'].call('GET',f'/minhas-oportunidades/{other}/view',status=403,inertia=True)
persist()
proof['completed_at']=datetime.now(timezone.utc).isoformat();proof['result']='passed';persist()
print(json.dumps({'result':'passed','target':EXPECTED,'categories':len(proof['cases']),'positive_cycle_id':id,'promoter_qa_id':actors['promoter']['id'],'visual_pending':True,'active_writes':0}))
except Exception:
proof['result']='failed';persist();raise